Skip to content

Commit 0f2baca

Browse files
authored
chore: refactored the implementation of suduko mux (#2486)
1 parent b18a335 commit 0f2baca

8 files changed

Lines changed: 969 additions & 326 deletions

File tree

adapter/outbound/sudoku.go

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ type Sudoku struct {
1919
option *SudokuOption
2020
baseConf sudoku.ProtocolConfig
2121

22+
httpMaskMu sync.Mutex
23+
httpMaskClient *sudoku.HTTPMaskTunnelClient
24+
2225
muxMu sync.Mutex
2326
muxClient *sudoku.MultiplexClient
2427
muxBackoffUntil time.Time
@@ -40,7 +43,7 @@ type SudokuOption struct {
4043
HTTPMaskMode string `proxy:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
4144
HTTPMaskTLS bool `proxy:"http-mask-tls,omitempty"` // only for http-mask-mode stream/poll/auto
4245
HTTPMaskHost string `proxy:"http-mask-host,omitempty"` // optional Host/SNI override (domain or domain:port)
43-
HTTPMaskMultiplex string `proxy:"http-mask-multiplex,omitempty"` // "off" (default), "auto", "on"
46+
HTTPMaskMultiplex string `proxy:"http-mask-multiplex,omitempty"` // "off" (default), "auto" (reuse h1/h2), "on" (single tunnel, multi-target)
4447
CustomTable string `proxy:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
4548
CustomTables []string `proxy:"custom-tables,omitempty"` // optional table rotation patterns, overrides custom-table when non-empty
4649
}
@@ -53,18 +56,12 @@ func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
5356
}
5457

5558
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
56-
if !cfg.DisableHTTPMask && muxMode != "off" {
57-
shouldTry := muxMode == "on" || (muxMode == "auto" && httpTunnelModeEnabled(cfg.HTTPMaskMode))
58-
if shouldTry {
59-
stream, muxErr := s.dialMultiplex(ctx, cfg.TargetAddress, muxMode)
60-
if muxErr == nil {
61-
return NewConn(stream, s), nil
62-
}
63-
if muxMode != "auto" {
64-
return nil, muxErr
65-
}
66-
s.noteMuxFailure(muxMode, muxErr)
59+
if muxMode == "on" && !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
60+
stream, muxErr := s.dialMultiplex(ctx, cfg.TargetAddress, muxMode)
61+
if muxErr == nil {
62+
return NewConn(stream, s), nil
6763
}
64+
return nil, muxErr
6865
}
6966

7067
c, err := s.dialAndHandshake(ctx, cfg)
@@ -229,6 +226,7 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
229226

230227
func (s *Sudoku) Close() error {
231228
s.resetMuxClient()
229+
s.resetHTTPMaskClient()
232230
return s.Base.Close()
233231
}
234232

@@ -261,7 +259,17 @@ func (s *Sudoku) dialAndHandshake(ctx context.Context, cfg *sudoku.ProtocolConfi
261259

262260
var c net.Conn
263261
if !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
264-
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext)
262+
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
263+
switch muxMode {
264+
case "auto", "on":
265+
client, errX := s.getOrCreateHTTPMaskClient(cfg)
266+
if errX != nil {
267+
return nil, errX
268+
}
269+
c, err = client.Dial(ctx)
270+
default:
271+
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext)
272+
}
265273
}
266274
if c == nil && err == nil {
267275
c, err = s.dialer.DialContext(ctx, "tcp", s.addr)
@@ -380,3 +388,35 @@ func (s *Sudoku) resetMuxClient() {
380388
s.muxClient = nil
381389
}
382390
}
391+
392+
func (s *Sudoku) getOrCreateHTTPMaskClient(cfg *sudoku.ProtocolConfig) (*sudoku.HTTPMaskTunnelClient, error) {
393+
if s == nil {
394+
return nil, fmt.Errorf("nil adapter")
395+
}
396+
if cfg == nil {
397+
return nil, fmt.Errorf("config is required")
398+
}
399+
400+
s.httpMaskMu.Lock()
401+
defer s.httpMaskMu.Unlock()
402+
403+
if s.httpMaskClient != nil {
404+
return s.httpMaskClient, nil
405+
}
406+
407+
c, err := sudoku.NewHTTPMaskTunnelClient(cfg.ServerAddress, cfg, s.dialer.DialContext)
408+
if err != nil {
409+
return nil, err
410+
}
411+
s.httpMaskClient = c
412+
return c, nil
413+
}
414+
415+
func (s *Sudoku) resetHTTPMaskClient() {
416+
s.httpMaskMu.Lock()
417+
defer s.httpMaskMu.Unlock()
418+
if s.httpMaskClient != nil {
419+
s.httpMaskClient.CloseIdleConnections()
420+
s.httpMaskClient = nil
421+
}
422+
}

docs/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1068,7 +1068,7 @@ proxies: # socks5
10681068
# http-mask-mode: legacy # 可选:legacy(默认)、stream、poll、auto;stream/poll/auto 支持走 CDN/反代
10691069
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效;true 强制 https;false 强制 http(不会根据端口自动推断)
10701070
# http-mask-host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 http-mask-mode 为 stream/poll/auto 时生效
1071-
# http-mask-multiplex: off # 可选:off(默认)、auto、on;复用单条隧道并在其内多路复用多个目标连接
1071+
# http-mask-multiplex: off # 可选:off(默认)、auto(复用 h1.1 keep-alive / h2 连接,减少每次建链 RTT)、on(单条隧道内多路复用多个目标连接;仅在 http-mask-mode=stream/poll/auto 生效)
10721072
enable-pure-downlink: false # 是否启用混淆下行,false的情况下能在保证数据安全的前提下极大提升下行速度,与服务端端保持相同(如果此处为false,则要求aead不可为none)
10731073

10741074
# anytls

transport/sudoku/config.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,10 @@ type ProtocolConfig struct {
5858
// HTTPMaskHost optionally overrides the HTTP Host header / SNI host for HTTP tunnel modes (client-side).
5959
HTTPMaskHost string
6060

61-
// HTTPMaskMultiplex controls whether the client reuses a single (HTTP-masked) tunnel connection and
62-
// opens multiple logical target streams inside it (reduces RTT for subsequent connections).
63-
// Values: "off" / "auto" / "on".
61+
// HTTPMaskMultiplex controls multiplex behavior when HTTPMask tunnel modes are enabled:
62+
// - "off": disable reuse; each Dial establishes its own HTTPMask tunnel
63+
// - "auto": reuse underlying HTTP connections across multiple tunnel dials (HTTP/1.1 keep-alive / HTTP/2)
64+
// - "on": enable "single tunnel, multi-target" mux (Sudoku-level multiplex; Dial behaves like "auto" otherwise)
6465
HTTPMaskMultiplex string
6566
}
6667

transport/sudoku/httpmask_tunnel.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,59 @@ func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *Protocol
8383
Mode: cfg.HTTPMaskMode,
8484
TLSEnabled: cfg.HTTPMaskTLSEnabled,
8585
HostOverride: cfg.HTTPMaskHost,
86+
Multiplex: cfg.HTTPMaskMultiplex,
8687
DialContext: dial,
8788
})
8889
}
90+
91+
type HTTPMaskTunnelClient struct {
92+
mode string
93+
client *httpmask.TunnelClient
94+
}
95+
96+
func NewHTTPMaskTunnelClient(serverAddress string, cfg *ProtocolConfig, dial TunnelDialer) (*HTTPMaskTunnelClient, error) {
97+
if cfg == nil {
98+
return nil, fmt.Errorf("config is required")
99+
}
100+
if cfg.DisableHTTPMask {
101+
return nil, fmt.Errorf("http mask is disabled")
102+
}
103+
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
104+
case "stream", "poll", "auto":
105+
default:
106+
return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode)
107+
}
108+
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMultiplex)) {
109+
case "auto", "on":
110+
default:
111+
return nil, fmt.Errorf("http-mask-multiplex=%q does not enable reuse", cfg.HTTPMaskMultiplex)
112+
}
113+
114+
c, err := httpmask.NewTunnelClient(serverAddress, httpmask.TunnelClientOptions{
115+
TLSEnabled: cfg.HTTPMaskTLSEnabled,
116+
HostOverride: cfg.HTTPMaskHost,
117+
DialContext: dial,
118+
})
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
return &HTTPMaskTunnelClient{
124+
mode: cfg.HTTPMaskMode,
125+
client: c,
126+
}, nil
127+
}
128+
129+
func (c *HTTPMaskTunnelClient) Dial(ctx context.Context) (net.Conn, error) {
130+
if c == nil || c.client == nil {
131+
return nil, fmt.Errorf("nil httpmask tunnel client")
132+
}
133+
return c.client.DialTunnel(ctx, c.mode)
134+
}
135+
136+
func (c *HTTPMaskTunnelClient) CloseIdleConnections() {
137+
if c == nil || c.client == nil {
138+
return
139+
}
140+
c.client.CloseIdleConnections()
141+
}

transport/sudoku/multiplex.go

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package sudoku
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"net"
78
"strings"
8-
"time"
99

1010
"github.com/metacubex/mihomo/transport/sudoku/multiplex"
1111
)
@@ -46,26 +46,19 @@ func (c *MultiplexClient) Dial(ctx context.Context, targetAddress string) (net.C
4646
return nil, fmt.Errorf("target address cannot be empty")
4747
}
4848

49-
stream, err := c.sess.OpenStream()
49+
addrBuf, err := EncodeAddress(targetAddress)
5050
if err != nil {
51-
return nil, err
51+
return nil, fmt.Errorf("encode target address failed: %w", err)
5252
}
5353

54-
if deadline, ok := ctx.Deadline(); ok {
55-
_ = stream.SetWriteDeadline(deadline)
56-
defer stream.SetWriteDeadline(time.Time{})
54+
if ctx != nil && ctx.Err() != nil {
55+
return nil, ctx.Err()
5756
}
5857

59-
addrBuf, err := EncodeAddress(targetAddress)
58+
stream, err := c.sess.OpenStream(addrBuf)
6059
if err != nil {
61-
_ = stream.Close()
62-
return nil, fmt.Errorf("encode target address failed: %w", err)
63-
}
64-
if _, err := stream.Write(addrBuf); err != nil {
65-
_ = stream.Close()
66-
return nil, fmt.Errorf("send target address failed: %w", err)
60+
return nil, err
6761
}
68-
6962
return stream, nil
7063
}
7164

@@ -114,18 +107,21 @@ func (s *MultiplexServer) AcceptStream() (net.Conn, error) {
114107
if s == nil || s.sess == nil {
115108
return nil, fmt.Errorf("nil session")
116109
}
117-
return s.sess.AcceptStream()
110+
c, _, err := s.sess.AcceptStream()
111+
return c, err
118112
}
119113

120-
// AcceptTCP accepts a multiplex stream and reads the target address preface, returning the stream positioned at
121-
// application data.
114+
// AcceptTCP accepts a multiplex stream and returns the target address declared in the open frame.
122115
func (s *MultiplexServer) AcceptTCP() (net.Conn, string, error) {
123-
stream, err := s.AcceptStream()
116+
if s == nil || s.sess == nil {
117+
return nil, "", fmt.Errorf("nil session")
118+
}
119+
stream, payload, err := s.sess.AcceptStream()
124120
if err != nil {
125121
return nil, "", err
126122
}
127123

128-
target, err := DecodeAddress(stream)
124+
target, err := DecodeAddress(bytes.NewReader(payload))
129125
if err != nil {
130126
_ = stream.Close()
131127
return nil, "", err
@@ -147,4 +143,3 @@ func (s *MultiplexServer) IsClosed() bool {
147143
}
148144
return s.sess.IsClosed()
149145
}
150-

transport/sudoku/multiplex/mux.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package multiplex
2+
3+
import (
4+
"fmt"
5+
"io"
6+
)
7+
8+
const (
9+
// MagicByte marks a Sudoku tunnel connection that will switch into multiplex mode.
10+
// It is sent after the Sudoku handshake + downlink mode byte.
11+
//
12+
// Keep it distinct from UoTMagicByte and address type bytes.
13+
MagicByte byte = 0xED
14+
Version byte = 0x01
15+
)
16+
17+
func WritePreface(w io.Writer) error {
18+
if w == nil {
19+
return fmt.Errorf("nil writer")
20+
}
21+
_, err := w.Write([]byte{MagicByte, Version})
22+
return err
23+
}
24+
25+
func ReadVersion(r io.Reader) (byte, error) {
26+
var b [1]byte
27+
if _, err := io.ReadFull(r, b[:]); err != nil {
28+
return 0, err
29+
}
30+
return b[0], nil
31+
}
32+
33+
func ValidateVersion(v byte) error {
34+
if v != Version {
35+
return fmt.Errorf("unsupported multiplex version: %d", v)
36+
}
37+
return nil
38+
}
39+

0 commit comments

Comments
 (0)