feat: add Sudoku protocol inbound & outbound support (#2397)
Some checks are pending
Test / test (1.20, macos-15-intel) (push) Waiting to run
Test / test (1.20, macos-latest) (push) Waiting to run
Test / test (1.20, ubuntu-24.04-arm) (push) Waiting to run
Test / test (1.20, ubuntu-latest) (push) Waiting to run
Test / test (1.20, windows-latest) (push) Waiting to run
Test / test (1.21, macos-15-intel) (push) Waiting to run
Test / test (1.21, macos-latest) (push) Waiting to run
Test / test (1.21, ubuntu-24.04-arm) (push) Waiting to run
Test / test (1.21, ubuntu-latest) (push) Waiting to run
Test / test (1.21, windows-latest) (push) Waiting to run
Test / test (1.22, macos-15-intel) (push) Waiting to run
Test / test (1.22, macos-latest) (push) Waiting to run
Test / test (1.22, ubuntu-24.04-arm) (push) Waiting to run
Test / test (1.22, ubuntu-latest) (push) Waiting to run
Test / test (1.22, windows-latest) (push) Waiting to run
Test / test (1.23, macos-15-intel) (push) Waiting to run
Test / test (1.23, macos-latest) (push) Waiting to run
Test / test (1.23, ubuntu-24.04-arm) (push) Waiting to run
Test / test (1.23, ubuntu-latest) (push) Waiting to run
Test / test (1.23, windows-latest) (push) Waiting to run
Test / test (1.24, macos-15-intel) (push) Waiting to run
Test / test (1.24, macos-latest) (push) Waiting to run
Test / test (1.24, ubuntu-24.04-arm) (push) Waiting to run
Test / test (1.24, ubuntu-latest) (push) Waiting to run
Test / test (1.24, windows-latest) (push) Waiting to run
Test / test (1.25, macos-15-intel) (push) Waiting to run
Test / test (1.25, macos-latest) (push) Waiting to run
Test / test (1.25, ubuntu-24.04-arm) (push) Waiting to run
Test / test (1.25, ubuntu-latest) (push) Waiting to run
Test / test (1.25, windows-latest) (push) Waiting to run
Trigger CMFA Update / trigger-CMFA-update (push) Waiting to run

This commit is contained in:
futai 2025-11-28 23:40:00 +08:00 committed by GitHub
parent 8b6ba22b90
commit 6cf1743961
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 700 additions and 3 deletions

263
adapter/outbound/sudoku.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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:

View File

@ -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:

View File

@ -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: "<client_key>" # 如果你使用sudoku生成的ED25519密钥对请填写密钥对中的私钥否则填入和服务端相同的uuid
aead-method: chacha20-poly1305 # 可选值chacha20-poly1305、aes-128-gcm、none 我们保证在none的情况下sudoku混淆层仍然确保安全
padding-min: 2 # 最小填充字节数
padding-max: 7 # 最大填充字节数
seed: "<seed-or-key>" # 如果使用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: "<server_key>" # 如果你使用sudoku生成的ED25519密钥对此处是密钥对中的公钥当然你也可以仅仅使用任意uuid充当key
aead-method: chacha20-poly1305 # 支持chacha20-poly1305或者aes-128-gcm以及nonesudoku的混淆层可以确保none情况下数据安全
padding-min: 1 # 填充最小长度
padding-max: 15 # 填充最大长度,均不建议过大
seed: "<seed-or-key>" # 如果你不使用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

4
go.mod
View File

@ -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

8
go.sum
View File

@ -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=

22
listener/config/sudoku.go Normal file
View File

@ -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)
}

128
listener/inbound/sudoku.go Normal file
View File

@ -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)

View File

@ -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)
}

View File

@ -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)
}

139
listener/sudoku/server.go Normal file
View File

@ -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
}