mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
refactor(wrap): заменить DTLS-мимикрию на noise-only AEAD
Фейковый DTLS wire-формат (0202b9b) был fingerprintable по содержимому: фиксированный content_type=0x17, нет handshake-записей, размеры пакетов кластеризовались в 6 фиксированных корзин padding. Throughput на shaped TURN-путях совпадал с wrap-off. Новый wire-формат неотличим от случайного после первого байта: [1B header | (prefix_len-1)B random | 12B nonce | AEAD ct | 16B tag] где prefix_len = 1 + (header & 0x07). Байт 0 распределён равномерно по 0..255, длина префикса закодирована в младших 3 битах. Nonce = 4B sessionID || 8B counter (BE). MSB sessionID кодирует направление (0 клиент, 1 сервер), чтобы клиент и сервер не коллизировали под одним ключом. counter стартует со случайного uint64 против reuse nonce между перезапусками процесса. AAD = nonce; случайный префикс — неаутентифицированный шум. Padding по фиксированным корзинам убран полностью. Усиление в 2-4x мелких ACK/keepalive-пакетов было вероятной причиной регрессии throughput. Overhead: 29..36 B на пакет (против 31..47 в DTLS-мимикрии, 12 в pre-AEAD XOR). Целостность обеспечивает Poly1305.
This commit is contained in:
+1
-1
@@ -2222,7 +2222,7 @@ func createSmuxSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr, i
|
||||
}
|
||||
|
||||
// relayPacketConn wraps a TURN relay PacketConn to direct all writes to the peer.
|
||||
// When wc != nil, packets are wrapped/unwrapped with DTLS-mimicry obfuscation.
|
||||
// When wc != nil, packets are wrapped/unwrapped with AEAD noise obfuscation.
|
||||
type relayPacketConn struct {
|
||||
relay net.PacketConn
|
||||
peer net.Addr
|
||||
|
||||
+66
-118
@@ -5,7 +5,6 @@ package main
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
@@ -15,41 +14,37 @@ import (
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
// Wire format — DTLS 1.2 ApplicationData mimicry:
|
||||
// Wire format — noise-only AEAD (no protocol mimicry):
|
||||
//
|
||||
// [1B content_type=0x17 | 2B version=0xFEFD | 2B epoch | 6B seq_num | 2B length |
|
||||
// AEAD_ciphertext(padded plaintext) | 16B Poly1305 tag]
|
||||
// [1B header | (prefix_len-1)B random | 12B nonce | AEAD ciphertext | 16B tag]
|
||||
//
|
||||
// Plaintext before encryption: [2B payload_length (BE) | payload | random padding]
|
||||
// Padded to the nearest bucket in padBuckets to defeat size fingerprinting.
|
||||
// prefix_len = 1 + (header & 0x07), so byte 0 is uniformly random across all
|
||||
// 256 values while encoding 1..8 bytes of leading random padding. There is
|
||||
// no fixed byte, no version, no length field — everything past the header
|
||||
// byte is indistinguishable from random to a passive observer.
|
||||
//
|
||||
// Nonce (12B) = XOR(SHA-256(key)[0:12], [00 00 | epoch(2B) | seq(6B) | 00 00])
|
||||
// AAD = the 13-byte DTLS record header.
|
||||
// Nonce (12B) = 4B sessionID || 8B counter (big-endian). sessionID has its
|
||||
// MSB set on server side, cleared on client side, preventing nonce reuse
|
||||
// between directions under the same key. counter starts at a random uint64
|
||||
// to avoid reuse across process restarts.
|
||||
//
|
||||
// Epoch MSB encodes direction: 0 = client, 1 = server.
|
||||
// This guarantees no nonce reuse between client and server under the same key.
|
||||
// AAD = nonce. The random prefix is not authenticated (it carries no
|
||||
// information).
|
||||
|
||||
const (
|
||||
wrapKeyLen = 32
|
||||
wrapHeaderLen = 13 // DTLS record header
|
||||
wrapTagLen = 16 // Poly1305 tag
|
||||
wrapLenPrefix = 2 // uint16 BE real payload length
|
||||
wrapOverhead = wrapHeaderLen + wrapTagLen + wrapLenPrefix // 31
|
||||
wrapNonceLen = 12
|
||||
wrapTagLen = 16
|
||||
wrapPrefixMin = 1
|
||||
wrapPrefixMax = 8
|
||||
)
|
||||
|
||||
// padBuckets defines plaintext size buckets. The padded plaintext
|
||||
// ([2B length | payload | padding]) is rounded up to the nearest bucket.
|
||||
var padBuckets = [...]int{128, 256, 512, 768, 1024, 1300}
|
||||
|
||||
// wrapConn holds per-connection state for DTLS-mimicry obfuscation.
|
||||
type wrapConn struct {
|
||||
aead cipher.AEAD
|
||||
nonceMask [12]byte // SHA-256(key)[0:12]
|
||||
epoch uint16
|
||||
seq atomic.Uint64
|
||||
sessionID [4]byte
|
||||
counter atomic.Uint64
|
||||
}
|
||||
|
||||
// newWrapConn creates wrap state. isServer must be true on the server side.
|
||||
func newWrapConn(key []byte, isServer bool) (*wrapConn, error) {
|
||||
if len(key) != wrapKeyLen {
|
||||
return nil, fmt.Errorf("wrap: key must be %d bytes (got %d)", wrapKeyLen, len(key))
|
||||
@@ -58,133 +53,86 @@ func newWrapConn(key []byte, isServer bool) (*wrapConn, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wrap: aead init: %w", err)
|
||||
}
|
||||
h := sha256.Sum256(key)
|
||||
var mask [12]byte
|
||||
copy(mask[:], h[:12])
|
||||
|
||||
var eb [2]byte
|
||||
if _, err := rand.Read(eb[:]); err != nil {
|
||||
return nil, fmt.Errorf("wrap: epoch rand: %w", err)
|
||||
w := &wrapConn{aead: aead}
|
||||
if _, err := rand.Read(w.sessionID[:]); err != nil {
|
||||
return nil, fmt.Errorf("wrap: sessionID rand: %w", err)
|
||||
}
|
||||
epoch := binary.BigEndian.Uint16(eb[:])
|
||||
if isServer {
|
||||
epoch |= 0x8000
|
||||
w.sessionID[0] |= 0x80
|
||||
} else {
|
||||
epoch &^= 0x8000
|
||||
w.sessionID[0] &^= 0x80
|
||||
}
|
||||
return &wrapConn{aead: aead, nonceMask: mask, epoch: epoch}, nil
|
||||
var cb [8]byte
|
||||
if _, err := rand.Read(cb[:]); err != nil {
|
||||
return nil, fmt.Errorf("wrap: counter rand: %w", err)
|
||||
}
|
||||
w.counter.Store(binary.BigEndian.Uint64(cb[:]))
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// deriveNonce builds the 12-byte AEAD nonce from header fields.
|
||||
func (w *wrapConn) deriveNonce(epoch uint16, seqNum uint64) [12]byte {
|
||||
var explicit [12]byte
|
||||
binary.BigEndian.PutUint16(explicit[2:4], epoch)
|
||||
binary.BigEndian.PutUint16(explicit[4:6], uint16(seqNum>>32))
|
||||
binary.BigEndian.PutUint32(explicit[6:10], uint32(seqNum))
|
||||
var nonce [12]byte
|
||||
for i := range 12 {
|
||||
nonce[i] = w.nonceMask[i] ^ explicit[i]
|
||||
}
|
||||
return nonce
|
||||
}
|
||||
|
||||
// padPlaintextSize returns the padded plaintext size for a given payload.
|
||||
func padPlaintextSize(payloadLen int) int {
|
||||
needed := wrapLenPrefix + payloadLen
|
||||
for _, b := range padBuckets {
|
||||
if b >= needed {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return needed
|
||||
}
|
||||
|
||||
// WrapMaxWire returns the maximum wire length for a given payload size.
|
||||
// wrapMaxWire returns max wire bytes for a given payload size.
|
||||
func wrapMaxWire(payloadLen int) int {
|
||||
return wrapHeaderLen + padPlaintextSize(payloadLen) + wrapTagLen
|
||||
return wrapPrefixMax + wrapNonceLen + payloadLen + wrapTagLen
|
||||
}
|
||||
|
||||
// wrapInto encrypts payload into dst. Returns total wire bytes written.
|
||||
// dst must have len >= wrapMaxWire(len(payload)).
|
||||
func (w *wrapConn) wrapInto(dst, payload []byte) (int, error) {
|
||||
paddedLen := padPlaintextSize(len(payload))
|
||||
wireLen := wrapHeaderLen + paddedLen + wrapTagLen
|
||||
// Header + prefix length.
|
||||
var hdr [1]byte
|
||||
if _, err := rand.Read(hdr[:]); err != nil {
|
||||
return 0, fmt.Errorf("wrap: prefix rand: %w", err)
|
||||
}
|
||||
prefixLen := wrapPrefixMin + int(hdr[0]&0x07) // 1..8
|
||||
|
||||
wireLen := prefixLen + wrapNonceLen + len(payload) + wrapTagLen
|
||||
if len(dst) < wireLen {
|
||||
return 0, errors.New("wrap: dst buffer too small")
|
||||
}
|
||||
|
||||
seq := w.seq.Add(1) - 1
|
||||
|
||||
// --- DTLS record header (13 bytes) ---
|
||||
dst[0] = 0x17 // ApplicationData
|
||||
dst[1] = 0xFE // DTLS 1.2
|
||||
dst[2] = 0xFD
|
||||
binary.BigEndian.PutUint16(dst[3:5], w.epoch)
|
||||
binary.BigEndian.PutUint16(dst[5:7], uint16(seq>>32))
|
||||
binary.BigEndian.PutUint32(dst[7:11], uint32(seq))
|
||||
binary.BigEndian.PutUint16(dst[11:13], uint16(paddedLen+wrapTagLen))
|
||||
|
||||
// --- Build plaintext: [2B len | payload | padding] ---
|
||||
plain := dst[wrapHeaderLen : wrapHeaderLen+paddedLen]
|
||||
binary.BigEndian.PutUint16(plain[0:2], uint16(len(payload)))
|
||||
copy(plain[wrapLenPrefix:], payload)
|
||||
// Random padding for the tail
|
||||
padStart := wrapLenPrefix + len(payload)
|
||||
if padStart < paddedLen {
|
||||
_, _ = rand.Read(plain[padStart:paddedLen])
|
||||
dst[0] = hdr[0]
|
||||
if prefixLen > 1 {
|
||||
if _, err := rand.Read(dst[1:prefixLen]); err != nil {
|
||||
return 0, fmt.Errorf("wrap: prefix rand: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- AEAD encrypt in-place ---
|
||||
nonce := w.deriveNonce(w.epoch, seq)
|
||||
aad := dst[:wrapHeaderLen]
|
||||
w.aead.Seal(dst[wrapHeaderLen:wrapHeaderLen], nonce[:], plain, aad)
|
||||
// Nonce: 4B sessionID || 8B counter.
|
||||
noncePos := prefixLen
|
||||
copy(dst[noncePos:noncePos+4], w.sessionID[:])
|
||||
ctr := w.counter.Add(1) - 1
|
||||
binary.BigEndian.PutUint64(dst[noncePos+4:noncePos+wrapNonceLen], ctr)
|
||||
|
||||
nonce := dst[noncePos : noncePos+wrapNonceLen]
|
||||
ctPos := noncePos + wrapNonceLen
|
||||
// In-place: write plaintext at ctPos, then Seal over it.
|
||||
copy(dst[ctPos:], payload)
|
||||
w.aead.Seal(dst[ctPos:ctPos], nonce, dst[ctPos:ctPos+len(payload)], nonce)
|
||||
|
||||
return wireLen, nil
|
||||
}
|
||||
|
||||
// unwrapPacket decrypts a wire-format packet into dst. Returns payload bytes.
|
||||
func (w *wrapConn) unwrapPacket(wire, dst []byte) (int, error) {
|
||||
if len(wire) < wrapHeaderLen+wrapTagLen+wrapLenPrefix {
|
||||
if len(wire) < wrapPrefixMin+wrapNonceLen+wrapTagLen {
|
||||
return 0, errors.New("wrap: packet too short")
|
||||
}
|
||||
if wire[0] != 0x17 {
|
||||
return 0, fmt.Errorf("wrap: unexpected content type 0x%02X", wire[0])
|
||||
prefixLen := wrapPrefixMin + int(wire[0]&0x07)
|
||||
if len(wire) < prefixLen+wrapNonceLen+wrapTagLen {
|
||||
return 0, errors.New("wrap: packet too short for prefix")
|
||||
}
|
||||
nonce := wire[prefixLen : prefixLen+wrapNonceLen]
|
||||
ct := wire[prefixLen+wrapNonceLen:]
|
||||
|
||||
epoch := binary.BigEndian.Uint16(wire[3:5])
|
||||
seqHigh := uint64(binary.BigEndian.Uint16(wire[5:7]))
|
||||
seqLow := uint64(binary.BigEndian.Uint32(wire[7:11]))
|
||||
seqNum := (seqHigh << 32) | seqLow
|
||||
bodyLen := int(binary.BigEndian.Uint16(wire[11:13]))
|
||||
|
||||
if wrapHeaderLen+bodyLen > len(wire) {
|
||||
return 0, errors.New("wrap: truncated packet")
|
||||
}
|
||||
|
||||
nonce := w.deriveNonce(epoch, seqNum)
|
||||
aad := wire[:wrapHeaderLen]
|
||||
ciphertext := wire[wrapHeaderLen : wrapHeaderLen+bodyLen]
|
||||
|
||||
plain, err := w.aead.Open(ciphertext[:0], nonce[:], ciphertext, aad)
|
||||
plain, err := w.aead.Open(ct[:0], nonce, ct, nonce)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("wrap: AEAD open: %w", err)
|
||||
}
|
||||
|
||||
if len(plain) < wrapLenPrefix {
|
||||
return 0, errors.New("wrap: plaintext too short for length prefix")
|
||||
}
|
||||
realLen := int(binary.BigEndian.Uint16(plain[0:2]))
|
||||
if realLen > len(plain)-wrapLenPrefix {
|
||||
return 0, fmt.Errorf("wrap: payload length %d exceeds plaintext %d", realLen, len(plain)-wrapLenPrefix)
|
||||
}
|
||||
if realLen > len(dst) {
|
||||
if len(plain) > len(dst) {
|
||||
return 0, errors.New("wrap: dst buffer too small")
|
||||
}
|
||||
copy(dst[:realLen], plain[wrapLenPrefix:wrapLenPrefix+realLen])
|
||||
return realLen, nil
|
||||
copy(dst[:len(plain)], plain)
|
||||
return len(plain), nil
|
||||
}
|
||||
|
||||
// --- Helper functions (unchanged API for flag parsing) ---
|
||||
// --- Helpers ---
|
||||
|
||||
func genWrapKeyHex() (string, error) {
|
||||
key := make([]byte, wrapKeyLen)
|
||||
|
||||
+25
-27
@@ -27,13 +27,6 @@ func TestWrapConnRoundTrip(t *testing.T) {
|
||||
}
|
||||
wire = wire[:n]
|
||||
|
||||
// Check DTLS header mimicry
|
||||
if wire[0] != 0x17 {
|
||||
t.Fatalf("content_type = 0x%02X, want 0x17", wire[0])
|
||||
}
|
||||
if wire[1] != 0xFE || wire[2] != 0xFD {
|
||||
t.Fatalf("version = 0x%02X%02X, want 0xFEFD", wire[1], wire[2])
|
||||
}
|
||||
if bytes.Contains(wire, payload) {
|
||||
t.Fatalf("wrapped packet contains plaintext payload")
|
||||
}
|
||||
@@ -65,35 +58,39 @@ func TestWrapConnRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapPaddingHidesSize(t *testing.T) {
|
||||
func TestWrapPrefixLenInRange(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x42}, wrapKeyLen)
|
||||
wc, _ := newWrapConn(key, false)
|
||||
payload := []byte("x")
|
||||
|
||||
// Two payloads of different size should produce same wire size if in same bucket
|
||||
p1 := make([]byte, 10)
|
||||
p2 := make([]byte, 100)
|
||||
|
||||
w1 := make([]byte, wrapMaxWire(len(p1)))
|
||||
w2 := make([]byte, wrapMaxWire(len(p2)))
|
||||
|
||||
n1, _ := wc.wrapInto(w1, p1)
|
||||
n2, _ := wc.wrapInto(w2, p2)
|
||||
|
||||
if n1 != n2 {
|
||||
t.Fatalf("different wire sizes for payloads in same bucket: %d vs %d", n1, n2)
|
||||
// Run many trials; every prefix_len must fall in [1..8].
|
||||
for range 200 {
|
||||
wire := make([]byte, wrapMaxWire(len(payload)))
|
||||
n, err := wc.wrapInto(wire, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("wrapInto: %v", err)
|
||||
}
|
||||
prefixLen := wrapPrefixMin + int(wire[0]&0x07)
|
||||
if prefixLen < 1 || prefixLen > 8 {
|
||||
t.Fatalf("prefix_len out of range: %d", prefixLen)
|
||||
}
|
||||
expected := prefixLen + wrapNonceLen + len(payload) + wrapTagLen
|
||||
if n != expected {
|
||||
t.Fatalf("wire size mismatch: got %d want %d (prefix_len=%d)", n, expected, prefixLen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapEpochDirection(t *testing.T) {
|
||||
func TestWrapDirectionBit(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x42}, wrapKeyLen)
|
||||
client, _ := newWrapConn(key, false)
|
||||
server, _ := newWrapConn(key, true)
|
||||
|
||||
if client.epoch&0x8000 != 0 {
|
||||
t.Fatalf("client epoch MSB should be 0, got 0x%04X", client.epoch)
|
||||
if client.sessionID[0]&0x80 != 0 {
|
||||
t.Fatalf("client sessionID MSB should be 0, got 0x%02X", client.sessionID[0])
|
||||
}
|
||||
if server.epoch&0x8000 == 0 {
|
||||
t.Fatalf("server epoch MSB should be 1, got 0x%04X", server.epoch)
|
||||
if server.sessionID[0]&0x80 == 0 {
|
||||
t.Fatalf("server sessionID MSB should be 1, got 0x%02X", server.sessionID[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,8 +136,9 @@ func TestUnwrapRejectsTamperedPacket(t *testing.T) {
|
||||
n, _ := client.wrapInto(wire, payload)
|
||||
wire = wire[:n]
|
||||
|
||||
// Flip a bit in the ciphertext
|
||||
wire[wrapHeaderLen+5] ^= 0xFF
|
||||
// Flip a bit in the ciphertext (past prefix+nonce).
|
||||
prefixLen := wrapPrefixMin + int(wire[0]&0x07)
|
||||
wire[prefixLen+wrapNonceLen+1] ^= 0xFF
|
||||
|
||||
dst := make([]byte, 1600)
|
||||
if _, err := server.unwrapPacket(wire, dst); err == nil {
|
||||
|
||||
+60
-104
@@ -5,7 +5,6 @@ package main
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -19,45 +18,28 @@ import (
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
// Wire format is identical to the client — see client/wrap.go for the
|
||||
// full specification. Server uses epoch MSB=1, client uses MSB=0.
|
||||
// Wire format is identical to client — see client/wrap.go. Server sets the
|
||||
// MSB of sessionID; client clears it. Counter init is random per process.
|
||||
|
||||
const (
|
||||
wrapKeyLen = 32
|
||||
wrapHeaderLen = 13 // DTLS 1.2 record header
|
||||
wrapTagLen = 16 // Poly1305 tag
|
||||
wrapLenPrefix = 2 // uint16 BE real payload length
|
||||
wrapNonceLen = 12
|
||||
wrapTagLen = 16
|
||||
wrapPrefixMin = 1
|
||||
wrapPrefixMax = 8
|
||||
)
|
||||
|
||||
var padBuckets = [...]int{128, 256, 512, 768, 1024, 1300}
|
||||
|
||||
// bufPool eliminates per-packet heap allocation on the hot read/write paths.
|
||||
var bufPool = sync.Pool{
|
||||
New: func() any {
|
||||
b := make([]byte, 1600)
|
||||
b := make([]byte, 1600+wrapPrefixMax+wrapNonceLen+wrapTagLen)
|
||||
return &b
|
||||
},
|
||||
}
|
||||
|
||||
func padPlaintextSize(payloadLen int) int {
|
||||
needed := wrapLenPrefix + payloadLen
|
||||
for _, b := range padBuckets {
|
||||
if b >= needed {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return needed
|
||||
}
|
||||
|
||||
func wrapMaxWire(payloadLen int) int {
|
||||
return wrapHeaderLen + padPlaintextSize(payloadLen) + wrapTagLen
|
||||
}
|
||||
|
||||
// wrapState holds the AEAD instance and nonce mask shared by all connections
|
||||
// under one key, plus per-connection epoch/seq state.
|
||||
// wrapState holds the AEAD instance shared by all connections under one key.
|
||||
type wrapState struct {
|
||||
aead cipher.AEAD
|
||||
nonceMask [12]byte
|
||||
aead cipher.AEAD
|
||||
}
|
||||
|
||||
func newWrapState(key []byte) (*wrapState, error) {
|
||||
@@ -68,22 +50,7 @@ func newWrapState(key []byte) (*wrapState, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wrap: aead init: %w", err)
|
||||
}
|
||||
h := sha256.Sum256(key)
|
||||
var mask [12]byte
|
||||
copy(mask[:], h[:12])
|
||||
return &wrapState{aead: aead, nonceMask: mask}, nil
|
||||
}
|
||||
|
||||
func (s *wrapState) deriveNonce(epoch uint16, seqNum uint64) [12]byte {
|
||||
var explicit [12]byte
|
||||
binary.BigEndian.PutUint16(explicit[2:4], epoch)
|
||||
binary.BigEndian.PutUint16(explicit[4:6], uint16(seqNum>>32))
|
||||
binary.BigEndian.PutUint32(explicit[6:10], uint32(seqNum))
|
||||
var nonce [12]byte
|
||||
for i := range 12 {
|
||||
nonce[i] = s.nonceMask[i] ^ explicit[i]
|
||||
}
|
||||
return nonce
|
||||
return &wrapState{aead: aead}, nil
|
||||
}
|
||||
|
||||
// --- Listener ---
|
||||
@@ -113,12 +80,18 @@ func (l *wrapPacketListener) Accept() (net.PacketConn, net.Addr, error) {
|
||||
if err != nil {
|
||||
return pc, addr, err
|
||||
}
|
||||
// Server epoch: random with MSB=1
|
||||
var eb [2]byte
|
||||
_, _ = rand.Read(eb[:])
|
||||
epoch := binary.BigEndian.Uint16(eb[:]) | 0x8000
|
||||
|
||||
return &wrapPacketConn{inner: pc, ws: l.ws, epoch: epoch}, addr, nil
|
||||
c := &wrapPacketConn{inner: pc, ws: l.ws}
|
||||
// Per-connection sessionID (server MSB=1) + random counter init.
|
||||
if _, err := rand.Read(c.sessionID[:]); err != nil {
|
||||
return nil, addr, fmt.Errorf("wrap: sessionID rand: %w", err)
|
||||
}
|
||||
c.sessionID[0] |= 0x80
|
||||
var cb [8]byte
|
||||
if _, err := rand.Read(cb[:]); err != nil {
|
||||
return nil, addr, fmt.Errorf("wrap: counter rand: %w", err)
|
||||
}
|
||||
c.counter.Store(binary.BigEndian.Uint64(cb[:]))
|
||||
return c, addr, nil
|
||||
}
|
||||
|
||||
func (l *wrapPacketListener) Close() error { return l.inner.Close() }
|
||||
@@ -127,17 +100,18 @@ func (l *wrapPacketListener) Addr() net.Addr { return l.inner.Addr() }
|
||||
// --- Per-peer PacketConn ---
|
||||
|
||||
type wrapPacketConn struct {
|
||||
inner net.PacketConn
|
||||
ws *wrapState
|
||||
epoch uint16
|
||||
seq atomic.Uint64
|
||||
inner net.PacketConn
|
||||
ws *wrapState
|
||||
sessionID [4]byte
|
||||
counter atomic.Uint64
|
||||
}
|
||||
|
||||
func (c *wrapPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
bp := bufPool.Get().(*[]byte)
|
||||
buf := *bp
|
||||
if cap(buf) < len(p)+wrapHeaderLen+wrapTagLen {
|
||||
buf = make([]byte, len(p)+wrapHeaderLen+wrapTagLen)
|
||||
need := len(p) + wrapPrefixMax + wrapNonceLen + wrapTagLen
|
||||
if cap(buf) < need {
|
||||
buf = make([]byte, need)
|
||||
*bp = buf
|
||||
}
|
||||
defer bufPool.Put(bp)
|
||||
@@ -147,45 +121,34 @@ func (c *wrapPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
return 0, addr, err
|
||||
}
|
||||
wire := buf[:n]
|
||||
if len(wire) < wrapHeaderLen+wrapTagLen+wrapLenPrefix {
|
||||
if len(wire) < wrapPrefixMin+wrapNonceLen+wrapTagLen {
|
||||
return 0, addr, errors.New("wrap: packet too short")
|
||||
}
|
||||
if wire[0] != 0x17 {
|
||||
return 0, addr, fmt.Errorf("wrap: unexpected content type 0x%02X", wire[0])
|
||||
prefixLen := wrapPrefixMin + int(wire[0]&0x07)
|
||||
if len(wire) < prefixLen+wrapNonceLen+wrapTagLen {
|
||||
return 0, addr, errors.New("wrap: packet too short for prefix")
|
||||
}
|
||||
nonce := wire[prefixLen : prefixLen+wrapNonceLen]
|
||||
ct := wire[prefixLen+wrapNonceLen:]
|
||||
|
||||
epoch := binary.BigEndian.Uint16(wire[3:5])
|
||||
seqHigh := uint64(binary.BigEndian.Uint16(wire[5:7]))
|
||||
seqLow := uint64(binary.BigEndian.Uint32(wire[7:11]))
|
||||
seqNum := (seqHigh << 32) | seqLow
|
||||
bodyLen := int(binary.BigEndian.Uint16(wire[11:13]))
|
||||
|
||||
if wrapHeaderLen+bodyLen > n {
|
||||
return 0, addr, errors.New("wrap: truncated packet")
|
||||
}
|
||||
|
||||
nonce := c.ws.deriveNonce(epoch, seqNum)
|
||||
aad := wire[:wrapHeaderLen]
|
||||
ciphertext := wire[wrapHeaderLen : wrapHeaderLen+bodyLen]
|
||||
|
||||
plain, err := c.ws.aead.Open(ciphertext[:0], nonce[:], ciphertext, aad)
|
||||
plain, err := c.ws.aead.Open(ct[:0], nonce, ct, nonce)
|
||||
if err != nil {
|
||||
return 0, addr, fmt.Errorf("wrap: AEAD open: %w", err)
|
||||
}
|
||||
if len(plain) < wrapLenPrefix {
|
||||
return 0, addr, errors.New("wrap: plaintext too short")
|
||||
if len(plain) > len(p) {
|
||||
return 0, addr, errors.New("wrap: dst buffer too small")
|
||||
}
|
||||
realLen := int(binary.BigEndian.Uint16(plain[0:2]))
|
||||
if realLen > len(plain)-wrapLenPrefix || realLen > len(p) {
|
||||
return 0, addr, errors.New("wrap: payload length mismatch")
|
||||
}
|
||||
copy(p[:realLen], plain[wrapLenPrefix:wrapLenPrefix+realLen])
|
||||
return realLen, addr, nil
|
||||
copy(p[:len(plain)], plain)
|
||||
return len(plain), addr, nil
|
||||
}
|
||||
|
||||
func (c *wrapPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||
paddedLen := padPlaintextSize(len(p))
|
||||
wireLen := wrapHeaderLen + paddedLen + wrapTagLen
|
||||
var hdr [1]byte
|
||||
if _, err := rand.Read(hdr[:]); err != nil {
|
||||
return 0, fmt.Errorf("wrap: prefix rand: %w", err)
|
||||
}
|
||||
prefixLen := wrapPrefixMin + int(hdr[0]&0x07)
|
||||
wireLen := prefixLen + wrapNonceLen + len(p) + wrapTagLen
|
||||
|
||||
bp := bufPool.Get().(*[]byte)
|
||||
out := *bp
|
||||
@@ -196,29 +159,22 @@ func (c *wrapPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||
out = out[:wireLen]
|
||||
defer bufPool.Put(bp)
|
||||
|
||||
seq := c.seq.Add(1) - 1
|
||||
|
||||
// DTLS record header
|
||||
out[0] = 0x17
|
||||
out[1] = 0xFE
|
||||
out[2] = 0xFD
|
||||
binary.BigEndian.PutUint16(out[3:5], c.epoch)
|
||||
binary.BigEndian.PutUint16(out[5:7], uint16(seq>>32))
|
||||
binary.BigEndian.PutUint32(out[7:11], uint32(seq))
|
||||
binary.BigEndian.PutUint16(out[11:13], uint16(paddedLen+wrapTagLen))
|
||||
|
||||
// Plaintext: [2B len | payload | random padding]
|
||||
plain := out[wrapHeaderLen : wrapHeaderLen+paddedLen]
|
||||
binary.BigEndian.PutUint16(plain[0:2], uint16(len(p)))
|
||||
copy(plain[wrapLenPrefix:], p)
|
||||
padStart := wrapLenPrefix + len(p)
|
||||
if padStart < paddedLen {
|
||||
_, _ = rand.Read(plain[padStart:paddedLen])
|
||||
out[0] = hdr[0]
|
||||
if prefixLen > 1 {
|
||||
if _, err := rand.Read(out[1:prefixLen]); err != nil {
|
||||
return 0, fmt.Errorf("wrap: prefix rand: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
nonce := c.ws.deriveNonce(c.epoch, seq)
|
||||
aad := out[:wrapHeaderLen]
|
||||
c.ws.aead.Seal(out[wrapHeaderLen:wrapHeaderLen], nonce[:], plain, aad)
|
||||
noncePos := prefixLen
|
||||
copy(out[noncePos:noncePos+4], c.sessionID[:])
|
||||
ctr := c.counter.Add(1) - 1
|
||||
binary.BigEndian.PutUint64(out[noncePos+4:noncePos+wrapNonceLen], ctr)
|
||||
|
||||
nonce := out[noncePos : noncePos+wrapNonceLen]
|
||||
ctPos := noncePos + wrapNonceLen
|
||||
copy(out[ctPos:], p)
|
||||
c.ws.aead.Seal(out[ctPos:ctPos], nonce, out[ctPos:ctPos+len(p)], nonce)
|
||||
|
||||
if _, err := c.inner.WriteTo(out, addr); err != nil {
|
||||
return 0, err
|
||||
|
||||
Reference in New Issue
Block a user