mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
feat: add TLS fragmentation
Introduce a feature to fragment TLS clientHello packets to circumvent TLS-based blockings (mostly seen in Iran's firewall)
This commit is contained in:
parent
a4914fd214
commit
8bc40a6508
@ -98,11 +98,33 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
|||||||
}
|
}
|
||||||
setMultiPathTCP(&dialer4)
|
setMultiPathTCP(&dialer4)
|
||||||
}
|
}
|
||||||
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
if options.TLSFragment.Enabled && options.TCPFastOpen {
|
||||||
|
return nil, E.New("TLS Fragmentation is not compatible with TCP Fast Open, set `tcp_fast_open` to `false` in your outbound if you intend to enable TLS fragmentation.")
|
||||||
|
}
|
||||||
|
var tlsFragment TLSFragment
|
||||||
|
if options.TLSFragment.Enabled {
|
||||||
|
tlsFragment.Enabled = true
|
||||||
|
|
||||||
|
sleep, err := option.ParseIntRange(options.TLSFragment.Sleep)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "invalid TLS fragment sleep period supplied")
|
||||||
|
}
|
||||||
|
tlsFragment.SleepMin = sleep[0]
|
||||||
|
tlsFragment.SleepMax = sleep[1]
|
||||||
|
|
||||||
|
size, err := option.ParseIntRange(options.TLSFragment.Size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "invalid TLS fragment size supplied")
|
||||||
|
}
|
||||||
|
tlsFragment.SizeMin = size[0]
|
||||||
|
tlsFragment.SizeMax = size[1]
|
||||||
|
|
||||||
|
}
|
||||||
|
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen, tlsFragment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tcpDialer6, err := newTCPDialer(dialer6, options.TCPFastOpen)
|
tcpDialer6, err := newTCPDialer(dialer6, options.TCPFastOpen, tlsFragment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -130,9 +152,9 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !address.IsIPv6() {
|
if !address.IsIPv6() {
|
||||||
return trackConn(DialSlowContext(&d.dialer4, ctx, network, address))
|
return trackConn(d.dialer4.DialContext(ctx, network, address))
|
||||||
} else {
|
} else {
|
||||||
return trackConn(DialSlowContext(&d.dialer6, ctx, network, address))
|
return trackConn(d.dialer6.DialContext(ctx, network, address))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,12 +4,10 @@ package dialer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/sagernet/tfo-go"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type tcpDialer = tfo.Dialer
|
type tcpDialer = ExtendedTCPDialer
|
||||||
|
|
||||||
func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
|
func newTCPDialer(dialer net.Dialer, tfoEnabled bool, tlsFragment TLSFragment) (tcpDialer, error) {
|
||||||
return tfo.Dialer{Dialer: dialer, DisableTFO: !tfoEnabled}, nil
|
return tcpDialer{Dialer: dialer, DisableTFO: !tfoEnabled, TLSFragment: tlsFragment}, nil
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,12 @@ import (
|
|||||||
|
|
||||||
type tcpDialer = net.Dialer
|
type tcpDialer = net.Dialer
|
||||||
|
|
||||||
func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
|
func newTCPDialer(dialer net.Dialer, tfoEnabled bool, tlsFragment TLSFragment) (tcpDialer, error) {
|
||||||
if tfoEnabled {
|
if tfoEnabled {
|
||||||
return dialer, E.New("TCP Fast Open requires go1.20, please recompile your binary.")
|
return dialer, E.New("TCP Fast Open requires go1.20, please recompile your binary.")
|
||||||
}
|
}
|
||||||
|
if tlsFragment.Enabled {
|
||||||
|
return tcpDialer{Dialer: dialer, DisableTFO: true, TLSFragment: tlsFragment}, nil
|
||||||
|
}
|
||||||
return dialer, nil
|
return dialer, nil
|
||||||
}
|
}
|
||||||
|
55
common/dialer/extended_tcp.go
Normal file
55
common/dialer/extended_tcp.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
//go:build go1.20
|
||||||
|
|
||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/tfo-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Custom TCP dialer with extra features such as "TCP Fast Open" or "TLS Fragmentation"
|
||||||
|
type ExtendedTCPDialer struct {
|
||||||
|
net.Dialer
|
||||||
|
DisableTFO bool
|
||||||
|
TLSFragment TLSFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ExtendedTCPDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
if (d.DisableTFO && !d.TLSFragment.Enabled) || N.NetworkName(network) != N.NetworkTCP {
|
||||||
|
switch N.NetworkName(network) {
|
||||||
|
case N.NetworkTCP, N.NetworkUDP:
|
||||||
|
return d.Dialer.DialContext(ctx, network, destination.String())
|
||||||
|
default:
|
||||||
|
return d.Dialer.DialContext(ctx, network, destination.AddrString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Create a TLS-Fragmented dialer
|
||||||
|
if d.TLSFragment.Enabled {
|
||||||
|
fragmentConn := &fragmentConn{
|
||||||
|
dialer: &d.Dialer,
|
||||||
|
fragment: d.TLSFragment,
|
||||||
|
network: network,
|
||||||
|
destination: destination,
|
||||||
|
}
|
||||||
|
conn, err := d.Dialer.DialContext(ctx, network, destination.String())
|
||||||
|
if err != nil {
|
||||||
|
fragmentConn.err = err
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fragmentConn.conn = conn
|
||||||
|
return fragmentConn, nil
|
||||||
|
}
|
||||||
|
// Create a TFO dialer
|
||||||
|
return &slowOpenConn{
|
||||||
|
dialer: &tfo.Dialer{Dialer: d.Dialer, DisableTFO: d.DisableTFO},
|
||||||
|
ctx: ctx,
|
||||||
|
network: network,
|
||||||
|
destination: destination,
|
||||||
|
create: make(chan struct{}),
|
||||||
|
},
|
||||||
|
nil
|
||||||
|
}
|
36
common/dialer/extended_tcp_stub.go
Normal file
36
common/dialer/extended_tcp_stub.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//go:build !go1.20
|
||||||
|
|
||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *ExtendedTCPDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
if !d.TLSFragment.Enabled || N.NetworkName(network) != N.NetworkTCP {
|
||||||
|
switch N.NetworkName(network) {
|
||||||
|
case N.NetworkTCP, N.NetworkUDP:
|
||||||
|
return d.Dialer.DialContext(ctx, network, destination.String())
|
||||||
|
default:
|
||||||
|
return d.Dialer.DialContext(ctx, network, destination.AddrString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Create a TLS-Fragmented dialer
|
||||||
|
fragmentConn := &fragmentConn{
|
||||||
|
dialer: &d.Dialer,
|
||||||
|
fragment: d.TLSFragment,
|
||||||
|
network: network,
|
||||||
|
destination: destination,
|
||||||
|
}
|
||||||
|
conn, err := d.Dialer.DialContext(ctx, network, destination.String())
|
||||||
|
if err != nil {
|
||||||
|
fragmentConn.err = err
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fragmentConn.conn = conn
|
||||||
|
return fragmentConn, nil
|
||||||
|
}
|
154
common/dialer/fragment.go
Normal file
154
common/dialer/fragment.go
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TLSFragment struct {
|
||||||
|
Enabled bool
|
||||||
|
SizeMin uint64
|
||||||
|
SizeMax uint64
|
||||||
|
SleepMin uint64
|
||||||
|
SleepMax uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type fragmentConn struct {
|
||||||
|
dialer *net.Dialer
|
||||||
|
fragment TLSFragment
|
||||||
|
network string
|
||||||
|
destination M.Socksaddr
|
||||||
|
conn net.Conn
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fragmentConn) Read(b []byte) (n int, err error) {
|
||||||
|
if c.conn == nil {
|
||||||
|
return 0, c.err
|
||||||
|
}
|
||||||
|
return c.conn.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func randBetween(left int64, right int64) int64 {
|
||||||
|
if left == right {
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
bigInt, _ := rand.Int(rand.Reader, big.NewInt(right-left))
|
||||||
|
return left + bigInt.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fragmentConn) Write(b []byte) (n int, err error) {
|
||||||
|
if c.conn == nil {
|
||||||
|
return 0, c.err
|
||||||
|
}
|
||||||
|
// Check if payload is a valid TLS clientHello packet
|
||||||
|
if len(b) >= 5 && b[0] == 22 {
|
||||||
|
clientHelloLen := int(binary.BigEndian.Uint16(b[3:5]))
|
||||||
|
clientHelloData := b[5:]
|
||||||
|
|
||||||
|
for i := 0; i < clientHelloLen; {
|
||||||
|
fragmentEnd := i + int(randBetween(int64(c.fragment.SizeMin), int64(c.fragment.SizeMax)))
|
||||||
|
if fragmentEnd > clientHelloLen {
|
||||||
|
fragmentEnd = clientHelloLen
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment := clientHelloData[i:fragmentEnd]
|
||||||
|
i = fragmentEnd
|
||||||
|
|
||||||
|
header := make([]byte, 5)
|
||||||
|
header[0] = b[0]
|
||||||
|
binary.BigEndian.PutUint16(header[1:], binary.BigEndian.Uint16(b[1:3]))
|
||||||
|
binary.BigEndian.PutUint16(header[3:], uint16(len(fragment)))
|
||||||
|
payload := append(header, fragment...)
|
||||||
|
|
||||||
|
_, err := c.conn.Write(payload)
|
||||||
|
if err != nil {
|
||||||
|
c.err = err
|
||||||
|
return 0, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
randomInterval := randBetween(int64(c.fragment.SleepMin), int64(c.fragment.SleepMax))
|
||||||
|
time.Sleep(time.Duration(randomInterval))
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write directly if not a clientHello packet
|
||||||
|
return c.conn.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fragmentConn) Close() error {
|
||||||
|
return common.Close(c.conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fragmentConn) LocalAddr() net.Addr {
|
||||||
|
if c.conn == nil {
|
||||||
|
return M.Socksaddr{}
|
||||||
|
}
|
||||||
|
return c.conn.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fragmentConn) RemoteAddr() net.Addr {
|
||||||
|
if c.conn == nil {
|
||||||
|
return M.Socksaddr{}
|
||||||
|
}
|
||||||
|
return c.conn.RemoteAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fragmentConn) SetDeadline(t time.Time) error {
|
||||||
|
if c.conn == nil {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
return c.conn.SetDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fragmentConn) SetReadDeadline(t time.Time) error {
|
||||||
|
if c.conn == nil {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
return c.conn.SetReadDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fragmentConn) SetWriteDeadline(t time.Time) error {
|
||||||
|
if c.conn == nil {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
return c.conn.SetWriteDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fragmentConn) Upstream() any {
|
||||||
|
return c.conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fragmentConn) ReaderReplaceable() bool {
|
||||||
|
return c.conn != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fragmentConn) WriterReplaceable() bool {
|
||||||
|
return c.conn != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fragmentConn) LazyHeadroom() bool {
|
||||||
|
return c.conn == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fragmentConn) NeedHandshake() bool {
|
||||||
|
return c.conn == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fragmentConn) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
if c.conn == nil {
|
||||||
|
return 0, c.err
|
||||||
|
}
|
||||||
|
return bufio.Copy(w, c.conn)
|
||||||
|
}
|
@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/sagernet/tfo-go"
|
"github.com/sagernet/tfo-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -29,24 +28,6 @@ type slowOpenConn struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
|
||||||
if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP {
|
|
||||||
switch N.NetworkName(network) {
|
|
||||||
case N.NetworkTCP, N.NetworkUDP:
|
|
||||||
return dialer.Dialer.DialContext(ctx, network, destination.String())
|
|
||||||
default:
|
|
||||||
return dialer.Dialer.DialContext(ctx, network, destination.AddrString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &slowOpenConn{
|
|
||||||
dialer: dialer,
|
|
||||||
ctx: ctx,
|
|
||||||
network: network,
|
|
||||||
destination: destination,
|
|
||||||
create: make(chan struct{}),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *slowOpenConn) Read(b []byte) (n int, err error) {
|
func (c *slowOpenConn) Read(b []byte) (n int, err error) {
|
||||||
if c.conn == nil {
|
if c.conn == nil {
|
||||||
select {
|
select {
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
//go:build !go1.20
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
|
||||||
switch N.NetworkName(network) {
|
|
||||||
case N.NetworkTCP, N.NetworkUDP:
|
|
||||||
return dialer.DialContext(ctx, network, destination.String())
|
|
||||||
default:
|
|
||||||
return dialer.DialContext(ctx, network, destination.AddrString())
|
|
||||||
}
|
|
||||||
}
|
|
38
option/fragment.go
Normal file
38
option/fragment.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TLSFragmentOptions struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
Size string `json:"size,omitempty"` // Fragment size in Bytes
|
||||||
|
Sleep string `json:"sleep,omitempty"` // Time to sleep between sending the fragments in milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseIntRange(str string) ([]uint64, error) {
|
||||||
|
var err error
|
||||||
|
result := make([]uint64, 2)
|
||||||
|
|
||||||
|
splitString := strings.Split(str, "-")
|
||||||
|
if len(splitString) == 2 {
|
||||||
|
result[0], err = strconv.ParseUint(splitString[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "Error parsing string to integer")
|
||||||
|
}
|
||||||
|
result[1], err = strconv.ParseUint(splitString[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "Error parsing string to integer")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result[0], err = strconv.ParseUint(splitString[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "Error parsing string to integer")
|
||||||
|
}
|
||||||
|
result[1] = result[0]
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
@ -140,6 +140,7 @@ type DialerOptions struct {
|
|||||||
ConnectTimeout Duration `json:"connect_timeout,omitempty"`
|
ConnectTimeout Duration `json:"connect_timeout,omitempty"`
|
||||||
TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
|
TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
|
||||||
TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
|
TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
|
||||||
|
TLSFragment TLSFragmentOptions `json:"tls_fragment,omitempty"`
|
||||||
UDPFragment *bool `json:"udp_fragment,omitempty"`
|
UDPFragment *bool `json:"udp_fragment,omitempty"`
|
||||||
UDPFragmentDefault bool `json:"-"`
|
UDPFragmentDefault bool `json:"-"`
|
||||||
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
|
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
|
||||||
|
Loading…
x
Reference in New Issue
Block a user