mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-03-04 12:57:31 +00:00
feat: sudoku support ws transport (#2589)
This commit is contained in:
@@ -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"`
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user