mirror of
https://github.com/bolucat/Archive.git
synced 2025-12-24 13:28:37 +08:00
Update On Tue Dec 23 19:42:16 CET 2025
This commit is contained in:
1
.github/update.log
vendored
1
.github/update.log
vendored
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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、auto;stream/poll/auto 支持走 CDN/反代
|
||||
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效;true 强制 https;false 强制 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、auto;stream/poll/auto 支持走 CDN/反代
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
144
clash-meta/transport/sudoku/config.go
Normal file
144
clash-meta/transport/sudoku/config.go
Normal 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
|
||||
}
|
||||
130
clash-meta/transport/sudoku/crypto/aead.go
Normal file
130
clash-meta/transport/sudoku/crypto/aead.go
Normal 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)
|
||||
}
|
||||
116
clash-meta/transport/sudoku/crypto/ed25519.go
Normal file
116
clash-meta/transport/sudoku/crypto/ed25519.go
Normal 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())
|
||||
}
|
||||
@@ -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{}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/saba-futai/sudoku/pkg/obfs/httpmask"
|
||||
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
88
clash-meta/transport/sudoku/httpmask_tunnel.go
Normal file
88
clash-meta/transport/sudoku/httpmask_tunnel.go
Normal 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,
|
||||
})
|
||||
}
|
||||
445
clash-meta/transport/sudoku/httpmask_tunnel_test.go
Normal file
445
clash-meta/transport/sudoku/httpmask_tunnel_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
246
clash-meta/transport/sudoku/obfs/httpmask/masker.go
Normal file
246
clash-meta/transport/sudoku/obfs/httpmask/masker.go
Normal 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: 4KB–10MB. 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
|
||||
}
|
||||
}
|
||||
}
|
||||
1691
clash-meta/transport/sudoku/obfs/httpmask/tunnel.go
Normal file
1691
clash-meta/transport/sudoku/obfs/httpmask/tunnel.go
Normal file
File diff suppressed because it is too large
Load Diff
212
clash-meta/transport/sudoku/obfs/sudoku/conn.go
Normal file
212
clash-meta/transport/sudoku/obfs/sudoku/conn.go
Normal 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
|
||||
}
|
||||
46
clash-meta/transport/sudoku/obfs/sudoku/grid.go
Normal file
46
clash-meta/transport/sudoku/obfs/sudoku/grid.go
Normal 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
|
||||
}
|
||||
204
clash-meta/transport/sudoku/obfs/sudoku/layout.go
Normal file
204
clash-meta/transport/sudoku/obfs/sudoku/layout.go
Normal 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
|
||||
}
|
||||
332
clash-meta/transport/sudoku/obfs/sudoku/packed.go
Normal file
332
clash-meta/transport/sudoku/obfs/sudoku/packed.go
Normal 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)
|
||||
}
|
||||
153
clash-meta/transport/sudoku/obfs/sudoku/table.go
Normal file
153
clash-meta/transport/sudoku/obfs/sudoku/table.go
Normal 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])
|
||||
}
|
||||
38
clash-meta/transport/sudoku/obfs/sudoku/table_set.go
Normal file
38
clash-meta/transport/sudoku/obfs/sudoku/table_set.go
Normal 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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
14
clash-nyanpasu/backend/Cargo.lock
generated
14
clash-nyanpasu/backend/Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
238
clash-nyanpasu/pnpm-lock.yaml
generated
238
clash-nyanpasu/pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
+
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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、auto;stream/poll/auto 支持走 CDN/反代
|
||||
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效;true 强制 https;false 强制 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、auto;stream/poll/auto 支持走 CDN/反代
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
144
mihomo/transport/sudoku/config.go
Normal file
144
mihomo/transport/sudoku/config.go
Normal 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
|
||||
}
|
||||
130
mihomo/transport/sudoku/crypto/aead.go
Normal file
130
mihomo/transport/sudoku/crypto/aead.go
Normal 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)
|
||||
}
|
||||
116
mihomo/transport/sudoku/crypto/ed25519.go
Normal file
116
mihomo/transport/sudoku/crypto/ed25519.go
Normal 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())
|
||||
}
|
||||
@@ -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{}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/saba-futai/sudoku/pkg/obfs/httpmask"
|
||||
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
88
mihomo/transport/sudoku/httpmask_tunnel.go
Normal file
88
mihomo/transport/sudoku/httpmask_tunnel.go
Normal 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,
|
||||
})
|
||||
}
|
||||
445
mihomo/transport/sudoku/httpmask_tunnel_test.go
Normal file
445
mihomo/transport/sudoku/httpmask_tunnel_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
246
mihomo/transport/sudoku/obfs/httpmask/masker.go
Normal file
246
mihomo/transport/sudoku/obfs/httpmask/masker.go
Normal 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: 4KB–10MB. 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
|
||||
}
|
||||
}
|
||||
}
|
||||
1691
mihomo/transport/sudoku/obfs/httpmask/tunnel.go
Normal file
1691
mihomo/transport/sudoku/obfs/httpmask/tunnel.go
Normal file
File diff suppressed because it is too large
Load Diff
212
mihomo/transport/sudoku/obfs/sudoku/conn.go
Normal file
212
mihomo/transport/sudoku/obfs/sudoku/conn.go
Normal 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
|
||||
}
|
||||
46
mihomo/transport/sudoku/obfs/sudoku/grid.go
Normal file
46
mihomo/transport/sudoku/obfs/sudoku/grid.go
Normal 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
|
||||
}
|
||||
204
mihomo/transport/sudoku/obfs/sudoku/layout.go
Normal file
204
mihomo/transport/sudoku/obfs/sudoku/layout.go
Normal 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
|
||||
}
|
||||
332
mihomo/transport/sudoku/obfs/sudoku/packed.go
Normal file
332
mihomo/transport/sudoku/obfs/sudoku/packed.go
Normal 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)
|
||||
}
|
||||
153
mihomo/transport/sudoku/obfs/sudoku/table.go
Normal file
153
mihomo/transport/sudoku/obfs/sudoku/table.go
Normal 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])
|
||||
}
|
||||
38
mihomo/transport/sudoku/obfs/sudoku/table_set.go
Normal file
38
mihomo/transport/sudoku/obfs/sudoku/table_set.go
Normal 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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
29
shadowsocks-rust/Cargo.lock
generated
29
shadowsocks-rust/Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user