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/go.mod b/go.mod index 2047d0eb..01b0723d 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,14 @@ module github.com/metacubex/mihomo -go 1.20 +go 1.24.7 + +toolchain go1.24.10 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 @@ -53,16 +55,19 @@ require ( gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 go.uber.org/automaxprocs v1.6.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.33.0 // lastest version compatible with golang1.20 + golang.org/x/crypto v0.45.0 // lastest version compatible with golang1.20 golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // lastest version compatible with golang1.20 - golang.org/x/net v0.35.0 // lastest version compatible with golang1.20 - golang.org/x/sync v0.11.0 // lastest version compatible with golang1.20 - golang.org/x/sys v0.30.0 // lastest version compatible with golang1.20 + golang.org/x/net v0.47.0 // lastest version compatible with golang1.20 + golang.org/x/sync v0.18.0 // lastest version compatible with golang1.20 + golang.org/x/sys v0.38.0 // lastest version compatible with golang1.20 google.golang.org/protobuf v1.34.2 // lastest version compatible with golang1.20 gopkg.in/yaml.v3 v3.0.1 ) +require github.com/saba-futai/sudoku v0.0.0-00010101000000-000000000000 + 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 @@ -104,8 +109,10 @@ require ( github.com/vishvananda/netns v0.0.4 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect - golang.org/x/mod v0.20.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.7.0 // indirect - golang.org/x/tools v0.24.0 // indirect + golang.org/x/tools v0.38.0 // indirect ) + +replace github.com/saba-futai/sudoku => ../sudoku-main diff --git a/go.sum b/go.sum index bc29d7fe..3795926b 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,9 @@ +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= +github.com/RyuaNerin/testingutil v0.1.0/go.mod h1:yTqj6Ta/ycHMPJHRyO12Mz3VrvTloWOsy23WOZH19AA= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= @@ -23,13 +26,14 @@ 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= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po= github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg= +github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c/go.mod h1:ETASDWf/FmEb6Ysrtd1QhjNedUU/ZQxBCRLh60bQ/UI= github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY= github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA= @@ -43,6 +47,7 @@ github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hH github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= @@ -57,6 +62,7 @@ github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0= github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -66,6 +72,7 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= +github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k= @@ -80,6 +87,7 @@ github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZY github.com/klauspost/reedsolomon v1.12.3 h1:tzUznbfc3OFwJaTebv/QdhnFf2Xvb7gZ24XaHLBPmdc= github.com/klauspost/reedsolomon v1.12.3/go.mod h1:3K5rXwABAvzGeR01r6pWZieUALXO/Tq7bFKGIb4m4WI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -154,6 +162,7 @@ github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:U github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0= github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo= github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0= @@ -167,6 +176,7 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 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/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= @@ -210,6 +220,7 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4= gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= @@ -217,25 +228,26 @@ gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAo go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -248,22 +260,24 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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/sudoku/server.go b/listener/sudoku/server.go new file mode 100644 index 00000000..76cdd15c --- /dev/null +++ b/listener/sudoku/server.go @@ -0,0 +1,124 @@ +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 + } + + return &Listener{ + listener: l, + addr: config.Listen, + protoConf: protoConf, + }, nil +}