mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-24 02:53:20 +03:00
Add OpenVPN, TrustTunnel, Sudoku, inbound managers. Fixes
This commit is contained in:
93
transport/sudoku/obfs/sudoku/ascii_mode.go
Normal file
93
transport/sudoku/obfs/sudoku/ascii_mode.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
asciiModeTokenASCII = "ascii"
|
||||
asciiModeTokenEntropy = "entropy"
|
||||
)
|
||||
|
||||
// ASCIIMode describes the preferred wire layout for each traffic direction.
|
||||
// Uplink is client->server, Downlink is server->client.
|
||||
type ASCIIMode struct {
|
||||
Uplink string
|
||||
Downlink string
|
||||
}
|
||||
|
||||
// ParseASCIIMode accepts legacy symmetric values ("ascii"/"entropy"/"prefer_*")
|
||||
// and directional values like "up_ascii_down_entropy".
|
||||
func ParseASCIIMode(mode string) (ASCIIMode, error) {
|
||||
raw := strings.ToLower(strings.TrimSpace(mode))
|
||||
switch raw {
|
||||
case "", "entropy", "prefer_entropy":
|
||||
return ASCIIMode{Uplink: asciiModeTokenEntropy, Downlink: asciiModeTokenEntropy}, nil
|
||||
case "ascii", "prefer_ascii":
|
||||
return ASCIIMode{Uplink: asciiModeTokenASCII, Downlink: asciiModeTokenASCII}, nil
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(raw, "up_") {
|
||||
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
|
||||
}
|
||||
parts := strings.SplitN(strings.TrimPrefix(raw, "up_"), "_down_", 2)
|
||||
if len(parts) != 2 {
|
||||
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
|
||||
}
|
||||
|
||||
up, ok := normalizeASCIIModeToken(parts[0])
|
||||
if !ok {
|
||||
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
|
||||
}
|
||||
down, ok := normalizeASCIIModeToken(parts[1])
|
||||
if !ok {
|
||||
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
|
||||
}
|
||||
return ASCIIMode{Uplink: up, Downlink: down}, nil
|
||||
}
|
||||
|
||||
// NormalizeASCIIMode returns the canonical config string for a supported mode.
|
||||
func NormalizeASCIIMode(mode string) (string, error) {
|
||||
parsed, err := ParseASCIIMode(mode)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return parsed.Canonical(), nil
|
||||
}
|
||||
|
||||
func (m ASCIIMode) Canonical() string {
|
||||
if m.Uplink == asciiModeTokenASCII && m.Downlink == asciiModeTokenASCII {
|
||||
return "prefer_ascii"
|
||||
}
|
||||
if m.Uplink == asciiModeTokenEntropy && m.Downlink == asciiModeTokenEntropy {
|
||||
return "prefer_entropy"
|
||||
}
|
||||
return "up_" + m.Uplink + "_down_" + m.Downlink
|
||||
}
|
||||
|
||||
func (m ASCIIMode) uplinkPreference() string {
|
||||
return singleDirectionPreference(m.Uplink)
|
||||
}
|
||||
|
||||
func (m ASCIIMode) downlinkPreference() string {
|
||||
return singleDirectionPreference(m.Downlink)
|
||||
}
|
||||
|
||||
func normalizeASCIIModeToken(token string) (string, bool) {
|
||||
switch strings.ToLower(strings.TrimSpace(token)) {
|
||||
case "ascii", "prefer_ascii":
|
||||
return asciiModeTokenASCII, true
|
||||
case "entropy", "prefer_entropy", "":
|
||||
return asciiModeTokenEntropy, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func singleDirectionPreference(token string) string {
|
||||
if token == asciiModeTokenASCII {
|
||||
return "prefer_ascii"
|
||||
}
|
||||
return "prefer_entropy"
|
||||
}
|
||||
193
transport/sudoku/obfs/sudoku/conn.go
Normal file
193
transport/sudoku/obfs/sudoku/conn.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
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 atomic.Bool
|
||||
recordLock sync.Mutex
|
||||
|
||||
rawBuf []byte
|
||||
pendingData pendingBuffer
|
||||
hintBuf [4]byte
|
||||
hintCount int
|
||||
writeMu sync.Mutex
|
||||
writeBuf []byte
|
||||
|
||||
rng randomSource
|
||||
paddingThreshold uint64
|
||||
}
|
||||
|
||||
func (sc *Conn) CloseWrite() error {
|
||||
if sc == nil || sc.Conn == nil {
|
||||
return nil
|
||||
}
|
||||
if cw, ok := sc.Conn.(interface{ CloseWrite() error }); ok {
|
||||
return cw.CloseWrite()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc *Conn) CloseRead() error {
|
||||
if sc == nil || sc.Conn == nil {
|
||||
return nil
|
||||
}
|
||||
if cr, ok := sc.Conn.(interface{ CloseRead() error }); ok {
|
||||
return cr.CloseRead()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewConn(c net.Conn, table *Table, pMin, pMax int, record bool) *Conn {
|
||||
localRng := newSeededRand()
|
||||
|
||||
sc := &Conn{
|
||||
Conn: c,
|
||||
table: table,
|
||||
reader: bufio.NewReaderSize(c, IOBufferSize),
|
||||
rawBuf: make([]byte, IOBufferSize),
|
||||
pendingData: newPendingBuffer(4096),
|
||||
writeBuf: make([]byte, 0, 4096),
|
||||
rng: localRng,
|
||||
paddingThreshold: pickPaddingThreshold(localRng, pMin, pMax),
|
||||
}
|
||||
if record {
|
||||
sc.recorder = new(bytes.Buffer)
|
||||
sc.recording.Store(true)
|
||||
}
|
||||
return sc
|
||||
}
|
||||
|
||||
func (sc *Conn) StopRecording() {
|
||||
sc.recordLock.Lock()
|
||||
sc.recording.Store(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
|
||||
}
|
||||
|
||||
sc.writeMu.Lock()
|
||||
defer sc.writeMu.Unlock()
|
||||
|
||||
sc.writeBuf = encodeSudokuPayload(sc.writeBuf[:0], sc.table, sc.rng, sc.paddingThreshold, p)
|
||||
return len(p), writeFull(sc.Conn, sc.writeBuf)
|
||||
}
|
||||
|
||||
func (sc *Conn) Read(p []byte) (n int, err error) {
|
||||
if n, ok := drainPending(p, &sc.pendingData); ok {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
for {
|
||||
if sc.pendingData.available() > 0 {
|
||||
break
|
||||
}
|
||||
|
||||
nr, rErr := sc.reader.Read(sc.rawBuf)
|
||||
if nr > 0 {
|
||||
chunk := sc.rawBuf[:nr]
|
||||
if sc.recording.Load() {
|
||||
sc.recordLock.Lock()
|
||||
if sc.recording.Load() && sc.recorder != nil {
|
||||
sc.recorder.Write(chunk)
|
||||
}
|
||||
sc.recordLock.Unlock()
|
||||
}
|
||||
|
||||
layout := sc.table.layout
|
||||
for _, b := range chunk {
|
||||
if !layout.hintTable[b] {
|
||||
continue
|
||||
}
|
||||
|
||||
sc.hintBuf[sc.hintCount] = b
|
||||
sc.hintCount++
|
||||
if sc.hintCount == len(sc.hintBuf) {
|
||||
key := packHintsToKey(sc.hintBuf)
|
||||
val, ok := sc.table.DecodeMap[key]
|
||||
if !ok {
|
||||
return 0, ErrInvalidSudokuMapMiss
|
||||
}
|
||||
sc.pendingData.appendByte(val)
|
||||
sc.hintCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rErr != nil {
|
||||
return 0, rErr
|
||||
}
|
||||
if sc.pendingData.available() > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
n, _ = drainPending(p, &sc.pendingData)
|
||||
return n, nil
|
||||
}
|
||||
36
transport/sudoku/obfs/sudoku/encode.go
Normal file
36
transport/sudoku/obfs/sudoku/encode.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package sudoku
|
||||
|
||||
func encodeSudokuPayload(dst []byte, table *Table, rng randomSource, paddingThreshold uint64, p []byte) []byte {
|
||||
if len(p) == 0 {
|
||||
return dst[:0]
|
||||
}
|
||||
|
||||
outCapacity := len(p)*6 + 1
|
||||
if cap(dst) < outCapacity {
|
||||
dst = make([]byte, 0, outCapacity)
|
||||
}
|
||||
out := dst[:0]
|
||||
pads := table.PaddingPool
|
||||
padLen := len(pads)
|
||||
|
||||
for _, b := range p {
|
||||
if shouldPad(rng, paddingThreshold) {
|
||||
out = append(out, pads[rng.Intn(padLen)])
|
||||
}
|
||||
|
||||
puzzles := table.EncodeTable[b]
|
||||
puzzle := puzzles[rng.Intn(len(puzzles))]
|
||||
perm := perm4[rng.Intn(len(perm4))]
|
||||
for _, idx := range perm {
|
||||
if shouldPad(rng, paddingThreshold) {
|
||||
out = append(out, pads[rng.Intn(padLen)])
|
||||
}
|
||||
out = append(out, puzzle[idx])
|
||||
}
|
||||
}
|
||||
|
||||
if shouldPad(rng, paddingThreshold) {
|
||||
out = append(out, pads[rng.Intn(padLen)])
|
||||
}
|
||||
return out
|
||||
}
|
||||
46
transport/sudoku/obfs/sudoku/grid.go
Normal file
46
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
|
||||
}
|
||||
255
transport/sudoku/obfs/sudoku/layout.go
Normal file
255
transport/sudoku/obfs/sudoku/layout.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type byteLayout struct {
|
||||
name string
|
||||
hintMask byte
|
||||
hintValue byte
|
||||
padMarker byte
|
||||
paddingPool []byte
|
||||
|
||||
hintTable [256]bool
|
||||
encodeHint [4][16]byte
|
||||
encodeGroup [64]byte
|
||||
decodeGroup [256]byte
|
||||
groupValid [256]bool
|
||||
}
|
||||
|
||||
func (l *byteLayout) isHint(b byte) bool {
|
||||
return l != nil && l.hintTable[b]
|
||||
}
|
||||
|
||||
func (l *byteLayout) hintByte(val, pos byte) byte {
|
||||
return l.encodeHint[val&0x03][pos&0x0F]
|
||||
}
|
||||
|
||||
func (l *byteLayout) groupByte(group byte) byte {
|
||||
return l.encodeGroup[group&0x3F]
|
||||
}
|
||||
|
||||
func (l *byteLayout) decodePackedGroup(b byte) (byte, bool) {
|
||||
if l == nil {
|
||||
return 0, false
|
||||
}
|
||||
return l.decodeGroup[b], l.groupValid[b]
|
||||
}
|
||||
|
||||
// resolveLayout picks the byte layout for a single traffic direction.
|
||||
// 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))
|
||||
}
|
||||
|
||||
layout := &byteLayout{
|
||||
name: "ascii",
|
||||
hintMask: 0x40,
|
||||
hintValue: 0x40,
|
||||
padMarker: 0x3F,
|
||||
paddingPool: padding,
|
||||
}
|
||||
|
||||
for val := 0; val < 4; val++ {
|
||||
for pos := 0; pos < 16; pos++ {
|
||||
b := byte(0x40 | (byte(val) << 4) | byte(pos))
|
||||
if b == 0x7F {
|
||||
b = '\n'
|
||||
}
|
||||
layout.encodeHint[val][pos] = b
|
||||
}
|
||||
}
|
||||
for group := 0; group < 64; group++ {
|
||||
b := byte(0x40 | byte(group))
|
||||
if b == 0x7F {
|
||||
b = '\n'
|
||||
}
|
||||
layout.encodeGroup[group] = b
|
||||
}
|
||||
for b := 0; b < 256; b++ {
|
||||
wire := byte(b)
|
||||
if (wire & 0x40) == 0x40 {
|
||||
layout.hintTable[wire] = true
|
||||
layout.decodeGroup[wire] = wire & 0x3F
|
||||
layout.groupValid[wire] = true
|
||||
}
|
||||
}
|
||||
layout.hintTable['\n'] = true
|
||||
layout.decodeGroup['\n'] = 0x3F
|
||||
layout.groupValid['\n'] = true
|
||||
|
||||
return layout
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
layout := &byteLayout{
|
||||
name: "entropy",
|
||||
hintMask: 0x90,
|
||||
hintValue: 0x00,
|
||||
padMarker: 0x80,
|
||||
paddingPool: padding,
|
||||
}
|
||||
|
||||
for val := 0; val < 4; val++ {
|
||||
for pos := 0; pos < 16; pos++ {
|
||||
layout.encodeHint[val][pos] = (byte(val) << 5) | byte(pos)
|
||||
}
|
||||
}
|
||||
for group := 0; group < 64; group++ {
|
||||
v := byte(group)
|
||||
layout.encodeGroup[group] = ((v & 0x30) << 1) | (v & 0x0F)
|
||||
}
|
||||
for b := 0; b < 256; b++ {
|
||||
wire := byte(b)
|
||||
if (wire & 0x90) != 0 {
|
||||
continue
|
||||
}
|
||||
layout.hintTable[wire] = true
|
||||
layout.decodeGroup[wire] = ((wire >> 1) & 0x30) | (wire & 0x0F)
|
||||
layout.groupValid[wire] = true
|
||||
}
|
||||
|
||||
return layout
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
layout := &byteLayout{
|
||||
name: fmt.Sprintf("custom(%s)", cleaned),
|
||||
hintMask: xMask,
|
||||
hintValue: xMask,
|
||||
padMarker: padding[0],
|
||||
paddingPool: padding,
|
||||
}
|
||||
|
||||
for val := 0; val < 4; val++ {
|
||||
for pos := 0; pos < 16; pos++ {
|
||||
layout.encodeHint[val][pos] = encodeBits(byte(val), byte(pos), -1)
|
||||
}
|
||||
}
|
||||
for group := 0; group < 64; group++ {
|
||||
val := byte(group>>4) & 0x03
|
||||
pos := byte(group) & 0x0F
|
||||
layout.encodeGroup[group] = encodeBits(val, pos, -1)
|
||||
}
|
||||
for b := 0; b < 256; b++ {
|
||||
wire := byte(b)
|
||||
if (wire & xMask) != xMask {
|
||||
continue
|
||||
}
|
||||
layout.hintTable[wire] = true
|
||||
|
||||
var val, pos byte
|
||||
if wire&(1<<pBits[0]) != 0 {
|
||||
val |= 0x02
|
||||
}
|
||||
if wire&(1<<pBits[1]) != 0 {
|
||||
val |= 0x01
|
||||
}
|
||||
for i, bit := range vBits {
|
||||
if wire&(1<<bit) != 0 {
|
||||
pos |= 1 << (3 - uint8(i))
|
||||
}
|
||||
}
|
||||
layout.decodeGroup[wire] = (val << 4) | pos
|
||||
layout.groupValid[wire] = true
|
||||
}
|
||||
|
||||
return layout, nil
|
||||
}
|
||||
359
transport/sudoku/obfs/sudoku/packed.go
Normal file
359
transport/sudoku/obfs/sudoku/packed.go
Normal file
@@ -0,0 +1,359 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
RngBatchSize = 128
|
||||
|
||||
packedProtectedPrefixBytes = 14
|
||||
)
|
||||
|
||||
// PackedConn encodes traffic with the packed Sudoku layout while preserving
|
||||
// the same padding model as the regular connection.
|
||||
type PackedConn struct {
|
||||
net.Conn
|
||||
table *Table
|
||||
reader *bufio.Reader
|
||||
|
||||
// Read-side buffers.
|
||||
rawBuf []byte
|
||||
pendingData pendingBuffer
|
||||
|
||||
// Write-side state.
|
||||
writeMu sync.Mutex
|
||||
writeBuf []byte
|
||||
bitBuf uint64
|
||||
bitCount int
|
||||
|
||||
// Read-side bit accumulator.
|
||||
readBitBuf uint64
|
||||
readBits int
|
||||
|
||||
// Padding selection matches Conn's threshold-based model.
|
||||
rng randomSource
|
||||
paddingThreshold uint64
|
||||
padMarker byte
|
||||
padPool []byte
|
||||
}
|
||||
|
||||
func (pc *PackedConn) CloseWrite() error {
|
||||
if pc == nil || pc.Conn == nil {
|
||||
return nil
|
||||
}
|
||||
if cw, ok := pc.Conn.(interface{ CloseWrite() error }); ok {
|
||||
return cw.CloseWrite()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pc *PackedConn) CloseRead() error {
|
||||
if pc == nil || pc.Conn == nil {
|
||||
return nil
|
||||
}
|
||||
if cr, ok := pc.Conn.(interface{ CloseRead() error }); ok {
|
||||
return cr.CloseRead()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewPackedConn(c net.Conn, table *Table, pMin, pMax int) *PackedConn {
|
||||
localRng := newSeededRand()
|
||||
|
||||
pc := &PackedConn{
|
||||
Conn: c,
|
||||
table: table,
|
||||
reader: bufio.NewReaderSize(c, IOBufferSize),
|
||||
rawBuf: make([]byte, IOBufferSize),
|
||||
pendingData: newPendingBuffer(4096),
|
||||
writeBuf: make([]byte, 0, 4096),
|
||||
rng: localRng,
|
||||
paddingThreshold: pickPaddingThreshold(localRng, pMin, pMax),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (pc *PackedConn) maybeAddPadding(out []byte) []byte {
|
||||
if shouldPad(pc.rng, pc.paddingThreshold) {
|
||||
out = append(out, pc.getPaddingByte())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (pc *PackedConn) appendGroup(out []byte, group byte) []byte {
|
||||
out = pc.maybeAddPadding(out)
|
||||
return append(out, pc.table.layout.groupByte(group))
|
||||
}
|
||||
|
||||
func (pc *PackedConn) appendForcedPadding(out []byte) []byte {
|
||||
return append(out, pc.getPaddingByte())
|
||||
}
|
||||
|
||||
func (pc *PackedConn) nextProtectedPrefixGap() int {
|
||||
return 1 + pc.rng.Intn(2)
|
||||
}
|
||||
|
||||
func (pc *PackedConn) writeProtectedPrefix(out []byte, p []byte) ([]byte, int) {
|
||||
if len(p) == 0 {
|
||||
return out, 0
|
||||
}
|
||||
|
||||
limit := len(p)
|
||||
if limit > packedProtectedPrefixBytes {
|
||||
limit = packedProtectedPrefixBytes
|
||||
}
|
||||
|
||||
for padCount := 0; padCount < 1+pc.rng.Intn(2); padCount++ {
|
||||
out = pc.appendForcedPadding(out)
|
||||
}
|
||||
|
||||
gap := pc.nextProtectedPrefixGap()
|
||||
effective := 0
|
||||
for i := 0; i < limit; i++ {
|
||||
pc.bitBuf = (pc.bitBuf << 8) | uint64(p[i])
|
||||
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.appendGroup(out, group&0x3F)
|
||||
}
|
||||
|
||||
effective++
|
||||
if effective >= gap {
|
||||
out = pc.appendForcedPadding(out)
|
||||
effective = 0
|
||||
gap = pc.nextProtectedPrefixGap()
|
||||
}
|
||||
}
|
||||
|
||||
return out, limit
|
||||
}
|
||||
|
||||
func (pc *PackedConn) Write(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
pc.writeMu.Lock()
|
||||
defer pc.writeMu.Unlock()
|
||||
|
||||
needed := len(p)*3/2 + 32
|
||||
if cap(pc.writeBuf) < needed {
|
||||
pc.writeBuf = make([]byte, 0, needed)
|
||||
}
|
||||
out := pc.writeBuf[:0]
|
||||
|
||||
var prefixN int
|
||||
out, prefixN = pc.writeProtectedPrefix(out, p)
|
||||
|
||||
i := prefixN
|
||||
n := len(p)
|
||||
|
||||
for pc.bitCount > 0 && i < n {
|
||||
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.appendGroup(out, group&0x3F)
|
||||
}
|
||||
}
|
||||
|
||||
for i+11 < n {
|
||||
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
|
||||
|
||||
out = pc.appendGroup(out, g1)
|
||||
out = pc.appendGroup(out, g2)
|
||||
out = pc.appendGroup(out, g3)
|
||||
out = pc.appendGroup(out, g4)
|
||||
}
|
||||
}
|
||||
|
||||
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.appendGroup(out, g1)
|
||||
out = pc.appendGroup(out, g2)
|
||||
out = pc.appendGroup(out, g3)
|
||||
out = pc.appendGroup(out, g4)
|
||||
}
|
||||
|
||||
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.appendGroup(out, group&0x3F)
|
||||
}
|
||||
}
|
||||
|
||||
if pc.bitCount > 0 {
|
||||
group := byte(pc.bitBuf << (6 - pc.bitCount))
|
||||
pc.bitBuf = 0
|
||||
pc.bitCount = 0
|
||||
out = pc.appendGroup(out, group&0x3F)
|
||||
out = append(out, pc.padMarker)
|
||||
}
|
||||
|
||||
out = pc.maybeAddPadding(out)
|
||||
|
||||
if len(out) > 0 {
|
||||
pc.writeBuf = out[:0]
|
||||
return len(p), writeFull(pc.Conn, out)
|
||||
}
|
||||
pc.writeBuf = out[:0]
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
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.table.layout.groupByte(group&0x3F))
|
||||
out = append(out, pc.padMarker)
|
||||
}
|
||||
|
||||
out = pc.maybeAddPadding(out)
|
||||
|
||||
if len(out) > 0 {
|
||||
pc.writeBuf = out[:0]
|
||||
return writeFull(pc.Conn, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeFull(w io.Writer, b []byte) error {
|
||||
for len(b) > 0 {
|
||||
n, err := w.Write(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
b = b[n:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pc *PackedConn) Read(p []byte) (int, error) {
|
||||
if n, ok := drainPending(p, &pc.pendingData); ok {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
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.hintTable[b] {
|
||||
if b == padMarker {
|
||||
rBuf = 0
|
||||
rBits = 0
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
group, ok := layout.decodePackedGroup(b)
|
||||
if !ok {
|
||||
return 0, ErrInvalidSudokuMapMiss
|
||||
}
|
||||
|
||||
rBuf = (rBuf << 6) | uint64(group)
|
||||
rBits += 6
|
||||
|
||||
if rBits >= 8 {
|
||||
rBits -= 8
|
||||
val := byte(rBuf >> rBits)
|
||||
pc.pendingData.appendByte(val)
|
||||
if rBits == 0 {
|
||||
rBuf = 0
|
||||
} else {
|
||||
rBuf &= (uint64(1) << rBits) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pc.readBitBuf = rBuf
|
||||
pc.readBits = rBits
|
||||
}
|
||||
|
||||
if rErr != nil {
|
||||
if rErr == io.EOF {
|
||||
pc.readBitBuf = 0
|
||||
pc.readBits = 0
|
||||
}
|
||||
if pc.pendingData.available() > 0 {
|
||||
break
|
||||
}
|
||||
return 0, rErr
|
||||
}
|
||||
|
||||
if pc.pendingData.available() > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
n, _ := drainPending(p, &pc.pendingData)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (pc *PackedConn) getPaddingByte() byte {
|
||||
return pc.padPool[pc.rng.Intn(len(pc.padPool))]
|
||||
}
|
||||
42
transport/sudoku/obfs/sudoku/padding_prob.go
Normal file
42
transport/sudoku/obfs/sudoku/padding_prob.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package sudoku
|
||||
|
||||
const probOne = uint64(1) << 32
|
||||
|
||||
func pickPaddingThreshold(r randomSource, pMin, pMax int) uint64 {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
if pMin < 0 {
|
||||
pMin = 0
|
||||
}
|
||||
if pMax < pMin {
|
||||
pMax = pMin
|
||||
}
|
||||
if pMax > 100 {
|
||||
pMax = 100
|
||||
}
|
||||
if pMin > 100 {
|
||||
pMin = 100
|
||||
}
|
||||
|
||||
min := uint64(pMin) * probOne / 100
|
||||
max := uint64(pMax) * probOne / 100
|
||||
if max <= min {
|
||||
return min
|
||||
}
|
||||
u := uint64(r.Uint32())
|
||||
return min + (u * (max - min) >> 32)
|
||||
}
|
||||
|
||||
func shouldPad(r randomSource, threshold uint64) bool {
|
||||
if threshold == 0 {
|
||||
return false
|
||||
}
|
||||
if threshold >= probOne {
|
||||
return true
|
||||
}
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
return uint64(r.Uint32()) < threshold
|
||||
}
|
||||
57
transport/sudoku/obfs/sudoku/pending.go
Normal file
57
transport/sudoku/obfs/sudoku/pending.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package sudoku
|
||||
|
||||
type pendingBuffer struct {
|
||||
data []byte
|
||||
off int
|
||||
}
|
||||
|
||||
func newPendingBuffer(capacity int) pendingBuffer {
|
||||
return pendingBuffer{data: make([]byte, 0, capacity)}
|
||||
}
|
||||
|
||||
func (p *pendingBuffer) available() int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return len(p.data) - p.off
|
||||
}
|
||||
|
||||
func (p *pendingBuffer) reset() {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
p.data = p.data[:0]
|
||||
p.off = 0
|
||||
}
|
||||
|
||||
func (p *pendingBuffer) ensureAppendCapacity(extra int) {
|
||||
if p == nil || extra <= 0 || p.off == 0 {
|
||||
return
|
||||
}
|
||||
if cap(p.data)-len(p.data) >= extra {
|
||||
return
|
||||
}
|
||||
|
||||
unread := len(p.data) - p.off
|
||||
copy(p.data[:unread], p.data[p.off:])
|
||||
p.data = p.data[:unread]
|
||||
p.off = 0
|
||||
}
|
||||
|
||||
func (p *pendingBuffer) appendByte(b byte) {
|
||||
p.ensureAppendCapacity(1)
|
||||
p.data = append(p.data, b)
|
||||
}
|
||||
|
||||
func drainPending(dst []byte, pending *pendingBuffer) (int, bool) {
|
||||
if pending == nil || pending.available() == 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
n := copy(dst, pending.data[pending.off:])
|
||||
pending.off += n
|
||||
if pending.off == len(pending.data) {
|
||||
pending.reset()
|
||||
}
|
||||
return n, true
|
||||
}
|
||||
56
transport/sudoku/obfs/sudoku/rand.go
Normal file
56
transport/sudoku/obfs/sudoku/rand.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
crypto_rand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"time"
|
||||
)
|
||||
|
||||
type randomSource interface {
|
||||
Uint32() uint32
|
||||
Uint64() uint64
|
||||
Intn(n int) int
|
||||
}
|
||||
|
||||
type sudokuRand struct {
|
||||
state uint64
|
||||
}
|
||||
|
||||
func newSeededRand() *sudokuRand {
|
||||
seed := time.Now().UnixNano()
|
||||
var seedBytes [8]byte
|
||||
if _, err := crypto_rand.Read(seedBytes[:]); err == nil {
|
||||
seed = int64(binary.BigEndian.Uint64(seedBytes[:]))
|
||||
}
|
||||
return newSudokuRand(seed)
|
||||
}
|
||||
|
||||
func newSudokuRand(seed int64) *sudokuRand {
|
||||
state := uint64(seed)
|
||||
if state == 0 {
|
||||
state = 0x9e3779b97f4a7c15
|
||||
}
|
||||
return &sudokuRand{state: state}
|
||||
}
|
||||
|
||||
func (r *sudokuRand) Uint64() uint64 {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
r.state += 0x9e3779b97f4a7c15
|
||||
z := r.state
|
||||
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9
|
||||
z = (z ^ (z >> 27)) * 0x94d049bb133111eb
|
||||
return z ^ (z >> 31)
|
||||
}
|
||||
|
||||
func (r *sudokuRand) Uint32() uint32 {
|
||||
return uint32(r.Uint64() >> 32)
|
||||
}
|
||||
|
||||
func (r *sudokuRand) Intn(n int) int {
|
||||
if n <= 1 {
|
||||
return 0
|
||||
}
|
||||
return int((uint64(r.Uint32()) * uint64(n)) >> 32)
|
||||
}
|
||||
214
transport/sudoku/obfs/sudoku/table.go
Normal file
214
transport/sudoku/obfs/sudoku/table.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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
|
||||
opposite *Table
|
||||
hint uint32
|
||||
}
|
||||
|
||||
// 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 {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// NewTableWithCustom initializes the uplink/probe Sudoku table using either predefined
|
||||
// or directional layouts. Directional modes such as "up_ascii_down_entropy" return the
|
||||
// client->server table and internally attach the opposite direction table for runtime use.
|
||||
// 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) {
|
||||
asciiMode, err := ParseASCIIMode(mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uplinkPattern := customPatternForToken(asciiMode.Uplink, customPattern)
|
||||
downlinkPattern := customPatternForToken(asciiMode.Downlink, customPattern)
|
||||
hint := tableHintFingerprint(key, asciiMode.Canonical(), uplinkPattern, downlinkPattern)
|
||||
|
||||
uplink, err := newSingleDirectionTable(key, asciiMode.uplinkPreference(), uplinkPattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uplink.hint = hint
|
||||
if asciiMode.Uplink == asciiMode.Downlink {
|
||||
uplink.opposite = uplink
|
||||
return uplink, nil
|
||||
}
|
||||
|
||||
downlink, err := newSingleDirectionTable(key, asciiMode.downlinkPreference(), downlinkPattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
downlink.hint = hint
|
||||
uplink.opposite = downlink
|
||||
downlink.opposite = uplink
|
||||
return uplink, nil
|
||||
}
|
||||
|
||||
func newSingleDirectionTable(key string, mode string, customPattern string) (*Table, error) {
|
||||
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.hintByte(p.val-1, p.pos)
|
||||
}
|
||||
|
||||
t.EncodeTable[byteVal] = append(t.EncodeTable[byteVal], currentHints)
|
||||
// 生成解码键 (需要对 Hints 进行排序以忽略传输顺序)
|
||||
key := packHintsToKey(currentHints)
|
||||
t.DecodeMap[key] = byte(byteVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func customPatternForToken(token string, customPattern string) string {
|
||||
if token == asciiModeTokenEntropy {
|
||||
return customPattern
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *Table) OppositeDirection() *Table {
|
||||
if t == nil || t.opposite == nil {
|
||||
return t
|
||||
}
|
||||
return t.opposite
|
||||
}
|
||||
|
||||
func (t *Table) Hint() uint32 {
|
||||
if t == nil {
|
||||
return 0
|
||||
}
|
||||
return t.hint
|
||||
}
|
||||
|
||||
func tableHintFingerprint(key string, mode string, uplinkPattern string, downlinkPattern string) uint32 {
|
||||
sum := sha256.Sum256([]byte(strings.Join([]string{
|
||||
"sudoku-table-hint",
|
||||
key,
|
||||
mode,
|
||||
strings.ToLower(strings.TrimSpace(uplinkPattern)),
|
||||
strings.ToLower(strings.TrimSpace(downlinkPattern)),
|
||||
}, "\x00")))
|
||||
return binary.BigEndian.Uint32(sum[:4])
|
||||
}
|
||||
|
||||
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
transport/sudoku/obfs/sudoku/table_set.go
Normal file
38
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
|
||||
}
|
||||
Reference in New Issue
Block a user