mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-03-02 02:39:53 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11000dccd7 | ||
|
|
0818aa54aa | ||
|
|
edbfebeacd | ||
|
|
06f5fbac06 | ||
|
|
f38fc2020f | ||
|
|
97bce45eba | ||
|
|
bc28cd486a | ||
|
|
cdabd1e8b1 | ||
|
|
c5b0f00bb2 | ||
|
|
c128d23dec | ||
|
|
ee37a353d0 | ||
|
|
0cf37de1a8 | ||
|
|
ae6069c178 | ||
|
|
c8e33a4347 | ||
|
|
19a6b5d6f7 | ||
|
|
efb800866e | ||
|
|
94c8d60f72 | ||
|
|
0f2baca2de | ||
|
|
b18a33552c | ||
|
|
487de9b548 | ||
|
|
1a6230ec03 | ||
|
|
e6bf56b9af | ||
|
|
0ad9ac325a | ||
|
|
d6b1263236 | ||
|
|
4d7670339b | ||
|
|
0cffc8d76d | ||
|
|
1f8bee9710 | ||
|
|
eb30d3f331 | ||
|
|
10f4bebdfa | ||
|
|
06387d5045 | ||
|
|
c393e917eb | ||
|
|
4f0a6fa117 | ||
|
|
4f9bfd216f | ||
|
|
498f81aad3 | ||
|
|
9168bee6b7 | ||
|
|
e6c0e3b19c | ||
|
|
287f9e5185 | ||
|
|
c456370f4f | ||
|
|
10ef29f5cd | ||
|
|
85ba7f6a0a | ||
|
|
7daf37bc15 | ||
|
|
64015b7634 |
43
.github/patch/go1.25.patch
vendored
43
.github/patch/go1.25.patch
vendored
@@ -1,4 +1,5 @@
|
||||
Subject: [PATCH] Fix os.RemoveAll not working on Windows7
|
||||
Subject: [PATCH] Revert "os: remove 5ms sleep on Windows in (*Process).Wait"
|
||||
Fix os.RemoveAll not working on Windows7
|
||||
Revert "runtime: always use LoadLibraryEx to load system libraries"
|
||||
Revert "syscall: remove Windows 7 console handle workaround"
|
||||
Revert "net: remove sysSocket fallback for Windows 7"
|
||||
@@ -841,3 +842,43 @@ diff --git a/src/os/root_windows.go b/src/os/root_windows.go
|
||||
+func checkPathEscapesLstat(r *Root, name string) error {
|
||||
+ return checkPathEscapes(r, name)
|
||||
+}
|
||||
Index: src/os/exec_windows.go
|
||||
IDEA additional info:
|
||||
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||
<+>UTF-8
|
||||
===================================================================
|
||||
diff --git a/src/os/exec_windows.go b/src/os/exec_windows.go
|
||||
--- a/src/os/exec_windows.go (revision 0a52622d2331ff975fb0442617ec19bc352bb2ed)
|
||||
+++ b/src/os/exec_windows.go (revision fb3d09a67fe97008ad76fea97ae88170072cbdbb)
|
||||
@@ -10,6 +10,7 @@
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
+ _ "unsafe"
|
||||
)
|
||||
|
||||
// Note that Process.handle is never nil because Windows always requires
|
||||
@@ -49,9 +50,23 @@
|
||||
// than statusDone.
|
||||
p.doRelease(statusReleased)
|
||||
|
||||
+ var maj, min, build uint32
|
||||
+ rtlGetNtVersionNumbers(&maj, &min, &build)
|
||||
+ if maj < 10 {
|
||||
+ // NOTE(brainman): It seems that sometimes process is not dead
|
||||
+ // when WaitForSingleObject returns. But we do not know any
|
||||
+ // other way to wait for it. Sleeping for a while seems to do
|
||||
+ // the trick sometimes.
|
||||
+ // See https://golang.org/issue/25965 for details.
|
||||
+ time.Sleep(5 * time.Millisecond)
|
||||
+ }
|
||||
+
|
||||
return &ProcessState{p.Pid, syscall.WaitStatus{ExitCode: ec}, &u}, nil
|
||||
}
|
||||
|
||||
+//go:linkname rtlGetNtVersionNumbers syscall.rtlGetNtVersionNumbers
|
||||
+func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32)
|
||||
+
|
||||
func (p *Process) signal(sig Signal) error {
|
||||
handle, status := p.handleTransientAcquire()
|
||||
switch status {
|
||||
|
||||
43
.github/patch/go1.26.patch
vendored
43
.github/patch/go1.26.patch
vendored
@@ -1,4 +1,5 @@
|
||||
Subject: [PATCH] Fix os.RemoveAll not working on Windows7
|
||||
Subject: [PATCH] Revert "os: remove 5ms sleep on Windows in (*Process).Wait"
|
||||
Fix os.RemoveAll not working on Windows7
|
||||
Revert "runtime: always use LoadLibraryEx to load system libraries"
|
||||
Revert "syscall: remove Windows 7 console handle workaround"
|
||||
Revert "net: remove sysSocket fallback for Windows 7"
|
||||
@@ -840,3 +841,43 @@ diff --git a/src/os/root_windows.go b/src/os/root_windows.go
|
||||
+func checkPathEscapesLstat(r *Root, name string) error {
|
||||
+ return checkPathEscapes(r, name)
|
||||
+}
|
||||
Index: src/os/exec_windows.go
|
||||
IDEA additional info:
|
||||
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||
<+>UTF-8
|
||||
===================================================================
|
||||
diff --git a/src/os/exec_windows.go b/src/os/exec_windows.go
|
||||
--- a/src/os/exec_windows.go (revision d47e0d22130d597dcf9daa6b41fd9501274f0cb2)
|
||||
+++ b/src/os/exec_windows.go (revision 00e8daec9a4d88f44a8dc55d3bdb71878e525b41)
|
||||
@@ -10,6 +10,7 @@
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
+ _ "unsafe"
|
||||
)
|
||||
|
||||
// Note that Process.handle is never nil because Windows always requires
|
||||
@@ -49,9 +50,23 @@
|
||||
// than statusDone.
|
||||
p.doRelease(statusReleased)
|
||||
|
||||
+ var maj, min, build uint32
|
||||
+ rtlGetNtVersionNumbers(&maj, &min, &build)
|
||||
+ if maj < 10 {
|
||||
+ // NOTE(brainman): It seems that sometimes process is not dead
|
||||
+ // when WaitForSingleObject returns. But we do not know any
|
||||
+ // other way to wait for it. Sleeping for a while seems to do
|
||||
+ // the trick sometimes.
|
||||
+ // See https://golang.org/issue/25965 for details.
|
||||
+ time.Sleep(5 * time.Millisecond)
|
||||
+ }
|
||||
+
|
||||
return &ProcessState{p.Pid, syscall.WaitStatus{ExitCode: ec}, &u}, nil
|
||||
}
|
||||
|
||||
+//go:linkname rtlGetNtVersionNumbers syscall.rtlGetNtVersionNumbers
|
||||
+func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32)
|
||||
+
|
||||
func (p *Process) signal(sig Signal) error {
|
||||
handle, status := p.handleTransientAcquire()
|
||||
switch status {
|
||||
|
||||
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -155,12 +155,14 @@ jobs:
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25'
|
||||
check-latest: true # Always check for the latest patch release
|
||||
|
||||
- name: Set up Go
|
||||
if: ${{ matrix.jobs.goversion != '' && matrix.jobs.abi != '1' }}
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.jobs.goversion }}
|
||||
check-latest: true # Always check for the latest patch release
|
||||
|
||||
- name: Set up Go1.24 loongarch abi1
|
||||
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
|
||||
@@ -178,6 +180,7 @@ jobs:
|
||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||
# f0894a00f4b756d4b9b4078af2e686b359493583: "os: remove 5ms sleep on Windows in (*Process).Wait"
|
||||
# sepical fix:
|
||||
# - os.RemoveAll not working on Windows7
|
||||
- name: Revert Golang1.25 commit for Windows7/8
|
||||
|
||||
7
.github/workflows/test.yml
vendored
7
.github/workflows/test.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- 'ubuntu-24.04-arm' # arm64 linux
|
||||
- 'macos-15-intel' # amd64 macos
|
||||
go-version:
|
||||
- '1.26.0-rc.1'
|
||||
- '1.26.0-rc.2'
|
||||
- '1.25'
|
||||
- '1.24'
|
||||
- '1.23'
|
||||
@@ -48,15 +48,16 @@ jobs:
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true # Always check for the latest patch release
|
||||
|
||||
- name: Revert Golang commit for Windows7/8
|
||||
if: ${{ runner.os == 'Windows' && matrix.go-version != '1.20' && matrix.go-version != '1.26.0-rc.1' }}
|
||||
if: ${{ runner.os == 'Windows' && matrix.go-version != '1.20' && matrix.go-version != '1.26.0-rc.2' }}
|
||||
run: |
|
||||
cd $(go env GOROOT)
|
||||
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go${{matrix.go-version}}.patch
|
||||
|
||||
- name: Revert Golang commit for Windows7/8
|
||||
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.26.0-rc.1' }}
|
||||
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.26.0-rc.2' }}
|
||||
run: |
|
||||
cd $(go env GOROOT)
|
||||
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go1.26.patch
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/metacubex/mihomo/component/keepalive"
|
||||
"github.com/metacubex/mihomo/component/mptcp"
|
||||
|
||||
"github.com/metacubex/tfo-go"
|
||||
)
|
||||
@@ -34,13 +35,13 @@ func Tfo() bool {
|
||||
func SetMPTCP(open bool) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
setMultiPathTCP(&lc.ListenConfig, open)
|
||||
mptcp.SetNetListenConfig(&lc.ListenConfig, open)
|
||||
}
|
||||
|
||||
func MPTCP() bool {
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
return getMultiPathTCP(&lc.ListenConfig)
|
||||
return mptcp.GetNetListenConfig(&lc.ListenConfig)
|
||||
}
|
||||
|
||||
func preResolve(network, address string) (string, error) {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
//go:build !go1.21
|
||||
|
||||
package inbound
|
||||
|
||||
import "net"
|
||||
|
||||
const multipathTCPAvailable = false
|
||||
|
||||
func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) {
|
||||
}
|
||||
|
||||
func getMultiPathTCP(listenConfig *net.ListenConfig) bool {
|
||||
return false
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//go:build go1.21
|
||||
|
||||
package inbound
|
||||
|
||||
import "net"
|
||||
|
||||
const multipathTCPAvailable = true
|
||||
|
||||
func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) {
|
||||
listenConfig.SetMultipathTCP(open)
|
||||
}
|
||||
|
||||
func getMultiPathTCP(listenConfig *net.ListenConfig) bool {
|
||||
return listenConfig.MultipathTCP()
|
||||
}
|
||||
@@ -114,6 +114,7 @@ type kcpTunOption struct {
|
||||
AutoExpire int `obfs:"autoexpire,omitempty"`
|
||||
ScavengeTTL int `obfs:"scavengettl,omitempty"`
|
||||
MTU int `obfs:"mtu,omitempty"`
|
||||
RateLimit int `obfs:"ratelimit,omitempty"`
|
||||
SndWnd int `obfs:"sndwnd,omitempty"`
|
||||
RcvWnd int `obfs:"rcvwnd,omitempty"`
|
||||
DataShard int `obfs:"datashard,omitempty"`
|
||||
@@ -128,6 +129,7 @@ type kcpTunOption struct {
|
||||
SockBuf int `obfs:"sockbuf,omitempty"`
|
||||
SmuxVer int `obfs:"smuxver,omitempty"`
|
||||
SmuxBuf int `obfs:"smuxbuf,omitempty"`
|
||||
FrameSize int `obfs:"framesize,omitempty"`
|
||||
StreamBuf int `obfs:"streambuf,omitempty"`
|
||||
KeepAlive int `obfs:"keepalive,omitempty"`
|
||||
}
|
||||
@@ -426,6 +428,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
AutoExpire: kcptunOpt.AutoExpire,
|
||||
ScavengeTTL: kcptunOpt.ScavengeTTL,
|
||||
MTU: kcptunOpt.MTU,
|
||||
RateLimit: kcptunOpt.RateLimit,
|
||||
SndWnd: kcptunOpt.SndWnd,
|
||||
RcvWnd: kcptunOpt.RcvWnd,
|
||||
DataShard: kcptunOpt.DataShard,
|
||||
@@ -440,6 +443,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
SockBuf: kcptunOpt.SockBuf,
|
||||
SmuxVer: kcptunOpt.SmuxVer,
|
||||
SmuxBuf: kcptunOpt.SmuxBuf,
|
||||
FrameSize: kcptunOpt.FrameSize,
|
||||
StreamBuf: kcptunOpt.StreamBuf,
|
||||
KeepAlive: kcptunOpt.KeepAlive,
|
||||
})
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
N "github.com/metacubex/mihomo/common/net"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
@@ -16,6 +18,14 @@ type Sudoku struct {
|
||||
*Base
|
||||
option *SudokuOption
|
||||
baseConf sudoku.ProtocolConfig
|
||||
|
||||
httpMaskMu sync.Mutex
|
||||
httpMaskClient *sudoku.HTTPMaskTunnelClient
|
||||
|
||||
muxMu sync.Mutex
|
||||
muxClient *sudoku.MultiplexClient
|
||||
muxBackoffUntil time.Time
|
||||
muxLastErr error
|
||||
}
|
||||
|
||||
type SudokuOption struct {
|
||||
@@ -30,9 +40,13 @@ type SudokuOption struct {
|
||||
TableType string `proxy:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
|
||||
EnablePureDownlink *bool `proxy:"enable-pure-downlink,omitempty"`
|
||||
HTTPMask bool `proxy:"http-mask,omitempty"`
|
||||
HTTPMaskStrategy string `proxy:"http-mask-strategy,omitempty"` // "random" (default), "post", "websocket"
|
||||
CustomTable string `proxy:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
|
||||
CustomTables []string `proxy:"custom-tables,omitempty"` // optional table rotation patterns, overrides custom-table when non-empty
|
||||
HTTPMaskMode string `proxy:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
|
||||
HTTPMaskTLS bool `proxy:"http-mask-tls,omitempty"` // only for http-mask-mode stream/poll/auto
|
||||
HTTPMaskHost string `proxy:"http-mask-host,omitempty"` // optional Host/SNI override (domain or domain:port)
|
||||
PathRoot string `proxy:"path-root,omitempty"` // optional first-level path prefix for HTTP tunnel endpoints
|
||||
HTTPMaskMultiplex string `proxy:"http-mask-multiplex,omitempty"` // "off" (default), "auto" (reuse h1/h2), "on" (single tunnel, multi-target)
|
||||
CustomTable string `proxy:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
|
||||
CustomTables []string `proxy:"custom-tables,omitempty"` // optional table rotation patterns, overrides custom-table when non-empty
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
@@ -42,26 +56,20 @@ func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err := s.dialer.DialContext(ctx, "tcp", s.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
||||
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
|
||||
if muxMode == "on" && !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
|
||||
stream, muxErr := s.dialMultiplex(ctx, cfg.TargetAddress, muxMode)
|
||||
if muxErr == nil {
|
||||
return NewConn(stream, s), nil
|
||||
}
|
||||
return nil, muxErr
|
||||
}
|
||||
|
||||
defer func() {
|
||||
safeConnClose(c, err)
|
||||
}()
|
||||
|
||||
if ctx.Done() != nil {
|
||||
done := N.SetupContextForConn(ctx, c)
|
||||
defer done(&err)
|
||||
}
|
||||
|
||||
c, err = sudoku.ClientHandshakeWithOptions(c, cfg, sudoku.ClientHandshakeOptions{
|
||||
HTTPMaskStrategy: s.option.HTTPMaskStrategy,
|
||||
})
|
||||
c, err := s.dialAndHandshake(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { safeConnClose(c, err) }()
|
||||
|
||||
addrBuf, err := sudoku.EncodeAddress(cfg.TargetAddress)
|
||||
if err != nil {
|
||||
@@ -69,7 +77,6 @@ func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
|
||||
}
|
||||
|
||||
if _, err = c.Write(addrBuf); err != nil {
|
||||
_ = c.Close()
|
||||
return nil, fmt.Errorf("send target address failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -87,23 +94,7 @@ func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err := s.dialer.DialContext(ctx, "tcp", s.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
safeConnClose(c, err)
|
||||
}()
|
||||
|
||||
if ctx.Done() != nil {
|
||||
done := N.SetupContextForConn(ctx, c)
|
||||
defer done(&err)
|
||||
}
|
||||
|
||||
c, err = sudoku.ClientHandshakeWithOptions(c, cfg, sudoku.ClientHandshakeOptions{
|
||||
HTTPMaskStrategy: s.option.HTTPMaskStrategy,
|
||||
})
|
||||
c, err := s.dialAndHandshake(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -190,6 +181,17 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
|
||||
EnablePureDownlink: enablePureDownlink,
|
||||
HandshakeTimeoutSeconds: defaultConf.HandshakeTimeoutSeconds,
|
||||
DisableHTTPMask: !option.HTTPMask,
|
||||
HTTPMaskMode: defaultConf.HTTPMaskMode,
|
||||
HTTPMaskTLSEnabled: option.HTTPMaskTLS,
|
||||
HTTPMaskHost: option.HTTPMaskHost,
|
||||
HTTPMaskPathRoot: strings.TrimSpace(option.PathRoot),
|
||||
HTTPMaskMultiplex: defaultConf.HTTPMaskMultiplex,
|
||||
}
|
||||
if option.HTTPMaskMode != "" {
|
||||
baseConf.HTTPMaskMode = option.HTTPMaskMode
|
||||
}
|
||||
if option.HTTPMaskMultiplex != "" {
|
||||
baseConf.HTTPMaskMultiplex = option.HTTPMaskMultiplex
|
||||
}
|
||||
tables, err := sudoku.NewTablesWithCustomPatterns(sudoku.ClientAEADSeed(option.Key), tableType, option.CustomTable, option.CustomTables)
|
||||
if err != nil {
|
||||
@@ -223,3 +225,212 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
|
||||
outbound.dialer = option.NewDialer(outbound.DialOptions())
|
||||
return outbound, nil
|
||||
}
|
||||
|
||||
func (s *Sudoku) Close() error {
|
||||
s.resetMuxClient()
|
||||
s.resetHTTPMaskClient()
|
||||
return s.Base.Close()
|
||||
}
|
||||
|
||||
func normalizeHTTPMaskMultiplex(mode string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
||||
case "", "off":
|
||||
return "off"
|
||||
case "auto":
|
||||
return "auto"
|
||||
case "on":
|
||||
return "on"
|
||||
default:
|
||||
return "off"
|
||||
}
|
||||
}
|
||||
|
||||
func httpTunnelModeEnabled(mode string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
||||
case "stream", "poll", "auto":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sudoku) dialAndHandshake(ctx context.Context, cfg *sudoku.ProtocolConfig) (_ net.Conn, err error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("config is required")
|
||||
}
|
||||
|
||||
handshakeCfg := *cfg
|
||||
if !handshakeCfg.DisableHTTPMask && httpTunnelModeEnabled(handshakeCfg.HTTPMaskMode) {
|
||||
handshakeCfg.DisableHTTPMask = true
|
||||
}
|
||||
|
||||
upgrade := func(raw net.Conn) (net.Conn, error) {
|
||||
return sudoku.ClientHandshakeWithOptions(raw, &handshakeCfg, sudoku.ClientHandshakeOptions{})
|
||||
}
|
||||
|
||||
var (
|
||||
c net.Conn
|
||||
handshakeDone bool
|
||||
)
|
||||
if !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
|
||||
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
|
||||
switch muxMode {
|
||||
case "auto", "on":
|
||||
client, errX := s.getOrCreateHTTPMaskClient(cfg)
|
||||
if errX != nil {
|
||||
return nil, errX
|
||||
}
|
||||
c, err = client.Dial(ctx, upgrade)
|
||||
default:
|
||||
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext, upgrade)
|
||||
}
|
||||
if err == nil && c != nil {
|
||||
handshakeDone = true
|
||||
}
|
||||
}
|
||||
if c == nil && err == nil {
|
||||
c, err = s.dialer.DialContext(ctx, "tcp", s.addr)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
||||
}
|
||||
|
||||
defer func() { safeConnClose(c, err) }()
|
||||
|
||||
if ctx.Done() != nil {
|
||||
done := N.SetupContextForConn(ctx, c)
|
||||
defer done(&err)
|
||||
}
|
||||
|
||||
if !handshakeDone {
|
||||
c, err = sudoku.ClientHandshakeWithOptions(c, &handshakeCfg, sudoku.ClientHandshakeOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *Sudoku) dialMultiplex(ctx context.Context, targetAddress string, mode string) (net.Conn, error) {
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
client, err := s.getOrCreateMuxClient(ctx, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stream, err := client.Dial(ctx, targetAddress)
|
||||
if err != nil {
|
||||
s.resetMuxClient()
|
||||
continue
|
||||
}
|
||||
|
||||
return stream, nil
|
||||
}
|
||||
return nil, fmt.Errorf("multiplex open stream failed")
|
||||
}
|
||||
|
||||
func (s *Sudoku) getOrCreateMuxClient(ctx context.Context, mode string) (*sudoku.MultiplexClient, error) {
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("nil adapter")
|
||||
}
|
||||
|
||||
if mode == "auto" {
|
||||
s.muxMu.Lock()
|
||||
backoffUntil := s.muxBackoffUntil
|
||||
lastErr := s.muxLastErr
|
||||
s.muxMu.Unlock()
|
||||
if time.Now().Before(backoffUntil) {
|
||||
return nil, fmt.Errorf("multiplex temporarily disabled: %v", lastErr)
|
||||
}
|
||||
}
|
||||
|
||||
s.muxMu.Lock()
|
||||
if s.muxClient != nil && !s.muxClient.IsClosed() {
|
||||
client := s.muxClient
|
||||
s.muxMu.Unlock()
|
||||
return client, nil
|
||||
}
|
||||
s.muxMu.Unlock()
|
||||
|
||||
s.muxMu.Lock()
|
||||
defer s.muxMu.Unlock()
|
||||
|
||||
if s.muxClient != nil && !s.muxClient.IsClosed() {
|
||||
return s.muxClient, nil
|
||||
}
|
||||
|
||||
baseCfg := s.baseConf
|
||||
baseConn, err := s.dialAndHandshake(ctx, &baseCfg)
|
||||
if err != nil {
|
||||
if mode == "auto" {
|
||||
s.muxLastErr = err
|
||||
s.muxBackoffUntil = time.Now().Add(45 * time.Second)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := sudoku.StartMultiplexClient(baseConn)
|
||||
if err != nil {
|
||||
_ = baseConn.Close()
|
||||
if mode == "auto" {
|
||||
s.muxLastErr = err
|
||||
s.muxBackoffUntil = time.Now().Add(45 * time.Second)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.muxClient = client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (s *Sudoku) noteMuxFailure(mode string, err error) {
|
||||
if mode != "auto" {
|
||||
return
|
||||
}
|
||||
s.muxMu.Lock()
|
||||
s.muxLastErr = err
|
||||
s.muxBackoffUntil = time.Now().Add(45 * time.Second)
|
||||
s.muxMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Sudoku) resetMuxClient() {
|
||||
s.muxMu.Lock()
|
||||
defer s.muxMu.Unlock()
|
||||
if s.muxClient != nil {
|
||||
_ = s.muxClient.Close()
|
||||
s.muxClient = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sudoku) getOrCreateHTTPMaskClient(cfg *sudoku.ProtocolConfig) (*sudoku.HTTPMaskTunnelClient, error) {
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("nil adapter")
|
||||
}
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("config is required")
|
||||
}
|
||||
|
||||
s.httpMaskMu.Lock()
|
||||
defer s.httpMaskMu.Unlock()
|
||||
|
||||
if s.httpMaskClient != nil {
|
||||
return s.httpMaskClient, nil
|
||||
}
|
||||
|
||||
c, err := sudoku.NewHTTPMaskTunnelClient(cfg.ServerAddress, cfg, s.dialer.DialContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.httpMaskClient = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *Sudoku) resetHTTPMaskClient() {
|
||||
s.httpMaskMu.Lock()
|
||||
defer s.httpMaskMu.Unlock()
|
||||
if s.httpMaskClient != nil {
|
||||
s.httpMaskClient.CloseIdleConnections()
|
||||
s.httpMaskClient = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,6 +356,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
||||
t.gunTLSConfig = tlsConfig
|
||||
t.gunConfig = &gun.Config{
|
||||
ServiceName: option.GrpcOpts.GrpcServiceName,
|
||||
UserAgent: option.GrpcOpts.GrpcUserAgent,
|
||||
Host: option.SNI,
|
||||
ClientFingerprint: option.ClientFingerprint,
|
||||
}
|
||||
|
||||
@@ -462,6 +462,7 @@ func NewVless(option VlessOption) (*Vless, error) {
|
||||
|
||||
gunConfig := &gun.Config{
|
||||
ServiceName: v.option.GrpcOpts.GrpcServiceName,
|
||||
UserAgent: v.option.GrpcOpts.GrpcUserAgent,
|
||||
Host: v.option.ServerName,
|
||||
ClientFingerprint: v.option.ClientFingerprint,
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ type HTTP2Options struct {
|
||||
|
||||
type GrpcOptions struct {
|
||||
GrpcServiceName string `proxy:"grpc-service-name,omitempty"`
|
||||
GrpcUserAgent string `proxy:"grpc-user-agent,omitempty"`
|
||||
}
|
||||
|
||||
type WSOptions struct {
|
||||
@@ -467,6 +468,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
||||
|
||||
gunConfig := &gun.Config{
|
||||
ServiceName: v.option.GrpcOpts.GrpcServiceName,
|
||||
UserAgent: v.option.GrpcOpts.GrpcUserAgent,
|
||||
Host: v.option.ServerName,
|
||||
ClientFingerprint: v.option.ClientFingerprint,
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
"github.com/metacubex/mihomo/common/convert"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/common/yaml"
|
||||
"github.com/metacubex/mihomo/component/profile/cachefile"
|
||||
"github.com/metacubex/mihomo/component/resource"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
@@ -21,7 +22,6 @@ import (
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"github.com/metacubex/http"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
//go:build android && cgo
|
||||
// +build android,cgo
|
||||
|
||||
// kanged from https://github.com/golang/mobile/blob/c713f31d574bb632a93f169b2cc99c9e753fef0e/app/android.go#L89
|
||||
|
||||
|
||||
674
common/deque/deque.go
Normal file
674
common/deque/deque.go
Normal file
@@ -0,0 +1,674 @@
|
||||
package deque
|
||||
|
||||
// copy and modified from https://github.com/gammazero/deque/blob/v1.2.0/deque.go
|
||||
// which is licensed under MIT.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// minCapacity is the smallest capacity that deque may have. Must be power of 2
|
||||
// for bitwise modulus: x % n == x & (n - 1).
|
||||
const minCapacity = 8
|
||||
|
||||
// Deque represents a single instance of the deque data structure. A Deque
|
||||
// instance contains items of the type specified by the type argument.
|
||||
//
|
||||
// For example, to create a Deque that contains strings do one of the
|
||||
// following:
|
||||
//
|
||||
// var stringDeque deque.Deque[string]
|
||||
// stringDeque := new(deque.Deque[string])
|
||||
// stringDeque := &deque.Deque[string]{}
|
||||
//
|
||||
// To create a Deque that will never resize to have space for less than 64
|
||||
// items, specify a base capacity:
|
||||
//
|
||||
// var d deque.Deque[int]
|
||||
// d.SetBaseCap(64)
|
||||
//
|
||||
// To ensure the Deque can store 1000 items without needing to resize while
|
||||
// items are added:
|
||||
//
|
||||
// d.Grow(1000)
|
||||
//
|
||||
// Any values supplied to [SetBaseCap] and [Grow] are rounded up to the nearest
|
||||
// power of 2, since the Deque grows by powers of 2.
|
||||
type Deque[T any] struct {
|
||||
buf []T
|
||||
head int
|
||||
tail int
|
||||
count int
|
||||
minCap int
|
||||
}
|
||||
|
||||
// Cap returns the current capacity of the Deque. If q is nil, q.Cap() is zero.
|
||||
func (q *Deque[T]) Cap() int {
|
||||
if q == nil {
|
||||
return 0
|
||||
}
|
||||
return len(q.buf)
|
||||
}
|
||||
|
||||
// Len returns the number of elements currently stored in the queue. If q is
|
||||
// nil, q.Len() returns zero.
|
||||
func (q *Deque[T]) Len() int {
|
||||
if q == nil {
|
||||
return 0
|
||||
}
|
||||
return q.count
|
||||
}
|
||||
|
||||
// PushBack appends an element to the back of the queue. Implements FIFO when
|
||||
// elements are removed with [PopFront], and LIFO when elements are removed with
|
||||
// [PopBack].
|
||||
func (q *Deque[T]) PushBack(elem T) {
|
||||
q.growIfFull()
|
||||
|
||||
q.buf[q.tail] = elem
|
||||
// Calculate new tail position.
|
||||
q.tail = q.next(q.tail)
|
||||
q.count++
|
||||
}
|
||||
|
||||
// PushFront prepends an element to the front of the queue.
|
||||
func (q *Deque[T]) PushFront(elem T) {
|
||||
q.growIfFull()
|
||||
|
||||
// Calculate new head position.
|
||||
q.head = q.prev(q.head)
|
||||
q.buf[q.head] = elem
|
||||
q.count++
|
||||
}
|
||||
|
||||
// PopFront removes and returns the element from the front of the queue.
|
||||
// Implements FIFO when used with [PushBack]. If the queue is empty, the call
|
||||
// panics.
|
||||
func (q *Deque[T]) PopFront() T {
|
||||
if q.count <= 0 {
|
||||
panic("deque: PopFront() called on empty queue")
|
||||
}
|
||||
ret := q.buf[q.head]
|
||||
var zero T
|
||||
q.buf[q.head] = zero
|
||||
// Calculate new head position.
|
||||
q.head = q.next(q.head)
|
||||
q.count--
|
||||
|
||||
q.shrinkIfExcess()
|
||||
return ret
|
||||
}
|
||||
|
||||
// IterPopFront returns an iterator that iteratively removes items from the
|
||||
// front of the deque. This is more efficient than removing items one at a time
|
||||
// because it avoids intermediate resizing. If a resize is necessary, only one
|
||||
// is done when iteration ends.
|
||||
func (q *Deque[T]) IterPopFront() func(yield func(T) bool) {
|
||||
return func(yield func(T) bool) {
|
||||
if q.Len() == 0 {
|
||||
return
|
||||
}
|
||||
var zero T
|
||||
for q.count != 0 {
|
||||
ret := q.buf[q.head]
|
||||
q.buf[q.head] = zero
|
||||
q.head = q.next(q.head)
|
||||
q.count--
|
||||
if !yield(ret) {
|
||||
break
|
||||
}
|
||||
}
|
||||
q.shrinkToFit()
|
||||
}
|
||||
}
|
||||
|
||||
// PopBack removes and returns the element from the back of the queue.
|
||||
// Implements LIFO when used with [PushBack]. If the queue is empty, the call
|
||||
// panics.
|
||||
func (q *Deque[T]) PopBack() T {
|
||||
if q.count <= 0 {
|
||||
panic("deque: PopBack() called on empty queue")
|
||||
}
|
||||
|
||||
// Calculate new tail position
|
||||
q.tail = q.prev(q.tail)
|
||||
|
||||
// Remove value at tail.
|
||||
ret := q.buf[q.tail]
|
||||
var zero T
|
||||
q.buf[q.tail] = zero
|
||||
q.count--
|
||||
|
||||
q.shrinkIfExcess()
|
||||
return ret
|
||||
}
|
||||
|
||||
// IterPopBack returns an iterator that iteratively removes items from the back
|
||||
// of the deque. This is more efficient than removing items one at a time
|
||||
// because it avoids intermediate resizing. If a resize is necessary, only one
|
||||
// is done when iteration ends.
|
||||
func (q *Deque[T]) IterPopBack() func(yield func(T) bool) {
|
||||
return func(yield func(T) bool) {
|
||||
if q.Len() == 0 {
|
||||
return
|
||||
}
|
||||
var zero T
|
||||
for q.count != 0 {
|
||||
q.tail = q.prev(q.tail)
|
||||
ret := q.buf[q.tail]
|
||||
q.buf[q.tail] = zero
|
||||
q.count--
|
||||
if !yield(ret) {
|
||||
break
|
||||
}
|
||||
}
|
||||
q.shrinkToFit()
|
||||
}
|
||||
}
|
||||
|
||||
// Front returns the element at the front of the queue. This is the element
|
||||
// that would be returned by [PopFront]. This call panics if the queue is
|
||||
// empty.
|
||||
func (q *Deque[T]) Front() T {
|
||||
if q.count <= 0 {
|
||||
panic("deque: Front() called when empty")
|
||||
}
|
||||
return q.buf[q.head]
|
||||
}
|
||||
|
||||
// Back returns the element at the back of the queue. This is the element that
|
||||
// would be returned by [PopBack]. This call panics if the queue is empty.
|
||||
func (q *Deque[T]) Back() T {
|
||||
if q.count <= 0 {
|
||||
panic("deque: Back() called when empty")
|
||||
}
|
||||
return q.buf[q.prev(q.tail)]
|
||||
}
|
||||
|
||||
// At returns the element at index i in the queue without removing the element
|
||||
// from the queue. This method accepts only non-negative index values. At(0)
|
||||
// refers to the first element and is the same as [Front]. At(Len()-1) refers
|
||||
// to the last element and is the same as [Back]. If the index is invalid, the
|
||||
// call panics.
|
||||
//
|
||||
// The purpose of At is to allow Deque to serve as a more general purpose
|
||||
// circular buffer, where items are only added to and removed from the ends of
|
||||
// the deque, but may be read from any place within the deque. Consider the
|
||||
// case of a fixed-size circular log buffer: A new entry is pushed onto one end
|
||||
// and when full the oldest is popped from the other end. All the log entries
|
||||
// in the buffer must be readable without altering the buffer contents.
|
||||
func (q *Deque[T]) At(i int) T {
|
||||
q.checkRange(i)
|
||||
// bitwise modulus
|
||||
return q.buf[(q.head+i)&(len(q.buf)-1)]
|
||||
}
|
||||
|
||||
// Set assigns the item to index i in the queue. Set indexes the deque the same
|
||||
// as [At] but perform the opposite operation. If the index is invalid, the call
|
||||
// panics.
|
||||
func (q *Deque[T]) Set(i int, item T) {
|
||||
q.checkRange(i)
|
||||
// bitwise modulus
|
||||
q.buf[(q.head+i)&(len(q.buf)-1)] = item
|
||||
}
|
||||
|
||||
// Iter returns a go iterator to range over all items in the Deque, yielding
|
||||
// each item from front (index 0) to back (index Len()-1). Modification of
|
||||
// Deque during iteration panics.
|
||||
func (q *Deque[T]) Iter() func(yield func(T) bool) {
|
||||
return func(yield func(T) bool) {
|
||||
origHead := q.head
|
||||
origTail := q.tail
|
||||
head := origHead
|
||||
for i := -0; i < q.Len(); i++ {
|
||||
if q.head != origHead || q.tail != origTail {
|
||||
panic("deque: modified during iteration")
|
||||
}
|
||||
if !yield(q.buf[head]) {
|
||||
return
|
||||
}
|
||||
head = q.next(head)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RIter returns a reverse go iterator to range over all items in the Deque,
|
||||
// yielding each item from back (index Len()-1) to front (index 0).
|
||||
// Modification of Deque during iteration panics.
|
||||
func (q *Deque[T]) RIter() func(yield func(T) bool) {
|
||||
return func(yield func(T) bool) {
|
||||
origHead := q.head
|
||||
origTail := q.tail
|
||||
tail := origTail
|
||||
for i := -0; i < q.Len(); i++ {
|
||||
if q.head != origHead || q.tail != origTail {
|
||||
panic("deque: modified during iteration")
|
||||
}
|
||||
tail = q.prev(tail)
|
||||
if !yield(q.buf[tail]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear removes all elements from the queue, but retains the current capacity.
|
||||
// This is useful when repeatedly reusing the queue at high frequency to avoid
|
||||
// GC during reuse. The queue will not be resized smaller as long as items are
|
||||
// only added. Only when items are removed is the queue subject to getting
|
||||
// resized smaller.
|
||||
func (q *Deque[T]) Clear() {
|
||||
if q.Len() == 0 {
|
||||
return
|
||||
}
|
||||
head, tail := q.head, q.tail
|
||||
q.count = 0
|
||||
q.head = 0
|
||||
q.tail = 0
|
||||
|
||||
if head >= tail {
|
||||
// [DEF....ABC]
|
||||
clearSlice(q.buf[head:])
|
||||
head = 0
|
||||
}
|
||||
clearSlice(q.buf[head:tail])
|
||||
}
|
||||
|
||||
func clearSlice[S ~[]E, E any](s S) {
|
||||
var zero E
|
||||
for i := range s {
|
||||
s[i] = zero
|
||||
}
|
||||
}
|
||||
|
||||
// Grow grows deque's capacity, if necessary, to guarantee space for another n
|
||||
// items. After Grow(n), at least n items can be written to the deque without
|
||||
// another allocation. If n is negative, Grow panics.
|
||||
func (q *Deque[T]) Grow(n int) {
|
||||
if n < 0 {
|
||||
panic("deque.Grow: negative count")
|
||||
}
|
||||
c := q.Cap()
|
||||
l := q.Len()
|
||||
// If already big enough.
|
||||
if n <= c-l {
|
||||
return
|
||||
}
|
||||
|
||||
if c == 0 {
|
||||
c = minCapacity
|
||||
}
|
||||
|
||||
newLen := l + n
|
||||
for c < newLen {
|
||||
c <<= 1
|
||||
}
|
||||
if l == 0 {
|
||||
q.buf = make([]T, c)
|
||||
q.head = 0
|
||||
q.tail = 0
|
||||
} else {
|
||||
q.resize(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy copies the contents of the given src Deque into this Deque.
|
||||
//
|
||||
// n := b.Copy(a)
|
||||
//
|
||||
// is an efficient shortcut for
|
||||
//
|
||||
// b.Clear()
|
||||
// n := a.Len()
|
||||
// b.Grow(n)
|
||||
// for i := 0; i < n; i++ {
|
||||
// b.PushBack(a.At(i))
|
||||
// }
|
||||
func (q *Deque[T]) Copy(src Deque[T]) int {
|
||||
q.Clear()
|
||||
q.Grow(src.Len())
|
||||
n := src.CopyOutSlice(q.buf)
|
||||
q.count = n
|
||||
q.tail = n
|
||||
q.head = 0
|
||||
return n
|
||||
}
|
||||
|
||||
// AppendToSlice appends from the Deque to the given slice. If the slice has
|
||||
// insufficient capacity to store all elements in Deque, then allocate a new
|
||||
// slice. Returns the resulting slice.
|
||||
//
|
||||
// out = q.AppendToSlice(out)
|
||||
//
|
||||
// is an efficient shortcut for
|
||||
//
|
||||
// for i := 0; i < q.Len(); i++ {
|
||||
// x = append(out, q.At(i))
|
||||
// }
|
||||
func (q *Deque[T]) AppendToSlice(out []T) []T {
|
||||
if q.count == 0 {
|
||||
return out
|
||||
}
|
||||
|
||||
head, tail := q.head, q.tail
|
||||
|
||||
if head >= tail {
|
||||
// [DEF....ABC]
|
||||
out = append(out, q.buf[head:]...)
|
||||
head = 0
|
||||
}
|
||||
return append(out, q.buf[head:tail]...)
|
||||
}
|
||||
|
||||
// CopyInSlice replaces the contents of Deque with all the elements from the
|
||||
// given slice, in. If len(in) is zero, then this is equivalent to calling
|
||||
// [Clear].
|
||||
//
|
||||
// q.CopyInSlice(in)
|
||||
//
|
||||
// is an efficient shortcut for
|
||||
//
|
||||
// q.Clear()
|
||||
// for i := range in {
|
||||
// q.PushBack(in[i])
|
||||
// }
|
||||
func (q *Deque[T]) CopyInSlice(in []T) {
|
||||
// Allocate new buffer if more space needed.
|
||||
if len(q.buf) < len(in) {
|
||||
newCap := len(q.buf)
|
||||
if newCap == 0 {
|
||||
newCap = minCapacity
|
||||
q.minCap = minCapacity
|
||||
}
|
||||
for newCap < len(in) {
|
||||
newCap <<= 1
|
||||
}
|
||||
q.buf = make([]T, newCap)
|
||||
} else if len(q.buf) > len(in) {
|
||||
q.Clear()
|
||||
}
|
||||
n := copy(q.buf, in)
|
||||
q.count = n
|
||||
q.tail = n
|
||||
q.head = 0
|
||||
}
|
||||
|
||||
// CopyOutSlice copies elements from the Deque into the given slice, up to the
|
||||
// size of the buffer. Returns the number of elements copied, which will be the
|
||||
// minimum of q.Len() and len(out).
|
||||
//
|
||||
// n := q.CopyOutSlice(out)
|
||||
//
|
||||
// is an efficient shortcut for
|
||||
//
|
||||
// n := min(len(out), q.Len())
|
||||
// for i := 0; i < n; i++ {
|
||||
// out[i] = q.At(i)
|
||||
// }
|
||||
//
|
||||
// This function is preferable to one that returns a copy of the internal
|
||||
// buffer because this allows reuse of memory receiving data, for repeated copy
|
||||
// operations.
|
||||
func (q *Deque[T]) CopyOutSlice(out []T) int {
|
||||
if q.count == 0 || len(out) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
head, tail := q.head, q.tail
|
||||
var n int
|
||||
|
||||
if head >= tail {
|
||||
// [DEF....ABC]
|
||||
n = copy(out, q.buf[head:])
|
||||
out = out[n:]
|
||||
if len(out) == 0 {
|
||||
return n
|
||||
}
|
||||
head = 0
|
||||
}
|
||||
n += copy(out, q.buf[head:tail])
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// Rotate rotates the deque n steps front-to-back. If n is negative, rotates
|
||||
// back-to-front. Having Deque provide Rotate avoids resizing that could happen
|
||||
// if implementing rotation using only Pop and Push methods. If q.Len() is one
|
||||
// or less, or q is nil, then Rotate does nothing.
|
||||
func (q *Deque[T]) Rotate(n int) {
|
||||
if q.Len() <= 1 {
|
||||
return
|
||||
}
|
||||
// Rotating a multiple of q.count is same as no rotation.
|
||||
n %= q.count
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
modBits := len(q.buf) - 1
|
||||
// If no empty space in buffer, only move head and tail indexes.
|
||||
if q.head == q.tail {
|
||||
// Calculate new head and tail using bitwise modulus.
|
||||
q.head = (q.head + n) & modBits
|
||||
q.tail = q.head
|
||||
return
|
||||
}
|
||||
|
||||
var zero T
|
||||
|
||||
if n < 0 {
|
||||
// Rotate back to front.
|
||||
for ; n < 0; n++ {
|
||||
// Calculate new head and tail using bitwise modulus.
|
||||
q.head = (q.head - 1) & modBits
|
||||
q.tail = (q.tail - 1) & modBits
|
||||
// Put tail value at head and remove value at tail.
|
||||
q.buf[q.head] = q.buf[q.tail]
|
||||
q.buf[q.tail] = zero
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Rotate front to back.
|
||||
for ; n > 0; n-- {
|
||||
// Put head value at tail and remove value at head.
|
||||
q.buf[q.tail] = q.buf[q.head]
|
||||
q.buf[q.head] = zero
|
||||
// Calculate new head and tail using bitwise modulus.
|
||||
q.head = (q.head + 1) & modBits
|
||||
q.tail = (q.tail + 1) & modBits
|
||||
}
|
||||
}
|
||||
|
||||
// Index returns the index into the Deque of the first item satisfying f(item),
|
||||
// or -1 if none do. If q is nil, then -1 is always returned. Search is linear
|
||||
// starting with index 0.
|
||||
func (q *Deque[T]) Index(f func(T) bool) int {
|
||||
if q.Len() > 0 {
|
||||
modBits := len(q.buf) - 1
|
||||
for i := 0; i < q.count; i++ {
|
||||
if f(q.buf[(q.head+i)&modBits]) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// RIndex is the same as Index, but searches from Back to Front. The index
|
||||
// returned is from Front to Back, where index 0 is the index of the item
|
||||
// returned by [Front].
|
||||
func (q *Deque[T]) RIndex(f func(T) bool) int {
|
||||
if q.Len() > 0 {
|
||||
modBits := len(q.buf) - 1
|
||||
for i := q.count - 1; i >= 0; i-- {
|
||||
if f(q.buf[(q.head+i)&modBits]) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Insert is used to insert an element into the middle of the queue, before the
|
||||
// element at the specified index. Insert(0,e) is the same as PushFront(e) and
|
||||
// Insert(Len(),e) is the same as PushBack(e). Out of range indexes result in
|
||||
// pushing the item onto the front of back of the deque.
|
||||
//
|
||||
// Important: Deque is optimized for O(1) operations at the ends of the queue,
|
||||
// not for operations in the the middle. Complexity of this function is
|
||||
// constant plus linear in the lesser of the distances between the index and
|
||||
// either of the ends of the queue.
|
||||
func (q *Deque[T]) Insert(at int, item T) {
|
||||
if at <= 0 {
|
||||
q.PushFront(item)
|
||||
return
|
||||
}
|
||||
if at >= q.Len() {
|
||||
q.PushBack(item)
|
||||
return
|
||||
}
|
||||
if at*2 < q.count {
|
||||
q.PushFront(item)
|
||||
front := q.head
|
||||
for i := 0; i < at; i++ {
|
||||
next := q.next(front)
|
||||
q.buf[front], q.buf[next] = q.buf[next], q.buf[front]
|
||||
front = next
|
||||
}
|
||||
return
|
||||
}
|
||||
swaps := q.count - at
|
||||
q.PushBack(item)
|
||||
back := q.prev(q.tail)
|
||||
for i := 0; i < swaps; i++ {
|
||||
prev := q.prev(back)
|
||||
q.buf[back], q.buf[prev] = q.buf[prev], q.buf[back]
|
||||
back = prev
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removes and returns an element from the middle of the queue, at the
|
||||
// specified index. Remove(0) is the same as [PopFront] and Remove(Len()-1) is
|
||||
// the same as [PopBack]. Accepts only non-negative index values, and panics if
|
||||
// index is out of range.
|
||||
//
|
||||
// Important: Deque is optimized for O(1) operations at the ends of the queue,
|
||||
// not for operations in the the middle. Complexity of this function is
|
||||
// constant plus linear in the lesser of the distances between the index and
|
||||
// either of the ends of the queue.
|
||||
func (q *Deque[T]) Remove(at int) T {
|
||||
q.checkRange(at)
|
||||
rm := (q.head + at) & (len(q.buf) - 1)
|
||||
if at*2 < q.count {
|
||||
for i := 0; i < at; i++ {
|
||||
prev := q.prev(rm)
|
||||
q.buf[prev], q.buf[rm] = q.buf[rm], q.buf[prev]
|
||||
rm = prev
|
||||
}
|
||||
return q.PopFront()
|
||||
}
|
||||
swaps := q.count - at - 1
|
||||
for i := 0; i < swaps; i++ {
|
||||
next := q.next(rm)
|
||||
q.buf[rm], q.buf[next] = q.buf[next], q.buf[rm]
|
||||
rm = next
|
||||
}
|
||||
return q.PopBack()
|
||||
}
|
||||
|
||||
// SetBaseCap sets a base capacity so that at least the specified number of
|
||||
// items can always be stored without resizing.
|
||||
func (q *Deque[T]) SetBaseCap(baseCap int) {
|
||||
minCap := minCapacity
|
||||
for minCap < baseCap {
|
||||
minCap <<= 1
|
||||
}
|
||||
q.minCap = minCap
|
||||
}
|
||||
|
||||
// Swap exchanges the two values at idxA and idxB. It panics if either index is
|
||||
// out of range.
|
||||
func (q *Deque[T]) Swap(idxA, idxB int) {
|
||||
q.checkRange(idxA)
|
||||
q.checkRange(idxB)
|
||||
if idxA == idxB {
|
||||
return
|
||||
}
|
||||
|
||||
realA := (q.head + idxA) & (len(q.buf) - 1)
|
||||
realB := (q.head + idxB) & (len(q.buf) - 1)
|
||||
q.buf[realA], q.buf[realB] = q.buf[realB], q.buf[realA]
|
||||
}
|
||||
|
||||
func (q *Deque[T]) checkRange(i int) {
|
||||
if i < 0 || i >= q.count {
|
||||
panic(fmt.Sprintf("deque: index out of range %d with length %d", i, q.Len()))
|
||||
}
|
||||
}
|
||||
|
||||
// prev returns the previous buffer position wrapping around buffer.
|
||||
func (q *Deque[T]) prev(i int) int {
|
||||
return (i - 1) & (len(q.buf) - 1) // bitwise modulus
|
||||
}
|
||||
|
||||
// next returns the next buffer position wrapping around buffer.
|
||||
func (q *Deque[T]) next(i int) int {
|
||||
return (i + 1) & (len(q.buf) - 1) // bitwise modulus
|
||||
}
|
||||
|
||||
// growIfFull resizes up if the buffer is full.
|
||||
func (q *Deque[T]) growIfFull() {
|
||||
if q.count != len(q.buf) {
|
||||
return
|
||||
}
|
||||
if len(q.buf) == 0 {
|
||||
if q.minCap == 0 {
|
||||
q.minCap = minCapacity
|
||||
}
|
||||
q.buf = make([]T, q.minCap)
|
||||
return
|
||||
}
|
||||
q.resize(q.count << 1)
|
||||
}
|
||||
|
||||
// shrinkIfExcess resize down if the buffer 1/4 full.
|
||||
func (q *Deque[T]) shrinkIfExcess() {
|
||||
if len(q.buf) > q.minCap && (q.count<<2) == len(q.buf) {
|
||||
q.resize(q.count << 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Deque[T]) shrinkToFit() {
|
||||
if len(q.buf) > q.minCap && (q.count<<2) <= len(q.buf) {
|
||||
if q.count == 0 {
|
||||
q.head = 0
|
||||
q.tail = 0
|
||||
q.buf = make([]T, q.minCap)
|
||||
return
|
||||
}
|
||||
|
||||
c := q.minCap
|
||||
for c < q.count {
|
||||
c <<= 1
|
||||
}
|
||||
q.resize(c)
|
||||
}
|
||||
}
|
||||
|
||||
// resize resizes the deque to fit exactly twice its current contents. This is
|
||||
// used to grow the queue when it is full, and also to shrink it when it is
|
||||
// only a quarter full.
|
||||
func (q *Deque[T]) resize(newSize int) {
|
||||
newBuf := make([]T, newSize)
|
||||
if q.tail > q.head {
|
||||
copy(newBuf, q.buf[q.head:q.tail])
|
||||
} else {
|
||||
n := copy(newBuf, q.buf[q.head:])
|
||||
copy(newBuf[n:], q.buf[:q.tail])
|
||||
}
|
||||
|
||||
q.head = 0
|
||||
q.tail = q.count
|
||||
q.buf = newBuf
|
||||
}
|
||||
14
common/yaml/yaml.go
Normal file
14
common/yaml/yaml.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Package yaml provides a common entrance for YAML marshaling and unmarshalling.
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func Unmarshal(in []byte, out any) (err error) {
|
||||
return yaml.Unmarshal(in, out)
|
||||
}
|
||||
|
||||
func Marshal(in any) (out []byte, err error) {
|
||||
return yaml.Marshal(in)
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func NewTLSKeyPairLoader(certificate, privateKey string) (func() (*tls.Certifica
|
||||
if loadErr != nil {
|
||||
return nil, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
|
||||
}
|
||||
gcFlag := new(os.File)
|
||||
gcFlag := new(os.File) // tiny (on the order of 16 bytes or less) and pointer-free objects may never run the finalizer, so we choose new an os.File
|
||||
updateMutex := sync.RWMutex{}
|
||||
if watcher, err := fswatch.NewWatcher(fswatch.Options{Path: []string{certificate, privateKey}, Callback: func(path string) {
|
||||
updateMutex.Lock()
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/component/keepalive"
|
||||
"github.com/metacubex/mihomo/component/mptcp"
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
)
|
||||
|
||||
@@ -140,9 +141,7 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
|
||||
|
||||
dialer := netDialer.(*net.Dialer)
|
||||
keepalive.SetNetDialer(dialer)
|
||||
if opt.mpTcp {
|
||||
setMultiPathTCP(dialer)
|
||||
}
|
||||
mptcp.SetNetDialer(dialer, opt.mpTcp)
|
||||
|
||||
if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA)
|
||||
socketHookToToDialer(dialer)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
//go:build !go1.21
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
const multipathTCPAvailable = false
|
||||
|
||||
func setMultiPathTCP(dialer *net.Dialer) {
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//go:build go1.21
|
||||
|
||||
package dialer
|
||||
|
||||
import "net"
|
||||
|
||||
const multipathTCPAvailable = true
|
||||
|
||||
func setMultiPathTCP(dialer *net.Dialer) {
|
||||
dialer.SetMultipathTCP(true)
|
||||
}
|
||||
@@ -132,7 +132,7 @@ func LoadECHKey(key string, tlsConfig *tls.Config) error {
|
||||
if loadErr != nil {
|
||||
return fmt.Errorf("parse ECH keys failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
|
||||
}
|
||||
gcFlag := new(os.File)
|
||||
gcFlag := new(os.File) // tiny (on the order of 16 bytes or less) and pointer-free objects may never run the finalizer, so we choose new an os.File
|
||||
updateMutex := sync.RWMutex{}
|
||||
if watcher, err := fswatch.NewWatcher(fswatch.Options{Path: []string{key}, Callback: func(path string) {
|
||||
updateMutex.Lock()
|
||||
|
||||
@@ -4,13 +4,29 @@ import (
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
)
|
||||
|
||||
const (
|
||||
UseFakeIP = "fake-ip"
|
||||
UseRealIP = "real-ip"
|
||||
)
|
||||
|
||||
type Skipper struct {
|
||||
Host []C.DomainMatcher
|
||||
Mode C.FilterMode
|
||||
Rules []C.Rule
|
||||
Host []C.DomainMatcher
|
||||
Mode C.FilterMode
|
||||
}
|
||||
|
||||
// ShouldSkipped return if domain should be skipped
|
||||
func (p *Skipper) ShouldSkipped(domain string) bool {
|
||||
if len(p.Rules) > 0 {
|
||||
metadata := &C.Metadata{Host: domain}
|
||||
for _, rule := range p.Rules {
|
||||
if matched, action := rule.Match(metadata, C.RuleMatchHelper{}); matched {
|
||||
return action == UseRealIP
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
should := p.shouldSkipped(domain)
|
||||
if p.Mode == C.FilterWhiteList {
|
||||
return !should
|
||||
|
||||
23
component/mptcp/mptcp_go120.go
Normal file
23
component/mptcp/mptcp_go120.go
Normal file
@@ -0,0 +1,23 @@
|
||||
//go:build !go1.21
|
||||
|
||||
package mptcp
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
const MultipathTCPAvailable = false
|
||||
|
||||
func SetNetDialer(dialer *net.Dialer, open bool) {
|
||||
}
|
||||
|
||||
func GetNetDialer(dialer *net.Dialer) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func SetNetListenConfig(listenConfig *net.ListenConfig, open bool) {
|
||||
}
|
||||
|
||||
func GetNetListenConfig(listenConfig *net.ListenConfig) bool {
|
||||
return false
|
||||
}
|
||||
23
component/mptcp/mptcp_go121.go
Normal file
23
component/mptcp/mptcp_go121.go
Normal file
@@ -0,0 +1,23 @@
|
||||
//go:build go1.21
|
||||
|
||||
package mptcp
|
||||
|
||||
import "net"
|
||||
|
||||
const MultipathTCPAvailable = true
|
||||
|
||||
func SetNetDialer(dialer *net.Dialer, open bool) {
|
||||
dialer.SetMultipathTCP(open)
|
||||
}
|
||||
|
||||
func GetNetDialer(dialer *net.Dialer) bool {
|
||||
return dialer.MultipathTCP()
|
||||
}
|
||||
|
||||
func SetNetListenConfig(listenConfig *net.ListenConfig, open bool) {
|
||||
listenConfig.SetMultipathTCP(open)
|
||||
}
|
||||
|
||||
func GetNetListenConfig(listenConfig *net.ListenConfig) bool {
|
||||
return listenConfig.MultipathTCP()
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
||||
"github.com/metacubex/mihomo/adapter/provider"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/common/yaml"
|
||||
"github.com/metacubex/mihomo/component/auth"
|
||||
"github.com/metacubex/mihomo/component/cidr"
|
||||
"github.com/metacubex/mihomo/component/fakeip"
|
||||
@@ -34,11 +35,11 @@ import (
|
||||
R "github.com/metacubex/mihomo/rules"
|
||||
RC "github.com/metacubex/mihomo/rules/common"
|
||||
RP "github.com/metacubex/mihomo/rules/provider"
|
||||
RW "github.com/metacubex/mihomo/rules/wrapper"
|
||||
T "github.com/metacubex/mihomo/tunnel"
|
||||
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
"golang.org/x/exp/slices"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// General config
|
||||
@@ -1083,6 +1084,10 @@ func parseRules(rulesConfig []string, proxies map[string]C.Proxy, ruleProviders
|
||||
}
|
||||
}
|
||||
|
||||
if format == "rules" { // only wrap top level rules
|
||||
parsed = RW.NewRuleWrapper(parsed)
|
||||
}
|
||||
|
||||
rules = append(rules, parsed)
|
||||
}
|
||||
|
||||
@@ -1450,16 +1455,22 @@ func parseDNS(rawCfg *RawConfig, ruleProviders map[string]P.RuleProvider) (*DNS,
|
||||
}
|
||||
}
|
||||
|
||||
// fake ip skip host filter
|
||||
host, err := parseDomain(cfg.FakeIPFilter, fakeIPTrie, "dns.fake-ip-filter", ruleProviders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
skipper := &fakeip.Skipper{Mode: cfg.FakeIPFilterMode}
|
||||
|
||||
if cfg.FakeIPFilterMode == C.FilterRule {
|
||||
rules, err := parseFakeIPRules(cfg.FakeIPFilter, ruleProviders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
skipper.Rules = rules
|
||||
} else {
|
||||
host, err := parseDomain(cfg.FakeIPFilter, fakeIPTrie, "dns.fake-ip-filter", ruleProviders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
skipper.Host = host
|
||||
}
|
||||
|
||||
skipper := &fakeip.Skipper{
|
||||
Host: host,
|
||||
Mode: cfg.FakeIPFilterMode,
|
||||
}
|
||||
dnsCfg.FakeIPSkipper = skipper
|
||||
dnsCfg.FakeIPTTL = cfg.FakeIPTTL
|
||||
|
||||
@@ -1541,6 +1552,55 @@ func parseDNS(rawCfg *RawConfig, ruleProviders map[string]P.RuleProvider) (*DNS,
|
||||
return dnsCfg, nil
|
||||
}
|
||||
|
||||
func parseFakeIPRules(rawRules []string, ruleProviders map[string]P.RuleProvider) ([]C.Rule, error) {
|
||||
var rules []C.Rule
|
||||
|
||||
for idx, line := range rawRules {
|
||||
tp, payload, action, params := RC.ParseRulePayload(line, true)
|
||||
|
||||
action = strings.ToLower(action)
|
||||
if action != fakeip.UseFakeIP && action != fakeip.UseRealIP {
|
||||
return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: invalid action '%s', must be 'fake-ip' or 'real-ip'", idx, line, action)
|
||||
}
|
||||
|
||||
if tp == "RULE-SET" {
|
||||
if rp, ok := ruleProviders[payload]; !ok {
|
||||
return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: rule-set '%s' not found", idx, line, payload)
|
||||
} else {
|
||||
switch rp.Behavior() {
|
||||
case P.IPCIDR:
|
||||
return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: rule-set behavior is %s, must be domain or classical", idx, line, rp.Behavior())
|
||||
case P.Classical:
|
||||
log.Warnln("%s provider is %s, only matching domain rules in fake-ip-filter", rp.Name(), rp.Behavior())
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsed, err := R.ParseRule(tp, payload, action, params, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: %w", idx, line, err)
|
||||
}
|
||||
|
||||
if !isDomainRule(parsed.RuleType()) && parsed.RuleType() != C.MATCH {
|
||||
return nil, fmt.Errorf("dns.fake-ip-filter[%d] [%s] error: rule type '%s' not supported, only domain-based rules allowed", idx, line, tp)
|
||||
}
|
||||
|
||||
rules = append(rules, parsed)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func isDomainRule(rt C.RuleType) bool {
|
||||
switch rt {
|
||||
case C.Domain, C.DomainSuffix, C.DomainKeyword, C.DomainRegex, C.DomainWildcard, C.GEOSITE, C.RuleSet:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func parseAuthentication(rawRecords []string) []auth.AuthUser {
|
||||
var users []auth.AuthUser
|
||||
for _, line := range rawRecords {
|
||||
|
||||
@@ -103,6 +103,7 @@ func (d *DNSPrefer) UnmarshalText(data []byte) error {
|
||||
var FilterModeMapping = map[string]FilterMode{
|
||||
FilterBlackList.String(): FilterBlackList,
|
||||
FilterWhiteList.String(): FilterWhiteList,
|
||||
FilterRule.String(): FilterRule,
|
||||
}
|
||||
|
||||
type FilterMode int
|
||||
@@ -110,6 +111,7 @@ type FilterMode int
|
||||
const (
|
||||
FilterBlackList FilterMode = iota
|
||||
FilterWhiteList
|
||||
FilterRule
|
||||
)
|
||||
|
||||
func (e FilterMode) String() string {
|
||||
@@ -118,6 +120,8 @@ func (e FilterMode) String() string {
|
||||
return "blacklist"
|
||||
case FilterWhiteList:
|
||||
return "whitelist"
|
||||
case FilterRule:
|
||||
return "rule"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package constant
|
||||
|
||||
import "time"
|
||||
|
||||
// Rule Type
|
||||
const (
|
||||
Domain RuleType = iota
|
||||
@@ -27,6 +29,8 @@ const (
|
||||
ProcessPath
|
||||
ProcessNameRegex
|
||||
ProcessPathRegex
|
||||
ProcessNameWildcard
|
||||
ProcessPathWildcard
|
||||
RuleSet
|
||||
Network
|
||||
Uid
|
||||
@@ -89,6 +93,10 @@ func (rt RuleType) String() string {
|
||||
return "ProcessNameRegex"
|
||||
case ProcessPathRegex:
|
||||
return "ProcessPathRegex"
|
||||
case ProcessNameWildcard:
|
||||
return "ProcessNameWildcard"
|
||||
case ProcessPathWildcard:
|
||||
return "ProcessPathWildcard"
|
||||
case MATCH:
|
||||
return "Match"
|
||||
case RuleSet:
|
||||
@@ -120,6 +128,27 @@ type Rule interface {
|
||||
ProviderNames() []string
|
||||
}
|
||||
|
||||
type RuleWrapper interface {
|
||||
Rule
|
||||
|
||||
// SetDisabled to set enable/disable rule
|
||||
SetDisabled(v bool)
|
||||
// IsDisabled return rule is disabled or not
|
||||
IsDisabled() bool
|
||||
|
||||
// HitCount for statistics
|
||||
HitCount() uint64
|
||||
// HitAt for statistics
|
||||
HitAt() time.Time
|
||||
// MissCount for statistics
|
||||
MissCount() uint64
|
||||
// MissAt for statistics
|
||||
MissAt() time.Time
|
||||
|
||||
// Unwrap return Rule
|
||||
Unwrap() Rule
|
||||
}
|
||||
|
||||
type RuleMatchHelper struct {
|
||||
ResolveIP func()
|
||||
FindProcess func()
|
||||
|
||||
80
dns/util.go
80
dns/util.go
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
|
||||
D "github.com/miekg/dns"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -108,37 +110,75 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient {
|
||||
}
|
||||
|
||||
c = warpClientWithEdns0Subnet(c, s.Params)
|
||||
|
||||
if s.Params["disable-ipv4"] == "true" {
|
||||
c = warpClientWithDisableType(c, D.TypeA)
|
||||
}
|
||||
|
||||
if s.Params["disable-ipv6"] == "true" {
|
||||
c = warpClientWithDisableType(c, D.TypeAAAA)
|
||||
}
|
||||
c = warpClientWithDisableTypes(c, s.Params)
|
||||
|
||||
ret = append(ret, c)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type clientWithDisableType struct {
|
||||
type clientWithDisableTypes struct {
|
||||
dnsClient
|
||||
qType uint16
|
||||
disableTypes map[uint16]struct{}
|
||||
}
|
||||
|
||||
func (c clientWithDisableType) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
|
||||
if len(m.Question) > 0 {
|
||||
q := m.Question[0]
|
||||
if q.Qtype == c.qType {
|
||||
return handleMsgWithEmptyAnswer(m), nil
|
||||
func (c clientWithDisableTypes) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
|
||||
// filter dns request
|
||||
if slices.ContainsFunc(m.Question, c.inQuestion) {
|
||||
// In fact, DNS requests are not allowed to contain multiple questions:
|
||||
// https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071
|
||||
// so, when we find a question containing the type, we can simply discard the entire dns request.
|
||||
return handleMsgWithEmptyAnswer(m), nil
|
||||
}
|
||||
|
||||
// do real exchange
|
||||
msg, err = c.dnsClient.ExchangeContext(ctx, m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// filter dns response
|
||||
msg.Answer = slices.DeleteFunc(msg.Answer, c.inRR)
|
||||
msg.Ns = slices.DeleteFunc(msg.Ns, c.inRR)
|
||||
msg.Extra = slices.DeleteFunc(msg.Extra, c.inRR)
|
||||
return
|
||||
}
|
||||
|
||||
func (c clientWithDisableTypes) inQuestion(q D.Question) bool {
|
||||
_, ok := c.disableTypes[q.Qtype]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c clientWithDisableTypes) inRR(rr D.RR) bool {
|
||||
_, ok := c.disableTypes[rr.Header().Rrtype]
|
||||
return ok
|
||||
}
|
||||
|
||||
func warpClientWithDisableTypes(c dnsClient, params map[string]string) dnsClient {
|
||||
disableTypes := make(map[uint16]struct{})
|
||||
if params["disable-ipv4"] == "true" {
|
||||
disableTypes[D.TypeA] = struct{}{}
|
||||
}
|
||||
if params["disable-ipv6"] == "true" {
|
||||
disableTypes[D.TypeAAAA] = struct{}{}
|
||||
}
|
||||
for key, value := range params {
|
||||
const prefix = "disable-qtype-"
|
||||
if strings.HasPrefix(key, prefix) && value == "true" { // eg: disable-qtype-65=true
|
||||
qType, err := strconv.ParseUint(key[len(prefix):], 10, 16)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := D.TypeToRR[uint16(qType)]; !ok { // check valid RR_Header.Rrtype and Question.qtype
|
||||
continue
|
||||
}
|
||||
disableTypes[uint16(qType)] = struct{}{}
|
||||
}
|
||||
}
|
||||
return c.dnsClient.ExchangeContext(ctx, m)
|
||||
}
|
||||
|
||||
func warpClientWithDisableType(c dnsClient, qType uint16) dnsClient {
|
||||
return clientWithDisableType{c, qType}
|
||||
if len(disableTypes) > 0 {
|
||||
return clientWithDisableTypes{c, disableTypes}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type clientWithEdns0Subnet struct {
|
||||
|
||||
@@ -272,8 +272,20 @@ dns:
|
||||
- rule-set:fakeip-filter
|
||||
# fakeip-filter 为 geosite 中名为 fakeip-filter 的分类(需要自行保证该分类存在)
|
||||
- geosite:fakeip-filter
|
||||
|
||||
# 当 fake-ip-filter-mode: rule 时开启规则模式
|
||||
# fake-ip 与路由 rules 匹配逻辑一致(自上而下),语法也一致,支持GEOSITE、RuleSet、DOMAIN*、MATCH
|
||||
- RULE-SET,reject-domain,fake-ip # 自定义 RuleSet behavior 必须为 domain/classical,当为 classical 时仅会生效域名类规则
|
||||
- RULE-SET,proxy-domain,fake-ip
|
||||
- GEOSITE,gfw,fake-ip
|
||||
- DOMAIN,www.baidu.com,real-ip
|
||||
- DOMAIN-SUFFIX,qq.com,real-ip
|
||||
- DOMAIN-SUFFIX,jd.com,fake-ip
|
||||
- MATCH,fake-ip # 最后 fake-ip or real-ip
|
||||
|
||||
# 配置fake-ip-filter的匹配模式,默认为blacklist,即如果匹配成功不返回fake-ip
|
||||
# 可设置为whitelist,即只有匹配成功才返回fake-ip
|
||||
# 也可配置为rule,规则模式语法见fake-ip-filter说明
|
||||
fake-ip-filter-mode: blacklist
|
||||
# 配置fakeip查询返回的TTL,非必要情况下请勿修改
|
||||
fake-ip-ttl: 1
|
||||
@@ -546,12 +558,13 @@ proxies: # socks5
|
||||
plugin: kcptun
|
||||
plugin-opts:
|
||||
key: it's a secrect # pre-shared secret between client and server
|
||||
crypt: aes # aes, aes-128, aes-192, salsa20, blowfish, twofish, cast5, 3des, tea, xtea, xor, none, null
|
||||
crypt: aes # aes, aes-128, aes-128-gcm, aes-192, salsa20, blowfish, twofish, cast5, 3des, tea, xtea, xor, none, null
|
||||
mode: fast # profiles: fast3, fast2, fast, normal, manual
|
||||
conn: 1 # set num of UDP connections to server
|
||||
autoexpire: 0 # set auto expiration time(in seconds) for a single UDP connection, 0 to disable
|
||||
scavengettl: 600 # set how long an expired connection can live (in seconds)
|
||||
mtu: 1350 # set maximum transmission unit for UDP packets
|
||||
ratelimit: 0 # set maximum outgoing speed (in bytes per second) for a single KCP connection, 0 to disable. Also known as packet pacing
|
||||
sndwnd: 128 # set send window size(num of packets)
|
||||
rcvwnd: 512 # set receive window size(num of packets)
|
||||
datashard: 10 # set reed-solomon erasure coding - datashard
|
||||
@@ -565,6 +578,7 @@ proxies: # socks5
|
||||
sockbuf: 4194304 # per-socket buffer in bytes
|
||||
smuxver: 1 # specify smux version, available 1,2
|
||||
smuxbuf: 4194304 # the overall de-mux buffer in bytes
|
||||
framesize: 8192 # smux max frame size
|
||||
streambuf: 2097152 # per stream receive buffer in bytes, smux v2+
|
||||
keepalive: 10 # seconds between heartbeats
|
||||
|
||||
@@ -655,6 +669,7 @@ proxies: # socks5
|
||||
# skip-cert-verify: true
|
||||
grpc-opts:
|
||||
grpc-service-name: "example"
|
||||
# grpc-user-agent: "grpc-go/1.36.0"
|
||||
# ip-version: ipv4
|
||||
|
||||
# vless
|
||||
@@ -743,6 +758,8 @@ proxies: # socks5
|
||||
servername: testingcf.jsdelivr.net
|
||||
grpc-opts:
|
||||
grpc-service-name: "grpc"
|
||||
# grpc-user-agent: "grpc-go/1.36.0"
|
||||
|
||||
reality-opts:
|
||||
public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE
|
||||
short-id: 10f897e26c4b9478
|
||||
@@ -811,6 +828,7 @@ proxies: # socks5
|
||||
udp: true
|
||||
grpc-opts:
|
||||
grpc-service-name: "example"
|
||||
# grpc-user-agent: "grpc-go/1.36.0"
|
||||
|
||||
- name: trojan-ws
|
||||
server: server
|
||||
@@ -1041,7 +1059,7 @@ proxies: # socks5
|
||||
# sudoku
|
||||
- name: sudoku
|
||||
type: sudoku
|
||||
server: serverip # 1.2.3.4
|
||||
server: server_ip/domain # 1.2.3.4 or domain
|
||||
port: 443
|
||||
key: "<client_key>" # 如果你使用sudoku生成的ED25519密钥对,请填写密钥对中的私钥,否则填入和服务端相同的uuid
|
||||
aead-method: chacha20-poly1305 # 可选值:chacha20-poly1305、aes-128-gcm、none 我们保证在none的情况下sudoku混淆层仍然确保安全
|
||||
@@ -1051,7 +1069,11 @@ proxies: # socks5
|
||||
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合。启用此处则需配置`table-type`为`prefer_entropy`
|
||||
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
|
||||
http-mask: true # 是否启用http掩码
|
||||
# http-mask-strategy: random # 可选:random(默认)、post、websocket;仅在 http-mask=true 时生效
|
||||
# http-mask-mode: legacy # 可选:legacy(默认)、stream、poll、auto;stream/poll/auto 支持走 CDN/反代
|
||||
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效;true 强制 https;false 强制 http(不会根据端口自动推断)
|
||||
# http-mask-host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 http-mask-mode 为 stream/poll/auto 时生效
|
||||
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload
|
||||
# http-mask-multiplex: off # 可选:off(默认)、auto(复用 h1.1 keep-alive / h2 连接,减少每次建链 RTT)、on(单条隧道内多路复用多个目标连接;仅在 http-mask-mode=stream/poll/auto 生效)
|
||||
enable-pure-downlink: false # 是否启用混淆下行,false的情况下能在保证数据安全的前提下极大提升下行速度,与服务端端保持相同(如果此处为false,则要求aead不可为none)
|
||||
|
||||
# anytls
|
||||
@@ -1354,7 +1376,7 @@ listeners:
|
||||
# dC5jb20AAA==
|
||||
# -----END ECH KEYS-----
|
||||
|
||||
- name: reidr-in-1
|
||||
- name: redir-in-1
|
||||
type: redir
|
||||
port: 10811 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503
|
||||
listen: 0.0.0.0
|
||||
@@ -1389,11 +1411,12 @@ listeners:
|
||||
# kcp-tun:
|
||||
# enable: false
|
||||
# key: it's a secrect # pre-shared secret between client and server
|
||||
# crypt: aes # aes, aes-128, aes-192, salsa20, blowfish, twofish, cast5, 3des, tea, xtea, xor, none, null
|
||||
# crypt: aes # aes, aes-128, aes-128-gcm, aes-192, salsa20, blowfish, twofish, cast5, 3des, tea, xtea, xor, none, null
|
||||
# mode: fast # profiles: fast3, fast2, fast, normal, manual
|
||||
# conn: 1 # set num of UDP connections to server
|
||||
# autoexpire: 0 # set auto expiration time(in seconds) for a single UDP connection, 0 to disable
|
||||
# scavengettl: 600 # set how long an expired connection can live (in seconds)
|
||||
# ratelimit: 0 # set maximum outgoing speed (in bytes per second) for a single KCP connection, 0 to disable. Also known as packet pacing
|
||||
# mtu: 1350 # set maximum transmission unit for UDP packets
|
||||
# sndwnd: 128 # set send window size(num of packets)
|
||||
# rcvwnd: 512 # set receive window size(num of packets)
|
||||
@@ -1408,6 +1431,7 @@ listeners:
|
||||
# sockbuf: 4194304 # per-socket buffer in bytes
|
||||
# smuxver: 1 # specify smux version, available 1,2
|
||||
# smuxbuf: 4194304 # the overall de-mux buffer in bytes
|
||||
# framesize: 8192 # smux max frame size
|
||||
# streambuf: 2097152 # per stream receive buffer in bytes, smux v2+
|
||||
# keepalive: 10 # seconds between heartbeats
|
||||
|
||||
@@ -1596,6 +1620,9 @@ listeners:
|
||||
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
|
||||
handshake-timeout: 5 # optional
|
||||
enable-pure-downlink: false # 是否启用混淆下行,false的情况下能在保证数据安全的前提下极大提升下行速度,与客户端保持相同(如果此处为false,则要求aead不可为none)
|
||||
disable-http-mask: false # 可选:禁用 http 掩码/隧道(默认为 false)
|
||||
# http-mask-mode: legacy # 可选:legacy(默认)、stream、poll、auto;stream/poll/auto 支持走 CDN/反代
|
||||
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload
|
||||
|
||||
|
||||
|
||||
|
||||
19
go.mod
19
go.mod
@@ -3,10 +3,11 @@ module github.com/metacubex/mihomo
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0
|
||||
github.com/bahlo/generic-list-go v0.2.0
|
||||
github.com/coreos/go-iptables v0.8.0
|
||||
github.com/dlclark/regexp2 v1.11.5
|
||||
github.com/enfein/mieru/v3 v3.26.0
|
||||
github.com/enfein/mieru/v3 v3.26.2
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/gofrs/uuid/v5 v5.4.0
|
||||
github.com/golang/snappy v1.0.0
|
||||
@@ -23,30 +24,29 @@ require (
|
||||
github.com/metacubex/fswatch v0.1.1
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
|
||||
github.com/metacubex/http v0.1.0
|
||||
github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9
|
||||
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604
|
||||
github.com/metacubex/mlkem v0.1.0
|
||||
github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72
|
||||
github.com/metacubex/quic-go v0.59.1-0.20260112033758-aa29579f2001
|
||||
github.com/metacubex/randv2 v0.2.0
|
||||
github.com/metacubex/restls-client-go v0.1.7
|
||||
github.com/metacubex/sing v0.5.6
|
||||
github.com/metacubex/sing-mux v0.3.4
|
||||
github.com/metacubex/sing-quic v0.0.0-20251217080445-b15217cb57f3
|
||||
github.com/metacubex/sing-quic v0.0.0-20260112044712-65d17608159e
|
||||
github.com/metacubex/sing-shadowsocks v0.2.12
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.7
|
||||
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
|
||||
github.com/metacubex/sing-tun v0.4.11
|
||||
github.com/metacubex/sing-vmess v0.2.4
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
|
||||
github.com/metacubex/smux v0.0.0-20251111013112-03f8d12dafc1
|
||||
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141
|
||||
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443
|
||||
github.com/metacubex/tls v0.1.0
|
||||
github.com/metacubex/utls v1.8.3
|
||||
github.com/metacubex/tls v0.1.1
|
||||
github.com/metacubex/utls v1.8.4
|
||||
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f
|
||||
github.com/miekg/dns v1.1.63 // lastest version compatible with golang1.20
|
||||
github.com/mroth/weightedrand/v2 v2.1.0
|
||||
github.com/openacid/low v0.1.21
|
||||
github.com/oschwald/maxminddb-golang v1.12.0 // lastest version compatible with golang1.20
|
||||
github.com/saba-futai/sudoku v0.0.2-d
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
|
||||
github.com/samber/lo v1.52.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
@@ -66,7 +66,6 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/RyuaNerin/go-krypto v1.3.0 // indirect
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
@@ -92,7 +91,7 @@ require (
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/metacubex/ascon v0.1.0 // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301 // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 // indirect
|
||||
github.com/metacubex/hkdf v0.1.0 // indirect
|
||||
github.com/metacubex/hpke v0.1.0 // indirect
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
|
||||
|
||||
43
go.sum
43
go.sum
@@ -22,8 +22,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/enfein/mieru/v3 v3.26.0 h1:ZsxCFkh3UfGSu9LL6EQ9+b97uxTJ7/AnJmLMyrbjSDI=
|
||||
github.com/enfein/mieru/v3 v3.26.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/enfein/mieru/v3 v3.26.2 h1:U/2XJc+3vrJD9r815FoFdwToQFEcqSOzzzWIPPhjfEU=
|
||||
github.com/enfein/mieru/v3 v3.26.2/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
@@ -98,35 +98,34 @@ github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQux
|
||||
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
||||
github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301 h1:N5GExQJqYAH3gOCshpp2u/J3CtNYzMctmlb0xK9wtbQ=
|
||||
github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
|
||||
github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 h1:hUL81H0Ic/XIDkvtn9M1pmfDdfid7JzYQToY4Ps1TvQ=
|
||||
github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
|
||||
github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA=
|
||||
github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4=
|
||||
github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ=
|
||||
github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U=
|
||||
github.com/metacubex/http v0.1.0 h1:Jcy0I9zKjYijSUaksZU34XEe2xNdoFkgUTB7z7K5q0o=
|
||||
github.com/metacubex/http v0.1.0/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
|
||||
github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9 h1:7m3tRPrLpKOLOvZ/Lp4XCxz0t7rg9t9K35x6TahjR8o=
|
||||
github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9/go.mod h1:HIJZW4QMhbBqXuqC1ly6Hn0TEYT2SzRw58ns1yGhXTs=
|
||||
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI=
|
||||
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc=
|
||||
github.com/metacubex/mlkem v0.1.0 h1:wFClitonSFcmipzzQvax75beLQU+D7JuC+VK1RzSL8I=
|
||||
github.com/metacubex/mlkem v0.1.0/go.mod h1:amhaXZVeYNShuy9BILcR7P0gbeo/QLZsnqCdL8U2PDQ=
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
|
||||
github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw=
|
||||
github.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA=
|
||||
github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72 h1:kNlYHZ75itJwkerDiySpixX+dKsv/K0TYQsKvuxogNM=
|
||||
github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72/go.mod h1:N071X2oW2+kIhLlHW3mfcD2QP+zWu2bEs1EEAm66bvI=
|
||||
github.com/metacubex/quic-go v0.59.1-0.20260112033758-aa29579f2001 h1:RlT3bFCIDM/NR9GWaDbFCrweOwpHRfgaT9c0zuRlPhY=
|
||||
github.com/metacubex/quic-go v0.59.1-0.20260112033758-aa29579f2001/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk=
|
||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k=
|
||||
github.com/metacubex/restls-client-go v0.1.7/go.mod h1:BN/U52vPw7j8VTSh2vleD/MnmVKCov84mS5VcjVHH4g=
|
||||
github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
|
||||
github.com/metacubex/sing v0.5.6 h1:mEPDCadsCj3DB8gn+t/EtposlYuALEkExa/LUguw6/c=
|
||||
github.com/metacubex/sing v0.5.6/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
|
||||
github.com/metacubex/sing-mux v0.3.4 h1:tf4r27CIkzaxq9kBlAXQkgMXq2HPp5Mta60Kb4RCZF0=
|
||||
github.com/metacubex/sing-mux v0.3.4/go.mod h1:SEJfAuykNj/ozbPqngEYqyggwSr81+L7Nu09NRD5mh4=
|
||||
github.com/metacubex/sing-quic v0.0.0-20251217080445-b15217cb57f3 h1:3LlkguIRAzyBWLxP5xrETi1AMIt3McZcDlXNgiyXMsE=
|
||||
github.com/metacubex/sing-quic v0.0.0-20251217080445-b15217cb57f3/go.mod h1:fAyoc/8IFK1yJp8meJvPNyGk7ZnKG1vmNaTwYx6NHA4=
|
||||
github.com/metacubex/sing-quic v0.0.0-20260112044712-65d17608159e h1:MLxp42z9Jd6LtY2suyawnl24oNzIsFxWc15bNeDIGxA=
|
||||
github.com/metacubex/sing-quic v0.0.0-20260112044712-65d17608159e/go.mod h1:+lgKTd52xAarGtqugALISShyw4KxnoEpYe2u0zJh26w=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A=
|
||||
@@ -139,14 +138,14 @@ github.com/metacubex/sing-vmess v0.2.4 h1:Tx6AGgCiEf400E/xyDuYyafsel6sGbR8oF7RkA
|
||||
github.com/metacubex/sing-vmess v0.2.4/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80=
|
||||
github.com/metacubex/smux v0.0.0-20251111013112-03f8d12dafc1 h1:a6DF0ze9miXes+rdwl8a4Wkvfpe0lXYU82sPJfDzz6s=
|
||||
github.com/metacubex/smux v0.0.0-20251111013112-03f8d12dafc1/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
|
||||
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 h1:DK2l6m2Fc85H2BhiAPgbJygiWhesPlfGmF+9Vw6ARdk=
|
||||
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141/go.mod h1:/yI4OiGOSn0SURhZdJF3CbtPg3nwK700bG8TZLMBvAg=
|
||||
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 h1:H6TnfM12tOoTizYE/qBHH3nEuibIelmHI+BVSxVJr8o=
|
||||
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
|
||||
github.com/metacubex/tls v0.1.0 h1:1kjR/1q2uU1cZIwiHYEnWzS4L+0Cu1/X3yfIQ76BzNY=
|
||||
github.com/metacubex/tls v0.1.0/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
|
||||
github.com/metacubex/utls v1.8.3 h1:0m/yCxm3SK6kWve2lKiFb1pue1wHitJ8sQQD4Ikqde4=
|
||||
github.com/metacubex/utls v1.8.3/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
|
||||
github.com/metacubex/tls v0.1.1 h1:BEcZrsPTTfNf4sKZ02EbZodv4UIj7fgHWa1Eqo12Bc0=
|
||||
github.com/metacubex/tls v0.1.1/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
|
||||
github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=
|
||||
github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
|
||||
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk=
|
||||
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f/go.mod h1:oPGcV994OGJedmmxrcK9+ni7jUEMGhR+uVQAdaduIP4=
|
||||
github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 h1:lhlqpYHopuTLx9xQt22kSA9HtnyTDmk5XjjQVCGHe2E=
|
||||
@@ -170,8 +169,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/saba-futai/sudoku v0.0.2-d h1:HW/gIyNUFcDchpMN+ZhluM86U/HGkWkkRV+9Km6WZM8=
|
||||
github.com/saba-futai/sudoku v0.0.2-d/go.mod h1:Rvggsoprp7HQM7bMIZUd1M27bPj8THRsZdY1dGbIAvo=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
@@ -185,16 +182,9 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
|
||||
@@ -247,7 +237,6 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
|
||||
@@ -424,6 +424,9 @@ func updateGeneral(general *config.General, logging bool) {
|
||||
mihomoHttp.SetUA(general.GlobalUA)
|
||||
resource.SetETag(general.ETagSupport)
|
||||
|
||||
if general.GlobalClientFingerprint != "" {
|
||||
log.Warnln("The `global-client-fingerprint` configuration is deprecated, please set `client-fingerprint` directly on the proxy instead")
|
||||
}
|
||||
tlsC.SetGlobalFingerprint(general.GlobalClientFingerprint)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
|
||||
@@ -12,26 +14,52 @@ import (
|
||||
func ruleRouter() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/", getRules)
|
||||
if !embedMode { // disallow update/patch rules in embed mode
|
||||
r.Patch("/disable", disableRules)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Index int `json:"index"`
|
||||
Type string `json:"type"`
|
||||
Payload string `json:"payload"`
|
||||
Proxy string `json:"proxy"`
|
||||
Size int `json:"size"`
|
||||
|
||||
// Extra contains information from RuleWrapper
|
||||
Extra *RuleExtra `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
type RuleExtra struct {
|
||||
Disabled bool `json:"disabled"`
|
||||
HitCount uint64 `json:"hitCount"`
|
||||
HitAt time.Time `json:"hitAt"`
|
||||
MissCount uint64 `json:"missCount"`
|
||||
MissAt time.Time `json:"missAt"`
|
||||
}
|
||||
|
||||
func getRules(w http.ResponseWriter, r *http.Request) {
|
||||
rawRules := tunnel.Rules()
|
||||
rules := []Rule{}
|
||||
for _, rule := range rawRules {
|
||||
rules := make([]Rule, 0, len(rawRules))
|
||||
for index, rule := range rawRules {
|
||||
r := Rule{
|
||||
Index: index,
|
||||
Type: rule.RuleType().String(),
|
||||
Payload: rule.Payload(),
|
||||
Proxy: rule.Adapter(),
|
||||
Size: -1,
|
||||
}
|
||||
if ruleWrapper, ok := rule.(constant.RuleWrapper); ok {
|
||||
r.Extra = &RuleExtra{
|
||||
Disabled: ruleWrapper.IsDisabled(),
|
||||
HitCount: ruleWrapper.HitCount(),
|
||||
HitAt: ruleWrapper.HitAt(),
|
||||
MissCount: ruleWrapper.MissCount(),
|
||||
MissAt: ruleWrapper.MissAt(),
|
||||
}
|
||||
rule = ruleWrapper.Unwrap() // unwrap RuleWrapper
|
||||
}
|
||||
if rule.RuleType() == constant.GEOIP || rule.RuleType() == constant.GEOSITE {
|
||||
r.Size = rule.(constant.RuleGroup).GetRecodeSize()
|
||||
}
|
||||
@@ -43,3 +71,29 @@ func getRules(w http.ResponseWriter, r *http.Request) {
|
||||
"rules": rules,
|
||||
})
|
||||
}
|
||||
|
||||
// disableRules disable or enable rules by their indexes.
|
||||
func disableRules(w http.ResponseWriter, r *http.Request) {
|
||||
// key: rule index, value: disabled
|
||||
var payload map[int]bool
|
||||
if err := render.DecodeJSON(r.Body, &payload); err != nil {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(payload) != 0 {
|
||||
rules := tunnel.Rules()
|
||||
for index, disabled := range payload {
|
||||
if index < 0 || index >= len(rules) {
|
||||
continue
|
||||
}
|
||||
rule := rules[index]
|
||||
if ruleWrapper, ok := rule.(constant.RuleWrapper); ok {
|
||||
ruleWrapper.SetDisabled(disabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render.NoContent(w, r)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ type SudokuServer struct {
|
||||
EnablePureDownlink *bool `json:"enable-pure-downlink,omitempty"`
|
||||
CustomTable string `json:"custom-table,omitempty"`
|
||||
CustomTables []string `json:"custom-tables,omitempty"`
|
||||
DisableHTTPMask bool `json:"disable-http-mask,omitempty"`
|
||||
HTTPMaskMode string `json:"http-mask-mode,omitempty"`
|
||||
PathRoot string `json:"path-root,omitempty"`
|
||||
|
||||
// mihomo private extension (not the part of standard Sudoku protocol)
|
||||
MuxOption sing.MuxOption `json:"mux-option,omitempty"`
|
||||
|
||||
@@ -14,6 +14,7 @@ type KcpTun struct {
|
||||
AutoExpire int `inbound:"autoexpire,omitempty"`
|
||||
ScavengeTTL int `inbound:"scavengettl,omitempty"`
|
||||
MTU int `inbound:"mtu,omitempty"`
|
||||
RateLimit int `inbound:"ratelimit,omitempty"`
|
||||
SndWnd int `inbound:"sndwnd,omitempty"`
|
||||
RcvWnd int `inbound:"rcvwnd,omitempty"`
|
||||
DataShard int `inbound:"datashard,omitempty"`
|
||||
@@ -28,6 +29,7 @@ type KcpTun struct {
|
||||
SockBuf int `inbound:"sockbuf,omitempty"`
|
||||
SmuxVer int `inbound:"smuxver,omitempty"`
|
||||
SmuxBuf int `inbound:"smuxbuf,omitempty"`
|
||||
FrameSize int `inbound:"framesize,omitempty"`
|
||||
StreamBuf int `inbound:"streambuf,omitempty"`
|
||||
KeepAlive int `inbound:"keepalive,omitempty"`
|
||||
}
|
||||
@@ -43,6 +45,7 @@ func (c KcpTun) Build() LC.KcpTun {
|
||||
AutoExpire: c.AutoExpire,
|
||||
ScavengeTTL: c.ScavengeTTL,
|
||||
MTU: c.MTU,
|
||||
RateLimit: c.RateLimit,
|
||||
SndWnd: c.SndWnd,
|
||||
RcvWnd: c.RcvWnd,
|
||||
DataShard: c.DataShard,
|
||||
@@ -57,6 +60,7 @@ func (c KcpTun) Build() LC.KcpTun {
|
||||
SockBuf: c.SockBuf,
|
||||
SmuxVer: c.SmuxVer,
|
||||
SmuxBuf: c.SmuxBuf,
|
||||
FrameSize: c.FrameSize,
|
||||
StreamBuf: c.StreamBuf,
|
||||
KeepAlive: c.KeepAlive,
|
||||
},
|
||||
|
||||
@@ -3,7 +3,9 @@ package inbound_test
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/metacubex/mihomo/adapter/outbound"
|
||||
@@ -149,6 +151,9 @@ func TestNewMieru(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInboundMieru(t *testing.T) {
|
||||
if runtime.GOOS == "windows" && strings.HasPrefix(runtime.Version(), "go1.26") {
|
||||
t.Skip("temporarily skipped on windows due to intermittent failures; tracked in PR")
|
||||
}
|
||||
t.Run("TCP_HANDSHAKE_STANDARD", func(t *testing.T) {
|
||||
testInboundMieruTCP(t, "HANDSHAKE_STANDARD")
|
||||
})
|
||||
|
||||
@@ -22,6 +22,9 @@ type SudokuOption struct {
|
||||
EnablePureDownlink *bool `inbound:"enable-pure-downlink,omitempty"`
|
||||
CustomTable string `inbound:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
|
||||
CustomTables []string `inbound:"custom-tables,omitempty"`
|
||||
DisableHTTPMask bool `inbound:"disable-http-mask,omitempty"`
|
||||
HTTPMaskMode string `inbound:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
|
||||
PathRoot string `inbound:"path-root,omitempty"` // optional first-level path prefix for HTTP tunnel endpoints
|
||||
|
||||
// mihomo private extension (not the part of standard Sudoku protocol)
|
||||
MuxOption MuxOption `inbound:"mux-option,omitempty"`
|
||||
@@ -59,6 +62,9 @@ func NewSudoku(options *SudokuOption) (*Sudoku, error) {
|
||||
EnablePureDownlink: options.EnablePureDownlink,
|
||||
CustomTable: options.CustomTable,
|
||||
CustomTables: options.CustomTables,
|
||||
DisableHTTPMask: options.DisableHTTPMask,
|
||||
HTTPMaskMode: options.HTTPMaskMode,
|
||||
PathRoot: strings.TrimSpace(options.PathRoot),
|
||||
}
|
||||
serverConf.MuxOption = options.MuxOption.Build()
|
||||
|
||||
|
||||
@@ -164,3 +164,23 @@ func TestInboundSudoku_CustomTable(t *testing.T) {
|
||||
testInboundSudoku(t, inboundOptions, outboundOptions)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInboundSudoku_HTTPMaskMode(t *testing.T) {
|
||||
key := "test_key_http_mask_mode"
|
||||
|
||||
for _, mode := range []string{"legacy", "stream", "poll", "auto"} {
|
||||
mode := mode
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
inboundOptions := inbound.SudokuOption{
|
||||
Key: key,
|
||||
HTTPMaskMode: mode,
|
||||
}
|
||||
outboundOptions := outbound.SudokuOption{
|
||||
Key: key,
|
||||
HTTPMask: true,
|
||||
HTTPMaskMode: mode,
|
||||
}
|
||||
testInboundSudoku(t, inboundOptions, outboundOptions)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ type Listener struct {
|
||||
addr string
|
||||
closed bool
|
||||
protoConf sudoku.ProtocolConfig
|
||||
tunnelSrv *sudoku.HTTPMaskTunnelServer
|
||||
handler *sing.ListenerHandler
|
||||
}
|
||||
|
||||
@@ -46,15 +47,57 @@ func (l *Listener) Close() error {
|
||||
}
|
||||
|
||||
func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
|
||||
session, err := sudoku.ServerHandshake(conn, &l.protoConf)
|
||||
handshakeConn := conn
|
||||
handshakeCfg := &l.protoConf
|
||||
if l.tunnelSrv != nil {
|
||||
c, cfg, done, err := l.tunnelSrv.WrapConn(conn)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
if done {
|
||||
return
|
||||
}
|
||||
if c != nil {
|
||||
handshakeConn = c
|
||||
}
|
||||
if cfg != nil {
|
||||
handshakeCfg = cfg
|
||||
}
|
||||
}
|
||||
|
||||
session, err := sudoku.ServerHandshake(handshakeConn, handshakeCfg)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
_ = handshakeConn.Close()
|
||||
if handshakeConn != conn {
|
||||
_ = conn.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch session.Type {
|
||||
case sudoku.SessionTypeUoT:
|
||||
l.handleUoTSession(session.Conn, tunnel, additions...)
|
||||
case sudoku.SessionTypeMultiplex:
|
||||
mux, err := sudoku.AcceptMultiplexServer(session.Conn)
|
||||
if err != nil {
|
||||
_ = session.Conn.Close()
|
||||
return
|
||||
}
|
||||
defer mux.Close()
|
||||
|
||||
for {
|
||||
stream, target, err := mux.AcceptTCP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
targetAddr := socks5.ParseAddr(target)
|
||||
if targetAddr == nil {
|
||||
_ = stream.Close()
|
||||
continue
|
||||
}
|
||||
go l.handler.HandleSocket(targetAddr, stream, additions...)
|
||||
}
|
||||
default:
|
||||
targetAddr := socks5.ParseAddr(session.Target)
|
||||
if targetAddr == nil {
|
||||
@@ -184,6 +227,9 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
|
||||
PaddingMax: paddingMax,
|
||||
EnablePureDownlink: enablePureDownlink,
|
||||
HandshakeTimeoutSeconds: handshakeTimeout,
|
||||
DisableHTTPMask: config.DisableHTTPMask,
|
||||
HTTPMaskMode: config.HTTPMaskMode,
|
||||
HTTPMaskPathRoot: strings.TrimSpace(config.PathRoot),
|
||||
}
|
||||
if len(tables) == 1 {
|
||||
protoConf.Table = tables[0]
|
||||
@@ -200,6 +246,7 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
|
||||
protoConf: protoConf,
|
||||
handler: h,
|
||||
}
|
||||
sl.tunnelSrv = sudoku.NewHTTPMaskTunnelServer(&sl.protoConf)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package tproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/metacubex/mihomo/adapter/inbound"
|
||||
"github.com/metacubex/mihomo/component/keepalive"
|
||||
"github.com/metacubex/mihomo/component/mptcp"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/transport/socks5"
|
||||
)
|
||||
@@ -46,7 +48,11 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener
|
||||
inbound.WithSpecialRules(""),
|
||||
}
|
||||
}
|
||||
l, err := net.Listen("tcp", addr)
|
||||
// Golang will then enable mptcp support for listeners by default when the major version of go.mod is 1.24 or higher.
|
||||
// This can cause tproxy to malfunction on certain Linux kernel versions, so we force to disable mptcp for tproxy.
|
||||
lc := net.ListenConfig{}
|
||||
mptcp.SetNetListenConfig(&lc, false)
|
||||
l, err := lc.Listen(context.Background(), "tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type Domain struct {
|
||||
*Base
|
||||
Base
|
||||
domain string
|
||||
adapter string
|
||||
}
|
||||
@@ -32,10 +32,10 @@ func (d *Domain) Payload() string {
|
||||
func NewDomain(domain string, adapter string) *Domain {
|
||||
punycode, _ := idna.ToASCII(strings.ToLower(domain))
|
||||
return &Domain{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
domain: punycode,
|
||||
adapter: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
//var _ C.Rule = (*Domain)(nil)
|
||||
var _ C.Rule = (*Domain)(nil)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type DomainKeyword struct {
|
||||
*Base
|
||||
Base
|
||||
keyword string
|
||||
adapter string
|
||||
}
|
||||
@@ -33,10 +33,10 @@ func (dk *DomainKeyword) Payload() string {
|
||||
func NewDomainKeyword(keyword string, adapter string) *DomainKeyword {
|
||||
punycode, _ := idna.ToASCII(strings.ToLower(keyword))
|
||||
return &DomainKeyword{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
keyword: punycode,
|
||||
adapter: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
//var _ C.Rule = (*DomainKeyword)(nil)
|
||||
var _ C.Rule = (*DomainKeyword)(nil)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
type DomainRegex struct {
|
||||
*Base
|
||||
Base
|
||||
regex *regexp2.Regexp
|
||||
adapter string
|
||||
}
|
||||
@@ -36,10 +36,10 @@ func NewDomainRegex(regex string, adapter string) (*DomainRegex, error) {
|
||||
return nil, err
|
||||
}
|
||||
return &DomainRegex{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
regex: r,
|
||||
adapter: adapter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
//var _ C.Rule = (*DomainRegex)(nil)
|
||||
var _ C.Rule = (*DomainRegex)(nil)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type DomainSuffix struct {
|
||||
*Base
|
||||
Base
|
||||
suffix string
|
||||
adapter string
|
||||
}
|
||||
@@ -33,10 +33,10 @@ func (ds *DomainSuffix) Payload() string {
|
||||
func NewDomainSuffix(suffix string, adapter string) *DomainSuffix {
|
||||
punycode, _ := idna.ToASCII(strings.ToLower(suffix))
|
||||
return &DomainSuffix{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
suffix: punycode,
|
||||
adapter: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
//var _ C.Rule = (*DomainSuffix)(nil)
|
||||
var _ C.Rule = (*DomainSuffix)(nil)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type DomainWildcard struct {
|
||||
*Base
|
||||
Base
|
||||
pattern string
|
||||
adapter string
|
||||
}
|
||||
@@ -34,8 +34,10 @@ var _ C.Rule = (*DomainWildcard)(nil)
|
||||
func NewDomainWildcard(pattern string, adapter string) (*DomainWildcard, error) {
|
||||
pattern = strings.ToLower(pattern)
|
||||
return &DomainWildcard{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
pattern: pattern,
|
||||
adapter: adapter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ C.Rule = (*DomainWildcard)(nil)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type DSCP struct {
|
||||
*Base
|
||||
Base
|
||||
ranges utils.IntRanges[uint8]
|
||||
payload string
|
||||
adapter string
|
||||
@@ -41,9 +41,11 @@ func NewDSCP(dscp string, adapter string) (*DSCP, error) {
|
||||
}
|
||||
}
|
||||
return &DSCP{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
payload: dscp,
|
||||
ranges: ranges,
|
||||
adapter: adapter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ C.Rule = (*DSCP)(nil)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
type Match struct {
|
||||
*Base
|
||||
Base
|
||||
adapter string
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ func (f *Match) Payload() string {
|
||||
|
||||
func NewMatch(adapter string) *Match {
|
||||
return &Match{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
adapter: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
//var _ C.Rule = (*Match)(nil)
|
||||
var _ C.Rule = (*Match)(nil)
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
type GEOIP struct {
|
||||
*Base
|
||||
Base
|
||||
country string
|
||||
adapter string
|
||||
noResolveIP bool
|
||||
@@ -128,6 +128,10 @@ func (g dnsFallbackFilter) MatchIp(ip netip.Addr) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if g.country == "lan" {
|
||||
return !g.isLan(ip)
|
||||
}
|
||||
|
||||
if geodata.GeodataMode() {
|
||||
matcher, err := g.getIPMatcher()
|
||||
if err != nil {
|
||||
@@ -186,6 +190,11 @@ func (g *GEOIP) getIPMatcher() (router.IPMatcher, error) {
|
||||
}
|
||||
|
||||
func (g *GEOIP) GetRecodeSize() int {
|
||||
// skip pseudorule lan
|
||||
if g.country == "lan" {
|
||||
return 0
|
||||
}
|
||||
|
||||
if matcher, err := g.GetIPMatcher(); err == nil {
|
||||
return matcher.Count()
|
||||
}
|
||||
@@ -196,7 +205,7 @@ func NewGEOIP(country string, adapter string, isSrc, noResolveIP bool) (*GEOIP,
|
||||
country = strings.ToLower(country)
|
||||
|
||||
geoip := &GEOIP{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
country: country,
|
||||
adapter: adapter,
|
||||
noResolveIP: noResolveIP,
|
||||
@@ -222,3 +231,5 @@ func NewGEOIP(country string, adapter string, isSrc, noResolveIP bool) (*GEOIP,
|
||||
|
||||
return geoip, nil
|
||||
}
|
||||
|
||||
var _ C.Rule = (*GEOIP)(nil)
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
type GEOSITE struct {
|
||||
*Base
|
||||
Base
|
||||
country string
|
||||
adapter string
|
||||
recodeSize int
|
||||
@@ -68,7 +68,7 @@ func NewGEOSITE(country string, adapter string) (*GEOSITE, error) {
|
||||
}
|
||||
|
||||
geoSite := &GEOSITE{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
country: country,
|
||||
adapter: adapter,
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type InName struct {
|
||||
*Base
|
||||
Base
|
||||
names []string
|
||||
adapter string
|
||||
payload string
|
||||
@@ -46,9 +46,11 @@ func NewInName(iNames, adapter string) (*InName, error) {
|
||||
}
|
||||
|
||||
return &InName{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
names: names,
|
||||
adapter: adapter,
|
||||
payload: iNames,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ C.Rule = (*InName)(nil)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type InType struct {
|
||||
*Base
|
||||
Base
|
||||
types []C.Type
|
||||
adapter string
|
||||
payload string
|
||||
@@ -51,7 +51,7 @@ func NewInType(iTypes, adapter string) (*InType, error) {
|
||||
}
|
||||
|
||||
return &InType{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
types: tps,
|
||||
adapter: adapter,
|
||||
payload: strings.ToUpper(iTypes),
|
||||
@@ -77,3 +77,5 @@ func parseInTypes(tps []string) (res []C.Type, err error) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var _ C.Rule = (*InType)(nil)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type InUser struct {
|
||||
*Base
|
||||
Base
|
||||
users []string
|
||||
adapter string
|
||||
payload string
|
||||
@@ -46,9 +46,11 @@ func NewInUser(iUsers, adapter string) (*InUser, error) {
|
||||
}
|
||||
|
||||
return &InUser{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
users: users,
|
||||
adapter: adapter,
|
||||
payload: iUsers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ C.Rule = (*InUser)(nil)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type ASN struct {
|
||||
*Base
|
||||
Base
|
||||
asn string
|
||||
adapter string
|
||||
noResolveIP bool
|
||||
@@ -64,10 +64,12 @@ func NewIPASN(asn string, adapter string, isSrc, noResolveIP bool) (*ASN, error)
|
||||
}
|
||||
|
||||
return &ASN{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
asn: asn,
|
||||
adapter: adapter,
|
||||
noResolveIP: noResolveIP,
|
||||
isSourceIP: isSrc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ C.Rule = (*ASN)(nil)
|
||||
|
||||
@@ -21,7 +21,7 @@ func WithIPCIDRNoResolve(noResolve bool) IPCIDROption {
|
||||
}
|
||||
|
||||
type IPCIDR struct {
|
||||
*Base
|
||||
Base
|
||||
ipnet netip.Prefix
|
||||
adapter string
|
||||
isSourceIP bool
|
||||
@@ -62,7 +62,7 @@ func NewIPCIDR(s string, adapter string, opts ...IPCIDROption) (*IPCIDR, error)
|
||||
}
|
||||
|
||||
ipcidr := &IPCIDR{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
ipnet: ipnet,
|
||||
adapter: adapter,
|
||||
}
|
||||
@@ -74,4 +74,4 @@ func NewIPCIDR(s string, adapter string, opts ...IPCIDROption) (*IPCIDR, error)
|
||||
return ipcidr, nil
|
||||
}
|
||||
|
||||
//var _ C.Rule = (*IPCIDR)(nil)
|
||||
var _ C.Rule = (*IPCIDR)(nil)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"net/netip"
|
||||
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
)
|
||||
|
||||
type IPSuffix struct {
|
||||
*Base
|
||||
Base
|
||||
ipBytes []byte
|
||||
bits int
|
||||
payload string
|
||||
@@ -68,7 +69,7 @@ func NewIPSuffix(payload, adapter string, isSrc, noResolveIP bool) (*IPSuffix, e
|
||||
}
|
||||
|
||||
return &IPSuffix{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
payload: payload,
|
||||
ipBytes: ipnet.Addr().AsSlice(),
|
||||
bits: ipnet.Bits(),
|
||||
@@ -77,3 +78,5 @@ func NewIPSuffix(payload, adapter string, isSrc, noResolveIP bool) (*IPSuffix, e
|
||||
noResolveIP: noResolveIP,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ C.Rule = (*IPSuffix)(nil)
|
||||
|
||||
@@ -2,19 +2,20 @@ package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"strings"
|
||||
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
)
|
||||
|
||||
type NetworkType struct {
|
||||
*Base
|
||||
Base
|
||||
network C.NetWork
|
||||
adapter string
|
||||
}
|
||||
|
||||
func NewNetworkType(network, adapter string) (*NetworkType, error) {
|
||||
ntType := NetworkType{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
}
|
||||
|
||||
ntType.adapter = adapter
|
||||
@@ -45,3 +46,5 @@ func (n *NetworkType) Adapter() string {
|
||||
func (n *NetworkType) Payload() string {
|
||||
return n.network.String()
|
||||
}
|
||||
|
||||
var _ C.Rule = (*NetworkType)(nil)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type Port struct {
|
||||
*Base
|
||||
Base
|
||||
adapter string
|
||||
port string
|
||||
ruleType C.RuleType
|
||||
@@ -49,7 +49,7 @@ func NewPort(port string, adapter string, ruleType C.RuleType) (*Port, error) {
|
||||
}
|
||||
|
||||
return &Port{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
adapter: adapter,
|
||||
port: port,
|
||||
ruleType: ruleType,
|
||||
|
||||
@@ -3,74 +3,72 @@ package common
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/metacubex/mihomo/component/wildcard"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
)
|
||||
|
||||
type Process struct {
|
||||
*Base
|
||||
Base
|
||||
pattern string
|
||||
adapter string
|
||||
process string
|
||||
nameOnly bool
|
||||
ruleType C.RuleType
|
||||
regexp *regexp2.Regexp
|
||||
}
|
||||
|
||||
func (ps *Process) RuleType() C.RuleType {
|
||||
if ps.nameOnly {
|
||||
if ps.regexp != nil {
|
||||
return C.ProcessNameRegex
|
||||
}
|
||||
return C.ProcessName
|
||||
}
|
||||
|
||||
if ps.regexp != nil {
|
||||
return C.ProcessPathRegex
|
||||
}
|
||||
return C.ProcessPath
|
||||
}
|
||||
|
||||
func (ps *Process) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) {
|
||||
if helper.FindProcess != nil {
|
||||
helper.FindProcess()
|
||||
}
|
||||
if ps.nameOnly {
|
||||
if ps.regexp != nil {
|
||||
match, _ := ps.regexp.MatchString(metadata.Process)
|
||||
return match, ps.adapter
|
||||
}
|
||||
return strings.EqualFold(metadata.Process, ps.process), ps.adapter
|
||||
}
|
||||
|
||||
if ps.regexp != nil {
|
||||
match, _ := ps.regexp.MatchString(metadata.ProcessPath)
|
||||
return match, ps.adapter
|
||||
}
|
||||
return strings.EqualFold(metadata.ProcessPath, ps.process), ps.adapter
|
||||
func (ps *Process) Payload() string {
|
||||
return ps.pattern
|
||||
}
|
||||
|
||||
func (ps *Process) Adapter() string {
|
||||
return ps.adapter
|
||||
}
|
||||
|
||||
func (ps *Process) Payload() string {
|
||||
return ps.process
|
||||
func (ps *Process) RuleType() C.RuleType {
|
||||
return ps.ruleType
|
||||
}
|
||||
|
||||
func NewProcess(process string, adapter string, nameOnly bool, regex bool) (*Process, error) {
|
||||
var r *regexp2.Regexp
|
||||
var err error
|
||||
if regex {
|
||||
r, err = regexp2.Compile(process, regexp2.IgnoreCase)
|
||||
func (ps *Process) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) {
|
||||
if helper.FindProcess != nil {
|
||||
helper.FindProcess()
|
||||
}
|
||||
var target string
|
||||
switch ps.ruleType {
|
||||
case C.ProcessName, C.ProcessNameRegex, C.ProcessNameWildcard:
|
||||
target = metadata.Process
|
||||
default:
|
||||
target = metadata.ProcessPath
|
||||
}
|
||||
|
||||
switch ps.ruleType {
|
||||
case C.ProcessNameRegex, C.ProcessPathRegex:
|
||||
match, _ := ps.regexp.MatchString(target)
|
||||
return match, ps.adapter
|
||||
case C.ProcessNameWildcard, C.ProcessPathWildcard:
|
||||
return wildcard.Match(strings.ToLower(ps.pattern), strings.ToLower(target)), ps.adapter
|
||||
default:
|
||||
return strings.EqualFold(target, ps.pattern), ps.adapter
|
||||
}
|
||||
}
|
||||
|
||||
func NewProcess(pattern string, adapter string, ruleType C.RuleType) (*Process, error) {
|
||||
ps := &Process{
|
||||
Base: Base{},
|
||||
pattern: pattern,
|
||||
adapter: adapter,
|
||||
ruleType: ruleType,
|
||||
}
|
||||
switch ps.ruleType {
|
||||
case C.ProcessNameRegex, C.ProcessPathRegex:
|
||||
r, err := regexp2.Compile(pattern, regexp2.IgnoreCase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ps.regexp = r
|
||||
default:
|
||||
}
|
||||
return &Process{
|
||||
Base: &Base{},
|
||||
adapter: adapter,
|
||||
process: process,
|
||||
nameOnly: nameOnly,
|
||||
regexp: r,
|
||||
}, nil
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
var _ C.Rule = (*Process)(nil)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type Uid struct {
|
||||
*Base
|
||||
Base
|
||||
uids utils.IntRanges[uint32]
|
||||
oUid string
|
||||
adapter string
|
||||
@@ -30,7 +30,7 @@ func NewUid(oUid, adapter string) (*Uid, error) {
|
||||
return nil, errPayload
|
||||
}
|
||||
return &Uid{
|
||||
Base: &Base{},
|
||||
Base: Base{},
|
||||
adapter: adapter,
|
||||
oUid: oUid,
|
||||
uids: uidRange,
|
||||
@@ -61,3 +61,5 @@ func (u *Uid) Adapter() string {
|
||||
func (u *Uid) Payload() string {
|
||||
return u.oUid
|
||||
}
|
||||
|
||||
var _ C.Rule = (*Uid)(nil)
|
||||
|
||||
@@ -2,18 +2,16 @@ package logic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/rules/common"
|
||||
|
||||
list "github.com/bahlo/generic-list-go"
|
||||
)
|
||||
|
||||
type Logic struct {
|
||||
*common.Base
|
||||
common.Base
|
||||
payload string
|
||||
adapter string
|
||||
ruleType C.RuleType
|
||||
@@ -24,7 +22,7 @@ type Logic struct {
|
||||
}
|
||||
|
||||
func NewSubRule(payload, adapter string, subRules map[string][]C.Rule, parseRule common.ParseRuleFunc) (*Logic, error) {
|
||||
logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.SubRules, subRules: subRules}
|
||||
logic := &Logic{Base: common.Base{}, payload: payload, adapter: adapter, ruleType: C.SubRules, subRules: subRules}
|
||||
err := logic.parsePayload(fmt.Sprintf("(%s)", payload), parseRule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -37,7 +35,7 @@ func NewSubRule(payload, adapter string, subRules map[string][]C.Rule, parseRule
|
||||
}
|
||||
|
||||
func NewNOT(payload string, adapter string, parseRule common.ParseRuleFunc) (*Logic, error) {
|
||||
logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.NOT}
|
||||
logic := &Logic{Base: common.Base{}, payload: payload, adapter: adapter, ruleType: C.NOT}
|
||||
err := logic.parsePayload(payload, parseRule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -50,7 +48,7 @@ func NewNOT(payload string, adapter string, parseRule common.ParseRuleFunc) (*Lo
|
||||
}
|
||||
|
||||
func NewOR(payload string, adapter string, parseRule common.ParseRuleFunc) (*Logic, error) {
|
||||
logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.OR}
|
||||
logic := &Logic{Base: common.Base{}, payload: payload, adapter: adapter, ruleType: C.OR}
|
||||
err := logic.parsePayload(payload, parseRule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -59,7 +57,7 @@ func NewOR(payload string, adapter string, parseRule common.ParseRuleFunc) (*Log
|
||||
}
|
||||
|
||||
func NewAND(payload string, adapter string, parseRule common.ParseRuleFunc) (*Logic, error) {
|
||||
logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.AND}
|
||||
logic := &Logic{Base: common.Base{}, payload: payload, adapter: adapter, ruleType: C.AND}
|
||||
err := logic.parsePayload(payload, parseRule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -70,7 +68,6 @@ func NewAND(payload string, adapter string, parseRule common.ParseRuleFunc) (*Lo
|
||||
type Range struct {
|
||||
start int
|
||||
end int
|
||||
index int
|
||||
}
|
||||
|
||||
func (r Range) containRange(preStart, preEnd int) bool {
|
||||
@@ -89,40 +86,35 @@ func (logic *Logic) payloadToRule(subPayload string, parseRule common.ParseRuleF
|
||||
}
|
||||
|
||||
func (logic *Logic) format(payload string) ([]Range, error) {
|
||||
stack := list.New[Range]()
|
||||
num := 0
|
||||
stack := make([]int, 0)
|
||||
subRanges := make([]Range, 0)
|
||||
for i, c := range payload {
|
||||
if c == '(' {
|
||||
sr := Range{
|
||||
start: i,
|
||||
index: num,
|
||||
}
|
||||
|
||||
num++
|
||||
stack.PushBack(sr)
|
||||
stack = append(stack, i) // push
|
||||
} else if c == ')' {
|
||||
if stack.Len() == 0 {
|
||||
if len(stack) == 0 {
|
||||
return nil, fmt.Errorf("missing '('")
|
||||
}
|
||||
|
||||
sr := stack.Back()
|
||||
stack.Remove(sr)
|
||||
sr.Value.end = i
|
||||
subRanges = append(subRanges, sr.Value)
|
||||
back := len(stack) - 1
|
||||
start := stack[back] // back
|
||||
stack = stack[:back] // pop
|
||||
subRanges = append(subRanges, Range{
|
||||
start: start,
|
||||
end: i,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if stack.Len() != 0 {
|
||||
if len(stack) != 0 {
|
||||
return nil, fmt.Errorf("format error is missing )")
|
||||
}
|
||||
|
||||
sortResult := make([]Range, len(subRanges))
|
||||
for _, sr := range subRanges {
|
||||
sortResult[sr.index] = sr
|
||||
}
|
||||
sort.Slice(subRanges, func(i, j int) bool {
|
||||
return subRanges[i].start < subRanges[j].start
|
||||
})
|
||||
|
||||
return sortResult, nil
|
||||
return subRanges, nil
|
||||
}
|
||||
|
||||
func (logic *Logic) findSubRuleRange(payload string, ruleRanges []Range) []Range {
|
||||
@@ -152,36 +144,32 @@ func (logic *Logic) findSubRuleRange(payload string, ruleRanges []Range) []Range
|
||||
}
|
||||
|
||||
func (logic *Logic) parsePayload(payload string, parseRule common.ParseRuleFunc) error {
|
||||
regex, err := regexp.Compile("\\(.*\\)")
|
||||
if !strings.HasPrefix(payload, "(") || !strings.HasSuffix(payload, ")") { // the payload must be "(xxx)" format
|
||||
return fmt.Errorf("payload format error")
|
||||
}
|
||||
|
||||
subAllRanges, err := logic.format(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if regex.MatchString(payload) {
|
||||
subAllRanges, err := logic.format(payload)
|
||||
rules := make([]C.Rule, 0, len(subAllRanges))
|
||||
|
||||
subRanges := logic.findSubRuleRange(payload, subAllRanges)
|
||||
for _, subRange := range subRanges {
|
||||
subPayload := payload[subRange.start+1 : subRange.end]
|
||||
|
||||
rule, err := logic.payloadToRule(subPayload, parseRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rules := make([]C.Rule, 0, len(subAllRanges))
|
||||
|
||||
subRanges := logic.findSubRuleRange(payload, subAllRanges)
|
||||
for _, subRange := range subRanges {
|
||||
subPayload := payload[subRange.start+1 : subRange.end]
|
||||
|
||||
rule, err := logic.payloadToRule(subPayload, parseRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
logic.rules = rules
|
||||
|
||||
return nil
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return fmt.Errorf("payload format error")
|
||||
logic.rules = rules
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (logic *Logic) RuleType() C.RuleType {
|
||||
@@ -265,3 +253,5 @@ func (logic *Logic) ProviderNames() (names []string) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var _ C.Rule = (*Logic)(nil)
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package logic_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
// https://github.com/golang/go/wiki/CodeReviewComments#import-dot
|
||||
. "github.com/metacubex/mihomo/rules/logic"
|
||||
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/rules"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var ParseRule = rules.ParseRule
|
||||
@@ -38,6 +40,12 @@ func TestNOT(t *testing.T) {
|
||||
}, C.RuleMatchHelper{})
|
||||
assert.Equal(t, false, m)
|
||||
|
||||
_, err = NewNOT("(DST-PORT,5600-6666)", "DIRECT", ParseRule)
|
||||
assert.NotEqual(t, nil, err)
|
||||
|
||||
_, err = NewNOT("DST-PORT,5600-6666", "DIRECT", ParseRule)
|
||||
assert.NotEqual(t, nil, err)
|
||||
|
||||
_, err = NewNOT("((DST-PORT,5600-6666),(DOMAIN,baidu.com))", "DIRECT", ParseRule)
|
||||
assert.NotEqual(t, nil, err)
|
||||
|
||||
|
||||
@@ -56,13 +56,17 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string]
|
||||
case "DSCP":
|
||||
parsed, parseErr = RC.NewDSCP(payload, target)
|
||||
case "PROCESS-NAME":
|
||||
parsed, parseErr = RC.NewProcess(payload, target, true, false)
|
||||
parsed, parseErr = RC.NewProcess(payload, target, C.ProcessName)
|
||||
case "PROCESS-PATH":
|
||||
parsed, parseErr = RC.NewProcess(payload, target, false, false)
|
||||
parsed, parseErr = RC.NewProcess(payload, target, C.ProcessPath)
|
||||
case "PROCESS-NAME-REGEX":
|
||||
parsed, parseErr = RC.NewProcess(payload, target, true, true)
|
||||
parsed, parseErr = RC.NewProcess(payload, target, C.ProcessNameRegex)
|
||||
case "PROCESS-PATH-REGEX":
|
||||
parsed, parseErr = RC.NewProcess(payload, target, false, true)
|
||||
parsed, parseErr = RC.NewProcess(payload, target, C.ProcessPathRegex)
|
||||
case "PROCESS-NAME-WILDCARD":
|
||||
parsed, parseErr = RC.NewProcess(payload, target, C.ProcessNameWildcard)
|
||||
case "PROCESS-PATH-WILDCARD":
|
||||
parsed, parseErr = RC.NewProcess(payload, target, C.ProcessPathWildcard)
|
||||
case "NETWORK":
|
||||
parsed, parseErr = RC.NewNetworkType(payload, target)
|
||||
case "UID":
|
||||
|
||||
@@ -12,15 +12,16 @@ import (
|
||||
)
|
||||
|
||||
type ruleProviderSchema struct {
|
||||
Type string `provider:"type"`
|
||||
Behavior string `provider:"behavior"`
|
||||
Path string `provider:"path,omitempty"`
|
||||
URL string `provider:"url,omitempty"`
|
||||
Proxy string `provider:"proxy,omitempty"`
|
||||
Format string `provider:"format,omitempty"`
|
||||
Interval int `provider:"interval,omitempty"`
|
||||
SizeLimit int64 `provider:"size-limit,omitempty"`
|
||||
Payload []string `provider:"payload,omitempty"`
|
||||
Type string `provider:"type"`
|
||||
Behavior string `provider:"behavior"`
|
||||
Path string `provider:"path,omitempty"`
|
||||
URL string `provider:"url,omitempty"`
|
||||
Proxy string `provider:"proxy,omitempty"`
|
||||
Format string `provider:"format,omitempty"`
|
||||
Interval int `provider:"interval,omitempty"`
|
||||
SizeLimit int64 `provider:"size-limit,omitempty"`
|
||||
Payload []string `provider:"payload,omitempty"`
|
||||
Header map[string][]string `provider:"header,omitempty"`
|
||||
}
|
||||
|
||||
func ParseRuleProvider(name string, mapping map[string]any, parse common.ParseRuleFunc) (P.RuleProvider, error) {
|
||||
@@ -54,7 +55,7 @@ func ParseRuleProvider(name string, mapping map[string]any, parse common.ParseRu
|
||||
return nil, C.Path.ErrNotSafePath(path)
|
||||
}
|
||||
}
|
||||
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, nil, resource.DefaultHttpTimeout, schema.SizeLimit)
|
||||
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit)
|
||||
case "inline":
|
||||
return NewInlineProvider(name, behavior, schema.Payload, parse), nil
|
||||
default:
|
||||
|
||||
@@ -10,12 +10,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/common/pool"
|
||||
"github.com/metacubex/mihomo/common/yaml"
|
||||
"github.com/metacubex/mihomo/component/resource"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
P "github.com/metacubex/mihomo/constant/provider"
|
||||
"github.com/metacubex/mihomo/rules/common"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var tunnel P.Tunnel
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
type RuleSet struct {
|
||||
*common.Base
|
||||
common.Base
|
||||
ruleProviderName string
|
||||
adapter string
|
||||
isSrc bool
|
||||
@@ -66,7 +66,7 @@ func (rs *RuleSet) getProvider() (P.RuleProvider, bool) {
|
||||
|
||||
func NewRuleSet(ruleProviderName string, adapter string, isSrc bool, noResolveIP bool) (*RuleSet, error) {
|
||||
rs := &RuleSet{
|
||||
Base: &common.Base{},
|
||||
Base: common.Base{},
|
||||
ruleProviderName: ruleProviderName,
|
||||
adapter: adapter,
|
||||
isSrc: isSrc,
|
||||
@@ -74,3 +74,5 @@ func NewRuleSet(ruleProviderName string, adapter string, isSrc bool, noResolveIP
|
||||
}
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
var _ C.Rule = (*RuleSet)(nil)
|
||||
|
||||
110
rules/wrapper/wrapper.go
Normal file
110
rules/wrapper/wrapper.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
)
|
||||
|
||||
type RuleWrapper struct {
|
||||
C.Rule
|
||||
disabled atomic.Bool
|
||||
hitCount atomic.Uint64
|
||||
hitAt atomicTime
|
||||
missCount atomic.Uint64
|
||||
missAt atomicTime
|
||||
}
|
||||
|
||||
func (r *RuleWrapper) IsDisabled() bool {
|
||||
return r.disabled.Load()
|
||||
}
|
||||
|
||||
func (r *RuleWrapper) SetDisabled(v bool) {
|
||||
r.disabled.Store(v)
|
||||
}
|
||||
|
||||
func (r *RuleWrapper) HitCount() uint64 {
|
||||
return r.hitCount.Load()
|
||||
}
|
||||
|
||||
func (r *RuleWrapper) HitAt() time.Time {
|
||||
return r.hitAt.Load()
|
||||
}
|
||||
|
||||
func (r *RuleWrapper) MissCount() uint64 {
|
||||
return r.missCount.Load()
|
||||
}
|
||||
|
||||
func (r *RuleWrapper) MissAt() time.Time {
|
||||
return r.missAt.Load()
|
||||
}
|
||||
|
||||
func (r *RuleWrapper) Unwrap() C.Rule {
|
||||
return r.Rule
|
||||
}
|
||||
|
||||
func (r *RuleWrapper) Hit() {
|
||||
r.hitCount.Add(1)
|
||||
r.hitAt.Store(time.Now())
|
||||
}
|
||||
|
||||
func (r *RuleWrapper) Miss() {
|
||||
r.missCount.Add(1)
|
||||
r.missAt.Store(time.Now())
|
||||
}
|
||||
|
||||
func (r *RuleWrapper) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) {
|
||||
if r.IsDisabled() {
|
||||
return false, ""
|
||||
}
|
||||
ok, adapter := r.Rule.Match(metadata, helper)
|
||||
if ok {
|
||||
r.Hit()
|
||||
} else {
|
||||
r.Miss()
|
||||
}
|
||||
return ok, adapter
|
||||
}
|
||||
|
||||
func NewRuleWrapper(rule C.Rule) C.RuleWrapper {
|
||||
return &RuleWrapper{Rule: rule}
|
||||
}
|
||||
|
||||
// atomicTime is a wrapper of [atomic.Int64] to provide atomic time storage.
|
||||
// it only saves unix nanosecond export from time.Time.
|
||||
// unlike atomic.TypedValue[time.Time] always escapes a new time.Time to heap when storing.
|
||||
// that will lead to higher GC pressure during high frequency writes.
|
||||
// be careful, it discards monotime so should not be used for internal time comparisons.
|
||||
type atomicTime struct {
|
||||
i atomic.Int64
|
||||
}
|
||||
|
||||
func (t *atomicTime) Load() time.Time {
|
||||
return time.Unix(0, t.i.Load())
|
||||
}
|
||||
|
||||
func (t *atomicTime) Store(v time.Time) {
|
||||
t.i.Store(v.UnixNano())
|
||||
}
|
||||
|
||||
func (t *atomicTime) Swap(v time.Time) time.Time {
|
||||
return time.Unix(0, t.i.Swap(v.UnixNano()))
|
||||
}
|
||||
|
||||
func (t *atomicTime) CompareAndSwap(old, new time.Time) bool {
|
||||
return t.i.CompareAndSwap(old.UnixNano(), new.UnixNano())
|
||||
}
|
||||
|
||||
func (t *atomicTime) MarshalText() ([]byte, error) {
|
||||
return t.Load().MarshalText()
|
||||
}
|
||||
|
||||
func (t *atomicTime) UnmarshalText(text []byte) error {
|
||||
var v time.Time
|
||||
if err := v.UnmarshalText(text); err != nil {
|
||||
return err
|
||||
}
|
||||
t.Store(v)
|
||||
return nil
|
||||
}
|
||||
@@ -60,6 +60,7 @@ type Conn struct {
|
||||
|
||||
type Config struct {
|
||||
ServiceName string
|
||||
UserAgent string
|
||||
Host string
|
||||
ClientFingerprint string
|
||||
}
|
||||
@@ -347,6 +348,12 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
|
||||
path := ServiceNameToPath(serviceName)
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
|
||||
header := defaultHeader.Clone()
|
||||
if cfg.UserAgent != "" {
|
||||
header["user-agent"] = []string{cfg.UserAgent}
|
||||
}
|
||||
|
||||
request := &http.Request{
|
||||
Method: http.MethodPost,
|
||||
Body: reader,
|
||||
@@ -360,7 +367,7 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
|
||||
Proto: "HTTP/2",
|
||||
ProtoMajor: 2,
|
||||
ProtoMinor: 0,
|
||||
Header: defaultHeader,
|
||||
Header: header,
|
||||
}
|
||||
request = request.WithContext(transport.ctx)
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ func (b *BrutalSender) OnCongestionEvent(number congestion.PacketNumber, lostByt
|
||||
}
|
||||
|
||||
func (b *BrutalSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime monotime.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) {
|
||||
currentTimestamp := int64(eventTime)
|
||||
currentTimestamp := int64(time.Duration(eventTime) / time.Second)
|
||||
slot := currentTimestamp % pktInfoSlotCount
|
||||
if b.pktInfoSlots[slot].Timestamp == currentTimestamp {
|
||||
b.pktInfoSlots[slot].LossCount += uint64(len(lostPackets))
|
||||
|
||||
@@ -70,6 +70,7 @@ func (c *Client) createConn(ctx context.Context, dial DialFn) (*smux.Session, er
|
||||
kcpconn.SetWindowSize(config.SndWnd, config.RcvWnd)
|
||||
kcpconn.SetMtu(config.MTU)
|
||||
kcpconn.SetACKNoDelay(config.AckNodelay)
|
||||
kcpconn.SetRateLimit(uint32(config.RateLimit))
|
||||
|
||||
_ = kcpconn.SetDSCP(config.DSCP)
|
||||
_ = kcpconn.SetReadBuffer(config.SockBuf)
|
||||
@@ -78,6 +79,7 @@ func (c *Client) createConn(ctx context.Context, dial DialFn) (*smux.Session, er
|
||||
smuxConfig.Version = config.SmuxVer
|
||||
smuxConfig.MaxReceiveBuffer = config.SmuxBuf
|
||||
smuxConfig.MaxStreamBuffer = config.StreamBuf
|
||||
smuxConfig.MaxFrameSize = config.FrameSize
|
||||
smuxConfig.KeepAliveInterval = time.Duration(config.KeepAlive) * time.Second
|
||||
if smuxConfig.KeepAliveInterval >= smuxConfig.KeepAliveTimeout {
|
||||
smuxConfig.KeepAliveTimeout = 3 * smuxConfig.KeepAliveInterval
|
||||
|
||||
@@ -26,6 +26,7 @@ type Config struct {
|
||||
AutoExpire int `json:"autoexpire"`
|
||||
ScavengeTTL int `json:"scavengettl"`
|
||||
MTU int `json:"mtu"`
|
||||
RateLimit int `json:"ratelimit"`
|
||||
SndWnd int `json:"sndwnd"`
|
||||
RcvWnd int `json:"rcvwnd"`
|
||||
DataShard int `json:"datashard"`
|
||||
@@ -40,6 +41,7 @@ type Config struct {
|
||||
SockBuf int `json:"sockbuf"`
|
||||
SmuxVer int `json:"smuxver"`
|
||||
SmuxBuf int `json:"smuxbuf"`
|
||||
FrameSize int `json:"framesize"`
|
||||
StreamBuf int `json:"streambuf"`
|
||||
KeepAlive int `json:"keepalive"`
|
||||
}
|
||||
@@ -87,6 +89,9 @@ func (config *Config) FillDefaults() {
|
||||
if config.SmuxBuf == 0 {
|
||||
config.SmuxBuf = 4194304
|
||||
}
|
||||
if config.FrameSize == 0 {
|
||||
config.FrameSize = 8192
|
||||
}
|
||||
if config.StreamBuf == 0 {
|
||||
config.StreamBuf = 2097152
|
||||
}
|
||||
@@ -144,6 +149,8 @@ func (config *Config) NewBlock() (block kcp.BlockCrypt) {
|
||||
block, _ = kcp.NewXTEABlockCrypt(pass[:16])
|
||||
case "salsa20":
|
||||
block, _ = kcp.NewSalsa20BlockCrypt(pass)
|
||||
case "aes-128-gcm":
|
||||
block, _ = kcp.NewAESGCMCrypt(pass[:16])
|
||||
default:
|
||||
config.Crypt = "aes"
|
||||
block, _ = kcp.NewAESBlockCrypt(pass)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package kcptun copy and modify from:
|
||||
// https://github.com/xtaci/kcptun/tree/52492c72592627d0005cbedbc4ba37fc36a95c3f
|
||||
// https://github.com/xtaci/kcptun/tree/f54f35175bed6ddda4e47aa35c9d7ae8b7e7eb85
|
||||
// adopt for mihomo
|
||||
// without SM4,QPP,tcpraw support
|
||||
package kcptun
|
||||
|
||||
@@ -43,6 +43,7 @@ func (s *Server) Serve(pc net.PacketConn, handler func(net.Conn)) error {
|
||||
conn.SetMtu(s.config.MTU)
|
||||
conn.SetWindowSize(s.config.SndWnd, s.config.RcvWnd)
|
||||
conn.SetACKNoDelay(s.config.AckNodelay)
|
||||
conn.SetRateLimit(uint32(s.config.RateLimit))
|
||||
|
||||
var netConn net.Conn = conn
|
||||
if !s.config.NoComp {
|
||||
@@ -55,6 +56,7 @@ func (s *Server) Serve(pc net.PacketConn, handler func(net.Conn)) error {
|
||||
smuxConfig.Version = s.config.SmuxVer
|
||||
smuxConfig.MaxReceiveBuffer = s.config.SmuxBuf
|
||||
smuxConfig.MaxStreamBuffer = s.config.StreamBuf
|
||||
smuxConfig.MaxFrameSize = s.config.FrameSize
|
||||
smuxConfig.KeepAliveInterval = time.Duration(s.config.KeepAlive) * time.Second
|
||||
if smuxConfig.KeepAliveInterval >= smuxConfig.KeepAliveTimeout {
|
||||
smuxConfig.KeepAliveTimeout = 3 * smuxConfig.KeepAliveInterval
|
||||
|
||||
178
transport/sudoku/config.go
Normal file
178
transport/sudoku/config.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||
)
|
||||
|
||||
// ProtocolConfig defines the configuration for the Sudoku protocol stack.
|
||||
// It is intentionally kept close to the upstream Sudoku project to ensure wire compatibility.
|
||||
type ProtocolConfig struct {
|
||||
// Client-only: "host:port".
|
||||
ServerAddress string
|
||||
|
||||
// Pre-shared key (or ED25519 key material) used to derive crypto and tables.
|
||||
Key string
|
||||
|
||||
// "aes-128-gcm", "chacha20-poly1305", or "none".
|
||||
AEADMethod string
|
||||
|
||||
// Table is the single obfuscation table to use when table rotation is disabled.
|
||||
Table *sudoku.Table
|
||||
|
||||
// Tables is an optional candidate set for table rotation.
|
||||
// If provided (len>0), the client will pick one table per connection and the server will
|
||||
// probe the handshake to detect which one was used, keeping the handshake format unchanged.
|
||||
// When Tables is set, Table may be nil.
|
||||
Tables []*sudoku.Table
|
||||
|
||||
// Padding insertion ratio (0-100). Must satisfy PaddingMax >= PaddingMin.
|
||||
PaddingMin int
|
||||
PaddingMax int
|
||||
|
||||
// EnablePureDownlink toggles the bandwidth-optimized downlink mode.
|
||||
EnablePureDownlink bool
|
||||
|
||||
// Client-only: final target "host:port".
|
||||
TargetAddress string
|
||||
|
||||
// Server-side handshake timeout (seconds).
|
||||
HandshakeTimeoutSeconds int
|
||||
|
||||
// DisableHTTPMask disables all HTTP camouflage layers.
|
||||
DisableHTTPMask bool
|
||||
|
||||
// HTTPMaskMode controls how the HTTP layer behaves:
|
||||
// - "legacy": write a fake HTTP/1.1 header then switch to raw stream (default, not CDN-compatible)
|
||||
// - "stream": real HTTP tunnel (stream-one or split), CDN-compatible
|
||||
// - "poll": plain HTTP tunnel (authorize/push/pull), strong restricted-network pass-through
|
||||
// - "auto": try stream then fall back to poll
|
||||
HTTPMaskMode string
|
||||
|
||||
// HTTPMaskTLSEnabled enables HTTPS for HTTP tunnel modes (client-side).
|
||||
// If false, the tunnel uses HTTP (no port-based inference).
|
||||
HTTPMaskTLSEnabled bool
|
||||
|
||||
// HTTPMaskHost optionally overrides the HTTP Host header / SNI host for HTTP tunnel modes (client-side).
|
||||
HTTPMaskHost string
|
||||
|
||||
// HTTPMaskPathRoot optionally prefixes all HTTP mask paths with a first-level segment.
|
||||
// Example: "aabbcc" => "/aabbcc/session", "/aabbcc/api/v1/upload", ...
|
||||
HTTPMaskPathRoot string
|
||||
|
||||
// HTTPMaskMultiplex controls multiplex behavior when HTTPMask tunnel modes are enabled:
|
||||
// - "off": disable reuse; each Dial establishes its own HTTPMask tunnel
|
||||
// - "auto": reuse underlying HTTP connections across multiple tunnel dials (HTTP/1.1 keep-alive / HTTP/2)
|
||||
// - "on": enable "single tunnel, multi-target" mux (Sudoku-level multiplex; Dial behaves like "auto" otherwise)
|
||||
HTTPMaskMultiplex string
|
||||
}
|
||||
|
||||
func (c *ProtocolConfig) Validate() error {
|
||||
if c.Table == nil && len(c.Tables) == 0 {
|
||||
return fmt.Errorf("table cannot be nil (or provide tables)")
|
||||
}
|
||||
for i, t := range c.Tables {
|
||||
if t == nil {
|
||||
return fmt.Errorf("tables[%d] cannot be nil", i)
|
||||
}
|
||||
}
|
||||
|
||||
if c.Key == "" {
|
||||
return fmt.Errorf("key cannot be empty")
|
||||
}
|
||||
|
||||
switch c.AEADMethod {
|
||||
case "aes-128-gcm", "chacha20-poly1305", "none":
|
||||
default:
|
||||
return fmt.Errorf("invalid aead-method: %s, must be one of: aes-128-gcm, chacha20-poly1305, none", c.AEADMethod)
|
||||
}
|
||||
|
||||
if c.PaddingMin < 0 || c.PaddingMin > 100 {
|
||||
return fmt.Errorf("padding-min must be between 0 and 100, got %d", c.PaddingMin)
|
||||
}
|
||||
if c.PaddingMax < 0 || c.PaddingMax > 100 {
|
||||
return fmt.Errorf("padding-max must be between 0 and 100, got %d", c.PaddingMax)
|
||||
}
|
||||
if c.PaddingMax < c.PaddingMin {
|
||||
return fmt.Errorf("padding-max (%d) must be >= padding-min (%d)", c.PaddingMax, c.PaddingMin)
|
||||
}
|
||||
|
||||
if !c.EnablePureDownlink && c.AEADMethod == "none" {
|
||||
return fmt.Errorf("bandwidth optimized downlink requires AEAD")
|
||||
}
|
||||
|
||||
if c.HandshakeTimeoutSeconds < 0 {
|
||||
return fmt.Errorf("handshake-timeout must be >= 0, got %d", c.HandshakeTimeoutSeconds)
|
||||
}
|
||||
|
||||
switch strings.ToLower(strings.TrimSpace(c.HTTPMaskMode)) {
|
||||
case "", "legacy", "stream", "poll", "auto":
|
||||
default:
|
||||
return fmt.Errorf("invalid http-mask-mode: %s, must be one of: legacy, stream, poll, auto", c.HTTPMaskMode)
|
||||
}
|
||||
|
||||
if v := strings.TrimSpace(c.HTTPMaskPathRoot); v != "" {
|
||||
if strings.Contains(v, "/") {
|
||||
return fmt.Errorf("invalid http-mask-path-root: must be a single path segment")
|
||||
}
|
||||
for i := 0; i < len(v); i++ {
|
||||
ch := v[i]
|
||||
switch {
|
||||
case ch >= 'a' && ch <= 'z':
|
||||
case ch >= 'A' && ch <= 'Z':
|
||||
case ch >= '0' && ch <= '9':
|
||||
case ch == '_' || ch == '-':
|
||||
default:
|
||||
return fmt.Errorf("invalid http-mask-path-root: contains invalid character %q", ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch strings.ToLower(strings.TrimSpace(c.HTTPMaskMultiplex)) {
|
||||
case "", "off", "auto", "on":
|
||||
default:
|
||||
return fmt.Errorf("invalid http-mask-multiplex: %s, must be one of: off, auto, on", c.HTTPMaskMultiplex)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ProtocolConfig) ValidateClient() error {
|
||||
if err := c.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.ServerAddress == "" {
|
||||
return fmt.Errorf("server address cannot be empty")
|
||||
}
|
||||
if c.TargetAddress == "" {
|
||||
return fmt.Errorf("target address cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DefaultConfig() *ProtocolConfig {
|
||||
return &ProtocolConfig{
|
||||
AEADMethod: "chacha20-poly1305",
|
||||
PaddingMin: 10,
|
||||
PaddingMax: 30,
|
||||
EnablePureDownlink: true,
|
||||
HandshakeTimeoutSeconds: 5,
|
||||
HTTPMaskMode: "legacy",
|
||||
HTTPMaskMultiplex: "off",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ProtocolConfig) tableCandidates() []*sudoku.Table {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
if len(c.Tables) > 0 {
|
||||
return c.Tables
|
||||
}
|
||||
if c.Table != nil {
|
||||
return []*sudoku.Table{c.Table}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
130
transport/sudoku/crypto/aead.go
Normal file
130
transport/sudoku/crypto/aead.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
type AEADConn struct {
|
||||
net.Conn
|
||||
aead cipher.AEAD
|
||||
readBuf bytes.Buffer
|
||||
nonceSize int
|
||||
}
|
||||
|
||||
func NewAEADConn(c net.Conn, key string, method string) (*AEADConn, error) {
|
||||
if method == "none" {
|
||||
return &AEADConn{Conn: c, aead: nil}, nil
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(key))
|
||||
keyBytes := h.Sum(nil)
|
||||
|
||||
var aead cipher.AEAD
|
||||
var err error
|
||||
|
||||
switch method {
|
||||
case "aes-128-gcm":
|
||||
block, _ := aes.NewCipher(keyBytes[:16])
|
||||
aead, err = cipher.NewGCM(block)
|
||||
case "chacha20-poly1305":
|
||||
aead, err = chacha20poly1305.New(keyBytes)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported cipher: %s", method)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AEADConn{
|
||||
Conn: c,
|
||||
aead: aead,
|
||||
nonceSize: aead.NonceSize(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cc *AEADConn) Write(p []byte) (int, error) {
|
||||
if cc.aead == nil {
|
||||
return cc.Conn.Write(p)
|
||||
}
|
||||
|
||||
maxPayload := 65535 - cc.nonceSize - cc.aead.Overhead()
|
||||
totalWritten := 0
|
||||
var frameBuf bytes.Buffer
|
||||
header := make([]byte, 2)
|
||||
nonce := make([]byte, cc.nonceSize)
|
||||
|
||||
for len(p) > 0 {
|
||||
chunkSize := len(p)
|
||||
if chunkSize > maxPayload {
|
||||
chunkSize = maxPayload
|
||||
}
|
||||
chunk := p[:chunkSize]
|
||||
p = p[chunkSize:]
|
||||
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return totalWritten, err
|
||||
}
|
||||
|
||||
ciphertext := cc.aead.Seal(nil, nonce, chunk, nil)
|
||||
frameLen := len(nonce) + len(ciphertext)
|
||||
binary.BigEndian.PutUint16(header, uint16(frameLen))
|
||||
|
||||
frameBuf.Reset()
|
||||
frameBuf.Write(header)
|
||||
frameBuf.Write(nonce)
|
||||
frameBuf.Write(ciphertext)
|
||||
|
||||
if _, err := cc.Conn.Write(frameBuf.Bytes()); err != nil {
|
||||
return totalWritten, err
|
||||
}
|
||||
totalWritten += chunkSize
|
||||
}
|
||||
return totalWritten, nil
|
||||
}
|
||||
|
||||
func (cc *AEADConn) Read(p []byte) (int, error) {
|
||||
if cc.aead == nil {
|
||||
return cc.Conn.Read(p)
|
||||
}
|
||||
|
||||
if cc.readBuf.Len() > 0 {
|
||||
return cc.readBuf.Read(p)
|
||||
}
|
||||
|
||||
header := make([]byte, 2)
|
||||
if _, err := io.ReadFull(cc.Conn, header); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
frameLen := int(binary.BigEndian.Uint16(header))
|
||||
|
||||
body := make([]byte, frameLen)
|
||||
if _, err := io.ReadFull(cc.Conn, body); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(body) < cc.nonceSize {
|
||||
return 0, errors.New("frame too short")
|
||||
}
|
||||
nonce := body[:cc.nonceSize]
|
||||
ciphertext := body[cc.nonceSize:]
|
||||
|
||||
plaintext, err := cc.aead.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return 0, errors.New("decryption failed")
|
||||
}
|
||||
|
||||
cc.readBuf.Write(plaintext)
|
||||
return cc.readBuf.Read(p)
|
||||
}
|
||||
116
transport/sudoku/crypto/ed25519.go
Normal file
116
transport/sudoku/crypto/ed25519.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"filippo.io/edwards25519"
|
||||
)
|
||||
|
||||
// KeyPair holds the scalar private key and point public key
|
||||
type KeyPair struct {
|
||||
Private *edwards25519.Scalar
|
||||
Public *edwards25519.Point
|
||||
}
|
||||
|
||||
// GenerateMasterKey generates a random master private key (scalar) and its public key (point)
|
||||
func GenerateMasterKey() (*KeyPair, error) {
|
||||
// 1. Generate random scalar x (32 bytes)
|
||||
var seed [64]byte
|
||||
if _, err := rand.Read(seed[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
x, err := edwards25519.NewScalar().SetUniformBytes(seed[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. Calculate Public Key P = x * G
|
||||
P := new(edwards25519.Point).ScalarBaseMult(x)
|
||||
|
||||
return &KeyPair{Private: x, Public: P}, nil
|
||||
}
|
||||
|
||||
// SplitPrivateKey takes a master private key x and returns a new random split key (r, k)
|
||||
// such that x = r + k (mod L).
|
||||
// Returns hex encoded string of r || k (64 bytes)
|
||||
func SplitPrivateKey(x *edwards25519.Scalar) (string, error) {
|
||||
// 1. Generate random r (32 bytes)
|
||||
var seed [64]byte
|
||||
if _, err := rand.Read(seed[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
r, err := edwards25519.NewScalar().SetUniformBytes(seed[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 2. Calculate k = x - r (mod L)
|
||||
k := new(edwards25519.Scalar).Subtract(x, r)
|
||||
|
||||
// 3. Encode r and k
|
||||
rBytes := r.Bytes()
|
||||
kBytes := k.Bytes()
|
||||
|
||||
full := make([]byte, 64)
|
||||
copy(full[:32], rBytes)
|
||||
copy(full[32:], kBytes)
|
||||
|
||||
return hex.EncodeToString(full), nil
|
||||
}
|
||||
|
||||
// RecoverPublicKey takes a split private key (r, k) or a master private key (x)
|
||||
// and returns the public key P.
|
||||
// Input can be:
|
||||
// - 32 bytes hex (Master Scalar x)
|
||||
// - 64 bytes hex (Split Key r || k)
|
||||
func RecoverPublicKey(keyHex string) (*edwards25519.Point, error) {
|
||||
keyBytes, err := hex.DecodeString(keyHex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid hex: %w", err)
|
||||
}
|
||||
|
||||
if len(keyBytes) == 32 {
|
||||
// Master Key x
|
||||
x, err := edwards25519.NewScalar().SetCanonicalBytes(keyBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid scalar: %w", err)
|
||||
}
|
||||
return new(edwards25519.Point).ScalarBaseMult(x), nil
|
||||
|
||||
} else if len(keyBytes) == 64 {
|
||||
// Split Key r || k
|
||||
rBytes := keyBytes[:32]
|
||||
kBytes := keyBytes[32:]
|
||||
|
||||
r, err := edwards25519.NewScalar().SetCanonicalBytes(rBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid scalar r: %w", err)
|
||||
}
|
||||
k, err := edwards25519.NewScalar().SetCanonicalBytes(kBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid scalar k: %w", err)
|
||||
}
|
||||
|
||||
// sum = r + k
|
||||
sum := new(edwards25519.Scalar).Add(r, k)
|
||||
|
||||
// P = sum * G
|
||||
return new(edwards25519.Point).ScalarBaseMult(sum), nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid key length: must be 32 bytes (Master) or 64 bytes (Split)")
|
||||
}
|
||||
|
||||
// EncodePoint returns the hex string of the compressed point
|
||||
func EncodePoint(p *edwards25519.Point) string {
|
||||
return hex.EncodeToString(p.Bytes())
|
||||
}
|
||||
|
||||
// EncodeScalar returns the hex string of the scalar
|
||||
func EncodeScalar(s *edwards25519.Scalar) string {
|
||||
return hex.EncodeToString(s.Bytes())
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sudokuobfs "github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
||||
sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||
)
|
||||
|
||||
type discardConn struct{}
|
||||
|
||||
@@ -2,38 +2,38 @@ package sudoku
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/saba-futai/sudoku/apis"
|
||||
"github.com/saba-futai/sudoku/pkg/crypto"
|
||||
"github.com/saba-futai/sudoku/pkg/obfs/httpmask"
|
||||
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
||||
"github.com/metacubex/mihomo/transport/sudoku/crypto"
|
||||
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
|
||||
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||
|
||||
"github.com/metacubex/mihomo/log"
|
||||
)
|
||||
|
||||
type ProtocolConfig = apis.ProtocolConfig
|
||||
|
||||
func DefaultConfig() *ProtocolConfig { return apis.DefaultConfig() }
|
||||
|
||||
type SessionType int
|
||||
|
||||
const (
|
||||
SessionTypeTCP SessionType = iota
|
||||
SessionTypeUoT
|
||||
SessionTypeMultiplex
|
||||
)
|
||||
|
||||
type ServerSession struct {
|
||||
Conn net.Conn
|
||||
Type SessionType
|
||||
Target string
|
||||
|
||||
// UserHash is a stable per-key identifier derived from the handshake payload.
|
||||
// It is primarily useful for debugging / user attribution when table rotation is enabled.
|
||||
UserHash string
|
||||
}
|
||||
|
||||
type bufferedConn struct {
|
||||
@@ -105,14 +105,14 @@ const (
|
||||
downlinkModePacked byte = 0x02
|
||||
)
|
||||
|
||||
func downlinkMode(cfg *apis.ProtocolConfig) byte {
|
||||
func downlinkMode(cfg *ProtocolConfig) byte {
|
||||
if cfg.EnablePureDownlink {
|
||||
return downlinkModePure
|
||||
}
|
||||
return downlinkModePacked
|
||||
}
|
||||
|
||||
func buildClientObfsConn(raw net.Conn, cfg *apis.ProtocolConfig, table *sudoku.Table) net.Conn {
|
||||
func buildClientObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table) net.Conn {
|
||||
baseReader := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false)
|
||||
baseWriter := newSudokuObfsWriter(raw, table, cfg.PaddingMin, cfg.PaddingMax)
|
||||
if cfg.EnablePureDownlink {
|
||||
@@ -130,7 +130,7 @@ func buildClientObfsConn(raw net.Conn, cfg *apis.ProtocolConfig, table *sudoku.T
|
||||
}
|
||||
}
|
||||
|
||||
func buildServerObfsConn(raw net.Conn, cfg *apis.ProtocolConfig, table *sudoku.Table, record bool) (*sudoku.Conn, net.Conn) {
|
||||
func buildServerObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table, record bool) (*sudoku.Conn, net.Conn) {
|
||||
uplink := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, record)
|
||||
if cfg.EnablePureDownlink {
|
||||
downlink := &directionalConn{
|
||||
@@ -152,7 +152,17 @@ func buildServerObfsConn(raw net.Conn, cfg *apis.ProtocolConfig, table *sudoku.T
|
||||
func buildHandshakePayload(key string) [16]byte {
|
||||
var payload [16]byte
|
||||
binary.BigEndian.PutUint64(payload[:8], uint64(time.Now().Unix()))
|
||||
hash := sha256.Sum256([]byte(key))
|
||||
|
||||
// Align with upstream: only decode hex bytes when this key is an ED25519 key material.
|
||||
// For plain UUID/strings (even if they look like hex), hash the string bytes as-is.
|
||||
src := []byte(key)
|
||||
if _, err := crypto.RecoverPublicKey(key); err == nil {
|
||||
if keyBytes, decErr := hex.DecodeString(key); decErr == nil && len(keyBytes) > 0 {
|
||||
src = keyBytes
|
||||
}
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(src)
|
||||
copy(payload[8:], hash[:8])
|
||||
return payload
|
||||
}
|
||||
@@ -189,12 +199,12 @@ type ClientHandshakeOptions struct {
|
||||
}
|
||||
|
||||
// ClientHandshake performs the client-side Sudoku handshake (without sending target address).
|
||||
func ClientHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (net.Conn, error) {
|
||||
func ClientHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, error) {
|
||||
return ClientHandshakeWithOptions(rawConn, cfg, ClientHandshakeOptions{})
|
||||
}
|
||||
|
||||
// ClientHandshakeWithOptions performs the client-side Sudoku handshake (without sending target address).
|
||||
func ClientHandshakeWithOptions(rawConn net.Conn, cfg *apis.ProtocolConfig, opt ClientHandshakeOptions) (net.Conn, error) {
|
||||
func ClientHandshakeWithOptions(rawConn net.Conn, cfg *ProtocolConfig, opt ClientHandshakeOptions) (net.Conn, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("config is required")
|
||||
}
|
||||
@@ -203,12 +213,12 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *apis.ProtocolConfig, opt
|
||||
}
|
||||
|
||||
if !cfg.DisableHTTPMask {
|
||||
if err := WriteHTTPMaskHeader(rawConn, cfg.ServerAddress, opt.HTTPMaskStrategy); err != nil {
|
||||
if err := WriteHTTPMaskHeader(rawConn, cfg.ServerAddress, cfg.HTTPMaskPathRoot, opt.HTTPMaskStrategy); err != nil {
|
||||
return nil, fmt.Errorf("write http mask failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
table, tableID, err := pickClientTable(cfg)
|
||||
table, err := pickClientTable(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -220,9 +230,6 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *apis.ProtocolConfig, opt
|
||||
}
|
||||
|
||||
handshake := buildHandshakePayload(cfg.Key)
|
||||
if len(tableCandidates(cfg)) > 1 {
|
||||
handshake[15] = tableID
|
||||
}
|
||||
if _, err := cConn.Write(handshake[:]); err != nil {
|
||||
cConn.Close()
|
||||
return nil, fmt.Errorf("send handshake failed: %w", err)
|
||||
@@ -236,7 +243,7 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *apis.ProtocolConfig, opt
|
||||
}
|
||||
|
||||
// ServerHandshake performs Sudoku server-side handshake and detects UoT preface.
|
||||
func ServerHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (*ServerSession, error) {
|
||||
func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (*ServerSession, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("config is required")
|
||||
}
|
||||
@@ -260,7 +267,7 @@ func ServerHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (*ServerSession
|
||||
}
|
||||
}
|
||||
|
||||
selectedTable, preRead, err := selectTableByProbe(bufReader, cfg, tableCandidates(cfg))
|
||||
selectedTable, preRead, err := selectTableByProbe(bufReader, cfg, cfg.tableCandidates())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -285,6 +292,7 @@ func ServerHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (*ServerSession
|
||||
return nil, fmt.Errorf("timestamp skew detected")
|
||||
}
|
||||
|
||||
userHash := userHashFromHandshake(handshakeBuf[:])
|
||||
sConn.StopRecording()
|
||||
|
||||
modeBuf := []byte{0}
|
||||
@@ -303,6 +311,11 @@ func ServerHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (*ServerSession
|
||||
return nil, fmt.Errorf("read first byte failed: %w", err)
|
||||
}
|
||||
|
||||
if firstByte[0] == MultiplexMagicByte {
|
||||
rawConn.SetReadDeadline(time.Time{})
|
||||
return &ServerSession{Conn: cConn, Type: SessionTypeMultiplex, UserHash: userHash}, nil
|
||||
}
|
||||
|
||||
if firstByte[0] == UoTMagicByte {
|
||||
version := make([]byte, 1)
|
||||
if _, err := io.ReadFull(cConn, version); err != nil {
|
||||
@@ -314,7 +327,7 @@ func ServerHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (*ServerSession
|
||||
return nil, fmt.Errorf("unsupported uot version: %d", version[0])
|
||||
}
|
||||
rawConn.SetReadDeadline(time.Time{})
|
||||
return &ServerSession{Conn: cConn, Type: SessionTypeUoT}, nil
|
||||
return &ServerSession{Conn: cConn, Type: SessionTypeUoT, UserHash: userHash}, nil
|
||||
}
|
||||
|
||||
prefixed := &preBufferedConn{Conn: cConn, buf: firstByte}
|
||||
@@ -327,9 +340,10 @@ func ServerHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (*ServerSession
|
||||
rawConn.SetReadDeadline(time.Time{})
|
||||
log.Debugln("[Sudoku] incoming TCP session target: %s", target)
|
||||
return &ServerSession{
|
||||
Conn: prefixed,
|
||||
Type: SessionTypeTCP,
|
||||
Target: target,
|
||||
Conn: prefixed,
|
||||
Type: SessionTypeTCP,
|
||||
Target: target,
|
||||
UserHash: userHash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -361,11 +375,9 @@ func normalizeHTTPMaskStrategy(strategy string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// randomByte returns a cryptographically random byte (with a math/rand fallback).
|
||||
func randomByte() byte {
|
||||
var b [1]byte
|
||||
if _, err := rand.Read(b[:]); err == nil {
|
||||
return b[0]
|
||||
func userHashFromHandshake(handshakeBuf []byte) string {
|
||||
if len(handshakeBuf) < 16 {
|
||||
return ""
|
||||
}
|
||||
return byte(time.Now().UnixNano())
|
||||
return hex.EncodeToString(handshakeBuf[8:16])
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/saba-futai/sudoku/apis"
|
||||
sudokuobfs "github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
||||
sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||
)
|
||||
|
||||
func TestPackedConnRoundTrip_WithPadding(t *testing.T) {
|
||||
@@ -67,8 +66,8 @@ func TestPackedConnRoundTrip_WithPadding(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func newPackedConfig(table *sudokuobfs.Table) *apis.ProtocolConfig {
|
||||
cfg := apis.DefaultConfig()
|
||||
func newPackedConfig(table *sudokuobfs.Table) *ProtocolConfig {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Key = "sudoku-test-key"
|
||||
cfg.Table = table
|
||||
cfg.PaddingMin = 10
|
||||
@@ -118,7 +117,7 @@ func TestPackedDownlinkSoak(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func runPackedTCPSession(id int, cfg *apis.ProtocolConfig, errCh chan<- error) {
|
||||
func runPackedTCPSession(id int, cfg *ProtocolConfig, errCh chan<- error) {
|
||||
serverConn, clientConn := net.Pipe()
|
||||
target := fmt.Sprintf("1.1.1.%d:80", (id%200)+1)
|
||||
payload := []byte{0x42, byte(id)}
|
||||
@@ -176,7 +175,7 @@ func runPackedTCPSession(id int, cfg *apis.ProtocolConfig, errCh chan<- error) {
|
||||
}
|
||||
}
|
||||
|
||||
func runPackedUoTSession(id int, cfg *apis.ProtocolConfig, errCh chan<- error) {
|
||||
func runPackedUoTSession(id int, cfg *ProtocolConfig, errCh chan<- error) {
|
||||
serverConn, clientConn := net.Pipe()
|
||||
target := "8.8.8.8:53"
|
||||
payload := []byte{0xaa, byte(id)}
|
||||
|
||||
@@ -7,10 +7,11 @@ import (
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/saba-futai/sudoku/pkg/obfs/httpmask"
|
||||
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -92,24 +93,24 @@ func appendCommonHeaders(buf []byte, host string, r *rand.Rand) []byte {
|
||||
|
||||
// WriteHTTPMaskHeader writes an HTTP/1.x request header as a mask, according to strategy.
|
||||
// Supported strategies: ""/"random", "post", "websocket".
|
||||
func WriteHTTPMaskHeader(w io.Writer, host string, strategy string) error {
|
||||
func WriteHTTPMaskHeader(w io.Writer, host string, pathRoot string, strategy string) error {
|
||||
switch normalizeHTTPMaskStrategy(strategy) {
|
||||
case "random":
|
||||
return httpmask.WriteRandomRequestHeader(w, host)
|
||||
return httpmask.WriteRandomRequestHeaderWithPathRoot(w, host, pathRoot)
|
||||
case "post":
|
||||
return writeHTTPMaskPOST(w, host)
|
||||
return writeHTTPMaskPOST(w, host, pathRoot)
|
||||
case "websocket":
|
||||
return writeHTTPMaskWebSocket(w, host)
|
||||
return writeHTTPMaskWebSocket(w, host, pathRoot)
|
||||
default:
|
||||
return fmt.Errorf("unsupported http-mask-strategy: %s", strategy)
|
||||
}
|
||||
}
|
||||
|
||||
func writeHTTPMaskPOST(w io.Writer, host string) error {
|
||||
func writeHTTPMaskPOST(w io.Writer, host string, pathRoot string) error {
|
||||
r := httpMaskRngPool.Get().(*rand.Rand)
|
||||
defer httpMaskRngPool.Put(r)
|
||||
|
||||
path := httpMaskPaths[r.Intn(len(httpMaskPaths))]
|
||||
path := joinPathRoot(pathRoot, httpMaskPaths[r.Intn(len(httpMaskPaths))])
|
||||
ctype := httpMaskContentTypes[r.Intn(len(httpMaskContentTypes))]
|
||||
|
||||
bufPtr := httpMaskBufPool.Get().(*[]byte)
|
||||
@@ -140,11 +141,11 @@ func writeHTTPMaskPOST(w io.Writer, host string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func writeHTTPMaskWebSocket(w io.Writer, host string) error {
|
||||
func writeHTTPMaskWebSocket(w io.Writer, host string, pathRoot string) error {
|
||||
r := httpMaskRngPool.Get().(*rand.Rand)
|
||||
defer httpMaskRngPool.Put(r)
|
||||
|
||||
path := httpMaskPaths[r.Intn(len(httpMaskPaths))]
|
||||
path := joinPathRoot(pathRoot, httpMaskPaths[r.Intn(len(httpMaskPaths))])
|
||||
|
||||
bufPtr := httpMaskBufPool.Get().(*[]byte)
|
||||
buf := *bufPtr
|
||||
@@ -177,3 +178,37 @@ func writeHTTPMaskWebSocket(w io.Writer, host string) error {
|
||||
_, err := w.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func normalizePathRoot(root string) string {
|
||||
root = strings.TrimSpace(root)
|
||||
root = strings.Trim(root, "/")
|
||||
if root == "" {
|
||||
return ""
|
||||
}
|
||||
for i := 0; i < len(root); i++ {
|
||||
c := root[i]
|
||||
switch {
|
||||
case c >= 'a' && c <= 'z':
|
||||
case c >= 'A' && c <= 'Z':
|
||||
case c >= '0' && c <= '9':
|
||||
case c == '_' || c == '-':
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return "/" + root
|
||||
}
|
||||
|
||||
func joinPathRoot(root, path string) string {
|
||||
root = normalizePathRoot(root)
|
||||
if root == "" {
|
||||
return path
|
||||
}
|
||||
if path == "" {
|
||||
return root
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
return root + path
|
||||
}
|
||||
|
||||
157
transport/sudoku/httpmask_tunnel.go
Normal file
157
transport/sudoku/httpmask_tunnel.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
|
||||
)
|
||||
|
||||
type HTTPMaskTunnelServer struct {
|
||||
cfg *ProtocolConfig
|
||||
ts *httpmask.TunnelServer
|
||||
}
|
||||
|
||||
func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer {
|
||||
if cfg == nil {
|
||||
return &HTTPMaskTunnelServer{}
|
||||
}
|
||||
|
||||
var ts *httpmask.TunnelServer
|
||||
if !cfg.DisableHTTPMask {
|
||||
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
|
||||
case "stream", "poll", "auto":
|
||||
ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{
|
||||
Mode: cfg.HTTPMaskMode,
|
||||
PathRoot: cfg.HTTPMaskPathRoot,
|
||||
AuthKey: ClientAEADSeed(cfg.Key),
|
||||
})
|
||||
}
|
||||
}
|
||||
return &HTTPMaskTunnelServer{cfg: cfg, ts: ts}
|
||||
}
|
||||
|
||||
// WrapConn inspects an accepted TCP connection and upgrades it to an HTTP tunnel stream when needed.
|
||||
//
|
||||
// Returns:
|
||||
// - done=true: this TCP connection has been fully handled (e.g., stream/poll control request), caller should return
|
||||
// - done=false: handshakeConn+cfg are ready for ServerHandshake
|
||||
func (s *HTTPMaskTunnelServer) WrapConn(rawConn net.Conn) (handshakeConn net.Conn, cfg *ProtocolConfig, done bool, err error) {
|
||||
if rawConn == nil {
|
||||
return nil, nil, true, fmt.Errorf("nil conn")
|
||||
}
|
||||
if s == nil {
|
||||
return rawConn, nil, false, nil
|
||||
}
|
||||
if s.ts == nil {
|
||||
return rawConn, s.cfg, false, nil
|
||||
}
|
||||
|
||||
res, c, err := s.ts.HandleConn(rawConn)
|
||||
if err != nil {
|
||||
return nil, nil, true, err
|
||||
}
|
||||
|
||||
switch res {
|
||||
case httpmask.HandleDone:
|
||||
return nil, nil, true, nil
|
||||
case httpmask.HandlePassThrough:
|
||||
return c, s.cfg, false, nil
|
||||
case httpmask.HandleStartTunnel:
|
||||
inner := *s.cfg
|
||||
inner.DisableHTTPMask = true
|
||||
return c, &inner, false, nil
|
||||
default:
|
||||
return nil, nil, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
type TunnelDialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
// DialHTTPMaskTunnel dials a CDN-capable HTTP tunnel (stream/poll/auto) and returns a stream carrying raw Sudoku bytes.
|
||||
func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *ProtocolConfig, dial TunnelDialer, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("config is required")
|
||||
}
|
||||
if cfg.DisableHTTPMask {
|
||||
return nil, fmt.Errorf("http mask is disabled")
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
|
||||
case "stream", "poll", "auto":
|
||||
default:
|
||||
return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode)
|
||||
}
|
||||
return httpmask.DialTunnel(ctx, serverAddress, httpmask.TunnelDialOptions{
|
||||
Mode: cfg.HTTPMaskMode,
|
||||
TLSEnabled: cfg.HTTPMaskTLSEnabled,
|
||||
HostOverride: cfg.HTTPMaskHost,
|
||||
PathRoot: cfg.HTTPMaskPathRoot,
|
||||
AuthKey: ClientAEADSeed(cfg.Key),
|
||||
Upgrade: upgrade,
|
||||
Multiplex: cfg.HTTPMaskMultiplex,
|
||||
DialContext: dial,
|
||||
})
|
||||
}
|
||||
|
||||
type HTTPMaskTunnelClient struct {
|
||||
mode string
|
||||
pathRoot string
|
||||
authKey string
|
||||
client *httpmask.TunnelClient
|
||||
}
|
||||
|
||||
func NewHTTPMaskTunnelClient(serverAddress string, cfg *ProtocolConfig, dial TunnelDialer) (*HTTPMaskTunnelClient, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("config is required")
|
||||
}
|
||||
if cfg.DisableHTTPMask {
|
||||
return nil, fmt.Errorf("http mask is disabled")
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
|
||||
case "stream", "poll", "auto":
|
||||
default:
|
||||
return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode)
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMultiplex)) {
|
||||
case "auto", "on":
|
||||
default:
|
||||
return nil, fmt.Errorf("http-mask-multiplex=%q does not enable reuse", cfg.HTTPMaskMultiplex)
|
||||
}
|
||||
|
||||
c, err := httpmask.NewTunnelClient(serverAddress, httpmask.TunnelClientOptions{
|
||||
TLSEnabled: cfg.HTTPMaskTLSEnabled,
|
||||
HostOverride: cfg.HTTPMaskHost,
|
||||
DialContext: dial,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HTTPMaskTunnelClient{
|
||||
mode: cfg.HTTPMaskMode,
|
||||
pathRoot: cfg.HTTPMaskPathRoot,
|
||||
authKey: ClientAEADSeed(cfg.Key),
|
||||
client: c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *HTTPMaskTunnelClient) Dial(ctx context.Context, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) {
|
||||
if c == nil || c.client == nil {
|
||||
return nil, fmt.Errorf("nil httpmask tunnel client")
|
||||
}
|
||||
return c.client.DialTunnel(ctx, httpmask.TunnelDialOptions{
|
||||
Mode: c.mode,
|
||||
PathRoot: c.pathRoot,
|
||||
AuthKey: c.authKey,
|
||||
Upgrade: upgrade,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *HTTPMaskTunnelClient) CloseIdleConnections() {
|
||||
if c == nil || c.client == nil {
|
||||
return
|
||||
}
|
||||
c.client.CloseIdleConnections()
|
||||
}
|
||||
445
transport/sudoku/httpmask_tunnel_test.go
Normal file
445
transport/sudoku/httpmask_tunnel_test.go
Normal file
@@ -0,0 +1,445 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func startTunnelServer(t *testing.T, cfg *ProtocolConfig, handle func(*ServerSession) error) (addr string, stop func(), errCh <-chan error) {
|
||||
t.Helper()
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
|
||||
errC := make(chan error, 128)
|
||||
done := make(chan struct{})
|
||||
|
||||
tunnelSrv := NewHTTPMaskTunnelServer(cfg)
|
||||
var wg sync.WaitGroup
|
||||
var stopOnce sync.Once
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
close(done)
|
||||
return
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(conn net.Conn) {
|
||||
defer wg.Done()
|
||||
|
||||
handshakeConn, handshakeCfg, handled, err := tunnelSrv.WrapConn(conn)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||
return
|
||||
}
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
errC <- err
|
||||
return
|
||||
}
|
||||
if handled {
|
||||
return
|
||||
}
|
||||
if handshakeConn == nil || handshakeCfg == nil {
|
||||
_ = conn.Close()
|
||||
errC <- fmt.Errorf("wrap conn returned nil")
|
||||
return
|
||||
}
|
||||
|
||||
session, err := ServerHandshake(handshakeConn, handshakeCfg)
|
||||
if err != nil {
|
||||
_ = handshakeConn.Close()
|
||||
if handshakeConn != conn {
|
||||
_ = conn.Close()
|
||||
}
|
||||
errC <- err
|
||||
return
|
||||
}
|
||||
defer session.Conn.Close()
|
||||
|
||||
if handleErr := handle(session); handleErr != nil {
|
||||
errC <- handleErr
|
||||
}
|
||||
}(c)
|
||||
}
|
||||
}()
|
||||
|
||||
stop = func() {
|
||||
stopOnce.Do(func() {
|
||||
_ = ln.Close()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("server did not stop")
|
||||
}
|
||||
|
||||
ch := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
}()
|
||||
select {
|
||||
case <-ch:
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatalf("server goroutines did not exit")
|
||||
}
|
||||
close(errC)
|
||||
})
|
||||
}
|
||||
|
||||
return ln.Addr().String(), stop, errC
|
||||
}
|
||||
|
||||
func newTunnelTestTable(t *testing.T, key string) *ProtocolConfig {
|
||||
t.Helper()
|
||||
|
||||
tables, err := NewTablesWithCustomPatterns(ClientAEADSeed(key), "prefer_ascii", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build tables: %v", err)
|
||||
}
|
||||
if len(tables) != 1 {
|
||||
t.Fatalf("unexpected tables: %d", len(tables))
|
||||
}
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.Key = key
|
||||
cfg.AEADMethod = "chacha20-poly1305"
|
||||
cfg.Table = tables[0]
|
||||
cfg.PaddingMin = 0
|
||||
cfg.PaddingMax = 0
|
||||
cfg.HandshakeTimeoutSeconds = 5
|
||||
cfg.EnablePureDownlink = true
|
||||
cfg.DisableHTTPMask = false
|
||||
return cfg
|
||||
}
|
||||
|
||||
func TestHTTPMaskTunnel_Stream_TCPRoundTrip(t *testing.T) {
|
||||
key := "tunnel-stream-key"
|
||||
target := "1.1.1.1:80"
|
||||
|
||||
serverCfg := newTunnelTestTable(t, key)
|
||||
serverCfg.HTTPMaskMode = "stream"
|
||||
|
||||
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
|
||||
if s.Type != SessionTypeTCP {
|
||||
return fmt.Errorf("unexpected session type: %v", s.Type)
|
||||
}
|
||||
if s.Target != target {
|
||||
return fmt.Errorf("target mismatch: %s", s.Target)
|
||||
}
|
||||
_, _ = s.Conn.Write([]byte("ok"))
|
||||
return nil
|
||||
})
|
||||
defer stop()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
clientCfg := *serverCfg
|
||||
clientCfg.ServerAddress = addr
|
||||
clientCfg.HTTPMaskHost = "example.com"
|
||||
|
||||
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("dial tunnel: %v", err)
|
||||
}
|
||||
defer tunnelConn.Close()
|
||||
|
||||
handshakeCfg := clientCfg
|
||||
handshakeCfg.DisableHTTPMask = true
|
||||
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("client handshake: %v", err)
|
||||
}
|
||||
defer cConn.Close()
|
||||
|
||||
addrBuf, err := EncodeAddress(target)
|
||||
if err != nil {
|
||||
t.Fatalf("encode addr: %v", err)
|
||||
}
|
||||
if _, err := cConn.Write(addrBuf); err != nil {
|
||||
t.Fatalf("write addr: %v", err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 2)
|
||||
if _, err := io.ReadFull(cConn, buf); err != nil {
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
if string(buf) != "ok" {
|
||||
t.Fatalf("unexpected payload: %q", buf)
|
||||
}
|
||||
|
||||
stop()
|
||||
for err := range errCh {
|
||||
t.Fatalf("server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPMaskTunnel_Poll_UoTRoundTrip(t *testing.T) {
|
||||
key := "tunnel-poll-key"
|
||||
target := "8.8.8.8:53"
|
||||
payload := []byte{0xaa, 0xbb, 0xcc, 0xdd}
|
||||
|
||||
serverCfg := newTunnelTestTable(t, key)
|
||||
serverCfg.HTTPMaskMode = "poll"
|
||||
|
||||
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
|
||||
if s.Type != SessionTypeUoT {
|
||||
return fmt.Errorf("unexpected session type: %v", s.Type)
|
||||
}
|
||||
gotAddr, gotPayload, err := ReadDatagram(s.Conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("server read datagram: %w", err)
|
||||
}
|
||||
if gotAddr != target {
|
||||
return fmt.Errorf("uot target mismatch: %s", gotAddr)
|
||||
}
|
||||
if !bytes.Equal(gotPayload, payload) {
|
||||
return fmt.Errorf("uot payload mismatch: %x", gotPayload)
|
||||
}
|
||||
if err := WriteDatagram(s.Conn, gotAddr, gotPayload); err != nil {
|
||||
return fmt.Errorf("server write datagram: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
defer stop()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
clientCfg := *serverCfg
|
||||
clientCfg.ServerAddress = addr
|
||||
|
||||
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("dial tunnel: %v", err)
|
||||
}
|
||||
defer tunnelConn.Close()
|
||||
|
||||
handshakeCfg := clientCfg
|
||||
handshakeCfg.DisableHTTPMask = true
|
||||
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("client handshake: %v", err)
|
||||
}
|
||||
defer cConn.Close()
|
||||
|
||||
if err := WritePreface(cConn); err != nil {
|
||||
t.Fatalf("write preface: %v", err)
|
||||
}
|
||||
if err := WriteDatagram(cConn, target, payload); err != nil {
|
||||
t.Fatalf("write datagram: %v", err)
|
||||
}
|
||||
gotAddr, gotPayload, err := ReadDatagram(cConn)
|
||||
if err != nil {
|
||||
t.Fatalf("read datagram: %v", err)
|
||||
}
|
||||
if gotAddr != target {
|
||||
t.Fatalf("uot target mismatch: %s", gotAddr)
|
||||
}
|
||||
if !bytes.Equal(gotPayload, payload) {
|
||||
t.Fatalf("uot payload mismatch: %x", gotPayload)
|
||||
}
|
||||
|
||||
stop()
|
||||
for err := range errCh {
|
||||
t.Fatalf("server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPMaskTunnel_Auto_TCPRoundTrip(t *testing.T) {
|
||||
key := "tunnel-auto-key"
|
||||
target := "9.9.9.9:443"
|
||||
|
||||
serverCfg := newTunnelTestTable(t, key)
|
||||
serverCfg.HTTPMaskMode = "auto"
|
||||
|
||||
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
|
||||
if s.Type != SessionTypeTCP {
|
||||
return fmt.Errorf("unexpected session type: %v", s.Type)
|
||||
}
|
||||
if s.Target != target {
|
||||
return fmt.Errorf("target mismatch: %s", s.Target)
|
||||
}
|
||||
_, _ = s.Conn.Write([]byte("ok"))
|
||||
return nil
|
||||
})
|
||||
defer stop()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
clientCfg := *serverCfg
|
||||
clientCfg.ServerAddress = addr
|
||||
|
||||
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("dial tunnel: %v", err)
|
||||
}
|
||||
defer tunnelConn.Close()
|
||||
|
||||
handshakeCfg := clientCfg
|
||||
handshakeCfg.DisableHTTPMask = true
|
||||
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("client handshake: %v", err)
|
||||
}
|
||||
defer cConn.Close()
|
||||
|
||||
addrBuf, err := EncodeAddress(target)
|
||||
if err != nil {
|
||||
t.Fatalf("encode addr: %v", err)
|
||||
}
|
||||
if _, err := cConn.Write(addrBuf); err != nil {
|
||||
t.Fatalf("write addr: %v", err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 2)
|
||||
if _, err := io.ReadFull(cConn, buf); err != nil {
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
if string(buf) != "ok" {
|
||||
t.Fatalf("unexpected payload: %q", buf)
|
||||
}
|
||||
|
||||
stop()
|
||||
for err := range errCh {
|
||||
t.Fatalf("server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPMaskTunnel_Validation(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Key = "k"
|
||||
cfg.Table = NewTable("seed", "prefer_ascii")
|
||||
cfg.ServerAddress = "127.0.0.1:1"
|
||||
|
||||
cfg.DisableHTTPMask = true
|
||||
cfg.HTTPMaskMode = "stream"
|
||||
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext, nil); err == nil {
|
||||
t.Fatalf("expected error for disabled http mask")
|
||||
}
|
||||
|
||||
cfg.DisableHTTPMask = false
|
||||
cfg.HTTPMaskMode = "legacy"
|
||||
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext, nil); err == nil {
|
||||
t.Fatalf("expected error for legacy mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPMaskTunnel_Soak_Concurrent(t *testing.T) {
|
||||
key := "tunnel-soak-key"
|
||||
target := "1.0.0.1:80"
|
||||
|
||||
serverCfg := newTunnelTestTable(t, key)
|
||||
serverCfg.HTTPMaskMode = "stream"
|
||||
serverCfg.EnablePureDownlink = false
|
||||
|
||||
const (
|
||||
sessions = 8
|
||||
payloadLen = 64 * 1024
|
||||
)
|
||||
|
||||
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
|
||||
if s.Type != SessionTypeTCP {
|
||||
return fmt.Errorf("unexpected session type: %v", s.Type)
|
||||
}
|
||||
if s.Target != target {
|
||||
return fmt.Errorf("target mismatch: %s", s.Target)
|
||||
}
|
||||
buf := make([]byte, payloadLen)
|
||||
if _, err := io.ReadFull(s.Conn, buf); err != nil {
|
||||
return fmt.Errorf("server read payload: %w", err)
|
||||
}
|
||||
_, err := s.Conn.Write(buf)
|
||||
return err
|
||||
})
|
||||
defer stop()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
runErr := make(chan error, sessions)
|
||||
|
||||
for i := 0; i < sessions; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
clientCfg := *serverCfg
|
||||
clientCfg.ServerAddress = addr
|
||||
clientCfg.HTTPMaskHost = strings.TrimSpace(clientCfg.HTTPMaskHost)
|
||||
|
||||
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
|
||||
if err != nil {
|
||||
runErr <- fmt.Errorf("dial: %w", err)
|
||||
return
|
||||
}
|
||||
defer tunnelConn.Close()
|
||||
|
||||
handshakeCfg := clientCfg
|
||||
handshakeCfg.DisableHTTPMask = true
|
||||
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
|
||||
if err != nil {
|
||||
runErr <- fmt.Errorf("handshake: %w", err)
|
||||
return
|
||||
}
|
||||
defer cConn.Close()
|
||||
|
||||
addrBuf, err := EncodeAddress(target)
|
||||
if err != nil {
|
||||
runErr <- fmt.Errorf("encode addr: %w", err)
|
||||
return
|
||||
}
|
||||
if _, err := cConn.Write(addrBuf); err != nil {
|
||||
runErr <- fmt.Errorf("write addr: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
payload := bytes.Repeat([]byte{byte(id)}, payloadLen)
|
||||
if _, err := cConn.Write(payload); err != nil {
|
||||
runErr <- fmt.Errorf("write payload: %w", err)
|
||||
return
|
||||
}
|
||||
echo := make([]byte, payloadLen)
|
||||
if _, err := io.ReadFull(cConn, echo); err != nil {
|
||||
runErr <- fmt.Errorf("read echo: %w", err)
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(echo, payload) {
|
||||
runErr <- fmt.Errorf("echo mismatch")
|
||||
return
|
||||
}
|
||||
runErr <- nil
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(runErr)
|
||||
|
||||
for err := range runErr {
|
||||
if err != nil {
|
||||
t.Fatalf("soak: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
stop()
|
||||
for err := range errCh {
|
||||
t.Fatalf("server error: %v", err)
|
||||
}
|
||||
}
|
||||
145
transport/sudoku/multiplex.go
Normal file
145
transport/sudoku/multiplex.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/metacubex/mihomo/transport/sudoku/multiplex"
|
||||
)
|
||||
|
||||
const (
|
||||
MultiplexMagicByte byte = multiplex.MagicByte
|
||||
MultiplexVersion byte = multiplex.Version
|
||||
)
|
||||
|
||||
// StartMultiplexClient writes the multiplex preface and upgrades an already-handshaked Sudoku tunnel into a multiplex session.
|
||||
func StartMultiplexClient(conn net.Conn) (*MultiplexClient, error) {
|
||||
if conn == nil {
|
||||
return nil, fmt.Errorf("nil conn")
|
||||
}
|
||||
|
||||
if err := multiplex.WritePreface(conn); err != nil {
|
||||
return nil, fmt.Errorf("write multiplex preface failed: %w", err)
|
||||
}
|
||||
|
||||
sess, err := multiplex.NewClientSession(conn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("start multiplex session failed: %w", err)
|
||||
}
|
||||
|
||||
return &MultiplexClient{sess: sess}, nil
|
||||
}
|
||||
|
||||
type MultiplexClient struct {
|
||||
sess *multiplex.Session
|
||||
}
|
||||
|
||||
// Dial opens a new logical stream, writes the target address, and returns the stream as net.Conn.
|
||||
func (c *MultiplexClient) Dial(ctx context.Context, targetAddress string) (net.Conn, error) {
|
||||
if c == nil || c.sess == nil || c.sess.IsClosed() {
|
||||
return nil, fmt.Errorf("multiplex session is closed")
|
||||
}
|
||||
if strings.TrimSpace(targetAddress) == "" {
|
||||
return nil, fmt.Errorf("target address cannot be empty")
|
||||
}
|
||||
|
||||
addrBuf, err := EncodeAddress(targetAddress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode target address failed: %w", err)
|
||||
}
|
||||
|
||||
if ctx != nil && ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
stream, err := c.sess.OpenStream(addrBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
func (c *MultiplexClient) Close() error {
|
||||
if c == nil || c.sess == nil {
|
||||
return nil
|
||||
}
|
||||
return c.sess.Close()
|
||||
}
|
||||
|
||||
func (c *MultiplexClient) IsClosed() bool {
|
||||
if c == nil || c.sess == nil {
|
||||
return true
|
||||
}
|
||||
return c.sess.IsClosed()
|
||||
}
|
||||
|
||||
// AcceptMultiplexServer upgrades a server-side, already-handshaked Sudoku connection into a multiplex session.
|
||||
//
|
||||
// The caller must have already consumed the multiplex magic byte (MultiplexMagicByte). This function consumes the
|
||||
// multiplex version byte and starts the session.
|
||||
func AcceptMultiplexServer(conn net.Conn) (*MultiplexServer, error) {
|
||||
if conn == nil {
|
||||
return nil, fmt.Errorf("nil conn")
|
||||
}
|
||||
v, err := multiplex.ReadVersion(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := multiplex.ValidateVersion(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sess, err := multiplex.NewServerSession(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &MultiplexServer{sess: sess}, nil
|
||||
}
|
||||
|
||||
// MultiplexServer wraps a multiplex session created from a handshaked Sudoku tunnel connection.
|
||||
type MultiplexServer struct {
|
||||
sess *multiplex.Session
|
||||
}
|
||||
|
||||
func (s *MultiplexServer) AcceptStream() (net.Conn, error) {
|
||||
if s == nil || s.sess == nil {
|
||||
return nil, fmt.Errorf("nil session")
|
||||
}
|
||||
c, _, err := s.sess.AcceptStream()
|
||||
return c, err
|
||||
}
|
||||
|
||||
// AcceptTCP accepts a multiplex stream and returns the target address declared in the open frame.
|
||||
func (s *MultiplexServer) AcceptTCP() (net.Conn, string, error) {
|
||||
if s == nil || s.sess == nil {
|
||||
return nil, "", fmt.Errorf("nil session")
|
||||
}
|
||||
stream, payload, err := s.sess.AcceptStream()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
target, err := DecodeAddress(bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
_ = stream.Close()
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return stream, target, nil
|
||||
}
|
||||
|
||||
func (s *MultiplexServer) Close() error {
|
||||
if s == nil || s.sess == nil {
|
||||
return nil
|
||||
}
|
||||
return s.sess.Close()
|
||||
}
|
||||
|
||||
func (s *MultiplexServer) IsClosed() bool {
|
||||
if s == nil || s.sess == nil {
|
||||
return true
|
||||
}
|
||||
return s.sess.IsClosed()
|
||||
}
|
||||
39
transport/sudoku/multiplex/mux.go
Normal file
39
transport/sudoku/multiplex/mux.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package multiplex
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
// MagicByte marks a Sudoku tunnel connection that will switch into multiplex mode.
|
||||
// It is sent after the Sudoku handshake + downlink mode byte.
|
||||
//
|
||||
// Keep it distinct from UoTMagicByte and address type bytes.
|
||||
MagicByte byte = 0xED
|
||||
Version byte = 0x01
|
||||
)
|
||||
|
||||
func WritePreface(w io.Writer) error {
|
||||
if w == nil {
|
||||
return fmt.Errorf("nil writer")
|
||||
}
|
||||
_, err := w.Write([]byte{MagicByte, Version})
|
||||
return err
|
||||
}
|
||||
|
||||
func ReadVersion(r io.Reader) (byte, error) {
|
||||
var b [1]byte
|
||||
if _, err := io.ReadFull(r, b[:]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return b[0], nil
|
||||
}
|
||||
|
||||
func ValidateVersion(v byte) error {
|
||||
if v != Version {
|
||||
return fmt.Errorf("unsupported multiplex version: %d", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
504
transport/sudoku/multiplex/session.go
Normal file
504
transport/sudoku/multiplex/session.go
Normal file
@@ -0,0 +1,504 @@
|
||||
package multiplex
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
frameOpen byte = 0x01
|
||||
frameData byte = 0x02
|
||||
frameClose byte = 0x03
|
||||
frameReset byte = 0x04
|
||||
)
|
||||
|
||||
const (
|
||||
headerSize = 1 + 4 + 4
|
||||
maxFrameSize = 256 * 1024
|
||||
maxDataPayload = 32 * 1024
|
||||
)
|
||||
|
||||
type acceptEvent struct {
|
||||
stream *stream
|
||||
payload []byte
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
conn net.Conn
|
||||
|
||||
writeMu sync.Mutex
|
||||
|
||||
streamsMu sync.Mutex
|
||||
streams map[uint32]*stream
|
||||
nextID uint32
|
||||
|
||||
acceptCh chan acceptEvent
|
||||
|
||||
closed chan struct{}
|
||||
closeOnce sync.Once
|
||||
closeErr error
|
||||
}
|
||||
|
||||
func NewClientSession(conn net.Conn) (*Session, error) {
|
||||
if conn == nil {
|
||||
return nil, fmt.Errorf("nil conn")
|
||||
}
|
||||
s := &Session{
|
||||
conn: conn,
|
||||
streams: make(map[uint32]*stream),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
go s.readLoop()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func NewServerSession(conn net.Conn) (*Session, error) {
|
||||
if conn == nil {
|
||||
return nil, fmt.Errorf("nil conn")
|
||||
}
|
||||
s := &Session{
|
||||
conn: conn,
|
||||
streams: make(map[uint32]*stream),
|
||||
acceptCh: make(chan acceptEvent, 256),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
go s.readLoop()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Session) IsClosed() bool {
|
||||
if s == nil {
|
||||
return true
|
||||
}
|
||||
select {
|
||||
case <-s.closed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) closedErr() error {
|
||||
s.streamsMu.Lock()
|
||||
err := s.closeErr
|
||||
s.streamsMu.Unlock()
|
||||
if err == nil {
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Session) closeWithError(err error) {
|
||||
if err == nil {
|
||||
err = io.ErrClosedPipe
|
||||
}
|
||||
s.closeOnce.Do(func() {
|
||||
s.streamsMu.Lock()
|
||||
if s.closeErr == nil {
|
||||
s.closeErr = err
|
||||
}
|
||||
streams := make([]*stream, 0, len(s.streams))
|
||||
for _, st := range s.streams {
|
||||
streams = append(streams, st)
|
||||
}
|
||||
s.streams = make(map[uint32]*stream)
|
||||
s.streamsMu.Unlock()
|
||||
|
||||
for _, st := range streams {
|
||||
st.closeNoSend(err)
|
||||
}
|
||||
|
||||
close(s.closed)
|
||||
_ = s.conn.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Session) Close() error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
s.closeWithError(io.ErrClosedPipe)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) registerStream(st *stream) {
|
||||
s.streamsMu.Lock()
|
||||
s.streams[st.id] = st
|
||||
s.streamsMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Session) getStream(id uint32) *stream {
|
||||
s.streamsMu.Lock()
|
||||
st := s.streams[id]
|
||||
s.streamsMu.Unlock()
|
||||
return st
|
||||
}
|
||||
|
||||
func (s *Session) removeStream(id uint32) {
|
||||
s.streamsMu.Lock()
|
||||
delete(s.streams, id)
|
||||
s.streamsMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Session) nextStreamID() uint32 {
|
||||
s.streamsMu.Lock()
|
||||
s.nextID++
|
||||
id := s.nextID
|
||||
if id == 0 {
|
||||
s.nextID++
|
||||
id = s.nextID
|
||||
}
|
||||
s.streamsMu.Unlock()
|
||||
return id
|
||||
}
|
||||
|
||||
func (s *Session) sendFrame(frameType byte, streamID uint32, payload []byte) error {
|
||||
if s.IsClosed() {
|
||||
return s.closedErr()
|
||||
}
|
||||
if len(payload) > maxFrameSize {
|
||||
return fmt.Errorf("mux payload too large: %d", len(payload))
|
||||
}
|
||||
|
||||
var header [headerSize]byte
|
||||
header[0] = frameType
|
||||
binary.BigEndian.PutUint32(header[1:5], streamID)
|
||||
binary.BigEndian.PutUint32(header[5:9], uint32(len(payload)))
|
||||
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
|
||||
if err := writeFull(s.conn, header[:]); err != nil {
|
||||
s.closeWithError(err)
|
||||
return err
|
||||
}
|
||||
if len(payload) > 0 {
|
||||
if err := writeFull(s.conn, payload); err != nil {
|
||||
s.closeWithError(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) sendReset(streamID uint32, msg string) {
|
||||
if msg == "" {
|
||||
msg = "reset"
|
||||
}
|
||||
_ = s.sendFrame(frameReset, streamID, []byte(msg))
|
||||
_ = s.sendFrame(frameClose, streamID, nil)
|
||||
}
|
||||
|
||||
func (s *Session) OpenStream(openPayload []byte) (net.Conn, error) {
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("nil session")
|
||||
}
|
||||
if s.IsClosed() {
|
||||
return nil, s.closedErr()
|
||||
}
|
||||
|
||||
streamID := s.nextStreamID()
|
||||
st := newStream(s, streamID)
|
||||
s.registerStream(st)
|
||||
|
||||
if err := s.sendFrame(frameOpen, streamID, openPayload); err != nil {
|
||||
st.closeNoSend(err)
|
||||
s.removeStream(streamID)
|
||||
return nil, fmt.Errorf("mux open failed: %w", err)
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
func (s *Session) AcceptStream() (net.Conn, []byte, error) {
|
||||
if s == nil {
|
||||
return nil, nil, fmt.Errorf("nil session")
|
||||
}
|
||||
if s.acceptCh == nil {
|
||||
return nil, nil, fmt.Errorf("accept is not supported on client sessions")
|
||||
}
|
||||
select {
|
||||
case ev := <-s.acceptCh:
|
||||
return ev.stream, ev.payload, nil
|
||||
case <-s.closed:
|
||||
return nil, nil, s.closedErr()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) readLoop() {
|
||||
var header [headerSize]byte
|
||||
for {
|
||||
if _, err := io.ReadFull(s.conn, header[:]); err != nil {
|
||||
s.closeWithError(err)
|
||||
return
|
||||
}
|
||||
frameType := header[0]
|
||||
streamID := binary.BigEndian.Uint32(header[1:5])
|
||||
n := int(binary.BigEndian.Uint32(header[5:9]))
|
||||
if n < 0 || n > maxFrameSize {
|
||||
s.closeWithError(fmt.Errorf("invalid mux frame length: %d", n))
|
||||
return
|
||||
}
|
||||
|
||||
var payload []byte
|
||||
if n > 0 {
|
||||
payload = make([]byte, n)
|
||||
if _, err := io.ReadFull(s.conn, payload); err != nil {
|
||||
s.closeWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch frameType {
|
||||
case frameOpen:
|
||||
if s.acceptCh == nil {
|
||||
s.sendReset(streamID, "unexpected open")
|
||||
continue
|
||||
}
|
||||
if streamID == 0 {
|
||||
s.sendReset(streamID, "invalid stream id")
|
||||
continue
|
||||
}
|
||||
if existing := s.getStream(streamID); existing != nil {
|
||||
s.sendReset(streamID, "stream already exists")
|
||||
continue
|
||||
}
|
||||
st := newStream(s, streamID)
|
||||
s.registerStream(st)
|
||||
go func() {
|
||||
select {
|
||||
case s.acceptCh <- acceptEvent{stream: st, payload: payload}:
|
||||
case <-s.closed:
|
||||
st.closeNoSend(io.ErrClosedPipe)
|
||||
s.removeStream(streamID)
|
||||
}
|
||||
}()
|
||||
|
||||
case frameData:
|
||||
st := s.getStream(streamID)
|
||||
if st == nil {
|
||||
continue
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
continue
|
||||
}
|
||||
st.enqueue(payload)
|
||||
|
||||
case frameClose:
|
||||
st := s.getStream(streamID)
|
||||
if st == nil {
|
||||
continue
|
||||
}
|
||||
st.closeNoSend(io.EOF)
|
||||
s.removeStream(streamID)
|
||||
|
||||
case frameReset:
|
||||
st := s.getStream(streamID)
|
||||
if st == nil {
|
||||
continue
|
||||
}
|
||||
msg := trimASCII(payload)
|
||||
if msg == "" {
|
||||
msg = "reset"
|
||||
}
|
||||
st.closeNoSend(errors.New(msg))
|
||||
s.removeStream(streamID)
|
||||
|
||||
default:
|
||||
s.closeWithError(fmt.Errorf("unknown mux frame type: %d", frameType))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeFull(w io.Writer, b []byte) error {
|
||||
for len(b) > 0 {
|
||||
n, err := w.Write(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b = b[n:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func trimASCII(b []byte) string {
|
||||
i := 0
|
||||
j := len(b)
|
||||
for i < j {
|
||||
c := b[i]
|
||||
if c != ' ' && c != '\n' && c != '\r' && c != '\t' {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
for j > i {
|
||||
c := b[j-1]
|
||||
if c != ' ' && c != '\n' && c != '\r' && c != '\t' {
|
||||
break
|
||||
}
|
||||
j--
|
||||
}
|
||||
if i >= j {
|
||||
return ""
|
||||
}
|
||||
out := make([]byte, j-i)
|
||||
copy(out, b[i:j])
|
||||
return string(out)
|
||||
}
|
||||
|
||||
type stream struct {
|
||||
session *Session
|
||||
id uint32
|
||||
|
||||
mu sync.Mutex
|
||||
cond *sync.Cond
|
||||
closed bool
|
||||
closeErr error
|
||||
readBuf []byte
|
||||
queue [][]byte
|
||||
|
||||
localAddr net.Addr
|
||||
remoteAddr net.Addr
|
||||
}
|
||||
|
||||
func newStream(session *Session, id uint32) *stream {
|
||||
st := &stream{
|
||||
session: session,
|
||||
id: id,
|
||||
localAddr: &net.TCPAddr{},
|
||||
remoteAddr: &net.TCPAddr{},
|
||||
}
|
||||
st.cond = sync.NewCond(&st.mu)
|
||||
return st
|
||||
}
|
||||
|
||||
func (c *stream) enqueue(payload []byte) {
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.queue = append(c.queue, payload)
|
||||
c.cond.Signal()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *stream) closeNoSend(err error) {
|
||||
if err == nil {
|
||||
err = io.EOF
|
||||
}
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.closed = true
|
||||
if c.closeErr == nil {
|
||||
c.closeErr = err
|
||||
}
|
||||
c.cond.Broadcast()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *stream) closedErr() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.closeErr == nil {
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
return c.closeErr
|
||||
}
|
||||
|
||||
func (c *stream) Read(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
for len(c.readBuf) == 0 && len(c.queue) == 0 && !c.closed {
|
||||
c.cond.Wait()
|
||||
}
|
||||
if len(c.readBuf) == 0 && len(c.queue) > 0 {
|
||||
c.readBuf = c.queue[0]
|
||||
c.queue = c.queue[1:]
|
||||
}
|
||||
if len(c.readBuf) == 0 && c.closed {
|
||||
if c.closeErr == nil {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
return 0, c.closeErr
|
||||
}
|
||||
|
||||
n := copy(p, c.readBuf)
|
||||
c.readBuf = c.readBuf[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *stream) Write(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if c.session == nil || c.session.IsClosed() {
|
||||
if c.session != nil {
|
||||
return 0, c.session.closedErr()
|
||||
}
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
closed := c.closed
|
||||
c.mu.Unlock()
|
||||
if closed {
|
||||
return 0, c.closedErr()
|
||||
}
|
||||
|
||||
written := 0
|
||||
for len(p) > 0 {
|
||||
chunk := p
|
||||
if len(chunk) > maxDataPayload {
|
||||
chunk = p[:maxDataPayload]
|
||||
}
|
||||
if err := c.session.sendFrame(frameData, c.id, chunk); err != nil {
|
||||
return written, err
|
||||
}
|
||||
written += len(chunk)
|
||||
p = p[len(chunk):]
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func (c *stream) Close() error {
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
c.closed = true
|
||||
if c.closeErr == nil {
|
||||
c.closeErr = io.ErrClosedPipe
|
||||
}
|
||||
c.cond.Broadcast()
|
||||
c.mu.Unlock()
|
||||
|
||||
_ = c.session.sendFrame(frameClose, c.id, nil)
|
||||
c.session.removeStream(c.id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *stream) LocalAddr() net.Addr { return c.localAddr }
|
||||
func (c *stream) RemoteAddr() net.Addr { return c.remoteAddr }
|
||||
|
||||
func (c *stream) SetDeadline(t time.Time) error {
|
||||
_ = c.SetReadDeadline(t)
|
||||
_ = c.SetWriteDeadline(t)
|
||||
return nil
|
||||
}
|
||||
func (c *stream) SetReadDeadline(time.Time) error { return nil }
|
||||
func (c *stream) SetWriteDeadline(time.Time) error { return nil }
|
||||
|
||||
260
transport/sudoku/multiplex_test.go
Normal file
260
transport/sudoku/multiplex_test.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||
)
|
||||
|
||||
func TestUserHash_StableAcrossTableRotation(t *testing.T) {
|
||||
tables := []*sudokuobfs.Table{
|
||||
sudokuobfs.NewTable("seed-a", "prefer_ascii"),
|
||||
sudokuobfs.NewTable("seed-b", "prefer_ascii"),
|
||||
}
|
||||
key := "userhash-stability-key"
|
||||
target := "example.com:80"
|
||||
|
||||
serverCfg := DefaultConfig()
|
||||
serverCfg.Key = key
|
||||
serverCfg.AEADMethod = "chacha20-poly1305"
|
||||
serverCfg.Tables = tables
|
||||
serverCfg.PaddingMin = 0
|
||||
serverCfg.PaddingMax = 0
|
||||
serverCfg.EnablePureDownlink = true
|
||||
serverCfg.HandshakeTimeoutSeconds = 5
|
||||
serverCfg.DisableHTTPMask = true
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
|
||||
const attempts = 32
|
||||
hashCh := make(chan string, attempts)
|
||||
errCh := make(chan error, attempts)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
session, err := ServerHandshake(conn, serverCfg)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
defer session.Conn.Close()
|
||||
hashCh <- session.UserHash
|
||||
}(c)
|
||||
}
|
||||
}()
|
||||
|
||||
clientCfg := DefaultConfig()
|
||||
*clientCfg = *serverCfg
|
||||
clientCfg.ServerAddress = ln.Addr().String()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
for i := 0; i < attempts; i++ {
|
||||
raw, err := (&net.Dialer{}).DialContext(ctx, "tcp", clientCfg.ServerAddress)
|
||||
if err != nil {
|
||||
t.Fatalf("dial %d: %v", i, err)
|
||||
}
|
||||
cConn, err := ClientHandshake(raw, clientCfg)
|
||||
if err != nil {
|
||||
_ = raw.Close()
|
||||
t.Fatalf("handshake %d: %v", i, err)
|
||||
}
|
||||
|
||||
addrBuf, err := EncodeAddress(target)
|
||||
if err != nil {
|
||||
_ = cConn.Close()
|
||||
t.Fatalf("encode addr %d: %v", i, err)
|
||||
}
|
||||
if _, err := cConn.Write(addrBuf); err != nil {
|
||||
_ = cConn.Close()
|
||||
t.Fatalf("write addr %d: %v", i, err)
|
||||
}
|
||||
_ = cConn.Close()
|
||||
}
|
||||
|
||||
unique := map[string]struct{}{}
|
||||
deadline := time.After(10 * time.Second)
|
||||
for i := 0; i < attempts; i++ {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
t.Fatalf("server handshake error: %v", err)
|
||||
case h := <-hashCh:
|
||||
if h == "" {
|
||||
t.Fatalf("empty user hash")
|
||||
}
|
||||
if len(h) != 16 {
|
||||
t.Fatalf("unexpected user hash length: %d", len(h))
|
||||
}
|
||||
unique[h] = struct{}{}
|
||||
case <-deadline:
|
||||
t.Fatalf("timeout waiting for server handshakes")
|
||||
}
|
||||
}
|
||||
if len(unique) != 1 {
|
||||
t.Fatalf("user hash should be stable across table rotation; got %d distinct values", len(unique))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiplex_TCP_Echo(t *testing.T) {
|
||||
table := sudokuobfs.NewTable("seed", "prefer_ascii")
|
||||
key := "test-key-mux"
|
||||
target := "example.com:80"
|
||||
|
||||
serverCfg := DefaultConfig()
|
||||
serverCfg.Key = key
|
||||
serverCfg.AEADMethod = "chacha20-poly1305"
|
||||
serverCfg.Table = table
|
||||
serverCfg.PaddingMin = 0
|
||||
serverCfg.PaddingMax = 0
|
||||
serverCfg.EnablePureDownlink = true
|
||||
serverCfg.HandshakeTimeoutSeconds = 5
|
||||
serverCfg.DisableHTTPMask = true
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
|
||||
var handshakes int64
|
||||
var streams int64
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
raw, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer raw.Close()
|
||||
|
||||
session, err := ServerHandshake(raw, serverCfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
atomic.AddInt64(&handshakes, 1)
|
||||
|
||||
if session.Type != SessionTypeMultiplex {
|
||||
_ = session.Conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
mux, err := AcceptMultiplexServer(session.Conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer mux.Close()
|
||||
|
||||
for {
|
||||
stream, dst, err := mux.AcceptTCP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if dst != target {
|
||||
_ = stream.Close()
|
||||
return
|
||||
}
|
||||
atomic.AddInt64(&streams, 1)
|
||||
go func(c net.Conn) {
|
||||
defer c.Close()
|
||||
_, _ = io.Copy(c, c)
|
||||
}(stream)
|
||||
}
|
||||
}()
|
||||
|
||||
clientCfg := DefaultConfig()
|
||||
*clientCfg = *serverCfg
|
||||
clientCfg.ServerAddress = ln.Addr().String()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
raw, err := (&net.Dialer{}).DialContext(ctx, "tcp", clientCfg.ServerAddress)
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = raw.Close() })
|
||||
|
||||
cConn, err := ClientHandshake(raw, clientCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("client handshake: %v", err)
|
||||
}
|
||||
|
||||
mux, err := StartMultiplexClient(cConn)
|
||||
if err != nil {
|
||||
_ = cConn.Close()
|
||||
t.Fatalf("start mux: %v", err)
|
||||
}
|
||||
defer mux.Close()
|
||||
|
||||
for i := 0; i < 6; i++ {
|
||||
s, err := mux.Dial(ctx, target)
|
||||
if err != nil {
|
||||
t.Fatalf("dial stream %d: %v", i, err)
|
||||
}
|
||||
|
||||
msg := []byte("hello-mux")
|
||||
if _, err := s.Write(msg); err != nil {
|
||||
_ = s.Close()
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
buf := make([]byte, len(msg))
|
||||
if _, err := io.ReadFull(s, buf); err != nil {
|
||||
_ = s.Close()
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
_ = s.Close()
|
||||
if !bytes.Equal(buf, msg) {
|
||||
t.Fatalf("echo mismatch: got %q", buf)
|
||||
}
|
||||
}
|
||||
|
||||
_ = mux.Close()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("server did not exit")
|
||||
}
|
||||
|
||||
if got := atomic.LoadInt64(&handshakes); got != 1 {
|
||||
t.Fatalf("unexpected handshake count: %d", got)
|
||||
}
|
||||
if got := atomic.LoadInt64(&streams); got < 6 {
|
||||
t.Fatalf("unexpected stream count: %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiplex_Boundary_InvalidVersion(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
t.Cleanup(func() { _ = client.Close() })
|
||||
t.Cleanup(func() { _ = server.Close() })
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := AcceptMultiplexServer(server)
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
// AcceptMultiplexServer expects the magic byte to have been consumed already; write a bad version byte.
|
||||
_, _ = client.Write([]byte{0xFF})
|
||||
if err := <-errCh; err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
}
|
||||
137
transport/sudoku/obfs/httpmask/auth.go
Normal file
137
transport/sudoku/obfs/httpmask/auth.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package httpmask
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
tunnelAuthHeaderKey = "Authorization"
|
||||
tunnelAuthHeaderPrefix = "Bearer "
|
||||
)
|
||||
|
||||
type tunnelAuth struct {
|
||||
key [32]byte // derived HMAC key
|
||||
skew time.Duration
|
||||
}
|
||||
|
||||
func newTunnelAuth(key string, skew time.Duration) *tunnelAuth {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
return nil
|
||||
}
|
||||
if skew <= 0 {
|
||||
skew = 60 * time.Second
|
||||
}
|
||||
|
||||
// Domain separation: keep this HMAC key independent from other uses of cfg.Key.
|
||||
h := sha256.New()
|
||||
_, _ = h.Write([]byte("sudoku-httpmask-auth-v1:"))
|
||||
_, _ = h.Write([]byte(key))
|
||||
|
||||
var sum [32]byte
|
||||
h.Sum(sum[:0])
|
||||
|
||||
return &tunnelAuth{key: sum, skew: skew}
|
||||
}
|
||||
|
||||
func (a *tunnelAuth) token(mode TunnelMode, method, path string, now time.Time) string {
|
||||
if a == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
ts := now.Unix()
|
||||
sig := a.sign(mode, method, path, ts)
|
||||
|
||||
var buf [8 + 16]byte
|
||||
binary.BigEndian.PutUint64(buf[:8], uint64(ts))
|
||||
copy(buf[8:], sig[:])
|
||||
return base64.RawURLEncoding.EncodeToString(buf[:])
|
||||
}
|
||||
|
||||
func (a *tunnelAuth) verify(headers map[string]string, mode TunnelMode, method, path string, now time.Time) bool {
|
||||
if a == nil {
|
||||
return true
|
||||
}
|
||||
if headers == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
val := strings.TrimSpace(headers["authorization"])
|
||||
if val == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Accept both "Bearer <token>" and raw token forms (for forward proxies / CDNs that may normalize headers).
|
||||
if len(val) > len(tunnelAuthHeaderPrefix) && strings.EqualFold(val[:len(tunnelAuthHeaderPrefix)], tunnelAuthHeaderPrefix) {
|
||||
val = strings.TrimSpace(val[len(tunnelAuthHeaderPrefix):])
|
||||
}
|
||||
if val == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
raw, err := base64.RawURLEncoding.DecodeString(val)
|
||||
if err != nil || len(raw) != 8+16 {
|
||||
return false
|
||||
}
|
||||
|
||||
ts := int64(binary.BigEndian.Uint64(raw[:8]))
|
||||
nowTS := now.Unix()
|
||||
delta := nowTS - ts
|
||||
if delta < 0 {
|
||||
delta = -delta
|
||||
}
|
||||
if delta > int64(a.skew.Seconds()) {
|
||||
return false
|
||||
}
|
||||
|
||||
want := a.sign(mode, method, path, ts)
|
||||
return subtle.ConstantTimeCompare(raw[8:], want[:]) == 1
|
||||
}
|
||||
|
||||
func (a *tunnelAuth) sign(mode TunnelMode, method, path string, ts int64) [16]byte {
|
||||
method = strings.ToUpper(strings.TrimSpace(method))
|
||||
if method == "" {
|
||||
method = "GET"
|
||||
}
|
||||
path = strings.TrimSpace(path)
|
||||
|
||||
var tsBuf [8]byte
|
||||
binary.BigEndian.PutUint64(tsBuf[:], uint64(ts))
|
||||
|
||||
mac := hmac.New(sha256.New, a.key[:])
|
||||
_, _ = mac.Write([]byte(mode))
|
||||
_, _ = mac.Write([]byte{0})
|
||||
_, _ = mac.Write([]byte(method))
|
||||
_, _ = mac.Write([]byte{0})
|
||||
_, _ = mac.Write([]byte(path))
|
||||
_, _ = mac.Write([]byte{0})
|
||||
_, _ = mac.Write(tsBuf[:])
|
||||
|
||||
var full [32]byte
|
||||
mac.Sum(full[:0])
|
||||
|
||||
var out [16]byte
|
||||
copy(out[:], full[:16])
|
||||
return out
|
||||
}
|
||||
|
||||
type headerSetter interface {
|
||||
Set(key, value string)
|
||||
}
|
||||
|
||||
func applyTunnelAuthHeader(h headerSetter, auth *tunnelAuth, mode TunnelMode, method, path string) {
|
||||
if auth == nil || h == nil {
|
||||
return
|
||||
}
|
||||
token := auth.token(mode, method, path, time.Now())
|
||||
if token == "" {
|
||||
return
|
||||
}
|
||||
h.Set(tunnelAuthHeaderKey, tunnelAuthHeaderPrefix+token)
|
||||
}
|
||||
252
transport/sudoku/obfs/httpmask/masker.go
Normal file
252
transport/sudoku/obfs/httpmask/masker.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package httpmask
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
userAgents = []string{
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36",
|
||||
}
|
||||
accepts = []string{
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||
"application/json, text/plain, */*",
|
||||
"application/octet-stream",
|
||||
"*/*",
|
||||
}
|
||||
acceptLanguages = []string{
|
||||
"en-US,en;q=0.9",
|
||||
"en-GB,en;q=0.9",
|
||||
"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
}
|
||||
acceptEncodings = []string{
|
||||
"gzip, deflate, br",
|
||||
"gzip, deflate",
|
||||
"br, gzip, deflate",
|
||||
}
|
||||
paths = []string{
|
||||
"/api/v1/upload",
|
||||
"/data/sync",
|
||||
"/uploads/raw",
|
||||
"/api/report",
|
||||
"/feed/update",
|
||||
"/v2/events",
|
||||
"/v1/telemetry",
|
||||
"/session",
|
||||
"/stream",
|
||||
"/ws",
|
||||
}
|
||||
contentTypes = []string{
|
||||
"application/octet-stream",
|
||||
"application/x-protobuf",
|
||||
"application/json",
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
rngPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
},
|
||||
}
|
||||
headerBufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
b := make([]byte, 0, 1024)
|
||||
return &b
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// LooksLikeHTTPRequestStart reports whether peek4 looks like a supported HTTP/1.x request method prefix.
|
||||
func LooksLikeHTTPRequestStart(peek4 []byte) bool {
|
||||
if len(peek4) < 4 {
|
||||
return false
|
||||
}
|
||||
// Common methods: "GET ", "POST", "HEAD", "PUT ", "OPTI" (OPTIONS), "PATC" (PATCH), "DELE" (DELETE)
|
||||
return bytes.Equal(peek4, []byte("GET ")) ||
|
||||
bytes.Equal(peek4, []byte("POST")) ||
|
||||
bytes.Equal(peek4, []byte("HEAD")) ||
|
||||
bytes.Equal(peek4, []byte("PUT ")) ||
|
||||
bytes.Equal(peek4, []byte("OPTI")) ||
|
||||
bytes.Equal(peek4, []byte("PATC")) ||
|
||||
bytes.Equal(peek4, []byte("DELE"))
|
||||
}
|
||||
|
||||
func trimPortForHost(host string) string {
|
||||
if host == "" {
|
||||
return host
|
||||
}
|
||||
// Accept "example.com:443" / "1.2.3.4:443" / "[::1]:443"
|
||||
h, _, err := net.SplitHostPort(host)
|
||||
if err == nil && h != "" {
|
||||
return h
|
||||
}
|
||||
// If it's not in host:port form, keep as-is.
|
||||
return host
|
||||
}
|
||||
|
||||
func appendCommonHeaders(buf []byte, host string, r *rand.Rand) []byte {
|
||||
ua := userAgents[r.Intn(len(userAgents))]
|
||||
accept := accepts[r.Intn(len(accepts))]
|
||||
lang := acceptLanguages[r.Intn(len(acceptLanguages))]
|
||||
enc := acceptEncodings[r.Intn(len(acceptEncodings))]
|
||||
|
||||
buf = append(buf, "Host: "...)
|
||||
buf = append(buf, host...)
|
||||
buf = append(buf, "\r\nUser-Agent: "...)
|
||||
buf = append(buf, ua...)
|
||||
buf = append(buf, "\r\nAccept: "...)
|
||||
buf = append(buf, accept...)
|
||||
buf = append(buf, "\r\nAccept-Language: "...)
|
||||
buf = append(buf, lang...)
|
||||
buf = append(buf, "\r\nAccept-Encoding: "...)
|
||||
buf = append(buf, enc...)
|
||||
buf = append(buf, "\r\nConnection: keep-alive\r\n"...)
|
||||
|
||||
// A couple of common cache headers; keep them static for simplicity.
|
||||
buf = append(buf, "Cache-Control: no-cache\r\nPragma: no-cache\r\n"...)
|
||||
return buf
|
||||
}
|
||||
|
||||
// WriteRandomRequestHeader writes a plausible HTTP/1.1 request header as a mask.
|
||||
func WriteRandomRequestHeader(w io.Writer, host string) error {
|
||||
return WriteRandomRequestHeaderWithPathRoot(w, host, "")
|
||||
}
|
||||
|
||||
// WriteRandomRequestHeaderWithPathRoot is like WriteRandomRequestHeader but prefixes all paths with pathRoot.
|
||||
// pathRoot must be a single segment (e.g. "aabbcc"); invalid inputs are treated as empty (disabled).
|
||||
func WriteRandomRequestHeaderWithPathRoot(w io.Writer, host string, pathRoot string) error {
|
||||
// Get RNG from pool
|
||||
r := rngPool.Get().(*rand.Rand)
|
||||
defer rngPool.Put(r)
|
||||
|
||||
path := joinPathRoot(pathRoot, paths[r.Intn(len(paths))])
|
||||
ctype := contentTypes[r.Intn(len(contentTypes))]
|
||||
|
||||
// Use buffer pool
|
||||
bufPtr := headerBufPool.Get().(*[]byte)
|
||||
buf := *bufPtr
|
||||
buf = buf[:0]
|
||||
defer func() {
|
||||
if cap(buf) <= 4096 {
|
||||
*bufPtr = buf
|
||||
headerBufPool.Put(bufPtr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Weighted template selection. Keep a conservative default (POST w/ Content-Length),
|
||||
// but occasionally rotate to other realistic templates (e.g. WebSocket upgrade).
|
||||
switch r.Intn(10) {
|
||||
case 0, 1: // ~20% WebSocket-like upgrade
|
||||
hostNoPort := trimPortForHost(host)
|
||||
var keyBytes [16]byte
|
||||
for i := 0; i < len(keyBytes); i++ {
|
||||
keyBytes[i] = byte(r.Intn(256))
|
||||
}
|
||||
wsKey := base64.StdEncoding.EncodeToString(keyBytes[:])
|
||||
|
||||
buf = append(buf, "GET "...)
|
||||
buf = append(buf, path...)
|
||||
buf = append(buf, " HTTP/1.1\r\n"...)
|
||||
buf = appendCommonHeaders(buf, host, r)
|
||||
buf = append(buf, "Upgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: "...)
|
||||
buf = append(buf, wsKey...)
|
||||
buf = append(buf, "\r\nOrigin: https://"...)
|
||||
buf = append(buf, hostNoPort...)
|
||||
buf = append(buf, "\r\n\r\n"...)
|
||||
default: // ~80% POST upload
|
||||
// Random Content-Length: 4KB–10MB. Small enough to look plausible, large enough
|
||||
// to justify long-lived writes on keep-alive connections.
|
||||
const minCL = int64(4 * 1024)
|
||||
const maxCL = int64(10 * 1024 * 1024)
|
||||
contentLength := minCL + r.Int63n(maxCL-minCL+1)
|
||||
|
||||
buf = append(buf, "POST "...)
|
||||
buf = append(buf, path...)
|
||||
buf = append(buf, " HTTP/1.1\r\n"...)
|
||||
buf = appendCommonHeaders(buf, host, r)
|
||||
buf = append(buf, "Content-Type: "...)
|
||||
buf = append(buf, ctype...)
|
||||
buf = append(buf, "\r\nContent-Length: "...)
|
||||
buf = strconv.AppendInt(buf, contentLength, 10)
|
||||
// A couple of extra headers seen in real clients.
|
||||
if r.Intn(2) == 0 {
|
||||
buf = append(buf, "\r\nX-Requested-With: XMLHttpRequest"...)
|
||||
}
|
||||
if r.Intn(3) == 0 {
|
||||
buf = append(buf, "\r\nReferer: https://"...)
|
||||
buf = append(buf, trimPortForHost(host)...)
|
||||
buf = append(buf, "/"...)
|
||||
}
|
||||
buf = append(buf, "\r\n\r\n"...)
|
||||
}
|
||||
|
||||
_, err := w.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
// ConsumeHeader 读取并消耗 HTTP 头部,返回消耗的数据和剩余的 reader 数据
|
||||
// 如果不是 POST 请求或格式严重错误,返回 error
|
||||
func ConsumeHeader(r *bufio.Reader) ([]byte, error) {
|
||||
var consumed bytes.Buffer
|
||||
|
||||
// 1. 读取请求行
|
||||
// Use ReadSlice to avoid allocation if line fits in buffer
|
||||
line, err := r.ReadSlice('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
consumed.Write(line)
|
||||
|
||||
// Basic method validation: accept common HTTP/1.x methods used by our masker.
|
||||
// Keep it strict enough to reject obvious garbage.
|
||||
switch {
|
||||
case bytes.HasPrefix(line, []byte("POST ")),
|
||||
bytes.HasPrefix(line, []byte("GET ")),
|
||||
bytes.HasPrefix(line, []byte("HEAD ")),
|
||||
bytes.HasPrefix(line, []byte("PUT ")),
|
||||
bytes.HasPrefix(line, []byte("DELETE ")),
|
||||
bytes.HasPrefix(line, []byte("OPTIONS ")),
|
||||
bytes.HasPrefix(line, []byte("PATCH ")):
|
||||
default:
|
||||
return consumed.Bytes(), fmt.Errorf("invalid method or garbage: %s", strings.TrimSpace(string(line)))
|
||||
}
|
||||
|
||||
// 2. 循环读取头部,直到遇到空行
|
||||
for {
|
||||
line, err = r.ReadSlice('\n')
|
||||
if err != nil {
|
||||
return consumed.Bytes(), err
|
||||
}
|
||||
consumed.Write(line)
|
||||
|
||||
// Check for empty line (\r\n or \n)
|
||||
// ReadSlice includes the delimiter
|
||||
n := len(line)
|
||||
if n == 2 && line[0] == '\r' && line[1] == '\n' {
|
||||
return consumed.Bytes(), nil
|
||||
}
|
||||
if n == 1 && line[0] == '\n' {
|
||||
return consumed.Bytes(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
52
transport/sudoku/obfs/httpmask/pathroot.go
Normal file
52
transport/sudoku/obfs/httpmask/pathroot.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package httpmask
|
||||
|
||||
import "strings"
|
||||
|
||||
// normalizePathRoot normalizes the configured path root into "/<segment>" form.
|
||||
//
|
||||
// It is intentionally strict: only a single path segment is allowed, consisting of
|
||||
// [A-Za-z0-9_-]. Invalid inputs are treated as empty (disabled).
|
||||
func normalizePathRoot(root string) string {
|
||||
root = strings.TrimSpace(root)
|
||||
root = strings.Trim(root, "/")
|
||||
if root == "" {
|
||||
return ""
|
||||
}
|
||||
for i := 0; i < len(root); i++ {
|
||||
c := root[i]
|
||||
switch {
|
||||
case c >= 'a' && c <= 'z':
|
||||
case c >= 'A' && c <= 'Z':
|
||||
case c >= '0' && c <= '9':
|
||||
case c == '_' || c == '-':
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return "/" + root
|
||||
}
|
||||
|
||||
func joinPathRoot(root, path string) string {
|
||||
root = normalizePathRoot(root)
|
||||
if root == "" {
|
||||
return path
|
||||
}
|
||||
if path == "" {
|
||||
return root
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
return root + path
|
||||
}
|
||||
|
||||
func stripPathRoot(root, fullPath string) (string, bool) {
|
||||
root = normalizePathRoot(root)
|
||||
if root == "" {
|
||||
return fullPath, true
|
||||
}
|
||||
if !strings.HasPrefix(fullPath, root+"/") {
|
||||
return "", false
|
||||
}
|
||||
return strings.TrimPrefix(fullPath, root), true
|
||||
}
|
||||
2130
transport/sudoku/obfs/httpmask/tunnel.go
Normal file
2130
transport/sudoku/obfs/httpmask/tunnel.go
Normal file
File diff suppressed because it is too large
Load Diff
212
transport/sudoku/obfs/sudoku/conn.go
Normal file
212
transport/sudoku/obfs/sudoku/conn.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
crypto_rand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const IOBufferSize = 32 * 1024
|
||||
|
||||
var perm4 = [24][4]byte{
|
||||
{0, 1, 2, 3},
|
||||
{0, 1, 3, 2},
|
||||
{0, 2, 1, 3},
|
||||
{0, 2, 3, 1},
|
||||
{0, 3, 1, 2},
|
||||
{0, 3, 2, 1},
|
||||
{1, 0, 2, 3},
|
||||
{1, 0, 3, 2},
|
||||
{1, 2, 0, 3},
|
||||
{1, 2, 3, 0},
|
||||
{1, 3, 0, 2},
|
||||
{1, 3, 2, 0},
|
||||
{2, 0, 1, 3},
|
||||
{2, 0, 3, 1},
|
||||
{2, 1, 0, 3},
|
||||
{2, 1, 3, 0},
|
||||
{2, 3, 0, 1},
|
||||
{2, 3, 1, 0},
|
||||
{3, 0, 1, 2},
|
||||
{3, 0, 2, 1},
|
||||
{3, 1, 0, 2},
|
||||
{3, 1, 2, 0},
|
||||
{3, 2, 0, 1},
|
||||
{3, 2, 1, 0},
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
net.Conn
|
||||
table *Table
|
||||
reader *bufio.Reader
|
||||
recorder *bytes.Buffer
|
||||
recording bool
|
||||
recordLock sync.Mutex
|
||||
|
||||
rawBuf []byte
|
||||
pendingData []byte
|
||||
hintBuf []byte
|
||||
|
||||
rng *rand.Rand
|
||||
paddingRate float32
|
||||
}
|
||||
|
||||
func NewConn(c net.Conn, table *Table, pMin, pMax int, record bool) *Conn {
|
||||
var seedBytes [8]byte
|
||||
if _, err := crypto_rand.Read(seedBytes[:]); err != nil {
|
||||
binary.BigEndian.PutUint64(seedBytes[:], uint64(rand.Int63()))
|
||||
}
|
||||
seed := int64(binary.BigEndian.Uint64(seedBytes[:]))
|
||||
localRng := rand.New(rand.NewSource(seed))
|
||||
|
||||
min := float32(pMin) / 100.0
|
||||
rng := float32(pMax-pMin) / 100.0
|
||||
rate := min + localRng.Float32()*rng
|
||||
|
||||
sc := &Conn{
|
||||
Conn: c,
|
||||
table: table,
|
||||
reader: bufio.NewReaderSize(c, IOBufferSize),
|
||||
rawBuf: make([]byte, IOBufferSize),
|
||||
pendingData: make([]byte, 0, 4096),
|
||||
hintBuf: make([]byte, 0, 4),
|
||||
rng: localRng,
|
||||
paddingRate: rate,
|
||||
}
|
||||
if record {
|
||||
sc.recorder = new(bytes.Buffer)
|
||||
sc.recording = true
|
||||
}
|
||||
return sc
|
||||
}
|
||||
|
||||
func (sc *Conn) StopRecording() {
|
||||
sc.recordLock.Lock()
|
||||
sc.recording = false
|
||||
sc.recorder = nil
|
||||
sc.recordLock.Unlock()
|
||||
}
|
||||
|
||||
func (sc *Conn) GetBufferedAndRecorded() []byte {
|
||||
if sc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
sc.recordLock.Lock()
|
||||
defer sc.recordLock.Unlock()
|
||||
|
||||
var recorded []byte
|
||||
if sc.recorder != nil {
|
||||
recorded = sc.recorder.Bytes()
|
||||
}
|
||||
|
||||
buffered := sc.reader.Buffered()
|
||||
if buffered > 0 {
|
||||
peeked, _ := sc.reader.Peek(buffered)
|
||||
full := make([]byte, len(recorded)+len(peeked))
|
||||
copy(full, recorded)
|
||||
copy(full[len(recorded):], peeked)
|
||||
return full
|
||||
}
|
||||
return recorded
|
||||
}
|
||||
|
||||
func (sc *Conn) Write(p []byte) (n int, err error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
outCapacity := len(p) * 6
|
||||
out := make([]byte, 0, outCapacity)
|
||||
pads := sc.table.PaddingPool
|
||||
padLen := len(pads)
|
||||
|
||||
for _, b := range p {
|
||||
if sc.rng.Float32() < sc.paddingRate {
|
||||
out = append(out, pads[sc.rng.Intn(padLen)])
|
||||
}
|
||||
|
||||
puzzles := sc.table.EncodeTable[b]
|
||||
puzzle := puzzles[sc.rng.Intn(len(puzzles))]
|
||||
|
||||
perm := perm4[sc.rng.Intn(len(perm4))]
|
||||
for _, idx := range perm {
|
||||
if sc.rng.Float32() < sc.paddingRate {
|
||||
out = append(out, pads[sc.rng.Intn(padLen)])
|
||||
}
|
||||
out = append(out, puzzle[idx])
|
||||
}
|
||||
}
|
||||
|
||||
if sc.rng.Float32() < sc.paddingRate {
|
||||
out = append(out, pads[sc.rng.Intn(padLen)])
|
||||
}
|
||||
|
||||
_, err = sc.Conn.Write(out)
|
||||
return len(p), err
|
||||
}
|
||||
|
||||
func (sc *Conn) Read(p []byte) (n int, err error) {
|
||||
if len(sc.pendingData) > 0 {
|
||||
n = copy(p, sc.pendingData)
|
||||
if n == len(sc.pendingData) {
|
||||
sc.pendingData = sc.pendingData[:0]
|
||||
} else {
|
||||
sc.pendingData = sc.pendingData[n:]
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
for {
|
||||
if len(sc.pendingData) > 0 {
|
||||
break
|
||||
}
|
||||
|
||||
nr, rErr := sc.reader.Read(sc.rawBuf)
|
||||
if nr > 0 {
|
||||
chunk := sc.rawBuf[:nr]
|
||||
sc.recordLock.Lock()
|
||||
if sc.recording {
|
||||
sc.recorder.Write(chunk)
|
||||
}
|
||||
sc.recordLock.Unlock()
|
||||
|
||||
for _, b := range chunk {
|
||||
if !sc.table.layout.isHint(b) {
|
||||
continue
|
||||
}
|
||||
|
||||
sc.hintBuf = append(sc.hintBuf, b)
|
||||
if len(sc.hintBuf) == 4 {
|
||||
key := packHintsToKey([4]byte{sc.hintBuf[0], sc.hintBuf[1], sc.hintBuf[2], sc.hintBuf[3]})
|
||||
val, ok := sc.table.DecodeMap[key]
|
||||
if !ok {
|
||||
return 0, errors.New("INVALID_SUDOKU_MAP_MISS")
|
||||
}
|
||||
sc.pendingData = append(sc.pendingData, val)
|
||||
sc.hintBuf = sc.hintBuf[:0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rErr != nil {
|
||||
return 0, rErr
|
||||
}
|
||||
if len(sc.pendingData) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
n = copy(p, sc.pendingData)
|
||||
if n == len(sc.pendingData) {
|
||||
sc.pendingData = sc.pendingData[:0]
|
||||
} else {
|
||||
sc.pendingData = sc.pendingData[n:]
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
46
transport/sudoku/obfs/sudoku/grid.go
Normal file
46
transport/sudoku/obfs/sudoku/grid.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package sudoku
|
||||
|
||||
// Grid represents a 4x4 sudoku grid
|
||||
type Grid [16]uint8
|
||||
|
||||
// GenerateAllGrids generates all valid 4x4 Sudoku grids
|
||||
func GenerateAllGrids() []Grid {
|
||||
var grids []Grid
|
||||
var g Grid
|
||||
var backtrack func(int)
|
||||
|
||||
backtrack = func(idx int) {
|
||||
if idx == 16 {
|
||||
grids = append(grids, g)
|
||||
return
|
||||
}
|
||||
row, col := idx/4, idx%4
|
||||
br, bc := (row/2)*2, (col/2)*2
|
||||
for num := uint8(1); num <= 4; num++ {
|
||||
valid := true
|
||||
for i := 0; i < 4; i++ {
|
||||
if g[row*4+i] == num || g[i*4+col] == num {
|
||||
valid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if valid {
|
||||
for r := 0; r < 2; r++ {
|
||||
for c := 0; c < 2; c++ {
|
||||
if g[(br+r)*4+(bc+c)] == num {
|
||||
valid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if valid {
|
||||
g[idx] = num
|
||||
backtrack(idx + 1)
|
||||
g[idx] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
backtrack(0)
|
||||
return grids
|
||||
}
|
||||
221
transport/sudoku/obfs/sudoku/layout.go
Normal file
221
transport/sudoku/obfs/sudoku/layout.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type byteLayout struct {
|
||||
name string
|
||||
hintMask byte
|
||||
hintValue byte
|
||||
padMarker byte
|
||||
paddingPool []byte
|
||||
|
||||
encodeHint func(val, pos byte) byte
|
||||
encodeGroup func(group byte) byte
|
||||
decodeGroup func(b byte) (byte, bool)
|
||||
}
|
||||
|
||||
func (l *byteLayout) isHint(b byte) bool {
|
||||
if (b & l.hintMask) == l.hintValue {
|
||||
return true
|
||||
}
|
||||
// ASCII layout maps the single non-printable marker (0x7F) to '\n' on the wire.
|
||||
return l.name == "ascii" && b == '\n'
|
||||
}
|
||||
|
||||
// resolveLayout picks the byte layout based on ASCII preference and optional custom pattern.
|
||||
// ASCII always wins if requested. Custom patterns are ignored when ASCII is preferred.
|
||||
func resolveLayout(mode string, customPattern string) (*byteLayout, error) {
|
||||
switch strings.ToLower(mode) {
|
||||
case "ascii", "prefer_ascii":
|
||||
return newASCIILayout(), nil
|
||||
case "entropy", "prefer_entropy", "":
|
||||
// fallback to entropy unless a custom pattern is provided
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid ascii mode: %s", mode)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(customPattern) != "" {
|
||||
return newCustomLayout(customPattern)
|
||||
}
|
||||
return newEntropyLayout(), nil
|
||||
}
|
||||
|
||||
func newASCIILayout() *byteLayout {
|
||||
padding := make([]byte, 0, 32)
|
||||
for i := 0; i < 32; i++ {
|
||||
padding = append(padding, byte(0x20+i))
|
||||
}
|
||||
return &byteLayout{
|
||||
name: "ascii",
|
||||
hintMask: 0x40,
|
||||
hintValue: 0x40,
|
||||
padMarker: 0x3F,
|
||||
paddingPool: padding,
|
||||
encodeHint: func(val, pos byte) byte {
|
||||
b := 0x40 | ((val & 0x03) << 4) | (pos & 0x0F)
|
||||
// Avoid DEL (0x7F) in prefer_ascii mode; map it to '\n' to reduce fingerprint.
|
||||
if b == 0x7F {
|
||||
return '\n'
|
||||
}
|
||||
return b
|
||||
},
|
||||
encodeGroup: func(group byte) byte {
|
||||
b := 0x40 | (group & 0x3F)
|
||||
// Avoid DEL (0x7F) in prefer_ascii mode; map it to '\n' to reduce fingerprint.
|
||||
if b == 0x7F {
|
||||
return '\n'
|
||||
}
|
||||
return b
|
||||
},
|
||||
decodeGroup: func(b byte) (byte, bool) {
|
||||
if b == '\n' {
|
||||
return 0x3F, true
|
||||
}
|
||||
if (b & 0x40) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return b & 0x3F, true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newEntropyLayout() *byteLayout {
|
||||
padding := make([]byte, 0, 16)
|
||||
for i := 0; i < 8; i++ {
|
||||
padding = append(padding, byte(0x80+i))
|
||||
padding = append(padding, byte(0x10+i))
|
||||
}
|
||||
return &byteLayout{
|
||||
name: "entropy",
|
||||
hintMask: 0x90,
|
||||
hintValue: 0x00,
|
||||
padMarker: 0x80,
|
||||
paddingPool: padding,
|
||||
encodeHint: func(val, pos byte) byte {
|
||||
return ((val & 0x03) << 5) | (pos & 0x0F)
|
||||
},
|
||||
encodeGroup: func(group byte) byte {
|
||||
v := group & 0x3F
|
||||
return ((v & 0x30) << 1) | (v & 0x0F)
|
||||
},
|
||||
decodeGroup: func(b byte) (byte, bool) {
|
||||
if (b & 0x90) != 0 {
|
||||
return 0, false
|
||||
}
|
||||
return ((b >> 1) & 0x30) | (b & 0x0F), true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newCustomLayout(pattern string) (*byteLayout, error) {
|
||||
cleaned := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(pattern), " ", ""))
|
||||
if len(cleaned) != 8 {
|
||||
return nil, fmt.Errorf("custom table must have 8 symbols, got %d", len(cleaned))
|
||||
}
|
||||
|
||||
var xBits, pBits, vBits []uint8
|
||||
for i, c := range cleaned {
|
||||
bit := uint8(7 - i)
|
||||
switch c {
|
||||
case 'x':
|
||||
xBits = append(xBits, bit)
|
||||
case 'p':
|
||||
pBits = append(pBits, bit)
|
||||
case 'v':
|
||||
vBits = append(vBits, bit)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid char %q in custom table", c)
|
||||
}
|
||||
}
|
||||
|
||||
if len(xBits) != 2 || len(pBits) != 2 || len(vBits) != 4 {
|
||||
return nil, fmt.Errorf("custom table must contain exactly 2 x, 2 p, 4 v")
|
||||
}
|
||||
|
||||
xMask := byte(0)
|
||||
for _, b := range xBits {
|
||||
xMask |= 1 << b
|
||||
}
|
||||
|
||||
encodeBits := func(val, pos byte, dropX int) byte {
|
||||
var out byte
|
||||
out |= xMask
|
||||
if dropX >= 0 {
|
||||
out &^= 1 << xBits[dropX]
|
||||
}
|
||||
if (val & 0x02) != 0 {
|
||||
out |= 1 << pBits[0]
|
||||
}
|
||||
if (val & 0x01) != 0 {
|
||||
out |= 1 << pBits[1]
|
||||
}
|
||||
for i, bit := range vBits {
|
||||
if (pos>>(3-uint8(i)))&0x01 == 1 {
|
||||
out |= 1 << bit
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
decodeGroup := func(b byte) (byte, bool) {
|
||||
if (b & xMask) != xMask {
|
||||
return 0, false
|
||||
}
|
||||
var val, pos byte
|
||||
if b&(1<<pBits[0]) != 0 {
|
||||
val |= 0x02
|
||||
}
|
||||
if b&(1<<pBits[1]) != 0 {
|
||||
val |= 0x01
|
||||
}
|
||||
for i, bit := range vBits {
|
||||
if b&(1<<bit) != 0 {
|
||||
pos |= 1 << (3 - uint8(i))
|
||||
}
|
||||
}
|
||||
group := (val << 4) | (pos & 0x0F)
|
||||
return group, true
|
||||
}
|
||||
|
||||
paddingSet := make(map[byte]struct{})
|
||||
var padding []byte
|
||||
for drop := range xBits {
|
||||
for val := 0; val < 4; val++ {
|
||||
for pos := 0; pos < 16; pos++ {
|
||||
b := encodeBits(byte(val), byte(pos), drop)
|
||||
if bits.OnesCount8(b) >= 5 {
|
||||
if _, ok := paddingSet[b]; !ok {
|
||||
paddingSet[b] = struct{}{}
|
||||
padding = append(padding, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(padding, func(i, j int) bool { return padding[i] < padding[j] })
|
||||
if len(padding) == 0 {
|
||||
return nil, fmt.Errorf("custom table produced empty padding pool")
|
||||
}
|
||||
|
||||
return &byteLayout{
|
||||
name: fmt.Sprintf("custom(%s)", cleaned),
|
||||
hintMask: xMask,
|
||||
hintValue: xMask,
|
||||
padMarker: padding[0],
|
||||
paddingPool: padding,
|
||||
encodeHint: func(val, pos byte) byte {
|
||||
return encodeBits(val, pos, -1)
|
||||
},
|
||||
encodeGroup: func(group byte) byte {
|
||||
val := (group >> 4) & 0x03
|
||||
pos := group & 0x0F
|
||||
return encodeBits(val, pos, -1)
|
||||
},
|
||||
decodeGroup: decodeGroup,
|
||||
}, nil
|
||||
}
|
||||
332
transport/sudoku/obfs/sudoku/packed.go
Normal file
332
transport/sudoku/obfs/sudoku/packed.go
Normal file
@@ -0,0 +1,332 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
crypto_rand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// 每次从 RNG 获取批量随机数的缓存大小,减少 RNG 函数调用开销
|
||||
RngBatchSize = 128
|
||||
)
|
||||
|
||||
// 1. 使用 12字节->16组 的块处理优化 Write (减少循环开销)
|
||||
// 2. 使用浮点随机概率判断 Padding,与纯 Sudoku 保持流量特征一致
|
||||
// 3. Read 使用 copy 移动避免底层数组泄漏
|
||||
type PackedConn struct {
|
||||
net.Conn
|
||||
table *Table
|
||||
reader *bufio.Reader
|
||||
|
||||
// 读缓冲
|
||||
rawBuf []byte
|
||||
pendingData []byte // 解码后尚未被 Read 取走的字节
|
||||
|
||||
// 写缓冲与状态
|
||||
writeMu sync.Mutex
|
||||
writeBuf []byte
|
||||
bitBuf uint64 // 暂存的位数据
|
||||
bitCount int // 暂存的位数
|
||||
|
||||
// 读状态
|
||||
readBitBuf uint64
|
||||
readBits int
|
||||
|
||||
// 随机数与填充控制 - 使用浮点随机,与 Conn 一致
|
||||
rng *rand.Rand
|
||||
paddingRate float32 // 与 Conn 保持一致的随机概率模型
|
||||
padMarker byte
|
||||
padPool []byte
|
||||
}
|
||||
|
||||
func NewPackedConn(c net.Conn, table *Table, pMin, pMax int) *PackedConn {
|
||||
var seedBytes [8]byte
|
||||
if _, err := crypto_rand.Read(seedBytes[:]); err != nil {
|
||||
binary.BigEndian.PutUint64(seedBytes[:], uint64(rand.Int63()))
|
||||
}
|
||||
seed := int64(binary.BigEndian.Uint64(seedBytes[:]))
|
||||
localRng := rand.New(rand.NewSource(seed))
|
||||
|
||||
// 与 Conn 保持一致的 padding 概率计算
|
||||
min := float32(pMin) / 100.0
|
||||
rng := float32(pMax-pMin) / 100.0
|
||||
rate := min + localRng.Float32()*rng
|
||||
|
||||
pc := &PackedConn{
|
||||
Conn: c,
|
||||
table: table,
|
||||
reader: bufio.NewReaderSize(c, IOBufferSize),
|
||||
rawBuf: make([]byte, IOBufferSize),
|
||||
pendingData: make([]byte, 0, 4096),
|
||||
writeBuf: make([]byte, 0, 4096),
|
||||
rng: localRng,
|
||||
paddingRate: rate,
|
||||
}
|
||||
|
||||
pc.padMarker = table.layout.padMarker
|
||||
for _, b := range table.PaddingPool {
|
||||
if b != pc.padMarker {
|
||||
pc.padPool = append(pc.padPool, b)
|
||||
}
|
||||
}
|
||||
if len(pc.padPool) == 0 {
|
||||
pc.padPool = append(pc.padPool, pc.padMarker)
|
||||
}
|
||||
return pc
|
||||
}
|
||||
|
||||
// maybeAddPadding 内联辅助:根据浮点概率插入 padding
|
||||
func (pc *PackedConn) maybeAddPadding(out []byte) []byte {
|
||||
if pc.rng.Float32() < pc.paddingRate {
|
||||
out = append(out, pc.getPaddingByte())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Write 极致优化版 - 批量处理 12 字节
|
||||
func (pc *PackedConn) Write(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
pc.writeMu.Lock()
|
||||
defer pc.writeMu.Unlock()
|
||||
|
||||
// 1. 预分配内存,避免 append 导致的多次扩容
|
||||
// 预估:原数据 * 1.5 (4/3 + padding 余量)
|
||||
needed := len(p)*3/2 + 32
|
||||
if cap(pc.writeBuf) < needed {
|
||||
pc.writeBuf = make([]byte, 0, needed)
|
||||
}
|
||||
out := pc.writeBuf[:0]
|
||||
|
||||
i := 0
|
||||
n := len(p)
|
||||
|
||||
// 2. 头部对齐处理 (Slow Path)
|
||||
for pc.bitCount > 0 && i < n {
|
||||
out = pc.maybeAddPadding(out)
|
||||
b := p[i]
|
||||
i++
|
||||
pc.bitBuf = (pc.bitBuf << 8) | uint64(b)
|
||||
pc.bitCount += 8
|
||||
for pc.bitCount >= 6 {
|
||||
pc.bitCount -= 6
|
||||
group := byte(pc.bitBuf >> pc.bitCount)
|
||||
if pc.bitCount == 0 {
|
||||
pc.bitBuf = 0
|
||||
} else {
|
||||
pc.bitBuf &= (1 << pc.bitCount) - 1
|
||||
}
|
||||
out = pc.maybeAddPadding(out)
|
||||
out = append(out, pc.encodeGroup(group&0x3F))
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 极速批量处理 (Fast Path) - 每次处理 12 字节 → 生成 16 个编码组
|
||||
for i+11 < n {
|
||||
// 处理 4 组,每组 3 字节
|
||||
for batch := 0; batch < 4; batch++ {
|
||||
b1, b2, b3 := p[i], p[i+1], p[i+2]
|
||||
i += 3
|
||||
|
||||
g1 := (b1 >> 2) & 0x3F
|
||||
g2 := ((b1 & 0x03) << 4) | ((b2 >> 4) & 0x0F)
|
||||
g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03)
|
||||
g4 := b3 & 0x3F
|
||||
|
||||
// 每个组之前都有概率插入 padding
|
||||
out = pc.maybeAddPadding(out)
|
||||
out = append(out, pc.encodeGroup(g1))
|
||||
out = pc.maybeAddPadding(out)
|
||||
out = append(out, pc.encodeGroup(g2))
|
||||
out = pc.maybeAddPadding(out)
|
||||
out = append(out, pc.encodeGroup(g3))
|
||||
out = pc.maybeAddPadding(out)
|
||||
out = append(out, pc.encodeGroup(g4))
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 处理剩余的 3 字节块
|
||||
for i+2 < n {
|
||||
b1, b2, b3 := p[i], p[i+1], p[i+2]
|
||||
i += 3
|
||||
|
||||
g1 := (b1 >> 2) & 0x3F
|
||||
g2 := ((b1 & 0x03) << 4) | ((b2 >> 4) & 0x0F)
|
||||
g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03)
|
||||
g4 := b3 & 0x3F
|
||||
|
||||
out = pc.maybeAddPadding(out)
|
||||
out = append(out, pc.encodeGroup(g1))
|
||||
out = pc.maybeAddPadding(out)
|
||||
out = append(out, pc.encodeGroup(g2))
|
||||
out = pc.maybeAddPadding(out)
|
||||
out = append(out, pc.encodeGroup(g3))
|
||||
out = pc.maybeAddPadding(out)
|
||||
out = append(out, pc.encodeGroup(g4))
|
||||
}
|
||||
|
||||
// 5. 尾部处理 (Tail Path) - 处理剩余的 1 或 2 个字节
|
||||
for ; i < n; i++ {
|
||||
b := p[i]
|
||||
pc.bitBuf = (pc.bitBuf << 8) | uint64(b)
|
||||
pc.bitCount += 8
|
||||
for pc.bitCount >= 6 {
|
||||
pc.bitCount -= 6
|
||||
group := byte(pc.bitBuf >> pc.bitCount)
|
||||
if pc.bitCount == 0 {
|
||||
pc.bitBuf = 0
|
||||
} else {
|
||||
pc.bitBuf &= (1 << pc.bitCount) - 1
|
||||
}
|
||||
out = pc.maybeAddPadding(out)
|
||||
out = append(out, pc.encodeGroup(group&0x3F))
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 处理残留位
|
||||
if pc.bitCount > 0 {
|
||||
out = pc.maybeAddPadding(out)
|
||||
group := byte(pc.bitBuf << (6 - pc.bitCount))
|
||||
pc.bitBuf = 0
|
||||
pc.bitCount = 0
|
||||
out = append(out, pc.encodeGroup(group&0x3F))
|
||||
out = append(out, pc.padMarker)
|
||||
}
|
||||
|
||||
// 尾部可能添加 padding
|
||||
out = pc.maybeAddPadding(out)
|
||||
|
||||
// 发送数据
|
||||
if len(out) > 0 {
|
||||
_, err := pc.Conn.Write(out)
|
||||
pc.writeBuf = out[:0]
|
||||
return len(p), err
|
||||
}
|
||||
pc.writeBuf = out[:0]
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Flush 处理最后不足 6 bit 的情况
|
||||
func (pc *PackedConn) Flush() error {
|
||||
pc.writeMu.Lock()
|
||||
defer pc.writeMu.Unlock()
|
||||
|
||||
out := pc.writeBuf[:0]
|
||||
if pc.bitCount > 0 {
|
||||
group := byte(pc.bitBuf << (6 - pc.bitCount))
|
||||
pc.bitBuf = 0
|
||||
pc.bitCount = 0
|
||||
|
||||
out = append(out, pc.encodeGroup(group&0x3F))
|
||||
out = append(out, pc.padMarker)
|
||||
}
|
||||
|
||||
// 尾部随机添加 padding
|
||||
out = pc.maybeAddPadding(out)
|
||||
|
||||
if len(out) > 0 {
|
||||
_, err := pc.Conn.Write(out)
|
||||
pc.writeBuf = out[:0]
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read 优化版:减少切片操作,避免内存泄漏
|
||||
func (pc *PackedConn) Read(p []byte) (int, error) {
|
||||
// 1. 优先返回待处理区的数据
|
||||
if len(pc.pendingData) > 0 {
|
||||
n := copy(p, pc.pendingData)
|
||||
if n == len(pc.pendingData) {
|
||||
pc.pendingData = pc.pendingData[:0]
|
||||
} else {
|
||||
// 优化:移动剩余数据到数组头部,避免切片指向中间导致内存泄漏
|
||||
remaining := len(pc.pendingData) - n
|
||||
copy(pc.pendingData, pc.pendingData[n:])
|
||||
pc.pendingData = pc.pendingData[:remaining]
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// 2. 循环读取直到解出数据或出错
|
||||
for {
|
||||
nr, rErr := pc.reader.Read(pc.rawBuf)
|
||||
if nr > 0 {
|
||||
// 缓存频繁访问的变量
|
||||
rBuf := pc.readBitBuf
|
||||
rBits := pc.readBits
|
||||
padMarker := pc.padMarker
|
||||
layout := pc.table.layout
|
||||
|
||||
for _, b := range pc.rawBuf[:nr] {
|
||||
if !layout.isHint(b) {
|
||||
if b == padMarker {
|
||||
rBuf = 0
|
||||
rBits = 0
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
group, ok := layout.decodeGroup(b)
|
||||
if !ok {
|
||||
return 0, ErrInvalidSudokuMapMiss
|
||||
}
|
||||
|
||||
rBuf = (rBuf << 6) | uint64(group)
|
||||
rBits += 6
|
||||
|
||||
if rBits >= 8 {
|
||||
rBits -= 8
|
||||
val := byte(rBuf >> rBits)
|
||||
pc.pendingData = append(pc.pendingData, val)
|
||||
}
|
||||
}
|
||||
|
||||
pc.readBitBuf = rBuf
|
||||
pc.readBits = rBits
|
||||
}
|
||||
|
||||
if rErr != nil {
|
||||
if rErr == io.EOF {
|
||||
pc.readBitBuf = 0
|
||||
pc.readBits = 0
|
||||
}
|
||||
if len(pc.pendingData) > 0 {
|
||||
break
|
||||
}
|
||||
return 0, rErr
|
||||
}
|
||||
|
||||
if len(pc.pendingData) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 返回解码后的数据 - 优化:避免底层数组泄漏
|
||||
n := copy(p, pc.pendingData)
|
||||
if n == len(pc.pendingData) {
|
||||
pc.pendingData = pc.pendingData[:0]
|
||||
} else {
|
||||
remaining := len(pc.pendingData) - n
|
||||
copy(pc.pendingData, pc.pendingData[n:])
|
||||
pc.pendingData = pc.pendingData[:remaining]
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// getPaddingByte 从 Pool 中随机取 Padding 字节
|
||||
func (pc *PackedConn) getPaddingByte() byte {
|
||||
return pc.padPool[pc.rng.Intn(len(pc.padPool))]
|
||||
}
|
||||
|
||||
// encodeGroup 编码 6-bit 组
|
||||
func (pc *PackedConn) encodeGroup(group byte) byte {
|
||||
return pc.table.layout.encodeGroup(group)
|
||||
}
|
||||
153
transport/sudoku/obfs/sudoku/table.go
Normal file
153
transport/sudoku/obfs/sudoku/table.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"log"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidSudokuMapMiss = errors.New("INVALID_SUDOKU_MAP_MISS")
|
||||
)
|
||||
|
||||
type Table struct {
|
||||
EncodeTable [256][][4]byte
|
||||
DecodeMap map[uint32]byte
|
||||
PaddingPool []byte
|
||||
IsASCII bool // 标记当前模式
|
||||
layout *byteLayout
|
||||
}
|
||||
|
||||
// NewTable initializes the obfuscation tables with built-in layouts.
|
||||
// Equivalent to calling NewTableWithCustom(key, mode, "").
|
||||
func NewTable(key string, mode string) *Table {
|
||||
t, err := NewTableWithCustom(key, mode, "")
|
||||
if err != nil {
|
||||
log.Panicf("failed to build table: %v", err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// NewTableWithCustom initializes obfuscation tables using either predefined or custom layouts.
|
||||
// mode: "prefer_ascii" or "prefer_entropy". If a custom pattern is provided, ASCII mode still takes precedence.
|
||||
// The customPattern must contain 8 characters with exactly 2 x, 2 p, and 4 v (case-insensitive).
|
||||
func NewTableWithCustom(key string, mode string, customPattern string) (*Table, error) {
|
||||
start := time.Now()
|
||||
|
||||
layout, err := resolveLayout(mode, customPattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &Table{
|
||||
DecodeMap: make(map[uint32]byte),
|
||||
IsASCII: layout.name == "ascii",
|
||||
layout: layout,
|
||||
}
|
||||
t.PaddingPool = append(t.PaddingPool, layout.paddingPool...)
|
||||
|
||||
// 生成数独网格 (逻辑不变)
|
||||
allGrids := GenerateAllGrids()
|
||||
h := sha256.New()
|
||||
h.Write([]byte(key))
|
||||
seed := int64(binary.BigEndian.Uint64(h.Sum(nil)[:8]))
|
||||
rng := rand.New(rand.NewSource(seed))
|
||||
|
||||
shuffledGrids := make([]Grid, 288)
|
||||
copy(shuffledGrids, allGrids)
|
||||
rng.Shuffle(len(shuffledGrids), func(i, j int) {
|
||||
shuffledGrids[i], shuffledGrids[j] = shuffledGrids[j], shuffledGrids[i]
|
||||
})
|
||||
|
||||
// 预计算组合
|
||||
var combinations [][]int
|
||||
var combine func(int, int, []int)
|
||||
combine = func(s, k int, c []int) {
|
||||
if k == 0 {
|
||||
tmp := make([]int, len(c))
|
||||
copy(tmp, c)
|
||||
combinations = append(combinations, tmp)
|
||||
return
|
||||
}
|
||||
for i := s; i <= 16-k; i++ {
|
||||
c = append(c, i)
|
||||
combine(i+1, k-1, c)
|
||||
c = c[:len(c)-1]
|
||||
}
|
||||
}
|
||||
combine(0, 4, []int{})
|
||||
|
||||
// 构建映射表
|
||||
for byteVal := 0; byteVal < 256; byteVal++ {
|
||||
targetGrid := shuffledGrids[byteVal]
|
||||
for _, positions := range combinations {
|
||||
var currentHints [4]byte
|
||||
|
||||
// 1. 计算抽象提示 (Abstract Hints)
|
||||
// 我们先计算出 val 和 pos,后面再根据模式编码成 byte
|
||||
var rawParts [4]struct{ val, pos byte }
|
||||
|
||||
for i, pos := range positions {
|
||||
val := targetGrid[pos] // 1..4
|
||||
rawParts[i] = struct{ val, pos byte }{val, uint8(pos)}
|
||||
}
|
||||
|
||||
// 检查唯一性 (数独逻辑)
|
||||
matchCount := 0
|
||||
for _, g := range allGrids {
|
||||
match := true
|
||||
for _, p := range rawParts {
|
||||
if g[p.pos] != p.val {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
matchCount++
|
||||
if matchCount > 1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matchCount == 1 {
|
||||
// 唯一确定,生成最终编码字节
|
||||
for i, p := range rawParts {
|
||||
currentHints[i] = t.layout.encodeHint(p.val-1, p.pos)
|
||||
}
|
||||
|
||||
t.EncodeTable[byteVal] = append(t.EncodeTable[byteVal], currentHints)
|
||||
// 生成解码键 (需要对 Hints 进行排序以忽略传输顺序)
|
||||
key := packHintsToKey(currentHints)
|
||||
t.DecodeMap[key] = byte(byteVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Printf("[Init] Sudoku Tables initialized (%s) in %v", layout.name, time.Since(start))
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func packHintsToKey(hints [4]byte) uint32 {
|
||||
// Sorting network for 4 elements (Bubble sort unrolled)
|
||||
// Swap if a > b
|
||||
if hints[0] > hints[1] {
|
||||
hints[0], hints[1] = hints[1], hints[0]
|
||||
}
|
||||
if hints[2] > hints[3] {
|
||||
hints[2], hints[3] = hints[3], hints[2]
|
||||
}
|
||||
if hints[0] > hints[2] {
|
||||
hints[0], hints[2] = hints[2], hints[0]
|
||||
}
|
||||
if hints[1] > hints[3] {
|
||||
hints[1], hints[3] = hints[3], hints[1]
|
||||
}
|
||||
if hints[1] > hints[2] {
|
||||
hints[1], hints[2] = hints[2], hints[1]
|
||||
}
|
||||
|
||||
return uint32(hints[0])<<24 | uint32(hints[1])<<16 | uint32(hints[2])<<8 | uint32(hints[3])
|
||||
}
|
||||
38
transport/sudoku/obfs/sudoku/table_set.go
Normal file
38
transport/sudoku/obfs/sudoku/table_set.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package sudoku
|
||||
|
||||
import "fmt"
|
||||
|
||||
// TableSet is a small helper for managing multiple Sudoku tables (e.g. for per-connection rotation).
|
||||
// It is intentionally decoupled from the tunnel/app layers.
|
||||
type TableSet struct {
|
||||
Tables []*Table
|
||||
}
|
||||
|
||||
// NewTableSet builds one or more tables from key/mode and a list of custom X/P/V patterns.
|
||||
// If patterns is empty, it builds a single default table (customPattern="").
|
||||
func NewTableSet(key string, mode string, patterns []string) (*TableSet, error) {
|
||||
if len(patterns) == 0 {
|
||||
t, err := NewTableWithCustom(key, mode, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TableSet{Tables: []*Table{t}}, nil
|
||||
}
|
||||
|
||||
tables := make([]*Table, 0, len(patterns))
|
||||
for i, pattern := range patterns {
|
||||
t, err := NewTableWithCustom(key, mode, pattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build table[%d] (%q): %w", i, pattern, err)
|
||||
}
|
||||
tables = append(tables, t)
|
||||
}
|
||||
return &TableSet{Tables: tables}, nil
|
||||
}
|
||||
|
||||
func (ts *TableSet) Candidates() []*Table {
|
||||
if ts == nil {
|
||||
return nil
|
||||
}
|
||||
return ts.Tables
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"math/rand"
|
||||
"net"
|
||||
|
||||
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
||||
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||
)
|
||||
|
||||
// perm4 matches github.com/saba-futai/sudoku/pkg/obfs/sudoku perm4.
|
||||
|
||||
@@ -3,6 +3,7 @@ package sudoku
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
crand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -10,34 +11,24 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/saba-futai/sudoku/apis"
|
||||
"github.com/saba-futai/sudoku/pkg/crypto"
|
||||
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
||||
"github.com/metacubex/mihomo/transport/sudoku/crypto"
|
||||
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||
)
|
||||
|
||||
func tableCandidates(cfg *apis.ProtocolConfig) []*sudoku.Table {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
if len(cfg.Tables) > 0 {
|
||||
return cfg.Tables
|
||||
}
|
||||
if cfg.Table != nil {
|
||||
return []*sudoku.Table{cfg.Table}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pickClientTable(cfg *apis.ProtocolConfig) (*sudoku.Table, byte, error) {
|
||||
candidates := tableCandidates(cfg)
|
||||
func pickClientTable(cfg *ProtocolConfig) (*sudoku.Table, error) {
|
||||
candidates := cfg.tableCandidates()
|
||||
if len(candidates) == 0 {
|
||||
return nil, 0, fmt.Errorf("no table configured")
|
||||
return nil, fmt.Errorf("no table configured")
|
||||
}
|
||||
if len(candidates) == 1 {
|
||||
return candidates[0], 0, nil
|
||||
return candidates[0], nil
|
||||
}
|
||||
idx := int(randomByte()) % len(candidates)
|
||||
return candidates[idx], byte(idx), nil
|
||||
var b [1]byte
|
||||
if _, err := crand.Read(b[:]); err != nil {
|
||||
return nil, fmt.Errorf("random table pick failed: %w", err)
|
||||
}
|
||||
idx := int(b[0]) % len(candidates)
|
||||
return candidates[idx], nil
|
||||
}
|
||||
|
||||
type readOnlyConn struct {
|
||||
@@ -62,7 +53,7 @@ func drainBuffered(r *bufio.Reader) ([]byte, error) {
|
||||
return out, err
|
||||
}
|
||||
|
||||
func probeHandshakeBytes(probe []byte, cfg *apis.ProtocolConfig, table *sudoku.Table) error {
|
||||
func probeHandshakeBytes(probe []byte, cfg *ProtocolConfig, table *sudoku.Table) error {
|
||||
rc := &readOnlyConn{Reader: bytes.NewReader(probe)}
|
||||
_, obfsConn := buildServerObfsConn(rc, cfg, table, false)
|
||||
cConn, err := crypto.NewAEADConn(obfsConn, cfg.Key, cfg.AEADMethod)
|
||||
@@ -90,7 +81,7 @@ func probeHandshakeBytes(probe []byte, cfg *apis.ProtocolConfig, table *sudoku.T
|
||||
return nil
|
||||
}
|
||||
|
||||
func selectTableByProbe(r *bufio.Reader, cfg *apis.ProtocolConfig, tables []*sudoku.Table) (*sudoku.Table, []byte, error) {
|
||||
func selectTableByProbe(r *bufio.Reader, cfg *ProtocolConfig, tables []*sudoku.Table) (*sudoku.Table, []byte, error) {
|
||||
const (
|
||||
maxProbeBytes = 64 * 1024
|
||||
readChunk = 4 * 1024
|
||||
|
||||
@@ -3,7 +3,7 @@ package sudoku
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
||||
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||
)
|
||||
|
||||
// NewTablesWithCustomPatterns builds one or more obfuscation tables from x/v/p custom patterns.
|
||||
|
||||
@@ -200,7 +200,7 @@ func (m *maxAckHeightTracker) Update(
|
||||
// Compute how many extra bytes were delivered vs max bandwidth.
|
||||
extraBytesAcked := m.aggregationEpochBytes - expectedBytesAcked
|
||||
newEvent := extraAckedEvent{
|
||||
extraAcked: expectedBytesAcked,
|
||||
extraAcked: extraBytesAcked,
|
||||
bytesAcked: m.aggregationEpochBytes,
|
||||
timeDelta: aggregationDelta,
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
//
|
||||
|
||||
const (
|
||||
minBps = 65536 // 64 kbps
|
||||
minBps = 65536 // 64 KB/s
|
||||
|
||||
invalidPacketNumber = -1
|
||||
initialCongestionWindowPackets = 32
|
||||
@@ -543,7 +543,7 @@ func (b *bbrSender) bandwidthEstimate() Bandwidth {
|
||||
}
|
||||
|
||||
func (b *bbrSender) bandwidthForPacer() congestion.ByteCount {
|
||||
bps := congestion.ByteCount(float64(b.bandwidthEstimate()) * b.congestionWindowGain / float64(BytesPerSecond))
|
||||
bps := congestion.ByteCount(float64(b.PacingRate()) / float64(BytesPerSecond))
|
||||
if bps < minBps {
|
||||
// We need to make sure that the bandwidth value for pacer is never zero,
|
||||
// otherwise it will go into an edge case where HasPacingBudget = false
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user