浏览代码

Add TLS record fragment support

世界 5 月之前
父节点
当前提交
88babbf3a7

+ 1 - 0
adapter/inbound.go

@@ -74,6 +74,7 @@ type InboundContext struct {
 	UDPTimeout                time.Duration
 	TLSFragment               bool
 	TLSFragmentFallbackDelay  time.Duration
+	TLSRecordFragment         bool
 
 	NetworkStrategy     *C.NetworkStrategy
 	NetworkType         []C.InterfaceType

+ 31 - 9
common/tlsfragment/conn.go

@@ -1,7 +1,9 @@
 package tf
 
 import (
+	"bytes"
 	"context"
+	"encoding/binary"
 	"math/rand"
 	"net"
 	"strings"
@@ -17,17 +19,19 @@ type Conn struct {
 	tcpConn            *net.TCPConn
 	ctx                context.Context
 	firstPacketWritten bool
+	splitRecord        bool
 	fallbackDelay      time.Duration
 }
 
-func NewConn(conn net.Conn, ctx context.Context, fallbackDelay time.Duration) (*Conn, error) {
+func NewConn(conn net.Conn, ctx context.Context, splitRecord bool, fallbackDelay time.Duration) *Conn {
 	tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn)
 	return &Conn{
 		Conn:          conn,
 		tcpConn:       tcpConn,
 		ctx:           ctx,
+		splitRecord:   splitRecord,
 		fallbackDelay: fallbackDelay,
-	}, nil
+	}
 }
 
 func (c *Conn) Write(b []byte) (n int, err error) {
@@ -37,10 +41,12 @@ func (c *Conn) Write(b []byte) (n int, err error) {
 		}()
 		serverName := indexTLSServerName(b)
 		if serverName != nil {
-			if c.tcpConn != nil {
-				err = c.tcpConn.SetNoDelay(true)
-				if err != nil {
-					return
+			if !c.splitRecord {
+				if c.tcpConn != nil {
+					err = c.tcpConn.SetNoDelay(true)
+					if err != nil {
+						return
+					}
 				}
 			}
 			splits := strings.Split(serverName.ServerName, ".")
@@ -61,16 +67,25 @@ func (c *Conn) Write(b []byte) (n int, err error) {
 					currentIndex++
 				}
 			}
+			var buffer bytes.Buffer
 			for i := 0; i <= len(splitIndexes); i++ {
 				var payload []byte
 				if i == 0 {
 					payload = b[:splitIndexes[i]]
+					if c.splitRecord {
+						payload = payload[recordLayerHeaderLen:]
+					}
 				} else if i == len(splitIndexes) {
 					payload = b[splitIndexes[i-1]:]
 				} else {
 					payload = b[splitIndexes[i-1]:splitIndexes[i]]
 				}
-				if c.tcpConn != nil && i != len(splitIndexes) {
+				if c.splitRecord {
+					payloadLen := uint16(len(payload))
+					buffer.Write(b[:3])
+					binary.Write(&buffer, binary.BigEndian, payloadLen)
+					buffer.Write(payload)
+				} else if c.tcpConn != nil && i != len(splitIndexes) {
 					err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay)
 					if err != nil {
 						return
@@ -82,11 +97,18 @@ func (c *Conn) Write(b []byte) (n int, err error) {
 					}
 				}
 			}
-			if c.tcpConn != nil {
-				err = c.tcpConn.SetNoDelay(false)
+			if c.splitRecord {
+				_, err = c.tcpConn.Write(buffer.Bytes())
 				if err != nil {
 					return
 				}
+			} else {
+				if c.tcpConn != nil {
+					err = c.tcpConn.SetNoDelay(false)
+					if err != nil {
+						return
+					}
+				}
 			}
 			return len(b), nil
 		}

+ 32 - 0
common/tlsfragment/conn_test.go

@@ -0,0 +1,32 @@
+package tf_test
+
+import (
+	"context"
+	"crypto/tls"
+	"net"
+	"testing"
+
+	tf "github.com/sagernet/sing-box/common/tlsfragment"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestTLSFragment(t *testing.T) {
+	t.Parallel()
+	tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
+	require.NoError(t, err)
+	tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), false, 0), &tls.Config{
+		ServerName: "www.cloudflare.com",
+	})
+	require.NoError(t, tlsConn.Handshake())
+}
+
+func TestTLSRecordFragment(t *testing.T) {
+	t.Parallel()
+	tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
+	require.NoError(t, err)
+	tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, 0), &tls.Config{
+		ServerName: "www.cloudflare.com",
+	})
+	require.NoError(t, tlsConn.Handshake())
+}

+ 24 - 5
docs/configuration/route/rule_action.md

@@ -6,6 +6,7 @@ icon: material/new-box
 
     :material-plus: [tls_fragment](#tls_fragment)  
     :material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay)  
+    :material-plus: [tls_record_fragment](#tls_record_fragment)  
     :material-plus: [resolve.disable_cache](#disable_cache)  
     :material-plus: [resolve.rewrite_ttl](#rewrite_ttl)  
     :material-plus: [resolve.client_subnet](#client_subnet)
@@ -91,7 +92,8 @@ Not available when `method` is set to drop.
   "udp_connect": false,
   "udp_timeout": "",
   "tls_fragment": false,
-  "tls_fragment_fallback_delay": ""
+  "tls_fragment_fallback_delay": "",
+  "tls_record_fragment": ""
 }
 ```
 
@@ -164,13 +166,19 @@ If no protocol is sniffed, the following ports will be recognized as protocols b
 
 Fragment TLS handshakes to bypass firewalls.
 
-This feature is intended to circumvent simple firewalls based on **plaintext packet matching**, and should not be used to circumvent real censorship.
+This feature is intended to circumvent simple firewalls based on **plaintext packet matching**,
+and should not be used to circumvent real censorship.
 
-Since it is not designed for performance, it should not be applied to all connections, but only to server names that are known to be blocked.
+Due to poor performance, try `tls_record_fragment` first, and only apply to server names known to be blocked.
 
-On Linux, Apple platforms, (administrator privileges required) Windows, the wait time can be automatically detected, otherwise it will fall back to waiting for a fixed time specified by `tls_fragment_fallback_delay`.
+On Linux, Apple platforms, (administrator privileges required) Windows,
+the wait time can be automatically detected, otherwise it will fall back to
+waiting for a fixed time specified by `tls_fragment_fallback_delay`.
 
-In addition, if the actual wait time is less than 20ms, it will also fall back to waiting for a fixed time, because the target is considered to be local or behind a transparent proxy.
+In addition, if the actual wait time is less than 20ms, it will also fall back to waiting for a fixed time,
+because the target is considered to be local or behind a transparent proxy.
+
+Conflict with `tls_record_fragment`.
 
 #### tls_fragment_fallback_delay
 
@@ -180,6 +188,17 @@ The fallback value used when TLS segmentation cannot automatically determine the
 
 `500ms` is used by default.
 
+#### tls_record_fragment
+
+!!! question "Since sing-box 1.12.0"
+
+Fragment TLS handshake into multiple TLS records to bypass firewalls.
+
+This feature is intended to circumvent simple firewalls based on **plaintext packet matching**,
+and should not be used to circumvent real censorship.
+
+Conflict with `tls_fragment`.
+
 ### sniff
 
 ```json

+ 20 - 3
docs/configuration/route/rule_action.zh.md

@@ -5,7 +5,11 @@ icon: material/new-box
 !!! quote "sing-box 1.12.0 中的更改"
 
     :material-plus: [tls_fragment](#tls_fragment)  
-    :material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay)
+    :material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay)  
+    :material-plus: [tls_record_fragment](#tls_record_fragment)  
+    :material-plus: [resolve.disable_cache](#disable_cache)  
+    :material-plus: [resolve.rewrite_ttl](#rewrite_ttl)  
+    :material-plus: [resolve.client_subnet](#client_subnet)
 
 ## 最终动作
 
@@ -159,12 +163,15 @@ UDP 连接超时时间。
 
 此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。
 
-由于它不是为性能设计的,不应被应用于所有连接,而仅应用于已知被阻止的服务器名称。
+由于性能不佳,请首先尝试 `tls_record_fragment`,且仅应用于已知被阻止的服务器名称。
 
-在 Linux、Apple 平台和需要管理员权限的 Windows 系统上,可自动检测等待时间。若无法自动检测,将回退使用 `tls_fragment_fallback_delay` 指定的固定等待时间。
+在 Linux、Apple 平台和需要管理员权限的 Windows 系统上,可自动检测等待时间。
+若无法自动检测,将回退使用 `tls_fragment_fallback_delay` 指定的固定等待时间。
 
 此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。
 
+与 `tls_record_fragment` 冲突。
+
 #### tls_fragment_fallback_delay
 
 !!! question "自 sing-box 1.12.0 起"
@@ -173,6 +180,16 @@ UDP 连接超时时间。
 
 默认使用 `500ms`。
 
+#### tls_record_fragment
+
+!!! question "自 sing-box 1.12.0 起"
+
+通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。
+
+此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。
+
+与 `tls_fragment` 冲突。
+
 ### sniff
 
 ```json

+ 4 - 0
option/rule_action.go

@@ -158,6 +158,7 @@ type RawRouteOptionsActionOptions struct {
 
 	TLSFragment              bool               `json:"tls_fragment,omitempty"`
 	TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"`
+	TLSRecordFragment        bool               `json:"tls_record_fragment,omitempty"`
 }
 
 type RouteOptionsActionOptions RawRouteOptionsActionOptions
@@ -170,6 +171,9 @@ func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error {
 	if *r == (RouteOptionsActionOptions{}) {
 		return E.New("empty route option action")
 	}
+	if r.TLSFragment && r.TLSRecordFragment {
+		return E.New("`tls_fragment` and `tls_record_fragment` are mutually exclusive")
+	}
 	return nil
 }
 

+ 3 - 9
route/conn.go

@@ -95,15 +95,9 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co
 		if fallbackDelay == 0 {
 			fallbackDelay = C.TLSFragmentFallbackDelay
 		}
-		var newConn *tf.Conn
-		newConn, err = tf.NewConn(remoteConn, ctx, fallbackDelay)
-		if err != nil {
-			conn.Close()
-			remoteConn.Close()
-			m.logger.ErrorContext(ctx, err)
-			return
-		}
-		remoteConn = newConn
+		remoteConn = tf.NewConn(remoteConn, ctx, false, fallbackDelay)
+	} else if metadata.TLSRecordFragment {
+		remoteConn = tf.NewConn(remoteConn, ctx, true, 0)
 	}
 	m.access.Lock()
 	element := m.connections.PushBack(conn)

+ 18 - 10
route/rule/rule_action.go

@@ -41,6 +41,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
 				UDPConnect:                action.RouteOptions.UDPConnect,
 				TLSFragment:               action.RouteOptions.TLSFragment,
 				TLSFragmentFallbackDelay:  time.Duration(action.RouteOptions.TLSFragmentFallbackDelay),
+				TLSRecordFragment:         action.RouteOptions.TLSRecordFragment,
 			},
 		}, nil
 	case C.RuleActionTypeRouteOptions:
@@ -54,6 +55,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
 			UDPTimeout:                time.Duration(action.RouteOptionsOptions.UDPTimeout),
 			TLSFragment:               action.RouteOptionsOptions.TLSFragment,
 			TLSFragmentFallbackDelay:  time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay),
+			TLSRecordFragment:         action.RouteOptionsOptions.TLSRecordFragment,
 		}, nil
 	case C.RuleActionTypeDirect:
 		directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false)
@@ -153,15 +155,7 @@ func (r *RuleActionRoute) Type() string {
 func (r *RuleActionRoute) String() string {
 	var descriptions []string
 	descriptions = append(descriptions, r.Outbound)
-	if r.UDPDisableDomainUnmapping {
-		descriptions = append(descriptions, "udp-disable-domain-unmapping")
-	}
-	if r.UDPConnect {
-		descriptions = append(descriptions, "udp-connect")
-	}
-	if r.TLSFragment {
-		descriptions = append(descriptions, "tls-fragment")
-	}
+	descriptions = append(descriptions, r.Descriptions()...)
 	return F.ToString("route(", strings.Join(descriptions, ","), ")")
 }
 
@@ -177,6 +171,7 @@ type RuleActionRouteOptions struct {
 	UDPTimeout                time.Duration
 	TLSFragment               bool
 	TLSFragmentFallbackDelay  time.Duration
+	TLSRecordFragment         bool
 }
 
 func (r *RuleActionRouteOptions) Type() string {
@@ -184,6 +179,10 @@ func (r *RuleActionRouteOptions) Type() string {
 }
 
 func (r *RuleActionRouteOptions) String() string {
+	return F.ToString("route-options(", strings.Join(r.Descriptions(), ","), ")")
+}
+
+func (r *RuleActionRouteOptions) Descriptions() []string {
 	var descriptions []string
 	if r.OverrideAddress.IsValid() {
 		descriptions = append(descriptions, F.ToString("override-address=", r.OverrideAddress.AddrString()))
@@ -212,7 +211,16 @@ func (r *RuleActionRouteOptions) String() string {
 	if r.UDPTimeout > 0 {
 		descriptions = append(descriptions, "udp-timeout")
 	}
-	return F.ToString("route-options(", strings.Join(descriptions, ","), ")")
+	if r.TLSFragment {
+		descriptions = append(descriptions, "tls-fragment")
+	}
+	if r.TLSFragmentFallbackDelay > 0 {
+		descriptions = append(descriptions, F.ToString("tls-fragment-fallback-delay=", r.TLSFragmentFallbackDelay.String()))
+	}
+	if r.TLSRecordFragment {
+		descriptions = append(descriptions, "tls-record-fragment")
+	}
+	return descriptions
 }
 
 type RuleActionDNSRoute struct {