Compare commits

...

42 Commits

Author SHA1 Message Date
wwqgtxx
11000dccd7 chore: add common/deque package 2026-01-16 11:05:15 +08:00
wwqgtxx
0818aa54aa chore: provider a common entrance for YAML package 2026-01-16 11:05:13 +08:00
wwqgtxx
edbfebeacd fix: CVE-2025-68121 for crypto/tls 2026-01-16 08:27:29 +08:00
saba-futai
06f5fbac06 feat: add path-root for sudoku (#2511) 2026-01-14 21:25:05 +08:00
Shaw
f38fc2020f feat: add grpc-user-agent to grpc-opts (#2512) 2026-01-14 21:02:09 +08:00
wwqgtxx
97bce45eba chore: deprecated global-client-fingerprint, please set client-fingerprint directly on the proxy instead 2026-01-14 10:40:26 +08:00
Davoyan
bc28cd486a doc: fix typo in config.yaml (#2459) 2026-01-14 09:01:18 +08:00
wwqgtxx
cdabd1e8b1 chore: update utls 2026-01-14 08:02:37 +08:00
Toby
c5b0f00bb2 fix: logic issues with BBR impl
98872a4f38
2026-01-12 13:34:59 +08:00
wwqgtxx
c128d23dec chore: update quic-go to 0.59.0 2026-01-12 12:48:18 +08:00
wwqgtxx
ee37a353d0 fix: incorrect timestamp conversion in brutal 2026-01-12 12:45:52 +08:00
wwqgtxx
0cf37de1a8 chore: better time storage in rule wrapper 2026-01-12 00:50:55 +08:00
potoo0
ae6069c178 chore: moving rules disabled and hit/miss counts data to extra for restful api (#2503) 2026-01-11 21:11:38 +08:00
wwqgtxx
c8e33a4347 chore: decrease rule wrapper memory usage 2026-01-11 20:57:28 +08:00
potoo0
19a6b5d6f7 feat: support rule disabling and hit/miss count/at tracking in restful api (#2502) 2026-01-11 19:37:08 +08:00
wwqgtxx
efb800866e chore: update quic-go to 0.58.1 2026-01-11 17:19:53 +08:00
wwqgtxx
94c8d60f72 chore: simplified logic rule parsing 2026-01-08 23:42:01 +08:00
saba-futai
0f2baca2de chore: refactored the implementation of suduko mux (#2486) 2026-01-07 00:25:33 +08:00
wwqgtxx
b18a33552c chore: remove unused pointer in rules implements 2026-01-06 09:29:09 +08:00
wwqgtxx
487de9b548 feat: add PROCESS-NAME-WILDCARD and PROCESS-PATH-WILDCARD 2026-01-06 08:52:06 +08:00
enfein
1a6230ec03 chore: update mieru version (#2484)
Fix https://github.com/enfein/mieru/issues/247
2026-01-06 07:48:46 +08:00
wwqgtxx
e6bf56b9af fix: os.(*Process).Wait not working on Windows7 2026-01-05 20:26:19 +08:00
wwqgtxx
0ad9ac325a feat: support aes-128-gcm, ratelimit and framesize for kcptun 2026-01-05 12:25:30 +08:00
saba-futai
d6b1263236 feat: support http-mask-multiplex for suduko (#2482) 2026-01-04 22:24:42 +08:00
wwqgtxx
4d7670339b feat: all dns client support disable-qtype-<int> params 2026-01-02 22:43:58 +08:00
wwqgtxx
0cffc8d76d chore: revert "chore: update quic-go to 0.58.0"
This reverts commit 64015b7634.
2026-01-02 17:09:40 +08:00
wwqgtxx
1f8bee9710 chore: force to disable mptcp for tproxy 2025-12-31 08:43:23 +08:00
wwqgtxx
eb30d3f331 chore: add a code comment for tproxy listener 2025-12-31 02:07:52 +08:00
wwqgtxx
10f4bebdfa fix: only clear dstIP if it is confirmed to be a fake IP 2025-12-30 17:16:09 +08:00
David
06387d5045 feat: support fake-ip-filter-mode: rule mode (#2469) 2025-12-29 08:14:09 +08:00
wwqgtxx
c393e917eb fix: gvisor compatibility on go1.26 2025-12-27 17:57:30 +08:00
wwqgtxx
4f0a6fa117 fix: gvisor panic 2025-12-27 17:16:35 +08:00
wwqgtxx
4f9bfd216f chore: add some comments for the finalizer 2025-12-27 16:38:58 +08:00
joshua
498f81aad3 feat: add header support for rule provider (#2463) 2025-12-24 23:10:38 +08:00
wwqgtxx
9168bee6b7 chore: align internal logic 2025-12-24 18:26:55 +08:00
HolgerHuo
e6c0e3b19c fix: handle geoip:lan when GetRecodeSize() (#2460) 2025-12-24 08:34:19 +08:00
wwqgtxx
287f9e5185 chore: temporarily skip mieru inbound test in go1.26 on windows 2025-12-23 23:49:19 +08:00
wwqgtxx
c456370f4f fix: missing context cancel in pullLoop 2025-12-23 23:26:05 +08:00
wwqgtxx
10ef29f5cd chore: apply global ca in sudoku code 2025-12-23 23:15:52 +08:00
wwqgtxx
85ba7f6a0a chore: change import paths in sudoku code 2025-12-23 23:14:39 +08:00
saba-futai
7daf37bc15 feat: support http-mask-mode, http-mask-tls and http-mask-host for sudoku (#2456) 2025-12-23 23:08:38 +08:00
wwqgtxx
64015b7634 chore: update quic-go to 0.58.0 2025-12-22 17:29:28 +08:00
101 changed files with 7467 additions and 420 deletions

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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,
})

View File

@@ -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
}
}

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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 (

View File

@@ -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
View 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
View 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)
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -1,12 +0,0 @@
//go:build !go1.21
package dialer
import (
"net"
)
const multipathTCPAvailable = false
func setMultiPathTCP(dialer *net.Dialer) {
}

View File

@@ -1,11 +0,0 @@
//go:build go1.21
package dialer
import "net"
const multipathTCPAvailable = true
func setMultiPathTCP(dialer *net.Dialer) {
dialer.SetMultipathTCP(true)
}

View File

@@ -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()

View File

@@ -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

View 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
}

View 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()
}

View File

@@ -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 {

View File

@@ -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"
}

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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、autostream/poll/auto 支持走 CDN/反代
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效true 强制 httpsfalse 强制 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、autostream/poll/auto 支持走 CDN/反代
# path-root: "" # 可选HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload

19
go.mod
View File

@@ -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
View File

@@ -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=

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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"`

View File

@@ -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,
},

View File

@@ -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")
})

View File

@@ -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()

View File

@@ -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)
})
}
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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":

View File

@@ -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:

View File

@@ -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

View File

@@ -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
View 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
}

View File

@@ -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)

View File

@@ -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))

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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
View 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
}

View 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)
}

View 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())
}

View File

@@ -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{}

View File

@@ -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])
}

View File

@@ -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)}

View File

@@ -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
}

View 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()
}

View 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)
}
}

View 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()
}

View 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
}

View 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 }

View 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")
}
}

View 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)
}

View 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: 4KB10MB. 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
}
}
}

View 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
}

File diff suppressed because it is too large Load Diff

View 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
}

View 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
}

View 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
}

View 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)
}

View 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])
}

View 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
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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,
}

View File

@@ -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