From 6cf17439619d038cfd4646a90fbf43bc394d03b6 Mon Sep 17 00:00:00 2001 From: futai <120904569+saba-futai@users.noreply.github.com> Date: Fri, 28 Nov 2025 23:40:00 +0800 Subject: [PATCH] feat: add Sudoku protocol inbound & outbound support (#2397) --- adapter/outbound/sudoku.go | 263 ++++++++++++++++++++++++++++++++ adapter/parser.go | 7 + constant/adapters.go | 3 + constant/metadata.go | 5 + docs/config.yaml | 26 ++++ go.mod | 4 +- go.sum | 8 +- listener/config/sudoku.go | 22 +++ listener/inbound/sudoku.go | 128 ++++++++++++++++ listener/inbound/sudoku_test.go | 91 +++++++++++ listener/parse.go | 7 + listener/sudoku/server.go | 139 +++++++++++++++++ 12 files changed, 700 insertions(+), 3 deletions(-) create mode 100644 adapter/outbound/sudoku.go create mode 100644 listener/config/sudoku.go create mode 100644 listener/inbound/sudoku.go create mode 100644 listener/inbound/sudoku_test.go create mode 100644 listener/sudoku/server.go diff --git a/adapter/outbound/sudoku.go b/adapter/outbound/sudoku.go new file mode 100644 index 00000000..1db9ad2d --- /dev/null +++ b/adapter/outbound/sudoku.go @@ -0,0 +1,263 @@ +package outbound + +import ( + "context" + "crypto/sha256" + "encoding/binary" + "fmt" + "io" + "net" + "strconv" + "strings" + "time" + + "github.com/saba-futai/sudoku/apis" + "github.com/saba-futai/sudoku/pkg/crypto" + "github.com/saba-futai/sudoku/pkg/obfs/httpmask" + "github.com/saba-futai/sudoku/pkg/obfs/sudoku" + + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/proxydialer" + C "github.com/metacubex/mihomo/constant" +) + +type Sudoku struct { + *Base + option *SudokuOption + table *sudoku.Table + baseConf apis.ProtocolConfig +} + +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"` + Seed string `proxy:"seed,omitempty"` + TableType string `proxy:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy" +} + +// DialContext implements C.ProxyAdapter +func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { + return s.DialContextWithDialer(ctx, dialer.NewDialer(s.DialOptions()...), metadata) +} + +// DialContextWithDialer implements C.ProxyAdapter +func (s *Sudoku) DialContextWithDialer(ctx context.Context, d C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { + if len(s.option.DialerProxy) > 0 { + d, err = proxydialer.NewByName(s.option.DialerProxy, d) + if err != nil { + return nil, err + } + } + + cfg, err := s.buildConfig(metadata) + if err != nil { + return nil, err + } + + c, err := d.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) + } + + c, err = s.streamConn(c, cfg) + if err != nil { + return nil, err + } + + return NewConn(c, s), nil +} + +// ListenPacketContext implements C.ProxyAdapter +func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { + return nil, C.ErrNotSupport +} + +// SupportUOT implements C.ProxyAdapter +func (s *Sudoku) SupportUOT() bool { + return false // Sudoku protocol only supports TCP +} + +// SupportWithDialer implements C.ProxyAdapter +func (s *Sudoku) SupportWithDialer() C.NetWork { + return C.TCP +} + +// ProxyInfo implements C.ProxyAdapter +func (s *Sudoku) ProxyInfo() C.ProxyInfo { + info := s.Base.ProxyInfo() + info.DialerProxy = s.option.DialerProxy + return info +} + +func (s *Sudoku) buildConfig(metadata *C.Metadata) (*apis.ProtocolConfig, error) { + if metadata == nil || metadata.DstPort == 0 || !metadata.Valid() { + return nil, fmt.Errorf("invalid metadata for sudoku outbound") + } + + cfg := s.baseConf + cfg.TargetAddress = metadata.RemoteAddress() + + if err := cfg.ValidateClient(); err != nil { + return nil, err + } + return &cfg, nil +} + +func (s *Sudoku) streamConn(rawConn net.Conn, cfg *apis.ProtocolConfig) (_ net.Conn, err error) { + if err = httpmask.WriteRandomRequestHeader(rawConn, cfg.ServerAddress); err != nil { + return nil, fmt.Errorf("write http mask failed: %w", err) + } + + obfsConn := sudoku.NewConn(rawConn, cfg.Table, cfg.PaddingMin, cfg.PaddingMax, false) + cConn, err := crypto.NewAEADConn(obfsConn, cfg.Key, cfg.AEADMethod) + if err != nil { + return nil, fmt.Errorf("setup crypto failed: %w", err) + } + + handshake := buildSudokuHandshakePayload(cfg.Key) + if _, err = cConn.Write(handshake[:]); err != nil { + cConn.Close() + return nil, fmt.Errorf("send handshake failed: %w", err) + } + + if err = writeTargetAddress(cConn, cfg.TargetAddress); err != nil { + cConn.Close() + return nil, fmt.Errorf("send target address failed: %w", err) + } + + return cConn, nil +} + +func NewSudoku(option SudokuOption) (*Sudoku, error) { + if option.Server == "" { + return nil, fmt.Errorf("server is required") + } + if option.Port <= 0 || option.Port > 65535 { + return nil, fmt.Errorf("invalid port: %d", option.Port) + } + if option.Key == "" { + return nil, fmt.Errorf("key is required") + } + + tableType := strings.ToLower(option.TableType) + if tableType == "" { + tableType = "prefer_ascii" + } + if tableType != "prefer_ascii" && tableType != "prefer_entropy" { + return nil, fmt.Errorf("table-type must be prefer_ascii or prefer_entropy") + } + + seed := option.Seed + if seed == "" { + seed = option.Key + } + + table := sudoku.NewTable(seed, tableType) + + defaultConf := apis.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 + } + + baseConf := apis.ProtocolConfig{ + ServerAddress: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), + Key: option.Key, + AEADMethod: defaultConf.AEADMethod, + Table: table, + PaddingMin: paddingMin, + PaddingMax: paddingMax, + HandshakeTimeoutSeconds: defaultConf.HandshakeTimeoutSeconds, + } + if option.AEADMethod != "" { + baseConf.AEADMethod = option.AEADMethod + } + + return &Sudoku{ + Base: &Base{ + name: option.Name, + addr: baseConf.ServerAddress, + tp: C.Sudoku, + udp: false, + tfo: option.TFO, + mpTcp: option.MPTCP, + iface: option.Interface, + rmark: option.RoutingMark, + prefer: C.NewDNSPrefer(option.IPVersion), + }, + option: &option, + table: table, + baseConf: baseConf, + }, nil +} + +func buildSudokuHandshakePayload(key string) [16]byte { + var payload [16]byte + binary.BigEndian.PutUint64(payload[:8], uint64(time.Now().Unix())) + hash := sha256.Sum256([]byte(key)) + copy(payload[8:], hash[:8]) + return payload +} + +func writeTargetAddress(w io.Writer, rawAddr string) error { + host, portStr, err := net.SplitHostPort(rawAddr) + if err != nil { + return err + } + + portInt, err := net.LookupPort("tcp", portStr) + if err != nil { + return err + } + + var buf []byte + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + buf = append(buf, 0x01) // IPv4 + buf = append(buf, ip4...) + } else { + buf = append(buf, 0x04) // IPv6 + buf = append(buf, ip...) + } + } else { + if len(host) > 255 { + return fmt.Errorf("domain too long") + } + buf = append(buf, 0x03) // domain + buf = append(buf, byte(len(host))) + buf = append(buf, host...) + } + + var portBytes [2]byte + binary.BigEndian.PutUint16(portBytes[:], uint16(portInt)) + buf = append(buf, portBytes[:]...) + + _, err = w.Write(buf) + return err +} diff --git a/adapter/parser.go b/adapter/parser.go index 3052ab10..af93ebf4 100644 --- a/adapter/parser.go +++ b/adapter/parser.go @@ -146,6 +146,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { break } proxy, err = outbound.NewAnyTLS(*anytlsOption) + case "sudoku": + sudokuOption := &outbound.SudokuOption{} + err = decoder.Decode(mapping, sudokuOption) + if err != nil { + break + } + proxy, err = outbound.NewSudoku(*sudokuOption) default: return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) } diff --git a/constant/adapters.go b/constant/adapters.go index 97e9235b..1cd146c1 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -44,6 +44,7 @@ const ( Ssh Mieru AnyTLS + Sudoku ) const ( @@ -230,6 +231,8 @@ func (at AdapterType) String() string { return "Mieru" case AnyTLS: return "AnyTLS" + case Sudoku: + return "Sudoku" case Relay: return "Relay" case Selector: diff --git a/constant/metadata.go b/constant/metadata.go index 23cdcd86..63f08d43 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -39,6 +39,7 @@ const ( HYSTERIA2 ANYTLS MIERU + SUDOKU INNER ) @@ -112,6 +113,8 @@ func (t Type) String() string { return "AnyTLS" case MIERU: return "Mieru" + case SUDOKU: + return "Sudoku" case INNER: return "Inner" default: @@ -154,6 +157,8 @@ func ParseType(t string) (*Type, error) { res = ANYTLS case "MIERU": res = MIERU + case "SUDOKU": + res = SUDOKU case "INNER": res = INNER default: diff --git a/docs/config.yaml b/docs/config.yaml index d6421e10..0d5fb75e 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -1038,6 +1038,18 @@ proxies: # socks5 # 如果想开启 0-RTT 握手,请设置为 HANDSHAKE_NO_WAIT,否则请设置为 HANDSHAKE_STANDARD。默认值为 HANDSHAKE_STANDARD # handshake-mode: HANDSHAKE_STANDARD + # sudoku + - name: sudoku + type: sudoku + server: serverip # 1.2.3.4 + port: 443 + key: "" # 如果你使用sudoku生成的ED25519密钥对,请填写密钥对中的私钥,否则填入和服务端相同的uuid + aead-method: chacha20-poly1305 # 可选值:chacha20-poly1305、aes-128-gcm、none 我们保证在none的情况下sudoku混淆层仍然确保安全 + padding-min: 2 # 最小填充字节数 + padding-max: 7 # 最大填充字节数 + seed: "" # 如果使用sudoku生成的ED25519密钥对,请填写密钥对中的公钥(如果你有安全焦虑,填入私钥也可以,只是私钥长度比较长不好看而已),否则填入和服务端相同的uuid + table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy 前者全ascii映射,后者保证熵值(汉明1)低于3 + # anytls - name: anytls type: anytls @@ -1567,6 +1579,19 @@ listeners: username1: password1 username2: password2 + - name: sudoku-in-1 + type: sudoku + port: 8443 # 仅支持单端口 + listen: 0.0.0.0 + key: "" # 如果你使用sudoku生成的ED25519密钥对,此处是密钥对中的公钥,当然,你也可以仅仅使用任意uuid充当key + aead-method: chacha20-poly1305 # 支持chacha20-poly1305或者aes-128-gcm以及none,sudoku的混淆层可以确保none情况下数据安全 + padding-min: 1 # 填充最小长度 + padding-max: 15 # 填充最大长度,均不建议过大 + seed: "" # 如果你不使用ED25519密钥对,就请填入客户端的key,否则仍然是公钥 + table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy 前者全ascii映射,后者保证熵值(汉明1)低于3 + handshake-timeout: 5 # optional + + - name: trojan-in-1 type: trojan port: 10819 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 @@ -1715,3 +1740,4 @@ listeners: # alpn: # - h3 # max-udp-relay-packet-size: 1500 + diff --git a/go.mod b/go.mod index 2047d0eb..d7e40c7e 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,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.23.0 + github.com/enfein/mieru/v3 v3.24.0 github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/render v1.0.3 github.com/gobwas/ws v1.4.0 @@ -43,6 +43,7 @@ require ( github.com/mroth/weightedrand/v2 v2.1.0 github.com/openacid/low v0.1.21 github.com/oschwald/maxminddb-golang v1.12.0 // lastest version compatible with golang1.20 + github.com/saba-futai/sudoku v0.0.1-e github.com/sagernet/cors v1.2.1 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/samber/lo v1.52.0 @@ -63,6 +64,7 @@ require ( ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/RyuaNerin/go-krypto v1.3.0 // indirect github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect github.com/ajg/form v1.5.1 // indirect diff --git a/go.sum b/go.sum index bc29d7fe..15ba6b90 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg= github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM= github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss= @@ -23,8 +25,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.23.0 h1:f/dd3UAoi36FD9DZ9x49t6Ps0oHeSjrVSgWzvEstn0E= -github.com/enfein/mieru/v3 v3.23.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= +github.com/enfein/mieru/v3 v3.24.0 h1:UpS6fTj242wAz2Xa/ieavMN8owcWdPzLFB11UqYs5GY= +github.com/enfein/mieru/v3 v3.24.0/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= @@ -169,6 +171,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/saba-futai/sudoku v0.0.1-e h1:PetJcOdoybBWGT1k65puNv+kt6Cmger6i/TSfuu6CdM= +github.com/saba-futai/sudoku v0.0.1-e/go.mod h1:2ZRzRwz93cS2K/o2yOG4CPJEltcvk5y6vbvUmjftGU0= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= diff --git a/listener/config/sudoku.go b/listener/config/sudoku.go new file mode 100644 index 00000000..855795f1 --- /dev/null +++ b/listener/config/sudoku.go @@ -0,0 +1,22 @@ +package config + +import "encoding/json" + +// SudokuServer describes a Sudoku inbound server configuration. +// It is internal to the listener layer and mainly used for logging and wiring. +type SudokuServer struct { + Enable bool `json:"enable"` + Listen string `json:"listen"` + Key string `json:"key"` + AEADMethod string `json:"aead-method,omitempty"` + PaddingMin *int `json:"padding-min,omitempty"` + PaddingMax *int `json:"padding-max,omitempty"` + Seed string `json:"seed,omitempty"` + TableType string `json:"table-type,omitempty"` + HandshakeTimeoutSecond *int `json:"handshake-timeout,omitempty"` +} + +func (s SudokuServer) String() string { + b, _ := json.Marshal(s) + return string(b) +} diff --git a/listener/inbound/sudoku.go b/listener/inbound/sudoku.go new file mode 100644 index 00000000..d6e84af3 --- /dev/null +++ b/listener/inbound/sudoku.go @@ -0,0 +1,128 @@ +package inbound + +import ( + "errors" + "fmt" + "strings" + + "github.com/saba-futai/sudoku/apis" + + C "github.com/metacubex/mihomo/constant" + LC "github.com/metacubex/mihomo/listener/config" + sudokuListener "github.com/metacubex/mihomo/listener/sudoku" + "github.com/metacubex/mihomo/log" +) + +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"` + Seed string `inbound:"seed,omitempty"` + TableType string `inbound:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy" + HandshakeTimeoutSecond *int `inbound:"handshake-timeout,omitempty"` +} + +func (o SudokuOption) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +type Sudoku struct { + *Base + config *SudokuOption + listeners []*sudokuListener.Listener + serverConf LC.SudokuServer +} + +func NewSudoku(options *SudokuOption) (*Sudoku, error) { + if options.Key == "" { + return nil, fmt.Errorf("sudoku inbound requires key") + } + base, err := NewBase(&options.BaseOption) + if err != nil { + return nil, err + } + + defaultConf := apis.DefaultConfig() + + serverConf := LC.SudokuServer{ + Enable: true, + Listen: base.RawAddress(), + Key: options.Key, + AEADMethod: options.AEADMethod, + PaddingMin: options.PaddingMin, + PaddingMax: options.PaddingMax, + Seed: options.Seed, + TableType: options.TableType, + } + if options.HandshakeTimeoutSecond != nil { + serverConf.HandshakeTimeoutSecond = options.HandshakeTimeoutSecond + } else { + // Use Sudoku default if not specified. + v := defaultConf.HandshakeTimeoutSeconds + serverConf.HandshakeTimeoutSecond = &v + } + + return &Sudoku{ + Base: base, + config: options, + serverConf: serverConf, + }, nil +} + +// Config implements constant.InboundListener +func (s *Sudoku) Config() C.InboundConfig { + return s.config +} + +// Address implements constant.InboundListener +func (s *Sudoku) Address() string { + var addrList []string + for _, l := range s.listeners { + addrList = append(addrList, l.Address()) + } + return strings.Join(addrList, ",") +} + +// Listen implements constant.InboundListener +func (s *Sudoku) Listen(tunnel C.Tunnel) error { + if s.serverConf.Key == "" { + return fmt.Errorf("sudoku inbound requires key") + } + + var errs []error + for _, addr := range strings.Split(s.RawAddress(), ",") { + conf := s.serverConf + conf.Listen = addr + + l, err := sudokuListener.New(conf, tunnel, s.Additions()...) + if err != nil { + errs = append(errs, err) + continue + } + s.listeners = append(s.listeners, l) + } + if len(errs) > 0 { + return errors.Join(errs...) + } + + log.Infoln("Sudoku[%s] inbound listening at: %s", s.Name(), s.Address()) + return nil +} + +// Close implements constant.InboundListener +func (s *Sudoku) Close() error { + var errs []error + for _, l := range s.listeners { + if err := l.Close(); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} + +var _ C.InboundListener = (*Sudoku)(nil) diff --git a/listener/inbound/sudoku_test.go b/listener/inbound/sudoku_test.go new file mode 100644 index 00000000..6d3e35b1 --- /dev/null +++ b/listener/inbound/sudoku_test.go @@ -0,0 +1,91 @@ +package inbound_test + +import ( + "net/netip" + "testing" + + "github.com/metacubex/mihomo/adapter/outbound" + "github.com/metacubex/mihomo/listener/inbound" + "github.com/stretchr/testify/assert" +) + +func testInboundSudoku(t *testing.T, inboundOptions inbound.SudokuOption, outboundOptions outbound.SudokuOption) { + t.Parallel() + + inboundOptions.BaseOption = inbound.BaseOption{ + NameStr: "sudoku_inbound", + Listen: "127.0.0.1", + Port: "0", + } + in, err := inbound.NewSudoku(&inboundOptions) + if !assert.NoError(t, err) { + return + } + + tunnel := NewHttpTestTunnel() + defer tunnel.Close() + + err = in.Listen(tunnel) + if !assert.NoError(t, err) { + return + } + defer in.Close() + + addrPort, err := netip.ParseAddrPort(in.Address()) + if !assert.NoError(t, err) { + return + } + + outboundOptions.Name = "sudoku_outbound" + outboundOptions.Server = addrPort.Addr().String() + outboundOptions.Port = int(addrPort.Port()) + + out, err := outbound.NewSudoku(outboundOptions) + if !assert.NoError(t, err) { + return + } + defer out.Close() + + tunnel.DoTest(t, out) +} + +func TestInboundSudoku_Basic(t *testing.T) { + key := "test_key" + inboundOptions := inbound.SudokuOption{ + Key: key, + } + outboundOptions := outbound.SudokuOption{ + Key: key, + } + testInboundSudoku(t, inboundOptions, outboundOptions) +} + +func TestInboundSudoku_Entropy(t *testing.T) { + key := "test_key_entropy" + inboundOptions := inbound.SudokuOption{ + Key: key, + TableType: "prefer_entropy", + } + outboundOptions := outbound.SudokuOption{ + Key: key, + TableType: "prefer_entropy", + } + testInboundSudoku(t, inboundOptions, outboundOptions) +} + +func TestInboundSudoku_Padding(t *testing.T) { + key := "test_key_padding" + min := 10 + max := 100 + inboundOptions := inbound.SudokuOption{ + Key: key, + PaddingMin: &min, + PaddingMax: &max, + } + outboundOptions := outbound.SudokuOption{ + Key: key, + PaddingMin: &min, + PaddingMax: &max, + } + testInboundSudoku(t, inboundOptions, outboundOptions) +} diff --git a/listener/parse.go b/listener/parse.go index 4e893bf1..35941141 100644 --- a/listener/parse.go +++ b/listener/parse.go @@ -134,6 +134,13 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) { return nil, err } listener, err = IN.NewMieru(mieruOption) + case "sudoku": + sudokuOption := &IN.SudokuOption{} + err = decoder.Decode(mapping, sudokuOption) + if err != nil { + return nil, err + } + listener, err = IN.NewSudoku(sudokuOption) default: return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) } diff --git a/listener/sudoku/server.go b/listener/sudoku/server.go new file mode 100644 index 00000000..87b2abd9 --- /dev/null +++ b/listener/sudoku/server.go @@ -0,0 +1,139 @@ +package sudoku + +import ( + "net" + "strings" + + "github.com/saba-futai/sudoku/apis" + sudokuobfs "github.com/saba-futai/sudoku/pkg/obfs/sudoku" + + "github.com/metacubex/mihomo/adapter/inbound" + C "github.com/metacubex/mihomo/constant" + LC "github.com/metacubex/mihomo/listener/config" + "github.com/metacubex/mihomo/transport/socks5" +) + +type Listener struct { + listener net.Listener + addr string + closed bool + protoConf apis.ProtocolConfig +} + +// RawAddress implements C.Listener +func (l *Listener) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *Listener) Address() string { + if l.listener == nil { + return "" + } + return l.listener.Addr().String() +} + +// Close implements C.Listener +func (l *Listener) Close() error { + l.closed = true + if l.listener != nil { + return l.listener.Close() + } + return nil +} + +func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { + tunnelConn, target, err := apis.ServerHandshake(conn, &l.protoConf) + if err != nil { + _ = conn.Close() + return + } + + targetAddr := socks5.ParseAddr(target) + if targetAddr == nil { + _ = tunnelConn.Close() + return + } + + tunnel.HandleTCPConn(inbound.NewSocket(targetAddr, tunnelConn, C.SUDOKU, additions...)) +} + +func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-SUDOKU"), + inbound.WithSpecialRules(""), + } + } + + l, err := inbound.Listen("tcp", config.Listen) + if err != nil { + return nil, err + } + + seed := config.Seed + if seed == "" { + seed = config.Key + } + + tableType := strings.ToLower(config.TableType) + if tableType == "" { + tableType = "prefer_ascii" + } + + table := sudokuobfs.NewTable(seed, tableType) + + defaultConf := apis.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 + } + + handshakeTimeout := defaultConf.HandshakeTimeoutSeconds + if config.HandshakeTimeoutSecond != nil { + handshakeTimeout = *config.HandshakeTimeoutSecond + } + + protoConf := apis.ProtocolConfig{ + Key: config.Key, + AEADMethod: defaultConf.AEADMethod, + Table: table, + PaddingMin: paddingMin, + PaddingMax: paddingMax, + HandshakeTimeoutSeconds: handshakeTimeout, + } + if config.AEADMethod != "" { + protoConf.AEADMethod = config.AEADMethod + } + + sl := &Listener{ + listener: l, + addr: config.Listen, + protoConf: protoConf, + } + + go func() { + for { + c, err := l.Accept() + if err != nil { + if sl.closed { + break + } + continue + } + go sl.handleConn(c, tunnel, additions...) + } + }() + + return sl, nil +}