Compare commits

...

26 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
74 changed files with 3746 additions and 723 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

@@ -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,12 +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"`
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)
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
@@ -45,40 +56,20 @@ func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
return nil, err
}
var c net.Conn
if !cfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext)
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
}
}
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)
return nil, muxErr
}
defer func() {
safeConnClose(c, err)
}()
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
handshakeCfg := *cfg
if !handshakeCfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(handshakeCfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
handshakeCfg.DisableHTTPMask = true
}
}
c, err = sudoku.ClientHandshakeWithOptions(c, &handshakeCfg, 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 {
@@ -86,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)
}
@@ -104,37 +94,7 @@ func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
return nil, err
}
var c net.Conn
if !cfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext)
}
}
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)
}
handshakeCfg := *cfg
if !handshakeCfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(handshakeCfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
handshakeCfg.DisableHTTPMask = true
}
}
c, err = sudoku.ClientHandshakeWithOptions(c, &handshakeCfg, sudoku.ClientHandshakeOptions{HTTPMaskStrategy: s.option.HTTPMaskStrategy})
c, err := s.dialAndHandshake(ctx, cfg)
if err != nil {
return nil, err
}
@@ -224,10 +184,15 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
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 {
return nil, fmt.Errorf("build table(s) failed: %w", err)
@@ -260,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

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

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

@@ -558,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
@@ -577,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
@@ -667,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
@@ -755,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
@@ -823,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
@@ -1066,7 +1072,8 @@ proxies: # socks5
# 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 时生效
# http-mask-strategy: random # 可选random默认、post、websocket仅 legacy 下生效
# 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
@@ -1369,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
@@ -1404,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)
@@ -1423,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
@@ -1613,6 +1622,7 @@ listeners:
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

14
go.mod
View File

@@ -7,7 +7,7 @@ require (
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
@@ -24,24 +24,24 @@ 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.58.1-0.20251222092318-72a81ab195ec
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

37
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=
@@ -106,27 +106,26 @@ 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.58.1-0.20251222092318-72a81ab195ec h1:5ePGO2Xht06fpwjNIzfY5XS+82xwDHHx4xGbqgLbxjA=
github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk=
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=
@@ -183,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=
@@ -245,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

@@ -22,6 +22,7 @@ type SudokuServer struct {
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

@@ -24,6 +24,7 @@ type SudokuOption struct {
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"`
@@ -63,6 +64,7 @@ func NewSudoku(options *SudokuOption) (*Sudoku, error) {
CustomTables: options.CustomTables,
DisableHTTPMask: options.DisableHTTPMask,
HTTPMaskMode: options.HTTPMaskMode,
PathRoot: strings.TrimSpace(options.PathRoot),
}
serverConf.MuxOption = options.MuxOption.Build()

View File

@@ -2,7 +2,6 @@ package inbound_test
import (
"net/netip"
"runtime"
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
@@ -167,10 +166,6 @@ func TestInboundSudoku_CustomTable(t *testing.T) {
}
func TestInboundSudoku_HTTPMaskMode(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("temporarily skipped on windows due to intermittent failures; tracked in PR")
}
key := "test_key_http_mask_mode"
for _, mode := range []string{"legacy", "stream", "poll", "auto"} {

View File

@@ -78,6 +78,26 @@ func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbou
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 {
@@ -209,6 +229,7 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
HandshakeTimeoutSeconds: handshakeTimeout,
DisableHTTPMask: config.DisableHTTPMask,
HTTPMaskMode: config.HTTPMaskMode,
HTTPMaskPathRoot: strings.TrimSpace(config.PathRoot),
}
if len(tables) == 1 {
protoConf.Table = tables[0]

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

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

View File

@@ -57,6 +57,16 @@ type ProtocolConfig struct {
// 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 {
@@ -103,6 +113,29 @@ func (c *ProtocolConfig) Validate() error {
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
}
@@ -127,6 +160,7 @@ func DefaultConfig() *ProtocolConfig {
EnablePureDownlink: true,
HandshakeTimeoutSeconds: 5,
HTTPMaskMode: "legacy",
HTTPMaskMultiplex: "off",
}
}

View File

@@ -2,9 +2,9 @@ package sudoku
import (
"bufio"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"net"
@@ -23,12 +23,17 @@ 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 {
@@ -147,7 +152,17 @@ func buildServerObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table,
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
}
@@ -198,12 +213,12 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *ProtocolConfig, opt Clien
}
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
}
@@ -215,9 +230,6 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *ProtocolConfig, opt Clien
}
handshake := buildHandshakePayload(cfg.Key)
if len(cfg.tableCandidates()) > 1 {
handshake[15] = tableID
}
if _, err := cConn.Write(handshake[:]); err != nil {
cConn.Close()
return nil, fmt.Errorf("send handshake failed: %w", err)
@@ -280,6 +292,7 @@ func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (*ServerSession, err
return nil, fmt.Errorf("timestamp skew detected")
}
userHash := userHashFromHandshake(handshakeBuf[:])
sConn.StopRecording()
modeBuf := []byte{0}
@@ -298,6 +311,11 @@ func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (*ServerSession, err
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 {
@@ -309,7 +327,7 @@ func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (*ServerSession, err
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}
@@ -322,9 +340,10 @@ func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (*ServerSession, err
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
}
@@ -356,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

@@ -7,6 +7,7 @@ import (
"math/rand"
"net"
"strconv"
"strings"
"sync"
"time"
@@ -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

@@ -23,7 +23,11 @@ func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer {
if !cfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{Mode: cfg.HTTPMaskMode})
ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{
Mode: cfg.HTTPMaskMode,
PathRoot: cfg.HTTPMaskPathRoot,
AuthKey: ClientAEADSeed(cfg.Key),
})
}
}
return &HTTPMaskTunnelServer{cfg: cfg, ts: ts}
@@ -67,7 +71,7 @@ func (s *HTTPMaskTunnelServer) WrapConn(rawConn net.Conn) (handshakeConn net.Con
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) (net.Conn, error) {
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")
}
@@ -83,6 +87,71 @@ func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *Protocol
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

@@ -154,7 +154,7 @@ func TestHTTPMaskTunnel_Stream_TCPRoundTrip(t *testing.T) {
clientCfg.ServerAddress = addr
clientCfg.HTTPMaskHost = "example.com"
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
@@ -225,7 +225,7 @@ func TestHTTPMaskTunnel_Poll_UoTRoundTrip(t *testing.T) {
clientCfg := *serverCfg
clientCfg.ServerAddress = addr
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
@@ -287,7 +287,7 @@ func TestHTTPMaskTunnel_Auto_TCPRoundTrip(t *testing.T) {
clientCfg := *serverCfg
clientCfg.ServerAddress = addr
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
@@ -331,13 +331,13 @@ func TestHTTPMaskTunnel_Validation(t *testing.T) {
cfg.DisableHTTPMask = true
cfg.HTTPMaskMode = "stream"
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext); err == nil {
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); err == nil {
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext, nil); err == nil {
t.Fatalf("expected error for legacy mode")
}
}
@@ -385,7 +385,7 @@ func TestHTTPMaskTunnel_Soak_Concurrent(t *testing.T) {
clientCfg.ServerAddress = addr
clientCfg.HTTPMaskHost = strings.TrimSpace(clientCfg.HTTPMaskHost)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
if err != nil {
runErr <- fmt.Errorf("dial: %w", err)
return

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

@@ -129,11 +129,17 @@ func appendCommonHeaders(buf []byte, host string, r *rand.Rand) []byte {
// 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 := paths[r.Intn(len(paths))]
path := joinPathRoot(pathRoot, paths[r.Intn(len(paths))])
ctype := contentTypes[r.Intn(len(contentTypes))]
// Use buffer pool

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

@@ -20,7 +20,11 @@ type byteLayout struct {
}
func (l *byteLayout) isHint(b byte) bool {
return (b & l.hintMask) == l.hintValue
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.
@@ -53,12 +57,25 @@ func newASCIILayout() *byteLayout {
padMarker: 0x3F,
paddingPool: padding,
encodeHint: func(val, pos byte) byte {
return 0x40 | ((val & 0x03) << 4) | (pos & 0x0F)
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 {
return 0x40 | (group & 0x3F)
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
}

View File

@@ -3,6 +3,7 @@ package sudoku
import (
"bufio"
"bytes"
crand "crypto/rand"
"encoding/binary"
"errors"
"fmt"
@@ -14,16 +15,20 @@ import (
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
func pickClientTable(cfg *ProtocolConfig) (*sudoku.Table, byte, error) {
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 {

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