feat: sudoku support ws transport (#2589)

This commit is contained in:
saba-futai
2026-03-01 10:22:53 +08:00
committed by GitHub
parent dda1d525c1
commit 9033717190
33 changed files with 2331 additions and 581 deletions

View File

@@ -23,6 +23,7 @@ type SudokuServer struct {
DisableHTTPMask bool `json:"disable-http-mask,omitempty"`
HTTPMaskMode string `json:"http-mask-mode,omitempty"`
PathRoot string `json:"path-root,omitempty"`
Fallback string `json:"fallback,omitempty"`
// mihomo private extension (not the part of standard Sudoku protocol)
MuxOption sing.MuxOption `json:"mux-option,omitempty"`

View File

@@ -13,23 +13,31 @@ import (
type SudokuOption struct {
BaseOption
Key string `inbound:"key"`
AEADMethod string `inbound:"aead-method,omitempty"`
PaddingMin *int `inbound:"padding-min,omitempty"`
PaddingMax *int `inbound:"padding-max,omitempty"`
TableType string `inbound:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
HandshakeTimeoutSecond *int `inbound:"handshake-timeout,omitempty"`
EnablePureDownlink *bool `inbound:"enable-pure-downlink,omitempty"`
CustomTable string `inbound:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
CustomTables []string `inbound:"custom-tables,omitempty"`
DisableHTTPMask bool `inbound:"disable-http-mask,omitempty"`
HTTPMaskMode string `inbound:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
PathRoot string `inbound:"path-root,omitempty"` // optional first-level path prefix for HTTP tunnel endpoints
Key string `inbound:"key"`
AEADMethod string `inbound:"aead-method,omitempty"`
PaddingMin *int `inbound:"padding-min,omitempty"`
PaddingMax *int `inbound:"padding-max,omitempty"`
TableType string `inbound:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
HandshakeTimeoutSecond *int `inbound:"handshake-timeout,omitempty"`
EnablePureDownlink *bool `inbound:"enable-pure-downlink,omitempty"`
CustomTable string `inbound:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
CustomTables []string `inbound:"custom-tables,omitempty"`
DisableHTTPMask bool `inbound:"disable-http-mask,omitempty"`
HTTPMaskMode string `inbound:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
PathRoot string `inbound:"path-root,omitempty"` // optional first-level path prefix for HTTP tunnel endpoints
Fallback string `inbound:"fallback,omitempty"`
HTTPMaskOptions *SudokuHTTPMaskOptions `inbound:"httpmask,omitempty"`
// mihomo private extension (not the part of standard Sudoku protocol)
MuxOption MuxOption `inbound:"mux-option,omitempty"`
}
type SudokuHTTPMaskOptions struct {
Disable bool `inbound:"disable,omitempty"`
Mode string `inbound:"mode,omitempty"`
PathRoot string `inbound:"path_root,omitempty"`
}
func (o SudokuOption) Equal(config C.InboundConfig) bool {
return optionToString(o) == optionToString(config)
}
@@ -65,6 +73,16 @@ func NewSudoku(options *SudokuOption) (*Sudoku, error) {
DisableHTTPMask: options.DisableHTTPMask,
HTTPMaskMode: options.HTTPMaskMode,
PathRoot: strings.TrimSpace(options.PathRoot),
Fallback: strings.TrimSpace(options.Fallback),
}
if hm := options.HTTPMaskOptions; hm != nil {
serverConf.DisableHTTPMask = hm.Disable
if hm.Mode != "" {
serverConf.HTTPMaskMode = hm.Mode
}
if pr := strings.TrimSpace(hm.PathRoot); pr != "" {
serverConf.PathRoot = pr
}
}
serverConf.MuxOption = options.MuxOption.Build()

View File

@@ -168,16 +168,17 @@ func TestInboundSudoku_CustomTable(t *testing.T) {
func TestInboundSudoku_HTTPMaskMode(t *testing.T) {
key := "test_key_http_mask_mode"
for _, mode := range []string{"legacy", "stream", "poll", "auto"} {
for _, mode := range []string{"ws", "stream", "poll", "auto"} {
mode := mode
t.Run(mode, func(t *testing.T) {
inboundOptions := inbound.SudokuOption{
Key: key,
HTTPMaskMode: mode,
}
httpMask := true
outboundOptions := outbound.SudokuOption{
Key: key,
HTTPMask: true,
HTTPMask: &httpMask,
HTTPMaskMode: mode,
}
testInboundSudoku(t, inboundOptions, outboundOptions)

View File

@@ -1,16 +1,19 @@
package sudoku
import (
"bytes"
"errors"
"io"
"net"
"strings"
"time"
"github.com/metacubex/mihomo/adapter/inbound"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/utils"
C "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/inner"
"github.com/metacubex/mihomo/listener/sing"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/transport/socks5"
@@ -23,6 +26,7 @@ type Listener struct {
closed bool
protoConf sudoku.ProtocolConfig
tunnelSrv *sudoku.HTTPMaskTunnelServer
fallback string
handler *sing.ListenerHandler
}
@@ -49,12 +53,19 @@ func (l *Listener) Close() error {
}
func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
log.Debugln("[Sudoku] accepted %s", conn.RemoteAddr())
handshakeConn := conn
handshakeCfg := &l.protoConf
closeConns := func() {
_ = handshakeConn.Close()
if handshakeConn != conn {
_ = conn.Close()
}
}
if l.tunnelSrv != nil {
c, cfg, done, err := l.tunnelSrv.WrapConn(conn)
if err != nil {
_ = conn.Close()
closeConns()
return
}
if done {
@@ -68,9 +79,43 @@ func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbou
}
}
session, err := sudoku.ServerHandshake(handshakeConn, handshakeCfg)
if l.fallback != "" {
if r, ok := handshakeConn.(interface{ IsHTTPMaskRejected() bool }); ok && r.IsHTTPMaskRejected() {
fb, err := inner.HandleTcp(tunnel, l.fallback, "")
if err != nil {
closeConns()
return
}
N.Relay(handshakeConn, fb)
return
}
}
cConn, meta, err := sudoku.ServerHandshake(handshakeConn, handshakeCfg)
if err != nil {
_ = handshakeConn.Close()
fallbackAddr := l.fallback
var susp *sudoku.SuspiciousError
isSuspicious := errors.As(err, &susp) && susp != nil && susp.Conn != nil
if isSuspicious {
log.Warnln("[Sudoku] suspicious handshake from %s: %v", conn.RemoteAddr(), err)
if fallbackAddr != "" {
fb, err := inner.HandleTcp(tunnel, fallbackAddr, "")
if err == nil {
relayToFallback(susp.Conn, conn, fb)
return
}
}
} else {
log.Debugln("[Sudoku] handshake failed from %s: %v", conn.RemoteAddr(), err)
}
closeConns()
return
}
session, err := sudoku.ReadServerSession(cConn, meta)
if err != nil {
log.Warnln("[Sudoku] read session failed from %s: %v", conn.RemoteAddr(), err)
_ = cConn.Close()
if handshakeConn != conn {
_ = conn.Close()
}
@@ -103,6 +148,7 @@ func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbou
default:
targetAddr := socks5.ParseAddr(session.Target)
if targetAddr == nil {
log.Warnln("[Sudoku] invalid target from %s: %q", conn.RemoteAddr(), session.Target)
_ = session.Conn.Close()
return
}
@@ -164,6 +210,24 @@ func (p *uotPacket) LocalAddr() net.Addr {
return p.rAddr
}
func relayToFallback(wrapper net.Conn, rawConn net.Conn, fallback net.Conn) {
if wrapper != nil {
if recorder, ok := wrapper.(interface{ GetBufferedAndRecorded() []byte }); ok {
badData := recorder.GetBufferedAndRecorded()
if len(badData) > 0 {
_ = fallback.SetWriteDeadline(time.Now().Add(3 * time.Second))
if _, err := io.Copy(fallback, bytes.NewReader(badData)); err != nil {
_ = fallback.Close()
_ = rawConn.Close()
return
}
_ = fallback.SetWriteDeadline(time.Time{})
}
}
}
N.Relay(rawConn, fallback)
}
func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) {
if len(additions) == 0 {
additions = []inbound.Addition{
@@ -188,42 +252,24 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
return nil, err
}
tableType := strings.ToLower(config.TableType)
if tableType == "" {
tableType = "prefer_ascii"
}
defaultConf := sudoku.DefaultConfig()
paddingMin := defaultConf.PaddingMin
paddingMax := defaultConf.PaddingMax
if config.PaddingMin != nil {
paddingMin = *config.PaddingMin
}
if config.PaddingMax != nil {
paddingMax = *config.PaddingMax
}
if config.PaddingMin == nil && config.PaddingMax != nil && paddingMax < paddingMin {
paddingMin = paddingMax
}
if config.PaddingMax == nil && config.PaddingMin != nil && paddingMax < paddingMin {
paddingMax = paddingMin
}
enablePureDownlink := defaultConf.EnablePureDownlink
if config.EnablePureDownlink != nil {
enablePureDownlink = *config.EnablePureDownlink
}
tables, err := sudoku.NewTablesWithCustomPatterns(config.Key, tableType, config.CustomTable, config.CustomTables)
tableType, err := sudoku.NormalizeTableType(config.TableType)
if err != nil {
_ = l.Close()
return nil, err
}
handshakeTimeout := defaultConf.HandshakeTimeoutSeconds
if config.HandshakeTimeoutSecond != nil {
handshakeTimeout = *config.HandshakeTimeoutSecond
defaultConf := sudoku.DefaultConfig()
paddingMin, paddingMax := sudoku.ResolvePadding(config.PaddingMin, config.PaddingMax, defaultConf.PaddingMin, defaultConf.PaddingMax)
enablePureDownlink := sudoku.DerefBool(config.EnablePureDownlink, defaultConf.EnablePureDownlink)
tables, err := sudoku.NewServerTablesWithCustomPatterns(sudoku.ServerAEADSeed(config.Key), tableType, config.CustomTable, config.CustomTables)
if err != nil {
_ = l.Close()
return nil, err
}
handshakeTimeout := sudoku.DerefInt(config.HandshakeTimeoutSecond, defaultConf.HandshakeTimeoutSeconds)
protoConf := sudoku.ProtocolConfig{
Key: config.Key,
AEADMethod: defaultConf.AEADMethod,
@@ -249,8 +295,13 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
addr: config.Listen,
protoConf: protoConf,
handler: h,
fallback: strings.TrimSpace(config.Fallback),
}
if sl.fallback != "" {
sl.tunnelSrv = sudoku.NewHTTPMaskTunnelServerWithFallback(&sl.protoConf)
} else {
sl.tunnelSrv = sudoku.NewHTTPMaskTunnelServer(&sl.protoConf)
}
sl.tunnelSrv = sudoku.NewHTTPMaskTunnelServer(&sl.protoConf)
go func() {
for {