feat: tor analyzer (phase 1)

This commit is contained in:
eddc005 2024-06-12 23:52:41 +01:00
parent 1de95ed53e
commit cd9ffba37c
5 changed files with 197 additions and 0 deletions

61
analyzer/tcp/tor.go Normal file
View File

@ -0,0 +1,61 @@
package tcp
import (
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/ruleset/builtins/tor"
)
var _ analyzer.TCPAnalyzer = (*TorAnalyzer)(nil)
type TorAnalyzer struct{
directory tor.TorDirectory
}
func (a *TorAnalyzer) Init() error {
var err error
a.directory, err = tor.GetOnionooDirectory()
return err
}
func (a *TorAnalyzer) Name() string {
return "tor"
}
// For now only TCP metadata is needed
func (a *TorAnalyzer) Limit() int {
return 1
}
func (a *TorAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
isRelay := a.directory.Query(info.DstIP, info.DstPort)
return newTorStream(logger, isRelay)
}
type torStream struct {
logger analyzer.Logger
isRelay bool // Public relay identifier
}
func newTorStream(logger analyzer.Logger, isRelay bool) *torStream {
return &torStream{logger: logger, isRelay: isRelay}
}
func (s *torStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyzer.PropUpdate, done bool) {
if skip != 0 {
return nil, true
}
if len(data) == 0 {
return nil, false
}
return &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: analyzer.PropMap{
"relay": s.isRelay,
},
}, true
}
func (s *torStream) Close(limited bool) *analyzer.PropUpdate {
return nil
}

View File

@ -93,6 +93,7 @@ var analyzers = []analyzer.Analyzer{
&tcp.SocksAnalyzer{},
&tcp.SSHAnalyzer{},
&tcp.TLSAnalyzer{},
&tcp.TorAnalyzer{},
&tcp.TrojanAnalyzer{},
&udp.DNSAnalyzer{},
&udp.OpenVPNAnalyzer{},

View File

@ -0,0 +1,9 @@
package tor
import "net"
type TorDirectory interface {
Init() error
Add(ip net.IP, port uint16)
Query(ip net.IP, port uint16) bool
}

View File

@ -0,0 +1,111 @@
package tor
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strconv"
"sync"
)
const (
onionooUrl = "https://onionoo.torproject.org/details"
)
var _ TorDirectory = (*OnionooDirectory)(nil)
// Singleton instance
var onionooInstance *OnionooDirectory
var once sync.Once
func GetOnionooDirectory() (*OnionooDirectory, error) {
var err error
// Singleton initialization
once.Do(func() {
onionooInstance = &OnionooDirectory{
directory: make(map[string]struct{}),
}
err = onionooInstance.Init()
})
return onionooInstance, err
}
type OnionooDirectory struct {
directory map[string]struct{}
sync.RWMutex
}
// example detail entry
// {..., "or_addresses":["195.15.242.99:9001","[2001:1600:10:100::201]:9001"], ...}
type OnionooDetail struct {
OrAddresses []string `json:"or_addresses"`
}
type OnionooResponse struct {
Relays []OnionooDetail `json:"relays"`
}
func (d *OnionooDirectory) Init() error {
response, err := d.downloadDirectory(onionooUrl)
if err != nil {
return err
}
for _, relay := range response.Relays {
for _, address := range relay.OrAddresses {
ipStr, portStr, err := net.SplitHostPort(address)
if err != nil {
continue
}
ip := net.ParseIP(ipStr)
port, err := strconv.ParseUint(portStr, 10, 16)
if ip != nil && err == nil {
d.Add(ip, uint16(port))
}
}
}
// TODO: log number of entries loaded
return nil
}
func (d *OnionooDirectory) Add(ip net.IP, port uint16) {
d.Lock()
defer d.Unlock()
addr := net.JoinHostPort(ip.String(), strconv.FormatUint(uint64(port), 10))
d.directory[addr] = struct{}{}
}
func (d *OnionooDirectory) Query(ip net.IP, port uint16) bool {
d.RLock()
defer d.RUnlock()
addr := net.JoinHostPort(ip.String(), strconv.FormatUint(uint64(port), 10))
_, exists := d.directory[addr]
return exists
}
func (d *OnionooDirectory) downloadDirectory(url string) (*OnionooResponse, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch onionoo data: status code %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var onionooResponse OnionooResponse
err = json.Unmarshal(body, &onionooResponse)
if err != nil {
return nil, fmt.Errorf("failed to parse onionoo json response: %s", err)
}
return &onionooResponse, nil
}

View File

@ -18,6 +18,7 @@ import (
"gopkg.in/yaml.v3"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/tcp"
"github.com/apernet/OpenGFW/modifier"
"github.com/apernet/OpenGFW/ruleset/builtins"
)
@ -153,6 +154,9 @@ func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier
} else if a, ok := fullAnMap[name]; ok {
// Analyzer, add to dependency map
depAnMap[name] = a
if err:= analyzersInit(a); err != nil {
return nil, err
}
}
}
cr := compiledExprRule{
@ -242,6 +246,17 @@ func analyzersToMap(ans []analyzer.Analyzer) map[string]analyzer.Analyzer {
return anMap
}
// analyzersInit invokes custom analyzer init logics
func analyzersInit(a analyzer.Analyzer) error {
switch impl := a.(type) {
case *tcp.TorAnalyzer:
if err := impl.Init(); err != nil {
return err
}
}
return nil
}
// modifiersToMap converts a list of modifiers to a map of name -> modifier.
// This is for easier lookup when compiling rules.
func modifiersToMap(mods []modifier.Modifier) map[string]modifier.Modifier {