Explorar el Código

Improve TLS fragments

世界 hace 4 meses
padre
commit
ea190ca428

+ 6 - 6
common/tls/ech.go

@@ -25,7 +25,7 @@ import (
 	"golang.org/x/crypto/cryptobyte"
 )
 
-func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
+func parseECHClientConfig(ctx context.Context, stdConfig *STDClientConfig, options option.OutboundTLSOptions) (Config, error) {
 	var echConfig []byte
 	if len(options.ECH.Config) > 0 {
 		echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
@@ -45,11 +45,11 @@ func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions
 		if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
 			return nil, E.New("invalid ECH configs pem")
 		}
-		tlsConfig.EncryptedClientHelloConfigList = block.Bytes
-		return &STDClientConfig{tlsConfig}, nil
+		stdConfig.config.EncryptedClientHelloConfigList = block.Bytes
+		return stdConfig, nil
 	} else {
 		return &STDECHClientConfig{
-			STDClientConfig: STDClientConfig{tlsConfig},
+			STDClientConfig: stdConfig,
 			dnsRouter:       service.FromContext[adapter.DNSRouter](ctx),
 		}, nil
 	}
@@ -103,7 +103,7 @@ func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
 }
 
 type STDECHClientConfig struct {
-	STDClientConfig
+	*STDClientConfig
 	access     sync.Mutex
 	dnsRouter  adapter.DNSRouter
 	lastTTL    time.Duration
@@ -171,7 +171,7 @@ func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Con
 }
 
 func (s *STDECHClientConfig) Clone() Config {
-	return &STDECHClientConfig{STDClientConfig: STDClientConfig{s.config.Clone()}, dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate}
+	return &STDECHClientConfig{STDClientConfig: s.STDClientConfig.Clone().(*STDClientConfig), dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate}
 }
 
 func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {

+ 15 - 4
common/tls/std_client.go

@@ -7,15 +7,21 @@ import (
 	"net"
 	"os"
 	"strings"
+	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/tlsfragment"
 	"github.com/sagernet/sing-box/option"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/ntp"
 )
 
 type STDClientConfig struct {
-	config *tls.Config
+	ctx                   context.Context
+	config                *tls.Config
+	fragment              bool
+	fragmentFallbackDelay time.Duration
+	recordFragment        bool
 }
 
 func (s *STDClientConfig) ServerName() string {
@@ -39,11 +45,14 @@ func (s *STDClientConfig) Config() (*STDConfig, error) {
 }
 
 func (s *STDClientConfig) Client(conn net.Conn) (Conn, error) {
+	if s.recordFragment {
+		conn = tf.NewConn(conn, s.ctx, s.fragment, s.recordFragment, s.fragmentFallbackDelay)
+	}
 	return tls.Client(conn, s.config), nil
 }
 
 func (s *STDClientConfig) Clone() Config {
-	return &STDClientConfig{s.config.Clone()}
+	return &STDClientConfig{s.ctx, s.config.Clone(), s.fragment, s.fragmentFallbackDelay, s.recordFragment}
 }
 
 func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
@@ -127,8 +136,10 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
 		}
 		tlsConfig.RootCAs = certPool
 	}
+	stdConfig := &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
 	if options.ECH != nil && options.ECH.Enabled {
-		return parseECHClientConfig(ctx, options, &tlsConfig)
+		return parseECHClientConfig(ctx, stdConfig, options)
+	} else {
+		return stdConfig, nil
 	}
-	return &STDClientConfig{&tlsConfig}, nil
 }

+ 13 - 5
common/tls/utls_client.go

@@ -11,8 +11,10 @@ import (
 	"net/netip"
 	"os"
 	"strings"
+	"time"
 
 	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/tlsfragment"
 	"github.com/sagernet/sing-box/option"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/ntp"
@@ -22,8 +24,12 @@ import (
 )
 
 type UTLSClientConfig struct {
-	config *utls.Config
-	id     utls.ClientHelloID
+	ctx                   context.Context
+	config                *utls.Config
+	id                    utls.ClientHelloID
+	fragment              bool
+	fragmentFallbackDelay time.Duration
+	recordFragment        bool
 }
 
 func (e *UTLSClientConfig) ServerName() string {
@@ -50,6 +56,9 @@ func (e *UTLSClientConfig) Config() (*STDConfig, error) {
 }
 
 func (e *UTLSClientConfig) Client(conn net.Conn) (Conn, error) {
+	if e.recordFragment {
+		conn = tf.NewConn(conn, e.ctx, e.fragment, e.recordFragment, e.fragmentFallbackDelay)
+	}
 	return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)}, e.config.NextProtos}, nil
 }
 
@@ -59,8 +68,7 @@ func (e *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []by
 
 func (e *UTLSClientConfig) Clone() Config {
 	return &UTLSClientConfig{
-		config: e.config.Clone(),
-		id:     e.id,
+		e.ctx, e.config.Clone(), e.id, e.fragment, e.fragmentFallbackDelay, e.recordFragment,
 	}
 }
 
@@ -192,7 +200,7 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
 	if err != nil {
 		return nil, err
 	}
-	return &UTLSClientConfig{&tlsConfig, id}, nil
+	return &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}, nil
 }
 
 var (

+ 31 - 17
common/tlsfragment/conn.go

@@ -9,6 +9,7 @@ import (
 	"strings"
 	"time"
 
+	C "github.com/sagernet/sing-box/constant"
 	N "github.com/sagernet/sing/common/network"
 
 	"golang.org/x/net/publicsuffix"
@@ -19,16 +20,21 @@ type Conn struct {
 	tcpConn            *net.TCPConn
 	ctx                context.Context
 	firstPacketWritten bool
+	splitPacket        bool
 	splitRecord        bool
 	fallbackDelay      time.Duration
 }
 
-func NewConn(conn net.Conn, ctx context.Context, splitRecord bool, fallbackDelay time.Duration) *Conn {
+func NewConn(conn net.Conn, ctx context.Context, splitPacket bool, splitRecord bool, fallbackDelay time.Duration) *Conn {
+	if fallbackDelay == 0 {
+		fallbackDelay = C.TLSFragmentFallbackDelay
+	}
 	tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn)
 	return &Conn{
 		Conn:          conn,
 		tcpConn:       tcpConn,
 		ctx:           ctx,
+		splitPacket:   splitPacket,
 		splitRecord:   splitRecord,
 		fallbackDelay: fallbackDelay,
 	}
@@ -41,7 +47,7 @@ func (c *Conn) Write(b []byte) (n int, err error) {
 		}()
 		serverName := indexTLSServerName(b)
 		if serverName != nil {
-			if !c.splitRecord {
+			if c.splitPacket {
 				if c.tcpConn != nil {
 					err = c.tcpConn.SetNoDelay(true)
 					if err != nil {
@@ -81,33 +87,41 @@ func (c *Conn) Write(b []byte) (n int, err error) {
 					payload = b[splitIndexes[i-1]:splitIndexes[i]]
 				}
 				if c.splitRecord {
+					if c.splitPacket {
+						buffer.Reset()
+					}
 					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
+					if c.splitPacket {
+						payload = buffer.Bytes()
 					}
-				} else {
-					_, err = c.Conn.Write(payload)
-					if err != nil {
-						return
+				}
+				if c.splitPacket {
+					if c.tcpConn != nil && i != len(splitIndexes) {
+						err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay)
+						if err != nil {
+							return
+						}
+					} else {
+						_, err = c.Conn.Write(payload)
+						if err != nil {
+							return
+						}
 					}
 				}
 			}
-			if c.splitRecord {
+			if c.splitRecord && !c.splitPacket {
 				_, err = c.Conn.Write(buffer.Bytes())
 				if err != nil {
 					return
 				}
-			} else {
-				if c.tcpConn != nil {
-					err = c.tcpConn.SetNoDelay(false)
-					if err != nil {
-						return
-					}
+			}
+			if c.tcpConn != nil {
+				err = c.tcpConn.SetNoDelay(false)
+				if err != nil {
+					return
 				}
 			}
 			return len(b), nil

+ 12 - 2
common/tlsfragment/conn_test.go

@@ -15,7 +15,7 @@ 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{
+	tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, false, 0), &tls.Config{
 		ServerName: "www.cloudflare.com",
 	})
 	require.NoError(t, tlsConn.Handshake())
@@ -25,7 +25,17 @@ 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{
+	tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), false, true, 0), &tls.Config{
+		ServerName: "www.cloudflare.com",
+	})
+	require.NoError(t, tlsConn.Handshake())
+}
+
+func TestTLS2Fragment(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, true, 0), &tls.Config{
 		ServerName: "www.cloudflare.com",
 	})
 	require.NoError(t, tlsConn.Handshake())

+ 1 - 8
docs/configuration/route/rule_action.md

@@ -172,14 +172,12 @@ and should not be used to circumvent real censorship.
 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
+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.
 
-Conflict with `tls_record_fragment`.
-
 #### tls_fragment_fallback_delay
 
 !!! question "Since sing-box 1.12.0"
@@ -194,11 +192,6 @@ The fallback value used when TLS segmentation cannot automatically determine the
 
 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

+ 0 - 6
docs/configuration/route/rule_action.zh.md

@@ -170,8 +170,6 @@ UDP 连接超时时间。
 
 此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。
 
-与 `tls_record_fragment` 冲突。
-
 #### tls_fragment_fallback_delay
 
 !!! question "自 sing-box 1.12.0 起"
@@ -186,10 +184,6 @@ UDP 连接超时时间。
 
 通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。
 
-此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。
-
-与 `tls_fragment` 冲突。
-
 ### sniff
 
 ```json

+ 44 - 0
docs/configuration/shared/tls.md

@@ -4,6 +4,9 @@ icon: material/alert-decagram
 
 !!! quote "Changes in sing-box 1.12.0"
 
+    :material-plus: [fragment](#fragment)  
+    :material-plus: [fragment_fallback_delay](#fragment_fallback_delay)  
+    :material-plus: [record_fragment](#record_fragment)  
     :material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled)  
     :material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled)
 
@@ -82,6 +85,9 @@ icon: material/alert-decagram
   "cipher_suites": [],
   "certificate": "",
   "certificate_path": "",
+  "fragment": false,
+  "fragment_fallback_delay": "",
+  "record_fragment": false,
   "ech": {
     "enabled": false,
     "config": [],
@@ -313,6 +319,44 @@ The path to ECH configuration, in PEM format.
 
 If empty, load from DNS will be attempted.
 
+#### fragment
+
+!!! question "Since sing-box 1.12.0"
+
+==Client only==
+
+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.
+
+Due to poor performance, try `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 `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.
+
+#### fragment_fallback_delay
+
+!!! question "Since sing-box 1.12.0"
+
+==Client only==
+
+The fallback value used when TLS segmentation cannot automatically determine the wait time.
+
+`500ms` is used by default.
+
+#### record_fragment
+
+!!! question "Since sing-box 1.12.0"
+
+==Client only==
+
+Fragment TLS handshake into multiple TLS records to bypass firewalls.
+
 ### ACME Fields
 
 #### domain

+ 41 - 0
docs/configuration/shared/tls.zh.md

@@ -4,6 +4,9 @@ icon: material/alert-decagram
 
 !!! 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_record_fragment](#tls_record_fragment)  
     :material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled)  
     :material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled)
 
@@ -82,6 +85,9 @@ icon: material/alert-decagram
   "cipher_suites": [],
   "certificate": [],
   "certificate_path": "",
+  "fragment": false,
+  "fragment_fallback_delay": "",
+  "record_fragment": false,
   "ech": {
     "enabled": false,
     "pq_signature_schemes_enabled": false,
@@ -305,6 +311,41 @@ ECH PEM 配置路径
 如果为 true,则始终使用最大可能的 TLS 记录大小。
 如果为 false,则可能会调整 TLS 记录的大小以尝试改善延迟。
 
+#### tls_fragment
+
+!!! question "自 sing-box 1.12.0 起"
+
+==仅客户端==
+
+通过分段 TLS 握手数据包来绕过防火墙检测。
+
+此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。
+
+由于性能不佳,请首先尝试 `tls_record_fragment`,且仅应用于已知被阻止的服务器名称。
+
+在 Linux、Apple 平台和需要管理员权限的 Windows 系统上,可自动检测等待时间。
+若无法自动检测,将回退使用 `tls_fragment_fallback_delay` 指定的固定等待时间。
+
+此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。
+
+#### tls_fragment_fallback_delay
+
+!!! question "自 sing-box 1.12.0 起"
+
+==仅客户端==
+
+当 TLS 分片功能无法自动判定等待时间时使用的回退值。
+
+默认使用 `500ms`。
+
+#### tls_record_fragment
+
+==仅客户端==
+
+!!! question "自 sing-box 1.12.0 起"
+
+通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。
+
 ### ACME 字段
 
 #### domain

+ 16 - 13
option/tls.go

@@ -37,19 +37,22 @@ func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTL
 }
 
 type OutboundTLSOptions struct {
-	Enabled         bool                       `json:"enabled,omitempty"`
-	DisableSNI      bool                       `json:"disable_sni,omitempty"`
-	ServerName      string                     `json:"server_name,omitempty"`
-	Insecure        bool                       `json:"insecure,omitempty"`
-	ALPN            badoption.Listable[string] `json:"alpn,omitempty"`
-	MinVersion      string                     `json:"min_version,omitempty"`
-	MaxVersion      string                     `json:"max_version,omitempty"`
-	CipherSuites    badoption.Listable[string] `json:"cipher_suites,omitempty"`
-	Certificate     badoption.Listable[string] `json:"certificate,omitempty"`
-	CertificatePath string                     `json:"certificate_path,omitempty"`
-	ECH             *OutboundECHOptions        `json:"ech,omitempty"`
-	UTLS            *OutboundUTLSOptions       `json:"utls,omitempty"`
-	Reality         *OutboundRealityOptions    `json:"reality,omitempty"`
+	Enabled               bool                       `json:"enabled,omitempty"`
+	DisableSNI            bool                       `json:"disable_sni,omitempty"`
+	ServerName            string                     `json:"server_name,omitempty"`
+	Insecure              bool                       `json:"insecure,omitempty"`
+	ALPN                  badoption.Listable[string] `json:"alpn,omitempty"`
+	MinVersion            string                     `json:"min_version,omitempty"`
+	MaxVersion            string                     `json:"max_version,omitempty"`
+	CipherSuites          badoption.Listable[string] `json:"cipher_suites,omitempty"`
+	Certificate           badoption.Listable[string] `json:"certificate,omitempty"`
+	CertificatePath       string                     `json:"certificate_path,omitempty"`
+	Fragment              bool                       `json:"fragment,omitempty"`
+	FragmentFallbackDelay badoption.Duration         `json:"fragment_fallback_delay,omitempty"`
+	RecordFragment        bool                       `json:"record_fragment,omitempty"`
+	ECH                   *OutboundECHOptions        `json:"ech,omitempty"`
+	UTLS                  *OutboundUTLSOptions       `json:"utls,omitempty"`
+	Reality               *OutboundRealityOptions    `json:"reality,omitempty"`
 }
 
 type OutboundTLSOptionsContainer struct {

+ 2 - 8
route/conn.go

@@ -90,14 +90,8 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co
 		m.logger.ErrorContext(ctx, err)
 		return
 	}
-	if metadata.TLSFragment {
-		fallbackDelay := metadata.TLSFragmentFallbackDelay
-		if fallbackDelay == 0 {
-			fallbackDelay = C.TLSFragmentFallbackDelay
-		}
-		remoteConn = tf.NewConn(remoteConn, ctx, false, fallbackDelay)
-	} else if metadata.TLSRecordFragment {
-		remoteConn = tf.NewConn(remoteConn, ctx, true, 0)
+	if metadata.TLSFragment || metadata.TLSRecordFragment {
+		remoteConn = tf.NewConn(remoteConn, ctx, metadata.TLSFragment, metadata.TLSRecordFragment, metadata.TLSFragmentFallbackDelay)
 	}
 	m.access.Lock()
 	element := m.connections.PushBack(conn)