mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-03-03 12:17:31 +00:00
feat: sudoku support ws transport (#2589)
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
N "github.com/metacubex/mihomo/common/net"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/transport/sudoku"
|
||||
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
|
||||
)
|
||||
|
||||
type Sudoku struct {
|
||||
@@ -18,32 +19,43 @@ type Sudoku struct {
|
||||
option *SudokuOption
|
||||
baseConf sudoku.ProtocolConfig
|
||||
|
||||
httpMaskMu sync.Mutex
|
||||
httpMaskClient *sudoku.HTTPMaskTunnelClient
|
||||
|
||||
muxMu sync.Mutex
|
||||
muxClient *sudoku.MultiplexClient
|
||||
|
||||
httpMaskMu sync.Mutex
|
||||
httpMaskClient *httpmask.TunnelClient
|
||||
httpMaskKey string
|
||||
}
|
||||
|
||||
type SudokuOption struct {
|
||||
BasicOption
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
Key string `proxy:"key"`
|
||||
AEADMethod string `proxy:"aead-method,omitempty"`
|
||||
PaddingMin *int `proxy:"padding-min,omitempty"`
|
||||
PaddingMax *int `proxy:"padding-max,omitempty"`
|
||||
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)
|
||||
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
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
Key string `proxy:"key"`
|
||||
AEADMethod string `proxy:"aead-method,omitempty"`
|
||||
PaddingMin *int `proxy:"padding-min,omitempty"`
|
||||
PaddingMax *int `proxy:"padding-max,omitempty"`
|
||||
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", "ws"
|
||||
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)
|
||||
HTTPMaskOptions *SudokuHTTPMaskOptions `proxy:"httpmask,omitempty"`
|
||||
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
|
||||
}
|
||||
|
||||
type SudokuHTTPMaskOptions struct {
|
||||
Disable bool `proxy:"disable,omitempty"`
|
||||
Mode string `proxy:"mode,omitempty"`
|
||||
TLS bool `proxy:"tls,omitempty"`
|
||||
Host string `proxy:"host,omitempty"`
|
||||
PathRoot string `proxy:"path_root,omitempty"`
|
||||
Multiplex string `proxy:"multiplex,omitempty"`
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
@@ -73,7 +85,7 @@ func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
|
||||
return nil, fmt.Errorf("encode target address failed: %w", err)
|
||||
}
|
||||
|
||||
if _, err = c.Write(addrBuf); err != nil {
|
||||
if err = sudoku.WriteKIPMessage(c, sudoku.KIPTypeOpenTCP, addrBuf); err != nil {
|
||||
return nil, fmt.Errorf("send target address failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -96,9 +108,9 @@ func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = sudoku.WritePreface(c); err != nil {
|
||||
if err = sudoku.WriteKIPMessage(c, sudoku.KIPTypeStartUoT, nil); err != nil {
|
||||
_ = c.Close()
|
||||
return nil, fmt.Errorf("send uot preface failed: %w", err)
|
||||
return nil, fmt.Errorf("start uot failed: %w", err)
|
||||
}
|
||||
|
||||
return newPacketConn(N.NewThreadSafePacketConn(sudoku.NewUoTPacketConn(c)), s), nil
|
||||
@@ -141,32 +153,45 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
|
||||
return nil, fmt.Errorf("key is required")
|
||||
}
|
||||
|
||||
tableType := strings.ToLower(option.TableType)
|
||||
if tableType == "" {
|
||||
tableType = "prefer_ascii"
|
||||
defaultConf := sudoku.DefaultConfig()
|
||||
tableType, err := sudoku.NormalizeTableType(option.TableType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tableType != "prefer_ascii" && tableType != "prefer_entropy" {
|
||||
return nil, fmt.Errorf("table-type must be prefer_ascii or prefer_entropy")
|
||||
paddingMin, paddingMax := sudoku.ResolvePadding(option.PaddingMin, option.PaddingMax, defaultConf.PaddingMin, defaultConf.PaddingMax)
|
||||
enablePureDownlink := sudoku.DerefBool(option.EnablePureDownlink, defaultConf.EnablePureDownlink)
|
||||
|
||||
disableHTTPMask := defaultConf.DisableHTTPMask
|
||||
if option.HTTPMask != nil {
|
||||
disableHTTPMask = !*option.HTTPMask
|
||||
}
|
||||
httpMaskMode := defaultConf.HTTPMaskMode
|
||||
if option.HTTPMaskMode != "" {
|
||||
httpMaskMode = option.HTTPMaskMode
|
||||
}
|
||||
httpMaskTLS := option.HTTPMaskTLS
|
||||
httpMaskHost := option.HTTPMaskHost
|
||||
pathRoot := strings.TrimSpace(option.PathRoot)
|
||||
httpMaskMultiplex := defaultConf.HTTPMaskMultiplex
|
||||
if option.HTTPMaskMultiplex != "" {
|
||||
httpMaskMultiplex = option.HTTPMaskMultiplex
|
||||
}
|
||||
|
||||
defaultConf := sudoku.DefaultConfig()
|
||||
paddingMin := defaultConf.PaddingMin
|
||||
paddingMax := defaultConf.PaddingMax
|
||||
if option.PaddingMin != nil {
|
||||
paddingMin = *option.PaddingMin
|
||||
}
|
||||
if option.PaddingMax != nil {
|
||||
paddingMax = *option.PaddingMax
|
||||
}
|
||||
if option.PaddingMin == nil && option.PaddingMax != nil && paddingMax < paddingMin {
|
||||
paddingMin = paddingMax
|
||||
}
|
||||
if option.PaddingMax == nil && option.PaddingMin != nil && paddingMax < paddingMin {
|
||||
paddingMax = paddingMin
|
||||
}
|
||||
enablePureDownlink := defaultConf.EnablePureDownlink
|
||||
if option.EnablePureDownlink != nil {
|
||||
enablePureDownlink = *option.EnablePureDownlink
|
||||
if hm := option.HTTPMaskOptions; hm != nil {
|
||||
disableHTTPMask = hm.Disable
|
||||
if hm.Mode != "" {
|
||||
httpMaskMode = hm.Mode
|
||||
}
|
||||
httpMaskTLS = hm.TLS
|
||||
httpMaskHost = hm.Host
|
||||
if pr := strings.TrimSpace(hm.PathRoot); pr != "" {
|
||||
pathRoot = pr
|
||||
}
|
||||
if mux := strings.TrimSpace(hm.Multiplex); mux != "" {
|
||||
httpMaskMultiplex = mux
|
||||
} else {
|
||||
httpMaskMultiplex = defaultConf.HTTPMaskMultiplex
|
||||
}
|
||||
}
|
||||
|
||||
baseConf := sudoku.ProtocolConfig{
|
||||
@@ -177,20 +202,14 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
|
||||
PaddingMax: paddingMax,
|
||||
EnablePureDownlink: enablePureDownlink,
|
||||
HandshakeTimeoutSeconds: defaultConf.HandshakeTimeoutSeconds,
|
||||
DisableHTTPMask: !option.HTTPMask,
|
||||
HTTPMaskMode: defaultConf.HTTPMaskMode,
|
||||
HTTPMaskTLSEnabled: option.HTTPMaskTLS,
|
||||
HTTPMaskHost: option.HTTPMaskHost,
|
||||
HTTPMaskPathRoot: strings.TrimSpace(option.PathRoot),
|
||||
HTTPMaskMultiplex: defaultConf.HTTPMaskMultiplex,
|
||||
DisableHTTPMask: disableHTTPMask,
|
||||
HTTPMaskMode: httpMaskMode,
|
||||
HTTPMaskTLSEnabled: httpMaskTLS,
|
||||
HTTPMaskHost: httpMaskHost,
|
||||
HTTPMaskPathRoot: pathRoot,
|
||||
HTTPMaskMultiplex: 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)
|
||||
tables, err := sudoku.NewClientTablesWithCustomPatterns(sudoku.ClientAEADSeed(option.Key), tableType, option.CustomTable, option.CustomTables)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build table(s) failed: %w", err)
|
||||
}
|
||||
@@ -244,7 +263,7 @@ func normalizeHTTPMaskMultiplex(mode string) string {
|
||||
|
||||
func httpTunnelModeEnabled(mode string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
||||
case "stream", "poll", "auto":
|
||||
case "stream", "poll", "auto", "ws":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -271,14 +290,24 @@ func (s *Sudoku) dialAndHandshake(ctx context.Context, cfg *sudoku.ProtocolConfi
|
||||
)
|
||||
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
|
||||
if muxMode == "auto" && strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) != "ws" {
|
||||
if client, cerr := s.getOrCreateHTTPMaskClient(cfg); cerr == nil && client != nil {
|
||||
c, err = client.DialTunnel(ctx, httpmask.TunnelDialOptions{
|
||||
Mode: cfg.HTTPMaskMode,
|
||||
TLSEnabled: cfg.HTTPMaskTLSEnabled,
|
||||
HostOverride: cfg.HTTPMaskHost,
|
||||
PathRoot: cfg.HTTPMaskPathRoot,
|
||||
AuthKey: sudoku.ClientAEADSeed(cfg.Key),
|
||||
Upgrade: upgrade,
|
||||
Multiplex: cfg.HTTPMaskMultiplex,
|
||||
DialContext: s.dialer.DialContext,
|
||||
})
|
||||
if err != nil {
|
||||
s.resetHTTPMaskClient()
|
||||
}
|
||||
}
|
||||
c, err = client.Dial(ctx, upgrade)
|
||||
default:
|
||||
}
|
||||
if c == nil && err == nil {
|
||||
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext, upgrade)
|
||||
}
|
||||
if err == nil && c != nil {
|
||||
@@ -372,34 +401,51 @@ func (s *Sudoku) resetMuxClient() {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
s.httpMaskKey = ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sudoku) getOrCreateHTTPMaskClient(cfg *sudoku.ProtocolConfig) (*httpmask.TunnelClient, error) {
|
||||
if s == nil || cfg == nil {
|
||||
return nil, fmt.Errorf("nil adapter or config")
|
||||
}
|
||||
|
||||
key := cfg.ServerAddress + "|" + strconv.FormatBool(cfg.HTTPMaskTLSEnabled) + "|" + strings.TrimSpace(cfg.HTTPMaskHost)
|
||||
|
||||
s.httpMaskMu.Lock()
|
||||
if s.httpMaskClient != nil && s.httpMaskKey == key {
|
||||
client := s.httpMaskClient
|
||||
s.httpMaskMu.Unlock()
|
||||
return client, nil
|
||||
}
|
||||
s.httpMaskMu.Unlock()
|
||||
|
||||
client, err := httpmask.NewTunnelClient(cfg.ServerAddress, httpmask.TunnelClientOptions{
|
||||
TLSEnabled: cfg.HTTPMaskTLSEnabled,
|
||||
HostOverride: cfg.HTTPMaskHost,
|
||||
DialContext: s.dialer.DialContext,
|
||||
MaxIdleConns: 32,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.httpMaskMu.Lock()
|
||||
defer s.httpMaskMu.Unlock()
|
||||
if s.httpMaskClient != nil && s.httpMaskKey == key {
|
||||
client.CloseIdleConnections()
|
||||
return s.httpMaskClient, nil
|
||||
}
|
||||
if s.httpMaskClient != nil {
|
||||
s.httpMaskClient.CloseIdleConnections()
|
||||
}
|
||||
s.httpMaskClient = client
|
||||
s.httpMaskKey = key
|
||||
return client, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user