Skip to content

Commit

Permalink
update singbox fragment
Browse files Browse the repository at this point in the history
  • Loading branch information
hiddify-com committed Jul 30, 2024
1 parent 288c87c commit cc4e2cf
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 72 deletions.
11 changes: 5 additions & 6 deletions common/dialer/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,19 +119,18 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
}
tlsFragment.Enabled = true

sleep, err := option.ParseIntRange(options.TLSFragment.Sleep)
sleep, err := option.Parse2IntRange(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]
tlsFragment.Sleep = sleep

size, err := option.ParseIntRange(options.TLSFragment.Size)
size, err := option.Parse2IntRange(options.TLSFragment.Size)
if err != nil {
return nil, E.Cause(err, "invalid TLS fragment size supplied")
}
tlsFragment.SizeMin = size[0]
tlsFragment.SizeMax = size[1]
tlsFragment.Size = size

}
if options.IsWireGuardListener {
Expand Down
198 changes: 172 additions & 26 deletions common/dialer/fragment.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,22 @@ package dialer

import (
"encoding/binary"
"errors"
"io"
"net"
"os"
"time"

"github.com/sagernet/sing-box/option"
opt "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
)

type TLSFragment struct {
Enabled bool
SizeMin int
SizeMax int
SleepMin int
SleepMax int
Enabled bool
Sleep opt.IntRange
Size opt.IntRange
}

type fragmentConn struct {
Expand All @@ -30,51 +29,198 @@ type fragmentConn struct {
err error
}

func (c *fragmentConn) Read(b []byte) (n int, err error) {
if c.conn == nil {
return 0, c.err
// isClientHelloPacket checks if data resembles a TLS clientHello packet
func isClientHelloPacket(b []byte) bool {
// Check if the packet is at least 5 bytes long and the content type is 22 (TLS handshake)
if len(b) < 5 || b[0] != 22 {
return false
}
return c.conn.Read(b)

// Check if the protocol version is TLS 1.0 or higher (0x0301 or greater)
version := uint16(b[1])<<8 | uint16(b[2])
if version < 0x0301 {
return false
}

// Check if the handshake message type is ClientHello (1)
if b[5] != 1 {
return false
}

return true
}

func (c *fragmentConn) Write(b []byte) (n int, err error) {
if c.conn == nil {
return 0, c.err
// parseSniInfo parses the ClientHello message and extracts the SNI info
func parseSniInfo(data []byte) (sni string, startIndex int, length int, err error) {
// Skip the first 5 bytes of the TLS record header
data = data[5:]

messageLen := int(data[1])<<16 | int(data[2])<<8 | int(data[3])
if len(data) < 4+messageLen {
return "", 0, 0, errors.New("data too short for complete handshake message")
}

// Skip the handshake message header
data = data[4 : 4+messageLen]

if len(data) < 34 {
return "", 0, 0, errors.New("data too short for ClientHello fixed part")
}

sessionIDLen := int(data[34])
offset := 35 + sessionIDLen

if len(data) < offset+2 {
return "", 0, 0, errors.New("data too short for cipher suites length")
}

cipherSuitesLen := int(binary.BigEndian.Uint16(data[offset : offset+2]))
offset += 2 + cipherSuitesLen

if len(data) < offset+1 {
return "", 0, 0, errors.New("data too short for compression methods length")
}

compressionMethodsLen := int(data[offset])
offset += 1 + compressionMethodsLen

if len(data) < offset+2 {
return "", 0, 0, errors.New("data too short for extensions length")
}

extensionsLen := int(binary.BigEndian.Uint16(data[offset : offset+2]))
offset += 2

if len(data) < offset+extensionsLen {
return "", 0, 0, errors.New("data too short for complete extensions")
}

extensions := data[offset : offset+extensionsLen]
for len(extensions) >= 4 {
extType := binary.BigEndian.Uint16(extensions[:2])
extLen := int(binary.BigEndian.Uint16(extensions[2:4]))
if len(extensions) < 4+extLen {
return "", 0, 0, errors.New("extension length mismatch")
}
if extType == 0x00 { // SNI extension
sniData := extensions[4 : 4+extLen]
if len(sniData) < 2 {
return "", 0, 0, errors.New("invalid SNI extension data")
}
serverNameListLen := int(binary.BigEndian.Uint16(sniData[:2]))
if len(sniData) < 2+serverNameListLen {
return "", 0, 0, errors.New("SNI list length mismatch")
}
serverNameList := sniData[2 : 2+serverNameListLen]
for len(serverNameList) >= 3 {
nameType := serverNameList[0]
nameLen := int(binary.BigEndian.Uint16(serverNameList[1:3]))
if len(serverNameList) < 3+nameLen {
return "", 0, 0, errors.New("server name length mismatch")
}
if nameType == 0 { // host_name
sni = string(serverNameList[3 : 3+nameLen])
startIndex = offset + 4 + 2 + 3
length = nameLen
return sni, startIndex, length, nil
}
serverNameList = serverNameList[3+nameLen:]
}
}
extensions = extensions[4+extLen:]
}
// Do not fragment if it's not a TLS clientHello packet
if len(b) < 7 || b[0] != 22 {
return c.conn.Write(b)

return "", 0, 0, errors.New("SNI not found")
}

// selectRandomIndices selects random indices to chunk data into fragments based on a given range
func selectRandomIndices(dataLen int, sizeRange opt.IntRange) []int {
var indices []int

for current := 0; current < dataLen; {
// Ensure the chunk size does not exceed the remaining length
chunkSize := int(sizeRange.UniformRand())
if current+chunkSize > dataLen {
chunkSize = dataLen - current
}

current += chunkSize
indices = append(indices, current)
}

return indices
}

func fragmentTLSClientHello(b []byte, sizeRange opt.IntRange) [][]byte {
var fragments [][]byte
var fragmentIndices []int
clientHelloLen := int(binary.BigEndian.Uint16(b[3:5]))
clientHelloData := b[5:]

for fragmentStart := 0; fragmentStart < clientHelloLen; {
fragmentEnd := fragmentStart + option.RandBetween(c.fragment.SizeMin, c.fragment.SizeMax)
if fragmentEnd > clientHelloLen {
fragmentEnd = clientHelloLen
_, sniStartIdx, sniLen, err := parseSniInfo(b)
if err != nil {
fragmentIndices = selectRandomIndices(clientHelloLen, sizeRange)
} else {
// select random indices in two parts, 0-randomIndexOfSni and randomIndexOfSni-packetEnd, ensuring the SNI ext is fragmented
sniExtFragmentIdx := opt.IntRange{Min: uint64(sniStartIdx), Max: uint64(sniStartIdx + sniLen)}.UniformRand()
preSniExtIdx := selectRandomIndices(int(sniExtFragmentIdx), sizeRange)
postSniExtIdx := selectRandomIndices(clientHelloLen-(sniStartIdx+sniLen), sizeRange)
for i := range postSniExtIdx {
postSniExtIdx[i] += sniStartIdx + sniLen
}
fragmentIndices = append(fragmentIndices, preSniExtIdx...)
fragmentIndices = append(fragmentIndices, postSniExtIdx...)
}

fragmentStart := 0
for _, fragmentEnd := range fragmentIndices {
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(fragmentEnd-fragmentStart))
payload := append(header, clientHelloData[fragmentStart:fragmentEnd]...)
fragments = append(fragments, payload)
fragmentStart = fragmentEnd
}

return fragments
}

_, err := c.conn.Write(payload)
func (c *fragmentConn) writeFragments(fragments [][]byte) (n int, err error) {
var totalWrittenBytes int
for _, fragment := range fragments {
lastWrittenBytes, err := c.conn.Write(fragment)
if err != nil {
c.err = err
return 0, c.err
return totalWrittenBytes, c.err
}
totalWrittenBytes += lastWrittenBytes

if c.fragment.SleepMax != 0 {
time.Sleep(time.Duration(option.RandBetween(c.fragment.SleepMin, c.fragment.SleepMax)) * time.Millisecond)
if c.fragment.Sleep.Max != 0 {
time.Sleep(time.Duration(c.fragment.Sleep.UniformRand()) * time.Millisecond)
}
}
return totalWrittenBytes, nil
}

fragmentStart = fragmentEnd
func (c *fragmentConn) Write(b []byte) (n int, err error) {
if c.conn == nil {
return 0, c.err
}

if isClientHelloPacket(b) {
fragments := fragmentTLSClientHello(b, c.fragment.Size)
return c.writeFragments(fragments)
}

return len(b), nil
return c.conn.Write(b)
}

func (c *fragmentConn) Read(b []byte) (n int, err error) {
if c.conn == nil {
return 0, c.err
}
return c.conn.Read(b)
}

func (c *fragmentConn) Close() error {
Expand Down
4 changes: 2 additions & 2 deletions common/tls/utls_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (

type UTLSClientConfig struct {
config *utls.Config
paddingSize []int
paddingSize option.IntRange
id utls.ClientHelloID
}

Expand Down Expand Up @@ -209,7 +209,7 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
if options.TLSTricks != nil {
switch options.TLSTricks.PaddingMode {
case "random":
paddingSize, err := option.ParseIntRange(options.TLSTricks.PaddingSize)
paddingSize, err := option.Parse2IntRange(options.TLSTricks.PaddingSize)
if err != nil {
return nil, E.Cause(err, "invalid Padding Size supplied")
}
Expand Down
3 changes: 1 addition & 2 deletions common/tls/utls_padding.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"net"
"strings"

"github.com/sagernet/sing-box/option"
utls "github.com/sagernet/utls"
)

Expand Down Expand Up @@ -115,7 +114,7 @@ func (e *FakePaddingExtension) Read(b []byte) (n int, err error) {

// makeTLSHelloPacketWithPadding creates a TLS hello packet with padding.
func makeTLSHelloPacketWithPadding(conn net.Conn, e *UTLSClientConfig, sni string) (*utls.UConn, error) {
paddingSize := option.RandBetween(e.paddingSize[0], e.paddingSize[1])
paddingSize := int(e.paddingSize.UniformRand())
if paddingSize <= 0 {
paddingSize = 1
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,4 @@ require (

replace github.com/sagernet/wireguard-go => github.com/hiddify/wireguard-go v0.0.0-20240727191222-383c1da14ff1

replace github.com/xtls/xray-core => github.com/hiddify/xray-core v0.0.0-20240727184702-fcf01338c17a
replace github.com/xtls/xray-core => github.com/hiddify/xray-core v0.0.0-20240729110224-c3df022f042a
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/hiddify/wireguard-go v0.0.0-20240727191222-383c1da14ff1 h1:xdbHlZtzs+jijAxy85qal835GglwmjohA/srHT8gm9s=
github.com/hiddify/wireguard-go v0.0.0-20240727191222-383c1da14ff1/go.mod h1:K4J7/npM+VAMUeUmTa2JaA02JmyheP0GpRBOUvn3ecc=
github.com/hiddify/xray-core v0.0.0-20240727184702-fcf01338c17a h1:+UaMu8WMXqCV/mMOFOnGuqaeClvTkO5Q77GVK7lhIoI=
github.com/hiddify/xray-core v0.0.0-20240727184702-fcf01338c17a/go.mod h1:nA9jtXividdYBW2qAGi58QeKyhrjwXrgKYpba4s4U54=
github.com/hiddify/xray-core v0.0.0-20240729110224-c3df022f042a h1:Pv2c4o73tAiq/dC/gQisxeKDy2ci/i4cA70WV+lEXcI=
github.com/hiddify/xray-core v0.0.0-20240729110224-c3df022f042a/go.mod h1:nA9jtXividdYBW2qAGi58QeKyhrjwXrgKYpba4s4U54=
github.com/imkira/go-observer/v2 v2.0.0-20230629064422-8e0b61f11f1b h1:1+115FqGoS8p6Iry9AYmrcWDvSveH0F7P2nX1LU00qg=
github.com/imkira/go-observer/v2 v2.0.0-20230629064422-8e0b61f11f1b/go.mod h1:XCscqBi1KKh7GcVDDAdkT/Cf6WDjnDAA1XM3nwmA0Ag=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
Expand Down
33 changes: 0 additions & 33 deletions option/fragment.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
package option

import (
"fmt"
"math/rand"
"strconv"
"strings"

E "github.com/sagernet/sing/common/exceptions"
)

type TLSFragmentOptions struct {
Expand All @@ -15,34 +10,6 @@ type TLSFragmentOptions struct {
Sleep string `json:"sleep,omitempty"` // Time to sleep between sending the fragments in milliseconds
}

func ParseIntRange(str string) ([]int, error) {
if str == "" {
return nil, E.New("Empty input")
}

splitString := strings.Split(str, "-")
s, err := strconv.ParseInt(splitString[0], 10, 32)
if err != nil {
return nil, E.Cause(err, "error parsing string to integer")
}
e := s
if len(splitString) == 2 {
e, err = strconv.ParseInt(splitString[1], 10, 32)
if err != nil {
return nil, E.Cause(err, "error parsing string to integer")
}

}
if s < 0 {
return nil, E.Cause(E.New(fmt.Sprintf("Negative value (%d) is not possible", s)), "invalid range")
}
if e < s {
return nil, E.Cause(E.New(fmt.Sprintf("upper bound value (%d) must be greater than or equal to lower bound value (%d)", e, s)), "invalid range")
}
return []int{int(s), int(e)}, nil

}

func RandBetween(min int, max int) int {
if max == min {
return min
Expand Down
Loading

0 comments on commit cc4e2cf

Please sign in to comment.