From 517a89fa9c0737db535af2f718882003d36d0609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 11 Aug 2022 10:36:28 +0800 Subject: [PATCH] Add back UoT support --- common/dialer/resolve.go | 2 +- docs/changelog.md | 8 ++- docs/configuration/outbound/shadowsocks.md | 7 +++ option/shadowsocks.go | 1 + outbound/shadowsocks.go | 33 ++++++++++-- route/router.go | 16 ++++-- test/shadowsocks_test.go | 63 ++++++++++++++++++++++ 7 files changed, 119 insertions(+), 11 deletions(-) diff --git a/common/dialer/resolve.go b/common/dialer/resolve.go index 99633e2d..90709733 100644 --- a/common/dialer/resolve.go +++ b/common/dialer/resolve.go @@ -51,7 +51,7 @@ func (d *ResolveDialer) DialContext(ctx context.Context, network string, destina } func (d *ResolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - if !destination.IsFqdn() { + if !destination.IsFqdn() || destination.Fqdn == "" { return d.dialer.ListenPacket(ctx, destination) } ctx, metadata := adapter.AppendContext(ctx) diff --git a/docs/changelog.md b/docs/changelog.md index 2527eeda..00297c92 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,9 +1,13 @@ -#### 2022/08/09 +#### 2022/08/11 + +* Add UoT option for [Shadowsocks](/configuration/outbound/shadowsocks) outbound, UoT support for all inbounds. + +#### 2022/08/10 * Add full-featured [Naive](/configuration/inbound/naive) inbound * Fix default dns server option [#9] by iKirby -#### 2022/08/08 +#### 2022/08/09 No changelog before. diff --git a/docs/configuration/outbound/shadowsocks.md b/docs/configuration/outbound/shadowsocks.md index fdc8ee12..2f5f33a4 100644 --- a/docs/configuration/outbound/shadowsocks.md +++ b/docs/configuration/outbound/shadowsocks.md @@ -14,6 +14,7 @@ "method": "2022-blake3-aes-128-gcm", "password": "8JCsPssfgS8tiRwiMlhARg==", "network": "udp", + "udp_over_tcp": false, "multiplex": {}, "detour": "upstream-out", @@ -85,6 +86,12 @@ One of `tcp` `udp`. Both is enabled by default. +#### udp_over_tcp + +Enable UDP over TCP protocol. + +Conflict with `multiplex`. + #### multiplex Multiplex configuration, see [Multiplex structure](/configuration/shared/multiplex). diff --git a/option/shadowsocks.go b/option/shadowsocks.go index 4caecc59..2d4b6422 100644 --- a/option/shadowsocks.go +++ b/option/shadowsocks.go @@ -27,5 +27,6 @@ type ShadowsocksOutboundOptions struct { Method string `json:"method"` Password string `json:"password"` Network NetworkList `json:"network,omitempty"` + UoT bool `json:"udp_over_tcp,omitempty"` MultiplexOptions *MultiplexOptions `json:"multiplex,omitempty"` } diff --git a/outbound/shadowsocks.go b/outbound/shadowsocks.go index ec26d644..ba82a26e 100644 --- a/outbound/shadowsocks.go +++ b/outbound/shadowsocks.go @@ -17,6 +17,7 @@ import ( 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/uot" ) var _ adapter.Outbound = (*Shadowsocks)(nil) @@ -26,6 +27,7 @@ type Shadowsocks struct { dialer N.Dialer method shadowsocks.Method serverAddr M.Socksaddr + uot bool multiplexDialer N.Dialer } @@ -45,10 +47,13 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte dialer: dialer.NewOutbound(router, options.OutboundDialerOptions), method: method, serverAddr: options.ServerOptions.Build(), + uot: options.UoT, } - outbound.multiplexDialer, err = mux.NewClientWithOptions(ctx, (*shadowsocksDialer)(outbound), common.PtrValueOrDefault(options.MultiplexOptions)) - if err != nil { - return nil, err + if !options.UoT { + outbound.multiplexDialer, err = mux.NewClientWithOptions(ctx, (*shadowsocksDialer)(outbound), common.PtrValueOrDefault(options.MultiplexOptions)) + if err != nil { + return nil, err + } } return outbound, nil } @@ -59,6 +64,17 @@ 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) + tcpConn, err := (*shadowsocksDialer)(h).DialContext(ctx, N.NetworkTCP, M.Socksaddr{ + Fqdn: uot.UOTMagicAddress, + Port: destination.Port, + }) + if err != nil { + return nil, err + } + return uot.NewClientConn(tcpConn), nil + } h.logger.InfoContext(ctx, "outbound packet connection to ", destination) } return (*shadowsocksDialer)(h).DialContext(ctx, network, destination) @@ -75,6 +91,17 @@ func (h *Shadowsocks) DialContext(ctx context.Context, network string, destinati func (h *Shadowsocks) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if h.multiplexDialer == nil { + if h.uot { + h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination) + tcpConn, err := (*shadowsocksDialer)(h).DialContext(ctx, N.NetworkTCP, M.Socksaddr{ + Fqdn: uot.UOTMagicAddress, + Port: destination.Port, + }) + if err != nil { + return nil, err + } + return uot.NewClientConn(tcpConn), nil + } h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return (*shadowsocksDialer)(h).ListenPacket(ctx, destination) } else { diff --git a/route/router.go b/route/router.go index 7a0bdaf2..5edebc31 100644 --- a/route/router.go +++ b/route/router.go @@ -35,6 +35,7 @@ import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/uot" ) var warnDefaultInterfaceOnUnsupportedPlatform = warning.New( @@ -492,9 +493,15 @@ func (r *Router) DefaultOutbound(network string) adapter.Outbound { } func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - if metadata.Destination.Fqdn == mux.Destination.Fqdn { + switch metadata.Destination.Fqdn { + case mux.Destination.Fqdn: r.logger.InfoContext(ctx, "inbound multiplex connection") return mux.NewConnection(ctx, r, r, r.logger, conn, metadata) + case uot.UOTMagicAddress: + r.logger.InfoContext(ctx, "inbound UoT connection") + metadata.Network = N.NetworkUDP + metadata.Destination = M.Socksaddr{} + return r.RoutePacketConnection(ctx, uot.NewClientConn(conn), metadata) } if metadata.SniffEnabled { buffer := buf.NewPacket() @@ -543,13 +550,12 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m if metadata.SniffEnabled { buffer := buf.NewPacket() buffer.FullReset() - _, err := conn.ReadPacket(buffer) + destination, err := conn.ReadPacket(buffer) if err != nil { buffer.Release() return err } sniffMetadata, err := sniff.PeekPacket(ctx, buffer.Bytes(), sniff.DomainNameQuery, sniff.QUICClientHello, sniff.STUNMessage) - originDestination := metadata.Destination if err == nil { metadata.Protocol = sniffMetadata.Protocol metadata.Domain = sniffMetadata.Domain @@ -562,9 +568,9 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol) } } - conn = bufio.NewCachedPacketConn(conn, buffer, originDestination) + conn = bufio.NewCachedPacketConn(conn, buffer, destination) } - if metadata.Destination.IsFqdn() && metadata.DomainStrategy != dns.DomainStrategyAsIS { + if metadata.Destination.IsFqdn() && metadata.Destination.Fqdn != uot.UOTMagicAddress && metadata.DomainStrategy != dns.DomainStrategyAsIS { addresses, err := r.Lookup(adapter.WithContext(ctx, &metadata), metadata.Destination.Fqdn, metadata.DomainStrategy) if err != nil { return err diff --git a/test/shadowsocks_test.go b/test/shadowsocks_test.go index e6f9538c..c82d65e2 100644 --- a/test/shadowsocks_test.go +++ b/test/shadowsocks_test.go @@ -8,6 +8,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" F "github.com/sagernet/sing/common/format" "github.com/stretchr/testify/require" @@ -193,6 +194,68 @@ func testShadowsocksSelf(t *testing.T, method string, password string) { testSuit(t, clientPort, testPort) } +func TestShadowsocksUoT(t *testing.T) { + method := shadowaead_2022.List[0] + password := mkBase64(t, 16) + startInstance(t, option.Options{ + Log: &option.LogOptions{ + Level: "trace", + }, + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + MixedOptions: option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeShadowsocks, + ShadowsocksOptions: option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: serverPort, + }, + Method: method, + Password: password, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeShadowsocks, + Tag: "ss-out", + ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Method: method, + Password: password, + UoT: true, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + DefaultOptions: option.DefaultRule{ + Inbound: []string{"mixed-in"}, + Outbound: "ss-out", + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + func mkBase64(t *testing.T, length int) string { psk := make([]byte, length) _, err := rand.Read(psk)