Update On Tue Dec 23 19:42:16 CET 2025

This commit is contained in:
github-action[bot]
2025-12-23 19:42:17 +01:00
parent 9b108b813f
commit 52b2acfaee
128 changed files with 8723 additions and 567 deletions

1
.github/update.log vendored
View File

@@ -1220,3 +1220,4 @@ Update On Fri Dec 19 19:41:31 CET 2025
Update On Sat Dec 20 19:35:51 CET 2025
Update On Sun Dec 21 19:39:06 CET 2025
Update On Mon Dec 22 19:42:47 CET 2025
Update On Tue Dec 23 19:42:08 CET 2025

View File

@@ -30,6 +30,9 @@ type SudokuOption struct {
TableType string `proxy:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
EnablePureDownlink *bool `proxy:"enable-pure-downlink,omitempty"`
HTTPMask bool `proxy:"http-mask,omitempty"`
HTTPMaskMode string `proxy:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
HTTPMaskTLS bool `proxy:"http-mask-tls,omitempty"` // only for http-mask-mode stream/poll/auto
HTTPMaskHost string `proxy:"http-mask-host,omitempty"` // optional Host/SNI override (domain or domain:port)
HTTPMaskStrategy string `proxy:"http-mask-strategy,omitempty"` // "random" (default), "post", "websocket"
CustomTable string `proxy:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
CustomTables []string `proxy:"custom-tables,omitempty"` // optional table rotation patterns, overrides custom-table when non-empty
@@ -42,7 +45,16 @@ func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
return nil, err
}
c, err := s.dialer.DialContext(ctx, "tcp", s.addr)
var c net.Conn
if !cfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext)
}
}
if c == nil && err == nil {
c, err = s.dialer.DialContext(ctx, "tcp", s.addr)
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
}
@@ -56,9 +68,14 @@ func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
defer done(&err)
}
c, err = sudoku.ClientHandshakeWithOptions(c, cfg, sudoku.ClientHandshakeOptions{
HTTPMaskStrategy: s.option.HTTPMaskStrategy,
})
handshakeCfg := *cfg
if !handshakeCfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(handshakeCfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
handshakeCfg.DisableHTTPMask = true
}
}
c, err = sudoku.ClientHandshakeWithOptions(c, &handshakeCfg, sudoku.ClientHandshakeOptions{HTTPMaskStrategy: s.option.HTTPMaskStrategy})
if err != nil {
return nil, err
}
@@ -87,7 +104,16 @@ func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
return nil, err
}
c, err := s.dialer.DialContext(ctx, "tcp", s.addr)
var c net.Conn
if !cfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext)
}
}
if c == nil && err == nil {
c, err = s.dialer.DialContext(ctx, "tcp", s.addr)
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
}
@@ -101,9 +127,14 @@ func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
defer done(&err)
}
c, err = sudoku.ClientHandshakeWithOptions(c, cfg, sudoku.ClientHandshakeOptions{
HTTPMaskStrategy: s.option.HTTPMaskStrategy,
})
handshakeCfg := *cfg
if !handshakeCfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(handshakeCfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
handshakeCfg.DisableHTTPMask = true
}
}
c, err = sudoku.ClientHandshakeWithOptions(c, &handshakeCfg, sudoku.ClientHandshakeOptions{HTTPMaskStrategy: s.option.HTTPMaskStrategy})
if err != nil {
return nil, err
}
@@ -190,6 +221,12 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
EnablePureDownlink: enablePureDownlink,
HandshakeTimeoutSeconds: defaultConf.HandshakeTimeoutSeconds,
DisableHTTPMask: !option.HTTPMask,
HTTPMaskMode: defaultConf.HTTPMaskMode,
HTTPMaskTLSEnabled: option.HTTPMaskTLS,
HTTPMaskHost: option.HTTPMaskHost,
}
if option.HTTPMaskMode != "" {
baseConf.HTTPMaskMode = option.HTTPMaskMode
}
tables, err := sudoku.NewTablesWithCustomPatterns(sudoku.ClientAEADSeed(option.Key), tableType, option.CustomTable, option.CustomTables)
if err != nil {

View File

@@ -1041,7 +1041,7 @@ proxies: # socks5
# sudoku
- name: sudoku
type: sudoku
server: serverip # 1.2.3.4
server: server_ip/domain # 1.2.3.4 or domain
port: 443
key: "<client_key>" # 如果你使用sudoku生成的ED25519密钥对请填写密钥对中的私钥否则填入和服务端相同的uuid
aead-method: chacha20-poly1305 # 可选值chacha20-poly1305、aes-128-gcm、none 我们保证在none的情况下sudoku混淆层仍然确保安全
@@ -1051,7 +1051,10 @@ proxies: # socks5
# custom-table: xpxvvpvv # 可选自定义字节布局必须包含2个x、2个p、4个v可随意组合。启用此处则需配置`table-type`为`prefer_entropy`
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选自定义字节布局列表x/v/p用于 xvp 模式轮换;非空时覆盖 custom-table
http-mask: true # 是否启用http掩码
# http-mask-strategy: random # 可选random默认、post、websocket仅在 http-mask=true 时生效
# http-mask-mode: legacy # 可选legacy默认、stream、poll、autostream/poll/auto 支持走 CDN/反代
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效true 强制 httpsfalse 强制 http不会根据端口自动推断
# http-mask-host: "" # 可选:覆盖 Host/SNI支持 example.com 或 example.com:443仅在 http-mask-mode 为 stream/poll/auto 时生效
# http-mask-strategy: random # 可选random默认、post、websocket仅 legacy 下生效
enable-pure-downlink: false # 是否启用混淆下行false的情况下能在保证数据安全的前提下极大提升下行速度与服务端端保持相同(如果此处为false则要求aead不可为none)
# anytls
@@ -1596,6 +1599,8 @@ listeners:
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选自定义字节布局列表x/v/p用于 xvp 模式轮换;非空时覆盖 custom-table
handshake-timeout: 5 # optional
enable-pure-downlink: false # 是否启用混淆下行false的情况下能在保证数据安全的前提下极大提升下行速度与客户端保持相同(如果此处为false则要求aead不可为none)
disable-http-mask: false # 可选:禁用 http 掩码/隧道(默认为 false
# http-mask-mode: legacy # 可选legacy默认、stream、poll、autostream/poll/auto 支持走 CDN/反代

View File

@@ -3,6 +3,7 @@ module github.com/metacubex/mihomo
go 1.20
require (
filippo.io/edwards25519 v1.1.0
github.com/bahlo/generic-list-go v0.2.0
github.com/coreos/go-iptables v0.8.0
github.com/dlclark/regexp2 v1.11.5
@@ -46,7 +47,6 @@ 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.2-d
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
github.com/samber/lo v1.52.0
github.com/sirupsen/logrus v1.9.3
@@ -66,7 +66,6 @@ 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

View File

@@ -170,8 +170,6 @@ 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/saba-futai/sudoku v0.0.2-d h1:HW/gIyNUFcDchpMN+ZhluM86U/HGkWkkRV+9Km6WZM8=
github.com/saba-futai/sudoku v0.0.2-d/go.mod h1:Rvggsoprp7HQM7bMIZUd1M27bPj8THRsZdY1dGbIAvo=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=

View File

@@ -20,6 +20,8 @@ type SudokuServer struct {
EnablePureDownlink *bool `json:"enable-pure-downlink,omitempty"`
CustomTable string `json:"custom-table,omitempty"`
CustomTables []string `json:"custom-tables,omitempty"`
DisableHTTPMask bool `json:"disable-http-mask,omitempty"`
HTTPMaskMode string `json:"http-mask-mode,omitempty"`
// mihomo private extension (not the part of standard Sudoku protocol)
MuxOption sing.MuxOption `json:"mux-option,omitempty"`

View File

@@ -3,7 +3,9 @@ package inbound_test
import (
"net"
"net/netip"
"runtime"
"strconv"
"strings"
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
@@ -149,6 +151,9 @@ func TestNewMieru(t *testing.T) {
}
func TestInboundMieru(t *testing.T) {
if runtime.GOOS == "windows" && strings.HasPrefix(runtime.Version(), "go1.26") {
t.Skip("temporarily skipped on windows due to intermittent failures; tracked in PR")
}
t.Run("TCP_HANDSHAKE_STANDARD", func(t *testing.T) {
testInboundMieruTCP(t, "HANDSHAKE_STANDARD")
})

View File

@@ -22,6 +22,8 @@ type SudokuOption struct {
EnablePureDownlink *bool `inbound:"enable-pure-downlink,omitempty"`
CustomTable string `inbound:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
CustomTables []string `inbound:"custom-tables,omitempty"`
DisableHTTPMask bool `inbound:"disable-http-mask,omitempty"`
HTTPMaskMode string `inbound:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
// mihomo private extension (not the part of standard Sudoku protocol)
MuxOption MuxOption `inbound:"mux-option,omitempty"`
@@ -59,6 +61,8 @@ func NewSudoku(options *SudokuOption) (*Sudoku, error) {
EnablePureDownlink: options.EnablePureDownlink,
CustomTable: options.CustomTable,
CustomTables: options.CustomTables,
DisableHTTPMask: options.DisableHTTPMask,
HTTPMaskMode: options.HTTPMaskMode,
}
serverConf.MuxOption = options.MuxOption.Build()

View File

@@ -2,6 +2,7 @@ package inbound_test
import (
"net/netip"
"runtime"
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
@@ -164,3 +165,27 @@ func TestInboundSudoku_CustomTable(t *testing.T) {
testInboundSudoku(t, inboundOptions, outboundOptions)
})
}
func TestInboundSudoku_HTTPMaskMode(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("temporarily skipped on windows due to intermittent failures; tracked in PR")
}
key := "test_key_http_mask_mode"
for _, mode := range []string{"legacy", "stream", "poll", "auto"} {
mode := mode
t.Run(mode, func(t *testing.T) {
inboundOptions := inbound.SudokuOption{
Key: key,
HTTPMaskMode: mode,
}
outboundOptions := outbound.SudokuOption{
Key: key,
HTTPMask: true,
HTTPMaskMode: mode,
}
testInboundSudoku(t, inboundOptions, outboundOptions)
})
}
}

View File

@@ -20,6 +20,7 @@ type Listener struct {
addr string
closed bool
protoConf sudoku.ProtocolConfig
tunnelSrv *sudoku.HTTPMaskTunnelServer
handler *sing.ListenerHandler
}
@@ -46,9 +47,31 @@ func (l *Listener) Close() error {
}
func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
session, err := sudoku.ServerHandshake(conn, &l.protoConf)
handshakeConn := conn
handshakeCfg := &l.protoConf
if l.tunnelSrv != nil {
c, cfg, done, err := l.tunnelSrv.WrapConn(conn)
if err != nil {
_ = conn.Close()
return
}
if done {
return
}
if c != nil {
handshakeConn = c
}
if cfg != nil {
handshakeCfg = cfg
}
}
session, err := sudoku.ServerHandshake(handshakeConn, handshakeCfg)
if err != nil {
_ = conn.Close()
_ = handshakeConn.Close()
if handshakeConn != conn {
_ = conn.Close()
}
return
}
@@ -184,6 +207,8 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
PaddingMax: paddingMax,
EnablePureDownlink: enablePureDownlink,
HandshakeTimeoutSeconds: handshakeTimeout,
DisableHTTPMask: config.DisableHTTPMask,
HTTPMaskMode: config.HTTPMaskMode,
}
if len(tables) == 1 {
protoConf.Table = tables[0]
@@ -200,6 +225,7 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
protoConf: protoConf,
handler: h,
}
sl.tunnelSrv = sudoku.NewHTTPMaskTunnelServer(&sl.protoConf)
go func() {
for {

View File

@@ -0,0 +1,144 @@
package sudoku
import (
"fmt"
"strings"
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
// ProtocolConfig defines the configuration for the Sudoku protocol stack.
// It is intentionally kept close to the upstream Sudoku project to ensure wire compatibility.
type ProtocolConfig struct {
// Client-only: "host:port".
ServerAddress string
// Pre-shared key (or ED25519 key material) used to derive crypto and tables.
Key string
// "aes-128-gcm", "chacha20-poly1305", or "none".
AEADMethod string
// Table is the single obfuscation table to use when table rotation is disabled.
Table *sudoku.Table
// Tables is an optional candidate set for table rotation.
// If provided (len>0), the client will pick one table per connection and the server will
// probe the handshake to detect which one was used, keeping the handshake format unchanged.
// When Tables is set, Table may be nil.
Tables []*sudoku.Table
// Padding insertion ratio (0-100). Must satisfy PaddingMax >= PaddingMin.
PaddingMin int
PaddingMax int
// EnablePureDownlink toggles the bandwidth-optimized downlink mode.
EnablePureDownlink bool
// Client-only: final target "host:port".
TargetAddress string
// Server-side handshake timeout (seconds).
HandshakeTimeoutSeconds int
// DisableHTTPMask disables all HTTP camouflage layers.
DisableHTTPMask bool
// HTTPMaskMode controls how the HTTP layer behaves:
// - "legacy": write a fake HTTP/1.1 header then switch to raw stream (default, not CDN-compatible)
// - "stream": real HTTP tunnel (stream-one or split), CDN-compatible
// - "poll": plain HTTP tunnel (authorize/push/pull), strong restricted-network pass-through
// - "auto": try stream then fall back to poll
HTTPMaskMode string
// HTTPMaskTLSEnabled enables HTTPS for HTTP tunnel modes (client-side).
// If false, the tunnel uses HTTP (no port-based inference).
HTTPMaskTLSEnabled bool
// HTTPMaskHost optionally overrides the HTTP Host header / SNI host for HTTP tunnel modes (client-side).
HTTPMaskHost string
}
func (c *ProtocolConfig) Validate() error {
if c.Table == nil && len(c.Tables) == 0 {
return fmt.Errorf("table cannot be nil (or provide tables)")
}
for i, t := range c.Tables {
if t == nil {
return fmt.Errorf("tables[%d] cannot be nil", i)
}
}
if c.Key == "" {
return fmt.Errorf("key cannot be empty")
}
switch c.AEADMethod {
case "aes-128-gcm", "chacha20-poly1305", "none":
default:
return fmt.Errorf("invalid aead-method: %s, must be one of: aes-128-gcm, chacha20-poly1305, none", c.AEADMethod)
}
if c.PaddingMin < 0 || c.PaddingMin > 100 {
return fmt.Errorf("padding-min must be between 0 and 100, got %d", c.PaddingMin)
}
if c.PaddingMax < 0 || c.PaddingMax > 100 {
return fmt.Errorf("padding-max must be between 0 and 100, got %d", c.PaddingMax)
}
if c.PaddingMax < c.PaddingMin {
return fmt.Errorf("padding-max (%d) must be >= padding-min (%d)", c.PaddingMax, c.PaddingMin)
}
if !c.EnablePureDownlink && c.AEADMethod == "none" {
return fmt.Errorf("bandwidth optimized downlink requires AEAD")
}
if c.HandshakeTimeoutSeconds < 0 {
return fmt.Errorf("handshake-timeout must be >= 0, got %d", c.HandshakeTimeoutSeconds)
}
switch strings.ToLower(strings.TrimSpace(c.HTTPMaskMode)) {
case "", "legacy", "stream", "poll", "auto":
default:
return fmt.Errorf("invalid http-mask-mode: %s, must be one of: legacy, stream, poll, auto", c.HTTPMaskMode)
}
return nil
}
func (c *ProtocolConfig) ValidateClient() error {
if err := c.Validate(); err != nil {
return err
}
if c.ServerAddress == "" {
return fmt.Errorf("server address cannot be empty")
}
if c.TargetAddress == "" {
return fmt.Errorf("target address cannot be empty")
}
return nil
}
func DefaultConfig() *ProtocolConfig {
return &ProtocolConfig{
AEADMethod: "chacha20-poly1305",
PaddingMin: 10,
PaddingMax: 30,
EnablePureDownlink: true,
HandshakeTimeoutSeconds: 5,
HTTPMaskMode: "legacy",
}
}
func (c *ProtocolConfig) tableCandidates() []*sudoku.Table {
if c == nil {
return nil
}
if len(c.Tables) > 0 {
return c.Tables
}
if c.Table != nil {
return []*sudoku.Table{c.Table}
}
return nil
}

View File

@@ -0,0 +1,130 @@
package crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"golang.org/x/crypto/chacha20poly1305"
)
type AEADConn struct {
net.Conn
aead cipher.AEAD
readBuf bytes.Buffer
nonceSize int
}
func NewAEADConn(c net.Conn, key string, method string) (*AEADConn, error) {
if method == "none" {
return &AEADConn{Conn: c, aead: nil}, nil
}
h := sha256.New()
h.Write([]byte(key))
keyBytes := h.Sum(nil)
var aead cipher.AEAD
var err error
switch method {
case "aes-128-gcm":
block, _ := aes.NewCipher(keyBytes[:16])
aead, err = cipher.NewGCM(block)
case "chacha20-poly1305":
aead, err = chacha20poly1305.New(keyBytes)
default:
return nil, fmt.Errorf("unsupported cipher: %s", method)
}
if err != nil {
return nil, err
}
return &AEADConn{
Conn: c,
aead: aead,
nonceSize: aead.NonceSize(),
}, nil
}
func (cc *AEADConn) Write(p []byte) (int, error) {
if cc.aead == nil {
return cc.Conn.Write(p)
}
maxPayload := 65535 - cc.nonceSize - cc.aead.Overhead()
totalWritten := 0
var frameBuf bytes.Buffer
header := make([]byte, 2)
nonce := make([]byte, cc.nonceSize)
for len(p) > 0 {
chunkSize := len(p)
if chunkSize > maxPayload {
chunkSize = maxPayload
}
chunk := p[:chunkSize]
p = p[chunkSize:]
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return totalWritten, err
}
ciphertext := cc.aead.Seal(nil, nonce, chunk, nil)
frameLen := len(nonce) + len(ciphertext)
binary.BigEndian.PutUint16(header, uint16(frameLen))
frameBuf.Reset()
frameBuf.Write(header)
frameBuf.Write(nonce)
frameBuf.Write(ciphertext)
if _, err := cc.Conn.Write(frameBuf.Bytes()); err != nil {
return totalWritten, err
}
totalWritten += chunkSize
}
return totalWritten, nil
}
func (cc *AEADConn) Read(p []byte) (int, error) {
if cc.aead == nil {
return cc.Conn.Read(p)
}
if cc.readBuf.Len() > 0 {
return cc.readBuf.Read(p)
}
header := make([]byte, 2)
if _, err := io.ReadFull(cc.Conn, header); err != nil {
return 0, err
}
frameLen := int(binary.BigEndian.Uint16(header))
body := make([]byte, frameLen)
if _, err := io.ReadFull(cc.Conn, body); err != nil {
return 0, err
}
if len(body) < cc.nonceSize {
return 0, errors.New("frame too short")
}
nonce := body[:cc.nonceSize]
ciphertext := body[cc.nonceSize:]
plaintext, err := cc.aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
return 0, errors.New("decryption failed")
}
cc.readBuf.Write(plaintext)
return cc.readBuf.Read(p)
}

View File

@@ -0,0 +1,116 @@
package crypto
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"filippo.io/edwards25519"
)
// KeyPair holds the scalar private key and point public key
type KeyPair struct {
Private *edwards25519.Scalar
Public *edwards25519.Point
}
// GenerateMasterKey generates a random master private key (scalar) and its public key (point)
func GenerateMasterKey() (*KeyPair, error) {
// 1. Generate random scalar x (32 bytes)
var seed [64]byte
if _, err := rand.Read(seed[:]); err != nil {
return nil, err
}
x, err := edwards25519.NewScalar().SetUniformBytes(seed[:])
if err != nil {
return nil, err
}
// 2. Calculate Public Key P = x * G
P := new(edwards25519.Point).ScalarBaseMult(x)
return &KeyPair{Private: x, Public: P}, nil
}
// SplitPrivateKey takes a master private key x and returns a new random split key (r, k)
// such that x = r + k (mod L).
// Returns hex encoded string of r || k (64 bytes)
func SplitPrivateKey(x *edwards25519.Scalar) (string, error) {
// 1. Generate random r (32 bytes)
var seed [64]byte
if _, err := rand.Read(seed[:]); err != nil {
return "", err
}
r, err := edwards25519.NewScalar().SetUniformBytes(seed[:])
if err != nil {
return "", err
}
// 2. Calculate k = x - r (mod L)
k := new(edwards25519.Scalar).Subtract(x, r)
// 3. Encode r and k
rBytes := r.Bytes()
kBytes := k.Bytes()
full := make([]byte, 64)
copy(full[:32], rBytes)
copy(full[32:], kBytes)
return hex.EncodeToString(full), nil
}
// RecoverPublicKey takes a split private key (r, k) or a master private key (x)
// and returns the public key P.
// Input can be:
// - 32 bytes hex (Master Scalar x)
// - 64 bytes hex (Split Key r || k)
func RecoverPublicKey(keyHex string) (*edwards25519.Point, error) {
keyBytes, err := hex.DecodeString(keyHex)
if err != nil {
return nil, fmt.Errorf("invalid hex: %w", err)
}
if len(keyBytes) == 32 {
// Master Key x
x, err := edwards25519.NewScalar().SetCanonicalBytes(keyBytes)
if err != nil {
return nil, fmt.Errorf("invalid scalar: %w", err)
}
return new(edwards25519.Point).ScalarBaseMult(x), nil
} else if len(keyBytes) == 64 {
// Split Key r || k
rBytes := keyBytes[:32]
kBytes := keyBytes[32:]
r, err := edwards25519.NewScalar().SetCanonicalBytes(rBytes)
if err != nil {
return nil, fmt.Errorf("invalid scalar r: %w", err)
}
k, err := edwards25519.NewScalar().SetCanonicalBytes(kBytes)
if err != nil {
return nil, fmt.Errorf("invalid scalar k: %w", err)
}
// sum = r + k
sum := new(edwards25519.Scalar).Add(r, k)
// P = sum * G
return new(edwards25519.Point).ScalarBaseMult(sum), nil
}
return nil, errors.New("invalid key length: must be 32 bytes (Master) or 64 bytes (Split)")
}
// EncodePoint returns the hex string of the compressed point
func EncodePoint(p *edwards25519.Point) string {
return hex.EncodeToString(p.Bytes())
}
// EncodeScalar returns the hex string of the scalar
func EncodeScalar(s *edwards25519.Scalar) string {
return hex.EncodeToString(s.Bytes())
}

View File

@@ -7,7 +7,7 @@ import (
"testing"
"time"
sudokuobfs "github.com/saba-futai/sudoku/pkg/obfs/sudoku"
sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
type discardConn struct{}

View File

@@ -11,18 +11,13 @@ import (
"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"
"github.com/metacubex/mihomo/transport/sudoku/crypto"
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
"github.com/metacubex/mihomo/log"
)
type ProtocolConfig = apis.ProtocolConfig
func DefaultConfig() *ProtocolConfig { return apis.DefaultConfig() }
type SessionType int
const (
@@ -105,14 +100,14 @@ const (
downlinkModePacked byte = 0x02
)
func downlinkMode(cfg *apis.ProtocolConfig) byte {
func downlinkMode(cfg *ProtocolConfig) byte {
if cfg.EnablePureDownlink {
return downlinkModePure
}
return downlinkModePacked
}
func buildClientObfsConn(raw net.Conn, cfg *apis.ProtocolConfig, table *sudoku.Table) net.Conn {
func buildClientObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table) net.Conn {
baseReader := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false)
baseWriter := newSudokuObfsWriter(raw, table, cfg.PaddingMin, cfg.PaddingMax)
if cfg.EnablePureDownlink {
@@ -130,7 +125,7 @@ func buildClientObfsConn(raw net.Conn, cfg *apis.ProtocolConfig, table *sudoku.T
}
}
func buildServerObfsConn(raw net.Conn, cfg *apis.ProtocolConfig, table *sudoku.Table, record bool) (*sudoku.Conn, net.Conn) {
func buildServerObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table, record bool) (*sudoku.Conn, net.Conn) {
uplink := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, record)
if cfg.EnablePureDownlink {
downlink := &directionalConn{
@@ -189,12 +184,12 @@ type ClientHandshakeOptions struct {
}
// ClientHandshake performs the client-side Sudoku handshake (without sending target address).
func ClientHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (net.Conn, error) {
func ClientHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, error) {
return ClientHandshakeWithOptions(rawConn, cfg, ClientHandshakeOptions{})
}
// ClientHandshakeWithOptions performs the client-side Sudoku handshake (without sending target address).
func ClientHandshakeWithOptions(rawConn net.Conn, cfg *apis.ProtocolConfig, opt ClientHandshakeOptions) (net.Conn, error) {
func ClientHandshakeWithOptions(rawConn net.Conn, cfg *ProtocolConfig, opt ClientHandshakeOptions) (net.Conn, error) {
if cfg == nil {
return nil, fmt.Errorf("config is required")
}
@@ -220,7 +215,7 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *apis.ProtocolConfig, opt
}
handshake := buildHandshakePayload(cfg.Key)
if len(tableCandidates(cfg)) > 1 {
if len(cfg.tableCandidates()) > 1 {
handshake[15] = tableID
}
if _, err := cConn.Write(handshake[:]); err != nil {
@@ -236,7 +231,7 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *apis.ProtocolConfig, opt
}
// ServerHandshake performs Sudoku server-side handshake and detects UoT preface.
func ServerHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (*ServerSession, error) {
func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (*ServerSession, error) {
if cfg == nil {
return nil, fmt.Errorf("config is required")
}
@@ -260,7 +255,7 @@ func ServerHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (*ServerSession
}
}
selectedTable, preRead, err := selectTableByProbe(bufReader, cfg, tableCandidates(cfg))
selectedTable, preRead, err := selectTableByProbe(bufReader, cfg, cfg.tableCandidates())
if err != nil {
return nil, err
}

View File

@@ -9,8 +9,7 @@ import (
"testing"
"time"
"github.com/saba-futai/sudoku/apis"
sudokuobfs "github.com/saba-futai/sudoku/pkg/obfs/sudoku"
sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
func TestPackedConnRoundTrip_WithPadding(t *testing.T) {
@@ -67,8 +66,8 @@ func TestPackedConnRoundTrip_WithPadding(t *testing.T) {
}
}
func newPackedConfig(table *sudokuobfs.Table) *apis.ProtocolConfig {
cfg := apis.DefaultConfig()
func newPackedConfig(table *sudokuobfs.Table) *ProtocolConfig {
cfg := DefaultConfig()
cfg.Key = "sudoku-test-key"
cfg.Table = table
cfg.PaddingMin = 10
@@ -118,7 +117,7 @@ func TestPackedDownlinkSoak(t *testing.T) {
}
}
func runPackedTCPSession(id int, cfg *apis.ProtocolConfig, errCh chan<- error) {
func runPackedTCPSession(id int, cfg *ProtocolConfig, errCh chan<- error) {
serverConn, clientConn := net.Pipe()
target := fmt.Sprintf("1.1.1.%d:80", (id%200)+1)
payload := []byte{0x42, byte(id)}
@@ -176,7 +175,7 @@ func runPackedTCPSession(id int, cfg *apis.ProtocolConfig, errCh chan<- error) {
}
}
func runPackedUoTSession(id int, cfg *apis.ProtocolConfig, errCh chan<- error) {
func runPackedUoTSession(id int, cfg *ProtocolConfig, errCh chan<- error) {
serverConn, clientConn := net.Pipe()
target := "8.8.8.8:53"
payload := []byte{0xaa, byte(id)}

View File

@@ -10,7 +10,7 @@ import (
"sync"
"time"
"github.com/saba-futai/sudoku/pkg/obfs/httpmask"
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
)
var (

View File

@@ -0,0 +1,88 @@
package sudoku
import (
"context"
"fmt"
"net"
"strings"
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
)
type HTTPMaskTunnelServer struct {
cfg *ProtocolConfig
ts *httpmask.TunnelServer
}
func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer {
if cfg == nil {
return &HTTPMaskTunnelServer{}
}
var ts *httpmask.TunnelServer
if !cfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{Mode: cfg.HTTPMaskMode})
}
}
return &HTTPMaskTunnelServer{cfg: cfg, ts: ts}
}
// WrapConn inspects an accepted TCP connection and upgrades it to an HTTP tunnel stream when needed.
//
// Returns:
// - done=true: this TCP connection has been fully handled (e.g., stream/poll control request), caller should return
// - done=false: handshakeConn+cfg are ready for ServerHandshake
func (s *HTTPMaskTunnelServer) WrapConn(rawConn net.Conn) (handshakeConn net.Conn, cfg *ProtocolConfig, done bool, err error) {
if rawConn == nil {
return nil, nil, true, fmt.Errorf("nil conn")
}
if s == nil {
return rawConn, nil, false, nil
}
if s.ts == nil {
return rawConn, s.cfg, false, nil
}
res, c, err := s.ts.HandleConn(rawConn)
if err != nil {
return nil, nil, true, err
}
switch res {
case httpmask.HandleDone:
return nil, nil, true, nil
case httpmask.HandlePassThrough:
return c, s.cfg, false, nil
case httpmask.HandleStartTunnel:
inner := *s.cfg
inner.DisableHTTPMask = true
return c, &inner, false, nil
default:
return nil, nil, true, nil
}
}
type TunnelDialer func(ctx context.Context, network, addr string) (net.Conn, error)
// DialHTTPMaskTunnel dials a CDN-capable HTTP tunnel (stream/poll/auto) and returns a stream carrying raw Sudoku bytes.
func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *ProtocolConfig, dial TunnelDialer) (net.Conn, error) {
if cfg == nil {
return nil, fmt.Errorf("config is required")
}
if cfg.DisableHTTPMask {
return nil, fmt.Errorf("http mask is disabled")
}
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
default:
return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode)
}
return httpmask.DialTunnel(ctx, serverAddress, httpmask.TunnelDialOptions{
Mode: cfg.HTTPMaskMode,
TLSEnabled: cfg.HTTPMaskTLSEnabled,
HostOverride: cfg.HTTPMaskHost,
DialContext: dial,
})
}

View File

@@ -0,0 +1,445 @@
package sudoku
import (
"bytes"
"context"
"fmt"
"io"
"net"
"strings"
"sync"
"testing"
"time"
)
func startTunnelServer(t *testing.T, cfg *ProtocolConfig, handle func(*ServerSession) error) (addr string, stop func(), errCh <-chan error) {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
errC := make(chan error, 128)
done := make(chan struct{})
tunnelSrv := NewHTTPMaskTunnelServer(cfg)
var wg sync.WaitGroup
var stopOnce sync.Once
wg.Add(1)
go func() {
defer wg.Done()
for {
c, err := ln.Accept()
if err != nil {
close(done)
return
}
wg.Add(1)
go func(conn net.Conn) {
defer wg.Done()
handshakeConn, handshakeCfg, handled, err := tunnelSrv.WrapConn(conn)
if err != nil {
_ = conn.Close()
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
return
}
if err == io.EOF {
return
}
errC <- err
return
}
if handled {
return
}
if handshakeConn == nil || handshakeCfg == nil {
_ = conn.Close()
errC <- fmt.Errorf("wrap conn returned nil")
return
}
session, err := ServerHandshake(handshakeConn, handshakeCfg)
if err != nil {
_ = handshakeConn.Close()
if handshakeConn != conn {
_ = conn.Close()
}
errC <- err
return
}
defer session.Conn.Close()
if handleErr := handle(session); handleErr != nil {
errC <- handleErr
}
}(c)
}
}()
stop = func() {
stopOnce.Do(func() {
_ = ln.Close()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatalf("server did not stop")
}
ch := make(chan struct{})
go func() {
wg.Wait()
close(ch)
}()
select {
case <-ch:
case <-time.After(10 * time.Second):
t.Fatalf("server goroutines did not exit")
}
close(errC)
})
}
return ln.Addr().String(), stop, errC
}
func newTunnelTestTable(t *testing.T, key string) *ProtocolConfig {
t.Helper()
tables, err := NewTablesWithCustomPatterns(ClientAEADSeed(key), "prefer_ascii", "", nil)
if err != nil {
t.Fatalf("build tables: %v", err)
}
if len(tables) != 1 {
t.Fatalf("unexpected tables: %d", len(tables))
}
cfg := DefaultConfig()
cfg.Key = key
cfg.AEADMethod = "chacha20-poly1305"
cfg.Table = tables[0]
cfg.PaddingMin = 0
cfg.PaddingMax = 0
cfg.HandshakeTimeoutSeconds = 5
cfg.EnablePureDownlink = true
cfg.DisableHTTPMask = false
return cfg
}
func TestHTTPMaskTunnel_Stream_TCPRoundTrip(t *testing.T) {
key := "tunnel-stream-key"
target := "1.1.1.1:80"
serverCfg := newTunnelTestTable(t, key)
serverCfg.HTTPMaskMode = "stream"
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
if s.Type != SessionTypeTCP {
return fmt.Errorf("unexpected session type: %v", s.Type)
}
if s.Target != target {
return fmt.Errorf("target mismatch: %s", s.Target)
}
_, _ = s.Conn.Write([]byte("ok"))
return nil
})
defer stop()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
clientCfg := *serverCfg
clientCfg.ServerAddress = addr
clientCfg.HTTPMaskHost = "example.com"
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
defer tunnelConn.Close()
handshakeCfg := clientCfg
handshakeCfg.DisableHTTPMask = true
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
if err != nil {
t.Fatalf("client handshake: %v", err)
}
defer cConn.Close()
addrBuf, err := EncodeAddress(target)
if err != nil {
t.Fatalf("encode addr: %v", err)
}
if _, err := cConn.Write(addrBuf); err != nil {
t.Fatalf("write addr: %v", err)
}
buf := make([]byte, 2)
if _, err := io.ReadFull(cConn, buf); err != nil {
t.Fatalf("read: %v", err)
}
if string(buf) != "ok" {
t.Fatalf("unexpected payload: %q", buf)
}
stop()
for err := range errCh {
t.Fatalf("server error: %v", err)
}
}
func TestHTTPMaskTunnel_Poll_UoTRoundTrip(t *testing.T) {
key := "tunnel-poll-key"
target := "8.8.8.8:53"
payload := []byte{0xaa, 0xbb, 0xcc, 0xdd}
serverCfg := newTunnelTestTable(t, key)
serverCfg.HTTPMaskMode = "poll"
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
if s.Type != SessionTypeUoT {
return fmt.Errorf("unexpected session type: %v", s.Type)
}
gotAddr, gotPayload, err := ReadDatagram(s.Conn)
if err != nil {
return fmt.Errorf("server read datagram: %w", err)
}
if gotAddr != target {
return fmt.Errorf("uot target mismatch: %s", gotAddr)
}
if !bytes.Equal(gotPayload, payload) {
return fmt.Errorf("uot payload mismatch: %x", gotPayload)
}
if err := WriteDatagram(s.Conn, gotAddr, gotPayload); err != nil {
return fmt.Errorf("server write datagram: %w", err)
}
return nil
})
defer stop()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
clientCfg := *serverCfg
clientCfg.ServerAddress = addr
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
defer tunnelConn.Close()
handshakeCfg := clientCfg
handshakeCfg.DisableHTTPMask = true
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
if err != nil {
t.Fatalf("client handshake: %v", err)
}
defer cConn.Close()
if err := WritePreface(cConn); err != nil {
t.Fatalf("write preface: %v", err)
}
if err := WriteDatagram(cConn, target, payload); err != nil {
t.Fatalf("write datagram: %v", err)
}
gotAddr, gotPayload, err := ReadDatagram(cConn)
if err != nil {
t.Fatalf("read datagram: %v", err)
}
if gotAddr != target {
t.Fatalf("uot target mismatch: %s", gotAddr)
}
if !bytes.Equal(gotPayload, payload) {
t.Fatalf("uot payload mismatch: %x", gotPayload)
}
stop()
for err := range errCh {
t.Fatalf("server error: %v", err)
}
}
func TestHTTPMaskTunnel_Auto_TCPRoundTrip(t *testing.T) {
key := "tunnel-auto-key"
target := "9.9.9.9:443"
serverCfg := newTunnelTestTable(t, key)
serverCfg.HTTPMaskMode = "auto"
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
if s.Type != SessionTypeTCP {
return fmt.Errorf("unexpected session type: %v", s.Type)
}
if s.Target != target {
return fmt.Errorf("target mismatch: %s", s.Target)
}
_, _ = s.Conn.Write([]byte("ok"))
return nil
})
defer stop()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
clientCfg := *serverCfg
clientCfg.ServerAddress = addr
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
defer tunnelConn.Close()
handshakeCfg := clientCfg
handshakeCfg.DisableHTTPMask = true
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
if err != nil {
t.Fatalf("client handshake: %v", err)
}
defer cConn.Close()
addrBuf, err := EncodeAddress(target)
if err != nil {
t.Fatalf("encode addr: %v", err)
}
if _, err := cConn.Write(addrBuf); err != nil {
t.Fatalf("write addr: %v", err)
}
buf := make([]byte, 2)
if _, err := io.ReadFull(cConn, buf); err != nil {
t.Fatalf("read: %v", err)
}
if string(buf) != "ok" {
t.Fatalf("unexpected payload: %q", buf)
}
stop()
for err := range errCh {
t.Fatalf("server error: %v", err)
}
}
func TestHTTPMaskTunnel_Validation(t *testing.T) {
cfg := DefaultConfig()
cfg.Key = "k"
cfg.Table = NewTable("seed", "prefer_ascii")
cfg.ServerAddress = "127.0.0.1:1"
cfg.DisableHTTPMask = true
cfg.HTTPMaskMode = "stream"
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext); err == nil {
t.Fatalf("expected error for disabled http mask")
}
cfg.DisableHTTPMask = false
cfg.HTTPMaskMode = "legacy"
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext); err == nil {
t.Fatalf("expected error for legacy mode")
}
}
func TestHTTPMaskTunnel_Soak_Concurrent(t *testing.T) {
key := "tunnel-soak-key"
target := "1.0.0.1:80"
serverCfg := newTunnelTestTable(t, key)
serverCfg.HTTPMaskMode = "stream"
serverCfg.EnablePureDownlink = false
const (
sessions = 8
payloadLen = 64 * 1024
)
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
if s.Type != SessionTypeTCP {
return fmt.Errorf("unexpected session type: %v", s.Type)
}
if s.Target != target {
return fmt.Errorf("target mismatch: %s", s.Target)
}
buf := make([]byte, payloadLen)
if _, err := io.ReadFull(s.Conn, buf); err != nil {
return fmt.Errorf("server read payload: %w", err)
}
_, err := s.Conn.Write(buf)
return err
})
defer stop()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var wg sync.WaitGroup
runErr := make(chan error, sessions)
for i := 0; i < sessions; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
clientCfg := *serverCfg
clientCfg.ServerAddress = addr
clientCfg.HTTPMaskHost = strings.TrimSpace(clientCfg.HTTPMaskHost)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
if err != nil {
runErr <- fmt.Errorf("dial: %w", err)
return
}
defer tunnelConn.Close()
handshakeCfg := clientCfg
handshakeCfg.DisableHTTPMask = true
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
if err != nil {
runErr <- fmt.Errorf("handshake: %w", err)
return
}
defer cConn.Close()
addrBuf, err := EncodeAddress(target)
if err != nil {
runErr <- fmt.Errorf("encode addr: %w", err)
return
}
if _, err := cConn.Write(addrBuf); err != nil {
runErr <- fmt.Errorf("write addr: %w", err)
return
}
payload := bytes.Repeat([]byte{byte(id)}, payloadLen)
if _, err := cConn.Write(payload); err != nil {
runErr <- fmt.Errorf("write payload: %w", err)
return
}
echo := make([]byte, payloadLen)
if _, err := io.ReadFull(cConn, echo); err != nil {
runErr <- fmt.Errorf("read echo: %w", err)
return
}
if !bytes.Equal(echo, payload) {
runErr <- fmt.Errorf("echo mismatch")
return
}
runErr <- nil
}(i)
}
wg.Wait()
close(runErr)
for err := range runErr {
if err != nil {
t.Fatalf("soak: %v", err)
}
}
stop()
for err := range errCh {
t.Fatalf("server error: %v", err)
}
}

View File

@@ -0,0 +1,246 @@
package httpmask
import (
"bufio"
"bytes"
"encoding/base64"
"fmt"
"io"
"math/rand"
"net"
"strconv"
"strings"
"sync"
"time"
)
var (
userAgents = []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36",
}
accepts = []string{
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"application/json, text/plain, */*",
"application/octet-stream",
"*/*",
}
acceptLanguages = []string{
"en-US,en;q=0.9",
"en-GB,en;q=0.9",
"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7",
"de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
}
acceptEncodings = []string{
"gzip, deflate, br",
"gzip, deflate",
"br, gzip, deflate",
}
paths = []string{
"/api/v1/upload",
"/data/sync",
"/uploads/raw",
"/api/report",
"/feed/update",
"/v2/events",
"/v1/telemetry",
"/session",
"/stream",
"/ws",
}
contentTypes = []string{
"application/octet-stream",
"application/x-protobuf",
"application/json",
}
)
var (
rngPool = sync.Pool{
New: func() interface{} {
return rand.New(rand.NewSource(time.Now().UnixNano()))
},
}
headerBufPool = sync.Pool{
New: func() interface{} {
b := make([]byte, 0, 1024)
return &b
},
}
)
// LooksLikeHTTPRequestStart reports whether peek4 looks like a supported HTTP/1.x request method prefix.
func LooksLikeHTTPRequestStart(peek4 []byte) bool {
if len(peek4) < 4 {
return false
}
// Common methods: "GET ", "POST", "HEAD", "PUT ", "OPTI" (OPTIONS), "PATC" (PATCH), "DELE" (DELETE)
return bytes.Equal(peek4, []byte("GET ")) ||
bytes.Equal(peek4, []byte("POST")) ||
bytes.Equal(peek4, []byte("HEAD")) ||
bytes.Equal(peek4, []byte("PUT ")) ||
bytes.Equal(peek4, []byte("OPTI")) ||
bytes.Equal(peek4, []byte("PATC")) ||
bytes.Equal(peek4, []byte("DELE"))
}
func trimPortForHost(host string) string {
if host == "" {
return host
}
// Accept "example.com:443" / "1.2.3.4:443" / "[::1]:443"
h, _, err := net.SplitHostPort(host)
if err == nil && h != "" {
return h
}
// If it's not in host:port form, keep as-is.
return host
}
func appendCommonHeaders(buf []byte, host string, r *rand.Rand) []byte {
ua := userAgents[r.Intn(len(userAgents))]
accept := accepts[r.Intn(len(accepts))]
lang := acceptLanguages[r.Intn(len(acceptLanguages))]
enc := acceptEncodings[r.Intn(len(acceptEncodings))]
buf = append(buf, "Host: "...)
buf = append(buf, host...)
buf = append(buf, "\r\nUser-Agent: "...)
buf = append(buf, ua...)
buf = append(buf, "\r\nAccept: "...)
buf = append(buf, accept...)
buf = append(buf, "\r\nAccept-Language: "...)
buf = append(buf, lang...)
buf = append(buf, "\r\nAccept-Encoding: "...)
buf = append(buf, enc...)
buf = append(buf, "\r\nConnection: keep-alive\r\n"...)
// A couple of common cache headers; keep them static for simplicity.
buf = append(buf, "Cache-Control: no-cache\r\nPragma: no-cache\r\n"...)
return buf
}
// WriteRandomRequestHeader writes a plausible HTTP/1.1 request header as a mask.
func WriteRandomRequestHeader(w io.Writer, host string) error {
// Get RNG from pool
r := rngPool.Get().(*rand.Rand)
defer rngPool.Put(r)
path := paths[r.Intn(len(paths))]
ctype := contentTypes[r.Intn(len(contentTypes))]
// Use buffer pool
bufPtr := headerBufPool.Get().(*[]byte)
buf := *bufPtr
buf = buf[:0]
defer func() {
if cap(buf) <= 4096 {
*bufPtr = buf
headerBufPool.Put(bufPtr)
}
}()
// Weighted template selection. Keep a conservative default (POST w/ Content-Length),
// but occasionally rotate to other realistic templates (e.g. WebSocket upgrade).
switch r.Intn(10) {
case 0, 1: // ~20% WebSocket-like upgrade
hostNoPort := trimPortForHost(host)
var keyBytes [16]byte
for i := 0; i < len(keyBytes); i++ {
keyBytes[i] = byte(r.Intn(256))
}
wsKey := base64.StdEncoding.EncodeToString(keyBytes[:])
buf = append(buf, "GET "...)
buf = append(buf, path...)
buf = append(buf, " HTTP/1.1\r\n"...)
buf = appendCommonHeaders(buf, host, r)
buf = append(buf, "Upgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: "...)
buf = append(buf, wsKey...)
buf = append(buf, "\r\nOrigin: https://"...)
buf = append(buf, hostNoPort...)
buf = append(buf, "\r\n\r\n"...)
default: // ~80% POST upload
// Random Content-Length: 4KB10MB. Small enough to look plausible, large enough
// to justify long-lived writes on keep-alive connections.
const minCL = int64(4 * 1024)
const maxCL = int64(10 * 1024 * 1024)
contentLength := minCL + r.Int63n(maxCL-minCL+1)
buf = append(buf, "POST "...)
buf = append(buf, path...)
buf = append(buf, " HTTP/1.1\r\n"...)
buf = appendCommonHeaders(buf, host, r)
buf = append(buf, "Content-Type: "...)
buf = append(buf, ctype...)
buf = append(buf, "\r\nContent-Length: "...)
buf = strconv.AppendInt(buf, contentLength, 10)
// A couple of extra headers seen in real clients.
if r.Intn(2) == 0 {
buf = append(buf, "\r\nX-Requested-With: XMLHttpRequest"...)
}
if r.Intn(3) == 0 {
buf = append(buf, "\r\nReferer: https://"...)
buf = append(buf, trimPortForHost(host)...)
buf = append(buf, "/"...)
}
buf = append(buf, "\r\n\r\n"...)
}
_, err := w.Write(buf)
return err
}
// ConsumeHeader 读取并消耗 HTTP 头部,返回消耗的数据和剩余的 reader 数据
// 如果不是 POST 请求或格式严重错误,返回 error
func ConsumeHeader(r *bufio.Reader) ([]byte, error) {
var consumed bytes.Buffer
// 1. 读取请求行
// Use ReadSlice to avoid allocation if line fits in buffer
line, err := r.ReadSlice('\n')
if err != nil {
return nil, err
}
consumed.Write(line)
// Basic method validation: accept common HTTP/1.x methods used by our masker.
// Keep it strict enough to reject obvious garbage.
switch {
case bytes.HasPrefix(line, []byte("POST ")),
bytes.HasPrefix(line, []byte("GET ")),
bytes.HasPrefix(line, []byte("HEAD ")),
bytes.HasPrefix(line, []byte("PUT ")),
bytes.HasPrefix(line, []byte("DELETE ")),
bytes.HasPrefix(line, []byte("OPTIONS ")),
bytes.HasPrefix(line, []byte("PATCH ")):
default:
return consumed.Bytes(), fmt.Errorf("invalid method or garbage: %s", strings.TrimSpace(string(line)))
}
// 2. 循环读取头部,直到遇到空行
for {
line, err = r.ReadSlice('\n')
if err != nil {
return consumed.Bytes(), err
}
consumed.Write(line)
// Check for empty line (\r\n or \n)
// ReadSlice includes the delimiter
n := len(line)
if n == 2 && line[0] == '\r' && line[1] == '\n' {
return consumed.Bytes(), nil
}
if n == 1 && line[0] == '\n' {
return consumed.Bytes(), nil
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,212 @@
package sudoku
import (
"bufio"
"bytes"
crypto_rand "crypto/rand"
"encoding/binary"
"errors"
"math/rand"
"net"
"sync"
)
const IOBufferSize = 32 * 1024
var perm4 = [24][4]byte{
{0, 1, 2, 3},
{0, 1, 3, 2},
{0, 2, 1, 3},
{0, 2, 3, 1},
{0, 3, 1, 2},
{0, 3, 2, 1},
{1, 0, 2, 3},
{1, 0, 3, 2},
{1, 2, 0, 3},
{1, 2, 3, 0},
{1, 3, 0, 2},
{1, 3, 2, 0},
{2, 0, 1, 3},
{2, 0, 3, 1},
{2, 1, 0, 3},
{2, 1, 3, 0},
{2, 3, 0, 1},
{2, 3, 1, 0},
{3, 0, 1, 2},
{3, 0, 2, 1},
{3, 1, 0, 2},
{3, 1, 2, 0},
{3, 2, 0, 1},
{3, 2, 1, 0},
}
type Conn struct {
net.Conn
table *Table
reader *bufio.Reader
recorder *bytes.Buffer
recording bool
recordLock sync.Mutex
rawBuf []byte
pendingData []byte
hintBuf []byte
rng *rand.Rand
paddingRate float32
}
func NewConn(c net.Conn, table *Table, pMin, pMax int, record bool) *Conn {
var seedBytes [8]byte
if _, err := crypto_rand.Read(seedBytes[:]); err != nil {
binary.BigEndian.PutUint64(seedBytes[:], uint64(rand.Int63()))
}
seed := int64(binary.BigEndian.Uint64(seedBytes[:]))
localRng := rand.New(rand.NewSource(seed))
min := float32(pMin) / 100.0
rng := float32(pMax-pMin) / 100.0
rate := min + localRng.Float32()*rng
sc := &Conn{
Conn: c,
table: table,
reader: bufio.NewReaderSize(c, IOBufferSize),
rawBuf: make([]byte, IOBufferSize),
pendingData: make([]byte, 0, 4096),
hintBuf: make([]byte, 0, 4),
rng: localRng,
paddingRate: rate,
}
if record {
sc.recorder = new(bytes.Buffer)
sc.recording = true
}
return sc
}
func (sc *Conn) StopRecording() {
sc.recordLock.Lock()
sc.recording = false
sc.recorder = nil
sc.recordLock.Unlock()
}
func (sc *Conn) GetBufferedAndRecorded() []byte {
if sc == nil {
return nil
}
sc.recordLock.Lock()
defer sc.recordLock.Unlock()
var recorded []byte
if sc.recorder != nil {
recorded = sc.recorder.Bytes()
}
buffered := sc.reader.Buffered()
if buffered > 0 {
peeked, _ := sc.reader.Peek(buffered)
full := make([]byte, len(recorded)+len(peeked))
copy(full, recorded)
copy(full[len(recorded):], peeked)
return full
}
return recorded
}
func (sc *Conn) Write(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
outCapacity := len(p) * 6
out := make([]byte, 0, outCapacity)
pads := sc.table.PaddingPool
padLen := len(pads)
for _, b := range p {
if sc.rng.Float32() < sc.paddingRate {
out = append(out, pads[sc.rng.Intn(padLen)])
}
puzzles := sc.table.EncodeTable[b]
puzzle := puzzles[sc.rng.Intn(len(puzzles))]
perm := perm4[sc.rng.Intn(len(perm4))]
for _, idx := range perm {
if sc.rng.Float32() < sc.paddingRate {
out = append(out, pads[sc.rng.Intn(padLen)])
}
out = append(out, puzzle[idx])
}
}
if sc.rng.Float32() < sc.paddingRate {
out = append(out, pads[sc.rng.Intn(padLen)])
}
_, err = sc.Conn.Write(out)
return len(p), err
}
func (sc *Conn) Read(p []byte) (n int, err error) {
if len(sc.pendingData) > 0 {
n = copy(p, sc.pendingData)
if n == len(sc.pendingData) {
sc.pendingData = sc.pendingData[:0]
} else {
sc.pendingData = sc.pendingData[n:]
}
return n, nil
}
for {
if len(sc.pendingData) > 0 {
break
}
nr, rErr := sc.reader.Read(sc.rawBuf)
if nr > 0 {
chunk := sc.rawBuf[:nr]
sc.recordLock.Lock()
if sc.recording {
sc.recorder.Write(chunk)
}
sc.recordLock.Unlock()
for _, b := range chunk {
if !sc.table.layout.isHint(b) {
continue
}
sc.hintBuf = append(sc.hintBuf, b)
if len(sc.hintBuf) == 4 {
key := packHintsToKey([4]byte{sc.hintBuf[0], sc.hintBuf[1], sc.hintBuf[2], sc.hintBuf[3]})
val, ok := sc.table.DecodeMap[key]
if !ok {
return 0, errors.New("INVALID_SUDOKU_MAP_MISS")
}
sc.pendingData = append(sc.pendingData, val)
sc.hintBuf = sc.hintBuf[:0]
}
}
}
if rErr != nil {
return 0, rErr
}
if len(sc.pendingData) > 0 {
break
}
}
n = copy(p, sc.pendingData)
if n == len(sc.pendingData) {
sc.pendingData = sc.pendingData[:0]
} else {
sc.pendingData = sc.pendingData[n:]
}
return n, nil
}

View File

@@ -0,0 +1,46 @@
package sudoku
// Grid represents a 4x4 sudoku grid
type Grid [16]uint8
// GenerateAllGrids generates all valid 4x4 Sudoku grids
func GenerateAllGrids() []Grid {
var grids []Grid
var g Grid
var backtrack func(int)
backtrack = func(idx int) {
if idx == 16 {
grids = append(grids, g)
return
}
row, col := idx/4, idx%4
br, bc := (row/2)*2, (col/2)*2
for num := uint8(1); num <= 4; num++ {
valid := true
for i := 0; i < 4; i++ {
if g[row*4+i] == num || g[i*4+col] == num {
valid = false
break
}
}
if valid {
for r := 0; r < 2; r++ {
for c := 0; c < 2; c++ {
if g[(br+r)*4+(bc+c)] == num {
valid = false
break
}
}
}
}
if valid {
g[idx] = num
backtrack(idx + 1)
g[idx] = 0
}
}
}
backtrack(0)
return grids
}

View File

@@ -0,0 +1,204 @@
package sudoku
import (
"fmt"
"math/bits"
"sort"
"strings"
)
type byteLayout struct {
name string
hintMask byte
hintValue byte
padMarker byte
paddingPool []byte
encodeHint func(val, pos byte) byte
encodeGroup func(group byte) byte
decodeGroup func(b byte) (byte, bool)
}
func (l *byteLayout) isHint(b byte) bool {
return (b & l.hintMask) == l.hintValue
}
// resolveLayout picks the byte layout based on ASCII preference and optional custom pattern.
// ASCII always wins if requested. Custom patterns are ignored when ASCII is preferred.
func resolveLayout(mode string, customPattern string) (*byteLayout, error) {
switch strings.ToLower(mode) {
case "ascii", "prefer_ascii":
return newASCIILayout(), nil
case "entropy", "prefer_entropy", "":
// fallback to entropy unless a custom pattern is provided
default:
return nil, fmt.Errorf("invalid ascii mode: %s", mode)
}
if strings.TrimSpace(customPattern) != "" {
return newCustomLayout(customPattern)
}
return newEntropyLayout(), nil
}
func newASCIILayout() *byteLayout {
padding := make([]byte, 0, 32)
for i := 0; i < 32; i++ {
padding = append(padding, byte(0x20+i))
}
return &byteLayout{
name: "ascii",
hintMask: 0x40,
hintValue: 0x40,
padMarker: 0x3F,
paddingPool: padding,
encodeHint: func(val, pos byte) byte {
return 0x40 | ((val & 0x03) << 4) | (pos & 0x0F)
},
encodeGroup: func(group byte) byte {
return 0x40 | (group & 0x3F)
},
decodeGroup: func(b byte) (byte, bool) {
if (b & 0x40) == 0 {
return 0, false
}
return b & 0x3F, true
},
}
}
func newEntropyLayout() *byteLayout {
padding := make([]byte, 0, 16)
for i := 0; i < 8; i++ {
padding = append(padding, byte(0x80+i))
padding = append(padding, byte(0x10+i))
}
return &byteLayout{
name: "entropy",
hintMask: 0x90,
hintValue: 0x00,
padMarker: 0x80,
paddingPool: padding,
encodeHint: func(val, pos byte) byte {
return ((val & 0x03) << 5) | (pos & 0x0F)
},
encodeGroup: func(group byte) byte {
v := group & 0x3F
return ((v & 0x30) << 1) | (v & 0x0F)
},
decodeGroup: func(b byte) (byte, bool) {
if (b & 0x90) != 0 {
return 0, false
}
return ((b >> 1) & 0x30) | (b & 0x0F), true
},
}
}
func newCustomLayout(pattern string) (*byteLayout, error) {
cleaned := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(pattern), " ", ""))
if len(cleaned) != 8 {
return nil, fmt.Errorf("custom table must have 8 symbols, got %d", len(cleaned))
}
var xBits, pBits, vBits []uint8
for i, c := range cleaned {
bit := uint8(7 - i)
switch c {
case 'x':
xBits = append(xBits, bit)
case 'p':
pBits = append(pBits, bit)
case 'v':
vBits = append(vBits, bit)
default:
return nil, fmt.Errorf("invalid char %q in custom table", c)
}
}
if len(xBits) != 2 || len(pBits) != 2 || len(vBits) != 4 {
return nil, fmt.Errorf("custom table must contain exactly 2 x, 2 p, 4 v")
}
xMask := byte(0)
for _, b := range xBits {
xMask |= 1 << b
}
encodeBits := func(val, pos byte, dropX int) byte {
var out byte
out |= xMask
if dropX >= 0 {
out &^= 1 << xBits[dropX]
}
if (val & 0x02) != 0 {
out |= 1 << pBits[0]
}
if (val & 0x01) != 0 {
out |= 1 << pBits[1]
}
for i, bit := range vBits {
if (pos>>(3-uint8(i)))&0x01 == 1 {
out |= 1 << bit
}
}
return out
}
decodeGroup := func(b byte) (byte, bool) {
if (b & xMask) != xMask {
return 0, false
}
var val, pos byte
if b&(1<<pBits[0]) != 0 {
val |= 0x02
}
if b&(1<<pBits[1]) != 0 {
val |= 0x01
}
for i, bit := range vBits {
if b&(1<<bit) != 0 {
pos |= 1 << (3 - uint8(i))
}
}
group := (val << 4) | (pos & 0x0F)
return group, true
}
paddingSet := make(map[byte]struct{})
var padding []byte
for drop := range xBits {
for val := 0; val < 4; val++ {
for pos := 0; pos < 16; pos++ {
b := encodeBits(byte(val), byte(pos), drop)
if bits.OnesCount8(b) >= 5 {
if _, ok := paddingSet[b]; !ok {
paddingSet[b] = struct{}{}
padding = append(padding, b)
}
}
}
}
}
sort.Slice(padding, func(i, j int) bool { return padding[i] < padding[j] })
if len(padding) == 0 {
return nil, fmt.Errorf("custom table produced empty padding pool")
}
return &byteLayout{
name: fmt.Sprintf("custom(%s)", cleaned),
hintMask: xMask,
hintValue: xMask,
padMarker: padding[0],
paddingPool: padding,
encodeHint: func(val, pos byte) byte {
return encodeBits(val, pos, -1)
},
encodeGroup: func(group byte) byte {
val := (group >> 4) & 0x03
pos := group & 0x0F
return encodeBits(val, pos, -1)
},
decodeGroup: decodeGroup,
}, nil
}

View File

@@ -0,0 +1,332 @@
package sudoku
import (
"bufio"
crypto_rand "crypto/rand"
"encoding/binary"
"io"
"math/rand"
"net"
"sync"
)
const (
// 每次从 RNG 获取批量随机数的缓存大小,减少 RNG 函数调用开销
RngBatchSize = 128
)
// 1. 使用 12字节->16组 的块处理优化 Write (减少循环开销)
// 2. 使用浮点随机概率判断 Padding与纯 Sudoku 保持流量特征一致
// 3. Read 使用 copy 移动避免底层数组泄漏
type PackedConn struct {
net.Conn
table *Table
reader *bufio.Reader
// 读缓冲
rawBuf []byte
pendingData []byte // 解码后尚未被 Read 取走的字节
// 写缓冲与状态
writeMu sync.Mutex
writeBuf []byte
bitBuf uint64 // 暂存的位数据
bitCount int // 暂存的位数
// 读状态
readBitBuf uint64
readBits int
// 随机数与填充控制 - 使用浮点随机,与 Conn 一致
rng *rand.Rand
paddingRate float32 // 与 Conn 保持一致的随机概率模型
padMarker byte
padPool []byte
}
func NewPackedConn(c net.Conn, table *Table, pMin, pMax int) *PackedConn {
var seedBytes [8]byte
if _, err := crypto_rand.Read(seedBytes[:]); err != nil {
binary.BigEndian.PutUint64(seedBytes[:], uint64(rand.Int63()))
}
seed := int64(binary.BigEndian.Uint64(seedBytes[:]))
localRng := rand.New(rand.NewSource(seed))
// 与 Conn 保持一致的 padding 概率计算
min := float32(pMin) / 100.0
rng := float32(pMax-pMin) / 100.0
rate := min + localRng.Float32()*rng
pc := &PackedConn{
Conn: c,
table: table,
reader: bufio.NewReaderSize(c, IOBufferSize),
rawBuf: make([]byte, IOBufferSize),
pendingData: make([]byte, 0, 4096),
writeBuf: make([]byte, 0, 4096),
rng: localRng,
paddingRate: rate,
}
pc.padMarker = table.layout.padMarker
for _, b := range table.PaddingPool {
if b != pc.padMarker {
pc.padPool = append(pc.padPool, b)
}
}
if len(pc.padPool) == 0 {
pc.padPool = append(pc.padPool, pc.padMarker)
}
return pc
}
// maybeAddPadding 内联辅助:根据浮点概率插入 padding
func (pc *PackedConn) maybeAddPadding(out []byte) []byte {
if pc.rng.Float32() < pc.paddingRate {
out = append(out, pc.getPaddingByte())
}
return out
}
// Write 极致优化版 - 批量处理 12 字节
func (pc *PackedConn) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
pc.writeMu.Lock()
defer pc.writeMu.Unlock()
// 1. 预分配内存,避免 append 导致的多次扩容
// 预估:原数据 * 1.5 (4/3 + padding 余量)
needed := len(p)*3/2 + 32
if cap(pc.writeBuf) < needed {
pc.writeBuf = make([]byte, 0, needed)
}
out := pc.writeBuf[:0]
i := 0
n := len(p)
// 2. 头部对齐处理 (Slow Path)
for pc.bitCount > 0 && i < n {
out = pc.maybeAddPadding(out)
b := p[i]
i++
pc.bitBuf = (pc.bitBuf << 8) | uint64(b)
pc.bitCount += 8
for pc.bitCount >= 6 {
pc.bitCount -= 6
group := byte(pc.bitBuf >> pc.bitCount)
if pc.bitCount == 0 {
pc.bitBuf = 0
} else {
pc.bitBuf &= (1 << pc.bitCount) - 1
}
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(group&0x3F))
}
}
// 3. 极速批量处理 (Fast Path) - 每次处理 12 字节 → 生成 16 个编码组
for i+11 < n {
// 处理 4 组,每组 3 字节
for batch := 0; batch < 4; batch++ {
b1, b2, b3 := p[i], p[i+1], p[i+2]
i += 3
g1 := (b1 >> 2) & 0x3F
g2 := ((b1 & 0x03) << 4) | ((b2 >> 4) & 0x0F)
g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03)
g4 := b3 & 0x3F
// 每个组之前都有概率插入 padding
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g1))
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g2))
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g3))
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g4))
}
}
// 4. 处理剩余的 3 字节块
for i+2 < n {
b1, b2, b3 := p[i], p[i+1], p[i+2]
i += 3
g1 := (b1 >> 2) & 0x3F
g2 := ((b1 & 0x03) << 4) | ((b2 >> 4) & 0x0F)
g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03)
g4 := b3 & 0x3F
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g1))
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g2))
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g3))
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g4))
}
// 5. 尾部处理 (Tail Path) - 处理剩余的 1 或 2 个字节
for ; i < n; i++ {
b := p[i]
pc.bitBuf = (pc.bitBuf << 8) | uint64(b)
pc.bitCount += 8
for pc.bitCount >= 6 {
pc.bitCount -= 6
group := byte(pc.bitBuf >> pc.bitCount)
if pc.bitCount == 0 {
pc.bitBuf = 0
} else {
pc.bitBuf &= (1 << pc.bitCount) - 1
}
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(group&0x3F))
}
}
// 6. 处理残留位
if pc.bitCount > 0 {
out = pc.maybeAddPadding(out)
group := byte(pc.bitBuf << (6 - pc.bitCount))
pc.bitBuf = 0
pc.bitCount = 0
out = append(out, pc.encodeGroup(group&0x3F))
out = append(out, pc.padMarker)
}
// 尾部可能添加 padding
out = pc.maybeAddPadding(out)
// 发送数据
if len(out) > 0 {
_, err := pc.Conn.Write(out)
pc.writeBuf = out[:0]
return len(p), err
}
pc.writeBuf = out[:0]
return len(p), nil
}
// Flush 处理最后不足 6 bit 的情况
func (pc *PackedConn) Flush() error {
pc.writeMu.Lock()
defer pc.writeMu.Unlock()
out := pc.writeBuf[:0]
if pc.bitCount > 0 {
group := byte(pc.bitBuf << (6 - pc.bitCount))
pc.bitBuf = 0
pc.bitCount = 0
out = append(out, pc.encodeGroup(group&0x3F))
out = append(out, pc.padMarker)
}
// 尾部随机添加 padding
out = pc.maybeAddPadding(out)
if len(out) > 0 {
_, err := pc.Conn.Write(out)
pc.writeBuf = out[:0]
return err
}
return nil
}
// Read 优化版:减少切片操作,避免内存泄漏
func (pc *PackedConn) Read(p []byte) (int, error) {
// 1. 优先返回待处理区的数据
if len(pc.pendingData) > 0 {
n := copy(p, pc.pendingData)
if n == len(pc.pendingData) {
pc.pendingData = pc.pendingData[:0]
} else {
// 优化:移动剩余数据到数组头部,避免切片指向中间导致内存泄漏
remaining := len(pc.pendingData) - n
copy(pc.pendingData, pc.pendingData[n:])
pc.pendingData = pc.pendingData[:remaining]
}
return n, nil
}
// 2. 循环读取直到解出数据或出错
for {
nr, rErr := pc.reader.Read(pc.rawBuf)
if nr > 0 {
// 缓存频繁访问的变量
rBuf := pc.readBitBuf
rBits := pc.readBits
padMarker := pc.padMarker
layout := pc.table.layout
for _, b := range pc.rawBuf[:nr] {
if !layout.isHint(b) {
if b == padMarker {
rBuf = 0
rBits = 0
}
continue
}
group, ok := layout.decodeGroup(b)
if !ok {
return 0, ErrInvalidSudokuMapMiss
}
rBuf = (rBuf << 6) | uint64(group)
rBits += 6
if rBits >= 8 {
rBits -= 8
val := byte(rBuf >> rBits)
pc.pendingData = append(pc.pendingData, val)
}
}
pc.readBitBuf = rBuf
pc.readBits = rBits
}
if rErr != nil {
if rErr == io.EOF {
pc.readBitBuf = 0
pc.readBits = 0
}
if len(pc.pendingData) > 0 {
break
}
return 0, rErr
}
if len(pc.pendingData) > 0 {
break
}
}
// 3. 返回解码后的数据 - 优化:避免底层数组泄漏
n := copy(p, pc.pendingData)
if n == len(pc.pendingData) {
pc.pendingData = pc.pendingData[:0]
} else {
remaining := len(pc.pendingData) - n
copy(pc.pendingData, pc.pendingData[n:])
pc.pendingData = pc.pendingData[:remaining]
}
return n, nil
}
// getPaddingByte 从 Pool 中随机取 Padding 字节
func (pc *PackedConn) getPaddingByte() byte {
return pc.padPool[pc.rng.Intn(len(pc.padPool))]
}
// encodeGroup 编码 6-bit 组
func (pc *PackedConn) encodeGroup(group byte) byte {
return pc.table.layout.encodeGroup(group)
}

View File

@@ -0,0 +1,153 @@
package sudoku
import (
"crypto/sha256"
"encoding/binary"
"errors"
"log"
"math/rand"
"time"
)
var (
ErrInvalidSudokuMapMiss = errors.New("INVALID_SUDOKU_MAP_MISS")
)
type Table struct {
EncodeTable [256][][4]byte
DecodeMap map[uint32]byte
PaddingPool []byte
IsASCII bool // 标记当前模式
layout *byteLayout
}
// NewTable initializes the obfuscation tables with built-in layouts.
// Equivalent to calling NewTableWithCustom(key, mode, "").
func NewTable(key string, mode string) *Table {
t, err := NewTableWithCustom(key, mode, "")
if err != nil {
log.Panicf("failed to build table: %v", err)
}
return t
}
// NewTableWithCustom initializes obfuscation tables using either predefined or custom layouts.
// mode: "prefer_ascii" or "prefer_entropy". If a custom pattern is provided, ASCII mode still takes precedence.
// The customPattern must contain 8 characters with exactly 2 x, 2 p, and 4 v (case-insensitive).
func NewTableWithCustom(key string, mode string, customPattern string) (*Table, error) {
start := time.Now()
layout, err := resolveLayout(mode, customPattern)
if err != nil {
return nil, err
}
t := &Table{
DecodeMap: make(map[uint32]byte),
IsASCII: layout.name == "ascii",
layout: layout,
}
t.PaddingPool = append(t.PaddingPool, layout.paddingPool...)
// 生成数独网格 (逻辑不变)
allGrids := GenerateAllGrids()
h := sha256.New()
h.Write([]byte(key))
seed := int64(binary.BigEndian.Uint64(h.Sum(nil)[:8]))
rng := rand.New(rand.NewSource(seed))
shuffledGrids := make([]Grid, 288)
copy(shuffledGrids, allGrids)
rng.Shuffle(len(shuffledGrids), func(i, j int) {
shuffledGrids[i], shuffledGrids[j] = shuffledGrids[j], shuffledGrids[i]
})
// 预计算组合
var combinations [][]int
var combine func(int, int, []int)
combine = func(s, k int, c []int) {
if k == 0 {
tmp := make([]int, len(c))
copy(tmp, c)
combinations = append(combinations, tmp)
return
}
for i := s; i <= 16-k; i++ {
c = append(c, i)
combine(i+1, k-1, c)
c = c[:len(c)-1]
}
}
combine(0, 4, []int{})
// 构建映射表
for byteVal := 0; byteVal < 256; byteVal++ {
targetGrid := shuffledGrids[byteVal]
for _, positions := range combinations {
var currentHints [4]byte
// 1. 计算抽象提示 (Abstract Hints)
// 我们先计算出 val 和 pos后面再根据模式编码成 byte
var rawParts [4]struct{ val, pos byte }
for i, pos := range positions {
val := targetGrid[pos] // 1..4
rawParts[i] = struct{ val, pos byte }{val, uint8(pos)}
}
// 检查唯一性 (数独逻辑)
matchCount := 0
for _, g := range allGrids {
match := true
for _, p := range rawParts {
if g[p.pos] != p.val {
match = false
break
}
}
if match {
matchCount++
if matchCount > 1 {
break
}
}
}
if matchCount == 1 {
// 唯一确定,生成最终编码字节
for i, p := range rawParts {
currentHints[i] = t.layout.encodeHint(p.val-1, p.pos)
}
t.EncodeTable[byteVal] = append(t.EncodeTable[byteVal], currentHints)
// 生成解码键 (需要对 Hints 进行排序以忽略传输顺序)
key := packHintsToKey(currentHints)
t.DecodeMap[key] = byte(byteVal)
}
}
}
log.Printf("[Init] Sudoku Tables initialized (%s) in %v", layout.name, time.Since(start))
return t, nil
}
func packHintsToKey(hints [4]byte) uint32 {
// Sorting network for 4 elements (Bubble sort unrolled)
// Swap if a > b
if hints[0] > hints[1] {
hints[0], hints[1] = hints[1], hints[0]
}
if hints[2] > hints[3] {
hints[2], hints[3] = hints[3], hints[2]
}
if hints[0] > hints[2] {
hints[0], hints[2] = hints[2], hints[0]
}
if hints[1] > hints[3] {
hints[1], hints[3] = hints[3], hints[1]
}
if hints[1] > hints[2] {
hints[1], hints[2] = hints[2], hints[1]
}
return uint32(hints[0])<<24 | uint32(hints[1])<<16 | uint32(hints[2])<<8 | uint32(hints[3])
}

View File

@@ -0,0 +1,38 @@
package sudoku
import "fmt"
// TableSet is a small helper for managing multiple Sudoku tables (e.g. for per-connection rotation).
// It is intentionally decoupled from the tunnel/app layers.
type TableSet struct {
Tables []*Table
}
// NewTableSet builds one or more tables from key/mode and a list of custom X/P/V patterns.
// If patterns is empty, it builds a single default table (customPattern="").
func NewTableSet(key string, mode string, patterns []string) (*TableSet, error) {
if len(patterns) == 0 {
t, err := NewTableWithCustom(key, mode, "")
if err != nil {
return nil, err
}
return &TableSet{Tables: []*Table{t}}, nil
}
tables := make([]*Table, 0, len(patterns))
for i, pattern := range patterns {
t, err := NewTableWithCustom(key, mode, pattern)
if err != nil {
return nil, fmt.Errorf("build table[%d] (%q): %w", i, pattern, err)
}
tables = append(tables, t)
}
return &TableSet{Tables: tables}, nil
}
func (ts *TableSet) Candidates() []*Table {
if ts == nil {
return nil
}
return ts.Tables
}

View File

@@ -6,7 +6,7 @@ import (
"math/rand"
"net"
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
// perm4 matches github.com/saba-futai/sudoku/pkg/obfs/sudoku perm4.

View File

@@ -10,26 +10,12 @@ import (
"net"
"time"
"github.com/saba-futai/sudoku/apis"
"github.com/saba-futai/sudoku/pkg/crypto"
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
"github.com/metacubex/mihomo/transport/sudoku/crypto"
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
func tableCandidates(cfg *apis.ProtocolConfig) []*sudoku.Table {
if cfg == nil {
return nil
}
if len(cfg.Tables) > 0 {
return cfg.Tables
}
if cfg.Table != nil {
return []*sudoku.Table{cfg.Table}
}
return nil
}
func pickClientTable(cfg *apis.ProtocolConfig) (*sudoku.Table, byte, error) {
candidates := tableCandidates(cfg)
func pickClientTable(cfg *ProtocolConfig) (*sudoku.Table, byte, error) {
candidates := cfg.tableCandidates()
if len(candidates) == 0 {
return nil, 0, fmt.Errorf("no table configured")
}
@@ -62,7 +48,7 @@ func drainBuffered(r *bufio.Reader) ([]byte, error) {
return out, err
}
func probeHandshakeBytes(probe []byte, cfg *apis.ProtocolConfig, table *sudoku.Table) error {
func probeHandshakeBytes(probe []byte, cfg *ProtocolConfig, table *sudoku.Table) error {
rc := &readOnlyConn{Reader: bytes.NewReader(probe)}
_, obfsConn := buildServerObfsConn(rc, cfg, table, false)
cConn, err := crypto.NewAEADConn(obfsConn, cfg.Key, cfg.AEADMethod)
@@ -90,7 +76,7 @@ func probeHandshakeBytes(probe []byte, cfg *apis.ProtocolConfig, table *sudoku.T
return nil
}
func selectTableByProbe(r *bufio.Reader, cfg *apis.ProtocolConfig, tables []*sudoku.Table) (*sudoku.Table, []byte, error) {
func selectTableByProbe(r *bufio.Reader, cfg *ProtocolConfig, tables []*sudoku.Table) (*sudoku.Table, []byte, error) {
const (
maxProbeBytes = 64 * 1024
readChunk = 4 * 1024

View File

@@ -3,7 +3,7 @@ package sudoku
import (
"strings"
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
// NewTablesWithCustomPatterns builds one or more obfuscation tables from x/v/p custom patterns.

View File

@@ -2344,7 +2344,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -7716,9 +7716,9 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "reqwest"
version = "0.12.26"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -7938,7 +7938,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -8243,9 +8243,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.145"
version = "1.0.146"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
checksum = "217ca874ae0207aac254aa02c957ded05585a90892cc8d87f9e5fa49669dadd8"
dependencies = [
"indexmap 2.12.1",
"itoa",
@@ -9607,7 +9607,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix 1.0.8",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]

View File

@@ -36,7 +36,7 @@ use tauri::{Emitter, Manager};
use tauri_specta::{collect_commands, collect_events};
use utils::resolve::{is_window_opened, reset_window_open_counter};
rust_i18n::i18n!("../../locales");
rust_i18n::i18n!("./locales");
#[cfg(feature = "deadlock-detection")]
fn deadlock_detection() {
@@ -110,7 +110,7 @@ pub fn run() -> std::io::Result<()> {
let locale = utils::help::get_system_locale();
utils::help::mapping_to_i18n_key(&locale)
};
rust_i18n::set_locale(locale);
rust_i18n::set_locale(locale.to_lowercase().as_str());
if single_instance_result
.as_ref()
@@ -313,7 +313,7 @@ pub fn run() -> std::io::Result<()> {
}
let verge = { Config::verge().latest().language.clone().unwrap() };
rust_i18n::set_locale(verge.as_str());
rust_i18n::set_locale(verge.to_lowercase().as_str());
// show a dialog to print the single instance error
// Hold the guard until the end of the program if acquired

View File

@@ -32,6 +32,7 @@
"@radix-ui/react-switch": "1.2.6",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@tailwindcss/postcss": "4.1.17",
"@tanstack/react-virtual": "^3.13.13",
"@tanstack/router-zod-adapter": "1.81.5",
"@tauri-apps/api": "2.8.0",
"@types/json-schema": "7.0.15",
@@ -69,7 +70,7 @@
"@csstools/normalize.css": "12.1.1",
"@emotion/babel-plugin": "11.13.5",
"@emotion/react": "11.14.0",
"@iconify/json": "2.2.420",
"@iconify/json": "2.2.421",
"@monaco-editor/react": "4.7.0",
"@tanstack/react-query": "5.90.12",
"@tanstack/react-router": "1.134.15",

View File

@@ -0,0 +1,22 @@
import { ComponentProps } from 'react'
import { cn } from '@nyanpasu/ui'
export default function LogLevelBadge({
className,
...props
}: ComponentProps<'div'> & { children: string }) {
const childrenLower = props.children?.toLowerCase()
return (
<div
className={cn(
'inline-block rounded-full px-2 py-1 font-semibold uppercase',
childrenLower === 'info' && 'text-blue-500',
childrenLower === 'warn' && 'text-yellow-500',
childrenLower === 'error' && 'text-red-500',
className,
)}
{...props}
/>
)
}

View File

@@ -1,9 +1,94 @@
import { useEffect } from 'react'
import {
AppContentScrollArea,
useScrollArea,
} from '@/components/ui/scroll-area'
import { useClashLogs } from '@nyanpasu/interface'
import { cn } from '@nyanpasu/ui'
import { createFileRoute } from '@tanstack/react-router'
import { useVirtualizer } from '@tanstack/react-virtual'
import LogLevelBadge from './_modules/log-level-badge'
export const Route = createFileRoute('/(experimental)/experimental/logs')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/(experimental)/experimental/logs"!</div>
const InnerComponent = () => {
const {
query: { data: logs },
} = useClashLogs()
const { isBottom, viewportRef } = useScrollArea()
const rowVirtualizer = useVirtualizer({
count: logs?.length || 0,
getScrollElement: () => viewportRef.current,
estimateSize: () => 60,
overscan: 5,
measureElement: (element) => element?.getBoundingClientRect().height,
})
const virtualItems = rowVirtualizer.getVirtualItems()
useEffect(() => {
if (isBottom && logs && logs.length > 0) {
rowVirtualizer.scrollToIndex(logs.length - 1, {
align: 'end',
behavior: 'smooth',
})
}
}, [logs, isBottom, rowVirtualizer])
return (
<div
className={cn(
'relative mx-4 flex flex-col',
'divide-outline-variant divide-y',
)}
data-slot="logs-virtual-list"
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
}}
>
{virtualItems.map((virtualItem) => {
const log = logs?.[virtualItem.index]
if (!log) {
return null
}
return (
<div
key={virtualItem.key}
ref={rowVirtualizer.measureElement}
className={cn(
'absolute top-0 left-0 w-full select-text',
'font-mono break-all',
'flex flex-col py-2',
)}
data-index={virtualItem.index}
data-slot="logs-virtual-item"
style={{
transform: `translateY(${virtualItem.start}px)`,
}}
>
<div className="flex items-center gap-1">
<span className="font-semibold">{log.time}</span>
<LogLevelBadge>{log.type}</LogLevelBadge>
</div>
<div className="font-normal text-wrap">{log.payload}</div>
</div>
)
})}
</div>
)
}
function RouteComponent() {
return (
<AppContentScrollArea>
<InnerComponent />
</AppContentScrollArea>
)
}

View File

@@ -1,9 +1,85 @@
import {
AppContentScrollArea,
useScrollArea,
} from '@/components/ui/scroll-area'
import { useClashRules } from '@nyanpasu/interface'
import { cn } from '@nyanpasu/ui'
import { createFileRoute } from '@tanstack/react-router'
import { useVirtualizer } from '@tanstack/react-virtual'
export const Route = createFileRoute('/(experimental)/experimental/rules')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/(experimental)/experimental/rules"!</div>
const InnerComponent = () => {
const { data } = useClashRules()
const rules = data?.rules
const { viewportRef } = useScrollArea()
const rowVirtualizer = useVirtualizer({
count: rules?.length || 0,
getScrollElement: () => viewportRef.current,
estimateSize: () => 60,
overscan: 5,
measureElement: (element) => element?.getBoundingClientRect().height,
})
const virtualItems = rowVirtualizer.getVirtualItems()
return (
<div
className="relative mx-4 flex flex-col"
data-slot="rules-virtual-list"
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
}}
>
{virtualItems.map((virtualItem) => {
const rule = rules?.[virtualItem.index]
if (!rule) {
return null
}
return (
<div
key={virtualItem.key}
ref={rowVirtualizer.measureElement}
className={cn(
'absolute top-0 left-0 w-full select-text',
'font-mono break-all',
'flex items-center gap-2 py-2',
)}
data-index={virtualItem.index}
data-slot="rules-virtual-item"
style={{
transform: `translateY(${virtualItem.start}px)`,
}}
>
<div className="min-w-14">{virtualItem.index + 1}</div>
<div className="flex flex-col gap-1">
<div className="text-primary">{rule.payload || '-'}</div>
<div className="flex gap-8">
<div className="min-w-40 text-sm">{rule.type}</div>
<div className="text-s text-sm">{rule.proxy}</div>
</div>
</div>
</div>
)
})}
</div>
)
}
function RouteComponent() {
return (
<AppContentScrollArea>
<InnerComponent />
</AppContentScrollArea>
)
}

View File

@@ -24,7 +24,7 @@ const AppContent = () => {
function RouteComponent() {
return (
<div
className={cn('flex max-h-dvh min-h-dvh flex-col', 'bg-background')}
className={cn('flex max-h-dvh min-h-dvh flex-col', 'bg-background/30')}
onContextMenu={(e) => {
e.preventDefault()
}}

View File

@@ -67,8 +67,8 @@
"@types/fs-extra": "11.0.4",
"@types/lodash-es": "4.17.12",
"@types/node": "24.10.4",
"@typescript-eslint/eslint-plugin": "8.50.0",
"@typescript-eslint/parser": "8.50.0",
"@typescript-eslint/eslint-plugin": "8.50.1",
"@typescript-eslint/parser": "8.50.1",
"autoprefixer": "10.4.23",
"conventional-changelog-conventionalcommits": "9.1.0",
"cross-env": "10.1.0",
@@ -107,7 +107,7 @@
"tailwindcss": "4.1.17",
"tsx": "4.21.0",
"typescript": "5.9.3",
"typescript-eslint": "8.50.0"
"typescript-eslint": "8.50.1"
},
"packageManager": "pnpm@10.26.1",
"engines": {

View File

@@ -50,11 +50,11 @@ importers:
specifier: 24.10.4
version: 24.10.4
'@typescript-eslint/eslint-plugin':
specifier: 8.50.0
version: 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
specifier: 8.50.1
version: 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser':
specifier: 8.50.0
version: 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
specifier: 8.50.1
version: 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
autoprefixer:
specifier: 10.4.23
version: 10.4.23(postcss@8.5.6)
@@ -75,13 +75,13 @@ importers:
version: 10.1.8(eslint@9.39.2(jiti@2.6.1))
eslint-import-resolver-alias:
specifier: 1.1.2
version: 1.1.2(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))
version: 1.1.2(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))
eslint-plugin-html:
specifier: 8.1.3
version: 8.1.3
eslint-plugin-import:
specifier: 2.32.0
version: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
version: 2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-n:
specifier: 17.23.1
version: 17.23.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
@@ -111,7 +111,7 @@ importers:
version: 16.2.7
neostandard:
specifier: 0.12.2
version: 0.12.2(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
version: 0.12.2(@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
npm-run-all2:
specifier: 8.0.4
version: 8.0.4
@@ -170,8 +170,8 @@ importers:
specifier: 5.9.3
version: 5.9.3
typescript-eslint:
specifier: 8.50.0
version: 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
specifier: 8.50.1
version: 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
frontend/interface:
dependencies:
@@ -275,6 +275,9 @@ importers:
'@tailwindcss/postcss':
specifier: 4.1.17
version: 4.1.17
'@tanstack/react-virtual':
specifier: ^3.13.13
version: 3.13.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@tanstack/router-zod-adapter':
specifier: 1.81.5
version: 1.81.5(@tanstack/react-router@1.134.15(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(zod@4.1.13)
@@ -382,8 +385,8 @@ importers:
specifier: 11.14.0
version: 11.14.0(@types/react@19.2.7)(react@19.2.0)
'@iconify/json':
specifier: 2.2.420
version: 2.2.420
specifier: 2.2.421
version: 2.2.421
'@monaco-editor/react':
specifier: 4.7.0
version: 4.7.0(monaco-editor@0.54.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -605,8 +608,8 @@ importers:
specifier: 11.0.13
version: 11.0.13
p-retry:
specifier: 7.1.0
version: 7.1.0
specifier: 7.1.1
version: 7.1.1
semver:
specifier: 7.7.3
version: 7.7.3
@@ -1906,8 +1909,8 @@ packages:
prettier-plugin-ember-template-tag:
optional: true
'@iconify/json@2.2.420':
resolution: {integrity: sha512-3dgvkB8eiUA/PkGHaRB+x17MS1u22V2O1YtFpdpdFpbXp/fAdloOMbxeMXXgNlcGgjk7x4hUIeDunic/diHYqg==}
'@iconify/json@2.2.421':
resolution: {integrity: sha512-jc0BOjmdVz4DxfYlvJWEBl8FIdOtgRCDZkyOEDFlHqCnfw6Yyr41fDaiECU/FSzkqjwNuKu5H10N7bLN+JPQGA==}
'@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@@ -3537,6 +3540,12 @@ packages:
react: '>=16.8'
react-dom: '>=16.8'
'@tanstack/react-virtual@3.13.13':
resolution: {integrity: sha512-4o6oPMDvQv+9gMi8rE6gWmsOjtUZUYIJHv7EB+GblyYdi8U6OqLl8rhHWIUZSL1dUU2dPwTdTgybCKf9EjIrQg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/react-virtual@3.13.9':
resolution: {integrity: sha512-SPWC8kwG/dWBf7Py7cfheAPOxuvIv4fFQ54PdmYbg7CpXfsKxkucak43Q0qKsxVthhUJQ1A7CIMAIplq4BjVwA==}
peerDependencies:
@@ -3602,6 +3611,9 @@ packages:
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
engines: {node: '>=12'}
'@tanstack/virtual-core@3.13.13':
resolution: {integrity: sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==}
'@tanstack/virtual-core@3.13.9':
resolution: {integrity: sha512-3jztt0jpaoJO5TARe2WIHC1UQC3VMLAFUW5mmMo0yrkwtDB2AQP0+sh10BVUpWrnvHjSLvzFizydtEGLCJKFoQ==}
@@ -3942,16 +3954,16 @@ packages:
'@types/yargs@17.0.35':
resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
'@typescript-eslint/eslint-plugin@8.50.0':
resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==}
'@typescript-eslint/eslint-plugin@8.50.1':
resolution: {integrity: sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.50.0
'@typescript-eslint/parser': ^8.50.1
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/parser@8.50.0':
resolution: {integrity: sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==}
'@typescript-eslint/parser@8.50.1':
resolution: {integrity: sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@@ -3963,8 +3975,8 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/project-service@8.50.0':
resolution: {integrity: sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==}
'@typescript-eslint/project-service@8.50.1':
resolution: {integrity: sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
@@ -3973,8 +3985,8 @@ packages:
resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/scope-manager@8.50.0':
resolution: {integrity: sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==}
'@typescript-eslint/scope-manager@8.50.1':
resolution: {integrity: sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.46.2':
@@ -3983,20 +3995,20 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/tsconfig-utils@8.46.3':
resolution: {integrity: sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/tsconfig-utils@8.50.0':
resolution: {integrity: sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.50.0':
resolution: {integrity: sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==}
'@typescript-eslint/tsconfig-utils@8.50.1':
resolution: {integrity: sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.50.1':
resolution: {integrity: sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@@ -4010,22 +4022,22 @@ packages:
resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.46.3':
resolution: {integrity: sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.50.0':
resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.50.1':
resolution: {integrity: sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.46.2':
resolution: {integrity: sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/typescript-estree@8.50.0':
resolution: {integrity: sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==}
'@typescript-eslint/typescript-estree@8.50.1':
resolution: {integrity: sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
@@ -4037,8 +4049,8 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@8.50.0':
resolution: {integrity: sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==}
'@typescript-eslint/utils@8.50.1':
resolution: {integrity: sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@@ -4048,8 +4060,8 @@ packages:
resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.50.0':
resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==}
'@typescript-eslint/visitor-keys@8.50.1':
resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@uidotdev/usehooks@2.4.1':
@@ -7135,8 +7147,8 @@ packages:
resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
p-retry@7.1.0:
resolution: {integrity: sha512-xL4PiFRQa/f9L9ZvR4/gUCRNus4N8YX80ku8kv9Jqz+ZokkiZLM0bcvX0gm1F3PDi9SPRsww1BDsTWgE6Y1GLQ==}
p-retry@7.1.1:
resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==}
engines: {node: '>=20'}
package-manager-detector@1.3.0:
@@ -8421,8 +8433,8 @@ packages:
typedarray-to-buffer@3.1.5:
resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
typescript-eslint@8.50.0:
resolution: {integrity: sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==}
typescript-eslint@8.50.1:
resolution: {integrity: sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@@ -10535,7 +10547,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@iconify/json@2.2.420':
'@iconify/json@2.2.421':
dependencies:
'@iconify/types': 2.0.0
pathe: 2.0.3
@@ -12111,6 +12123,12 @@ snapshots:
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
'@tanstack/react-virtual@3.13.13(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@tanstack/virtual-core': 3.13.13
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
'@tanstack/react-virtual@3.13.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@tanstack/virtual-core': 3.13.9
@@ -12207,6 +12225,8 @@ snapshots:
'@tanstack/table-core@8.21.3': {}
'@tanstack/virtual-core@3.13.13': {}
'@tanstack/virtual-core@3.13.9': {}
'@tanstack/virtual-file-routes@1.133.19': {}
@@ -12562,14 +12582,14 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
'@typescript-eslint/eslint-plugin@8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.50.0
'@typescript-eslint/type-utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.50.0
'@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.50.1
'@typescript-eslint/type-utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.50.1
eslint: 9.39.2(jiti@2.6.1)
ignore: 7.0.5
natural-compare: 1.4.0
@@ -12578,12 +12598,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
'@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.50.0
'@typescript-eslint/types': 8.50.0
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.50.0
'@typescript-eslint/scope-manager': 8.50.1
'@typescript-eslint/types': 8.50.1
'@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.50.1
debug: 4.4.3
eslint: 9.39.2(jiti@2.6.1)
typescript: 5.9.3
@@ -12592,17 +12612,17 @@ snapshots:
'@typescript-eslint/project-service@8.46.2(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3)
'@typescript-eslint/types': 8.46.3
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
'@typescript-eslint/types': 8.50.0
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.50.0(typescript@5.9.3)':
'@typescript-eslint/project-service@8.50.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
'@typescript-eslint/types': 8.50.0
'@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3)
'@typescript-eslint/types': 8.50.1
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
@@ -12613,28 +12633,28 @@ snapshots:
'@typescript-eslint/types': 8.46.2
'@typescript-eslint/visitor-keys': 8.46.2
'@typescript-eslint/scope-manager@8.50.0':
'@typescript-eslint/scope-manager@8.50.1':
dependencies:
'@typescript-eslint/types': 8.50.0
'@typescript-eslint/visitor-keys': 8.50.0
'@typescript-eslint/types': 8.50.1
'@typescript-eslint/visitor-keys': 8.50.1
'@typescript-eslint/tsconfig-utils@8.46.2(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/tsconfig-utils@8.46.3(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/tsconfig-utils@8.50.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/type-utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
'@typescript-eslint/tsconfig-utils@8.50.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.50.0
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
typescript: 5.9.3
'@typescript-eslint/type-utils@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.50.1
'@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3)
'@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
debug: 4.4.3
eslint: 9.39.2(jiti@2.6.1)
ts-api-utils: 2.1.0(typescript@5.9.3)
@@ -12646,10 +12666,10 @@ snapshots:
'@typescript-eslint/types@8.46.2': {}
'@typescript-eslint/types@8.46.3': {}
'@typescript-eslint/types@8.50.0': {}
'@typescript-eslint/types@8.50.1': {}
'@typescript-eslint/typescript-estree@8.46.2(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.46.2(typescript@5.9.3)
@@ -12666,12 +12686,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.50.0(typescript@5.9.3)':
'@typescript-eslint/typescript-estree@8.50.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.50.0(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
'@typescript-eslint/types': 8.50.0
'@typescript-eslint/visitor-keys': 8.50.0
'@typescript-eslint/project-service': 8.50.1(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3)
'@typescript-eslint/types': 8.50.1
'@typescript-eslint/visitor-keys': 8.50.1
debug: 4.4.3
minimatch: 9.0.5
semver: 7.7.3
@@ -12692,12 +12712,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
'@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.8.0(eslint@9.39.2(jiti@2.6.1))
'@typescript-eslint/scope-manager': 8.50.0
'@typescript-eslint/types': 8.50.0
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.50.1
'@typescript-eslint/types': 8.50.1
'@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1)
typescript: 5.9.3
transitivePeerDependencies:
@@ -12708,9 +12728,9 @@ snapshots:
'@typescript-eslint/types': 8.46.2
eslint-visitor-keys: 4.2.1
'@typescript-eslint/visitor-keys@8.50.0':
'@typescript-eslint/visitor-keys@8.50.1':
dependencies:
'@typescript-eslint/types': 8.50.0
'@typescript-eslint/types': 8.50.1
eslint-visitor-keys: 4.2.1
'@uidotdev/usehooks@2.4.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
@@ -14419,9 +14439,9 @@ snapshots:
optionalDependencies:
unrs-resolver: 1.10.1
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))):
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))):
dependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
eslint-import-resolver-node@0.3.9:
dependencies:
@@ -14431,7 +14451,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -14442,16 +14462,16 @@ snapshots:
tinyglobby: 0.2.14
unrs-resolver: 1.10.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
transitivePeerDependencies:
@@ -14468,7 +14488,7 @@ snapshots:
dependencies:
htmlparser2: 10.0.0
eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)):
eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@typescript-eslint/types': 8.41.0
comment-parser: 1.4.1
@@ -14481,12 +14501,12 @@ snapshots:
stable-hash-x: 0.2.0
unrs-resolver: 1.10.1
optionalDependencies:
'@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint-import-resolver-node: 0.3.9
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -14497,7 +14517,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -14509,7 +14529,7 @@ snapshots:
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
'@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
@@ -16107,20 +16127,20 @@ snapshots:
sax: 1.3.0
optional: true
neostandard@0.12.2(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
neostandard@0.12.2(@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@humanwhocodes/gitignore-to-minimatch': 1.0.2
'@stylistic/eslint-plugin': 2.11.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-n: 17.23.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint-plugin-promise: 7.2.1(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
find-up: 5.0.0
globals: 15.15.0
peowly: 1.3.2
typescript-eslint: 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
typescript-eslint: 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
transitivePeerDependencies:
- '@typescript-eslint/utils'
- eslint-import-resolver-node
@@ -16340,7 +16360,7 @@ snapshots:
dependencies:
p-limit: 4.0.0
p-retry@7.1.0:
p-retry@7.1.1:
dependencies:
is-network-error: 1.1.0
@@ -17694,12 +17714,12 @@ snapshots:
dependencies:
is-typedarray: 1.0.0
typescript-eslint@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
typescript-eslint@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/eslint-plugin': 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3)
'@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1)
typescript: 5.9.3
transitivePeerDependencies:

View File

@@ -8,7 +8,7 @@
"@types/semver": "7.7.1",
"figlet": "1.9.4",
"filesize": "11.0.13",
"p-retry": "7.1.0",
"p-retry": "7.1.1",
"semver": "7.7.3",
"zod": "4.1.13"
},

View File

@@ -1,2 +1,2 @@
LINUX_VERSION-6.12 = .62
LINUX_KERNEL_HASH-6.12.62 = 13e2c685ac8fab5dd992dd105732554dae514aef350c2a8c7418e7b74eb62c13
LINUX_VERSION-6.12 = .63
LINUX_KERNEL_HASH-6.12.63 = 9502c5ffe4b894383c97abfccf74430a84732f04ee476b9c0d87635b29df7db3

View File

@@ -15556,7 +15556,7 @@ Signed-off-by: j-schambacher <joerg@hifiberry.com>
imply SND_SOC_MAX98088
imply SND_SOC_MAX98090
imply SND_SOC_MAX98095
@@ -175,6 +177,7 @@ config SND_SOC_ALL_CODECS
@@ -176,6 +178,7 @@ config SND_SOC_ALL_CODECS
imply SND_SOC_PCM179X_SPI
imply SND_SOC_PCM186X_I2C
imply SND_SOC_PCM186X_SPI
@@ -15564,7 +15564,7 @@ Signed-off-by: j-schambacher <joerg@hifiberry.com>
imply SND_SOC_PCM3008
imply SND_SOC_PCM3060_I2C
imply SND_SOC_PCM3060_SPI
@@ -267,6 +270,7 @@ config SND_SOC_ALL_CODECS
@@ -268,6 +271,7 @@ config SND_SOC_ALL_CODECS
imply SND_SOC_TLV320ADCX140
imply SND_SOC_TLV320AIC23_I2C
imply SND_SOC_TLV320AIC23_SPI
@@ -15572,7 +15572,7 @@ Signed-off-by: j-schambacher <joerg@hifiberry.com>
imply SND_SOC_TLV320AIC26
imply SND_SOC_TLV320AIC31XX
imply SND_SOC_TLV320AIC32X4_I2C
@@ -424,12 +428,12 @@ config SND_SOC_AD193X
@@ -425,12 +429,12 @@ config SND_SOC_AD193X
tristate
config SND_SOC_AD193X_SPI
@@ -15587,7 +15587,7 @@ Signed-off-by: j-schambacher <joerg@hifiberry.com>
depends on I2C
select SND_SOC_AD193X
@@ -1229,6 +1233,13 @@ config SND_SOC_LOCHNAGAR_SC
@@ -1230,6 +1234,13 @@ config SND_SOC_LOCHNAGAR_SC
This driver support the sound card functionality of the Cirrus
Logic Lochnagar audio development board.
@@ -15601,7 +15601,7 @@ Signed-off-by: j-schambacher <joerg@hifiberry.com>
config SND_SOC_MADERA
tristate
default y if SND_SOC_CS47L15=y
@@ -1638,6 +1649,10 @@ config SND_SOC_RT5616
@@ -1639,6 +1650,10 @@ config SND_SOC_RT5616
tristate "Realtek RT5616 CODEC"
depends on I2C
@@ -15612,7 +15612,7 @@ Signed-off-by: j-schambacher <joerg@hifiberry.com>
config SND_SOC_RT5631
tristate "Realtek ALC5631/RT5631 CODEC"
depends on I2C
@@ -1995,6 +2010,9 @@ config SND_SOC_TFA9879
@@ -1996,6 +2011,9 @@ config SND_SOC_TFA9879
tristate "NXP Semiconductors TFA9879 amplifier"
depends on I2C
@@ -15622,7 +15622,7 @@ Signed-off-by: j-schambacher <joerg@hifiberry.com>
config SND_SOC_TFA989X
tristate "NXP/Goodix TFA989X (TFA1) amplifiers"
depends on I2C
@@ -2596,4 +2614,8 @@ config SND_SOC_LPASS_TX_MACRO
@@ -2601,4 +2619,8 @@ config SND_SOC_LPASS_TX_MACRO
select SND_SOC_LPASS_MACRO_COMMON
tristate "Qualcomm TX Macro in LPASS(Low Power Audio SubSystem)"
@@ -15633,7 +15633,7 @@ Signed-off-by: j-schambacher <joerg@hifiberry.com>
endmenu
--- a/sound/soc/codecs/Makefile
+++ b/sound/soc/codecs/Makefile
@@ -820,3 +820,12 @@ obj-$(CONFIG_SND_SOC_LPASS_TX_MACRO) +=
@@ -822,3 +822,12 @@ obj-$(CONFIG_SND_SOC_LPASS_TX_MACRO) +=
# Mux
obj-$(CONFIG_SND_SOC_SIMPLE_MUX) += snd-soc-simple-mux.o

View File

@@ -351,7 +351,7 @@ Signed-off-by: Jonathan Bell <jonathan@raspberrypi.com>
--- a/drivers/usb/dwc3/host.c
+++ b/drivers/usb/dwc3/host.c
@@ -126,10 +126,12 @@ out:
@@ -129,10 +129,12 @@ out:
int dwc3_host_init(struct dwc3 *dwc)
{
@@ -364,7 +364,7 @@ Signed-off-by: Jonathan Bell <jonathan@raspberrypi.com>
/*
* Some platforms need to power off all Root hub ports immediately after DWC3 set to host
@@ -141,7 +143,12 @@ int dwc3_host_init(struct dwc3 *dwc)
@@ -144,7 +146,12 @@ int dwc3_host_init(struct dwc3 *dwc)
if (irq < 0)
return irq;

View File

@@ -52,7 +52,7 @@ Signed-off-by: Dom Cobley <popcornmix@gmail.com>
/* L2 intc private data structure */
struct brcmstb_l2_intc_data {
struct irq_domain *domain;
@@ -299,11 +309,18 @@ static int __init brcmstb_l2_lvl_intc_of
@@ -295,11 +305,18 @@ static int brcmstb_l2_lvl_intc_of_init(s
return brcmstb_l2_intc_of_init(np, parent, &l2_lvl_intc_init);
}

View File

@@ -6225,6 +6225,7 @@ CONFIG_SND_SOC_INTEL_SST_TOPLEVEL=y
# CONFIG_SND_SOC_MT8365 is not set
# CONFIG_SND_SOC_MTK_BTCVSD is not set
# CONFIG_SND_SOC_NAU8315 is not set
# CONFIG_SND_SOC_NAU8325 is not set
# CONFIG_SND_SOC_NAU8540 is not set
# CONFIG_SND_SOC_NAU8810 is not set
# CONFIG_SND_SOC_NAU8821 is not set

View File

@@ -409,7 +409,7 @@ Signed-off-by: Felix Fietkau <nbd@nbd.name>
--- a/net/ipv4/inet_timewait_sock.c
+++ b/net/ipv4/inet_timewait_sock.c
@@ -296,7 +296,7 @@ void __inet_twsk_schedule(struct inet_ti
@@ -285,7 +285,7 @@ void __inet_twsk_schedule(struct inet_ti
*/
if (!rearm) {

View File

@@ -55,7 +55,7 @@ Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
}
--- a/net/netfilter/nft_flow_offload.c
+++ b/net/netfilter/nft_flow_offload.c
@@ -487,47 +487,14 @@ static struct nft_expr_type nft_flow_off
@@ -494,47 +494,14 @@ static struct nft_expr_type nft_flow_off
.owner = THIS_MODULE,
};

View File

@@ -25,7 +25,7 @@ Signed-off-by: Nikhil Prakash V <nprakash@codeaurora.org>
@@ -86,6 +87,9 @@
#define TCSR_WCSS_CLK_ENABLE 0x14
#define MAX_HALT_REG 3
#define MAX_HALT_REG 4
+
+#define WCNSS_PAS_ID 6
+

View File

@@ -13,15 +13,6 @@ Signed-off-by: Sricharan R <sricharan@codeaurora.org>
--- a/drivers/remoteproc/qcom_q6v5_wcss.c
+++ b/drivers/remoteproc/qcom_q6v5_wcss.c
@@ -86,7 +86,7 @@
#define TCSR_WCSS_CLK_MASK 0x1F
#define TCSR_WCSS_CLK_ENABLE 0x14
-#define MAX_HALT_REG 3
+#define MAX_HALT_REG 4
#define WCNSS_PAS_ID 6
@@ -155,6 +155,7 @@ struct wcss_data {
u32 version;
bool aon_reset_required;
@@ -48,19 +39,6 @@ Signed-off-by: Sricharan R <sricharan@codeaurora.org>
}
return 0;
@@ -929,9 +933,9 @@ static int q6v5_wcss_init_mmio(struct q6
return -EINVAL;
}
- wcss->halt_q6 = halt_reg[0];
- wcss->halt_wcss = halt_reg[1];
- wcss->halt_nc = halt_reg[2];
+ wcss->halt_q6 = halt_reg[1];
+ wcss->halt_wcss = halt_reg[2];
+ wcss->halt_nc = halt_reg[3];
return 0;
}
@@ -1173,6 +1177,7 @@ static const struct wcss_data wcss_ipq80
.crash_reason_smem = WCSS_CRASH_REASON,
.aon_reset_required = true,

View File

@@ -30,6 +30,9 @@ type SudokuOption struct {
TableType string `proxy:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
EnablePureDownlink *bool `proxy:"enable-pure-downlink,omitempty"`
HTTPMask bool `proxy:"http-mask,omitempty"`
HTTPMaskMode string `proxy:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
HTTPMaskTLS bool `proxy:"http-mask-tls,omitempty"` // only for http-mask-mode stream/poll/auto
HTTPMaskHost string `proxy:"http-mask-host,omitempty"` // optional Host/SNI override (domain or domain:port)
HTTPMaskStrategy string `proxy:"http-mask-strategy,omitempty"` // "random" (default), "post", "websocket"
CustomTable string `proxy:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
CustomTables []string `proxy:"custom-tables,omitempty"` // optional table rotation patterns, overrides custom-table when non-empty
@@ -42,7 +45,16 @@ func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
return nil, err
}
c, err := s.dialer.DialContext(ctx, "tcp", s.addr)
var c net.Conn
if !cfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext)
}
}
if c == nil && err == nil {
c, err = s.dialer.DialContext(ctx, "tcp", s.addr)
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
}
@@ -56,9 +68,14 @@ func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
defer done(&err)
}
c, err = sudoku.ClientHandshakeWithOptions(c, cfg, sudoku.ClientHandshakeOptions{
HTTPMaskStrategy: s.option.HTTPMaskStrategy,
})
handshakeCfg := *cfg
if !handshakeCfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(handshakeCfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
handshakeCfg.DisableHTTPMask = true
}
}
c, err = sudoku.ClientHandshakeWithOptions(c, &handshakeCfg, sudoku.ClientHandshakeOptions{HTTPMaskStrategy: s.option.HTTPMaskStrategy})
if err != nil {
return nil, err
}
@@ -87,7 +104,16 @@ func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
return nil, err
}
c, err := s.dialer.DialContext(ctx, "tcp", s.addr)
var c net.Conn
if !cfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext)
}
}
if c == nil && err == nil {
c, err = s.dialer.DialContext(ctx, "tcp", s.addr)
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
}
@@ -101,9 +127,14 @@ func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
defer done(&err)
}
c, err = sudoku.ClientHandshakeWithOptions(c, cfg, sudoku.ClientHandshakeOptions{
HTTPMaskStrategy: s.option.HTTPMaskStrategy,
})
handshakeCfg := *cfg
if !handshakeCfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(handshakeCfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
handshakeCfg.DisableHTTPMask = true
}
}
c, err = sudoku.ClientHandshakeWithOptions(c, &handshakeCfg, sudoku.ClientHandshakeOptions{HTTPMaskStrategy: s.option.HTTPMaskStrategy})
if err != nil {
return nil, err
}
@@ -190,6 +221,12 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
EnablePureDownlink: enablePureDownlink,
HandshakeTimeoutSeconds: defaultConf.HandshakeTimeoutSeconds,
DisableHTTPMask: !option.HTTPMask,
HTTPMaskMode: defaultConf.HTTPMaskMode,
HTTPMaskTLSEnabled: option.HTTPMaskTLS,
HTTPMaskHost: option.HTTPMaskHost,
}
if option.HTTPMaskMode != "" {
baseConf.HTTPMaskMode = option.HTTPMaskMode
}
tables, err := sudoku.NewTablesWithCustomPatterns(sudoku.ClientAEADSeed(option.Key), tableType, option.CustomTable, option.CustomTables)
if err != nil {

View File

@@ -1041,7 +1041,7 @@ proxies: # socks5
# sudoku
- name: sudoku
type: sudoku
server: serverip # 1.2.3.4
server: server_ip/domain # 1.2.3.4 or domain
port: 443
key: "<client_key>" # 如果你使用sudoku生成的ED25519密钥对请填写密钥对中的私钥否则填入和服务端相同的uuid
aead-method: chacha20-poly1305 # 可选值chacha20-poly1305、aes-128-gcm、none 我们保证在none的情况下sudoku混淆层仍然确保安全
@@ -1051,7 +1051,10 @@ proxies: # socks5
# custom-table: xpxvvpvv # 可选自定义字节布局必须包含2个x、2个p、4个v可随意组合。启用此处则需配置`table-type`为`prefer_entropy`
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选自定义字节布局列表x/v/p用于 xvp 模式轮换;非空时覆盖 custom-table
http-mask: true # 是否启用http掩码
# http-mask-strategy: random # 可选random默认、post、websocket仅在 http-mask=true 时生效
# http-mask-mode: legacy # 可选legacy默认、stream、poll、autostream/poll/auto 支持走 CDN/反代
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效true 强制 httpsfalse 强制 http不会根据端口自动推断
# http-mask-host: "" # 可选:覆盖 Host/SNI支持 example.com 或 example.com:443仅在 http-mask-mode 为 stream/poll/auto 时生效
# http-mask-strategy: random # 可选random默认、post、websocket仅 legacy 下生效
enable-pure-downlink: false # 是否启用混淆下行false的情况下能在保证数据安全的前提下极大提升下行速度与服务端端保持相同(如果此处为false则要求aead不可为none)
# anytls
@@ -1596,6 +1599,8 @@ listeners:
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选自定义字节布局列表x/v/p用于 xvp 模式轮换;非空时覆盖 custom-table
handshake-timeout: 5 # optional
enable-pure-downlink: false # 是否启用混淆下行false的情况下能在保证数据安全的前提下极大提升下行速度与客户端保持相同(如果此处为false则要求aead不可为none)
disable-http-mask: false # 可选:禁用 http 掩码/隧道(默认为 false
# http-mask-mode: legacy # 可选legacy默认、stream、poll、autostream/poll/auto 支持走 CDN/反代

View File

@@ -3,6 +3,7 @@ module github.com/metacubex/mihomo
go 1.20
require (
filippo.io/edwards25519 v1.1.0
github.com/bahlo/generic-list-go v0.2.0
github.com/coreos/go-iptables v0.8.0
github.com/dlclark/regexp2 v1.11.5
@@ -46,7 +47,6 @@ 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.2-d
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
github.com/samber/lo v1.52.0
github.com/sirupsen/logrus v1.9.3
@@ -66,7 +66,6 @@ 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

View File

@@ -170,8 +170,6 @@ 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/saba-futai/sudoku v0.0.2-d h1:HW/gIyNUFcDchpMN+ZhluM86U/HGkWkkRV+9Km6WZM8=
github.com/saba-futai/sudoku v0.0.2-d/go.mod h1:Rvggsoprp7HQM7bMIZUd1M27bPj8THRsZdY1dGbIAvo=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=

View File

@@ -20,6 +20,8 @@ type SudokuServer struct {
EnablePureDownlink *bool `json:"enable-pure-downlink,omitempty"`
CustomTable string `json:"custom-table,omitempty"`
CustomTables []string `json:"custom-tables,omitempty"`
DisableHTTPMask bool `json:"disable-http-mask,omitempty"`
HTTPMaskMode string `json:"http-mask-mode,omitempty"`
// mihomo private extension (not the part of standard Sudoku protocol)
MuxOption sing.MuxOption `json:"mux-option,omitempty"`

View File

@@ -3,7 +3,9 @@ package inbound_test
import (
"net"
"net/netip"
"runtime"
"strconv"
"strings"
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
@@ -149,6 +151,9 @@ func TestNewMieru(t *testing.T) {
}
func TestInboundMieru(t *testing.T) {
if runtime.GOOS == "windows" && strings.HasPrefix(runtime.Version(), "go1.26") {
t.Skip("temporarily skipped on windows due to intermittent failures; tracked in PR")
}
t.Run("TCP_HANDSHAKE_STANDARD", func(t *testing.T) {
testInboundMieruTCP(t, "HANDSHAKE_STANDARD")
})

View File

@@ -22,6 +22,8 @@ type SudokuOption struct {
EnablePureDownlink *bool `inbound:"enable-pure-downlink,omitempty"`
CustomTable string `inbound:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
CustomTables []string `inbound:"custom-tables,omitempty"`
DisableHTTPMask bool `inbound:"disable-http-mask,omitempty"`
HTTPMaskMode string `inbound:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
// mihomo private extension (not the part of standard Sudoku protocol)
MuxOption MuxOption `inbound:"mux-option,omitempty"`
@@ -59,6 +61,8 @@ func NewSudoku(options *SudokuOption) (*Sudoku, error) {
EnablePureDownlink: options.EnablePureDownlink,
CustomTable: options.CustomTable,
CustomTables: options.CustomTables,
DisableHTTPMask: options.DisableHTTPMask,
HTTPMaskMode: options.HTTPMaskMode,
}
serverConf.MuxOption = options.MuxOption.Build()

View File

@@ -2,6 +2,7 @@ package inbound_test
import (
"net/netip"
"runtime"
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
@@ -164,3 +165,27 @@ func TestInboundSudoku_CustomTable(t *testing.T) {
testInboundSudoku(t, inboundOptions, outboundOptions)
})
}
func TestInboundSudoku_HTTPMaskMode(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("temporarily skipped on windows due to intermittent failures; tracked in PR")
}
key := "test_key_http_mask_mode"
for _, mode := range []string{"legacy", "stream", "poll", "auto"} {
mode := mode
t.Run(mode, func(t *testing.T) {
inboundOptions := inbound.SudokuOption{
Key: key,
HTTPMaskMode: mode,
}
outboundOptions := outbound.SudokuOption{
Key: key,
HTTPMask: true,
HTTPMaskMode: mode,
}
testInboundSudoku(t, inboundOptions, outboundOptions)
})
}
}

View File

@@ -20,6 +20,7 @@ type Listener struct {
addr string
closed bool
protoConf sudoku.ProtocolConfig
tunnelSrv *sudoku.HTTPMaskTunnelServer
handler *sing.ListenerHandler
}
@@ -46,9 +47,31 @@ func (l *Listener) Close() error {
}
func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
session, err := sudoku.ServerHandshake(conn, &l.protoConf)
handshakeConn := conn
handshakeCfg := &l.protoConf
if l.tunnelSrv != nil {
c, cfg, done, err := l.tunnelSrv.WrapConn(conn)
if err != nil {
_ = conn.Close()
return
}
if done {
return
}
if c != nil {
handshakeConn = c
}
if cfg != nil {
handshakeCfg = cfg
}
}
session, err := sudoku.ServerHandshake(handshakeConn, handshakeCfg)
if err != nil {
_ = conn.Close()
_ = handshakeConn.Close()
if handshakeConn != conn {
_ = conn.Close()
}
return
}
@@ -184,6 +207,8 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
PaddingMax: paddingMax,
EnablePureDownlink: enablePureDownlink,
HandshakeTimeoutSeconds: handshakeTimeout,
DisableHTTPMask: config.DisableHTTPMask,
HTTPMaskMode: config.HTTPMaskMode,
}
if len(tables) == 1 {
protoConf.Table = tables[0]
@@ -200,6 +225,7 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
protoConf: protoConf,
handler: h,
}
sl.tunnelSrv = sudoku.NewHTTPMaskTunnelServer(&sl.protoConf)
go func() {
for {

View File

@@ -0,0 +1,144 @@
package sudoku
import (
"fmt"
"strings"
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
// ProtocolConfig defines the configuration for the Sudoku protocol stack.
// It is intentionally kept close to the upstream Sudoku project to ensure wire compatibility.
type ProtocolConfig struct {
// Client-only: "host:port".
ServerAddress string
// Pre-shared key (or ED25519 key material) used to derive crypto and tables.
Key string
// "aes-128-gcm", "chacha20-poly1305", or "none".
AEADMethod string
// Table is the single obfuscation table to use when table rotation is disabled.
Table *sudoku.Table
// Tables is an optional candidate set for table rotation.
// If provided (len>0), the client will pick one table per connection and the server will
// probe the handshake to detect which one was used, keeping the handshake format unchanged.
// When Tables is set, Table may be nil.
Tables []*sudoku.Table
// Padding insertion ratio (0-100). Must satisfy PaddingMax >= PaddingMin.
PaddingMin int
PaddingMax int
// EnablePureDownlink toggles the bandwidth-optimized downlink mode.
EnablePureDownlink bool
// Client-only: final target "host:port".
TargetAddress string
// Server-side handshake timeout (seconds).
HandshakeTimeoutSeconds int
// DisableHTTPMask disables all HTTP camouflage layers.
DisableHTTPMask bool
// HTTPMaskMode controls how the HTTP layer behaves:
// - "legacy": write a fake HTTP/1.1 header then switch to raw stream (default, not CDN-compatible)
// - "stream": real HTTP tunnel (stream-one or split), CDN-compatible
// - "poll": plain HTTP tunnel (authorize/push/pull), strong restricted-network pass-through
// - "auto": try stream then fall back to poll
HTTPMaskMode string
// HTTPMaskTLSEnabled enables HTTPS for HTTP tunnel modes (client-side).
// If false, the tunnel uses HTTP (no port-based inference).
HTTPMaskTLSEnabled bool
// HTTPMaskHost optionally overrides the HTTP Host header / SNI host for HTTP tunnel modes (client-side).
HTTPMaskHost string
}
func (c *ProtocolConfig) Validate() error {
if c.Table == nil && len(c.Tables) == 0 {
return fmt.Errorf("table cannot be nil (or provide tables)")
}
for i, t := range c.Tables {
if t == nil {
return fmt.Errorf("tables[%d] cannot be nil", i)
}
}
if c.Key == "" {
return fmt.Errorf("key cannot be empty")
}
switch c.AEADMethod {
case "aes-128-gcm", "chacha20-poly1305", "none":
default:
return fmt.Errorf("invalid aead-method: %s, must be one of: aes-128-gcm, chacha20-poly1305, none", c.AEADMethod)
}
if c.PaddingMin < 0 || c.PaddingMin > 100 {
return fmt.Errorf("padding-min must be between 0 and 100, got %d", c.PaddingMin)
}
if c.PaddingMax < 0 || c.PaddingMax > 100 {
return fmt.Errorf("padding-max must be between 0 and 100, got %d", c.PaddingMax)
}
if c.PaddingMax < c.PaddingMin {
return fmt.Errorf("padding-max (%d) must be >= padding-min (%d)", c.PaddingMax, c.PaddingMin)
}
if !c.EnablePureDownlink && c.AEADMethod == "none" {
return fmt.Errorf("bandwidth optimized downlink requires AEAD")
}
if c.HandshakeTimeoutSeconds < 0 {
return fmt.Errorf("handshake-timeout must be >= 0, got %d", c.HandshakeTimeoutSeconds)
}
switch strings.ToLower(strings.TrimSpace(c.HTTPMaskMode)) {
case "", "legacy", "stream", "poll", "auto":
default:
return fmt.Errorf("invalid http-mask-mode: %s, must be one of: legacy, stream, poll, auto", c.HTTPMaskMode)
}
return nil
}
func (c *ProtocolConfig) ValidateClient() error {
if err := c.Validate(); err != nil {
return err
}
if c.ServerAddress == "" {
return fmt.Errorf("server address cannot be empty")
}
if c.TargetAddress == "" {
return fmt.Errorf("target address cannot be empty")
}
return nil
}
func DefaultConfig() *ProtocolConfig {
return &ProtocolConfig{
AEADMethod: "chacha20-poly1305",
PaddingMin: 10,
PaddingMax: 30,
EnablePureDownlink: true,
HandshakeTimeoutSeconds: 5,
HTTPMaskMode: "legacy",
}
}
func (c *ProtocolConfig) tableCandidates() []*sudoku.Table {
if c == nil {
return nil
}
if len(c.Tables) > 0 {
return c.Tables
}
if c.Table != nil {
return []*sudoku.Table{c.Table}
}
return nil
}

View File

@@ -0,0 +1,130 @@
package crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"golang.org/x/crypto/chacha20poly1305"
)
type AEADConn struct {
net.Conn
aead cipher.AEAD
readBuf bytes.Buffer
nonceSize int
}
func NewAEADConn(c net.Conn, key string, method string) (*AEADConn, error) {
if method == "none" {
return &AEADConn{Conn: c, aead: nil}, nil
}
h := sha256.New()
h.Write([]byte(key))
keyBytes := h.Sum(nil)
var aead cipher.AEAD
var err error
switch method {
case "aes-128-gcm":
block, _ := aes.NewCipher(keyBytes[:16])
aead, err = cipher.NewGCM(block)
case "chacha20-poly1305":
aead, err = chacha20poly1305.New(keyBytes)
default:
return nil, fmt.Errorf("unsupported cipher: %s", method)
}
if err != nil {
return nil, err
}
return &AEADConn{
Conn: c,
aead: aead,
nonceSize: aead.NonceSize(),
}, nil
}
func (cc *AEADConn) Write(p []byte) (int, error) {
if cc.aead == nil {
return cc.Conn.Write(p)
}
maxPayload := 65535 - cc.nonceSize - cc.aead.Overhead()
totalWritten := 0
var frameBuf bytes.Buffer
header := make([]byte, 2)
nonce := make([]byte, cc.nonceSize)
for len(p) > 0 {
chunkSize := len(p)
if chunkSize > maxPayload {
chunkSize = maxPayload
}
chunk := p[:chunkSize]
p = p[chunkSize:]
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return totalWritten, err
}
ciphertext := cc.aead.Seal(nil, nonce, chunk, nil)
frameLen := len(nonce) + len(ciphertext)
binary.BigEndian.PutUint16(header, uint16(frameLen))
frameBuf.Reset()
frameBuf.Write(header)
frameBuf.Write(nonce)
frameBuf.Write(ciphertext)
if _, err := cc.Conn.Write(frameBuf.Bytes()); err != nil {
return totalWritten, err
}
totalWritten += chunkSize
}
return totalWritten, nil
}
func (cc *AEADConn) Read(p []byte) (int, error) {
if cc.aead == nil {
return cc.Conn.Read(p)
}
if cc.readBuf.Len() > 0 {
return cc.readBuf.Read(p)
}
header := make([]byte, 2)
if _, err := io.ReadFull(cc.Conn, header); err != nil {
return 0, err
}
frameLen := int(binary.BigEndian.Uint16(header))
body := make([]byte, frameLen)
if _, err := io.ReadFull(cc.Conn, body); err != nil {
return 0, err
}
if len(body) < cc.nonceSize {
return 0, errors.New("frame too short")
}
nonce := body[:cc.nonceSize]
ciphertext := body[cc.nonceSize:]
plaintext, err := cc.aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
return 0, errors.New("decryption failed")
}
cc.readBuf.Write(plaintext)
return cc.readBuf.Read(p)
}

View File

@@ -0,0 +1,116 @@
package crypto
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"filippo.io/edwards25519"
)
// KeyPair holds the scalar private key and point public key
type KeyPair struct {
Private *edwards25519.Scalar
Public *edwards25519.Point
}
// GenerateMasterKey generates a random master private key (scalar) and its public key (point)
func GenerateMasterKey() (*KeyPair, error) {
// 1. Generate random scalar x (32 bytes)
var seed [64]byte
if _, err := rand.Read(seed[:]); err != nil {
return nil, err
}
x, err := edwards25519.NewScalar().SetUniformBytes(seed[:])
if err != nil {
return nil, err
}
// 2. Calculate Public Key P = x * G
P := new(edwards25519.Point).ScalarBaseMult(x)
return &KeyPair{Private: x, Public: P}, nil
}
// SplitPrivateKey takes a master private key x and returns a new random split key (r, k)
// such that x = r + k (mod L).
// Returns hex encoded string of r || k (64 bytes)
func SplitPrivateKey(x *edwards25519.Scalar) (string, error) {
// 1. Generate random r (32 bytes)
var seed [64]byte
if _, err := rand.Read(seed[:]); err != nil {
return "", err
}
r, err := edwards25519.NewScalar().SetUniformBytes(seed[:])
if err != nil {
return "", err
}
// 2. Calculate k = x - r (mod L)
k := new(edwards25519.Scalar).Subtract(x, r)
// 3. Encode r and k
rBytes := r.Bytes()
kBytes := k.Bytes()
full := make([]byte, 64)
copy(full[:32], rBytes)
copy(full[32:], kBytes)
return hex.EncodeToString(full), nil
}
// RecoverPublicKey takes a split private key (r, k) or a master private key (x)
// and returns the public key P.
// Input can be:
// - 32 bytes hex (Master Scalar x)
// - 64 bytes hex (Split Key r || k)
func RecoverPublicKey(keyHex string) (*edwards25519.Point, error) {
keyBytes, err := hex.DecodeString(keyHex)
if err != nil {
return nil, fmt.Errorf("invalid hex: %w", err)
}
if len(keyBytes) == 32 {
// Master Key x
x, err := edwards25519.NewScalar().SetCanonicalBytes(keyBytes)
if err != nil {
return nil, fmt.Errorf("invalid scalar: %w", err)
}
return new(edwards25519.Point).ScalarBaseMult(x), nil
} else if len(keyBytes) == 64 {
// Split Key r || k
rBytes := keyBytes[:32]
kBytes := keyBytes[32:]
r, err := edwards25519.NewScalar().SetCanonicalBytes(rBytes)
if err != nil {
return nil, fmt.Errorf("invalid scalar r: %w", err)
}
k, err := edwards25519.NewScalar().SetCanonicalBytes(kBytes)
if err != nil {
return nil, fmt.Errorf("invalid scalar k: %w", err)
}
// sum = r + k
sum := new(edwards25519.Scalar).Add(r, k)
// P = sum * G
return new(edwards25519.Point).ScalarBaseMult(sum), nil
}
return nil, errors.New("invalid key length: must be 32 bytes (Master) or 64 bytes (Split)")
}
// EncodePoint returns the hex string of the compressed point
func EncodePoint(p *edwards25519.Point) string {
return hex.EncodeToString(p.Bytes())
}
// EncodeScalar returns the hex string of the scalar
func EncodeScalar(s *edwards25519.Scalar) string {
return hex.EncodeToString(s.Bytes())
}

View File

@@ -7,7 +7,7 @@ import (
"testing"
"time"
sudokuobfs "github.com/saba-futai/sudoku/pkg/obfs/sudoku"
sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
type discardConn struct{}

View File

@@ -11,18 +11,13 @@ import (
"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"
"github.com/metacubex/mihomo/transport/sudoku/crypto"
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
"github.com/metacubex/mihomo/log"
)
type ProtocolConfig = apis.ProtocolConfig
func DefaultConfig() *ProtocolConfig { return apis.DefaultConfig() }
type SessionType int
const (
@@ -105,14 +100,14 @@ const (
downlinkModePacked byte = 0x02
)
func downlinkMode(cfg *apis.ProtocolConfig) byte {
func downlinkMode(cfg *ProtocolConfig) byte {
if cfg.EnablePureDownlink {
return downlinkModePure
}
return downlinkModePacked
}
func buildClientObfsConn(raw net.Conn, cfg *apis.ProtocolConfig, table *sudoku.Table) net.Conn {
func buildClientObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table) net.Conn {
baseReader := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false)
baseWriter := newSudokuObfsWriter(raw, table, cfg.PaddingMin, cfg.PaddingMax)
if cfg.EnablePureDownlink {
@@ -130,7 +125,7 @@ func buildClientObfsConn(raw net.Conn, cfg *apis.ProtocolConfig, table *sudoku.T
}
}
func buildServerObfsConn(raw net.Conn, cfg *apis.ProtocolConfig, table *sudoku.Table, record bool) (*sudoku.Conn, net.Conn) {
func buildServerObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table, record bool) (*sudoku.Conn, net.Conn) {
uplink := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, record)
if cfg.EnablePureDownlink {
downlink := &directionalConn{
@@ -189,12 +184,12 @@ type ClientHandshakeOptions struct {
}
// ClientHandshake performs the client-side Sudoku handshake (without sending target address).
func ClientHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (net.Conn, error) {
func ClientHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, error) {
return ClientHandshakeWithOptions(rawConn, cfg, ClientHandshakeOptions{})
}
// ClientHandshakeWithOptions performs the client-side Sudoku handshake (without sending target address).
func ClientHandshakeWithOptions(rawConn net.Conn, cfg *apis.ProtocolConfig, opt ClientHandshakeOptions) (net.Conn, error) {
func ClientHandshakeWithOptions(rawConn net.Conn, cfg *ProtocolConfig, opt ClientHandshakeOptions) (net.Conn, error) {
if cfg == nil {
return nil, fmt.Errorf("config is required")
}
@@ -220,7 +215,7 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *apis.ProtocolConfig, opt
}
handshake := buildHandshakePayload(cfg.Key)
if len(tableCandidates(cfg)) > 1 {
if len(cfg.tableCandidates()) > 1 {
handshake[15] = tableID
}
if _, err := cConn.Write(handshake[:]); err != nil {
@@ -236,7 +231,7 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *apis.ProtocolConfig, opt
}
// ServerHandshake performs Sudoku server-side handshake and detects UoT preface.
func ServerHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (*ServerSession, error) {
func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (*ServerSession, error) {
if cfg == nil {
return nil, fmt.Errorf("config is required")
}
@@ -260,7 +255,7 @@ func ServerHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (*ServerSession
}
}
selectedTable, preRead, err := selectTableByProbe(bufReader, cfg, tableCandidates(cfg))
selectedTable, preRead, err := selectTableByProbe(bufReader, cfg, cfg.tableCandidates())
if err != nil {
return nil, err
}

View File

@@ -9,8 +9,7 @@ import (
"testing"
"time"
"github.com/saba-futai/sudoku/apis"
sudokuobfs "github.com/saba-futai/sudoku/pkg/obfs/sudoku"
sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
func TestPackedConnRoundTrip_WithPadding(t *testing.T) {
@@ -67,8 +66,8 @@ func TestPackedConnRoundTrip_WithPadding(t *testing.T) {
}
}
func newPackedConfig(table *sudokuobfs.Table) *apis.ProtocolConfig {
cfg := apis.DefaultConfig()
func newPackedConfig(table *sudokuobfs.Table) *ProtocolConfig {
cfg := DefaultConfig()
cfg.Key = "sudoku-test-key"
cfg.Table = table
cfg.PaddingMin = 10
@@ -118,7 +117,7 @@ func TestPackedDownlinkSoak(t *testing.T) {
}
}
func runPackedTCPSession(id int, cfg *apis.ProtocolConfig, errCh chan<- error) {
func runPackedTCPSession(id int, cfg *ProtocolConfig, errCh chan<- error) {
serverConn, clientConn := net.Pipe()
target := fmt.Sprintf("1.1.1.%d:80", (id%200)+1)
payload := []byte{0x42, byte(id)}
@@ -176,7 +175,7 @@ func runPackedTCPSession(id int, cfg *apis.ProtocolConfig, errCh chan<- error) {
}
}
func runPackedUoTSession(id int, cfg *apis.ProtocolConfig, errCh chan<- error) {
func runPackedUoTSession(id int, cfg *ProtocolConfig, errCh chan<- error) {
serverConn, clientConn := net.Pipe()
target := "8.8.8.8:53"
payload := []byte{0xaa, byte(id)}

View File

@@ -10,7 +10,7 @@ import (
"sync"
"time"
"github.com/saba-futai/sudoku/pkg/obfs/httpmask"
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
)
var (

View File

@@ -0,0 +1,88 @@
package sudoku
import (
"context"
"fmt"
"net"
"strings"
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
)
type HTTPMaskTunnelServer struct {
cfg *ProtocolConfig
ts *httpmask.TunnelServer
}
func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer {
if cfg == nil {
return &HTTPMaskTunnelServer{}
}
var ts *httpmask.TunnelServer
if !cfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{Mode: cfg.HTTPMaskMode})
}
}
return &HTTPMaskTunnelServer{cfg: cfg, ts: ts}
}
// WrapConn inspects an accepted TCP connection and upgrades it to an HTTP tunnel stream when needed.
//
// Returns:
// - done=true: this TCP connection has been fully handled (e.g., stream/poll control request), caller should return
// - done=false: handshakeConn+cfg are ready for ServerHandshake
func (s *HTTPMaskTunnelServer) WrapConn(rawConn net.Conn) (handshakeConn net.Conn, cfg *ProtocolConfig, done bool, err error) {
if rawConn == nil {
return nil, nil, true, fmt.Errorf("nil conn")
}
if s == nil {
return rawConn, nil, false, nil
}
if s.ts == nil {
return rawConn, s.cfg, false, nil
}
res, c, err := s.ts.HandleConn(rawConn)
if err != nil {
return nil, nil, true, err
}
switch res {
case httpmask.HandleDone:
return nil, nil, true, nil
case httpmask.HandlePassThrough:
return c, s.cfg, false, nil
case httpmask.HandleStartTunnel:
inner := *s.cfg
inner.DisableHTTPMask = true
return c, &inner, false, nil
default:
return nil, nil, true, nil
}
}
type TunnelDialer func(ctx context.Context, network, addr string) (net.Conn, error)
// DialHTTPMaskTunnel dials a CDN-capable HTTP tunnel (stream/poll/auto) and returns a stream carrying raw Sudoku bytes.
func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *ProtocolConfig, dial TunnelDialer) (net.Conn, error) {
if cfg == nil {
return nil, fmt.Errorf("config is required")
}
if cfg.DisableHTTPMask {
return nil, fmt.Errorf("http mask is disabled")
}
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
default:
return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode)
}
return httpmask.DialTunnel(ctx, serverAddress, httpmask.TunnelDialOptions{
Mode: cfg.HTTPMaskMode,
TLSEnabled: cfg.HTTPMaskTLSEnabled,
HostOverride: cfg.HTTPMaskHost,
DialContext: dial,
})
}

View File

@@ -0,0 +1,445 @@
package sudoku
import (
"bytes"
"context"
"fmt"
"io"
"net"
"strings"
"sync"
"testing"
"time"
)
func startTunnelServer(t *testing.T, cfg *ProtocolConfig, handle func(*ServerSession) error) (addr string, stop func(), errCh <-chan error) {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
errC := make(chan error, 128)
done := make(chan struct{})
tunnelSrv := NewHTTPMaskTunnelServer(cfg)
var wg sync.WaitGroup
var stopOnce sync.Once
wg.Add(1)
go func() {
defer wg.Done()
for {
c, err := ln.Accept()
if err != nil {
close(done)
return
}
wg.Add(1)
go func(conn net.Conn) {
defer wg.Done()
handshakeConn, handshakeCfg, handled, err := tunnelSrv.WrapConn(conn)
if err != nil {
_ = conn.Close()
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
return
}
if err == io.EOF {
return
}
errC <- err
return
}
if handled {
return
}
if handshakeConn == nil || handshakeCfg == nil {
_ = conn.Close()
errC <- fmt.Errorf("wrap conn returned nil")
return
}
session, err := ServerHandshake(handshakeConn, handshakeCfg)
if err != nil {
_ = handshakeConn.Close()
if handshakeConn != conn {
_ = conn.Close()
}
errC <- err
return
}
defer session.Conn.Close()
if handleErr := handle(session); handleErr != nil {
errC <- handleErr
}
}(c)
}
}()
stop = func() {
stopOnce.Do(func() {
_ = ln.Close()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatalf("server did not stop")
}
ch := make(chan struct{})
go func() {
wg.Wait()
close(ch)
}()
select {
case <-ch:
case <-time.After(10 * time.Second):
t.Fatalf("server goroutines did not exit")
}
close(errC)
})
}
return ln.Addr().String(), stop, errC
}
func newTunnelTestTable(t *testing.T, key string) *ProtocolConfig {
t.Helper()
tables, err := NewTablesWithCustomPatterns(ClientAEADSeed(key), "prefer_ascii", "", nil)
if err != nil {
t.Fatalf("build tables: %v", err)
}
if len(tables) != 1 {
t.Fatalf("unexpected tables: %d", len(tables))
}
cfg := DefaultConfig()
cfg.Key = key
cfg.AEADMethod = "chacha20-poly1305"
cfg.Table = tables[0]
cfg.PaddingMin = 0
cfg.PaddingMax = 0
cfg.HandshakeTimeoutSeconds = 5
cfg.EnablePureDownlink = true
cfg.DisableHTTPMask = false
return cfg
}
func TestHTTPMaskTunnel_Stream_TCPRoundTrip(t *testing.T) {
key := "tunnel-stream-key"
target := "1.1.1.1:80"
serverCfg := newTunnelTestTable(t, key)
serverCfg.HTTPMaskMode = "stream"
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
if s.Type != SessionTypeTCP {
return fmt.Errorf("unexpected session type: %v", s.Type)
}
if s.Target != target {
return fmt.Errorf("target mismatch: %s", s.Target)
}
_, _ = s.Conn.Write([]byte("ok"))
return nil
})
defer stop()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
clientCfg := *serverCfg
clientCfg.ServerAddress = addr
clientCfg.HTTPMaskHost = "example.com"
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
defer tunnelConn.Close()
handshakeCfg := clientCfg
handshakeCfg.DisableHTTPMask = true
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
if err != nil {
t.Fatalf("client handshake: %v", err)
}
defer cConn.Close()
addrBuf, err := EncodeAddress(target)
if err != nil {
t.Fatalf("encode addr: %v", err)
}
if _, err := cConn.Write(addrBuf); err != nil {
t.Fatalf("write addr: %v", err)
}
buf := make([]byte, 2)
if _, err := io.ReadFull(cConn, buf); err != nil {
t.Fatalf("read: %v", err)
}
if string(buf) != "ok" {
t.Fatalf("unexpected payload: %q", buf)
}
stop()
for err := range errCh {
t.Fatalf("server error: %v", err)
}
}
func TestHTTPMaskTunnel_Poll_UoTRoundTrip(t *testing.T) {
key := "tunnel-poll-key"
target := "8.8.8.8:53"
payload := []byte{0xaa, 0xbb, 0xcc, 0xdd}
serverCfg := newTunnelTestTable(t, key)
serverCfg.HTTPMaskMode = "poll"
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
if s.Type != SessionTypeUoT {
return fmt.Errorf("unexpected session type: %v", s.Type)
}
gotAddr, gotPayload, err := ReadDatagram(s.Conn)
if err != nil {
return fmt.Errorf("server read datagram: %w", err)
}
if gotAddr != target {
return fmt.Errorf("uot target mismatch: %s", gotAddr)
}
if !bytes.Equal(gotPayload, payload) {
return fmt.Errorf("uot payload mismatch: %x", gotPayload)
}
if err := WriteDatagram(s.Conn, gotAddr, gotPayload); err != nil {
return fmt.Errorf("server write datagram: %w", err)
}
return nil
})
defer stop()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
clientCfg := *serverCfg
clientCfg.ServerAddress = addr
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
defer tunnelConn.Close()
handshakeCfg := clientCfg
handshakeCfg.DisableHTTPMask = true
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
if err != nil {
t.Fatalf("client handshake: %v", err)
}
defer cConn.Close()
if err := WritePreface(cConn); err != nil {
t.Fatalf("write preface: %v", err)
}
if err := WriteDatagram(cConn, target, payload); err != nil {
t.Fatalf("write datagram: %v", err)
}
gotAddr, gotPayload, err := ReadDatagram(cConn)
if err != nil {
t.Fatalf("read datagram: %v", err)
}
if gotAddr != target {
t.Fatalf("uot target mismatch: %s", gotAddr)
}
if !bytes.Equal(gotPayload, payload) {
t.Fatalf("uot payload mismatch: %x", gotPayload)
}
stop()
for err := range errCh {
t.Fatalf("server error: %v", err)
}
}
func TestHTTPMaskTunnel_Auto_TCPRoundTrip(t *testing.T) {
key := "tunnel-auto-key"
target := "9.9.9.9:443"
serverCfg := newTunnelTestTable(t, key)
serverCfg.HTTPMaskMode = "auto"
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
if s.Type != SessionTypeTCP {
return fmt.Errorf("unexpected session type: %v", s.Type)
}
if s.Target != target {
return fmt.Errorf("target mismatch: %s", s.Target)
}
_, _ = s.Conn.Write([]byte("ok"))
return nil
})
defer stop()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
clientCfg := *serverCfg
clientCfg.ServerAddress = addr
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
defer tunnelConn.Close()
handshakeCfg := clientCfg
handshakeCfg.DisableHTTPMask = true
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
if err != nil {
t.Fatalf("client handshake: %v", err)
}
defer cConn.Close()
addrBuf, err := EncodeAddress(target)
if err != nil {
t.Fatalf("encode addr: %v", err)
}
if _, err := cConn.Write(addrBuf); err != nil {
t.Fatalf("write addr: %v", err)
}
buf := make([]byte, 2)
if _, err := io.ReadFull(cConn, buf); err != nil {
t.Fatalf("read: %v", err)
}
if string(buf) != "ok" {
t.Fatalf("unexpected payload: %q", buf)
}
stop()
for err := range errCh {
t.Fatalf("server error: %v", err)
}
}
func TestHTTPMaskTunnel_Validation(t *testing.T) {
cfg := DefaultConfig()
cfg.Key = "k"
cfg.Table = NewTable("seed", "prefer_ascii")
cfg.ServerAddress = "127.0.0.1:1"
cfg.DisableHTTPMask = true
cfg.HTTPMaskMode = "stream"
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext); err == nil {
t.Fatalf("expected error for disabled http mask")
}
cfg.DisableHTTPMask = false
cfg.HTTPMaskMode = "legacy"
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext); err == nil {
t.Fatalf("expected error for legacy mode")
}
}
func TestHTTPMaskTunnel_Soak_Concurrent(t *testing.T) {
key := "tunnel-soak-key"
target := "1.0.0.1:80"
serverCfg := newTunnelTestTable(t, key)
serverCfg.HTTPMaskMode = "stream"
serverCfg.EnablePureDownlink = false
const (
sessions = 8
payloadLen = 64 * 1024
)
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
if s.Type != SessionTypeTCP {
return fmt.Errorf("unexpected session type: %v", s.Type)
}
if s.Target != target {
return fmt.Errorf("target mismatch: %s", s.Target)
}
buf := make([]byte, payloadLen)
if _, err := io.ReadFull(s.Conn, buf); err != nil {
return fmt.Errorf("server read payload: %w", err)
}
_, err := s.Conn.Write(buf)
return err
})
defer stop()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var wg sync.WaitGroup
runErr := make(chan error, sessions)
for i := 0; i < sessions; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
clientCfg := *serverCfg
clientCfg.ServerAddress = addr
clientCfg.HTTPMaskHost = strings.TrimSpace(clientCfg.HTTPMaskHost)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
if err != nil {
runErr <- fmt.Errorf("dial: %w", err)
return
}
defer tunnelConn.Close()
handshakeCfg := clientCfg
handshakeCfg.DisableHTTPMask = true
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
if err != nil {
runErr <- fmt.Errorf("handshake: %w", err)
return
}
defer cConn.Close()
addrBuf, err := EncodeAddress(target)
if err != nil {
runErr <- fmt.Errorf("encode addr: %w", err)
return
}
if _, err := cConn.Write(addrBuf); err != nil {
runErr <- fmt.Errorf("write addr: %w", err)
return
}
payload := bytes.Repeat([]byte{byte(id)}, payloadLen)
if _, err := cConn.Write(payload); err != nil {
runErr <- fmt.Errorf("write payload: %w", err)
return
}
echo := make([]byte, payloadLen)
if _, err := io.ReadFull(cConn, echo); err != nil {
runErr <- fmt.Errorf("read echo: %w", err)
return
}
if !bytes.Equal(echo, payload) {
runErr <- fmt.Errorf("echo mismatch")
return
}
runErr <- nil
}(i)
}
wg.Wait()
close(runErr)
for err := range runErr {
if err != nil {
t.Fatalf("soak: %v", err)
}
}
stop()
for err := range errCh {
t.Fatalf("server error: %v", err)
}
}

View File

@@ -0,0 +1,246 @@
package httpmask
import (
"bufio"
"bytes"
"encoding/base64"
"fmt"
"io"
"math/rand"
"net"
"strconv"
"strings"
"sync"
"time"
)
var (
userAgents = []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36",
}
accepts = []string{
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"application/json, text/plain, */*",
"application/octet-stream",
"*/*",
}
acceptLanguages = []string{
"en-US,en;q=0.9",
"en-GB,en;q=0.9",
"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7",
"de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
}
acceptEncodings = []string{
"gzip, deflate, br",
"gzip, deflate",
"br, gzip, deflate",
}
paths = []string{
"/api/v1/upload",
"/data/sync",
"/uploads/raw",
"/api/report",
"/feed/update",
"/v2/events",
"/v1/telemetry",
"/session",
"/stream",
"/ws",
}
contentTypes = []string{
"application/octet-stream",
"application/x-protobuf",
"application/json",
}
)
var (
rngPool = sync.Pool{
New: func() interface{} {
return rand.New(rand.NewSource(time.Now().UnixNano()))
},
}
headerBufPool = sync.Pool{
New: func() interface{} {
b := make([]byte, 0, 1024)
return &b
},
}
)
// LooksLikeHTTPRequestStart reports whether peek4 looks like a supported HTTP/1.x request method prefix.
func LooksLikeHTTPRequestStart(peek4 []byte) bool {
if len(peek4) < 4 {
return false
}
// Common methods: "GET ", "POST", "HEAD", "PUT ", "OPTI" (OPTIONS), "PATC" (PATCH), "DELE" (DELETE)
return bytes.Equal(peek4, []byte("GET ")) ||
bytes.Equal(peek4, []byte("POST")) ||
bytes.Equal(peek4, []byte("HEAD")) ||
bytes.Equal(peek4, []byte("PUT ")) ||
bytes.Equal(peek4, []byte("OPTI")) ||
bytes.Equal(peek4, []byte("PATC")) ||
bytes.Equal(peek4, []byte("DELE"))
}
func trimPortForHost(host string) string {
if host == "" {
return host
}
// Accept "example.com:443" / "1.2.3.4:443" / "[::1]:443"
h, _, err := net.SplitHostPort(host)
if err == nil && h != "" {
return h
}
// If it's not in host:port form, keep as-is.
return host
}
func appendCommonHeaders(buf []byte, host string, r *rand.Rand) []byte {
ua := userAgents[r.Intn(len(userAgents))]
accept := accepts[r.Intn(len(accepts))]
lang := acceptLanguages[r.Intn(len(acceptLanguages))]
enc := acceptEncodings[r.Intn(len(acceptEncodings))]
buf = append(buf, "Host: "...)
buf = append(buf, host...)
buf = append(buf, "\r\nUser-Agent: "...)
buf = append(buf, ua...)
buf = append(buf, "\r\nAccept: "...)
buf = append(buf, accept...)
buf = append(buf, "\r\nAccept-Language: "...)
buf = append(buf, lang...)
buf = append(buf, "\r\nAccept-Encoding: "...)
buf = append(buf, enc...)
buf = append(buf, "\r\nConnection: keep-alive\r\n"...)
// A couple of common cache headers; keep them static for simplicity.
buf = append(buf, "Cache-Control: no-cache\r\nPragma: no-cache\r\n"...)
return buf
}
// WriteRandomRequestHeader writes a plausible HTTP/1.1 request header as a mask.
func WriteRandomRequestHeader(w io.Writer, host string) error {
// Get RNG from pool
r := rngPool.Get().(*rand.Rand)
defer rngPool.Put(r)
path := paths[r.Intn(len(paths))]
ctype := contentTypes[r.Intn(len(contentTypes))]
// Use buffer pool
bufPtr := headerBufPool.Get().(*[]byte)
buf := *bufPtr
buf = buf[:0]
defer func() {
if cap(buf) <= 4096 {
*bufPtr = buf
headerBufPool.Put(bufPtr)
}
}()
// Weighted template selection. Keep a conservative default (POST w/ Content-Length),
// but occasionally rotate to other realistic templates (e.g. WebSocket upgrade).
switch r.Intn(10) {
case 0, 1: // ~20% WebSocket-like upgrade
hostNoPort := trimPortForHost(host)
var keyBytes [16]byte
for i := 0; i < len(keyBytes); i++ {
keyBytes[i] = byte(r.Intn(256))
}
wsKey := base64.StdEncoding.EncodeToString(keyBytes[:])
buf = append(buf, "GET "...)
buf = append(buf, path...)
buf = append(buf, " HTTP/1.1\r\n"...)
buf = appendCommonHeaders(buf, host, r)
buf = append(buf, "Upgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: "...)
buf = append(buf, wsKey...)
buf = append(buf, "\r\nOrigin: https://"...)
buf = append(buf, hostNoPort...)
buf = append(buf, "\r\n\r\n"...)
default: // ~80% POST upload
// Random Content-Length: 4KB10MB. Small enough to look plausible, large enough
// to justify long-lived writes on keep-alive connections.
const minCL = int64(4 * 1024)
const maxCL = int64(10 * 1024 * 1024)
contentLength := minCL + r.Int63n(maxCL-minCL+1)
buf = append(buf, "POST "...)
buf = append(buf, path...)
buf = append(buf, " HTTP/1.1\r\n"...)
buf = appendCommonHeaders(buf, host, r)
buf = append(buf, "Content-Type: "...)
buf = append(buf, ctype...)
buf = append(buf, "\r\nContent-Length: "...)
buf = strconv.AppendInt(buf, contentLength, 10)
// A couple of extra headers seen in real clients.
if r.Intn(2) == 0 {
buf = append(buf, "\r\nX-Requested-With: XMLHttpRequest"...)
}
if r.Intn(3) == 0 {
buf = append(buf, "\r\nReferer: https://"...)
buf = append(buf, trimPortForHost(host)...)
buf = append(buf, "/"...)
}
buf = append(buf, "\r\n\r\n"...)
}
_, err := w.Write(buf)
return err
}
// ConsumeHeader 读取并消耗 HTTP 头部,返回消耗的数据和剩余的 reader 数据
// 如果不是 POST 请求或格式严重错误,返回 error
func ConsumeHeader(r *bufio.Reader) ([]byte, error) {
var consumed bytes.Buffer
// 1. 读取请求行
// Use ReadSlice to avoid allocation if line fits in buffer
line, err := r.ReadSlice('\n')
if err != nil {
return nil, err
}
consumed.Write(line)
// Basic method validation: accept common HTTP/1.x methods used by our masker.
// Keep it strict enough to reject obvious garbage.
switch {
case bytes.HasPrefix(line, []byte("POST ")),
bytes.HasPrefix(line, []byte("GET ")),
bytes.HasPrefix(line, []byte("HEAD ")),
bytes.HasPrefix(line, []byte("PUT ")),
bytes.HasPrefix(line, []byte("DELETE ")),
bytes.HasPrefix(line, []byte("OPTIONS ")),
bytes.HasPrefix(line, []byte("PATCH ")):
default:
return consumed.Bytes(), fmt.Errorf("invalid method or garbage: %s", strings.TrimSpace(string(line)))
}
// 2. 循环读取头部,直到遇到空行
for {
line, err = r.ReadSlice('\n')
if err != nil {
return consumed.Bytes(), err
}
consumed.Write(line)
// Check for empty line (\r\n or \n)
// ReadSlice includes the delimiter
n := len(line)
if n == 2 && line[0] == '\r' && line[1] == '\n' {
return consumed.Bytes(), nil
}
if n == 1 && line[0] == '\n' {
return consumed.Bytes(), nil
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,212 @@
package sudoku
import (
"bufio"
"bytes"
crypto_rand "crypto/rand"
"encoding/binary"
"errors"
"math/rand"
"net"
"sync"
)
const IOBufferSize = 32 * 1024
var perm4 = [24][4]byte{
{0, 1, 2, 3},
{0, 1, 3, 2},
{0, 2, 1, 3},
{0, 2, 3, 1},
{0, 3, 1, 2},
{0, 3, 2, 1},
{1, 0, 2, 3},
{1, 0, 3, 2},
{1, 2, 0, 3},
{1, 2, 3, 0},
{1, 3, 0, 2},
{1, 3, 2, 0},
{2, 0, 1, 3},
{2, 0, 3, 1},
{2, 1, 0, 3},
{2, 1, 3, 0},
{2, 3, 0, 1},
{2, 3, 1, 0},
{3, 0, 1, 2},
{3, 0, 2, 1},
{3, 1, 0, 2},
{3, 1, 2, 0},
{3, 2, 0, 1},
{3, 2, 1, 0},
}
type Conn struct {
net.Conn
table *Table
reader *bufio.Reader
recorder *bytes.Buffer
recording bool
recordLock sync.Mutex
rawBuf []byte
pendingData []byte
hintBuf []byte
rng *rand.Rand
paddingRate float32
}
func NewConn(c net.Conn, table *Table, pMin, pMax int, record bool) *Conn {
var seedBytes [8]byte
if _, err := crypto_rand.Read(seedBytes[:]); err != nil {
binary.BigEndian.PutUint64(seedBytes[:], uint64(rand.Int63()))
}
seed := int64(binary.BigEndian.Uint64(seedBytes[:]))
localRng := rand.New(rand.NewSource(seed))
min := float32(pMin) / 100.0
rng := float32(pMax-pMin) / 100.0
rate := min + localRng.Float32()*rng
sc := &Conn{
Conn: c,
table: table,
reader: bufio.NewReaderSize(c, IOBufferSize),
rawBuf: make([]byte, IOBufferSize),
pendingData: make([]byte, 0, 4096),
hintBuf: make([]byte, 0, 4),
rng: localRng,
paddingRate: rate,
}
if record {
sc.recorder = new(bytes.Buffer)
sc.recording = true
}
return sc
}
func (sc *Conn) StopRecording() {
sc.recordLock.Lock()
sc.recording = false
sc.recorder = nil
sc.recordLock.Unlock()
}
func (sc *Conn) GetBufferedAndRecorded() []byte {
if sc == nil {
return nil
}
sc.recordLock.Lock()
defer sc.recordLock.Unlock()
var recorded []byte
if sc.recorder != nil {
recorded = sc.recorder.Bytes()
}
buffered := sc.reader.Buffered()
if buffered > 0 {
peeked, _ := sc.reader.Peek(buffered)
full := make([]byte, len(recorded)+len(peeked))
copy(full, recorded)
copy(full[len(recorded):], peeked)
return full
}
return recorded
}
func (sc *Conn) Write(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
outCapacity := len(p) * 6
out := make([]byte, 0, outCapacity)
pads := sc.table.PaddingPool
padLen := len(pads)
for _, b := range p {
if sc.rng.Float32() < sc.paddingRate {
out = append(out, pads[sc.rng.Intn(padLen)])
}
puzzles := sc.table.EncodeTable[b]
puzzle := puzzles[sc.rng.Intn(len(puzzles))]
perm := perm4[sc.rng.Intn(len(perm4))]
for _, idx := range perm {
if sc.rng.Float32() < sc.paddingRate {
out = append(out, pads[sc.rng.Intn(padLen)])
}
out = append(out, puzzle[idx])
}
}
if sc.rng.Float32() < sc.paddingRate {
out = append(out, pads[sc.rng.Intn(padLen)])
}
_, err = sc.Conn.Write(out)
return len(p), err
}
func (sc *Conn) Read(p []byte) (n int, err error) {
if len(sc.pendingData) > 0 {
n = copy(p, sc.pendingData)
if n == len(sc.pendingData) {
sc.pendingData = sc.pendingData[:0]
} else {
sc.pendingData = sc.pendingData[n:]
}
return n, nil
}
for {
if len(sc.pendingData) > 0 {
break
}
nr, rErr := sc.reader.Read(sc.rawBuf)
if nr > 0 {
chunk := sc.rawBuf[:nr]
sc.recordLock.Lock()
if sc.recording {
sc.recorder.Write(chunk)
}
sc.recordLock.Unlock()
for _, b := range chunk {
if !sc.table.layout.isHint(b) {
continue
}
sc.hintBuf = append(sc.hintBuf, b)
if len(sc.hintBuf) == 4 {
key := packHintsToKey([4]byte{sc.hintBuf[0], sc.hintBuf[1], sc.hintBuf[2], sc.hintBuf[3]})
val, ok := sc.table.DecodeMap[key]
if !ok {
return 0, errors.New("INVALID_SUDOKU_MAP_MISS")
}
sc.pendingData = append(sc.pendingData, val)
sc.hintBuf = sc.hintBuf[:0]
}
}
}
if rErr != nil {
return 0, rErr
}
if len(sc.pendingData) > 0 {
break
}
}
n = copy(p, sc.pendingData)
if n == len(sc.pendingData) {
sc.pendingData = sc.pendingData[:0]
} else {
sc.pendingData = sc.pendingData[n:]
}
return n, nil
}

View File

@@ -0,0 +1,46 @@
package sudoku
// Grid represents a 4x4 sudoku grid
type Grid [16]uint8
// GenerateAllGrids generates all valid 4x4 Sudoku grids
func GenerateAllGrids() []Grid {
var grids []Grid
var g Grid
var backtrack func(int)
backtrack = func(idx int) {
if idx == 16 {
grids = append(grids, g)
return
}
row, col := idx/4, idx%4
br, bc := (row/2)*2, (col/2)*2
for num := uint8(1); num <= 4; num++ {
valid := true
for i := 0; i < 4; i++ {
if g[row*4+i] == num || g[i*4+col] == num {
valid = false
break
}
}
if valid {
for r := 0; r < 2; r++ {
for c := 0; c < 2; c++ {
if g[(br+r)*4+(bc+c)] == num {
valid = false
break
}
}
}
}
if valid {
g[idx] = num
backtrack(idx + 1)
g[idx] = 0
}
}
}
backtrack(0)
return grids
}

View File

@@ -0,0 +1,204 @@
package sudoku
import (
"fmt"
"math/bits"
"sort"
"strings"
)
type byteLayout struct {
name string
hintMask byte
hintValue byte
padMarker byte
paddingPool []byte
encodeHint func(val, pos byte) byte
encodeGroup func(group byte) byte
decodeGroup func(b byte) (byte, bool)
}
func (l *byteLayout) isHint(b byte) bool {
return (b & l.hintMask) == l.hintValue
}
// resolveLayout picks the byte layout based on ASCII preference and optional custom pattern.
// ASCII always wins if requested. Custom patterns are ignored when ASCII is preferred.
func resolveLayout(mode string, customPattern string) (*byteLayout, error) {
switch strings.ToLower(mode) {
case "ascii", "prefer_ascii":
return newASCIILayout(), nil
case "entropy", "prefer_entropy", "":
// fallback to entropy unless a custom pattern is provided
default:
return nil, fmt.Errorf("invalid ascii mode: %s", mode)
}
if strings.TrimSpace(customPattern) != "" {
return newCustomLayout(customPattern)
}
return newEntropyLayout(), nil
}
func newASCIILayout() *byteLayout {
padding := make([]byte, 0, 32)
for i := 0; i < 32; i++ {
padding = append(padding, byte(0x20+i))
}
return &byteLayout{
name: "ascii",
hintMask: 0x40,
hintValue: 0x40,
padMarker: 0x3F,
paddingPool: padding,
encodeHint: func(val, pos byte) byte {
return 0x40 | ((val & 0x03) << 4) | (pos & 0x0F)
},
encodeGroup: func(group byte) byte {
return 0x40 | (group & 0x3F)
},
decodeGroup: func(b byte) (byte, bool) {
if (b & 0x40) == 0 {
return 0, false
}
return b & 0x3F, true
},
}
}
func newEntropyLayout() *byteLayout {
padding := make([]byte, 0, 16)
for i := 0; i < 8; i++ {
padding = append(padding, byte(0x80+i))
padding = append(padding, byte(0x10+i))
}
return &byteLayout{
name: "entropy",
hintMask: 0x90,
hintValue: 0x00,
padMarker: 0x80,
paddingPool: padding,
encodeHint: func(val, pos byte) byte {
return ((val & 0x03) << 5) | (pos & 0x0F)
},
encodeGroup: func(group byte) byte {
v := group & 0x3F
return ((v & 0x30) << 1) | (v & 0x0F)
},
decodeGroup: func(b byte) (byte, bool) {
if (b & 0x90) != 0 {
return 0, false
}
return ((b >> 1) & 0x30) | (b & 0x0F), true
},
}
}
func newCustomLayout(pattern string) (*byteLayout, error) {
cleaned := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(pattern), " ", ""))
if len(cleaned) != 8 {
return nil, fmt.Errorf("custom table must have 8 symbols, got %d", len(cleaned))
}
var xBits, pBits, vBits []uint8
for i, c := range cleaned {
bit := uint8(7 - i)
switch c {
case 'x':
xBits = append(xBits, bit)
case 'p':
pBits = append(pBits, bit)
case 'v':
vBits = append(vBits, bit)
default:
return nil, fmt.Errorf("invalid char %q in custom table", c)
}
}
if len(xBits) != 2 || len(pBits) != 2 || len(vBits) != 4 {
return nil, fmt.Errorf("custom table must contain exactly 2 x, 2 p, 4 v")
}
xMask := byte(0)
for _, b := range xBits {
xMask |= 1 << b
}
encodeBits := func(val, pos byte, dropX int) byte {
var out byte
out |= xMask
if dropX >= 0 {
out &^= 1 << xBits[dropX]
}
if (val & 0x02) != 0 {
out |= 1 << pBits[0]
}
if (val & 0x01) != 0 {
out |= 1 << pBits[1]
}
for i, bit := range vBits {
if (pos>>(3-uint8(i)))&0x01 == 1 {
out |= 1 << bit
}
}
return out
}
decodeGroup := func(b byte) (byte, bool) {
if (b & xMask) != xMask {
return 0, false
}
var val, pos byte
if b&(1<<pBits[0]) != 0 {
val |= 0x02
}
if b&(1<<pBits[1]) != 0 {
val |= 0x01
}
for i, bit := range vBits {
if b&(1<<bit) != 0 {
pos |= 1 << (3 - uint8(i))
}
}
group := (val << 4) | (pos & 0x0F)
return group, true
}
paddingSet := make(map[byte]struct{})
var padding []byte
for drop := range xBits {
for val := 0; val < 4; val++ {
for pos := 0; pos < 16; pos++ {
b := encodeBits(byte(val), byte(pos), drop)
if bits.OnesCount8(b) >= 5 {
if _, ok := paddingSet[b]; !ok {
paddingSet[b] = struct{}{}
padding = append(padding, b)
}
}
}
}
}
sort.Slice(padding, func(i, j int) bool { return padding[i] < padding[j] })
if len(padding) == 0 {
return nil, fmt.Errorf("custom table produced empty padding pool")
}
return &byteLayout{
name: fmt.Sprintf("custom(%s)", cleaned),
hintMask: xMask,
hintValue: xMask,
padMarker: padding[0],
paddingPool: padding,
encodeHint: func(val, pos byte) byte {
return encodeBits(val, pos, -1)
},
encodeGroup: func(group byte) byte {
val := (group >> 4) & 0x03
pos := group & 0x0F
return encodeBits(val, pos, -1)
},
decodeGroup: decodeGroup,
}, nil
}

View File

@@ -0,0 +1,332 @@
package sudoku
import (
"bufio"
crypto_rand "crypto/rand"
"encoding/binary"
"io"
"math/rand"
"net"
"sync"
)
const (
// 每次从 RNG 获取批量随机数的缓存大小,减少 RNG 函数调用开销
RngBatchSize = 128
)
// 1. 使用 12字节->16组 的块处理优化 Write (减少循环开销)
// 2. 使用浮点随机概率判断 Padding与纯 Sudoku 保持流量特征一致
// 3. Read 使用 copy 移动避免底层数组泄漏
type PackedConn struct {
net.Conn
table *Table
reader *bufio.Reader
// 读缓冲
rawBuf []byte
pendingData []byte // 解码后尚未被 Read 取走的字节
// 写缓冲与状态
writeMu sync.Mutex
writeBuf []byte
bitBuf uint64 // 暂存的位数据
bitCount int // 暂存的位数
// 读状态
readBitBuf uint64
readBits int
// 随机数与填充控制 - 使用浮点随机,与 Conn 一致
rng *rand.Rand
paddingRate float32 // 与 Conn 保持一致的随机概率模型
padMarker byte
padPool []byte
}
func NewPackedConn(c net.Conn, table *Table, pMin, pMax int) *PackedConn {
var seedBytes [8]byte
if _, err := crypto_rand.Read(seedBytes[:]); err != nil {
binary.BigEndian.PutUint64(seedBytes[:], uint64(rand.Int63()))
}
seed := int64(binary.BigEndian.Uint64(seedBytes[:]))
localRng := rand.New(rand.NewSource(seed))
// 与 Conn 保持一致的 padding 概率计算
min := float32(pMin) / 100.0
rng := float32(pMax-pMin) / 100.0
rate := min + localRng.Float32()*rng
pc := &PackedConn{
Conn: c,
table: table,
reader: bufio.NewReaderSize(c, IOBufferSize),
rawBuf: make([]byte, IOBufferSize),
pendingData: make([]byte, 0, 4096),
writeBuf: make([]byte, 0, 4096),
rng: localRng,
paddingRate: rate,
}
pc.padMarker = table.layout.padMarker
for _, b := range table.PaddingPool {
if b != pc.padMarker {
pc.padPool = append(pc.padPool, b)
}
}
if len(pc.padPool) == 0 {
pc.padPool = append(pc.padPool, pc.padMarker)
}
return pc
}
// maybeAddPadding 内联辅助:根据浮点概率插入 padding
func (pc *PackedConn) maybeAddPadding(out []byte) []byte {
if pc.rng.Float32() < pc.paddingRate {
out = append(out, pc.getPaddingByte())
}
return out
}
// Write 极致优化版 - 批量处理 12 字节
func (pc *PackedConn) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
pc.writeMu.Lock()
defer pc.writeMu.Unlock()
// 1. 预分配内存,避免 append 导致的多次扩容
// 预估:原数据 * 1.5 (4/3 + padding 余量)
needed := len(p)*3/2 + 32
if cap(pc.writeBuf) < needed {
pc.writeBuf = make([]byte, 0, needed)
}
out := pc.writeBuf[:0]
i := 0
n := len(p)
// 2. 头部对齐处理 (Slow Path)
for pc.bitCount > 0 && i < n {
out = pc.maybeAddPadding(out)
b := p[i]
i++
pc.bitBuf = (pc.bitBuf << 8) | uint64(b)
pc.bitCount += 8
for pc.bitCount >= 6 {
pc.bitCount -= 6
group := byte(pc.bitBuf >> pc.bitCount)
if pc.bitCount == 0 {
pc.bitBuf = 0
} else {
pc.bitBuf &= (1 << pc.bitCount) - 1
}
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(group&0x3F))
}
}
// 3. 极速批量处理 (Fast Path) - 每次处理 12 字节 → 生成 16 个编码组
for i+11 < n {
// 处理 4 组,每组 3 字节
for batch := 0; batch < 4; batch++ {
b1, b2, b3 := p[i], p[i+1], p[i+2]
i += 3
g1 := (b1 >> 2) & 0x3F
g2 := ((b1 & 0x03) << 4) | ((b2 >> 4) & 0x0F)
g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03)
g4 := b3 & 0x3F
// 每个组之前都有概率插入 padding
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g1))
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g2))
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g3))
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g4))
}
}
// 4. 处理剩余的 3 字节块
for i+2 < n {
b1, b2, b3 := p[i], p[i+1], p[i+2]
i += 3
g1 := (b1 >> 2) & 0x3F
g2 := ((b1 & 0x03) << 4) | ((b2 >> 4) & 0x0F)
g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03)
g4 := b3 & 0x3F
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g1))
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g2))
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g3))
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(g4))
}
// 5. 尾部处理 (Tail Path) - 处理剩余的 1 或 2 个字节
for ; i < n; i++ {
b := p[i]
pc.bitBuf = (pc.bitBuf << 8) | uint64(b)
pc.bitCount += 8
for pc.bitCount >= 6 {
pc.bitCount -= 6
group := byte(pc.bitBuf >> pc.bitCount)
if pc.bitCount == 0 {
pc.bitBuf = 0
} else {
pc.bitBuf &= (1 << pc.bitCount) - 1
}
out = pc.maybeAddPadding(out)
out = append(out, pc.encodeGroup(group&0x3F))
}
}
// 6. 处理残留位
if pc.bitCount > 0 {
out = pc.maybeAddPadding(out)
group := byte(pc.bitBuf << (6 - pc.bitCount))
pc.bitBuf = 0
pc.bitCount = 0
out = append(out, pc.encodeGroup(group&0x3F))
out = append(out, pc.padMarker)
}
// 尾部可能添加 padding
out = pc.maybeAddPadding(out)
// 发送数据
if len(out) > 0 {
_, err := pc.Conn.Write(out)
pc.writeBuf = out[:0]
return len(p), err
}
pc.writeBuf = out[:0]
return len(p), nil
}
// Flush 处理最后不足 6 bit 的情况
func (pc *PackedConn) Flush() error {
pc.writeMu.Lock()
defer pc.writeMu.Unlock()
out := pc.writeBuf[:0]
if pc.bitCount > 0 {
group := byte(pc.bitBuf << (6 - pc.bitCount))
pc.bitBuf = 0
pc.bitCount = 0
out = append(out, pc.encodeGroup(group&0x3F))
out = append(out, pc.padMarker)
}
// 尾部随机添加 padding
out = pc.maybeAddPadding(out)
if len(out) > 0 {
_, err := pc.Conn.Write(out)
pc.writeBuf = out[:0]
return err
}
return nil
}
// Read 优化版:减少切片操作,避免内存泄漏
func (pc *PackedConn) Read(p []byte) (int, error) {
// 1. 优先返回待处理区的数据
if len(pc.pendingData) > 0 {
n := copy(p, pc.pendingData)
if n == len(pc.pendingData) {
pc.pendingData = pc.pendingData[:0]
} else {
// 优化:移动剩余数据到数组头部,避免切片指向中间导致内存泄漏
remaining := len(pc.pendingData) - n
copy(pc.pendingData, pc.pendingData[n:])
pc.pendingData = pc.pendingData[:remaining]
}
return n, nil
}
// 2. 循环读取直到解出数据或出错
for {
nr, rErr := pc.reader.Read(pc.rawBuf)
if nr > 0 {
// 缓存频繁访问的变量
rBuf := pc.readBitBuf
rBits := pc.readBits
padMarker := pc.padMarker
layout := pc.table.layout
for _, b := range pc.rawBuf[:nr] {
if !layout.isHint(b) {
if b == padMarker {
rBuf = 0
rBits = 0
}
continue
}
group, ok := layout.decodeGroup(b)
if !ok {
return 0, ErrInvalidSudokuMapMiss
}
rBuf = (rBuf << 6) | uint64(group)
rBits += 6
if rBits >= 8 {
rBits -= 8
val := byte(rBuf >> rBits)
pc.pendingData = append(pc.pendingData, val)
}
}
pc.readBitBuf = rBuf
pc.readBits = rBits
}
if rErr != nil {
if rErr == io.EOF {
pc.readBitBuf = 0
pc.readBits = 0
}
if len(pc.pendingData) > 0 {
break
}
return 0, rErr
}
if len(pc.pendingData) > 0 {
break
}
}
// 3. 返回解码后的数据 - 优化:避免底层数组泄漏
n := copy(p, pc.pendingData)
if n == len(pc.pendingData) {
pc.pendingData = pc.pendingData[:0]
} else {
remaining := len(pc.pendingData) - n
copy(pc.pendingData, pc.pendingData[n:])
pc.pendingData = pc.pendingData[:remaining]
}
return n, nil
}
// getPaddingByte 从 Pool 中随机取 Padding 字节
func (pc *PackedConn) getPaddingByte() byte {
return pc.padPool[pc.rng.Intn(len(pc.padPool))]
}
// encodeGroup 编码 6-bit 组
func (pc *PackedConn) encodeGroup(group byte) byte {
return pc.table.layout.encodeGroup(group)
}

View File

@@ -0,0 +1,153 @@
package sudoku
import (
"crypto/sha256"
"encoding/binary"
"errors"
"log"
"math/rand"
"time"
)
var (
ErrInvalidSudokuMapMiss = errors.New("INVALID_SUDOKU_MAP_MISS")
)
type Table struct {
EncodeTable [256][][4]byte
DecodeMap map[uint32]byte
PaddingPool []byte
IsASCII bool // 标记当前模式
layout *byteLayout
}
// NewTable initializes the obfuscation tables with built-in layouts.
// Equivalent to calling NewTableWithCustom(key, mode, "").
func NewTable(key string, mode string) *Table {
t, err := NewTableWithCustom(key, mode, "")
if err != nil {
log.Panicf("failed to build table: %v", err)
}
return t
}
// NewTableWithCustom initializes obfuscation tables using either predefined or custom layouts.
// mode: "prefer_ascii" or "prefer_entropy". If a custom pattern is provided, ASCII mode still takes precedence.
// The customPattern must contain 8 characters with exactly 2 x, 2 p, and 4 v (case-insensitive).
func NewTableWithCustom(key string, mode string, customPattern string) (*Table, error) {
start := time.Now()
layout, err := resolveLayout(mode, customPattern)
if err != nil {
return nil, err
}
t := &Table{
DecodeMap: make(map[uint32]byte),
IsASCII: layout.name == "ascii",
layout: layout,
}
t.PaddingPool = append(t.PaddingPool, layout.paddingPool...)
// 生成数独网格 (逻辑不变)
allGrids := GenerateAllGrids()
h := sha256.New()
h.Write([]byte(key))
seed := int64(binary.BigEndian.Uint64(h.Sum(nil)[:8]))
rng := rand.New(rand.NewSource(seed))
shuffledGrids := make([]Grid, 288)
copy(shuffledGrids, allGrids)
rng.Shuffle(len(shuffledGrids), func(i, j int) {
shuffledGrids[i], shuffledGrids[j] = shuffledGrids[j], shuffledGrids[i]
})
// 预计算组合
var combinations [][]int
var combine func(int, int, []int)
combine = func(s, k int, c []int) {
if k == 0 {
tmp := make([]int, len(c))
copy(tmp, c)
combinations = append(combinations, tmp)
return
}
for i := s; i <= 16-k; i++ {
c = append(c, i)
combine(i+1, k-1, c)
c = c[:len(c)-1]
}
}
combine(0, 4, []int{})
// 构建映射表
for byteVal := 0; byteVal < 256; byteVal++ {
targetGrid := shuffledGrids[byteVal]
for _, positions := range combinations {
var currentHints [4]byte
// 1. 计算抽象提示 (Abstract Hints)
// 我们先计算出 val 和 pos后面再根据模式编码成 byte
var rawParts [4]struct{ val, pos byte }
for i, pos := range positions {
val := targetGrid[pos] // 1..4
rawParts[i] = struct{ val, pos byte }{val, uint8(pos)}
}
// 检查唯一性 (数独逻辑)
matchCount := 0
for _, g := range allGrids {
match := true
for _, p := range rawParts {
if g[p.pos] != p.val {
match = false
break
}
}
if match {
matchCount++
if matchCount > 1 {
break
}
}
}
if matchCount == 1 {
// 唯一确定,生成最终编码字节
for i, p := range rawParts {
currentHints[i] = t.layout.encodeHint(p.val-1, p.pos)
}
t.EncodeTable[byteVal] = append(t.EncodeTable[byteVal], currentHints)
// 生成解码键 (需要对 Hints 进行排序以忽略传输顺序)
key := packHintsToKey(currentHints)
t.DecodeMap[key] = byte(byteVal)
}
}
}
log.Printf("[Init] Sudoku Tables initialized (%s) in %v", layout.name, time.Since(start))
return t, nil
}
func packHintsToKey(hints [4]byte) uint32 {
// Sorting network for 4 elements (Bubble sort unrolled)
// Swap if a > b
if hints[0] > hints[1] {
hints[0], hints[1] = hints[1], hints[0]
}
if hints[2] > hints[3] {
hints[2], hints[3] = hints[3], hints[2]
}
if hints[0] > hints[2] {
hints[0], hints[2] = hints[2], hints[0]
}
if hints[1] > hints[3] {
hints[1], hints[3] = hints[3], hints[1]
}
if hints[1] > hints[2] {
hints[1], hints[2] = hints[2], hints[1]
}
return uint32(hints[0])<<24 | uint32(hints[1])<<16 | uint32(hints[2])<<8 | uint32(hints[3])
}

View File

@@ -0,0 +1,38 @@
package sudoku
import "fmt"
// TableSet is a small helper for managing multiple Sudoku tables (e.g. for per-connection rotation).
// It is intentionally decoupled from the tunnel/app layers.
type TableSet struct {
Tables []*Table
}
// NewTableSet builds one or more tables from key/mode and a list of custom X/P/V patterns.
// If patterns is empty, it builds a single default table (customPattern="").
func NewTableSet(key string, mode string, patterns []string) (*TableSet, error) {
if len(patterns) == 0 {
t, err := NewTableWithCustom(key, mode, "")
if err != nil {
return nil, err
}
return &TableSet{Tables: []*Table{t}}, nil
}
tables := make([]*Table, 0, len(patterns))
for i, pattern := range patterns {
t, err := NewTableWithCustom(key, mode, pattern)
if err != nil {
return nil, fmt.Errorf("build table[%d] (%q): %w", i, pattern, err)
}
tables = append(tables, t)
}
return &TableSet{Tables: tables}, nil
}
func (ts *TableSet) Candidates() []*Table {
if ts == nil {
return nil
}
return ts.Tables
}

View File

@@ -6,7 +6,7 @@ import (
"math/rand"
"net"
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
// perm4 matches github.com/saba-futai/sudoku/pkg/obfs/sudoku perm4.

View File

@@ -10,26 +10,12 @@ import (
"net"
"time"
"github.com/saba-futai/sudoku/apis"
"github.com/saba-futai/sudoku/pkg/crypto"
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
"github.com/metacubex/mihomo/transport/sudoku/crypto"
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
func tableCandidates(cfg *apis.ProtocolConfig) []*sudoku.Table {
if cfg == nil {
return nil
}
if len(cfg.Tables) > 0 {
return cfg.Tables
}
if cfg.Table != nil {
return []*sudoku.Table{cfg.Table}
}
return nil
}
func pickClientTable(cfg *apis.ProtocolConfig) (*sudoku.Table, byte, error) {
candidates := tableCandidates(cfg)
func pickClientTable(cfg *ProtocolConfig) (*sudoku.Table, byte, error) {
candidates := cfg.tableCandidates()
if len(candidates) == 0 {
return nil, 0, fmt.Errorf("no table configured")
}
@@ -62,7 +48,7 @@ func drainBuffered(r *bufio.Reader) ([]byte, error) {
return out, err
}
func probeHandshakeBytes(probe []byte, cfg *apis.ProtocolConfig, table *sudoku.Table) error {
func probeHandshakeBytes(probe []byte, cfg *ProtocolConfig, table *sudoku.Table) error {
rc := &readOnlyConn{Reader: bytes.NewReader(probe)}
_, obfsConn := buildServerObfsConn(rc, cfg, table, false)
cConn, err := crypto.NewAEADConn(obfsConn, cfg.Key, cfg.AEADMethod)
@@ -90,7 +76,7 @@ func probeHandshakeBytes(probe []byte, cfg *apis.ProtocolConfig, table *sudoku.T
return nil
}
func selectTableByProbe(r *bufio.Reader, cfg *apis.ProtocolConfig, tables []*sudoku.Table) (*sudoku.Table, []byte, error) {
func selectTableByProbe(r *bufio.Reader, cfg *ProtocolConfig, tables []*sudoku.Table) (*sudoku.Table, []byte, error) {
const (
maxProbeBytes = 64 * 1024
readChunk = 4 * 1024

View File

@@ -3,7 +3,7 @@ package sudoku
import (
"strings"
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
// NewTablesWithCustomPatterns builds one or more obfuscation tables from x/v/p custom patterns.

View File

@@ -582,55 +582,67 @@ function api_status()
local version = get_command("/usr/sbin/ddnstod -v")
-- Check connectivity to the tunnel server (BusyBox nc may not support -z/-w flags)
-- Check connectivity to the tunnel server via ping
local function resolve_host(host)
local out = sys.exec(string.format("nslookup %s 2>/dev/null", host)) or ""
-- Prefer public DNS to avoid local resolver issues; fallback to default resolver
local out = sys.exec(string.format("nslookup %s 223.5.5.5 2>/dev/null", host)) or ""
if out == "" then
out = sys.exec(string.format("nslookup %s 8.8.8.8 2>/dev/null", host)) or ""
end
if out == "" then
out = sys.exec(string.format("nslookup %s 2>/dev/null", host)) or ""
end
local ip = out:match("Address 1:%s*([%d%.]+)") or out:match("Address:%s*([%d%.]+)")
return ip or ""
end
local tunnel_ip = resolve_host("tunnel.kooldns.cn")
local tunnel_target = tunnel_ip ~= "" and tunnel_ip or "tunnel.kooldns.cn"
local tunnel_err = nil
local tunnel_ret = -1
local tunnel_targets = {}
local resolved_ip = resolve_host("tunnel.kooldns.cn")
if resolved_ip ~= "" then table.insert(tunnel_targets, resolved_ip) end
table.insert(tunnel_targets, "tunnel.kooldns.cn")
-- Fallback known IP to avoid DNS issues
table.insert(tunnel_targets, "125.39.21.43")
if tunnel_ip == "" then
-- Deduplicate targets
do
local seen = {}
local uniq = {}
for _, t in ipairs(tunnel_targets) do
if not seen[t] then
seen[t] = true
table.insert(uniq, t)
end
end
tunnel_targets = uniq
end
local function connect_target(target)
-- BusyBox ping: -c 1 (one packet), -W 2 (2s timeout)
local ret = sys.call(string.format("ping -c 1 -W 2 %s >/dev/null 2>&1", target))
if ret == 0 then
return 0, nil
end
return ret, string.format("ping exit %d to %s", ret, target)
end
local tunnel_ok = false
local tunnel_err = nil
if #tunnel_targets == 0 then
tunnel_err = "resolve tunnel.kooldns.cn failed"
else
local has_timeout = (sys.call("command -v timeout >/dev/null 2>&1") == 0)
if has_timeout then
-- Prefer timeout if available
tunnel_ret = sys.call(string.format("timeout 3 nc %s 4445 </dev/null >/dev/null 2>&1", tunnel_target))
else
-- Fallback: background nc and kill after ~3s if still running
local tmpl = table.concat({
"sh -c '",
"nc %s 4445 </dev/null >/dev/null 2>&1 & pid=$!;",
"for i in 1 2 3; do",
" sleep 1;",
" if ! kill -0 $pid 2>/dev/null; then",
" wait $pid;",
" exit $?;",
" fi;",
"done;",
"kill -9 $pid >/dev/null 2>&1;",
"wait $pid >/dev/null 2>&1;",
"exit 1",
"'"
}, " ")
tunnel_ret = sys.call(string.format(tmpl, tunnel_target))
end
if tunnel_ret ~= 0 then
if has_timeout then
tunnel_err = string.format("nc exit %d", tunnel_ret)
for _, target in ipairs(tunnel_targets) do
local ret, err = connect_target(target)
if ret == 0 then
tunnel_ok = true
tunnel_err = nil
break
else
tunnel_err = string.format("nc exit %d (no timeout available, BusyBox nc limited)", tunnel_ret)
tunnel_err = err
end
end
end
local tunnel_ok = (tunnel_ret == 0)
local did = ""
do
local idx = index

View File

@@ -1,15 +1,24 @@
<%+header%>
<script>
(function () {
window.ddnstoConfig = {
token: "<%=token or ''%>",
prefix: "<%=prefix or '/cgi-bin/luci/admin/services/ddnsto'%>",
api_base: "<%=api_base or '/cgi-bin/luci'%>",
lang: "<%=lang or 'zh-cn'%>"
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DDNSTO Plugin Settings Page</title>
<script type="module" crossorigin src="/luci-static/ddnsto/index.js"></script>
</head>
window.IstoreosFormConfig = {
getApi: "/cgi-bin/luci/admin/services/ddnsto/form/",
logApi: "/cgi-bin/luci/admin/services/ddnsto/log",
submitApi: "/cgi-bin/luci/admin/services/ddnsto/submit/"
};
})();
</script>
<body>
<div id="root"></div>
</body>
</html>
<div class="ddnsto-host">
<div id="app" class="ddnsto-root" data-ddnsto-shadow></div>
</div>
<script type="module" crossorigin src="/luci-static/ddnsto/index.js?v=<%=math.random(1,100000)%>"></script>
<%+footer%>

File diff suppressed because one or more lines are too long

View File

@@ -730,6 +730,9 @@ o = s:option(Value, _n("xudp_concurrency"), translate("XUDP Mux concurrency"))
o.default = 8
o:depends({ [_n("mux")] = true })
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = 0
--[[tcpMptcp]]
o = s:option(Flag, _n("tcpMptcp"), "tcpMptcp", translate("Enable Multipath TCP, need to be enabled in both server and client configuration."))
o.default = 0

View File

@@ -95,7 +95,7 @@ function gen_outbound(flag, node, tag, proxy_table)
local relay_port = node.port
new_port = get_new_port()
local config_file = string.format("%s_%s_%s.json", flag, tag, new_port)
if tag and node_id and tag ~= node_id then
if tag and node_id and not tag:find(node_id) then
config_file = string.format("%s_%s_%s_%s.json", flag, tag, node_id, new_port)
end
if run_socks_instance then

View File

@@ -74,7 +74,7 @@ function gen_outbound(flag, node, tag, proxy_table)
local relay_port = node.port
new_port = get_new_port()
local config_file = string.format("%s_%s_%s.json", flag, tag, new_port)
if tag and node_id and tag ~= node_id then
if tag and node_id and not tag:find(node_id) then
config_file = string.format("%s_%s_%s_%s.json", flag, tag, node_id, new_port)
end
if run_socks_instance then
@@ -146,6 +146,7 @@ function gen_outbound(flag, node, tag, proxy_table)
streamSettings = (node.streamSettings or node.protocol == "vmess" or node.protocol == "vless" or node.protocol == "socks" or node.protocol == "shadowsocks" or node.protocol == "trojan") and {
sockopt = {
mark = 255,
tcpFastOpen = (node.tcp_fast_open == "1") and true or nil,
tcpMptcp = (node.tcpMptcp == "1") and true or nil,
dialerProxy = (fragment or noise) and "dialerproxy" or nil
},

View File

@@ -35,59 +35,46 @@ local api = require "luci.passwall.api"
}
var global_id = null;
var global = document.getElementById("cbi-passwall-global");
var global = document.getElementById("cbi-<%=api.appname%>-global");
if (global) {
var node = global.getElementsByClassName("cbi-section-node")[0];
var node_id = node.getAttribute("id");
global_id = node_id;
var reg1 = new RegExp("(?<=" + node_id + "-).*?(?=(_node))");
var all_node = node.querySelectorAll("[id]");
var reg1 = /^cbid\..*node\.main$/;
for (var i = 0; i < node.childNodes.length; i++) {
var row = node.childNodes[i];
if (!row || !row.childNodes) continue;
for (var i = 0; i < all_node.length; i++) {
var el = all_node[i];
if (!reg1.test(el.id)) continue;
for (var k = 0; k < row.childNodes.length; k++) {
try {
var dom = row.childNodes[k];
if (!dom || !dom.id) continue;
var s = dom.id.match(reg1);
if (!s) continue;
var cbi_id = global_id + "-";
var cbid = dom.id.split(cbi_id).join(cbi_id.split("-").join(".")).split("cbi.").join("cbid.");
var dom_id = cbid + ".main";
if (!/_node\.main$/.test(dom_id)) continue;
var node_select = document.getElementById(dom_id);
if (!node_select) continue;
var hidden_select = document.getElementById(cbid);
var node_select_value = hidden_select ? hidden_select.options[0].value : "";
if (!node_select_value || node_select_value.indexOf("_default") === 0 || node_select_value.indexOf("_direct") === 0 || node_select_value.indexOf("_blackhole") === 0) {
continue;
}
var to_url = '<%=api.url("node_config")%>/' + node_select_value;
if (node_select_value.indexOf("Socks_") === 0) {
to_url = '<%=api.url("socks_config")%>/' +
node_select_value.substring("Socks_".length);
}
var html = '<a href="#" onclick="location.href=\'' + to_url + '\'"><%:Edit%></a>';
if (s[0] === "tcp" || s[0] === "udp") {
html += '<a href="#" onclick="window.open(\'' + '<%=api.url("get_redir_log")%>?name=default&proto=' + s[0] + '\', \'_blank\')"><%:Log%></a>';
}
node_select.insertAdjacentHTML("beforeend",
'<div class="node-actions" style="display:inline-flex; align-items:center; gap:4px; flex-wrap:wrap; margin-left:4px;">'
+ html + '</div>'
);
} catch (e) {
}
var node_select = el;
if (!node_select) continue;
var cbid = el.id.replace(/\.main$/, "");
var hidden_select = document.getElementById(cbid);
var node_select_value = hidden_select ? hidden_select.options[0].value : "";
if (!node_select_value || node_select_value.indexOf("_default") === 0 || node_select_value.indexOf("_direct") === 0 || node_select_value.indexOf("_blackhole") === 0) {
continue;
}
var to_url = '<%=api.url("node_config")%>/' + node_select_value;
if (node_select_value.indexOf("Socks_") === 0) {
to_url = '<%=api.url("socks_config")%>/' + node_select_value.substring("Socks_".length);
}
var html = '<a href="#" onclick="location.href=\'' + to_url + '\'"><%:Edit%></a>';
var m = cbid.match(/\.(tcp|udp)_node$/);
if (m && (m[1] === "tcp" || m[1] === "udp")) {
html += '<a href="#" onclick="window.open(\'' + '<%=api.url("get_redir_log")%>?name=default&proto=' + m[1] + '\', \'_blank\')"><%:Log%></a>';
}
node_select.insertAdjacentHTML("beforeend",
'<div class="node-actions" style="display:inline-flex; align-items:center; gap:4px; flex-wrap:wrap; margin-left:4px;">'
+ html + '</div>'
);
}
}
var socks = document.getElementById("cbi-passwall-socks");
var socks = document.getElementById("cbi-<%=api.appname%>-socks");
if (socks) {
var socks_enabled_dom = document.getElementById(global_id + "-socks_enabled");
socks_enabled_dom.parentNode.removeChild(socks_enabled_dom);

View File

@@ -254,7 +254,8 @@ check_ver() {
}
first_type() {
for p in "/bin/$1" "${TMP_BIN_PATH:-/tmp}/$1" "$1"; do
[ "${1#/}" != "$1" ] && [ -x "$1" ] && echo "$1" && return
for p in "/bin/$1" "/usr/bin/$1" "${TMP_BIN_PATH:-/tmp}/$1"; do
[ -x "$p" ] && echo "$p" && return
done
command -v "$1" 2>/dev/null || command -v "$2" 2>/dev/null
@@ -690,7 +691,7 @@ run_socks() {
case "$type" in
socks)
local _socks_address _socks_port _socks_username _socks_password
local _socks_address _socks_port _socks_username _socks_password _extra_param microsocks_fwd
if [ "$node2socks_port" = "0" ]; then
_socks_address=$(config_n_get $node address)
_socks_port=$(config_n_get $node port)
@@ -700,13 +701,24 @@ run_socks() {
_socks_address="127.0.0.1"
_socks_port=$node2socks_port
fi
[ "$http_port" != "0" ] && {
if [ "$http_port" != "0" ]; then
http_flag=1
config_file="${config_file//SOCKS/HTTP_SOCKS}"
local _extra_param="-local_http_address $bind -local_http_port $http_port"
}
_extra_param="-local_http_address $bind -local_http_port $http_port"
else
# 仅 passwall-packages 专用的 microsocks 才支持 socks 转发规则!
microsocks_fwd="$($(first_type microsocks) -V 2>/dev/null | grep -i forward)"
fi
local bin=$(first_type $(config_t_get global_app sing_box_file) sing-box)
if [ -n "$bin" ]; then
if [ -n "$microsocks_fwd" ]; then
local ext_name=$(echo "$config_file" | sed "s|^${TMP_PATH}/||; s|\.json\$||; s|/|_|g")
if [ -n "$_socks_username" ] && [ -n "$_socks_password" ]; then
_extra_param="-f \"0.0.0.0:0,${_socks_username}:${_socks_password}@${_socks_address}:${_socks_port},0.0.0.0:0\""
else
_extra_param="-f \"0.0.0.0:0,${_socks_address}:${_socks_port},0.0.0.0:0\""
fi
ln_run "$(first_type microsocks)" "microsocks_${ext_name}" $log_file -i $bind -p $socks_port ${_extra_param}
elif [ -n "$bin" ]; then
type="sing-box"
lua $UTIL_SINGBOX gen_proto_config -local_socks_address $bind -local_socks_port $socks_port ${_extra_param} -server_proto socks -server_address ${_socks_address} -server_port ${_socks_port} -server_username ${_socks_username} -server_password ${_socks_password} > $config_file
ln_run "$bin" ${type} $log_file run -c "$config_file"

View File

@@ -111,6 +111,10 @@ o:depends({ [_n("protocol")] = "_balancing" })
o.widget = "checkbox"
o.template = appname .. "/cbi/nodes_multivalue"
o.group = {}
for k, v in pairs(socks_list) do
o:value(v.id, v.remark)
o.group[#o.group+1] = v.group or ""
end
for i, v in pairs(nodes_table) do
o:value(v.id, v.remark)
o.group[#o.group+1] = v.group or ""
@@ -166,6 +170,10 @@ end
if is_balancer then
check_fallback_chain(arg[1])
end
for k, v in pairs(socks_list) do
o:value(v.id, v.remark)
o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
for k, v in pairs(fallback_table) do
o:value(v.id, v.remark)
o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
@@ -723,6 +731,9 @@ o = s:option(Value, _n("xudp_concurrency"), translate("XUDP Mux concurrency"))
o.default = 8
o:depends({ [_n("mux")] = true })
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = 0
--[[tcpMptcp]]
o = s:option(Flag, _n("tcpMptcp"), "tcpMptcp", translate("Enable Multipath TCP, need to be enabled in both server and client configuration."))
o.default = 0

View File

@@ -120,6 +120,10 @@ o:depends({ [_n("protocol")] = "_urltest" })
o.widget = "checkbox"
o.template = appname .. "/cbi/nodes_multivalue"
o.group = {}
for k, v in pairs(socks_list) do
o:value(v.id, v.remark)
o.group[#o.group+1] = v.group or ""
end
for i, v in pairs(nodes_table) do
o:value(v.id, v.remark)
o.group[#o.group+1] = v.group or ""

View File

@@ -46,7 +46,7 @@ function gen_outbound(flag, node, tag, proxy_table)
local relay_port = node.port
new_port = get_new_port()
local config_file = string.format("%s_%s_%s.json", flag, tag, new_port)
if tag and node_id and tag ~= node_id then
if tag and node_id and not tag:find(node_id) then
config_file = string.format("%s_%s_%s_%s.json", flag, tag, node_id, new_port)
end
if run_socks_instance then
@@ -1064,12 +1064,30 @@ function gen_config(var)
end
end
if is_new_ut_node then
local ut_node = uci:get_all(appname, ut_node_id)
local outbound = gen_outbound(flag, ut_node, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
if outbound then
outbound.tag = outbound.tag .. ":" .. ut_node.remarks
table.insert(outbounds, outbound)
valid_nodes[#valid_nodes + 1] = outbound.tag
local ut_node
if ut_node_id:find("Socks_") then
local socks_id = ut_node_id:sub(1 + #"Socks_")
local socks_node = uci:get_all(appname, socks_id) or nil
if socks_node then
ut_node = {
type = "sing-box",
protocol = "socks",
address = "127.0.0.1",
port = socks_node.port,
uot = "1",
remarks = "Socks_" .. socks_node.port
}
end
else
ut_node = uci:get_all(appname, ut_node_id)
end
if ut_node then
local outbound = gen_outbound(flag, ut_node, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
if outbound then
outbound.tag = outbound.tag .. ":" .. ut_node.remarks
table.insert(outbounds, outbound)
valid_nodes[#valid_nodes + 1] = outbound.tag
end
end
end
end

View File

@@ -71,7 +71,7 @@ function gen_outbound(flag, node, tag, proxy_table)
local relay_port = node.port
new_port = get_new_port()
local config_file = string.format("%s_%s_%s.json", flag, tag, new_port)
if tag and node_id and tag ~= node_id then
if tag and node_id and not tag:find(node_id) then
config_file = string.format("%s_%s_%s_%s.json", flag, tag, node_id, new_port)
end
if run_socks_instance then
@@ -142,6 +142,7 @@ function gen_outbound(flag, node, tag, proxy_table)
streamSettings = (node.streamSettings or node.protocol == "vmess" or node.protocol == "vless" or node.protocol == "socks" or node.protocol == "shadowsocks" or node.protocol == "trojan") and {
sockopt = {
mark = 255,
tcpFastOpen = (node.tcp_fast_open == "1") and true or nil,
tcpMptcp = (node.tcpMptcp == "1") and true or nil,
dialerProxy = (fragment or noise) and "dialerproxy" or nil
},
@@ -741,12 +742,31 @@ function gen_config(var)
end
end
if is_new_blc_node then
local blc_node = uci:get_all(appname, blc_node_id)
local outbound = gen_outbound(flag, blc_node, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
if outbound then
outbound.tag = outbound.tag .. ":" .. blc_node.remarks
table.insert(outbounds, outbound)
valid_nodes[#valid_nodes + 1] = outbound.tag
local blc_node
if blc_node_id:find("Socks_") then
local socks_id = blc_node_id:sub(1 + #"Socks_")
local socks_node = uci:get_all(appname, socks_id) or nil
if socks_node then
blc_node = {
type = "Xray",
protocol = "socks",
address = "127.0.0.1",
port = socks_node.port,
transport = "tcp",
stream_security = "none",
remarks = "Socks_" .. socks_node.port
}
end
else
blc_node = uci:get_all(appname, blc_node_id)
end
if blc_node then
local outbound = gen_outbound(flag, blc_node, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
if outbound then
outbound.tag = outbound.tag .. ":" .. blc_node.remarks
table.insert(outbounds, outbound)
valid_nodes[#valid_nodes + 1] = outbound.tag
end
end
end
end
@@ -766,17 +786,36 @@ function gen_config(var)
end
end
if is_new_node then
local fallback_node = uci:get_all(appname, fallback_node_id)
if fallback_node.protocol ~= "_balancing" then
local outbound = gen_outbound(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
if outbound then
outbound.tag = outbound.tag .. ":" .. fallback_node.remarks
table.insert(outbounds, outbound)
fallback_node_tag = outbound.tag
local fallback_node
if fallback_node_id:find("Socks_") then
local socks_id = fallback_node_id:sub(1 + #"Socks_")
local socks_node = uci:get_all(appname, socks_id) or nil
if socks_node then
fallback_node = {
type = "Xray",
protocol = "socks",
address = "127.0.0.1",
port = socks_node.port,
transport = "tcp",
stream_security = "none",
remarks = "Socks_" .. socks_node.port
}
end
else
if gen_balancer(fallback_node) then
fallback_node_tag = fallback_node_id
fallback_node = uci:get_all(appname, fallback_node_id)
end
if fallback_node then
if fallback_node.protocol ~= "_balancing" then
local outbound = gen_outbound(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
if outbound then
outbound.tag = outbound.tag .. ":" .. fallback_node.remarks
table.insert(outbounds, outbound)
fallback_node_tag = outbound.tag
end
else
if gen_balancer(fallback_node) then
fallback_node_tag = fallback_node_id
end
end
end
end
@@ -1604,7 +1643,11 @@ function gen_config(var)
end
for index, value in ipairs(config.outbounds) do
if not value["_flag_proxy_tag"] and value["_id"] and value.server and value.server_port and not no_run then
local s = value.settings
if not value["_flag_proxy_tag"] and value["_id"] and s and not no_run and
((s.vnext and s.vnext[1] and s.vnext[1].address and s.vnext[1].port) or
(s.servers and s.servers[1] and s.servers[1].address and s.servers[1].port) or
(s.peers and s.peers[1] and s.peers[1].endpoint)) then
sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list"))
end
for k, v in pairs(config.outbounds[index]) do

View File

@@ -36,59 +36,46 @@ local api = require "luci.passwall2.api"
var global_id = null;
var global = document.getElementById("cbi-passwall2-global");
var global = document.getElementById("cbi-<%=api.appname%>-global");
if (global) {
var node = global.getElementsByClassName("cbi-section-node")[0];
var node_id = node.getAttribute("id");
global_id = node_id;
var reg1 = new RegExp("(?<=" + node_id + "-).*?(?=(_node))");
var all_node = node.querySelectorAll("[id]");
var reg1 = /^cbid\..*node\.main$/;
for (var i = 0; i < node.childNodes.length; i++) {
var row = node.childNodes[i];
if (!row || !row.childNodes) continue;
for (var i = 0; i < all_node.length; i++) {
var el = all_node[i];
if (!reg1.test(el.id)) continue;
for (var k = 0; k < row.childNodes.length; k++) {
try {
var dom = row.childNodes[k];
if (!dom || !dom.id) continue;
var s = dom.id.match(reg1);
if (!s) continue;
var cbi_id = global_id + "-";
var cbid = dom.id.split(cbi_id).join(cbi_id.split("-").join(".")).split("cbi.").join("cbid.");
var dom_id = cbid + ".main";
if (!/_node\.main$/.test(dom_id)) continue;
var node_select = document.getElementById(dom_id);
if (!node_select) continue;
var hidden_select = document.getElementById(cbid);
var node_select_value = hidden_select ? hidden_select.options[0].value : "";
if (!node_select_value || node_select_value.indexOf("_default") === 0 || node_select_value.indexOf("_direct") === 0 || node_select_value.indexOf("_blackhole") === 0) {
continue;
}
var to_url = '<%=api.url("node_config")%>/' + node_select_value;
if (node_select_value.indexOf("Socks_") === 0) {
to_url = '<%=api.url("socks_config")%>/' +
node_select_value.substring("Socks_".length);
}
var html = '<a href="#" onclick="location.href=\'' + to_url + '\'"><%:Edit%></a>';
if (s[0] === "tcp" || s[0] === "udp") {
html += '<a href="#" onclick="window.open(\'' + '<%=api.url("get_redir_log")%>?name=default&proto=' + s[0] + '\', \'_blank\')"><%:Log%></a>';
}
node_select.insertAdjacentHTML("beforeend",
'<div class="node-actions" style="display:inline-flex; align-items:center; gap:4px; flex-wrap:wrap; margin-left:4px;">'
+ html + '</div>'
);
} catch (e) {
}
var node_select = el;
if (!node_select) continue;
var cbid = el.id.replace(/\.main$/, "");
var hidden_select = document.getElementById(cbid);
var node_select_value = hidden_select ? hidden_select.options[0].value : "";
if (!node_select_value || node_select_value.indexOf("_default") === 0 || node_select_value.indexOf("_direct") === 0 || node_select_value.indexOf("_blackhole") === 0) {
continue;
}
var to_url = '<%=api.url("node_config")%>/' + node_select_value;
if (node_select_value.indexOf("Socks_") === 0) {
to_url = '<%=api.url("socks_config")%>/' + node_select_value.substring("Socks_".length);
}
var html = '<a href="#" onclick="location.href=\'' + to_url + '\'"><%:Edit%></a>';
var m = cbid.match(/\.node$/);
if (m) {
html += '<a href="#" onclick="window.open(\'' + '<%=api.url("get_redir_log")%>?id=default&name=' + 'global' + '\', \'_blank\')"><%:Log%></a>';
}
node_select.insertAdjacentHTML("beforeend",
'<div class="node-actions" style="display:inline-flex; align-items:center; gap:4px; flex-wrap:wrap; margin-left:4px;">'
+ html + '</div>'
);
}
}
var socks = document.getElementById("cbi-passwall2-socks");
var socks = document.getElementById("cbi-<%=api.appname%>-socks");
if (socks) {
var socks_enabled_dom = document.getElementById(global_id + "-socks_enabled");
socks_enabled_dom.parentNode.removeChild(socks_enabled_dom);

View File

@@ -255,8 +255,11 @@ check_depends() {
}
first_type() {
local path_name=${1}
type -t -p "/bin/${path_name}" -p "${TMP_BIN_PATH}/${path_name}" -p "${path_name}" "$@" | head -n1
[ "${1#/}" != "$1" ] && [ -x "$1" ] && echo "$1" && return
for p in "/bin/$1" "/usr/bin/$1" "${TMP_BIN_PATH:-/tmp}/$1"; do
[ -x "$p" ] && echo "$p" && return
done
command -v "$1" 2>/dev/null || command -v "$2" 2>/dev/null
}
eval_set_val() {

View File

@@ -162,9 +162,12 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "arc-swap"
version = "1.7.1"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e"
dependencies = [
"rustversion",
]
[[package]]
name = "arrayref"
@@ -821,7 +824,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -928,7 +931,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2127,7 +2130,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2491,7 +2494,7 @@ dependencies = [
"once_cell",
"socket2 0.5.10",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2604,9 +2607,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "reqwest"
version = "0.12.26"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"bytes",
@@ -2758,7 +2761,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3433,7 +3436,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3760,9 +3763,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tun"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a1527bfc1f78acb1287bbd132ee44bdbf9d064c9f3ca176cb2635253f891f76"
checksum = "b35f176015650e3bd849e85808d809e5b54da2ba7df983c5c3b601a2a8f1095e"
dependencies = [
"bytes",
"cfg-if",
@@ -3775,7 +3778,7 @@ dependencies = [
"thiserror",
"tokio",
"tokio-util",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
"wintun-bindings",
]

View File

@@ -647,7 +647,7 @@ const CBIHandleImport = baseclass.extend(/** @lends hm.HandleImport.prototype */
E('br'),
//type_file_count ? _("%s rule-set of type '%s' need to be filled in manually.")
// .format(type_file_count, 'file') : ''
]));
]), 'info');
}
if (imported_count)
@@ -1149,7 +1149,7 @@ function renderResDownload(section_id) {
click: ui.createHandlerFn(this, (section_type, section_id, type, url, header) => {
if (type === 'http') {
return downloadFile(section_type, section_id, url, header).then((res) => {
ui.addNotification(null, E('p', _('Download successful.')));
ui.addNotification(null, E('p', _('Download successful.')), 'info');
}).catch((e) => {
ui.addNotification(null, E('p', _('Download failed: %s').format(e)), 'error');
});
@@ -1593,7 +1593,7 @@ function uploadCertificate(type, filename, ev) {
.then(L.bind((btn, res) => {
return L.resolveDefault(callWriteCertificate(filename), {}).then((ret) => {
if (ret.result === true)
ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(type, res.size)));
ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(type, res.size)), 'info');
else
ui.addNotification(null, E('p', _('Failed to upload %s, error: %s.').format(type, ret.error)), 'error');
});
@@ -1611,7 +1611,7 @@ function uploadInitialPack(ev, section_id) {
.then(L.bind((btn, res) => {
return L.resolveDefault(callWriteInitialPack(), {}).then((ret) => {
if (ret.result === true) {
ui.addNotification(null, E('p', _('Successfully uploaded.')));
ui.addNotification(null, E('p', _('Successfully uploaded.')), 'info');
return window.location = window.location.href.split('#')[0];
} else
ui.addNotification(null, E('p', _('Failed to upload, error: %s.').format(ret.error)), 'error');

View File

@@ -205,7 +205,7 @@ return view.extend({
ui.addNotification(null, E('p', _('No valid rule-set link found.')));
else
ui.addNotification(null, E('p', _('Successfully imported %s %s of total %s.')
.format(imported_count, _('rule-set'), input_links.length)));
.format(imported_count, _('rule-set'), input_links.length)), 'info');
}
if (imported_count)

View File

@@ -730,6 +730,9 @@ o = s:option(Value, _n("xudp_concurrency"), translate("XUDP Mux concurrency"))
o.default = 8
o:depends({ [_n("mux")] = true })
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = 0
--[[tcpMptcp]]
o = s:option(Flag, _n("tcpMptcp"), "tcpMptcp", translate("Enable Multipath TCP, need to be enabled in both server and client configuration."))
o.default = 0

Some files were not shown because too many files have changed in this diff Show More