瀏覽代碼

Add shadowtls (#49)

* Add shadowtls outbound

* Add shadowtls inbound

* Add shadowtls example

* Add shadowtls documentation
世界 3 年之前
父節點
當前提交
5813e0ce7a

+ 1 - 0
constant/proxy.go

@@ -18,6 +18,7 @@ const (
 	TypeHysteria    = "hysteria"
 	TypeTor         = "tor"
 	TypeSSH         = "ssh"
+	TypeShadowTLS   = "shadowtls"
 )
 
 const (

+ 31 - 0
docs/configuration/inbound/shadowtls.md

@@ -0,0 +1,31 @@
+### Structure
+
+```json
+{
+  "type": "shadowtls",
+  "tag": "st-in",
+
+  ... // Listen Fields
+
+  "handshake": {
+    "server": "google.com",
+    "server_port": 443,
+    
+    ... // Dial Fields
+  }
+}
+```
+
+### Listen Fields
+
+See [Listen Fields](/configuration/shared/listen) for details.
+
+
+### Fields
+
+#### handshake
+
+==Required==
+
+Handshake server address and [dial options](/configuration/shared/dial).
+

+ 29 - 0
docs/configuration/inbound/shadowtls.zh.md

@@ -0,0 +1,29 @@
+### 结构
+
+```json
+{
+  "type": "shadowtls",
+  "tag": "st-in",
+
+  ... // 监听字段
+
+  "handshake": {
+    "server": "google.com",
+    "server_port": 443,
+
+    ... // 拨号字段
+  }
+}
+```
+
+### 监听字段
+
+参阅 [监听字段](/zh/configuration/shared/listen/)。
+
+### 字段
+
+#### handshake
+
+==必填==
+
+握手服务器地址和 [拨号参数](/zh/configuration/shared/dial/)。

+ 38 - 0
docs/configuration/outbound/shadowtls.md

@@ -0,0 +1,38 @@
+### Structure
+
+```json
+{
+  "type": "shadowtls",
+  "tag": "st-out",
+  
+  "server": "127.0.0.1",
+  "server_port": 1080,
+  "tls": {},
+
+  ... // Dial Fields
+}
+```
+
+### Fields
+
+#### server
+
+==Required==
+
+The server address.
+
+#### server_port
+
+==Required==
+
+The server port.
+
+#### tls
+
+==Required==
+
+TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
+
+### Dial Fields
+
+See [Dial Fields](/configuration/shared/dial) for details.

+ 38 - 0
docs/configuration/outbound/shadowtls.zh.md

@@ -0,0 +1,38 @@
+### 结构
+
+```json
+{
+  "type": "shadowtls",
+  "tag": "st-out",
+  
+  "server": "127.0.0.1",
+  "server_port": 1080,
+  "tls": {},
+
+  ... // 拨号字段
+}
+```
+
+### 字段
+
+#### server
+
+==必填==
+
+服务器地址。
+
+#### server_port
+
+==必填==
+
+服务器端口。
+
+#### tls
+
+==必填==
+
+TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
+
+### 拨号字段
+
+参阅 [拨号字段](/zh/configuration/shared/dial/)。

+ 55 - 0
docs/examples/shadowtls.md

@@ -0,0 +1,55 @@
+#### Server
+
+```json
+{
+  "inbounds": [
+    {
+      "type": "shadowtls",
+      "listen": "::",
+      "listen_port": 4443,
+      "handshake": {
+        "server": "google.com",
+        "server_port": 443
+      },
+      "detour": "shadowsocks-in"
+    },
+    {
+      "type": "shadowsocks",
+      "tag": "shadowsocks-in",
+      "listen": "127.0.0.1",
+      "method": "2022-blake3-aes-128-gcm",
+      "password": "8JCsPssfgS8tiRwiMlhARg=="
+    }
+  ]
+}
+```
+
+#### Client
+
+```json
+{
+  "outbounds": [
+    {
+      "type": "shadowsocks",
+      "method": "2022-blake3-aes-128-gcm",
+      "password": "8JCsPssfgS8tiRwiMlhARg==",
+      "detour": "shadowtls-out",
+      "multiplex": {
+        "enabled": 1,
+        "max_connections": 4,
+        "min_streams": 4
+      }
+    },
+    {
+      "type": "shadowtls",
+      "tag": "shadowtls-out",
+      "server": "127.0.0.1",
+      "server_port": 4443,
+      "tls": {
+        "enabled": true,
+        "server_name": "google.com"
+      }
+    }
+  ]
+}
+```

+ 2 - 0
inbound/builder.go

@@ -39,6 +39,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
 		return NewNaive(ctx, router, logger, options.Tag, options.NaiveOptions)
 	case C.TypeHysteria:
 		return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions)
+	case C.TypeShadowTLS:
+		return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions)
 	default:
 		return nil, E.New("unknown inbound type: ", options.Type)
 	}

+ 0 - 12
inbound/shadowsocks.go

@@ -87,15 +87,3 @@ func (h *Shadowsocks) NewPacket(ctx context.Context, conn N.PacketConn, buffer *
 func (h *Shadowsocks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
 	return os.ErrInvalid
 }
-
-func (h *Shadowsocks) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
-	h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
-	return h.router.RouteConnection(ctx, conn, metadata)
-}
-
-func (h *Shadowsocks) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
-	ctx = log.ContextWithNewID(ctx)
-	h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
-	h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
-	return h.router.RoutePacketConnection(ctx, conn, metadata)
-}

+ 93 - 0
inbound/shadowtls.go

@@ -0,0 +1,93 @@
+package inbound
+
+import (
+	"bytes"
+	"context"
+	"encoding/binary"
+	"io"
+	"net"
+
+	"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"
+	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/task"
+)
+
+type ShadowTLS struct {
+	myInboundAdapter
+	handshakeDialer N.Dialer
+	handshakeAddr   M.Socksaddr
+}
+
+func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSInboundOptions) (*ShadowTLS, error) {
+	inbound := &ShadowTLS{
+		myInboundAdapter: myInboundAdapter{
+			protocol:      C.TypeShadowTLS,
+			network:       []string{N.NetworkTCP},
+			ctx:           ctx,
+			router:        router,
+			logger:        logger,
+			tag:           tag,
+			listenOptions: options.ListenOptions,
+		},
+		handshakeDialer: dialer.New(router, options.Handshake.DialerOptions),
+		handshakeAddr:   options.Handshake.ServerOptions.Build(),
+	}
+	inbound.connHandler = inbound
+	return inbound, nil
+}
+
+func (s *ShadowTLS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	handshakeConn, err := s.handshakeDialer.DialContext(ctx, N.NetworkTCP, s.handshakeAddr)
+	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
+	}
+	return s.newConnection(ctx, conn, metadata)
+}
+
+func (s *ShadowTLS) copyUntilHandshakeFinished(dst io.Writer, src io.Reader) error {
+	const handshake = 0x16
+	const changeCipherSpec = 0x14
+	var hasSeenChangeCipherSpec bool
+	var tlsHdr [5]byte
+	for {
+		_, err := io.ReadFull(src, tlsHdr[:])
+		if err != nil {
+			return err
+		}
+		length := binary.BigEndian.Uint16(tlsHdr[3:])
+		_, err = io.Copy(dst, io.MultiReader(bytes.NewReader(tlsHdr[:]), io.LimitReader(src, int64(length))))
+		if err != nil {
+			return err
+		}
+		if tlsHdr[0] != handshake {
+			if tlsHdr[0] != changeCipherSpec {
+				return E.New("unexpected tls frame type: ", tlsHdr[0])
+			}
+			if !hasSeenChangeCipherSpec {
+				hasSeenChangeCipherSpec = true
+				continue
+			}
+		}
+		if hasSeenChangeCipherSpec {
+			return nil
+		}
+	}
+}

+ 3 - 0
mkdocs.yml

@@ -68,6 +68,7 @@ nav:
           - Trojan: configuration/inbound/trojan.md
           - Naive: configuration/inbound/naive.md
           - Hysteria: configuration/inbound/hysteria.md
+          - ShadowTLS: configuration/inbound/shadowtls.md
           - Tun: configuration/inbound/tun.md
           - Redirect: configuration/inbound/redirect.md
           - TProxy: configuration/inbound/tproxy.md
@@ -82,6 +83,7 @@ nav:
           - Trojan: configuration/outbound/trojan.md
           - WireGuard: configuration/outbound/wireguard.md
           - Hysteria: configuration/outbound/hysteria.md
+          - ShadowTLS: configuration/outbound/shadowtls.md
           - Tor: configuration/outbound/tor.md
           - SSH: configuration/outbound/ssh.md
           - DNS: configuration/outbound/dns.md
@@ -95,6 +97,7 @@ nav:
       - Shadowsocks Server: examples/ss-server.md
       - Shadowsocks Client: examples/ss-client.md
       - Shadowsocks Tun: examples/ss-tun.md
+      - ShadowTLS: examples/shadowtls.md
       - DNS Hijack: examples/dns-hijack.md
   - Contributing:
       - contributing/index.md

+ 5 - 0
option/inbound.go

@@ -21,6 +21,7 @@ type _Inbound struct {
 	TrojanOptions      TrojanInboundOptions      `json:"-"`
 	NaiveOptions       NaiveInboundOptions       `json:"-"`
 	HysteriaOptions    HysteriaInboundOptions    `json:"-"`
+	ShadowTLSOptions   ShadowTLSInboundOptions   `json:"-"`
 }
 
 type Inbound _Inbound
@@ -52,6 +53,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
 		v = h.NaiveOptions
 	case C.TypeHysteria:
 		v = h.HysteriaOptions
+	case C.TypeShadowTLS:
+		v = h.ShadowTLSOptions
 	default:
 		return nil, E.New("unknown inbound type: ", h.Type)
 	}
@@ -89,6 +92,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
 		v = &h.NaiveOptions
 	case C.TypeHysteria:
 		v = &h.HysteriaOptions
+	case C.TypeShadowTLS:
+		v = &h.ShadowTLSOptions
 	default:
 		return E.New("unknown inbound type: ", h.Type)
 	}

+ 5 - 0
option/outbound.go

@@ -20,6 +20,7 @@ type _Outbound struct {
 	HysteriaOptions    HysteriaOutboundOptions    `json:"-"`
 	TorOptions         TorOutboundOptions         `json:"-"`
 	SSHOptions         SSHOutboundOptions         `json:"-"`
+	ShadowTLSOptions   ShadowTLSOutboundOptions   `json:"-"`
 	SelectorOptions    SelectorOutboundOptions    `json:"-"`
 }
 
@@ -50,6 +51,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
 		v = h.TorOptions
 	case C.TypeSSH:
 		v = h.SSHOptions
+	case C.TypeShadowTLS:
+		v = h.ShadowTLSOptions
 	case C.TypeSelector:
 		v = h.SelectorOptions
 	default:
@@ -87,6 +90,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
 		v = &h.TorOptions
 	case C.TypeSSH:
 		v = &h.SSHOptions
+	case C.TypeShadowTLS:
+		v = &h.ShadowTLSOptions
 	case C.TypeSelector:
 		v = &h.SelectorOptions
 	default:

+ 17 - 0
option/shadowtls.go

@@ -0,0 +1,17 @@
+package option
+
+type ShadowTLSInboundOptions struct {
+	ListenOptions
+	Handshake ShadowTLSHandshakeOptions `json:"handshake"`
+}
+
+type ShadowTLSHandshakeOptions struct {
+	ServerOptions
+	DialerOptions
+}
+
+type ShadowTLSOutboundOptions struct {
+	OutboundDialerOptions
+	ServerOptions
+	TLS *OutboundTLSOptions `json:"tls,omitempty"`
+}

+ 2 - 0
outbound/builder.go

@@ -39,6 +39,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
 		return NewTor(ctx, router, logger, options.Tag, options.TorOptions)
 	case C.TypeSSH:
 		return NewSSH(ctx, router, logger, options.Tag, options.SSHOptions)
+	case C.TypeShadowTLS:
+		return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions)
 	case C.TypeSelector:
 		return NewSelector(router, logger, options.Tag, options.SelectorOptions)
 	default:

+ 84 - 0
outbound/shadowtls.go

@@ -0,0 +1,84 @@
+package outbound
+
+import (
+	"context"
+	"crypto/tls"
+	"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/common"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+var _ adapter.Outbound = (*ShadowTLS)(nil)
+
+type ShadowTLS struct {
+	myOutboundAdapter
+	dialer     N.Dialer
+	serverAddr M.Socksaddr
+	tlsConfig  *tls.Config
+}
+
+func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSOutboundOptions) (*ShadowTLS, error) {
+	outbound := &ShadowTLS{
+		myOutboundAdapter: myOutboundAdapter{
+			protocol: C.TypeShadowTLS,
+			network:  []string{N.NetworkTCP},
+			router:   router,
+			logger:   logger,
+			tag:      tag,
+		},
+		dialer:     dialer.NewOutbound(router, options.OutboundDialerOptions),
+		serverAddr: options.ServerOptions.Build(),
+	}
+	if options.TLS == nil || !options.TLS.Enabled {
+		return nil, C.ErrTLSRequired
+	}
+	options.TLS.MinVersion = "1.2"
+	options.TLS.MaxVersion = "1.2"
+	var err error
+	outbound.tlsConfig, err = dialer.TLSConfig(options.Server, common.PtrValueOrDefault(options.TLS))
+	if err != nil {
+		return nil, err
+	}
+	return outbound, nil
+}
+
+func (s *ShadowTLS) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+	switch N.NetworkName(network) {
+	case N.NetworkTCP:
+	default:
+		return nil, os.ErrInvalid
+	}
+	conn, err := s.dialer.DialContext(ctx, N.NetworkTCP, s.serverAddr)
+	if err != nil {
+		return nil, err
+	}
+	tlsConn, err := dialer.TLSClient(ctx, conn, s.tlsConfig)
+	if err != nil {
+		return nil, err
+	}
+	err = tlsConn.HandshakeContext(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return conn, nil
+}
+
+func (s *ShadowTLS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+	return nil, os.ErrInvalid
+}
+
+func (s *ShadowTLS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	return NewConnection(ctx, s, conn, metadata)
+}
+
+func (s *ShadowTLS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	return os.ErrInvalid
+}

+ 4 - 2
route/router.go

@@ -519,8 +519,9 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
 		if !common.Contains(injectable.Network(), N.NetworkTCP) {
 			return E.New("inject: TCP unsupported")
 		}
-		metadata.InboundDetour = ""
 		metadata.LastInbound = metadata.Inbound
+		metadata.Inbound = metadata.InboundDetour
+		metadata.InboundDetour = ""
 		err := injectable.NewConnection(ctx, conn, metadata)
 		if err != nil {
 			return E.Cause(err, "inject ", detour.Tag())
@@ -599,8 +600,9 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
 		if !common.Contains(injectable.Network(), N.NetworkUDP) {
 			return E.New("inject: UDP unsupported")
 		}
-		metadata.InboundDetour = ""
 		metadata.LastInbound = metadata.Inbound
+		metadata.Inbound = metadata.InboundDetour
+		metadata.InboundDetour = ""
 		err := injectable.NewPacketConnection(ctx, conn, metadata)
 		if err != nil {
 			return E.Cause(err, "inject ", detour.Tag())

+ 6 - 0
test/box_test.go

@@ -9,6 +9,7 @@ import (
 	"github.com/sagernet/sing-box"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common/bufio"
+	"github.com/sagernet/sing/common/debug"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/protocol/socks"
@@ -17,6 +18,11 @@ import (
 )
 
 func startInstance(t *testing.T, options option.Options) {
+	if debug.Enabled {
+		options.Log = &option.LogOptions{
+			Level: "trace",
+		}
+	}
 	var instance *box.Box
 	var err error
 	for retry := 0; retry < 3; retry++ {

+ 3 - 1
test/clash_test.go

@@ -36,6 +36,7 @@ const (
 	ImageBoringTun             = "ghcr.io/ntkme/boringtun:edge"
 	ImageHysteria              = "tobyxdd/hysteria:latest"
 	ImageNginx                 = "nginx:stable"
+	ImageShadowTLS             = "ghcr.io/ihciah/shadow-tls:latest"
 )
 
 var allImages = []string{
@@ -46,7 +47,8 @@ var allImages = []string{
 	ImageNaive,
 	ImageBoringTun,
 	ImageHysteria,
-	// ImageNginx,
+	ImageNginx,
+	ImageShadowTLS,
 }
 
 var localIP = netip.MustParseAddr("127.0.0.1")

+ 170 - 0
test/shadowtls_test.go

@@ -0,0 +1,170 @@
+package main
+
+import (
+	"net/netip"
+	"testing"
+
+	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"
+)
+
+func TestShadowTLS(t *testing.T) {
+	method := shadowaead_2022.List[0]
+	password := mkBase64(t, 16)
+	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.TypeShadowTLS,
+				Tag:  "in",
+				ShadowTLSOptions: option.ShadowTLSInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+						Detour:     "detour",
+					},
+					Handshake: option.ShadowTLSHandshakeOptions{
+						ServerOptions: option.ServerOptions{
+							Server:     "google.com",
+							ServerPort: 443,
+						},
+					},
+				},
+			},
+			{
+				Type: C.TypeShadowsocks,
+				Tag:  "detour",
+				ShadowsocksOptions: option.ShadowsocksInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: otherPort,
+					},
+					Method:   method,
+					Password: password,
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeShadowsocks,
+				ShadowsocksOptions: option.ShadowsocksOutboundOptions{
+					Method:   method,
+					Password: password,
+					OutboundDialerOptions: option.OutboundDialerOptions{
+						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)
+}
+
+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{
+					OutboundDialerOptions: option.OutboundDialerOptions{
+						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)
+}