mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-01-28 19:56:49 +00:00
Add tls fragment support
This commit is contained in:
parent
7ecc62f2e0
commit
420d8d144a
|
@ -71,6 +71,8 @@ type InboundContext struct {
|
||||||
UDPDisableDomainUnmapping bool
|
UDPDisableDomainUnmapping bool
|
||||||
UDPConnect bool
|
UDPConnect bool
|
||||||
UDPTimeout time.Duration
|
UDPTimeout time.Duration
|
||||||
|
TLSFragment bool
|
||||||
|
TLSFragmentFallbackDelay time.Duration
|
||||||
|
|
||||||
NetworkStrategy *C.NetworkStrategy
|
NetworkStrategy *C.NetworkStrategy
|
||||||
NetworkType []C.InterfaceType
|
NetworkType []C.InterfaceType
|
||||||
|
|
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
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ const (
|
||||||
StopTimeout = 5 * time.Second
|
StopTimeout = 5 * time.Second
|
||||||
FatalStopTimeout = 10 * time.Second
|
FatalStopTimeout = 10 * time.Second
|
||||||
FakeIPMetadataSaveInterval = 10 * time.Second
|
FakeIPMetadataSaveInterval = 10 * time.Second
|
||||||
|
TLSFragmentFallbackDelay = 500 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
var PortProtocols = map[uint16]string{
|
var PortProtocols = map[uint16]string{
|
||||||
|
|
|
@ -150,6 +150,9 @@ type RawRouteOptionsActionOptions struct {
|
||||||
UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"`
|
UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"`
|
||||||
UDPConnect bool `json:"udp_connect,omitempty"`
|
UDPConnect bool `json:"udp_connect,omitempty"`
|
||||||
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
||||||
|
|
||||||
|
TLSFragment bool `json:"tls_fragment,omitempty"`
|
||||||
|
TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RouteOptionsActionOptions RawRouteOptionsActionOptions
|
type RouteOptionsActionOptions RawRouteOptionsActionOptions
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
|
"github.com/sagernet/sing-box/common/tlsfragment"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
@ -75,6 +76,21 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co
|
||||||
m.logger.ErrorContext(ctx, err)
|
m.logger.ErrorContext(ctx, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if metadata.TLSFragment {
|
||||||
|
fallbackDelay := metadata.TLSFragmentFallbackDelay
|
||||||
|
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
|
||||||
|
}
|
||||||
m.access.Lock()
|
m.access.Lock()
|
||||||
element := m.connections.PushBack(conn)
|
element := m.connections.PushBack(conn)
|
||||||
m.access.Unlock()
|
m.access.Unlock()
|
||||||
|
|
|
@ -454,6 +454,10 @@ match:
|
||||||
if routeOptions.UDPTimeout > 0 {
|
if routeOptions.UDPTimeout > 0 {
|
||||||
metadata.UDPTimeout = routeOptions.UDPTimeout
|
metadata.UDPTimeout = routeOptions.UDPTimeout
|
||||||
}
|
}
|
||||||
|
if routeOptions.TLSFragment {
|
||||||
|
metadata.TLSFragment = true
|
||||||
|
metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay
|
||||||
|
}
|
||||||
}
|
}
|
||||||
switch action := currentRule.Action().(type) {
|
switch action := currentRule.Action().(type) {
|
||||||
case *rule.RuleActionSniff:
|
case *rule.RuleActionSniff:
|
||||||
|
|
|
@ -36,6 +36,8 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
|
||||||
FallbackDelay: time.Duration(action.RouteOptions.FallbackDelay),
|
FallbackDelay: time.Duration(action.RouteOptions.FallbackDelay),
|
||||||
UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping,
|
UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping,
|
||||||
UDPConnect: action.RouteOptions.UDPConnect,
|
UDPConnect: action.RouteOptions.UDPConnect,
|
||||||
|
TLSFragment: action.RouteOptions.TLSFragment,
|
||||||
|
TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
case C.RuleActionTypeRouteOptions:
|
case C.RuleActionTypeRouteOptions:
|
||||||
|
@ -47,6 +49,8 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
|
||||||
UDPDisableDomainUnmapping: action.RouteOptionsOptions.UDPDisableDomainUnmapping,
|
UDPDisableDomainUnmapping: action.RouteOptionsOptions.UDPDisableDomainUnmapping,
|
||||||
UDPConnect: action.RouteOptionsOptions.UDPConnect,
|
UDPConnect: action.RouteOptionsOptions.UDPConnect,
|
||||||
UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout),
|
UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout),
|
||||||
|
TLSFragment: action.RouteOptionsOptions.TLSFragment,
|
||||||
|
TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay),
|
||||||
}, nil
|
}, nil
|
||||||
case C.RuleActionTypeDirect:
|
case C.RuleActionTypeDirect:
|
||||||
directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false)
|
directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false)
|
||||||
|
@ -142,6 +146,9 @@ func (r *RuleActionRoute) String() string {
|
||||||
if r.UDPConnect {
|
if r.UDPConnect {
|
||||||
descriptions = append(descriptions, "udp-connect")
|
descriptions = append(descriptions, "udp-connect")
|
||||||
}
|
}
|
||||||
|
if r.TLSFragment {
|
||||||
|
descriptions = append(descriptions, "tls-fragment")
|
||||||
|
}
|
||||||
return F.ToString("route(", strings.Join(descriptions, ","), ")")
|
return F.ToString("route(", strings.Join(descriptions, ","), ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,6 +162,8 @@ type RuleActionRouteOptions struct {
|
||||||
UDPDisableDomainUnmapping bool
|
UDPDisableDomainUnmapping bool
|
||||||
UDPConnect bool
|
UDPConnect bool
|
||||||
UDPTimeout time.Duration
|
UDPTimeout time.Duration
|
||||||
|
TLSFragment bool
|
||||||
|
TLSFragmentFallbackDelay time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RuleActionRouteOptions) Type() string {
|
func (r *RuleActionRouteOptions) Type() string {
|
||||||
|
@ -169,6 +178,9 @@ func (r *RuleActionRouteOptions) String() string {
|
||||||
if r.UDPConnect {
|
if r.UDPConnect {
|
||||||
descriptions = append(descriptions, "udp-connect")
|
descriptions = append(descriptions, "udp-connect")
|
||||||
}
|
}
|
||||||
|
if r.UDPTimeout > 0 {
|
||||||
|
descriptions = append(descriptions, "udp-timeout")
|
||||||
|
}
|
||||||
return F.ToString("route-options(", strings.Join(descriptions, ","), ")")
|
return F.ToString("route-options(", strings.Join(descriptions, ","), ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,10 @@ func (ho *HTTPObfs) Write(b []byte) (int, error) {
|
||||||
return ho.Conn.Write(b)
|
return ho.Conn.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ho *HTTPObfs) Upstream() any {
|
||||||
|
return ho.Conn
|
||||||
|
}
|
||||||
|
|
||||||
// NewHTTPObfs return a HTTPObfs
|
// NewHTTPObfs return a HTTPObfs
|
||||||
func NewHTTPObfs(conn net.Conn, host string, port string) net.Conn {
|
func NewHTTPObfs(conn net.Conn, host string, port string) net.Conn {
|
||||||
return &HTTPObfs{
|
return &HTTPObfs{
|
||||||
|
|
|
@ -113,6 +113,10 @@ func (to *TLSObfs) write(b []byte) (int, error) {
|
||||||
return len(b), err
|
return len(b), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (to *TLSObfs) Upstream() any {
|
||||||
|
return to.Conn
|
||||||
|
}
|
||||||
|
|
||||||
// NewTLSObfs return a SimpleObfs
|
// NewTLSObfs return a SimpleObfs
|
||||||
func NewTLSObfs(conn net.Conn, server string) net.Conn {
|
func NewTLSObfs(conn net.Conn, server string) net.Conn {
|
||||||
return &TLSObfs{
|
return &TLSObfs{
|
||||||
|
|
Loading…
Reference in a new issue