diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index 5f53edae..7c0debbe 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -19,6 +19,7 @@ import ( "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/uot" ) func init() { @@ -29,6 +30,7 @@ type ShadowSocks struct { *Base method shadowsocks.Method + option *ShadowSocksOption // obfs obfsMode string obfsOption *simpleObfsOption @@ -45,6 +47,7 @@ type ShadowSocksOption struct { UDP bool `proxy:"udp,omitempty"` Plugin string `proxy:"plugin,omitempty"` PluginOpts map[string]any `proxy:"plugin-opts,omitempty"` + UDPOverTCP bool `proxy:"udp-over-tcp,omitempty"` } type simpleObfsOption struct { @@ -96,6 +99,15 @@ func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, op // ListenPacketContext implements C.ProxyAdapter func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { + if ss.option.UDPOverTCP { + metadata.Host = uot.UOTMagicAddress + metadata.DstPort = "443" + tcpConn, err := ss.DialContext(ctx, metadata, opts...) + if err != nil { + return nil, err + } + return newPacketConn(uot.NewClientConn(tcpConn), ss), nil + } pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...) if err != nil { return nil, err @@ -167,6 +179,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { }, method: method, + option: &option, obfsMode: obfsMode, v2rayOption: v2rayOption, obfsOption: obfsOption, diff --git a/test/config/xray-shadowsocks.json b/test/config/xray-shadowsocks.json new file mode 100644 index 00000000..1df376dc --- /dev/null +++ b/test/config/xray-shadowsocks.json @@ -0,0 +1,27 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "shadowsocks", + "settings": { + "network": "tcp,udp", + "clients": [ + { + "method": "aes-128-gcm", + "level": 0, + "password": "FzcLbKs2dY9mhL" + } + ] + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ], + "log": { + "loglevel": "debug" + } +} \ No newline at end of file diff --git a/test/ss_test.go b/test/ss_test.go index dec98ed4..bec1734b 100644 --- a/test/ss_test.go +++ b/test/ss_test.go @@ -3,11 +3,13 @@ package main import ( "crypto/rand" "encoding/base64" + "fmt" "net" "testing" "time" "github.com/Dreamacro/clash/adapter/outbound" + C "github.com/Dreamacro/clash/constant" "github.com/docker/docker/api/types/container" "github.com/stretchr/testify/require" ) @@ -277,3 +279,37 @@ func Benchmark_Shadowsocks(b *testing.B) { require.True(b, TCPing(net.JoinHostPort(localIP.String(), "10002"))) benchmarkProxy(b, proxy) } + +func TestClash_ShadowsocksUoT(t *testing.T) { + configPath := C.Path.Resolve("xray-shadowsocks.json") + + cfg := &container.Config{ + Image: ImageVless, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{fmt.Sprintf("%s:/etc/xray/config.json", configPath)}, + } + + id, err := startContainer(cfg, hostCfg, "xray-ss") + require.NoError(t, err) + + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{ + Name: "ss", + Server: localIP.String(), + Port: 10002, + Password: "FzcLbKs2dY9mhL", + Cipher: "aes-128-gcm", + UDP: true, + UDPOverTCP: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +}