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:
samosvalishe
2026-05-18 20:26:54 +03:00
parent 369ba1e43c
commit e2dd09a729
4 changed files with 152 additions and 250 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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