瀏覽代碼

Add shadowtls v2 support

世界 3 年之前
父節點
當前提交
b0ad9bb6f1
共有 11 個文件被更改,包括 412 次插入55 次删除
  1. 1 1
      go.mod
  2. 2 2
      go.sum
  3. 85 12
      inbound/shadowtls.go
  4. 5 1
      option/shadowtls.go
  5. 29 6
      outbound/shadowtls.go
  6. 8 8
      test/go.mod
  7. 16 16
      test/go.sum
  8. 70 9
      test/shadowtls_test.go
  9. 40 0
      transport/shadowtls/client.go
  10. 96 0
      transport/shadowtls/conn.go
  11. 60 0
      transport/shadowtls/hash.go

+ 1 - 1
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

+ 2 - 2
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=

+ 85 - 12
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
+		}
+	}
+}

+ 5 - 1
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"`
 }

+ 29 - 6
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) {

+ 8 - 8
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

+ 16 - 16
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=

+ 70 - 9
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,7 @@ func TestShadowTLS(t *testing.T) {
 							ServerPort: 443,
 						},
 					},
+					Password: password,
 				},
 			},
 			{
@@ -50,7 +65,7 @@ func TestShadowTLS(t *testing.T) {
 						ListenPort: otherPort,
 					},
 					Method:   method,
-					Password: password,
+					Password: ssPassword,
 				},
 			},
 		},
@@ -59,7 +74,7 @@ func TestShadowTLS(t *testing.T) {
 				Type: C.TypeShadowsocks,
 				ShadowsocksOptions: option.ShadowsocksOutboundOptions{
 					Method:   method,
-					Password: password,
+					Password: ssPassword,
 					DialerOptions: option.DialerOptions{
 						Detour: "detour",
 					},
@@ -80,6 +95,7 @@ func TestShadowTLS(t *testing.T) {
 						Enabled:    true,
 						ServerName: "google.com",
 					},
+					Password: password,
 				},
 			},
 			{
@@ -99,12 +115,49 @@ func TestShadowTLS(t *testing.T) {
 	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", "0.0.0.0:" + F.ToString(serverPort), "127.0.0.1:" + F.ToString(otherPort), "google.com:443"},
+		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{
@@ -118,23 +171,30 @@ func TestShadowTLSOutbound(t *testing.T) {
 				},
 			},
 			{
-				Type: C.TypeMixed,
+				Type: C.TypeShadowsocks,
 				Tag:  "detour",
-				MixedOptions: option.HTTPMixedInboundOptions{
+				ShadowsocksOptions: option.ShadowsocksInboundOptions{
 					ListenOptions: option.ListenOptions{
 						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
 						ListenPort: otherPort,
 					},
+					Method:   method,
+					Password: password,
 				},
 			},
 		},
 		Outbounds: []option.Outbound{
 			{
-				Type: C.TypeSocks,
-				SocksOptions: option.SocksOutboundOptions{
+				Type: C.TypeShadowsocks,
+				ShadowsocksOptions: option.ShadowsocksOutboundOptions{
+					Method:   method,
+					Password: password,
 					DialerOptions: option.DialerOptions{
 						Detour: "detour",
 					},
+					MultiplexOptions: &option.MultiplexOptions{
+						Enabled: true,
+					},
 				},
 			},
 			{
@@ -149,6 +209,7 @@ func TestShadowTLSOutbound(t *testing.T) {
 						Enabled:    true,
 						ServerName: "google.com",
 					},
+					Password: "hello",
 				},
 			},
 			{
@@ -165,5 +226,5 @@ func TestShadowTLSOutbound(t *testing.T) {
 			}},
 		},
 	})
-	testTCP(t, clientPort, testPort)
+	testSuit(t, clientPort, testPort)
 }

+ 40 - 0
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)
+}

+ 96 - 0
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
+}

+ 60 - 0
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
+}