mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2025-12-19 00:10:06 +08:00
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
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:
parent
8b6ba22b90
commit
6cf1743961
263
adapter/outbound/sudoku.go
Normal file
263
adapter/outbound/sudoku.go
Normal 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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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以及none,sudoku的混淆层可以确保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
4
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
|
||||
|
||||
8
go.sum
8
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=
|
||||
|
||||
22
listener/config/sudoku.go
Normal file
22
listener/config/sudoku.go
Normal 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
128
listener/inbound/sudoku.go
Normal 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)
|
||||
91
listener/inbound/sudoku_test.go
Normal file
91
listener/inbound/sudoku_test.go
Normal 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)
|
||||
}
|
||||
@ -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
139
listener/sudoku/server.go
Normal 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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user