mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-05-27 06:31:46 +03:00
Add tls fragment support
This commit is contained in:
125
common/tlsfragment/conn.go
Normal file
125
common/tlsfragment/conn.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package tf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
net.Conn
|
||||
syscallConn syscall.Conn
|
||||
ctx context.Context
|
||||
firstPacketWritten bool
|
||||
fallbackDelay time.Duration
|
||||
}
|
||||
|
||||
func NewConn(conn net.Conn, ctx context.Context, fallbackDelay time.Duration) (*Conn, error) {
|
||||
syscallConn, _ := N.UnwrapReader(conn).(syscall.Conn)
|
||||
return &Conn{
|
||||
Conn: conn,
|
||||
syscallConn: syscallConn,
|
||||
ctx: ctx,
|
||||
fallbackDelay: fallbackDelay,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Write(b []byte) (n int, err error) {
|
||||
if !c.firstPacketWritten {
|
||||
defer func() {
|
||||
c.firstPacketWritten = true
|
||||
}()
|
||||
serverName := indexTLSServerName(b)
|
||||
if serverName != nil {
|
||||
tcpConn, isTCPConn := c.syscallConn.(interface {
|
||||
SetNoDelay(bool) error
|
||||
})
|
||||
if isTCPConn {
|
||||
err = tcpConn.SetNoDelay(true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
splits := strings.Split(string(b[serverName.Index:serverName.Index+serverName.Length]), ".")
|
||||
currentIndex := serverName.Index
|
||||
var striped bool
|
||||
if len(splits) > 3 {
|
||||
suffix := splits[len(splits)-3] + "." + splits[len(splits)-2] + "." + splits[len(splits)-1]
|
||||
if publicSuffixMatcher().Match(suffix) {
|
||||
splits = splits[:len(splits)-3]
|
||||
}
|
||||
striped = true
|
||||
}
|
||||
if !striped && len(splits) > 2 {
|
||||
suffix := splits[len(splits)-2] + "." + splits[len(splits)-1]
|
||||
if publicSuffixMatcher().Match(suffix) {
|
||||
splits = splits[:len(splits)-2]
|
||||
}
|
||||
striped = true
|
||||
}
|
||||
if !striped && len(splits) > 1 {
|
||||
suffix := splits[len(splits)-1]
|
||||
if publicSuffixMatcher().Match(suffix) {
|
||||
splits = splits[:len(splits)-1]
|
||||
}
|
||||
}
|
||||
if len(splits) > 1 && common.Contains(publicPrefix, splits[0]) {
|
||||
currentIndex += len(splits[0]) + 1
|
||||
splits = splits[1:]
|
||||
}
|
||||
var splitIndexes []int
|
||||
for i, split := range splits {
|
||||
splitAt := rand.Intn(len(split))
|
||||
splitIndexes = append(splitIndexes, currentIndex+splitAt)
|
||||
currentIndex += len(split)
|
||||
if i != len(splits)-1 {
|
||||
currentIndex++
|
||||
}
|
||||
}
|
||||
for i := 0; i <= len(splitIndexes); i++ {
|
||||
if i == 0 {
|
||||
_, err = c.Conn.Write(b[:splitIndexes[i]])
|
||||
} else if i == len(splitIndexes) {
|
||||
_, err = c.Conn.Write(b[splitIndexes[i-1]:])
|
||||
} else {
|
||||
_, err = c.Conn.Write(b[splitIndexes[i-1]:splitIndexes[i]])
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if c.syscallConn != nil && i != len(splitIndexes) {
|
||||
err = waitAck(c.ctx, c.syscallConn, c.fallbackDelay)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if isTCPConn {
|
||||
err = tcpConn.SetNoDelay(false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
}
|
||||
return c.Conn.Write(b)
|
||||
}
|
||||
|
||||
func (c *Conn) ReaderReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Conn) WriterReplaceable() bool {
|
||||
return c.firstPacketWritten
|
||||
}
|
||||
|
||||
func (c *Conn) Upstream() any {
|
||||
return c.Conn
|
||||
}
|
||||
137
common/tlsfragment/index.go
Normal file
137
common/tlsfragment/index.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package tf
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
const (
|
||||
recordLayerHeaderLen int = 5
|
||||
handshakeHeaderLen int = 6
|
||||
randomDataLen int = 32
|
||||
sessionIDHeaderLen int = 1
|
||||
cipherSuiteHeaderLen int = 2
|
||||
compressMethodHeaderLen int = 1
|
||||
extensionsHeaderLen int = 2
|
||||
extensionHeaderLen int = 4
|
||||
sniExtensionHeaderLen int = 5
|
||||
contentType uint8 = 22
|
||||
handshakeType uint8 = 1
|
||||
sniExtensionType uint16 = 0
|
||||
sniNameDNSHostnameType uint8 = 0
|
||||
tlsVersionBitmask uint16 = 0xFFFC
|
||||
tls13 uint16 = 0x0304
|
||||
)
|
||||
|
||||
type myServerName struct {
|
||||
Index int
|
||||
Length int
|
||||
sex []byte
|
||||
}
|
||||
|
||||
func indexTLSServerName(payload []byte) *myServerName {
|
||||
if len(payload) < recordLayerHeaderLen || payload[0] != contentType {
|
||||
return nil
|
||||
}
|
||||
segmentLen := binary.BigEndian.Uint16(payload[3:5])
|
||||
if len(payload) < recordLayerHeaderLen+int(segmentLen) {
|
||||
return nil
|
||||
}
|
||||
serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)])
|
||||
if serverName == nil {
|
||||
return nil
|
||||
}
|
||||
serverName.Length += recordLayerHeaderLen
|
||||
return serverName
|
||||
}
|
||||
|
||||
func indexTLSServerNameFromHandshake(hs []byte) *myServerName {
|
||||
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
|
||||
return nil
|
||||
}
|
||||
if hs[0] != handshakeType {
|
||||
return nil
|
||||
}
|
||||
handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])
|
||||
if len(hs[4:]) != int(handshakeLen) {
|
||||
return nil
|
||||
}
|
||||
tlsVersion := uint16(hs[4])<<8 | uint16(hs[5])
|
||||
if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 {
|
||||
return nil
|
||||
}
|
||||
sessionIDLen := hs[38]
|
||||
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) {
|
||||
return nil
|
||||
}
|
||||
cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):]
|
||||
if len(cs) < cipherSuiteHeaderLen {
|
||||
return nil
|
||||
}
|
||||
csLen := uint16(cs[0])<<8 | uint16(cs[1])
|
||||
numCiphers := int(csLen / 2)
|
||||
cipherSuites := make([]uint16, 0, numCiphers)
|
||||
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < numCiphers; i++ {
|
||||
cipherSuite := uint16(cs[2+i<<1])<<8 | uint16(cs[3+i<<1])
|
||||
cipherSuites = append(cipherSuites, cipherSuite)
|
||||
}
|
||||
compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)])
|
||||
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) {
|
||||
return nil
|
||||
}
|
||||
currentIndex := cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen)
|
||||
serverName := indexTLSServerNameFromExtensions(cs[currentIndex:])
|
||||
if serverName == nil {
|
||||
return nil
|
||||
}
|
||||
serverName.Index += currentIndex
|
||||
return serverName
|
||||
}
|
||||
|
||||
func indexTLSServerNameFromExtensions(exs []byte) *myServerName {
|
||||
if len(exs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(exs) < extensionsHeaderLen {
|
||||
return nil
|
||||
}
|
||||
exsLen := uint16(exs[0])<<8 | uint16(exs[1])
|
||||
exs = exs[extensionsHeaderLen:]
|
||||
if len(exs) < int(exsLen) {
|
||||
return nil
|
||||
}
|
||||
for currentIndex := extensionsHeaderLen; len(exs) > 0; {
|
||||
if len(exs) < extensionHeaderLen {
|
||||
return nil
|
||||
}
|
||||
exType := uint16(exs[0])<<8 | uint16(exs[1])
|
||||
exLen := uint16(exs[2])<<8 | uint16(exs[3])
|
||||
if len(exs) < extensionHeaderLen+int(exLen) {
|
||||
return nil
|
||||
}
|
||||
sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)]
|
||||
|
||||
switch exType {
|
||||
case sniExtensionType:
|
||||
if len(sex) < sniExtensionHeaderLen {
|
||||
return nil
|
||||
}
|
||||
sniType := sex[2]
|
||||
if sniType != sniNameDNSHostnameType {
|
||||
return nil
|
||||
}
|
||||
sniLen := uint16(sex[3])<<8 | uint16(sex[4])
|
||||
sex = sex[sniExtensionHeaderLen:]
|
||||
return &myServerName{
|
||||
Index: currentIndex + extensionHeaderLen + sniExtensionHeaderLen,
|
||||
Length: int(sniLen),
|
||||
sex: sex,
|
||||
}
|
||||
}
|
||||
exs = exs[4+exLen:]
|
||||
currentIndex += 4 + int(exLen)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
55
common/tlsfragment/public_suffix.go
Normal file
55
common/tlsfragment/public_suffix.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package tf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing/common/domain"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
var publicPrefix = []string{
|
||||
"www",
|
||||
}
|
||||
|
||||
//go:generate wget -O public_suffix_list.dat https://publicsuffix.org/list/public_suffix_list.dat
|
||||
|
||||
//go:embed public_suffix_list.dat
|
||||
var publicSuffix []byte
|
||||
|
||||
var publicSuffixMatcher = sync.OnceValue(func() *domain.Matcher {
|
||||
matcher, err := initPublicSuffixMatcher()
|
||||
if err != nil {
|
||||
panic(F.ToString("error in initialize public suffix matcher"))
|
||||
}
|
||||
return matcher
|
||||
})
|
||||
|
||||
func initPublicSuffixMatcher() (*domain.Matcher, error) {
|
||||
reader := bufio.NewReader(bytes.NewReader(publicSuffix))
|
||||
var domainList []string
|
||||
for {
|
||||
line, isPrefix, err := reader.ReadLine()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if isPrefix {
|
||||
return nil, E.New("unexpected prefix line")
|
||||
}
|
||||
lineStr := string(line)
|
||||
lineStr = strings.TrimSpace(lineStr)
|
||||
if lineStr == "" || strings.HasPrefix(lineStr, "//") {
|
||||
continue
|
||||
}
|
||||
domainList = append(domainList, lineStr)
|
||||
}
|
||||
return domain.NewMatcher(domainList, nil, false), nil
|
||||
}
|
||||
15692
common/tlsfragment/public_suffix_list.dat
Normal file
15692
common/tlsfragment/public_suffix_list.dat
Normal file
File diff suppressed because it is too large
Load Diff
89
common/tlsfragment/wait_darwin.go
Normal file
89
common/tlsfragment/wait_darwin.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package tf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/control"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
/*
|
||||
const tcpMaxNotifyAck = 10
|
||||
|
||||
type tcpNotifyAckID uint32
|
||||
|
||||
type tcpNotifyAckComplete struct {
|
||||
NotifyPending uint32
|
||||
NotifyCompleteCount uint32
|
||||
NotifyCompleteID [tcpMaxNotifyAck]tcpNotifyAckID
|
||||
}
|
||||
|
||||
var sizeOfTCPNotifyAckComplete = int(unsafe.Sizeof(tcpNotifyAckComplete{}))
|
||||
|
||||
func getsockoptTCPNotifyAckComplete(fd, level, opt int) (*tcpNotifyAckComplete, error) {
|
||||
var value tcpNotifyAckComplete
|
||||
vallen := uint32(sizeOfTCPNotifyAckComplete)
|
||||
err := getsockopt(fd, level, opt, unsafe.Pointer(&value), &vallen)
|
||||
return &value, err
|
||||
}
|
||||
|
||||
//go:linkname getsockopt golang.org/x/sys/unix.getsockopt
|
||||
func getsockopt(s int, level int, name int, val unsafe.Pointer, vallen *uint32) error
|
||||
|
||||
func waitAck(ctx context.Context, conn *net.TCPConn, _ time.Duration) error {
|
||||
const TCP_NOTIFY_ACKNOWLEDGEMENT = 0x212
|
||||
return control.Conn(conn, func(fd uintptr) error {
|
||||
err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, TCP_NOTIFY_ACKNOWLEDGEMENT, 1)
|
||||
if err != nil {
|
||||
if errors.Is(err, unix.EINVAL) {
|
||||
return waitAckFallback(ctx, conn, 0)
|
||||
}
|
||||
return err
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
var ackComplete *tcpNotifyAckComplete
|
||||
ackComplete, err = getsockoptTCPNotifyAckComplete(int(fd), unix.IPPROTO_TCP, TCP_NOTIFY_ACKNOWLEDGEMENT)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ackComplete.NotifyPending == 0 {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
func waitAck(ctx context.Context, conn syscall.Conn, fallbackDelay time.Duration) error {
|
||||
return control.Conn(conn, func(fd uintptr) error {
|
||||
start := time.Now()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
unacked, err := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_NWRITE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if unacked == 0 {
|
||||
if time.Since(start) <= 20*time.Millisecond {
|
||||
// under transparent proxy
|
||||
time.Sleep(fallbackDelay)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
}
|
||||
36
common/tlsfragment/wait_linux.go
Normal file
36
common/tlsfragment/wait_linux.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package tf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/control"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func waitAck(ctx context.Context, conn syscall.Conn, fallbackDelay time.Duration) error {
|
||||
return control.Conn(conn, func(fd uintptr) error {
|
||||
start := time.Now()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
tcpInfo, err := unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tcpInfo.Unacked == 0 {
|
||||
if time.Since(start) <= 20*time.Millisecond {
|
||||
// under transparent proxy
|
||||
time.Sleep(fallbackDelay)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
}
|
||||
14
common/tlsfragment/wait_stub.go
Normal file
14
common/tlsfragment/wait_stub.go
Normal file
@@ -0,0 +1,14 @@
|
||||
//go:build !(linux || darwin)
|
||||
|
||||
package tf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func waitAck(ctx context.Context, conn syscall.Conn, fallbackDelay time.Duration) error {
|
||||
time.Sleep(fallbackDelay)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user