From 43f31b40baa1569e71c151c48b8dd5e5cbd06efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 17 Mar 2023 12:24:29 +0800 Subject: [PATCH] Update UoT protocol --- docs/configuration/outbound/shadowsocks.md | 6 +- docs/configuration/outbound/shadowsocks.zh.md | 6 +- docs/configuration/outbound/socks.md | 6 +- docs/configuration/outbound/socks.zh.md | 6 +- docs/configuration/shared/udp-over-tcp.md | 81 +++++++++++++++++++ docs/examples/shadowsocks.md | 8 +- docs/examples/shadowtls.md | 2 + go.mod | 2 +- go.sum | 4 +- mkdocs.yml | 1 + option/shadowsocks.go | 15 ++-- option/simple.go | 11 ++- option/udp_over_tcp.go | 30 +++++++ outbound/shadowsocks.go | 62 ++++---------- outbound/socks.go | 60 ++++---------- route/router.go | 4 +- 16 files changed, 186 insertions(+), 118 deletions(-) create mode 100644 docs/configuration/shared/udp-over-tcp.md create mode 100644 option/udp_over_tcp.go diff --git a/docs/configuration/outbound/shadowsocks.md b/docs/configuration/outbound/shadowsocks.md index 6a0ae6ad..e004d77b 100644 --- a/docs/configuration/outbound/shadowsocks.md +++ b/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`. diff --git a/docs/configuration/outbound/shadowsocks.zh.md b/docs/configuration/outbound/shadowsocks.zh.md index 32a2de5d..16c627e3 100644 --- a/docs/configuration/outbound/shadowsocks.zh.md +++ b/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` 冲突。 diff --git a/docs/configuration/outbound/socks.md b/docs/configuration/outbound/socks.md index a1411078..94d83fe5 100644 --- a/docs/configuration/outbound/socks.md +++ b/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 diff --git a/docs/configuration/outbound/socks.zh.md b/docs/configuration/outbound/socks.zh.md index d2808b58..75548da7 100644 --- a/docs/configuration/outbound/socks.zh.md +++ b/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)。 ### 拨号字段 diff --git a/docs/configuration/shared/udp-over-tcp.md b/docs/configuration/shared/udp-over-tcp.md new file mode 100644 index 00000000..55381d6c --- /dev/null +++ b/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. \ No newline at end of file diff --git a/docs/examples/shadowsocks.md b/docs/examples/shadowsocks.md index 462ca449..7e002fab 100644 --- a/docs/examples/shadowsocks.md +++ b/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 } ] } diff --git a/docs/examples/shadowtls.md b/docs/examples/shadowtls.md index 56b49a9e..352a3058 100644 --- a/docs/examples/shadowtls.md +++ b/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", diff --git a/go.mod b/go.mod index b05b4417..15d333bb 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 45720c9b..b5f9ed24 100644 --- a/go.sum +++ b/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= diff --git a/mkdocs.yml b/mkdocs.yml index 4bd9f781..66b2f0df 100644 --- a/mkdocs.yml +++ b/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 diff --git a/option/shadowsocks.go b/option/shadowsocks.go index d12e9626..3dc96171 100644 --- a/option/shadowsocks.go +++ b/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"` } diff --git a/option/simple.go b/option/simple.go index f48dc51e..eede0512 100644 --- a/option/simple.go +++ b/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 { diff --git a/option/udp_over_tcp.go b/option/udp_over_tcp.go new file mode 100644 index 00000000..79529624 --- /dev/null +++ b/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)) +} diff --git a/outbound/shadowsocks.go b/outbound/shadowsocks.go index a037db1d..0ad6c4c2 100644 --- a/outbound/shadowsocks.go +++ b/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) diff --git a/outbound/socks.go b/outbound/socks.go index 0f336d32..3fc4b6e9 100644 --- a/outbound/socks.go +++ b/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) diff --git a/route/router.go b/route/router.go index 6e2f67cb..32299ebb 100644 --- a/route/router.go +++ b/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()