From 99aa1b0de1b6318d9d9e1eeb067028d813fdf49d Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 22 Apr 2025 20:49:54 +0800 Subject: [PATCH] feat: inbound support shadow-tls --- adapter/outbound/shadowsocks.go | 17 +++++--- docs/config.yaml | 10 +++++ listener/config/shadowsocks.go | 1 + listener/config/shadowtls.go | 22 ++++++++++ listener/inbound/common_test.go | 10 +++++ listener/inbound/shadowsocks.go | 2 + listener/inbound/shadowsocks_test.go | 59 +++++++++++++++++++++++-- listener/inbound/shadowtls.go | 58 ++++++++++++++++++++++++ listener/sing/dialer.go | 35 +++++++++++++++ listener/sing_shadowsocks/server.go | 63 ++++++++++++++++++++++++++- transport/sing-shadowtls/shadowtls.go | 3 +- 11 files changed, 270 insertions(+), 10 deletions(-) create mode 100644 listener/config/shadowtls.go create mode 100644 listener/inbound/shadowtls.go create mode 100644 listener/sing/dialer.go diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index 156b419a..85b078eb 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -84,11 +84,12 @@ type gostObfsOption struct { } type shadowTLSOption struct { - Password string `obfs:"password,omitempty"` - Host string `obfs:"host"` - Fingerprint string `obfs:"fingerprint,omitempty"` - SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` - Version int `obfs:"version,omitempty"` + Password string `obfs:"password,omitempty"` + Host string `obfs:"host"` + Fingerprint string `obfs:"fingerprint,omitempty"` + SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` + Version int `obfs:"version,omitempty"` + ALPN []string `obfs:"alpn,omitempty"` } type restlsOption struct { @@ -342,6 +343,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { SkipCertVerify: opt.SkipCertVerify, Version: opt.Version, } + + if opt.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array + shadowTLSOpt.ALPN = opt.ALPN + } else { + shadowTLSOpt.ALPN = shadowtls.DefaultALPN + } } else if option.Plugin == restls.Mode { obfsMode = restls.Mode restlsOpt := &restlsOption{} diff --git a/docs/config.yaml b/docs/config.yaml index be55e0ef..af6e3a61 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -448,6 +448,7 @@ proxies: # socks5 host: "cloud.tencent.com" password: "shadow_tls_password" version: 2 # support 1/2/3 + # alpn: ["h2","http/1.1"] - name: "ss5" type: ss @@ -1179,6 +1180,15 @@ listeners: # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) password: vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg= cipher: 2022-blake3-aes-256-gcm + # shadow-tls: + # enable: false # 设置为true时开启 + # version: 3 # 支持v1/v2/v3 + # password: password # v2设置项 + # users: # v3设置项 + # - name: 1 + # password: password + # handshake: + # dest: test.com:443 - name: vmess-in-1 type: vmess diff --git a/listener/config/shadowsocks.go b/listener/config/shadowsocks.go index c5c60f10..442743ef 100644 --- a/listener/config/shadowsocks.go +++ b/listener/config/shadowsocks.go @@ -13,6 +13,7 @@ type ShadowsocksServer struct { Cipher string Udp bool MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` + ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"` } func (t ShadowsocksServer) String() string { diff --git a/listener/config/shadowtls.go b/listener/config/shadowtls.go new file mode 100644 index 00000000..b427ad62 --- /dev/null +++ b/listener/config/shadowtls.go @@ -0,0 +1,22 @@ +package config + +type ShadowTLS struct { + Enable bool + Version int + Password string + Users []ShadowTLSUser + Handshake ShadowTLSHandshakeOptions + HandshakeForServerName map[string]ShadowTLSHandshakeOptions + StrictMode bool + WildcardSNI string +} + +type ShadowTLSUser struct { + Name string + Password string +} + +type ShadowTLSHandshakeOptions struct { + Dest string + Proxy string +} diff --git a/listener/inbound/common_test.go b/listener/inbound/common_test.go index 18a6eefa..528cbbf9 100644 --- a/listener/inbound/common_test.go +++ b/listener/inbound/common_test.go @@ -17,6 +17,7 @@ import ( N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/generater" C "github.com/metacubex/mihomo/constant" @@ -36,6 +37,7 @@ var tlsClientConfig, _ = ca.GetTLSConfig(nil, tlsFingerprint, "", "") var realityPrivateKey, realityPublickey string var realityDest = "itunes.apple.com" var realityShortid = "10f897e26c4b9478" +var realityRealDial = false func init() { rand.Read(httpData) @@ -205,6 +207,14 @@ func NewHttpTestTunnel() *TestTunnel { if metadata.DstPort == 443 { tlsConn := tls.Server(c, tlsConfig.Clone()) if metadata.Host == realityDest { // ignore the tls handshake error for realityDest + if realityRealDial { + rconn, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress()) + if err != nil { + panic(err) + } + N.Relay(rconn, tlsConn) + return + } ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout) defer cancel() if err := tlsConn.HandshakeContext(ctx); err != nil { diff --git a/listener/inbound/shadowsocks.go b/listener/inbound/shadowsocks.go index 51592ab3..994f4c59 100644 --- a/listener/inbound/shadowsocks.go +++ b/listener/inbound/shadowsocks.go @@ -15,6 +15,7 @@ type ShadowSocksOption struct { Cipher string `inbound:"cipher"` UDP bool `inbound:"udp,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` + ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"` } func (o ShadowSocksOption) Equal(config C.InboundConfig) bool { @@ -43,6 +44,7 @@ func NewShadowSocks(options *ShadowSocksOption) (*ShadowSocks, error) { Cipher: options.Cipher, Udp: options.UDP, MuxOption: options.MuxOption.Build(), + ShadowTLS: options.ShadowTLS.Build(), }, }, nil } diff --git a/listener/inbound/shadowsocks_test.go b/listener/inbound/shadowsocks_test.go index cf72c55c..2eaebcb2 100644 --- a/listener/inbound/shadowsocks_test.go +++ b/listener/inbound/shadowsocks_test.go @@ -3,12 +3,14 @@ package inbound_test import ( "crypto/rand" "encoding/base64" + "net" "net/netip" "strings" "testing" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/listener/inbound" + shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls" shadowsocks "github.com/metacubex/sing-shadowsocks" "github.com/metacubex/sing-shadowsocks/shadowaead" @@ -18,6 +20,7 @@ import ( ) var shadowsocksCipherList = []string{shadowsocks.MethodNone} +var shadowsocksCipherListShort = []string{shadowsocks.MethodNone} var shadowsocksPassword32 string var shadowsocksPassword16 string @@ -25,15 +28,17 @@ func init() { shadowsocksCipherList = append(shadowsocksCipherList, shadowaead.List...) shadowsocksCipherList = append(shadowsocksCipherList, shadowaead_2022.List...) shadowsocksCipherList = append(shadowsocksCipherList, shadowstream.List...) + shadowsocksCipherListShort = append(shadowsocksCipherListShort, shadowaead.List[0]) + shadowsocksCipherListShort = append(shadowsocksCipherListShort, shadowaead_2022.List[0]) passwordBytes := make([]byte, 32) rand.Read(passwordBytes) shadowsocksPassword32 = base64.StdEncoding.EncodeToString(passwordBytes) shadowsocksPassword16 = base64.StdEncoding.EncodeToString(passwordBytes[:16]) } -func testInboundShadowSocks(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption) { +func testInboundShadowSocks(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption, cipherList []string) { t.Parallel() - for _, cipher := range shadowsocksCipherList { + for _, cipher := range cipherList { cipher := cipher t.Run(cipher, func(t *testing.T) { inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value @@ -94,5 +99,53 @@ func testInboundShadowSocks0(t *testing.T, inboundOptions inbound.ShadowSocksOpt func TestInboundShadowSocks_Basic(t *testing.T) { inboundOptions := inbound.ShadowSocksOption{} outboundOptions := outbound.ShadowSocksOption{} - testInboundShadowSocks(t, inboundOptions, outboundOptions) + testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherList) +} + +func TestInboundShadowSocks_ShadowTlsv1(t *testing.T) { + inboundOptions := inbound.ShadowSocksOption{ + ShadowTLS: inbound.ShadowTLS{ + Enable: true, + Version: 1, + Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")}, + }, + } + outboundOptions := outbound.ShadowSocksOption{ + Plugin: shadowtls.Mode, + PluginOpts: map[string]any{"host": realityDest, "fingerprint": tlsFingerprint, "version": 1}, + } + testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherListShort) +} + +func TestInboundShadowSocks_ShadowTlsv2(t *testing.T) { + inboundOptions := inbound.ShadowSocksOption{ + ShadowTLS: inbound.ShadowTLS{ + Enable: true, + Version: 2, + Password: shadowsocksPassword16, + Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")}, + }, + } + outboundOptions := outbound.ShadowSocksOption{ + Plugin: shadowtls.Mode, + PluginOpts: map[string]any{"host": realityDest, "password": shadowsocksPassword16, "fingerprint": tlsFingerprint, "version": 2}, + } + outboundOptions.PluginOpts["alpn"] = []string{"http/1.1"} // shadowtls v2 work confuse with http/2 server, so we set alpn to http/1.1 to pass the test + testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherListShort) +} + +func TestInboundShadowSocks_ShadowTlsv3(t *testing.T) { + inboundOptions := inbound.ShadowSocksOption{ + ShadowTLS: inbound.ShadowTLS{ + Enable: true, + Version: 3, + Users: []inbound.ShadowTLSUser{{Name: "test", Password: shadowsocksPassword16}}, + Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")}, + }, + } + outboundOptions := outbound.ShadowSocksOption{ + Plugin: shadowtls.Mode, + PluginOpts: map[string]any{"host": realityDest, "password": shadowsocksPassword16, "fingerprint": tlsFingerprint, "version": 3}, + } + testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherListShort) } diff --git a/listener/inbound/shadowtls.go b/listener/inbound/shadowtls.go new file mode 100644 index 00000000..d71adbf0 --- /dev/null +++ b/listener/inbound/shadowtls.go @@ -0,0 +1,58 @@ +package inbound + +import ( + "github.com/metacubex/mihomo/common/utils" + LC "github.com/metacubex/mihomo/listener/config" +) + +type ShadowTLS struct { + Enable bool `inbound:"enable"` + Version int `inbound:"version,omitempty"` + Password string `inbound:"password,omitempty"` + Users []ShadowTLSUser `inbound:"users,omitempty"` + Handshake ShadowTLSHandshakeOptions `inbound:"handshake,omitempty"` + HandshakeForServerName map[string]ShadowTLSHandshakeOptions `inbound:"handshake-for-server-name,omitempty"` + StrictMode bool `inbound:"strict-mode,omitempty"` + WildcardSNI string `inbound:"wildcard-sni,omitempty"` +} + +type ShadowTLSUser struct { + Name string `inbound:"name,omitempty"` + Password string `inbound:"password,omitempty"` +} + +type ShadowTLSHandshakeOptions struct { + Dest string `inbound:"dest"` + Proxy string `inbound:"proxy,omitempty"` +} + +func (c ShadowTLS) Build() LC.ShadowTLS { + handshakeForServerName := make(map[string]LC.ShadowTLSHandshakeOptions) + for k, v := range c.HandshakeForServerName { + handshakeForServerName[k] = v.Build() + } + return LC.ShadowTLS{ + Enable: c.Enable, + Version: c.Version, + Password: c.Password, + Users: utils.Map(c.Users, ShadowTLSUser.Build), + Handshake: c.Handshake.Build(), + HandshakeForServerName: handshakeForServerName, + StrictMode: c.StrictMode, + WildcardSNI: c.WildcardSNI, + } +} + +func (c ShadowTLSUser) Build() LC.ShadowTLSUser { + return LC.ShadowTLSUser{ + Name: c.Name, + Password: c.Password, + } +} + +func (c ShadowTLSHandshakeOptions) Build() LC.ShadowTLSHandshakeOptions { + return LC.ShadowTLSHandshakeOptions{ + Dest: c.Dest, + Proxy: c.Proxy, + } +} diff --git a/listener/sing/dialer.go b/listener/sing/dialer.go new file mode 100644 index 00000000..9c8305b6 --- /dev/null +++ b/listener/sing/dialer.go @@ -0,0 +1,35 @@ +package sing + +import ( + "context" + "fmt" + "net" + + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/listener/inner" + + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type Dialer struct { + t C.Tunnel + proxy string +} + +func (d Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + if network != "tcp" && network != "tcp4" && network != "tcp6" { + return nil, fmt.Errorf("unsupported network %s", network) + } + return inner.HandleTcp(d.t, destination.String(), d.proxy) +} + +func (d Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, fmt.Errorf("unsupported ListenPacket") +} + +var _ N.Dialer = (*Dialer)(nil) + +func NewDialer(t C.Tunnel, proxy string) (d *Dialer) { + return &Dialer{t, proxy} +} diff --git a/listener/sing_shadowsocks/server.go b/listener/sing_shadowsocks/server.go index fe934ee6..020aa782 100644 --- a/listener/sing_shadowsocks/server.go +++ b/listener/sing_shadowsocks/server.go @@ -18,6 +18,7 @@ import ( shadowsocks "github.com/metacubex/sing-shadowsocks" "github.com/metacubex/sing-shadowsocks/shadowaead" "github.com/metacubex/sing-shadowsocks/shadowaead_2022" + shadowtls "github.com/metacubex/sing-shadowtls" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" @@ -31,10 +32,24 @@ type Listener struct { listeners []net.Listener udpListeners []net.PacketConn service shadowsocks.Service + shadowTLS *shadowtls.Service } var _listener *Listener +// shadowTLSService is a wrapper for shadowsocks.Service to support shadowTLS. +type shadowTLSService struct { + shadowsocks.Service + shadowTLS *shadowtls.Service +} + +func (s *shadowTLSService) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + if s.shadowTLS != nil { + return s.shadowTLS.NewConnection(ctx, conn, metadata) + } + return s.Service.NewConnection(ctx, conn, metadata) +} + func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addition) (C.MultiAddrListener, error) { var sl *Listener var err error @@ -60,7 +75,8 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi return nil, err } - sl = &Listener{false, config, nil, nil, nil} + sl = &Listener{} + sl.config = config switch { case config.Cipher == shadowsocks.MethodNone: @@ -77,6 +93,51 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi return nil, err } + if config.ShadowTLS.Enable { + buildHandshake := func(handshake LC.ShadowTLSHandshakeOptions) (handshakeConfig shadowtls.HandshakeConfig) { + handshakeConfig.Server = M.ParseSocksaddr(handshake.Dest) + handshakeConfig.Dialer = sing.NewDialer(tunnel, handshake.Proxy) + return + } + var handshakeForServerName map[string]shadowtls.HandshakeConfig + if config.ShadowTLS.Version > 1 { + handshakeForServerName = make(map[string]shadowtls.HandshakeConfig) + for serverName, serverOptions := range config.ShadowTLS.HandshakeForServerName { + handshakeForServerName[serverName] = buildHandshake(serverOptions) + } + } + var wildcardSNI shadowtls.WildcardSNI + switch config.ShadowTLS.WildcardSNI { + case "authed": + wildcardSNI = shadowtls.WildcardSNIAuthed + case "all": + wildcardSNI = shadowtls.WildcardSNIAll + default: + wildcardSNI = shadowtls.WildcardSNIOff + } + var shadowTLS *shadowtls.Service + shadowTLS, err = shadowtls.NewService(shadowtls.ServiceConfig{ + Version: config.ShadowTLS.Version, + Password: config.ShadowTLS.Password, + Users: common.Map(config.ShadowTLS.Users, func(it LC.ShadowTLSUser) shadowtls.User { + return shadowtls.User{Name: it.Name, Password: it.Password} + }), + Handshake: buildHandshake(config.ShadowTLS.Handshake), + HandshakeForServerName: handshakeForServerName, + StrictMode: config.ShadowTLS.StrictMode, + WildcardSNI: wildcardSNI, + Handler: sl.service, + Logger: log.SingLogger, + }) + if err != nil { + return nil, err + } + sl.service = &shadowTLSService{ + Service: sl.service, + shadowTLS: shadowTLS, + } + } + for _, addr := range strings.Split(config.Listen, ",") { addr := addr diff --git a/transport/sing-shadowtls/shadowtls.go b/transport/sing-shadowtls/shadowtls.go index ff7a2ccd..ed1b7d08 100644 --- a/transport/sing-shadowtls/shadowtls.go +++ b/transport/sing-shadowtls/shadowtls.go @@ -28,11 +28,12 @@ type ShadowTLSOption struct { ClientFingerprint string SkipCertVerify bool Version int + ALPN []string } func NewShadowTLS(ctx context.Context, conn net.Conn, option *ShadowTLSOption) (net.Conn, error) { tlsConfig := &tls.Config{ - NextProtos: DefaultALPN, + NextProtos: option.ALPN, MinVersion: tls.VersionTLS12, InsecureSkipVerify: option.SkipCertVerify, ServerName: option.Host,