From b0ad9bb6f1df6e8277b70742331423bfb46e8a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 6 Oct 2022 22:47:11 +0800 Subject: [PATCH] Add shadowtls v2 support --- go.mod | 2 +- go.sum | 4 +- inbound/shadowtls.go | 97 ++++++++++++++-- option/shadowtls.go | 6 +- outbound/shadowtls.go | 35 +++++- test/go.mod | 16 +-- test/go.sum | 32 +++--- test/shadowtls_test.go | 201 ++++++++++++++++++++++------------ transport/shadowtls/client.go | 40 +++++++ transport/shadowtls/conn.go | 96 ++++++++++++++++ transport/shadowtls/hash.go | 60 ++++++++++ 11 files changed, 473 insertions(+), 116 deletions(-) create mode 100644 transport/shadowtls/client.go create mode 100644 transport/shadowtls/conn.go create mode 100644 transport/shadowtls/hash.go diff --git a/go.mod b/go.mod index f42dd396..5a407967 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/pires/go-proxyproto v0.6.2 github.com/refraction-networking/utls v1.1.2 github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb - github.com/sagernet/sing v0.0.0-20221001030341-348376220066 + github.com/sagernet/sing v0.0.0-20221006081821-c4e9bf11fa00 github.com/sagernet/sing-dns v0.0.0-20220929010544-ee843807aae3 github.com/sagernet/sing-shadowsocks v0.0.0-20220819002358-7461bb09a8f6 github.com/sagernet/sing-tun v0.0.0-20221005115555-9a556307f6a3 diff --git a/go.sum b/go.sum index fa591b2e..80c39b64 100644 --- a/go.sum +++ b/go.sum @@ -145,8 +145,8 @@ github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb h1:wc0yQ+SBn4TaTY github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb/go.mod h1:MIccjRKnPTjWwAOpl+AUGWOkzyTd9tERytudxu+1ra4= github.com/sagernet/sing v0.0.0-20220812082120-05f9836bff8f/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= -github.com/sagernet/sing v0.0.0-20221001030341-348376220066 h1:kQSd+x9ZLBcjl2+VjCLlkxzlD8VO5Q9X3FHDb7OGoN4= -github.com/sagernet/sing v0.0.0-20221001030341-348376220066/go.mod h1:zvgDYKI+vCAW9RyfyrKTgleI+DOa8lzHMPC7VZo3OL4= +github.com/sagernet/sing v0.0.0-20221006081821-c4e9bf11fa00 h1:UkgEDnH3L9eBxob+3AbE9wM4mjFnRLnaPjfLSNe+C74= +github.com/sagernet/sing v0.0.0-20221006081821-c4e9bf11fa00/go.mod h1:zvgDYKI+vCAW9RyfyrKTgleI+DOa8lzHMPC7VZo3OL4= github.com/sagernet/sing-dns v0.0.0-20220929010544-ee843807aae3 h1:AEdyJxEDFq38z0pBX/0MpikQapGMIch+1ADe9k1bJqU= github.com/sagernet/sing-dns v0.0.0-20220929010544-ee843807aae3/go.mod h1:SrvWLfOSlnFmH32CWXicfilAGgIXR0VjrH6yRbuXYww= github.com/sagernet/sing-shadowsocks v0.0.0-20220819002358-7461bb09a8f6 h1:JJfDeYYhWunvtxsU/mOVNTmFQmnzGx9dY034qG6G3g4= diff --git a/inbound/shadowtls.go b/inbound/shadowtls.go index fcc22cc6..1f751d35 100644 --- a/inbound/shadowtls.go +++ b/inbound/shadowtls.go @@ -6,12 +6,17 @@ import ( "encoding/binary" "io" "net" + "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/shadowtls" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -22,6 +27,8 @@ type ShadowTLS struct { myInboundAdapter handshakeDialer N.Dialer handshakeAddr M.Socksaddr + v2 bool + password string } func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSInboundOptions) (*ShadowTLS, error) { @@ -37,6 +44,16 @@ func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.Context }, handshakeDialer: dialer.New(router, options.Handshake.DialerOptions), handshakeAddr: options.Handshake.ServerOptions.Build(), + password: options.Password, + } + switch options.Version { + case 0: + fallthrough + case 1: + case 2: + inbound.v2 = true + default: + return nil, E.New("unknown shadowtls protocol version: ", options.Version) } inbound.connHandler = inbound return inbound, nil @@ -47,19 +64,39 @@ func (s *ShadowTLS) NewConnection(ctx context.Context, conn net.Conn, metadata a if err != nil { return err } - var handshake task.Group - handshake.Append("client handshake", func(ctx context.Context) error { - return s.copyUntilHandshakeFinished(handshakeConn, conn) - }) - handshake.Append("server handshake", func(ctx context.Context) error { - return s.copyUntilHandshakeFinished(conn, handshakeConn) - }) - handshake.FastFail() - err = handshake.Run(ctx) - if err != nil { - return err + if !s.v2 { + var handshake task.Group + handshake.Append("client handshake", func(ctx context.Context) error { + return s.copyUntilHandshakeFinished(handshakeConn, conn) + }) + handshake.Append("server handshake", func(ctx context.Context) error { + return s.copyUntilHandshakeFinished(conn, handshakeConn) + }) + handshake.FastFail() + handshake.Cleanup(func() { + handshakeConn.Close() + }) + err = handshake.Run(ctx) + if err != nil { + return err + } + return s.newConnection(ctx, conn, metadata) + } else { + hashConn := shadowtls.NewHashWriteConn(conn, s.password) + go bufio.Copy(hashConn, handshakeConn) + var request *buf.Buffer + request, err = s.copyUntilHandshakeFinishedV2(handshakeConn, conn, hashConn) + if err == nil { + handshakeConn.Close() + return s.newConnection(ctx, bufio.NewCachedConn(shadowtls.NewConn(conn), request), metadata) + } else if err == os.ErrPermission { + s.logger.WarnContext(ctx, "fallback connection") + hashConn.Fallback() + return common.Error(bufio.Copy(handshakeConn, conn)) + } else { + return err + } } - return s.newConnection(ctx, conn, metadata) } func (s *ShadowTLS) copyUntilHandshakeFinished(dst io.Writer, src io.Reader) error { @@ -91,3 +128,39 @@ func (s *ShadowTLS) copyUntilHandshakeFinished(dst io.Writer, src io.Reader) err } } } + +func (s *ShadowTLS) copyUntilHandshakeFinishedV2(dst net.Conn, src io.Reader, hash *shadowtls.HashWriteConn) (*buf.Buffer, error) { + const applicationData = 0x17 + var tlsHdr [5]byte + var applicationDataCount int + for { + _, err := io.ReadFull(src, tlsHdr[:]) + if err != nil { + return nil, err + } + length := binary.BigEndian.Uint16(tlsHdr[3:]) + if tlsHdr[0] == applicationData { + data := buf.NewSize(int(length)) + _, err = data.ReadFullFrom(src, int(length)) + if err != nil { + data.Release() + return nil, err + } + if length >= 8 && bytes.Equal(data.To(8), hash.Sum()) { + data.Advance(8) + return data, nil + } + _, err = io.Copy(dst, io.MultiReader(bytes.NewReader(tlsHdr[:]), data)) + data.Release() + applicationDataCount++ + } else { + _, err = io.Copy(dst, io.MultiReader(bytes.NewReader(tlsHdr[:]), io.LimitReader(src, int64(length)))) + } + if err != nil { + return nil, err + } + if applicationDataCount > 3 { + return nil, os.ErrPermission + } + } +} diff --git a/option/shadowtls.go b/option/shadowtls.go index 4440aa69..7c15c52b 100644 --- a/option/shadowtls.go +++ b/option/shadowtls.go @@ -2,6 +2,8 @@ package option type ShadowTLSInboundOptions struct { ListenOptions + Version int `json:"version,omitempty"` + Password string `json:"password,omitempty"` Handshake ShadowTLSHandshakeOptions `json:"handshake"` } @@ -13,5 +15,7 @@ type ShadowTLSHandshakeOptions struct { type ShadowTLSOutboundOptions struct { DialerOptions ServerOptions - TLS *OutboundTLSOptions `json:"tls,omitempty"` + Version int `json:"version,omitempty"` + Password string `json:"password,omitempty"` + TLS *OutboundTLSOptions `json:"tls,omitempty"` } diff --git a/outbound/shadowtls.go b/outbound/shadowtls.go index d971faa9..60bcd985 100644 --- a/outbound/shadowtls.go +++ b/outbound/shadowtls.go @@ -11,7 +11,9 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/shadowtls" "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" ) @@ -23,6 +25,8 @@ type ShadowTLS struct { dialer N.Dialer serverAddr M.Socksaddr tlsConfig tls.Config + v2 bool + password string } func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSOutboundOptions) (*ShadowTLS, error) { @@ -36,12 +40,22 @@ func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.Context }, dialer: dialer.New(router, options.DialerOptions), serverAddr: options.ServerOptions.Build(), + password: options.Password, } if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired } - options.TLS.MinVersion = "1.2" - options.TLS.MaxVersion = "1.2" + switch options.Version { + case 0: + fallthrough + case 1: + options.TLS.MinVersion = "1.2" + options.TLS.MaxVersion = "1.2" + case 2: + outbound.v2 = true + default: + return nil, E.New("unknown shadowtls protocol version: ", options.Version) + } var err error outbound.tlsConfig, err = tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS)) if err != nil { @@ -60,11 +74,20 @@ func (s *ShadowTLS) DialContext(ctx context.Context, network string, destination if err != nil { return nil, err } - _, err = tls.ClientHandshake(ctx, conn, s.tlsConfig) - if err != nil { - return nil, err + if !s.v2 { + _, err = tls.ClientHandshake(ctx, conn, s.tlsConfig) + if err != nil { + return nil, err + } + return conn, nil + } else { + hashConn := shadowtls.NewHashReadConn(conn, s.password) + _, err = tls.ClientHandshake(ctx, hashConn, s.tlsConfig) + if err != nil { + return nil, err + } + return shadowtls.NewClientConn(hashConn), nil } - return conn, nil } func (s *ShadowTLS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { diff --git a/test/go.mod b/test/go.mod index 4ad70b27..dd2b2eaa 100644 --- a/test/go.mod +++ b/test/go.mod @@ -10,12 +10,12 @@ require ( github.com/docker/docker v20.10.18+incompatible github.com/docker/go-connections v0.4.0 github.com/gofrs/uuid v4.3.0+incompatible - github.com/sagernet/sing v0.0.0-20220930130214-cb9b17d6a4a7 + github.com/sagernet/sing v0.0.0-20221006081821-c4e9bf11fa00 github.com/sagernet/sing-shadowsocks v0.0.0-20220819002358-7461bb09a8f6 github.com/spyzhov/ajson v0.7.1 github.com/stretchr/testify v1.8.0 go.uber.org/goleak v1.2.0 - golang.org/x/net v0.0.0-20220927171203-f486391704dc + golang.org/x/net v0.0.0-20221004154528-8021a29435af ) //replace github.com/sagernet/sing => ../../sing @@ -26,10 +26,10 @@ require ( github.com/Microsoft/go-winio v0.5.1 // indirect github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.4 // indirect - github.com/caddyserver/certmagic v0.17.1 // indirect + github.com/caddyserver/certmagic v0.17.2 // indirect github.com/cloudflare/circl v1.2.1-0.20220831060716-4cf0150356fc // indirect github.com/cretz/bine v0.2.0 // indirect - github.com/database64128/tfo-go v1.1.2 // indirect + github.com/database64128/tfo-go/v2 v2.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/go-units v0.4.0 // indirect @@ -44,7 +44,7 @@ require ( github.com/google/btree v1.0.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/klauspost/compress v1.13.6 // indirect - github.com/klauspost/cpuid/v2 v2.1.0 // indirect + github.com/klauspost/cpuid/v2 v2.1.1 // indirect github.com/libdns/libdns v0.2.1 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/marten-seemann/qpack v0.2.1 // indirect @@ -68,7 +68,7 @@ require ( github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb // indirect github.com/sagernet/sing-dns v0.0.0-20220929010544-ee843807aae3 // indirect - github.com/sagernet/sing-tun v0.0.0-20220929163559-a93592a9b581 // indirect + github.com/sagernet/sing-tun v0.0.0-20221005115555-9a556307f6a3 // indirect github.com/sagernet/sing-vmess v0.0.0-20220925083655-063bc85ea685 // indirect github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195 // indirect github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e // indirect @@ -77,9 +77,9 @@ require ( go.etcd.io/bbolt v1.3.6 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.22.0 // indirect + go.uber.org/zap v1.23.0 // indirect go4.org/netipx v0.0.0-20220925034521-797b0c90d8ab // indirect - golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect + golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect diff --git a/test/go.sum b/test/go.sum index 6a588505..15ca3f9b 100644 --- a/test/go.sum +++ b/test/go.sum @@ -16,8 +16,8 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/caddyserver/certmagic v0.17.1 h1:VrWANhQAj3brK7jAUKyN6XBHg56WsyorI/84Ilq1tCQ= -github.com/caddyserver/certmagic v0.17.1/go.mod h1:pSS2aZcdKlrTZrb2DKuRafckx20o5Fz1EdDKEB8KOQM= +github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE= +github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.2.1-0.20220831060716-4cf0150356fc h1:307gdRLiZ08dwOIKwc5lAQ19DRFaQQvdhHalyB4Asx8= @@ -29,8 +29,8 @@ github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= -github.com/database64128/tfo-go v1.1.2 h1:GwxtJp09BdUTVEoeT421t231eNZoGOCRkklbl4WI1kU= -github.com/database64128/tfo-go v1.1.2/go.mod h1:jgrSUPyOvTGQyn6irCOpk7L2W/q/0VLZZcovQiMi+bI= +github.com/database64128/tfo-go/v2 v2.0.1 h1:VFfturVoq6NmPGfhXO1K15x82KR19aAfw1RYtTzyVv4= +github.com/database64128/tfo-go/v2 v2.0.1/go.mod h1:FDdt4JaAsRU66wsYHxSVytYimPkKIHupVsxM+5DhvjY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -102,8 +102,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= -github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0= +github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -165,14 +165,14 @@ github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb h1:wc0yQ+SBn4TaTY github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb/go.mod h1:MIccjRKnPTjWwAOpl+AUGWOkzyTd9tERytudxu+1ra4= github.com/sagernet/sing v0.0.0-20220812082120-05f9836bff8f/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= -github.com/sagernet/sing v0.0.0-20220930130214-cb9b17d6a4a7 h1:VQqdVA6/ctKVOeKHKy9ZYMeB7TqxnP0jvK/Ig9Y+pG8= -github.com/sagernet/sing v0.0.0-20220930130214-cb9b17d6a4a7/go.mod h1:zvgDYKI+vCAW9RyfyrKTgleI+DOa8lzHMPC7VZo3OL4= +github.com/sagernet/sing v0.0.0-20221006081821-c4e9bf11fa00 h1:UkgEDnH3L9eBxob+3AbE9wM4mjFnRLnaPjfLSNe+C74= +github.com/sagernet/sing v0.0.0-20221006081821-c4e9bf11fa00/go.mod h1:zvgDYKI+vCAW9RyfyrKTgleI+DOa8lzHMPC7VZo3OL4= github.com/sagernet/sing-dns v0.0.0-20220929010544-ee843807aae3 h1:AEdyJxEDFq38z0pBX/0MpikQapGMIch+1ADe9k1bJqU= github.com/sagernet/sing-dns v0.0.0-20220929010544-ee843807aae3/go.mod h1:SrvWLfOSlnFmH32CWXicfilAGgIXR0VjrH6yRbuXYww= github.com/sagernet/sing-shadowsocks v0.0.0-20220819002358-7461bb09a8f6 h1:JJfDeYYhWunvtxsU/mOVNTmFQmnzGx9dY034qG6G3g4= github.com/sagernet/sing-shadowsocks v0.0.0-20220819002358-7461bb09a8f6/go.mod h1:EX3RbZvrwAkPI2nuGa78T2iQXmrkT+/VQtskjou42xM= -github.com/sagernet/sing-tun v0.0.0-20220929163559-a93592a9b581 h1:ghpmEsGVdFQys5jxy01aVQvhbbUT2c4kJRZXHZBNerw= -github.com/sagernet/sing-tun v0.0.0-20220929163559-a93592a9b581/go.mod h1:qbqV9lwcXJnj1Tw4we7oA6Z8zGE/kCXQBCzuhzRWVw8= +github.com/sagernet/sing-tun v0.0.0-20221005115555-9a556307f6a3 h1:9Igu/lgB1na+YTSEX6/YtPugAlMRyxLCDb7X+I0gdAE= +github.com/sagernet/sing-tun v0.0.0-20221005115555-9a556307f6a3/go.mod h1:qbqV9lwcXJnj1Tw4we7oA6Z8zGE/kCXQBCzuhzRWVw8= github.com/sagernet/sing-vmess v0.0.0-20220925083655-063bc85ea685 h1:AZzFNRR/ZwMTceUQ1b/mxx6oyKqmFymdMn/yleJmoVM= github.com/sagernet/sing-vmess v0.0.0-20220925083655-063bc85ea685/go.mod h1:bwhAdSNET1X+j9DOXGj9NIQR39xgcWIk1rOQ9lLD+gM= github.com/sagernet/smux v0.0.0-20220831015742-e0f1988e3195 h1:5VBIbVw9q7aKbrFdT83mjkyvQ+VaRsQ6yflTepfln38= @@ -211,8 +211,8 @@ go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -go.uber.org/zap v1.22.0 h1:Zcye5DUgBloQ9BaT4qc9BnjOFog5TvBSAGkJ3Nf70c0= -go.uber.org/zap v1.22.0/go.mod h1:H4siCOZOrAolnUPJEkfaSjDqyP+BDS0DdDWzwcgt3+U= +go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= go4.org/netipx v0.0.0-20220925034521-797b0c90d8ab h1:+yW1yrZ09EYNu1spCUOHBBNRbrLnfmutwyhbhCv3b6Q= go4.org/netipx v0.0.0-20220925034521-797b0c90d8ab/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -221,8 +221,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A= -golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0= +golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= @@ -257,8 +257,8 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211111160137-58aab5ef257a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ= -golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4= +golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/test/shadowtls_test.go b/test/shadowtls_test.go index 8a02febb..d20b1b24 100644 --- a/test/shadowtls_test.go +++ b/test/shadowtls_test.go @@ -1,6 +1,9 @@ package main import ( + "context" + "net" + "net/http" "net/netip" "testing" @@ -8,11 +11,22 @@ import ( "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" ) func TestShadowTLS(t *testing.T) { + t.Run("v1", func(t *testing.T) { + testShadowTLS(t, "") + }) + t.Run("v2", func(t *testing.T) { + testShadowTLS(t, "hello") + }) +} + +func testShadowTLS(t *testing.T, password string) { method := shadowaead_2022.List[0] - password := mkBase64(t, 16) + ssPassword := mkBase64(t, 16) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { @@ -39,6 +53,121 @@ func TestShadowTLS(t *testing.T) { ServerPort: 443, }, }, + Password: password, + }, + }, + { + Type: C.TypeShadowsocks, + Tag: "detour", + ShadowsocksOptions: option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: otherPort, + }, + Method: method, + Password: ssPassword, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeShadowsocks, + ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Method: method, + Password: ssPassword, + DialerOptions: option.DialerOptions{ + Detour: "detour", + }, + MultiplexOptions: &option.MultiplexOptions{ + Enabled: true, + }, + }, + }, + { + Type: C.TypeShadowTLS, + Tag: "detour", + ShadowTLSOptions: option.ShadowTLSOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + }, + Password: password, + }, + }, + { + Type: C.TypeDirect, + Tag: "direct", + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{{ + DefaultOptions: option.DefaultRule{ + Inbound: []string{"detour"}, + Outbound: "direct", + }, + }}, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestShadowTLSv2Fallback(t *testing.T) { + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeShadowTLS, + ShadowTLSOptions: option.ShadowTLSInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: serverPort, + }, + Handshake: option.ShadowTLSHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "google.com", + ServerPort: 443, + }, + }, + Password: "hello", + }, + }, + }, + }) + client := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, network, "127.0.0.1:"+F.ToString(serverPort)) + }, + }, + } + response, err := client.Get("https://google.com") + require.NoError(t, err) + require.Equal(t, response.StatusCode, 200) + client.CloseIdleConnections() +} + +func TestShadowTLSOutbound(t *testing.T) { + method := shadowaead_2022.List[0] + password := mkBase64(t, 16) + startDockerContainer(t, DockerOptions{ + Image: ImageShadowTLS, + Ports: []uint16{serverPort, otherPort}, + EntryPoint: "shadow-tls", + Cmd: []string{"--threads", "1", "server", "--listen", "0.0.0.0:" + F.ToString(serverPort), "--server", "127.0.0.1:" + F.ToString(otherPort), "--tls", "google.com:443", "--password", "hello"}, + }) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + MixedOptions: option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: clientPort, + }, }, }, { @@ -80,6 +209,7 @@ func TestShadowTLS(t *testing.T) { Enabled: true, ServerName: "google.com", }, + Password: "hello", }, }, { @@ -98,72 +228,3 @@ func TestShadowTLS(t *testing.T) { }) testSuit(t, clientPort, testPort) } - -func TestShadowTLSOutbound(t *testing.T) { - startDockerContainer(t, DockerOptions{ - Image: ImageShadowTLS, - Ports: []uint16{serverPort, otherPort}, - EntryPoint: "shadow-tls", - Cmd: []string{"--threads", "1", "server", "0.0.0.0:" + F.ToString(serverPort), "127.0.0.1:" + F.ToString(otherPort), "google.com:443"}, - }) - startInstance(t, option.Options{ - Inbounds: []option.Inbound{ - { - Type: C.TypeMixed, - MixedOptions: option.HTTPMixedInboundOptions{ - ListenOptions: option.ListenOptions{ - Listen: option.ListenAddress(netip.IPv4Unspecified()), - ListenPort: clientPort, - }, - }, - }, - { - Type: C.TypeMixed, - Tag: "detour", - MixedOptions: option.HTTPMixedInboundOptions{ - ListenOptions: option.ListenOptions{ - Listen: option.ListenAddress(netip.IPv4Unspecified()), - ListenPort: otherPort, - }, - }, - }, - }, - Outbounds: []option.Outbound{ - { - Type: C.TypeSocks, - SocksOptions: option.SocksOutboundOptions{ - DialerOptions: option.DialerOptions{ - Detour: "detour", - }, - }, - }, - { - Type: C.TypeShadowTLS, - Tag: "detour", - ShadowTLSOptions: option.ShadowTLSOutboundOptions{ - ServerOptions: option.ServerOptions{ - Server: "127.0.0.1", - ServerPort: serverPort, - }, - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "google.com", - }, - }, - }, - { - Type: C.TypeDirect, - Tag: "direct", - }, - }, - Route: &option.RouteOptions{ - Rules: []option.Rule{{ - DefaultOptions: option.DefaultRule{ - Inbound: []string{"detour"}, - Outbound: "direct", - }, - }}, - }, - }) - testTCP(t, clientPort, testPort) -} diff --git a/transport/shadowtls/client.go b/transport/shadowtls/client.go new file mode 100644 index 00000000..dfed2091 --- /dev/null +++ b/transport/shadowtls/client.go @@ -0,0 +1,40 @@ +package shadowtls + +import ( + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + N "github.com/sagernet/sing/common/network" +) + +var _ N.VectorisedWriter = (*ClientConn)(nil) + +type ClientConn struct { + *Conn + hashConn *HashReadConn +} + +func NewClientConn(hashConn *HashReadConn) *ClientConn { + return &ClientConn{NewConn(hashConn.Conn), hashConn} +} + +func (c *ClientConn) Write(p []byte) (n int, err error) { + if c.hashConn != nil { + sum := c.hashConn.Sum() + c.hashConn = nil + _, err = bufio.WriteVectorised(c.Conn, [][]byte{sum, p}) + if err == nil { + n = len(p) + } + return + } + return c.Conn.Write(p) +} + +func (c *ClientConn) WriteVectorised(buffers []*buf.Buffer) error { + if c.hashConn != nil { + sum := c.hashConn.Sum() + c.hashConn = nil + return c.Conn.WriteVectorised(append([]*buf.Buffer{buf.As(sum)}, buffers...)) + } + return c.Conn.WriteVectorised(buffers) +} diff --git a/transport/shadowtls/conn.go b/transport/shadowtls/conn.go new file mode 100644 index 00000000..0bb3ee8b --- /dev/null +++ b/transport/shadowtls/conn.go @@ -0,0 +1,96 @@ +package shadowtls + +import ( + "encoding/binary" + "io" + "net" + + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + N "github.com/sagernet/sing/common/network" +) + +var ( + _ N.ExtendedConn = (*Conn)(nil) + _ N.VectorisedWriter = (*Conn)(nil) +) + +type Conn struct { + N.ExtendedConn + writer N.VectorisedWriter + readRemaining int +} + +func NewConn(conn net.Conn) *Conn { + return &Conn{ + ExtendedConn: bufio.NewExtendedConn(conn), + writer: bufio.NewVectorisedWriter(conn), + } +} + +func (c *Conn) Read(p []byte) (n int, err error) { + if c.readRemaining > 0 { + if len(p) > c.readRemaining { + p = p[:c.readRemaining] + } + n, err = c.ExtendedConn.Read(p) + c.readRemaining -= n + return + } + var tlsHeader [5]byte + _, err = io.ReadFull(c.ExtendedConn, common.Dup(tlsHeader[:])) + if err != nil { + return + } + length := int(binary.BigEndian.Uint16(tlsHeader[3:5])) + readLen := len(p) + if readLen > length { + readLen = length + } + n, err = c.ExtendedConn.Read(p[:readLen]) + if err != nil { + return + } + c.readRemaining = length - n + return +} + +func (c *Conn) Write(p []byte) (n int, err error) { + var header [5]byte + defer common.KeepAlive(header) + header[0] = 23 + for len(p) > 16384 { + binary.BigEndian.PutUint16(header[1:3], tls.VersionTLS12) + binary.BigEndian.PutUint16(header[3:5], uint16(16384)) + _, err = bufio.WriteVectorised(c.writer, [][]byte{common.Dup(header[:]), p[:16384]}) + common.KeepAlive(header) + if err != nil { + return + } + n += 16384 + p = p[16384:] + } + binary.BigEndian.PutUint16(header[1:3], tls.VersionTLS12) + binary.BigEndian.PutUint16(header[3:5], uint16(len(p))) + _, err = bufio.WriteVectorised(c.writer, [][]byte{common.Dup(header[:]), p}) + if err == nil { + n += len(p) + } + return +} + +func (c *Conn) WriteVectorised(buffers []*buf.Buffer) error { + var header [5]byte + defer common.KeepAlive(header) + header[0] = 23 + dataLen := buf.LenMulti(buffers) + binary.BigEndian.PutUint16(header[1:3], tls.VersionTLS12) + binary.BigEndian.PutUint16(header[3:5], uint16(dataLen)) + return c.writer.WriteVectorised(append([]*buf.Buffer{buf.As(header[:])}, buffers...)) +} + +func (c *Conn) Upstream() any { + return c.ExtendedConn +} diff --git a/transport/shadowtls/hash.go b/transport/shadowtls/hash.go new file mode 100644 index 00000000..0706f865 --- /dev/null +++ b/transport/shadowtls/hash.go @@ -0,0 +1,60 @@ +package shadowtls + +import ( + "crypto/hmac" + "crypto/sha1" + "hash" + "net" +) + +type HashReadConn struct { + net.Conn + hmac hash.Hash +} + +func NewHashReadConn(conn net.Conn, password string) *HashReadConn { + return &HashReadConn{ + conn, + hmac.New(sha1.New, []byte(password)), + } +} + +func (c *HashReadConn) Read(b []byte) (n int, err error) { + n, err = c.Conn.Read(b) + if err != nil { + return + } + _, err = c.hmac.Write(b[:n]) + return +} + +func (c *HashReadConn) Sum() []byte { + return c.hmac.Sum(nil)[:8] +} + +type HashWriteConn struct { + net.Conn + hmac hash.Hash +} + +func NewHashWriteConn(conn net.Conn, password string) *HashWriteConn { + return &HashWriteConn{ + conn, + hmac.New(sha1.New, []byte(password)), + } +} + +func (c *HashWriteConn) Write(p []byte) (n int, err error) { + if c.hmac != nil { + c.hmac.Write(p) + } + return c.Conn.Write(p) +} + +func (c *HashWriteConn) Sum() []byte { + return c.hmac.Sum(nil)[:8] +} + +func (c *HashWriteConn) Fallback() { + c.hmac = nil +}