Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tor analyzer (phase 1) #140

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions analyzer/tcp/tor.go
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ var analyzers = []analyzer.Analyzer{
&tcp.SocksAnalyzer{},
&tcp.SSHAnalyzer{},
&tcp.TLSAnalyzer{},
&tcp.TorAnalyzer{},
&tcp.TrojanAnalyzer{},
&udp.DNSAnalyzer{},
&udp.OpenVPNAnalyzer{},
Expand Down
9 changes: 9 additions & 0 deletions ruleset/builtins/tor/interface.go
Original file line number Diff line number Diff line change
@@ -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
}
111 changes: 111 additions & 0 deletions ruleset/builtins/tor/onionoo.go
Original file line number Diff line number Diff line change
@@ -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
}
15 changes: 15 additions & 0 deletions ruleset/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand Down
Loading