Kaynağa Gözat

Add tor outbound

世界 3 yıl önce
ebeveyn
işleme
e4cece6095

+ 1 - 0
constant/proxy.go

@@ -16,6 +16,7 @@ const (
 	TypeNaive       = "naive"
 	TypeWireGuard   = "wireguard"
 	TypeHysteria    = "hysteria"
+	TypeTor         = "tor"
 )
 
 const (

+ 4 - 0
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

+ 1 - 0
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)       |
 

+ 108 - 0
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`.

+ 1 - 1
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

+ 9 - 8
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
 

+ 2 - 0
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

+ 12 - 0
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=

+ 1 - 0
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:

+ 8 - 3
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:

+ 9 - 0
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"`
+}

+ 3 - 1
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:

+ 131 - 0
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)
+}

+ 203 - 0
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
+}

+ 15 - 0
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,
+	}
+}

+ 9 - 0
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{}
+}

+ 5 - 1
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)

+ 2 - 2
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,