Add Snell protocol. Refactor MASQUE HTTP/2, Fair Queue. Update XHTTP, OpenVPN, Sudoku, Fallback. Fixes

This commit is contained in:
Shtorm
2026-06-26 01:25:57 +03:00
parent d174962a04
commit edf38d33d6
107 changed files with 5346 additions and 708 deletions

View File

@@ -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
}

View 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))
}
}

View 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")
}
}

View File

@@ -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)
}

View 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)
}
}
}
}
}

View File

@@ -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
}

View File

@@ -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
}

View 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")
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)
}