mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-07-17 13:21:06 +03:00
Add Snell protocol. Refactor MASQUE HTTP/2, Fair Queue. Update XHTTP, OpenVPN, Sudoku, Fallback. Fixes
This commit is contained in:
@@ -18,9 +18,12 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
headerSize = 1 + 4 + 4
|
||||
maxFrameSize = 256 * 1024
|
||||
maxDataPayload = 32 * 1024
|
||||
headerSize = 1 + 4 + 4
|
||||
// maxQueuedBytesPerStream bounds unread payload retained by a single logical stream.
|
||||
// Backpressure is applied to the demux loop instead of dropping data.
|
||||
maxQueuedBytesPerStream = 4 * 1024 * 1024
|
||||
maxFrameSize = 256 * 1024
|
||||
maxDataPayload = 128 * 1024
|
||||
)
|
||||
|
||||
type acceptEvent struct {
|
||||
@@ -344,6 +347,8 @@ type stream struct {
|
||||
closeErr error
|
||||
readBuf []byte
|
||||
queue [][]byte
|
||||
// queuedBytes includes unread bytes in readBuf and queue.
|
||||
queuedBytes int
|
||||
|
||||
localAddr net.Addr
|
||||
remoteAddr net.Addr
|
||||
@@ -362,16 +367,20 @@ func newStream(session *Session, id uint32) *stream {
|
||||
|
||||
func (c *stream) enqueue(payload []byte) {
|
||||
c.mu.Lock()
|
||||
for !c.closed && c.queuedBytes+len(payload) > maxQueuedBytesPerStream {
|
||||
c.cond.Wait()
|
||||
}
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.queuedBytes += len(payload)
|
||||
if len(c.readBuf) == 0 && len(c.queue) == 0 {
|
||||
c.readBuf = payload
|
||||
} else {
|
||||
c.queue = append(c.queue, payload)
|
||||
}
|
||||
c.cond.Signal()
|
||||
c.cond.Broadcast()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -413,7 +422,11 @@ func (c *stream) Read(p []byte) (int, error) {
|
||||
}
|
||||
if len(c.readBuf) == 0 && len(c.queue) > 0 {
|
||||
c.readBuf = c.queue[0]
|
||||
c.queue[0] = nil
|
||||
c.queue = c.queue[1:]
|
||||
if len(c.queue) == 0 {
|
||||
c.queue = nil
|
||||
}
|
||||
}
|
||||
if len(c.readBuf) == 0 && c.closed {
|
||||
if c.closeErr == nil {
|
||||
@@ -424,6 +437,14 @@ func (c *stream) Read(p []byte) (int, error) {
|
||||
|
||||
n := copy(p, c.readBuf)
|
||||
c.readBuf = c.readBuf[n:]
|
||||
if len(c.readBuf) == 0 {
|
||||
c.readBuf = nil
|
||||
}
|
||||
c.queuedBytes -= n
|
||||
if c.queuedBytes < 0 {
|
||||
c.queuedBytes = 0
|
||||
}
|
||||
c.cond.Broadcast()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
|
||||
91
transport/sudoku/multiplex/session_backpressure_test.go
Normal file
91
transport/sudoku/multiplex/session_backpressure_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package multiplex
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestSession_LargeTransferBackpressure verifies that a transfer larger than
|
||||
// maxQueuedBytesPerStream completes correctly: the demux loop applies
|
||||
// backpressure (cond.Wait) instead of dropping data, and the reader draining
|
||||
// the stream wakes the blocked loop without deadlock.
|
||||
func TestSession_LargeTransferBackpressure(t *testing.T) {
|
||||
c1, c2 := net.Pipe()
|
||||
|
||||
client, err := NewClientSession(c1)
|
||||
if err != nil {
|
||||
t.Fatalf("client session: %v", err)
|
||||
}
|
||||
server, err := NewServerSession(c2)
|
||||
if err != nil {
|
||||
t.Fatalf("server session: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
// Payload bigger than the per-stream backpressure window (4MB).
|
||||
const total = 12 * 1024 * 1024
|
||||
payload := make([]byte, total)
|
||||
if _, err := rand.Read(payload); err != nil {
|
||||
t.Fatalf("rand: %v", err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
var writeErr error
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
stream, err := client.OpenStream([]byte("hello"))
|
||||
if err != nil {
|
||||
writeErr = err
|
||||
return
|
||||
}
|
||||
defer stream.Close()
|
||||
if _, err := stream.Write(payload); err != nil {
|
||||
writeErr = err
|
||||
return
|
||||
}
|
||||
_ = stream.(interface{ CloseWrite() error }).CloseWrite()
|
||||
}()
|
||||
|
||||
var got []byte
|
||||
var readErr error
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
stream, openPayload, err := server.AcceptStream()
|
||||
if err != nil {
|
||||
readErr = err
|
||||
return
|
||||
}
|
||||
if string(openPayload) != "hello" {
|
||||
readErr = io.ErrUnexpectedEOF
|
||||
return
|
||||
}
|
||||
got, readErr = io.ReadAll(stream)
|
||||
}()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() { wg.Wait(); close(done) }()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(30 * time.Second):
|
||||
t.Fatal("transfer deadlocked (backpressure did not release)")
|
||||
}
|
||||
|
||||
if writeErr != nil {
|
||||
t.Fatalf("write: %v", writeErr)
|
||||
}
|
||||
if readErr != nil {
|
||||
t.Fatalf("read: %v", readErr)
|
||||
}
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatalf("payload mismatch: got %d bytes, want %d", len(got), len(payload))
|
||||
}
|
||||
}
|
||||
56
transport/sudoku/obfs/sudoku/ascii_mode_test.go
Normal file
56
transport/sudoku/obfs/sudoku/ascii_mode_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package sudoku
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeASCIIMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"", "prefer_entropy"},
|
||||
{"entropy", "prefer_entropy"},
|
||||
{"prefer_ascii", "prefer_ascii"},
|
||||
{"up_ascii_down_entropy", "up_ascii_down_entropy"},
|
||||
{"up_entropy_down_ascii", "up_entropy_down_ascii"},
|
||||
{"up_prefer_ascii_down_prefer_entropy", "up_ascii_down_entropy"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got, err := NormalizeASCIIMode(tt.in)
|
||||
if err != nil {
|
||||
t.Fatalf("NormalizeASCIIMode(%q): %v", tt.in, err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("NormalizeASCIIMode(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := NormalizeASCIIMode("up_ascii_down_binary"); err == nil {
|
||||
t.Fatalf("expected invalid directional mode to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTableWithCustomDirectionalOpposite(t *testing.T) {
|
||||
table, err := NewTableWithCustom("seed", "up_ascii_down_entropy", "xpxvvpvv")
|
||||
if err != nil {
|
||||
t.Fatalf("NewTableWithCustom: %v", err)
|
||||
}
|
||||
if !table.IsASCII {
|
||||
t.Fatalf("uplink table should be ascii")
|
||||
}
|
||||
opposite := table.OppositeDirection()
|
||||
if opposite == nil || opposite == table {
|
||||
t.Fatalf("expected distinct opposite table")
|
||||
}
|
||||
if opposite.IsASCII {
|
||||
t.Fatalf("downlink table should be entropy/custom")
|
||||
}
|
||||
|
||||
symmetric, err := NewTableWithCustom("seed", "prefer_ascii", "xpxvvpvv")
|
||||
if err != nil {
|
||||
t.Fatalf("NewTableWithCustom symmetric: %v", err)
|
||||
}
|
||||
if symmetric.OppositeDirection() != symmetric {
|
||||
t.Fatalf("symmetric table should point to itself")
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package sudoku
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
|
||||
const IOBufferSize = 32 * 1024
|
||||
|
||||
const minDecodeReadSize = 64
|
||||
|
||||
var perm4 = [24][4]byte{
|
||||
{0, 1, 2, 3},
|
||||
{0, 1, 3, 2},
|
||||
@@ -52,7 +55,7 @@ type Conn struct {
|
||||
writeMu sync.Mutex
|
||||
writeBuf []byte
|
||||
|
||||
rng randomSource
|
||||
rng *sudokuRand
|
||||
paddingThreshold uint64
|
||||
}
|
||||
|
||||
@@ -97,6 +100,9 @@ func NewConn(c net.Conn, table *Table, pMin, pMax int, record bool) *Conn {
|
||||
}
|
||||
|
||||
func (sc *Conn) StopRecording() {
|
||||
if sc == nil {
|
||||
return
|
||||
}
|
||||
sc.recordLock.Lock()
|
||||
sc.recording.Store(false)
|
||||
sc.recorder = nil
|
||||
@@ -115,6 +121,9 @@ func (sc *Conn) GetBufferedAndRecorded() []byte {
|
||||
if sc.recorder != nil {
|
||||
recorded = sc.recorder.Bytes()
|
||||
}
|
||||
if sc.reader == nil {
|
||||
return recorded
|
||||
}
|
||||
|
||||
buffered := sc.reader.Buffered()
|
||||
if buffered > 0 {
|
||||
@@ -131,6 +140,9 @@ func (sc *Conn) Write(p []byte) (n int, err error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if sc == nil || sc.Conn == nil || sc.table == nil || sc.table.layout == nil || sc.rng == nil {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
sc.writeMu.Lock()
|
||||
defer sc.writeMu.Unlock()
|
||||
@@ -140,16 +152,19 @@ func (sc *Conn) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (sc *Conn) Read(p []byte) (n int, err error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if sc == nil || sc.Conn == nil || sc.reader == nil || len(sc.rawBuf) == 0 || sc.table == nil || sc.table.layout == nil {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
if n, ok := drainPending(p, &sc.pendingData); ok {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
outN := 0
|
||||
for {
|
||||
if sc.pendingData.available() > 0 {
|
||||
break
|
||||
}
|
||||
|
||||
nr, rErr := sc.reader.Read(sc.rawBuf)
|
||||
nr, rErr := readRawLimited(sc.Conn, sc.reader, sc.rawBuf[:sudokuReadSize(len(p)-outN, len(sc.rawBuf))])
|
||||
if nr > 0 {
|
||||
chunk := sc.rawBuf[:nr]
|
||||
if sc.recording.Load() {
|
||||
@@ -160,34 +175,80 @@ func (sc *Conn) Read(p []byte) (n int, err error) {
|
||||
sc.recordLock.Unlock()
|
||||
}
|
||||
|
||||
layout := sc.table.layout
|
||||
for _, b := range chunk {
|
||||
table := sc.table
|
||||
layout := table.layout
|
||||
for i := 0; i < len(chunk); {
|
||||
if sc.hintCount == 0 && outN < len(p) && i+3 < len(chunk) &&
|
||||
layout.hintTable[chunk[i]] &&
|
||||
layout.hintTable[chunk[i+1]] &&
|
||||
layout.hintTable[chunk[i+2]] &&
|
||||
layout.hintTable[chunk[i+3]] {
|
||||
val, ok := table.DecodeMap[packHintBytes(chunk[i], chunk[i+1], chunk[i+2], chunk[i+3])]
|
||||
if !ok {
|
||||
return 0, ErrInvalidSudokuMapMiss
|
||||
}
|
||||
p[outN] = val
|
||||
outN++
|
||||
i += 4
|
||||
continue
|
||||
}
|
||||
|
||||
b := chunk[i]
|
||||
i++
|
||||
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 sc.hintCount != len(sc.hintBuf) {
|
||||
continue
|
||||
}
|
||||
|
||||
val, ok := table.DecodeMap[packHintBytes(sc.hintBuf[0], sc.hintBuf[1], sc.hintBuf[2], sc.hintBuf[3])]
|
||||
if !ok {
|
||||
return 0, ErrInvalidSudokuMapMiss
|
||||
}
|
||||
outN = appendDecodedByte(p, outN, &sc.pendingData, val)
|
||||
sc.hintCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
if rErr != nil {
|
||||
if outN > 0 {
|
||||
return outN, nil
|
||||
}
|
||||
if n, ok := drainPending(p, &sc.pendingData); ok {
|
||||
return n, nil
|
||||
}
|
||||
return 0, rErr
|
||||
}
|
||||
if sc.pendingData.available() > 0 {
|
||||
break
|
||||
if outN > 0 {
|
||||
return outN, nil
|
||||
}
|
||||
}
|
||||
|
||||
n, _ = drainPending(p, &sc.pendingData)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func sudokuReadSize(decodedRemaining, maxRaw int) int {
|
||||
if maxRaw <= minDecodeReadSize || decodedRemaining <= 0 {
|
||||
return maxRaw
|
||||
}
|
||||
if decodedRemaining > (maxRaw-minDecodeReadSize)/5 {
|
||||
return maxRaw
|
||||
}
|
||||
|
||||
return decodedRemaining*5 + minDecodeReadSize
|
||||
}
|
||||
|
||||
func readRawLimited(conn net.Conn, reader *bufio.Reader, dst []byte) (int, error) {
|
||||
if len(dst) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if reader != nil && reader.Buffered() > 0 {
|
||||
return reader.Read(dst)
|
||||
}
|
||||
if conn == nil {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
return conn.Read(dst)
|
||||
}
|
||||
|
||||
51
transport/sudoku/obfs/sudoku/conn_roundtrip_test.go
Normal file
51
transport/sudoku/obfs/sudoku/conn_roundtrip_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestConn_Roundtrip exercises the optimized Conn encode/decode hot paths:
|
||||
// the no-padding fast path (pMin==pMax==0), the always-padding path
|
||||
// (pMin==pMax==100), a probabilistic range, and the adaptive read-size /
|
||||
// 4-byte fast hint decode path across a variety of payload sizes and modes.
|
||||
func TestConn_Roundtrip(t *testing.T) {
|
||||
modes := []string{"prefer_entropy", "prefer_ascii"}
|
||||
paddings := []struct{ min, max int }{
|
||||
{0, 0}, // no-padding specialized path
|
||||
{100, 100}, // always-padding specialized path
|
||||
{20, 60}, // probabilistic path
|
||||
}
|
||||
sizes := []int{1, 3, 4, 7, 16, 100, 1000, 64 * 1024}
|
||||
|
||||
for _, mode := range modes {
|
||||
for _, pad := range paddings {
|
||||
for _, size := range sizes {
|
||||
payload := make([]byte, size)
|
||||
for i := range payload {
|
||||
payload[i] = byte(i*31 + 7)
|
||||
}
|
||||
|
||||
table := NewTable("conn-roundtrip-seed", mode)
|
||||
|
||||
// Encode via Conn.Write.
|
||||
w := &mockConn{}
|
||||
enc := NewConn(w, table, pad.min, pad.max, false)
|
||||
if _, err := enc.Write(payload); err != nil {
|
||||
t.Fatalf("mode=%s pad=%v size=%d write: %v", mode, pad, size, err)
|
||||
}
|
||||
|
||||
// Decode via Conn.Read using the same table.
|
||||
dec := NewConn(&mockConn{readBuf: w.writeBuf}, table, pad.min, pad.max, false)
|
||||
got := make([]byte, size)
|
||||
if _, err := io.ReadFull(dec, got); err != nil {
|
||||
t.Fatalf("mode=%s pad=%v size=%d read: %v", mode, pad, size, err)
|
||||
}
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatalf("mode=%s pad=%v size=%d roundtrip mismatch", mode, pad, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
package sudoku
|
||||
|
||||
func encodeSudokuPayload(dst []byte, table *Table, rng randomSource, paddingThreshold uint64, p []byte) []byte {
|
||||
func encodeSudokuPayload(dst []byte, table *Table, rng *sudokuRand, paddingThreshold uint64, p []byte) []byte {
|
||||
if len(p) == 0 {
|
||||
return dst[:0]
|
||||
}
|
||||
if paddingThreshold == 0 {
|
||||
return encodeSudokuPayloadNoPadding(dst, table, rng, p)
|
||||
}
|
||||
|
||||
outCapacity := len(p)*6 + 1
|
||||
if cap(dst) < outCapacity {
|
||||
@@ -13,8 +16,25 @@ func encodeSudokuPayload(dst []byte, table *Table, rng randomSource, paddingThre
|
||||
pads := table.PaddingPool
|
||||
padLen := len(pads)
|
||||
|
||||
if paddingThreshold >= probOne {
|
||||
for _, b := range p {
|
||||
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 {
|
||||
out = append(out, pads[rng.Intn(padLen)], puzzle[idx])
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, pads[rng.Intn(padLen)])
|
||||
return out
|
||||
}
|
||||
|
||||
for _, b := range p {
|
||||
if shouldPad(rng, paddingThreshold) {
|
||||
if uint64(rng.Uint32()) < paddingThreshold {
|
||||
out = append(out, pads[rng.Intn(padLen)])
|
||||
}
|
||||
|
||||
@@ -22,15 +42,31 @@ func encodeSudokuPayload(dst []byte, table *Table, rng randomSource, paddingThre
|
||||
puzzle := puzzles[rng.Intn(len(puzzles))]
|
||||
perm := perm4[rng.Intn(len(perm4))]
|
||||
for _, idx := range perm {
|
||||
if shouldPad(rng, paddingThreshold) {
|
||||
if uint64(rng.Uint32()) < paddingThreshold {
|
||||
out = append(out, pads[rng.Intn(padLen)])
|
||||
}
|
||||
out = append(out, puzzle[idx])
|
||||
}
|
||||
}
|
||||
|
||||
if shouldPad(rng, paddingThreshold) {
|
||||
if uint64(rng.Uint32()) < paddingThreshold {
|
||||
out = append(out, pads[rng.Intn(padLen)])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func encodeSudokuPayloadNoPadding(dst []byte, table *Table, rng *sudokuRand, p []byte) []byte {
|
||||
outCapacity := len(p) * 4
|
||||
if cap(dst) < outCapacity {
|
||||
dst = make([]byte, 0, outCapacity)
|
||||
}
|
||||
out := dst[:0]
|
||||
|
||||
for _, b := range p {
|
||||
puzzles := table.EncodeTable[b]
|
||||
puzzle := puzzles[rng.Intn(len(puzzles))]
|
||||
perm := perm4[rng.Intn(len(perm4))]
|
||||
out = append(out, puzzle[perm[0]], puzzle[perm[1]], puzzle[perm[2]], puzzle[perm[3]])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
RngBatchSize = 128
|
||||
|
||||
packedProtectedPrefixBytes = 14
|
||||
packedIOBufferSize = 64 * 1024
|
||||
packedDecodeBufferSize = 96 * 1024
|
||||
)
|
||||
|
||||
// PackedConn encodes traffic with the packed Sudoku layout while preserving
|
||||
@@ -35,7 +35,7 @@ type PackedConn struct {
|
||||
readBits int
|
||||
|
||||
// Padding selection matches Conn's threshold-based model.
|
||||
rng randomSource
|
||||
rng *sudokuRand
|
||||
paddingThreshold uint64
|
||||
padMarker byte
|
||||
padPool []byte
|
||||
@@ -67,18 +67,20 @@ func NewPackedConn(c net.Conn, table *Table, pMin, pMax int) *PackedConn {
|
||||
pc := &PackedConn{
|
||||
Conn: c,
|
||||
table: table,
|
||||
reader: bufio.NewReaderSize(c, IOBufferSize),
|
||||
rawBuf: make([]byte, IOBufferSize),
|
||||
reader: bufio.NewReaderSize(c, packedIOBufferSize),
|
||||
rawBuf: make([]byte, packedDecodeBufferSize),
|
||||
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 table != nil && table.layout != nil {
|
||||
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 {
|
||||
@@ -87,18 +89,6 @@ func NewPackedConn(c net.Conn, table *Table, pMin, pMax int) *PackedConn {
|
||||
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())
|
||||
}
|
||||
@@ -134,7 +124,7 @@ func (pc *PackedConn) writeProtectedPrefix(out []byte, p []byte) ([]byte, int) {
|
||||
} else {
|
||||
pc.bitBuf &= (1 << pc.bitCount) - 1
|
||||
}
|
||||
out = pc.appendGroup(out, group&0x3F)
|
||||
out = appendPackedGroup(out, pc.table.layout, pc.rng, pc.paddingThreshold, pc.padPool, group)
|
||||
}
|
||||
|
||||
effective++
|
||||
@@ -148,19 +138,49 @@ func (pc *PackedConn) writeProtectedPrefix(out []byte, p []byte) ([]byte, int) {
|
||||
return out, limit
|
||||
}
|
||||
|
||||
func appendPackedGroup(out []byte, layout *byteLayout, rng *sudokuRand, paddingThreshold uint64, padPool []byte, group byte) []byte {
|
||||
if paddingThreshold != 0 {
|
||||
u := rng.Uint32()
|
||||
if uint64(u) < paddingThreshold {
|
||||
out = append(out, padPool[fastIntnFromUint32(rng.Uint32(), len(padPool))])
|
||||
}
|
||||
}
|
||||
return append(out, layout.encodeGroup[group&0x3F])
|
||||
}
|
||||
|
||||
func maybeAppendPackedPadding(out []byte, rng *sudokuRand, paddingThreshold uint64, padPool []byte) []byte {
|
||||
if paddingThreshold != 0 {
|
||||
u := rng.Uint32()
|
||||
if uint64(u) < paddingThreshold {
|
||||
out = append(out, padPool[fastIntnFromUint32(rng.Uint32(), len(padPool))])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (pc *PackedConn) Write(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if pc == nil || pc.Conn == nil || pc.table == nil || pc.table.layout == nil || pc.rng == nil || len(pc.padPool) == 0 {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
pc.writeMu.Lock()
|
||||
defer pc.writeMu.Unlock()
|
||||
|
||||
needed := len(p)*3/2 + 32
|
||||
if pc.paddingThreshold == 0 {
|
||||
needed = ((len(p)+2)/3)*4 + 32
|
||||
}
|
||||
if cap(pc.writeBuf) < needed {
|
||||
pc.writeBuf = make([]byte, 0, needed)
|
||||
}
|
||||
out := pc.writeBuf[:0]
|
||||
layout := pc.table.layout
|
||||
rng := pc.rng
|
||||
paddingThreshold := pc.paddingThreshold
|
||||
padPool := pc.padPool
|
||||
|
||||
var prefixN int
|
||||
out, prefixN = pc.writeProtectedPrefix(out, p)
|
||||
@@ -181,7 +201,7 @@ func (pc *PackedConn) Write(p []byte) (int, error) {
|
||||
} else {
|
||||
pc.bitBuf &= (1 << pc.bitCount) - 1
|
||||
}
|
||||
out = pc.appendGroup(out, group&0x3F)
|
||||
out = appendPackedGroup(out, layout, rng, paddingThreshold, padPool, group)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,10 +215,10 @@ func (pc *PackedConn) Write(p []byte) (int, error) {
|
||||
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)
|
||||
out = appendPackedGroup(out, layout, rng, paddingThreshold, padPool, g1)
|
||||
out = appendPackedGroup(out, layout, rng, paddingThreshold, padPool, g2)
|
||||
out = appendPackedGroup(out, layout, rng, paddingThreshold, padPool, g3)
|
||||
out = appendPackedGroup(out, layout, rng, paddingThreshold, padPool, g4)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,10 +231,10 @@ func (pc *PackedConn) Write(p []byte) (int, error) {
|
||||
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)
|
||||
out = appendPackedGroup(out, layout, rng, paddingThreshold, padPool, g1)
|
||||
out = appendPackedGroup(out, layout, rng, paddingThreshold, padPool, g2)
|
||||
out = appendPackedGroup(out, layout, rng, paddingThreshold, padPool, g3)
|
||||
out = appendPackedGroup(out, layout, rng, paddingThreshold, padPool, g4)
|
||||
}
|
||||
|
||||
for ; i < n; i++ {
|
||||
@@ -229,7 +249,7 @@ func (pc *PackedConn) Write(p []byte) (int, error) {
|
||||
} else {
|
||||
pc.bitBuf &= (1 << pc.bitCount) - 1
|
||||
}
|
||||
out = pc.appendGroup(out, group&0x3F)
|
||||
out = appendPackedGroup(out, layout, rng, paddingThreshold, padPool, group)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,11 +257,11 @@ func (pc *PackedConn) Write(p []byte) (int, error) {
|
||||
group := byte(pc.bitBuf << (6 - pc.bitCount))
|
||||
pc.bitBuf = 0
|
||||
pc.bitCount = 0
|
||||
out = pc.appendGroup(out, group&0x3F)
|
||||
out = appendPackedGroup(out, layout, rng, paddingThreshold, padPool, group)
|
||||
out = append(out, pc.padMarker)
|
||||
}
|
||||
|
||||
out = pc.maybeAddPadding(out)
|
||||
out = maybeAppendPackedPadding(out, rng, paddingThreshold, padPool)
|
||||
|
||||
if len(out) > 0 {
|
||||
pc.writeBuf = out[:0]
|
||||
@@ -252,6 +272,10 @@ func (pc *PackedConn) Write(p []byte) (int, error) {
|
||||
}
|
||||
|
||||
func (pc *PackedConn) Flush() error {
|
||||
if pc == nil || pc.Conn == nil || pc.table == nil || pc.table.layout == nil || pc.rng == nil || len(pc.padPool) == 0 {
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
|
||||
pc.writeMu.Lock()
|
||||
defer pc.writeMu.Unlock()
|
||||
|
||||
@@ -265,7 +289,7 @@ func (pc *PackedConn) Flush() error {
|
||||
out = append(out, pc.padMarker)
|
||||
}
|
||||
|
||||
out = pc.maybeAddPadding(out)
|
||||
out = maybeAppendPackedPadding(out, pc.rng, pc.paddingThreshold, pc.padPool)
|
||||
|
||||
if len(out) > 0 {
|
||||
pc.writeBuf = out[:0]
|
||||
@@ -289,19 +313,44 @@ func writeFull(w io.Writer, b []byte) error {
|
||||
}
|
||||
|
||||
func (pc *PackedConn) Read(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if pc == nil || pc.Conn == nil || pc.reader == nil || len(pc.rawBuf) == 0 || pc.table == nil || pc.table.layout == nil {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
if n, ok := drainPending(p, &pc.pendingData); ok {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
outN := 0
|
||||
for {
|
||||
nr, rErr := pc.reader.Read(pc.rawBuf)
|
||||
nr, rErr := readRawLimited(pc.Conn, pc.reader, pc.rawBuf[:packedReadSize(len(p)-outN, len(pc.rawBuf))])
|
||||
if nr > 0 {
|
||||
rBuf := pc.readBitBuf
|
||||
rBits := pc.readBits
|
||||
padMarker := pc.padMarker
|
||||
layout := pc.table.layout
|
||||
|
||||
for _, b := range pc.rawBuf[:nr] {
|
||||
chunk := pc.rawBuf[:nr]
|
||||
for i := 0; i < len(chunk); {
|
||||
if rBits == 0 && outN+3 <= len(p) && i+3 < len(chunk) &&
|
||||
layout.hintTable[chunk[i]] && layout.hintTable[chunk[i+1]] &&
|
||||
layout.hintTable[chunk[i+2]] && layout.hintTable[chunk[i+3]] {
|
||||
g1 := layout.decodeGroup[chunk[i]]
|
||||
g2 := layout.decodeGroup[chunk[i+1]]
|
||||
g3 := layout.decodeGroup[chunk[i+2]]
|
||||
g4 := layout.decodeGroup[chunk[i+3]]
|
||||
p[outN] = (g1 << 2) | (g2 >> 4)
|
||||
p[outN+1] = (g2 << 4) | (g3 >> 2)
|
||||
p[outN+2] = (g3 << 6) | g4
|
||||
outN += 3
|
||||
i += 4
|
||||
continue
|
||||
}
|
||||
|
||||
b := chunk[i]
|
||||
i++
|
||||
if !layout.hintTable[b] {
|
||||
if b == padMarker {
|
||||
rBuf = 0
|
||||
@@ -321,7 +370,7 @@ func (pc *PackedConn) Read(p []byte) (int, error) {
|
||||
if rBits >= 8 {
|
||||
rBits -= 8
|
||||
val := byte(rBuf >> rBits)
|
||||
pc.pendingData.appendByte(val)
|
||||
outN = appendDecodedByte(p, outN, &pc.pendingData, val)
|
||||
if rBits == 0 {
|
||||
rBuf = 0
|
||||
} else {
|
||||
@@ -339,21 +388,32 @@ func (pc *PackedConn) Read(p []byte) (int, error) {
|
||||
pc.readBitBuf = 0
|
||||
pc.readBits = 0
|
||||
}
|
||||
if pc.pendingData.available() > 0 {
|
||||
break
|
||||
if outN > 0 {
|
||||
return outN, nil
|
||||
}
|
||||
if n, ok := drainPending(p, &pc.pendingData); ok {
|
||||
return n, nil
|
||||
}
|
||||
return 0, rErr
|
||||
}
|
||||
|
||||
if pc.pendingData.available() > 0 {
|
||||
break
|
||||
if outN > 0 {
|
||||
return outN, nil
|
||||
}
|
||||
}
|
||||
|
||||
n, _ := drainPending(p, &pc.pendingData)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (pc *PackedConn) getPaddingByte() byte {
|
||||
return pc.padPool[pc.rng.Intn(len(pc.padPool))]
|
||||
}
|
||||
|
||||
func packedReadSize(decodedRemaining, maxRaw int) int {
|
||||
if maxRaw <= minDecodeReadSize || decodedRemaining <= 0 {
|
||||
return maxRaw
|
||||
}
|
||||
if decodedRemaining > (maxRaw-minDecodeReadSize)/2 {
|
||||
return maxRaw
|
||||
}
|
||||
|
||||
return decodedRemaining*2 + minDecodeReadSize
|
||||
}
|
||||
|
||||
90
transport/sudoku/obfs/sudoku/packed_prefix_test.go
Normal file
90
transport/sudoku/obfs/sudoku/packed_prefix_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type mockConn struct {
|
||||
readBuf []byte
|
||||
writeBuf []byte
|
||||
}
|
||||
|
||||
func (c *mockConn) Read(p []byte) (int, error) {
|
||||
if len(c.readBuf) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(p, c.readBuf)
|
||||
c.readBuf = c.readBuf[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *mockConn) Write(p []byte) (int, error) {
|
||||
c.writeBuf = append(c.writeBuf, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (c *mockConn) Close() error { return nil }
|
||||
func (c *mockConn) LocalAddr() net.Addr { return nil }
|
||||
func (c *mockConn) RemoteAddr() net.Addr { return nil }
|
||||
func (c *mockConn) SetDeadline(time.Time) error { return nil }
|
||||
func (c *mockConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (c *mockConn) SetWriteDeadline(time.Time) error { return nil }
|
||||
|
||||
func TestPackedConn_ProtectedPrefixPadding(t *testing.T) {
|
||||
table := NewTable("packed-prefix-seed", "prefer_ascii")
|
||||
mock := &mockConn{}
|
||||
writer := NewPackedConn(mock, table, 0, 0)
|
||||
writer.rng = newSudokuRand(1)
|
||||
|
||||
payload := bytes.Repeat([]byte{0}, 32)
|
||||
if _, err := writer.Write(payload); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
|
||||
wire := append([]byte(nil), mock.writeBuf...)
|
||||
if len(wire) < 20 {
|
||||
t.Fatalf("wire too short: %d", len(wire))
|
||||
}
|
||||
|
||||
firstHint := -1
|
||||
nonHintCount := 0
|
||||
maxHintRun := 0
|
||||
currentHintRun := 0
|
||||
for i, b := range wire[:20] {
|
||||
if table.layout.isHint(b) {
|
||||
if firstHint == -1 {
|
||||
firstHint = i
|
||||
}
|
||||
currentHintRun++
|
||||
if currentHintRun > maxHintRun {
|
||||
maxHintRun = currentHintRun
|
||||
}
|
||||
continue
|
||||
}
|
||||
nonHintCount++
|
||||
currentHintRun = 0
|
||||
}
|
||||
|
||||
if firstHint < 1 || firstHint > 2 {
|
||||
t.Fatalf("expected 1-2 leading padding bytes, first hint index=%d", firstHint)
|
||||
}
|
||||
if nonHintCount < 6 {
|
||||
t.Fatalf("expected dense prefix padding, got only %d non-hint bytes in first 20", nonHintCount)
|
||||
}
|
||||
if maxHintRun > 3 {
|
||||
t.Fatalf("prefix still exposes long hint run: %d", maxHintRun)
|
||||
}
|
||||
|
||||
reader := NewPackedConn(&mockConn{readBuf: wire}, table, 0, 0)
|
||||
decoded := make([]byte, len(payload))
|
||||
if _, err := io.ReadFull(reader, decoded); err != nil {
|
||||
t.Fatalf("read back: %v", err)
|
||||
}
|
||||
if !bytes.Equal(decoded, payload) {
|
||||
t.Fatalf("roundtrip mismatch")
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package sudoku
|
||||
|
||||
const probOne = uint64(1) << 32
|
||||
|
||||
func pickPaddingThreshold(r randomSource, pMin, pMax int) uint64 {
|
||||
func pickPaddingThreshold(r *sudokuRand, pMin, pMax int) uint64 {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
@@ -28,7 +28,7 @@ func pickPaddingThreshold(r randomSource, pMin, pMax int) uint64 {
|
||||
return min + (u * (max - min) >> 32)
|
||||
}
|
||||
|
||||
func shouldPad(r randomSource, threshold uint64) bool {
|
||||
func shouldPad(r *sudokuRand, threshold uint64) bool {
|
||||
if threshold == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -25,7 +25,10 @@ func (p *pendingBuffer) reset() {
|
||||
}
|
||||
|
||||
func (p *pendingBuffer) ensureAppendCapacity(extra int) {
|
||||
if p == nil || extra <= 0 || p.off == 0 {
|
||||
if p == nil || extra <= 0 {
|
||||
return
|
||||
}
|
||||
if p.off == 0 {
|
||||
return
|
||||
}
|
||||
if cap(p.data)-len(p.data) >= extra {
|
||||
@@ -43,6 +46,15 @@ func (p *pendingBuffer) appendByte(b byte) {
|
||||
p.data = append(p.data, b)
|
||||
}
|
||||
|
||||
func appendDecodedByte(dst []byte, n int, pending *pendingBuffer, b byte) int {
|
||||
if n < len(dst) {
|
||||
dst[n] = b
|
||||
return n + 1
|
||||
}
|
||||
pending.appendByte(b)
|
||||
return n
|
||||
}
|
||||
|
||||
func drainPending(dst []byte, pending *pendingBuffer) (int, bool) {
|
||||
if pending == nil || pending.available() == 0 {
|
||||
return 0, false
|
||||
|
||||
@@ -6,14 +6,10 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type randomSource interface {
|
||||
Uint32() uint32
|
||||
Uint64() uint64
|
||||
Intn(n int) int
|
||||
}
|
||||
|
||||
type sudokuRand struct {
|
||||
state uint64
|
||||
state uint64
|
||||
cached uint32
|
||||
haveCached bool
|
||||
}
|
||||
|
||||
func newSeededRand() *sudokuRand {
|
||||
@@ -37,20 +33,36 @@ 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)
|
||||
r.haveCached = false
|
||||
x := r.state
|
||||
x ^= x >> 12
|
||||
x ^= x << 25
|
||||
x ^= x >> 27
|
||||
r.state = x
|
||||
return x * 0x2545f4914f6cdd1d
|
||||
}
|
||||
|
||||
func (r *sudokuRand) Uint32() uint32 {
|
||||
return uint32(r.Uint64() >> 32)
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
if r.haveCached {
|
||||
r.haveCached = false
|
||||
return r.cached
|
||||
}
|
||||
v := r.Uint64()
|
||||
r.cached = uint32(v)
|
||||
r.haveCached = true
|
||||
return uint32(v >> 32)
|
||||
}
|
||||
|
||||
func (r *sudokuRand) Intn(n int) int {
|
||||
if n <= 1 {
|
||||
return 0
|
||||
}
|
||||
return int((uint64(r.Uint32()) * uint64(n)) >> 32)
|
||||
return fastIntnFromUint32(r.Uint32(), n)
|
||||
}
|
||||
|
||||
func fastIntnFromUint32(u uint32, n int) int {
|
||||
return int((uint64(u) * uint64(n)) >> 32)
|
||||
}
|
||||
|
||||
@@ -192,23 +192,27 @@ func tableHintFingerprint(key string, mode string, uplinkPattern string, downlin
|
||||
}
|
||||
|
||||
func packHintsToKey(hints [4]byte) uint32 {
|
||||
return packHintBytes(hints[0], hints[1], hints[2], hints[3])
|
||||
}
|
||||
|
||||
func packHintBytes(h0, h1, h2, h3 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 h0 > h1 {
|
||||
h0, h1 = h1, h0
|
||||
}
|
||||
if hints[2] > hints[3] {
|
||||
hints[2], hints[3] = hints[3], hints[2]
|
||||
if h2 > h3 {
|
||||
h2, h3 = h3, h2
|
||||
}
|
||||
if hints[0] > hints[2] {
|
||||
hints[0], hints[2] = hints[2], hints[0]
|
||||
if h0 > h2 {
|
||||
h0, h2 = h2, h0
|
||||
}
|
||||
if hints[1] > hints[3] {
|
||||
hints[1], hints[3] = hints[3], hints[1]
|
||||
if h1 > h3 {
|
||||
h1, h3 = h3, h1
|
||||
}
|
||||
if hints[1] > hints[2] {
|
||||
hints[1], hints[2] = hints[2], hints[1]
|
||||
if h1 > h2 {
|
||||
h1, h2 = h2, h1
|
||||
}
|
||||
|
||||
return uint32(hints[0])<<24 | uint32(hints[1])<<16 | uint32(hints[2])<<8 | uint32(hints[3])
|
||||
return uint32(h0)<<24 | uint32(h1)<<16 | uint32(h2)<<8 | uint32(h3)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user