Skip to content

Commit 6cf1743

Browse files
authored
feat: add Sudoku protocol inbound & outbound support (#2397)
1 parent 8b6ba22 commit 6cf1743

File tree

12 files changed

+700
-3
lines changed

12 files changed

+700
-3
lines changed

adapter/outbound/sudoku.go

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package outbound
2+
3+
import (
4+
"context"
5+
"crypto/sha256"
6+
"encoding/binary"
7+
"fmt"
8+
"io"
9+
"net"
10+
"strconv"
11+
"strings"
12+
"time"
13+
14+
"github.com/saba-futai/sudoku/apis"
15+
"github.com/saba-futai/sudoku/pkg/crypto"
16+
"github.com/saba-futai/sudoku/pkg/obfs/httpmask"
17+
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
18+
19+
N "github.com/metacubex/mihomo/common/net"
20+
"github.com/metacubex/mihomo/component/dialer"
21+
"github.com/metacubex/mihomo/component/proxydialer"
22+
C "github.com/metacubex/mihomo/constant"
23+
)
24+
25+
type Sudoku struct {
26+
*Base
27+
option *SudokuOption
28+
table *sudoku.Table
29+
baseConf apis.ProtocolConfig
30+
}
31+
32+
type SudokuOption struct {
33+
BasicOption
34+
Name string `proxy:"name"`
35+
Server string `proxy:"server"`
36+
Port int `proxy:"port"`
37+
Key string `proxy:"key"`
38+
AEADMethod string `proxy:"aead-method,omitempty"`
39+
PaddingMin *int `proxy:"padding-min,omitempty"`
40+
PaddingMax *int `proxy:"padding-max,omitempty"`
41+
Seed string `proxy:"seed,omitempty"`
42+
TableType string `proxy:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
43+
}
44+
45+
// DialContext implements C.ProxyAdapter
46+
func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
47+
return s.DialContextWithDialer(ctx, dialer.NewDialer(s.DialOptions()...), metadata)
48+
}
49+
50+
// DialContextWithDialer implements C.ProxyAdapter
51+
func (s *Sudoku) DialContextWithDialer(ctx context.Context, d C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
52+
if len(s.option.DialerProxy) > 0 {
53+
d, err = proxydialer.NewByName(s.option.DialerProxy, d)
54+
if err != nil {
55+
return nil, err
56+
}
57+
}
58+
59+
cfg, err := s.buildConfig(metadata)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
c, err := d.DialContext(ctx, "tcp", s.addr)
65+
if err != nil {
66+
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
67+
}
68+
69+
defer func() {
70+
safeConnClose(c, err)
71+
}()
72+
73+
if ctx.Done() != nil {
74+
done := N.SetupContextForConn(ctx, c)
75+
defer done(&err)
76+
}
77+
78+
c, err = s.streamConn(c, cfg)
79+
if err != nil {
80+
return nil, err
81+
}
82+
83+
return NewConn(c, s), nil
84+
}
85+
86+
// ListenPacketContext implements C.ProxyAdapter
87+
func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
88+
return nil, C.ErrNotSupport
89+
}
90+
91+
// SupportUOT implements C.ProxyAdapter
92+
func (s *Sudoku) SupportUOT() bool {
93+
return false // Sudoku protocol only supports TCP
94+
}
95+
96+
// SupportWithDialer implements C.ProxyAdapter
97+
func (s *Sudoku) SupportWithDialer() C.NetWork {
98+
return C.TCP
99+
}
100+
101+
// ProxyInfo implements C.ProxyAdapter
102+
func (s *Sudoku) ProxyInfo() C.ProxyInfo {
103+
info := s.Base.ProxyInfo()
104+
info.DialerProxy = s.option.DialerProxy
105+
return info
106+
}
107+
108+
func (s *Sudoku) buildConfig(metadata *C.Metadata) (*apis.ProtocolConfig, error) {
109+
if metadata == nil || metadata.DstPort == 0 || !metadata.Valid() {
110+
return nil, fmt.Errorf("invalid metadata for sudoku outbound")
111+
}
112+
113+
cfg := s.baseConf
114+
cfg.TargetAddress = metadata.RemoteAddress()
115+
116+
if err := cfg.ValidateClient(); err != nil {
117+
return nil, err
118+
}
119+
return &cfg, nil
120+
}
121+
122+
func (s *Sudoku) streamConn(rawConn net.Conn, cfg *apis.ProtocolConfig) (_ net.Conn, err error) {
123+
if err = httpmask.WriteRandomRequestHeader(rawConn, cfg.ServerAddress); err != nil {
124+
return nil, fmt.Errorf("write http mask failed: %w", err)
125+
}
126+
127+
obfsConn := sudoku.NewConn(rawConn, cfg.Table, cfg.PaddingMin, cfg.PaddingMax, false)
128+
cConn, err := crypto.NewAEADConn(obfsConn, cfg.Key, cfg.AEADMethod)
129+
if err != nil {
130+
return nil, fmt.Errorf("setup crypto failed: %w", err)
131+
}
132+
133+
handshake := buildSudokuHandshakePayload(cfg.Key)
134+
if _, err = cConn.Write(handshake[:]); err != nil {
135+
cConn.Close()
136+
return nil, fmt.Errorf("send handshake failed: %w", err)
137+
}
138+
139+
if err = writeTargetAddress(cConn, cfg.TargetAddress); err != nil {
140+
cConn.Close()
141+
return nil, fmt.Errorf("send target address failed: %w", err)
142+
}
143+
144+
return cConn, nil
145+
}
146+
147+
func NewSudoku(option SudokuOption) (*Sudoku, error) {
148+
if option.Server == "" {
149+
return nil, fmt.Errorf("server is required")
150+
}
151+
if option.Port <= 0 || option.Port > 65535 {
152+
return nil, fmt.Errorf("invalid port: %d", option.Port)
153+
}
154+
if option.Key == "" {
155+
return nil, fmt.Errorf("key is required")
156+
}
157+
158+
tableType := strings.ToLower(option.TableType)
159+
if tableType == "" {
160+
tableType = "prefer_ascii"
161+
}
162+
if tableType != "prefer_ascii" && tableType != "prefer_entropy" {
163+
return nil, fmt.Errorf("table-type must be prefer_ascii or prefer_entropy")
164+
}
165+
166+
seed := option.Seed
167+
if seed == "" {
168+
seed = option.Key
169+
}
170+
171+
table := sudoku.NewTable(seed, tableType)
172+
173+
defaultConf := apis.DefaultConfig()
174+
paddingMin := defaultConf.PaddingMin
175+
paddingMax := defaultConf.PaddingMax
176+
if option.PaddingMin != nil {
177+
paddingMin = *option.PaddingMin
178+
}
179+
if option.PaddingMax != nil {
180+
paddingMax = *option.PaddingMax
181+
}
182+
if option.PaddingMin == nil && option.PaddingMax != nil && paddingMax < paddingMin {
183+
paddingMin = paddingMax
184+
}
185+
if option.PaddingMax == nil && option.PaddingMin != nil && paddingMax < paddingMin {
186+
paddingMax = paddingMin
187+
}
188+
189+
baseConf := apis.ProtocolConfig{
190+
ServerAddress: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
191+
Key: option.Key,
192+
AEADMethod: defaultConf.AEADMethod,
193+
Table: table,
194+
PaddingMin: paddingMin,
195+
PaddingMax: paddingMax,
196+
HandshakeTimeoutSeconds: defaultConf.HandshakeTimeoutSeconds,
197+
}
198+
if option.AEADMethod != "" {
199+
baseConf.AEADMethod = option.AEADMethod
200+
}
201+
202+
return &Sudoku{
203+
Base: &Base{
204+
name: option.Name,
205+
addr: baseConf.ServerAddress,
206+
tp: C.Sudoku,
207+
udp: false,
208+
tfo: option.TFO,
209+
mpTcp: option.MPTCP,
210+
iface: option.Interface,
211+
rmark: option.RoutingMark,
212+
prefer: C.NewDNSPrefer(option.IPVersion),
213+
},
214+
option: &option,
215+
table: table,
216+
baseConf: baseConf,
217+
}, nil
218+
}
219+
220+
func buildSudokuHandshakePayload(key string) [16]byte {
221+
var payload [16]byte
222+
binary.BigEndian.PutUint64(payload[:8], uint64(time.Now().Unix()))
223+
hash := sha256.Sum256([]byte(key))
224+
copy(payload[8:], hash[:8])
225+
return payload
226+
}
227+
228+
func writeTargetAddress(w io.Writer, rawAddr string) error {
229+
host, portStr, err := net.SplitHostPort(rawAddr)
230+
if err != nil {
231+
return err
232+
}
233+
234+
portInt, err := net.LookupPort("tcp", portStr)
235+
if err != nil {
236+
return err
237+
}
238+
239+
var buf []byte
240+
if ip := net.ParseIP(host); ip != nil {
241+
if ip4 := ip.To4(); ip4 != nil {
242+
buf = append(buf, 0x01) // IPv4
243+
buf = append(buf, ip4...)
244+
} else {
245+
buf = append(buf, 0x04) // IPv6
246+
buf = append(buf, ip...)
247+
}
248+
} else {
249+
if len(host) > 255 {
250+
return fmt.Errorf("domain too long")
251+
}
252+
buf = append(buf, 0x03) // domain
253+
buf = append(buf, byte(len(host)))
254+
buf = append(buf, host...)
255+
}
256+
257+
var portBytes [2]byte
258+
binary.BigEndian.PutUint16(portBytes[:], uint16(portInt))
259+
buf = append(buf, portBytes[:]...)
260+
261+
_, err = w.Write(buf)
262+
return err
263+
}

adapter/parser.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
146146
break
147147
}
148148
proxy, err = outbound.NewAnyTLS(*anytlsOption)
149+
case "sudoku":
150+
sudokuOption := &outbound.SudokuOption{}
151+
err = decoder.Decode(mapping, sudokuOption)
152+
if err != nil {
153+
break
154+
}
155+
proxy, err = outbound.NewSudoku(*sudokuOption)
149156
default:
150157
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
151158
}

constant/adapters.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const (
4444
Ssh
4545
Mieru
4646
AnyTLS
47+
Sudoku
4748
)
4849

4950
const (
@@ -230,6 +231,8 @@ func (at AdapterType) String() string {
230231
return "Mieru"
231232
case AnyTLS:
232233
return "AnyTLS"
234+
case Sudoku:
235+
return "Sudoku"
233236
case Relay:
234237
return "Relay"
235238
case Selector:

constant/metadata.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const (
3939
HYSTERIA2
4040
ANYTLS
4141
MIERU
42+
SUDOKU
4243
INNER
4344
)
4445

@@ -112,6 +113,8 @@ func (t Type) String() string {
112113
return "AnyTLS"
113114
case MIERU:
114115
return "Mieru"
116+
case SUDOKU:
117+
return "Sudoku"
115118
case INNER:
116119
return "Inner"
117120
default:
@@ -154,6 +157,8 @@ func ParseType(t string) (*Type, error) {
154157
res = ANYTLS
155158
case "MIERU":
156159
res = MIERU
160+
case "SUDOKU":
161+
res = SUDOKU
157162
case "INNER":
158163
res = INNER
159164
default:

docs/config.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,6 +1038,18 @@ proxies: # socks5
10381038
# 如果想开启 0-RTT 握手,请设置为 HANDSHAKE_NO_WAIT,否则请设置为 HANDSHAKE_STANDARD。默认值为 HANDSHAKE_STANDARD
10391039
# handshake-mode: HANDSHAKE_STANDARD
10401040

1041+
# sudoku
1042+
- name: sudoku
1043+
type: sudoku
1044+
server: serverip # 1.2.3.4
1045+
port: 443
1046+
key: "<client_key>" # 如果你使用sudoku生成的ED25519密钥对,请填写密钥对中的私钥,否则填入和服务端相同的uuid
1047+
aead-method: chacha20-poly1305 # 可选值:chacha20-poly1305、aes-128-gcm、none 我们保证在none的情况下sudoku混淆层仍然确保安全
1048+
padding-min: 2 # 最小填充字节数
1049+
padding-max: 7 # 最大填充字节数
1050+
seed: "<seed-or-key>" # 如果使用sudoku生成的ED25519密钥对,请填写密钥对中的公钥(如果你有安全焦虑,填入私钥也可以,只是私钥长度比较长不好看而已),否则填入和服务端相同的uuid
1051+
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy 前者全ascii映射,后者保证熵值(汉明1)低于3
1052+
10411053
# anytls
10421054
- name: anytls
10431055
type: anytls
@@ -1567,6 +1579,19 @@ listeners:
15671579
username1: password1
15681580
username2: password2
15691581

1582+
- name: sudoku-in-1
1583+
type: sudoku
1584+
port: 8443 # 仅支持单端口
1585+
listen: 0.0.0.0
1586+
key: "<server_key>" # 如果你使用sudoku生成的ED25519密钥对,此处是密钥对中的公钥,当然,你也可以仅仅使用任意uuid充当key
1587+
aead-method: chacha20-poly1305 # 支持chacha20-poly1305或者aes-128-gcm以及none,sudoku的混淆层可以确保none情况下数据安全
1588+
padding-min: 1 # 填充最小长度
1589+
padding-max: 15 # 填充最大长度,均不建议过大
1590+
seed: "<seed-or-key>" # 如果你不使用ED25519密钥对,就请填入客户端的key,否则仍然是公钥
1591+
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy 前者全ascii映射,后者保证熵值(汉明1)低于3
1592+
handshake-timeout: 5 # optional
1593+
1594+
15701595
- name: trojan-in-1
15711596
type: trojan
15721597
port: 10819 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503
@@ -1715,3 +1740,4 @@ listeners:
17151740
# alpn:
17161741
# - h3
17171742
# max-udp-relay-packet-size: 1500
1743+

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/bahlo/generic-list-go v0.2.0
77
github.com/coreos/go-iptables v0.8.0
88
github.com/dlclark/regexp2 v1.11.5
9-
github.com/enfein/mieru/v3 v3.23.0
9+
github.com/enfein/mieru/v3 v3.24.0
1010
github.com/go-chi/chi/v5 v5.2.3
1111
github.com/go-chi/render v1.0.3
1212
github.com/gobwas/ws v1.4.0
@@ -43,6 +43,7 @@ require (
4343
github.com/mroth/weightedrand/v2 v2.1.0
4444
github.com/openacid/low v0.1.21
4545
github.com/oschwald/maxminddb-golang v1.12.0 // lastest version compatible with golang1.20
46+
github.com/saba-futai/sudoku v0.0.1-e
4647
github.com/sagernet/cors v1.2.1
4748
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
4849
github.com/samber/lo v1.52.0
@@ -63,6 +64,7 @@ require (
6364
)
6465

6566
require (
67+
filippo.io/edwards25519 v1.1.0 // indirect
6668
github.com/RyuaNerin/go-krypto v1.3.0 // indirect
6769
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
6870
github.com/ajg/form v1.5.1 // indirect

0 commit comments

Comments
 (0)