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