diff --git a/constant/proxy.go b/constant/proxy.go index fa4d25c6..5459f290 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -16,6 +16,7 @@ const ( TypeNaive = "naive" TypeWireGuard = "wireguard" TypeHysteria = "hysteria" + TypeTor = "tor" ) const ( diff --git a/docs/changelog.md b/docs/changelog.md index c7b82372..2c629617 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,7 @@ +#### 2022/08/21 + +* Add [Tor outbound](/configuration/outbound/tor) + #### 2022/08/20 * Attempt to unwrap ip-in-fqdn socksaddr diff --git a/docs/configuration/outbound/index.md b/docs/configuration/outbound/index.md index 6c74c047..53e8165c 100644 --- a/docs/configuration/outbound/index.md +++ b/docs/configuration/outbound/index.md @@ -24,6 +24,7 @@ | `trojan` | [Trojan](./trojan) | | `wireguard` | [Wireguard](./wireguard) | | `hysteria` | [Hysteria](./hysteria) | +| `tor` | [Tor](./tor) | | `dns` | [DNS](./dns) | | `selector` | [Selector](./selector) | diff --git a/docs/configuration/outbound/tor.md b/docs/configuration/outbound/tor.md new file mode 100644 index 00000000..a899fbd6 --- /dev/null +++ b/docs/configuration/outbound/tor.md @@ -0,0 +1,108 @@ +### Structure + +```json +{ + "outbounds": [ + { + "type": "tor", + "tag": "tor-out", + + "executable_path": "/usr/bin/tor", + "extra_args": [], + "data_directory": "$HOME/.cache/tor", + "torrc": { + "ClientOnly": 1 + }, + + "detour": "upstream-out", + "bind_interface": "en0", + "routing_mark": 1234, + "reuse_addr": false, + "connect_timeout": "5s", + "tcp_fast_open": false, + "domain_strategy": "prefer_ipv6", + "fallback_delay": "300ms" + } + ] +} +``` + +!!! info "" + + Embedded tor is not included by default, see [Installation](/#Installation). + +### Tor Fields + +#### executable_path + +The path to the Tor executable. + +Embedded Tor will be ignored if set. + +#### extra_args + +List of extra arguments passed to the Tor instance when started. + +#### data_directory + +==Recommended== + +The data directory of Tor. + +Each start will be very slow if not specified. + +#### torrc + +Map of torrc options. + +See [tor(1)](https://linux.die.net/man/1/tor) + +### Dial Fields + +#### detour + +The tag of the upstream outbound. + +Other dial fields will be ignored when enabled. + +#### bind_interface + +The network interface to bind to. + +#### routing_mark + +!!! error "" + + Linux only + +The iptables routing mark. + +#### reuse_addr + +Reuse listener address. + +#### connect_timeout + +Connect timeout, in golang's Duration format. + +A duration string is a possibly signed sequence of +decimal numbers, each with optional fraction and a unit suffix, +such as "300ms", "-1.5h" or "2h45m". +Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + +#### domain_strategy + +One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. + +If set, the server domain name will be resolved to IP before connecting. + +`dns.strategy` will be used if empty. + +#### fallback_delay + +The length of time to wait before spawning a RFC 6555 Fast Fallback connection. +That is, is the amount of time to wait for IPv6 to succeed before assuming +that IPv6 is misconfigured and falling back to IPv4 if `prefer_ipv4` is set. +If zero, a default delay of 300ms is used. + +Only take effect when `domain_strategy` is `prefer_ipv4` or `prefer_ipv6`. \ No newline at end of file diff --git a/docs/features.md b/docs/features.md index a53b7837..21226c97 100644 --- a/docs/features.md +++ b/docs/features.md @@ -25,7 +25,7 @@ | Shadowsocks AEAD 2022 outbound | X | X | | Shadowsocks UDP over TCP | X | X | | Multiplex (smux/yamux) | mux.cool | X | -| WireGuard/Hysteria outbound | X | X | +| Tor/WireGuard/Hysteria outbound | X | X | | Selector outbound and Clash API | X | ✔ | #### Sniffing diff --git a/docs/index.md b/docs/index.md index 72b4e7ab..b16dd490 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,14 +18,15 @@ Install with options: go install -v -tags with_clash_api github.com/sagernet/sing-box/cmd/sing-box@latest ``` -| Build Tag | Description | -|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `with_quic` | Build with QUIC support, see [QUIC and HTTP3 dns transports](./configuration/dns/server), [Naive inbound](./configuration/inbound/naive), [Hysteria Inbound](./configuration/inbound/hysteria) and [Hysteria Outbound](./configuration/outbound/hysteria). | -| `with_wireguard` | Build with WireGuard support, see [WireGuard outbound](./configuration/outbound/wireguard). | -| `with_acme` | Build with ACME TLS certificate issuer support, see [TLS](./configuration/shared/tls). | -| `with_clash_api` | Build with Clash api support, see [Experimental](./configuration/experimental#clash-api-fields). | -| `no_gvisor` | Build without gVisor tun stack support, see [Tun inbound](./configuration/inbound/tun#stack). | -| `with_lwip` (CGO required) | Build with LWIP tun stack support, see [Tun inbound](./configuration/inbound/tun#stack). | +| Build Tag | Description | +|------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `with_quic` | Build with QUIC support, see [QUIC and HTTP3 dns transports](./configuration/dns/server), [Naive inbound](./configuration/inbound/naive), [Hysteria Inbound](./configuration/inbound/hysteria) and [Hysteria Outbound](./configuration/outbound/hysteria). | +| `with_wireguard` | Build with WireGuard support, see [WireGuard outbound](./configuration/outbound/wireguard). | +| `with_acme` | Build with ACME TLS certificate issuer support, see [TLS](./configuration/shared/tls). | +| `with_clash_api` | Build with Clash api support, see [Experimental](./configuration/experimental#clash-api-fields). | +| `no_gvisor` | Build without gVisor tun stack support, see [Tun inbound](./configuration/inbound/tun#stack). | +| `with_embedded_tor` (CGO required) | Build with embedded Tor support, see [Tor outbound](./configuration/outbound/tor). | +| `with_lwip` (CGO required) | Build with LWIP tun stack support, see [Tun inbound](./configuration/inbound/tun#stack). | The binary is built under $GOPATH/bin diff --git a/go.mod b/go.mod index 5d14d8ca..39963d7a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/sagernet/sing-box go 1.18 require ( + berty.tech/go-libtor v1.0.385 + github.com/cretz/bine v0.2.0 github.com/database64128/tfo-go v1.1.1 github.com/dustin/go-humanize v1.0.0 github.com/fsnotify/fsnotify v1.5.4 diff --git a/go.sum b/go.sum index 1ed840a9..7e51d178 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,13 @@ +berty.tech/go-libtor v1.0.385 h1:RWK94C3hZj6Z2GdvePpHJLnWYobFr3bY/OdUJ5aoEXw= +berty.tech/go-libtor v1.0.385/go.mod h1:9swOOQVb+kmvuAlsgWUK/4c52pm69AdbJsxLzk+fJEw= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= +github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= +github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= github.com/database64128/tfo-go v1.1.1 h1:jcaCQBkEZZxV1t2wfOwt41WJKzgcNtLV7nGOm+hmZ3w= github.com/database64128/tfo-go v1.1.1/go.mod h1:b1wrRNZr7NKZhWQ8LSTvqo1r2ppLdYXZLIUDCPOgJrI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -135,8 +140,10 @@ go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.22.0 h1:Zcye5DUgBloQ9BaT4qc9BnjOFog5TvBSAGkJ3Nf70c0= go.uber.org/zap v1.22.0/go.mod h1:H4siCOZOrAolnUPJEkfaSjDqyP+BDS0DdDWzwcgt3+U= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 h1:GIAS/yBem/gq2MUqgNIzUHW7cJMmx3TGZOrnyYaNQ6c= golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= @@ -153,7 +160,9 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E= golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= @@ -163,6 +172,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -174,6 +184,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -187,6 +198,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= diff --git a/mkdocs.yml b/mkdocs.yml index ac03f141..2d6e454f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -67,6 +67,7 @@ nav: - Trojan: configuration/outbound/trojan.md - WireGuard: configuration/outbound/wireguard.md - Hysteria: configuration/outbound/hysteria.md + - Tor: configuration/outbound/tor.md - DNS: configuration/outbound/dns.md - Selector: configuration/outbound/selector.md - Route: diff --git a/option/outbound.go b/option/outbound.go index f1f84409..412890e5 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -17,7 +17,8 @@ type _Outbound struct { VMessOptions VMessOutboundOptions `json:"-"` TrojanOptions TrojanOutboundOptions `json:"-"` WireGuardOptions WireGuardOutboundOptions `json:"-"` - HysteriaOutbound HysteriaOutboundOptions `json:"-"` + HysteriaOptions HysteriaOutboundOptions `json:"-"` + TorOptions TorOutboundOptions `json:"-"` SelectorOptions SelectorOutboundOptions `json:"-"` } @@ -43,7 +44,9 @@ func (h Outbound) MarshalJSON() ([]byte, error) { case C.TypeWireGuard: v = h.WireGuardOptions case C.TypeHysteria: - v = h.HysteriaOutbound + v = h.HysteriaOptions + case C.TypeTor: + v = h.TorOptions case C.TypeSelector: v = h.SelectorOptions default: @@ -76,7 +79,9 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error { case C.TypeWireGuard: v = &h.WireGuardOptions case C.TypeHysteria: - v = &h.HysteriaOutbound + v = &h.HysteriaOptions + case C.TypeTor: + v = &h.TorOptions case C.TypeSelector: v = &h.SelectorOptions default: diff --git a/option/tor.go b/option/tor.go new file mode 100644 index 00000000..a4842fd4 --- /dev/null +++ b/option/tor.go @@ -0,0 +1,9 @@ +package option + +type TorOutboundOptions struct { + OutboundDialerOptions + ExecutablePath string `json:"executable_path,omitempty"` + ExtraArgs []string `json:"extra_args,omitempty"` + DataDirectory string `json:"data_directory,omitempty"` + Options map[string]string `json:"torrc,omitempty"` +} diff --git a/outbound/builder.go b/outbound/builder.go index 97735dae..0f9afca9 100644 --- a/outbound/builder.go +++ b/outbound/builder.go @@ -34,7 +34,9 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o case C.TypeWireGuard: return NewWireGuard(ctx, router, logger, options.Tag, options.WireGuardOptions) case C.TypeHysteria: - return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOutbound) + return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions) + case C.TypeTor: + return NewTor(ctx, router, logger, options.Tag, options.TorOptions) case C.TypeSelector: return NewSelector(router, logger, options.Tag, options.SelectorOptions) default: diff --git a/outbound/proxy.go b/outbound/proxy.go new file mode 100644 index 00000000..3b845dd9 --- /dev/null +++ b/outbound/proxy.go @@ -0,0 +1,131 @@ +package outbound + +import ( + std_bufio "bufio" + "context" + "encoding/hex" + "math/rand" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/protocol/http" + "github.com/sagernet/sing/protocol/socks" + "github.com/sagernet/sing/protocol/socks/socks4" + "github.com/sagernet/sing/protocol/socks/socks5" +) + +type ProxyListener struct { + ctx context.Context + logger log.ContextLogger + dialer N.Dialer + tcpListener *net.TCPListener + username string + password string + authenticator auth.Authenticator +} + +func NewProxyListener(ctx context.Context, logger log.ContextLogger, dialer N.Dialer) *ProxyListener { + var usernameB [64]byte + var passwordB [64]byte + rand.Read(usernameB[:]) + rand.Read(passwordB[:]) + username := hex.EncodeToString(usernameB[:]) + password := hex.EncodeToString(passwordB[:]) + return &ProxyListener{ + ctx: ctx, + logger: logger, + dialer: dialer, + authenticator: auth.NewAuthenticator([]auth.User{{Username: username, Password: password}}), + username: username, + password: password, + } +} + +func (l *ProxyListener) Start() error { + tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{ + IP: net.IPv4(127, 0, 0, 1), + }) + if err != nil { + return err + } + l.tcpListener = tcpListener + go l.acceptLoop() + return nil +} + +func (l *ProxyListener) Port() uint16 { + if l.tcpListener == nil { + panic("start listener first") + } + return M.SocksaddrFromNet(l.tcpListener.Addr()).Port +} + +func (l *ProxyListener) Username() string { + return l.username +} + +func (l *ProxyListener) Password() string { + return l.password +} + +func (l *ProxyListener) Close() error { + return common.Close(l.tcpListener) +} + +func (l *ProxyListener) acceptLoop() { + for { + tcpConn, err := l.tcpListener.AcceptTCP() + if err != nil { + return + } + ctx := log.ContextWithNewID(l.ctx) + go func() { + hErr := l.accept(ctx, tcpConn) + if hErr != nil { + if E.IsClosedOrCanceled(hErr) { + l.logger.DebugContext(ctx, E.Cause(hErr, "proxy connection closed")) + return + } + l.logger.ErrorContext(ctx, E.Cause(hErr, "proxy")) + } + }() + } +} + +func (l *ProxyListener) accept(ctx context.Context, conn *net.TCPConn) error { + headerType, err := rw.ReadByte(conn) + if err != nil { + return err + } + switch headerType { + case socks4.Version, socks5.Version: + return socks.HandleConnection0(ctx, conn, headerType, l.authenticator, l, M.Metadata{}) + } + reader := std_bufio.NewReader(bufio.NewCachedReader(conn, buf.As([]byte{headerType}))) + return http.HandleConnection(ctx, conn, reader, l.authenticator, l, M.Metadata{}) +} + +func (l *ProxyListener) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata M.Metadata) error { + var metadata adapter.InboundContext + metadata.Network = N.NetworkTCP + metadata.Destination = upstreamMetadata.Destination + l.logger.InfoContext(ctx, "proxy connection to ", metadata.Destination) + return NewConnection(ctx, l.dialer, conn, metadata) +} + +func (l *ProxyListener) NewPacketConnection(ctx context.Context, conn N.PacketConn, upstreamMetadata M.Metadata) error { + var metadata adapter.InboundContext + metadata.Network = N.NetworkUDP + metadata.Destination = upstreamMetadata.Destination + l.logger.InfoContext(ctx, "proxy packet connection to ", metadata.Destination) + return NewPacketConnection(ctx, l.dialer, conn, metadata) +} diff --git a/outbound/tor.go b/outbound/tor.go new file mode 100644 index 00000000..105bfd83 --- /dev/null +++ b/outbound/tor.go @@ -0,0 +1,203 @@ +package outbound + +import ( + "context" + "net" + "os" + "path/filepath" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/protocol/socks" + + "github.com/cretz/bine/control" + "github.com/cretz/bine/tor" +) + +var _ adapter.Outbound = (*Tor)(nil) + +type Tor struct { + myOutboundAdapter + ctx context.Context + proxy *ProxyListener + startConf *tor.StartConf + options map[string]string + events chan control.Event + instance *tor.Tor + socksClient *socks.Client +} + +func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TorOutboundOptions) (*Tor, error) { + startConf := newConfig() + startConf.DataDir = os.ExpandEnv(options.DataDirectory) + startConf.TempDataDirBase = os.TempDir() + if options.ExecutablePath != "" { + startConf.ExePath = options.ExecutablePath + startConf.ExtraArgs = options.ExtraArgs + startConf.ProcessCreator = nil + startConf.UseEmbeddedControlConn = false + } + if startConf.DataDir != "" { + torrcFile := filepath.Join(startConf.DataDir, "torrc") + if !rw.FileExists(torrcFile) { + err := rw.WriteFile(torrcFile, []byte("")) + if err != nil { + return nil, err + } + } + startConf.TorrcFile = torrcFile + } + return &Tor{ + myOutboundAdapter: myOutboundAdapter{ + protocol: C.TypeTor, + network: []string{N.NetworkTCP}, + router: router, + logger: logger, + tag: tag, + }, + ctx: ctx, + proxy: NewProxyListener(ctx, logger, dialer.NewOutbound(router, options.OutboundDialerOptions)), + startConf: &startConf, + options: options.Options, + }, nil +} + +func (t *Tor) Start() error { + err := t.start() + if err != nil { + t.Close() + } + return err +} + +var torLogEvents = []control.EventCode{ + control.EventCodeLogDebug, + control.EventCodeLogErr, + control.EventCodeLogInfo, + control.EventCodeLogNotice, + control.EventCodeLogWarn, +} + +func (t *Tor) start() error { + torInstance, err := tor.Start(t.ctx, t.startConf) + if err != nil { + return E.New(strings.ToLower(err.Error())) + } + t.instance = torInstance + t.events = make(chan control.Event, 8) + err = torInstance.Control.AddEventListener(t.events, torLogEvents...) + if err != nil { + return err + } + go t.recvLoop() + err = t.proxy.Start() + if err != nil { + return err + } + proxyPort := "127.0.0.1:" + F.ToString(t.proxy.Port()) + proxyUsername := t.proxy.Username() + proxyPassword := t.proxy.Password() + t.logger.Trace("created upstream proxy at ", proxyPort) + t.logger.Trace("upstream proxy username ", proxyUsername) + t.logger.Trace("upstream proxy password ", proxyPassword) + confOptions := []*control.KeyVal{ + control.NewKeyVal("Socks5Proxy", proxyPort), + control.NewKeyVal("Socks5ProxyUsername", proxyUsername), + control.NewKeyVal("Socks5ProxyPassword", proxyPassword), + } + err = torInstance.Control.ResetConf(confOptions...) + if err != nil { + return err + } + if len(t.options) > 0 { + for key, value := range t.options { + switch key { + case "Socks5Proxy", + "Socks5ProxyUsername", + "Socks5ProxyPassword": + continue + } + err = torInstance.Control.SetConf(control.NewKeyVal(key, value)) + if err != nil { + return E.Cause(err, "set ", key, "=", value) + } + } + } + err = torInstance.EnableNetwork(t.ctx, true) + if err != nil { + return err + } + info, err := torInstance.Control.GetInfo("net/listeners/socks") + if err != nil { + return err + } + if len(info) != 1 || info[0].Key != "net/listeners/socks" { + return E.New("get socks proxy address") + } + t.logger.Trace("obtained tor socks5 address ", info[0].Val) + // TODO: set password for tor socks5 server if supported + t.socksClient = socks.NewClient(N.SystemDialer, M.ParseSocksaddr(info[0].Val), socks.Version5, "", "") + return nil +} + +func (t *Tor) recvLoop() { + for rawEvent := range t.events { + switch event := rawEvent.(type) { + case *control.LogEvent: + event.Raw = strings.ToLower(event.Raw) + switch event.Severity { + case control.EventCodeLogDebug, control.EventCodeLogInfo: + t.logger.Trace(event.Raw) + case control.EventCodeLogNotice: + if strings.Contains(event.Raw, "disablenetwork") || strings.Contains(event.Raw, "socks listener") { + t.logger.Trace(event.Raw) + continue + } + t.logger.Info(event.Raw) + case control.EventCodeLogWarn: + t.logger.Warn(event.Raw) + case control.EventCodeLogErr: + t.logger.Error(event.Raw) + } + } + } +} + +func (t *Tor) Close() error { + err := common.Close( + common.PtrOrNil(t.proxy), + common.PtrOrNil(t.instance), + ) + if t.events != nil { + close(t.events) + t.events = nil + } + return err +} + +func (t *Tor) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + t.logger.InfoContext(ctx, "outbound connection to ", destination) + return t.socksClient.DialContext(ctx, network, destination) +} + +func (t *Tor) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, os.ErrInvalid +} + +func (t *Tor) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + return NewConnection(ctx, t, conn, metadata) +} + +func (t *Tor) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + return os.ErrInvalid +} diff --git a/outbound/tor_embed.go b/outbound/tor_embed.go new file mode 100644 index 00000000..a91e06ee --- /dev/null +++ b/outbound/tor_embed.go @@ -0,0 +1,15 @@ +//go:build with_embedded_tor + +package outbound + +import ( + "berty.tech/go-libtor" + "github.com/cretz/bine/tor" +) + +func newConfig() tor.StartConf { + return tor.StartConf{ + ProcessCreator: libtor.Creator, + UseEmbeddedControlConn: true, + } +} diff --git a/outbound/tor_external.go b/outbound/tor_external.go new file mode 100644 index 00000000..6bce95d1 --- /dev/null +++ b/outbound/tor_external.go @@ -0,0 +1,9 @@ +//go:build !with_embedded_tor + +package outbound + +import "github.com/cretz/bine/tor" + +func newConfig() tor.StartConf { + return tor.StartConf{} +} diff --git a/route/rule.go b/route/rule.go index 1d6d7d98..35d7d24f 100644 --- a/route/rule.go +++ b/route/rule.go @@ -267,7 +267,11 @@ func (r *DefaultRule) Outbound() string { } func (r *DefaultRule) String() string { - return strings.Join(F.MapToString(r.allItems), " ") + if !r.invert { + return strings.Join(F.MapToString(r.allItems), " ") + } else { + return "!(" + strings.Join(F.MapToString(r.allItems), " ") + ")" + } } var _ adapter.Rule = (*LogicalRule)(nil) diff --git a/test/hysteria_test.go b/test/hysteria_test.go index 057d12ca..9b0f299c 100644 --- a/test/hysteria_test.go +++ b/test/hysteria_test.go @@ -55,7 +55,7 @@ func TestHysteriaSelf(t *testing.T) { { Type: C.TypeHysteria, Tag: "hy-out", - HysteriaOutbound: option.HysteriaOutboundOptions{ + HysteriaOptions: option.HysteriaOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -159,7 +159,7 @@ func TestHysteriaOutbound(t *testing.T) { Outbounds: []option.Outbound{ { Type: C.TypeHysteria, - HysteriaOutbound: option.HysteriaOutboundOptions{ + HysteriaOptions: option.HysteriaOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort,