Browse Source

Update UoT protocol

世界 2 years ago
parent
commit
43f31b40ba

+ 4 - 2
docs/configuration/outbound/shadowsocks.md

@@ -12,7 +12,7 @@
   "plugin": "",
   "plugin_opts": "",
   "network": "udp",
-  "udp_over_tcp": false,
+  "udp_over_tcp": false | {},
   "multiplex": {},
 
   ... // Dial Fields
@@ -87,7 +87,9 @@ Both is enabled by default.
 
 #### udp_over_tcp
 
-Enable the UDP over TCP protocol.
+UDP over TCP configuration.
+
+See [UDP Over TCP](/configuration/shared/udp-over-tcp) for details.
 
 Conflict with `multiplex`.
 

+ 4 - 2
docs/configuration/outbound/shadowsocks.zh.md

@@ -12,7 +12,7 @@
   "plugin": "",
   "plugin_opts": "",
   "network": "udp",
-  "udp_over_tcp": false,
+  "udp_over_tcp": false | {},
   "multiplex": {},
 
   ... // 拨号字段
@@ -87,7 +87,9 @@ Shadowsocks SIP003 插件参数。
 
 #### udp_over_tcp
 
-启用 UDP over TCP 协议。
+UDP over TCP 配置。
+
+参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp)。
 
 与 `multiplex` 冲突。
 

+ 4 - 2
docs/configuration/outbound/socks.md

@@ -13,7 +13,7 @@
   "username": "sekai",
   "password": "admin",
   "network": "udp",
-  "udp_over_tcp": false,
+  "udp_over_tcp": false | {},
 
   ... // Dial Fields
 }
@@ -57,7 +57,9 @@ Both is enabled by default.
 
 #### udp_over_tcp
 
-Enable the UDP over TCP protocol.
+UDP over TCP protocol settings.
+
+See [UDP Over TCP](/configuration/shared/udp-over-tcp) for details.
 
 ### Dial Fields
 

+ 4 - 2
docs/configuration/outbound/socks.zh.md

@@ -13,7 +13,7 @@
   "username": "sekai",
   "password": "admin",
   "network": "udp",
-  "udp_over_tcp": false,
+  "udp_over_tcp": false | {},
 
   ... // 拨号字段
 }
@@ -57,7 +57,9 @@ SOCKS5 密码。
 
 #### udp_over_tcp
 
-启用 UDP over TCP 协议。
+UDP over TCP 配置。
+
+参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp)。
 
 ### 拨号字段
 

+ 81 - 0
docs/configuration/shared/udp-over-tcp.md

@@ -0,0 +1,81 @@
+# UDP over TCP
+
+!!! warning ""
+
+    It's a proprietary protocol created by SagerNet, not part of shadowsocks.
+
+The UDP over TCP protocol is used to transmit UDP packets in TCP.
+
+### Structure
+
+```json
+{
+  "enabled": true,
+  "version": 2
+}
+```
+
+!!! info ""
+
+    The structure can be replaced with a boolean value when the version is not specified.
+
+### Fields
+
+#### enabled
+
+Enable the UDP over TCP protocol.
+
+#### version
+
+The protocol version, `1` or `2`.
+
+2 is used by default.
+
+### Application support
+
+| Project      | UoT v1               | UoT v2     |
+|--------------|----------------------|------------|
+| sing-box     | v0 (2022/08/11)      | v1.2-beta9 |
+| Xray-core    | v1.5.7 (2022/06/05)  | /          |
+| Clash.Meta   | v1.12.0 (2022/07/02) | /          |
+| Shadowrocket | v2.2.12 (2022/08/13) | /          |
+
+### Protocol details
+
+#### Protocol version 1
+
+The client requests the magic address to the upper layer proxy protocol to indicate the request: `sp.udp-over-tcp.arpa`
+
+#### Stream format
+
+| ATYP | address  | port  | length | data     |
+|------|----------|-------|--------|----------|
+| u8   | variable | u16be | u16be  | variable |
+
+*ATYP / address / port*: Uses the SOCKS address format.
+
+#### Protocol version 2
+
+Protocol version 2 uses a new magic address: `sp.v2.udp-over-tcp.arpa`
+
+##### Request format
+
+| isConnect | ATYP | address  | port  |
+|-----------|------|----------|-------|
+| u8        | u8   | variable | u16be |
+
+**version**: Fixed to 2.
+
+**isConnect**: Set to 1 to indicates that the stream uses the connect format, 0 to disable.
+
+**ATYP / address / port**: Request destination, uses the SOCKS address format.
+
+##### Connect stream format
+
+| length | data     |
+|--------|----------|
+| u16be  | variable |
+
+##### Non-connect stream format
+
+As the same as the stream format in protocol version 1.

+ 7 - 1
docs/examples/shadowsocks.md

@@ -1,5 +1,9 @@
 # Shadowsocks
 
+!!! warning ""
+
+    For censorship bypass usage in China, we recommend using UDP over TCP and disabling UDP on the server.
+
 ## Single User
 
 #### Server
@@ -11,6 +15,7 @@
       "type": "shadowsocks",
       "listen": "::",
       "listen_port": 8080,
+      "network": "tcp",
       "method": "2022-blake3-aes-128-gcm",
       "password": "8JCsPssfgS8tiRwiMlhARg=="
     }
@@ -35,7 +40,8 @@
       "server": "127.0.0.1",
       "server_port": 8080,
       "method": "2022-blake3-aes-128-gcm",
-      "password": "8JCsPssfgS8tiRwiMlhARg=="
+      "password": "8JCsPssfgS8tiRwiMlhARg==",
+      "udp_over_tcp": true
     }
   ]
 }

+ 2 - 0
docs/examples/shadowtls.md

@@ -24,6 +24,7 @@
       "type": "shadowsocks",
       "tag": "shadowsocks-in",
       "listen": "127.0.0.1",
+      "network": "tcp",
       "method": "2022-blake3-aes-128-gcm",
       "password": "8JCsPssfgS8tiRwiMlhARg=="
     }
@@ -46,6 +47,7 @@
         "max_connections": 4,
         "min_streams": 4
       }
+      // or "udp_over_tcp": true
     },
     {
       "type": "shadowtls",

+ 1 - 1
go.mod

@@ -25,7 +25,7 @@ require (
 	github.com/sagernet/gomobile v0.0.0-20221130124640-349ebaa752ca
 	github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32
 	github.com/sagernet/reality v0.0.0-20230312150606-35ea9af0e0b8
-	github.com/sagernet/sing v0.1.9-0.20230315163130-ed73785ecc78
+	github.com/sagernet/sing v0.1.9-0.20230317044231-85a9429eadb6
 	github.com/sagernet/sing-dns v0.1.4
 	github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9
 	github.com/sagernet/sing-shadowtls v0.1.0

+ 2 - 2
go.sum

@@ -111,8 +111,8 @@ github.com/sagernet/reality v0.0.0-20230312150606-35ea9af0e0b8 h1:4M3+0/kqvJuTsi
 github.com/sagernet/reality v0.0.0-20230312150606-35ea9af0e0b8/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
 github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
 github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
-github.com/sagernet/sing v0.1.9-0.20230315163130-ed73785ecc78 h1:SO7TITxjoKyQFBVR0MJhTji9msxEXcv5p60imPrEyY4=
-github.com/sagernet/sing v0.1.9-0.20230315163130-ed73785ecc78/go.mod h1:9uHswk2hITw8leDbiLS/xn0t9nzBcbePxzm9PJhwdlw=
+github.com/sagernet/sing v0.1.9-0.20230317044231-85a9429eadb6 h1:h1wGLPBJLjujj9kYSbLiP1Tt6+IQnZ7Ok7jQd4u3xxk=
+github.com/sagernet/sing v0.1.9-0.20230317044231-85a9429eadb6/go.mod h1:9uHswk2hITw8leDbiLS/xn0t9nzBcbePxzm9PJhwdlw=
 github.com/sagernet/sing-dns v0.1.4 h1:7VxgeoSCiiazDSaXXQVcvrTBxFpOePPq/4XdgnUDN+0=
 github.com/sagernet/sing-dns v0.1.4/go.mod h1:1+6pCa48B1AI78lD+/i/dLgpw4MwfnsSpZo0Ds8wzzk=
 github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9 h1:qS39eA4C7x+zhEkySbASrtmb6ebdy5v0y2M6mgkmSO0=

+ 1 - 0
mkdocs.yml

@@ -64,6 +64,7 @@ nav:
           - TLS: configuration/shared/tls.md
           - Multiplex: configuration/shared/multiplex.md
           - V2Ray Transport: configuration/shared/v2ray-transport.md
+          - UDP over TCP: configuration/shared/udp-over-tcp.md
       - Inbound:
           - configuration/inbound/index.md
           - Direct: configuration/inbound/direct.md

+ 7 - 8
option/shadowsocks.go

@@ -23,12 +23,11 @@ type ShadowsocksDestination struct {
 type ShadowsocksOutboundOptions struct {
 	DialerOptions
 	ServerOptions
-	Method           string            `json:"method"`
-	Password         string            `json:"password"`
-	Plugin           string            `json:"plugin,omitempty"`
-	PluginOptions    string            `json:"plugin_opts,omitempty"`
-	Network          NetworkList       `json:"network,omitempty"`
-	UoT              bool              `json:"udp_over_tcp,omitempty"`
-	UoTVersion       int               `json:"udp_over_tcp_version,omitempty"`
-	MultiplexOptions *MultiplexOptions `json:"multiplex,omitempty"`
+	Method            string             `json:"method"`
+	Password          string             `json:"password"`
+	Plugin            string             `json:"plugin,omitempty"`
+	PluginOptions     string             `json:"plugin_opts,omitempty"`
+	Network           NetworkList        `json:"network,omitempty"`
+	UDPOverTCPOptions *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"`
+	MultiplexOptions  *MultiplexOptions  `json:"multiplex,omitempty"`
 }

+ 5 - 6
option/simple.go

@@ -17,12 +17,11 @@ type HTTPMixedInboundOptions struct {
 type SocksOutboundOptions struct {
 	DialerOptions
 	ServerOptions
-	Version    string      `json:"version,omitempty"`
-	Username   string      `json:"username,omitempty"`
-	Password   string      `json:"password,omitempty"`
-	Network    NetworkList `json:"network,omitempty"`
-	UoT        bool        `json:"udp_over_tcp,omitempty"`
-	UoTVersion int         `json:"udp_over_tcp_version,omitempty"`
+	Version           string             `json:"version,omitempty"`
+	Username          string             `json:"username,omitempty"`
+	Password          string             `json:"password,omitempty"`
+	Network           NetworkList        `json:"network,omitempty"`
+	UDPOverTCPOptions *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"`
 }
 
 type HTTPOutboundOptions struct {

+ 30 - 0
option/udp_over_tcp.go

@@ -0,0 +1,30 @@
+package option
+
+import (
+	"github.com/sagernet/sing-box/common/json"
+	"github.com/sagernet/sing/common/uot"
+)
+
+type _UDPOverTCPOptions struct {
+	Enabled bool  `json:"enabled,omitempty"`
+	Version uint8 `json:"version,omitempty"`
+}
+
+type UDPOverTCPOptions _UDPOverTCPOptions
+
+func (o UDPOverTCPOptions) MarshalJSON() ([]byte, error) {
+	switch o.Version {
+	case 0, uot.Version:
+		return json.Marshal(o.Enabled)
+	default:
+		return json.Marshal(_UDPOverTCPOptions(o))
+	}
+}
+
+func (o *UDPOverTCPOptions) UnmarshalJSON(bytes []byte) error {
+	err := json.Unmarshal(bytes, &o.Enabled)
+	if err == nil {
+		return nil
+	}
+	return json.Unmarshal(bytes, (*_UDPOverTCPOptions)(o))
+}

+ 17 - 45
outbound/shadowsocks.go

@@ -29,8 +29,7 @@ type Shadowsocks struct {
 	method          shadowsocks.Method
 	serverAddr      M.Socksaddr
 	plugin          sip003.Plugin
-	uot             bool
-	uotVersion      int
+	uotClient       *uot.Client
 	multiplexDialer N.Dialer
 }
 
@@ -50,7 +49,6 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 		dialer:     dialer.New(router, options.DialerOptions),
 		method:     method,
 		serverAddr: options.ServerOptions.Build(),
-		uot:        options.UoT,
 	}
 	if options.Plugin != "" {
 		outbound.plugin, err = sip003.CreatePlugin(options.Plugin, options.PluginOptions, router, outbound.dialer, outbound.serverAddr)
@@ -58,19 +56,18 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 			return nil, err
 		}
 	}
-	if !options.UoT {
+	uotOptions := common.PtrValueOrDefault(options.UDPOverTCPOptions)
+	if !uotOptions.Enabled {
 		outbound.multiplexDialer, err = mux.NewClientWithOptions(ctx, (*shadowsocksDialer)(outbound), common.PtrValueOrDefault(options.MultiplexOptions))
 		if err != nil {
 			return nil, err
 		}
 	}
-	switch options.UoTVersion {
-	case uot.LegacyVersion:
-		outbound.uotVersion = uot.LegacyVersion
-	case 0, uot.Version:
-		outbound.uotVersion = uot.Version
-	default:
-		return nil, E.New("unknown udp over tcp protocol version ", options.UoTVersion)
+	if uotOptions.Enabled {
+		outbound.uotClient = &uot.Client{
+			Dialer:  (*shadowsocksDialer)(outbound),
+			Version: uotOptions.Version,
+		}
 	}
 	return outbound, nil
 }
@@ -84,25 +81,12 @@ func (h *Shadowsocks) DialContext(ctx context.Context, network string, destinati
 		case N.NetworkTCP:
 			h.logger.InfoContext(ctx, "outbound connection to ", destination)
 		case N.NetworkUDP:
-			if h.uot {
-				h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
-				var uotDestination M.Socksaddr
-				if h.uotVersion == uot.Version {
-					uotDestination.Fqdn = uot.MagicAddress
-				} else {
-					uotDestination.Fqdn = uot.LegacyMagicAddress
-				}
-				tcpConn, err := (*shadowsocksDialer)(h).DialContext(ctx, N.NetworkTCP, uotDestination)
-				if err != nil {
-					return nil, err
-				}
-				if h.uotVersion == uot.Version {
-					return uot.NewLazyConn(tcpConn, uot.Request{IsConnect: true, Destination: destination}), nil
-				} else {
-					return uot.NewConn(tcpConn, false, destination), nil
-				}
+			if h.uotClient != nil {
+				h.logger.InfoContext(ctx, "outbound UoT connect packet connection to ", destination)
+				return h.uotClient.DialContext(ctx, network, destination)
+			} else {
+				h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 			}
-			h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 		}
 		return (*shadowsocksDialer)(h).DialContext(ctx, network, destination)
 	} else {
@@ -121,23 +105,11 @@ func (h *Shadowsocks) ListenPacket(ctx context.Context, destination M.Socksaddr)
 	metadata.Outbound = h.tag
 	metadata.Destination = destination
 	if h.multiplexDialer == nil {
-		if h.uot {
+		if h.uotClient != nil {
 			h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
-			var uotDestination M.Socksaddr
-			if h.uotVersion == uot.Version {
-				uotDestination.Fqdn = uot.MagicAddress
-			} else {
-				uotDestination.Fqdn = uot.LegacyMagicAddress
-			}
-			tcpConn, err := (*shadowsocksDialer)(h).DialContext(ctx, N.NetworkTCP, uotDestination)
-			if err != nil {
-				return nil, err
-			}
-			if h.uotVersion == uot.Version {
-				return uot.NewLazyConn(tcpConn, uot.Request{Destination: destination}), nil
-			} else {
-				return uot.NewConn(tcpConn, false, destination), nil
-			}
+			return h.uotClient.ListenPacket(ctx, destination)
+		} else {
+			h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 		}
 		h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 		return (*shadowsocksDialer)(h).ListenPacket(ctx, destination)

+ 15 - 45
outbound/socks.go

@@ -9,6 +9,7 @@ import (
 	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"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
@@ -20,10 +21,9 @@ var _ adapter.Outbound = (*Socks)(nil)
 
 type Socks struct {
 	myOutboundAdapter
-	client     *socks.Client
-	resolve    bool
-	uot        bool
-	uotVersion int
+	client    *socks.Client
+	resolve   bool
+	uotClient *uot.Client
 }
 
 func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, options option.SocksOutboundOptions) (*Socks, error) {
@@ -47,15 +47,13 @@ func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, optio
 		},
 		client:  socks.NewClient(dialer.New(router, options.DialerOptions), options.ServerOptions.Build(), version, options.Username, options.Password),
 		resolve: version == socks.Version4,
-		uot:     options.UoT,
 	}
-	switch options.UoTVersion {
-	case uot.LegacyVersion:
-		outbound.uotVersion = uot.LegacyVersion
-	case 0, uot.Version:
-		outbound.uotVersion = uot.Version
-	default:
-		return nil, E.New("unknown udp over tcp protocol version ", options.UoTVersion)
+	uotOptions := common.PtrValueOrDefault(options.UDPOverTCPOptions)
+	if uotOptions.Enabled {
+		outbound.uotClient = &uot.Client{
+			Dialer:  outbound.client,
+			Version: uotOptions.Version,
+		}
 	}
 	return outbound, nil
 }
@@ -68,23 +66,9 @@ func (h *Socks) DialContext(ctx context.Context, network string, destination M.S
 	case N.NetworkTCP:
 		h.logger.InfoContext(ctx, "outbound connection to ", destination)
 	case N.NetworkUDP:
-		if h.uot {
-			h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
-			var uotDestination M.Socksaddr
-			if h.uotVersion == uot.Version {
-				uotDestination.Fqdn = uot.MagicAddress
-			} else {
-				uotDestination.Fqdn = uot.LegacyMagicAddress
-			}
-			tcpConn, err := h.client.DialContext(ctx, N.NetworkTCP, uotDestination)
-			if err != nil {
-				return nil, err
-			}
-			if h.uotVersion == uot.Version {
-				return uot.NewLazyConn(tcpConn, uot.Request{IsConnect: true, Destination: destination}), nil
-			} else {
-				return uot.NewConn(tcpConn, false, destination), nil
-			}
+		if h.uotClient != nil {
+			h.logger.InfoContext(ctx, "outbound UoT connect packet connection to ", destination)
+			return h.uotClient.DialContext(ctx, network, destination)
 		}
 		h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 	default:
@@ -104,23 +88,9 @@ func (h *Socks) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.
 	ctx, metadata := adapter.AppendContext(ctx)
 	metadata.Outbound = h.tag
 	metadata.Destination = destination
-	if h.uot {
+	if h.uotClient != nil {
 		h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
-		var uotDestination M.Socksaddr
-		if h.uotVersion == uot.Version {
-			uotDestination.Fqdn = uot.MagicAddress
-		} else {
-			uotDestination.Fqdn = uot.LegacyMagicAddress
-		}
-		tcpConn, err := h.client.DialContext(ctx, N.NetworkTCP, uotDestination)
-		if err != nil {
-			return nil, err
-		}
-		if h.uotVersion == uot.Version {
-			return uot.NewLazyConn(tcpConn, uot.Request{Destination: destination}), nil
-		} else {
-			return uot.NewConn(tcpConn, false, destination), nil
-		}
+		return h.uotClient.ListenPacket(ctx, destination)
 	}
 	h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
 	return h.client.ListenPacket(ctx, destination)

+ 2 - 2
route/router.go

@@ -589,12 +589,12 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
 		}
 		metadata.Domain = metadata.Destination.Fqdn
 		metadata.Destination = request.Destination
-		return r.RoutePacketConnection(ctx, uot.NewConn(conn, request.IsConnect, metadata.Destination), metadata)
+		return r.RoutePacketConnection(ctx, uot.NewConn(conn, *request), metadata)
 	case uot.LegacyMagicAddress:
 		r.logger.InfoContext(ctx, "inbound legacy UoT connection")
 		metadata.Domain = metadata.Destination.Fqdn
 		metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()}
-		return r.RoutePacketConnection(ctx, uot.NewConn(conn, false, metadata.Destination), metadata)
+		return r.RoutePacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata)
 	}
 	if metadata.InboundOptions.SniffEnabled {
 		buffer := buf.NewPacket()