mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-05 21:35:15 +03:00
Add OpenVPN, TrustTunnel, Sudoku, inbound managers. Fixes
This commit is contained in:
212
transport/sudoku/config.go
Normal file
212
transport/sudoku/config.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/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 enables the pure Sudoku downlink mode.
|
||||
// When false, the connection uses the bandwidth-optimized packed downlink.
|
||||
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 (split-stream), CDN-compatible
|
||||
// - "poll": plain HTTP tunnel (authorize/push/pull), strong restricted-network pass-through
|
||||
// - "auto": try stream then fall back to poll
|
||||
// - "ws": WebSocket tunnel (GET upgrade), CDN-friendly
|
||||
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
|
||||
|
||||
// HTTPMaskPathRoot optionally prefixes all HTTP mask paths with a first-level segment.
|
||||
// Example: "aabbcc" => "/aabbcc/session", "/aabbcc/api/v1/upload", ...
|
||||
HTTPMaskPathRoot string
|
||||
|
||||
// HTTPMaskMultiplex controls multiplex behavior when HTTPMask tunnel modes are enabled:
|
||||
// - "off": disable reuse; each Dial establishes its own HTTPMask tunnel
|
||||
// - "auto": reuse underlying HTTP connections across multiple tunnel dials (HTTP/1.1 keep-alive / HTTP/2)
|
||||
// - "on": enable "single tunnel, multi-target" mux (Sudoku-level multiplex; Dial behaves like "auto" otherwise)
|
||||
HTTPMaskMultiplex 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.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", "ws":
|
||||
default:
|
||||
return fmt.Errorf("invalid http-mask-mode: %s, must be one of: legacy, stream, poll, auto, ws", c.HTTPMaskMode)
|
||||
}
|
||||
|
||||
if v := strings.TrimSpace(c.HTTPMaskPathRoot); v != "" {
|
||||
v = strings.Trim(v, "/")
|
||||
if v == "" || strings.Contains(v, "/") {
|
||||
return fmt.Errorf("invalid http-mask-path-root: must be a single path segment")
|
||||
}
|
||||
for i := 0; i < len(v); i++ {
|
||||
ch := v[i]
|
||||
switch {
|
||||
case ch >= 'a' && ch <= 'z':
|
||||
case ch >= 'A' && ch <= 'Z':
|
||||
case ch >= '0' && ch <= '9':
|
||||
case ch == '_' || ch == '-':
|
||||
default:
|
||||
return fmt.Errorf("invalid http-mask-path-root: contains invalid character %q", ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch strings.ToLower(strings.TrimSpace(c.HTTPMaskMultiplex)) {
|
||||
case "", "off", "auto", "on":
|
||||
default:
|
||||
return fmt.Errorf("invalid http-mask-multiplex: %s, must be one of: off, auto, on", c.HTTPMaskMultiplex)
|
||||
}
|
||||
|
||||
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",
|
||||
HTTPMaskMultiplex: "off",
|
||||
}
|
||||
}
|
||||
|
||||
func DerefInt(v *int, def int) int {
|
||||
if v == nil {
|
||||
return def
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
func DerefBool(v *bool, def bool) bool {
|
||||
if v == nil {
|
||||
return def
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
// ResolvePadding applies defaults and keeps min/max consistent when only one side is provided.
|
||||
func ResolvePadding(min, max *int, defMin, defMax int) (int, int) {
|
||||
paddingMin := DerefInt(min, defMin)
|
||||
paddingMax := DerefInt(max, defMax)
|
||||
switch {
|
||||
case min == nil && max != nil && paddingMax < paddingMin:
|
||||
paddingMin = paddingMax
|
||||
case max == nil && min != nil && paddingMax < paddingMin:
|
||||
paddingMax = paddingMin
|
||||
}
|
||||
return paddingMin, paddingMax
|
||||
}
|
||||
|
||||
func NormalizeTableType(tableType string) (string, error) {
|
||||
normalized, err := sudoku.NormalizeASCIIMode(tableType)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("table-type must be prefer_ascii, prefer_entropy, up_ascii_down_entropy, or up_entropy_down_ascii")
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user