mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
refactor: вынести wrap в internal/wrap
Объединяет client/wrap.go + server/wrap.go в internal/wrap с экспортируемым API: State (общий AEAD), Conn (per-stream RTP-состояние), WrapInto/Unwrap, Listen (серверный PacketListener), RelayPacketConn (клиентская обёртка TURN-relay), хелперы GenKeyHex/DecodeKey. Wire-формат сохранён побитово. relayPacketConn переезжает из client/main.go в пакет wrap как RelayPacketConn (его единственный осмысленный дом — он владеет *wrap.Conn). -573 строки (client/wrap.go, server/wrap.go, client/wrap_test.go удалены).
This commit is contained in:
+14
-58
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/cacggghp/vk-turn-proxy/internal/bond"
|
||||
"github.com/cacggghp/vk-turn-proxy/internal/netadapt"
|
||||
"github.com/cacggghp/vk-turn-proxy/internal/stats"
|
||||
"github.com/cacggghp/vk-turn-proxy/internal/wrap"
|
||||
"github.com/cacggghp/vk-turn-proxy/tcputil"
|
||||
"github.com/cbeuw/connutil"
|
||||
"github.com/google/uuid"
|
||||
@@ -929,10 +930,10 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
|
||||
// Do not set conn2 deadline (conn2 can sometimes be listenConn if direct mode is used)
|
||||
})
|
||||
var internalPipeAddr atomic.Value
|
||||
var wc *wrapConn
|
||||
if len(turnParams.wrapKey) == wrapKeyLen {
|
||||
var wc *wrap.Conn
|
||||
if len(turnParams.wrapKey) == wrap.KeyLen {
|
||||
var wcErr error
|
||||
wc, wcErr = newWrapConn(turnParams.wrapKey, false)
|
||||
wc, wcErr = wrap.NewConn(turnParams.wrapKey, false)
|
||||
if wcErr != nil {
|
||||
log.Printf("[STREAM %d] WRAP init failed: %v", streamID, wcErr)
|
||||
turncancel()
|
||||
@@ -947,7 +948,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
|
||||
// stream so the hot-path TX loop performs zero allocations.
|
||||
var wireBuf []byte
|
||||
if wc != nil {
|
||||
wireBuf = make([]byte, wrapMaxWire(len(buf)))
|
||||
wireBuf = make([]byte, wrap.MaxWire(len(buf)))
|
||||
}
|
||||
for {
|
||||
if turnctx.Err() != nil {
|
||||
@@ -965,7 +966,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
|
||||
|
||||
out := buf[:n]
|
||||
if wc != nil {
|
||||
written, wrapErr := wc.wrapInto(wireBuf, out)
|
||||
written, wrapErr := wc.WrapInto(wireBuf, out)
|
||||
if wrapErr != nil {
|
||||
log.Printf("[STREAM %d] WRAP failed: %v", streamID, wrapErr)
|
||||
return
|
||||
@@ -985,7 +986,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
|
||||
defer turncancel()
|
||||
readBufLen := 1600
|
||||
if wc != nil {
|
||||
readBufLen = wrapMaxWire(1600)
|
||||
readBufLen = wrap.MaxWire(1600)
|
||||
}
|
||||
buf := make([]byte, readBufLen)
|
||||
plain := make([]byte, 1600)
|
||||
@@ -1002,7 +1003,7 @@ func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UD
|
||||
if addr, ok := addr1.(net.Addr); ok {
|
||||
payload := buf[:n]
|
||||
if wc != nil {
|
||||
m, wrapErr := wc.unwrapPacket(payload, plain)
|
||||
m, wrapErr := wc.Unwrap(payload, plain)
|
||||
if wrapErr != nil {
|
||||
log.Printf("[STREAM %d] UNWRAP failed: %v (n=%d)", streamID, wrapErr, n)
|
||||
continue
|
||||
@@ -1145,7 +1146,7 @@ func main() {
|
||||
appDialer = dnsdial.AppDialer(*dnsFlag)
|
||||
dnsdial.InstallGlobalResolver(*dnsFlag)
|
||||
if *genWrapKey {
|
||||
key, err := genWrapKeyHex()
|
||||
key, err := wrap.GenKeyHex()
|
||||
if err != nil {
|
||||
log.Panicf("%v", err)
|
||||
}
|
||||
@@ -1165,7 +1166,7 @@ func main() {
|
||||
if *wrapMode && *direct {
|
||||
log.Panicf("-wrap requires DTLS; remove -no-dtls")
|
||||
}
|
||||
wrapKey, err := decodeWrapKey(*wrapMode, *wrapKeyHex)
|
||||
wrapKey, err := wrap.DecodeKey(*wrapMode, *wrapKeyHex)
|
||||
if err != nil {
|
||||
log.Panicf("%v", err)
|
||||
}
|
||||
@@ -1859,15 +1860,15 @@ func createSmuxSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr, i
|
||||
cleanup()
|
||||
return nil, nil, fmt.Errorf("generate cert: %w", err)
|
||||
}
|
||||
var relayWC *wrapConn
|
||||
if len(tp.wrapKey) == wrapKeyLen {
|
||||
relayWC, err = newWrapConn(tp.wrapKey, false)
|
||||
var relayWC *wrap.Conn
|
||||
if len(tp.wrapKey) == wrap.KeyLen {
|
||||
relayWC, err = wrap.NewConn(tp.wrapKey, false)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, fmt.Errorf("wrap init: %w", err)
|
||||
}
|
||||
}
|
||||
dtlsPC := &relayPacketConn{relay: relayConn, peer: peer, wc: relayWC}
|
||||
dtlsPC := &wrap.RelayPacketConn{Relay: relayConn, Peer: peer, Conn: relayWC}
|
||||
dtlsConn, err := dtls.ClientWithOptions(dtlsPC, peer,
|
||||
dtls.WithCertificates(certificate),
|
||||
dtls.WithInsecureSkipVerify(true),
|
||||
@@ -1915,51 +1916,6 @@ func createSmuxSession(ctx context.Context, tp *turnParams, peer *net.UDPAddr, i
|
||||
return smuxSess, cleanup, nil
|
||||
}
|
||||
|
||||
// relayPacketConn wraps a TURN relay PacketConn to direct all writes to the peer.
|
||||
// When wc != nil, packets are wrapped/unwrapped with SRTP-mimicry AEAD.
|
||||
type relayPacketConn struct {
|
||||
relay net.PacketConn
|
||||
peer net.Addr
|
||||
wc *wrapConn
|
||||
}
|
||||
|
||||
func (r *relayPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||
if r.wc == nil {
|
||||
return r.relay.ReadFrom(b)
|
||||
}
|
||||
buf := make([]byte, wrapMaxWire(len(b)))
|
||||
n, addr, err := r.relay.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return 0, addr, err
|
||||
}
|
||||
m, err := r.wc.unwrapPacket(buf[:n], b)
|
||||
if err != nil {
|
||||
return 0, addr, err
|
||||
}
|
||||
return m, addr, nil
|
||||
}
|
||||
|
||||
func (r *relayPacketConn) WriteTo(b []byte, _ net.Addr) (int, error) {
|
||||
if r.wc == nil {
|
||||
return r.relay.WriteTo(b, r.peer)
|
||||
}
|
||||
out := make([]byte, wrapMaxWire(len(b)))
|
||||
n, err := r.wc.wrapInto(out, b)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, err = r.relay.WriteTo(out[:n], r.peer); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (r *relayPacketConn) Close() error { return r.relay.Close() }
|
||||
func (r *relayPacketConn) LocalAddr() net.Addr { return r.relay.LocalAddr() }
|
||||
func (r *relayPacketConn) SetDeadline(t time.Time) error { return r.relay.SetDeadline(t) }
|
||||
func (r *relayPacketConn) SetReadDeadline(t time.Time) error { return r.relay.SetReadDeadline(t) }
|
||||
func (r *relayPacketConn) SetWriteDeadline(t time.Time) error { return r.relay.SetWriteDeadline(t) }
|
||||
|
||||
// pipe copies data bidirectionally between two connections.
|
||||
// It returns bytes copied as c1<-c2 and c2<-c1.
|
||||
func pipe(ctx context.Context, c1, c2 net.Conn) (int64, int64) {
|
||||
|
||||
-171
@@ -1,171 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
// Wire format — SRTP-like mimicry:
|
||||
//
|
||||
// [12B RTP header | 12B explicit nonce | AEAD ciphertext | 16B tag]
|
||||
//
|
||||
// RTP header (RFC 3550):
|
||||
//
|
||||
// byte 0: 0x80 V=2, P=0, X=0, CC=0
|
||||
// byte 1: 0x6F M=0, PT=111 (opus, typical voice PT)
|
||||
// byte 2-3: seq16 BE monotonic, init random
|
||||
// byte 4-7: ts32 BE monotonic, init random, increments by 960 (20ms @ 48kHz)
|
||||
// byte 8-11: SSRC random per conn, MSB encodes direction
|
||||
//
|
||||
// 12B explicit nonce = 4B sessionID || 8B counter (BE). sessionID MSB
|
||||
// matches SSRC MSB (direction bit). counter starts at a random uint64.
|
||||
// AAD = first 24 bytes (RTP header || nonce).
|
||||
//
|
||||
// VK TURN appears to forward SRTP-shaped ChannelData on a fast path and
|
||||
// drop anomalous payloads. AEAD ciphertext + 16B tag is plausible as
|
||||
// AES-GCM SRTP per RFC 7714.
|
||||
|
||||
const (
|
||||
wrapKeyLen = 32
|
||||
wrapRTPHdrLen = 12
|
||||
wrapNonceLen = 12
|
||||
wrapTagLen = 16
|
||||
wrapHeaderLen = wrapRTPHdrLen + wrapNonceLen // 24
|
||||
wrapOverhead = wrapHeaderLen + wrapTagLen // 40
|
||||
wrapRTPVersion = 0x80 // V=2, P=0, X=0, CC=0
|
||||
wrapRTPPT = 0x6F // M=0, PT=111 (opus)
|
||||
wrapTSStep = 960 // 20ms @ 48kHz
|
||||
)
|
||||
|
||||
type wrapConn struct {
|
||||
aead cipher.AEAD
|
||||
sessionID [4]byte // 4B prefix for nonce; MSB encodes direction
|
||||
ssrc [4]byte // SSRC for RTP header; MSB encodes direction
|
||||
counter atomic.Uint64
|
||||
seq atomic.Uint32 // RTP sequence (used as uint16)
|
||||
timestamp atomic.Uint32 // RTP timestamp
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wrap: aead init: %w", err)
|
||||
}
|
||||
w := &wrapConn{aead: aead}
|
||||
|
||||
var rnd [16]byte
|
||||
if _, err := rand.Read(rnd[:]); err != nil {
|
||||
return nil, fmt.Errorf("wrap: rand init: %w", err)
|
||||
}
|
||||
copy(w.sessionID[:], rnd[0:4])
|
||||
copy(w.ssrc[:], rnd[4:8])
|
||||
if isServer {
|
||||
w.sessionID[0] |= 0x80
|
||||
w.ssrc[0] |= 0x80
|
||||
} else {
|
||||
w.sessionID[0] &^= 0x80
|
||||
w.ssrc[0] &^= 0x80
|
||||
}
|
||||
w.seq.Store(uint32(binary.BigEndian.Uint16(rnd[8:10])))
|
||||
w.timestamp.Store(binary.BigEndian.Uint32(rnd[10:14]))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// wrapMaxWire returns max wire bytes for a given payload size.
|
||||
func wrapMaxWire(payloadLen int) int {
|
||||
return wrapOverhead + payloadLen
|
||||
}
|
||||
|
||||
func (w *wrapConn) wrapInto(dst, payload []byte) (int, error) {
|
||||
wireLen := wrapOverhead + len(payload)
|
||||
if len(dst) < wireLen {
|
||||
return 0, errors.New("wrap: dst buffer too small")
|
||||
}
|
||||
|
||||
// RTP header.
|
||||
dst[0] = wrapRTPVersion
|
||||
dst[1] = wrapRTPPT
|
||||
seq := uint16(w.seq.Add(1) - 1)
|
||||
binary.BigEndian.PutUint16(dst[2:4], seq)
|
||||
ts := w.timestamp.Add(wrapTSStep) - wrapTSStep
|
||||
binary.BigEndian.PutUint32(dst[4:8], ts)
|
||||
copy(dst[8:12], w.ssrc[:])
|
||||
|
||||
// Explicit nonce.
|
||||
noncePos := wrapRTPHdrLen
|
||||
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]
|
||||
aad := dst[:wrapHeaderLen]
|
||||
ctPos := wrapHeaderLen
|
||||
copy(dst[ctPos:], payload)
|
||||
w.aead.Seal(dst[ctPos:ctPos], nonce, dst[ctPos:ctPos+len(payload)], aad)
|
||||
|
||||
return wireLen, nil
|
||||
}
|
||||
|
||||
func (w *wrapConn) unwrapPacket(wire, dst []byte) (int, error) {
|
||||
if len(wire) < wrapOverhead {
|
||||
return 0, errors.New("wrap: packet too short")
|
||||
}
|
||||
nonce := wire[wrapRTPHdrLen : wrapRTPHdrLen+wrapNonceLen]
|
||||
aad := wire[:wrapHeaderLen]
|
||||
ct := wire[wrapHeaderLen:]
|
||||
|
||||
plain, err := w.aead.Open(ct[:0], nonce, ct, aad)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("wrap: AEAD open: %w", err)
|
||||
}
|
||||
if len(plain) > len(dst) {
|
||||
return 0, errors.New("wrap: dst buffer too small")
|
||||
}
|
||||
copy(dst[:len(plain)], plain)
|
||||
return len(plain), nil
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func genWrapKeyHex() (string, error) {
|
||||
key := make([]byte, wrapKeyLen)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return "", fmt.Errorf("wrap: key gen: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(key), nil
|
||||
}
|
||||
|
||||
func decodeWrapKey(enabled bool, raw string) ([]byte, error) {
|
||||
if !enabled {
|
||||
return nil, nil
|
||||
}
|
||||
if raw == "" {
|
||||
return nil, errors.New("-wrap requires -wrap-key")
|
||||
}
|
||||
key, err := hex.DecodeString(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("-wrap-key invalid hex: %w", err)
|
||||
}
|
||||
if len(key) != wrapKeyLen {
|
||||
return nil, fmt.Errorf("-wrap-key must decode to %d bytes (got %d)", wrapKeyLen, len(key))
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWrapConnRoundTrip(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x42}, wrapKeyLen)
|
||||
payload := []byte("dtls record bytes")
|
||||
|
||||
client, err := newWrapConn(key, false)
|
||||
if err != nil {
|
||||
t.Fatalf("newWrapConn(client): %v", err)
|
||||
}
|
||||
server, err := newWrapConn(key, true)
|
||||
if err != nil {
|
||||
t.Fatalf("newWrapConn(server): %v", err)
|
||||
}
|
||||
|
||||
wire := make([]byte, wrapMaxWire(len(payload)))
|
||||
n, err := client.wrapInto(wire, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("wrapInto: %v", err)
|
||||
}
|
||||
wire = wire[:n]
|
||||
|
||||
if wire[0] != wrapRTPVersion {
|
||||
t.Fatalf("RTP byte0 = 0x%02X, want 0x%02X", wire[0], wrapRTPVersion)
|
||||
}
|
||||
if wire[1] != wrapRTPPT {
|
||||
t.Fatalf("RTP byte1 (PT) = 0x%02X, want 0x%02X", wire[1], wrapRTPPT)
|
||||
}
|
||||
if bytes.Contains(wire, payload) {
|
||||
t.Fatalf("wrapped packet contains plaintext payload")
|
||||
}
|
||||
|
||||
dst := make([]byte, 1600)
|
||||
m, err := server.unwrapPacket(wire, dst)
|
||||
if err != nil {
|
||||
t.Fatalf("unwrapPacket: %v", err)
|
||||
}
|
||||
if m != len(payload) {
|
||||
t.Fatalf("unwrapped len = %d, want %d", m, len(payload))
|
||||
}
|
||||
if !bytes.Equal(dst[:m], payload) {
|
||||
t.Fatalf("round trip mismatch: got %q want %q", dst[:m], payload)
|
||||
}
|
||||
|
||||
// Server → Client
|
||||
wire2 := make([]byte, wrapMaxWire(len(payload)))
|
||||
n2, err := server.wrapInto(wire2, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("server wrapInto: %v", err)
|
||||
}
|
||||
m2, err := client.unwrapPacket(wire2[:n2], dst)
|
||||
if err != nil {
|
||||
t.Fatalf("client unwrapPacket: %v", err)
|
||||
}
|
||||
if !bytes.Equal(dst[:m2], payload) {
|
||||
t.Fatalf("server→client round trip mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapRTPHeaderProgression(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x42}, wrapKeyLen)
|
||||
wc, err := newWrapConn(key, false)
|
||||
if err != nil {
|
||||
t.Fatalf("newWrapConn: %v", err)
|
||||
}
|
||||
payload := []byte("x")
|
||||
|
||||
wire1 := make([]byte, wrapMaxWire(len(payload)))
|
||||
n1, err := wc.wrapInto(wire1, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("wrapInto 1: %v", err)
|
||||
}
|
||||
wire2 := make([]byte, wrapMaxWire(len(payload)))
|
||||
n2, err := wc.wrapInto(wire2, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("wrapInto 2: %v", err)
|
||||
}
|
||||
if n1 != n2 {
|
||||
t.Fatalf("wire size variance: %d vs %d", n1, n2)
|
||||
}
|
||||
|
||||
seq1 := binary.BigEndian.Uint16(wire1[2:4])
|
||||
seq2 := binary.BigEndian.Uint16(wire2[2:4])
|
||||
if seq2 != seq1+1 {
|
||||
t.Fatalf("seq did not increment: %d → %d", seq1, seq2)
|
||||
}
|
||||
|
||||
ts1 := binary.BigEndian.Uint32(wire1[4:8])
|
||||
ts2 := binary.BigEndian.Uint32(wire2[4:8])
|
||||
if ts2-ts1 != wrapTSStep {
|
||||
t.Fatalf("timestamp step = %d, want %d", ts2-ts1, wrapTSStep)
|
||||
}
|
||||
|
||||
// SSRC stable across packets.
|
||||
if !bytes.Equal(wire1[8:12], wire2[8:12]) {
|
||||
t.Fatalf("SSRC changed between packets")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapDirectionBit(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x42}, wrapKeyLen)
|
||||
client, err := newWrapConn(key, false)
|
||||
if err != nil {
|
||||
t.Fatalf("newWrapConn(client): %v", err)
|
||||
}
|
||||
server, err := newWrapConn(key, true)
|
||||
if err != nil {
|
||||
t.Fatalf("newWrapConn(server): %v", err)
|
||||
}
|
||||
|
||||
if client.sessionID[0]&0x80 != 0 {
|
||||
t.Fatalf("client sessionID MSB should be 0, got 0x%02X", client.sessionID[0])
|
||||
}
|
||||
if server.sessionID[0]&0x80 == 0 {
|
||||
t.Fatalf("server sessionID MSB should be 1, got 0x%02X", server.sessionID[0])
|
||||
}
|
||||
if client.ssrc[0]&0x80 != 0 {
|
||||
t.Fatalf("client SSRC MSB should be 0, got 0x%02X", client.ssrc[0])
|
||||
}
|
||||
if server.ssrc[0]&0x80 == 0 {
|
||||
t.Fatalf("server SSRC MSB should be 1, got 0x%02X", server.ssrc[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeWrapKeyRequiresValidKeyWhenEnabled(t *testing.T) {
|
||||
if key, err := decodeWrapKey(false, ""); err != nil || key != nil {
|
||||
t.Fatalf("disabled decodeWrapKey = (%v, %v), want (nil, nil)", key, err)
|
||||
}
|
||||
|
||||
if _, err := decodeWrapKey(true, ""); err == nil {
|
||||
t.Fatalf("decodeWrapKey accepted empty key")
|
||||
}
|
||||
|
||||
shortHex := strings.Repeat("ab", wrapKeyLen-1)
|
||||
if _, err := decodeWrapKey(true, shortHex); err == nil {
|
||||
t.Fatalf("decodeWrapKey accepted short key")
|
||||
}
|
||||
|
||||
fullHex := strings.Repeat("ab", wrapKeyLen)
|
||||
key, err := decodeWrapKey(true, fullHex)
|
||||
if err != nil {
|
||||
t.Fatalf("decodeWrapKey returned error: %v", err)
|
||||
}
|
||||
if len(key) != wrapKeyLen {
|
||||
t.Fatalf("decoded key len = %d, want %d", len(key), wrapKeyLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapRejectsShortPacket(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x42}, wrapKeyLen)
|
||||
wc, err := newWrapConn(key, false)
|
||||
if err != nil {
|
||||
t.Fatalf("newWrapConn: %v", err)
|
||||
}
|
||||
if _, err := wc.unwrapPacket([]byte("short"), make([]byte, 16)); err == nil {
|
||||
t.Fatalf("unwrapPacket accepted short packet")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapRejectsTamperedPacket(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x42}, wrapKeyLen)
|
||||
client, err := newWrapConn(key, false)
|
||||
if err != nil {
|
||||
t.Fatalf("newWrapConn(client): %v", err)
|
||||
}
|
||||
server, err := newWrapConn(key, true)
|
||||
if err != nil {
|
||||
t.Fatalf("newWrapConn(server): %v", err)
|
||||
}
|
||||
|
||||
payload := []byte("integrity test")
|
||||
wire := make([]byte, wrapMaxWire(len(payload)))
|
||||
n, err := client.wrapInto(wire, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("wrapInto: %v", err)
|
||||
}
|
||||
wire = wire[:n]
|
||||
|
||||
// Flip a bit in the ciphertext.
|
||||
wire[wrapHeaderLen+1] ^= 0xFF
|
||||
|
||||
dst := make([]byte, 1600)
|
||||
if _, uerr := server.unwrapPacket(wire, dst); uerr == nil {
|
||||
t.Fatalf("unwrapPacket accepted tampered ciphertext")
|
||||
}
|
||||
|
||||
// Re-wrap and tamper RTP header (AAD).
|
||||
n2, err := client.wrapInto(wire, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("wrapInto: %v", err)
|
||||
}
|
||||
wire = wire[:n2]
|
||||
wire[8] ^= 0x01 // flip a bit in SSRC
|
||||
if _, uerr := server.unwrapPacket(wire, dst); uerr == nil {
|
||||
t.Fatalf("unwrapPacket accepted tampered AAD")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package wrap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
dtlsnet "github.com/pion/dtls/v3/pkg/net"
|
||||
pionudp "github.com/pion/transport/v4/udp"
|
||||
)
|
||||
|
||||
// bufPool eliminates per-packet heap allocation on the hot read/write paths
|
||||
// of the server-side wrapped PacketConn.
|
||||
var bufPool = sync.Pool{
|
||||
New: func() any {
|
||||
b := make([]byte, 1600+Overhead)
|
||||
return &b
|
||||
},
|
||||
}
|
||||
|
||||
// Listen returns a dtls PacketListener that AEAD-wraps every accepted PacketConn.
|
||||
func Listen(addr *net.UDPAddr, key []byte) (dtlsnet.PacketListener, error) {
|
||||
state, err := NewState(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inner, err := pionudp.Listen("udp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wrap: udp listen: %w", err)
|
||||
}
|
||||
return &packetListener{
|
||||
inner: dtlsnet.PacketListenerFromListener(inner),
|
||||
state: state,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type packetListener struct {
|
||||
inner dtlsnet.PacketListener
|
||||
state *State
|
||||
}
|
||||
|
||||
func (l *packetListener) Accept() (net.PacketConn, net.Addr, error) {
|
||||
pc, addr, err := l.inner.Accept()
|
||||
if err != nil {
|
||||
return pc, addr, err
|
||||
}
|
||||
conn, err := NewConnFromState(l.state, true)
|
||||
if err != nil {
|
||||
return nil, addr, err
|
||||
}
|
||||
return &packetConn{inner: pc, conn: conn}, addr, nil
|
||||
}
|
||||
|
||||
func (l *packetListener) Close() error { return l.inner.Close() }
|
||||
func (l *packetListener) Addr() net.Addr { return l.inner.Addr() }
|
||||
|
||||
// packetConn is a per-peer net.PacketConn that AEAD-wraps reads and writes
|
||||
// using bufPool to keep the hot path allocation-free.
|
||||
type packetConn struct {
|
||||
inner net.PacketConn
|
||||
conn *Conn
|
||||
}
|
||||
|
||||
func (c *packetConn) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
bp := bufPool.Get().(*[]byte) //nolint:errcheck // pool New always returns *[]byte
|
||||
buf := *bp
|
||||
need := len(p) + Overhead
|
||||
if cap(buf) < need {
|
||||
buf = make([]byte, need)
|
||||
*bp = buf
|
||||
}
|
||||
defer bufPool.Put(bp)
|
||||
|
||||
n, addr, err := c.inner.ReadFrom(buf[:cap(buf)])
|
||||
if err != nil {
|
||||
return 0, addr, err
|
||||
}
|
||||
wire := buf[:n]
|
||||
if len(wire) < Overhead {
|
||||
return 0, addr, errors.New("wrap: packet too short")
|
||||
}
|
||||
m, err := c.conn.Unwrap(wire, p)
|
||||
if err != nil {
|
||||
return 0, addr, err
|
||||
}
|
||||
return m, addr, nil
|
||||
}
|
||||
|
||||
func (c *packetConn) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||
wireLen := Overhead + len(p)
|
||||
|
||||
bp := bufPool.Get().(*[]byte) //nolint:errcheck // pool New always returns *[]byte
|
||||
out := *bp
|
||||
if cap(out) < wireLen {
|
||||
out = make([]byte, wireLen)
|
||||
*bp = out
|
||||
}
|
||||
out = out[:wireLen]
|
||||
defer bufPool.Put(bp)
|
||||
|
||||
n, err := c.conn.WrapInto(out, p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, err := c.inner.WriteTo(out[:n], addr); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (c *packetConn) Close() error { return c.inner.Close() }
|
||||
func (c *packetConn) LocalAddr() net.Addr { return c.inner.LocalAddr() }
|
||||
func (c *packetConn) SetDeadline(t time.Time) error { return c.inner.SetDeadline(t) }
|
||||
func (c *packetConn) SetReadDeadline(t time.Time) error { return c.inner.SetReadDeadline(t) }
|
||||
func (c *packetConn) SetWriteDeadline(t time.Time) error { return c.inner.SetWriteDeadline(t) }
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package wrap
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RelayPacketConn wraps a TURN relay PacketConn to direct all writes to a fixed peer.
|
||||
// When Conn is non-nil, packets are wrapped/unwrapped with SRTP-mimicry AEAD.
|
||||
type RelayPacketConn struct {
|
||||
Relay net.PacketConn
|
||||
Peer net.Addr
|
||||
Conn *Conn
|
||||
}
|
||||
|
||||
func (r *RelayPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||
if r.Conn == nil {
|
||||
return r.Relay.ReadFrom(b)
|
||||
}
|
||||
buf := make([]byte, MaxWire(len(b)))
|
||||
n, addr, err := r.Relay.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return 0, addr, err
|
||||
}
|
||||
m, err := r.Conn.Unwrap(buf[:n], b)
|
||||
if err != nil {
|
||||
return 0, addr, err
|
||||
}
|
||||
return m, addr, nil
|
||||
}
|
||||
|
||||
func (r *RelayPacketConn) WriteTo(b []byte, _ net.Addr) (int, error) {
|
||||
if r.Conn == nil {
|
||||
return r.Relay.WriteTo(b, r.Peer)
|
||||
}
|
||||
out := make([]byte, MaxWire(len(b)))
|
||||
n, err := r.Conn.WrapInto(out, b)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, err = r.Relay.WriteTo(out[:n], r.Peer); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (r *RelayPacketConn) Close() error { return r.Relay.Close() }
|
||||
func (r *RelayPacketConn) LocalAddr() net.Addr { return r.Relay.LocalAddr() }
|
||||
func (r *RelayPacketConn) SetDeadline(t time.Time) error { return r.Relay.SetDeadline(t) }
|
||||
func (r *RelayPacketConn) SetReadDeadline(t time.Time) error { return r.Relay.SetReadDeadline(t) }
|
||||
func (r *RelayPacketConn) SetWriteDeadline(t time.Time) error { return r.Relay.SetWriteDeadline(t) }
|
||||
@@ -0,0 +1,201 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package wrap implements an SRTP-mimicry AEAD framing used to bypass
|
||||
// the VK TURN content filter.
|
||||
//
|
||||
// Wire format:
|
||||
//
|
||||
// [12B RTP header | 12B explicit nonce | AEAD ciphertext | 16B tag]
|
||||
//
|
||||
// RTP header (RFC 3550):
|
||||
//
|
||||
// byte 0: 0x80 V=2, P=0, X=0, CC=0
|
||||
// byte 1: 0x6F M=0, PT=111 (opus, typical voice PT)
|
||||
// byte 2-3: seq16 BE monotonic, init random
|
||||
// byte 4-7: ts32 BE monotonic, init random, increments by 960 (20ms @ 48kHz)
|
||||
// byte 8-11: SSRC random per conn, MSB encodes direction
|
||||
//
|
||||
// 12B explicit nonce = 4B sessionID || 8B counter (BE). sessionID MSB
|
||||
// matches SSRC MSB (direction bit). counter starts at a random uint64.
|
||||
// AAD = first 24 bytes (RTP header || nonce).
|
||||
//
|
||||
// Wire format is frozen — bit-exact compatibility with deployed peers is required.
|
||||
package wrap
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
const (
|
||||
KeyLen = 32
|
||||
rtpHdrLen = 12
|
||||
nonceLen = 12
|
||||
tagLen = 16
|
||||
headerLen = rtpHdrLen + nonceLen // 24
|
||||
Overhead = headerLen + tagLen // 40
|
||||
rtpVersion = 0x80 // V=2, P=0, X=0, CC=0
|
||||
rtpPT = 0x6F // M=0, PT=111 (opus)
|
||||
tsStep = 960 // 20ms @ 48kHz
|
||||
)
|
||||
|
||||
// MaxWire returns max wire bytes for a given payload size.
|
||||
func MaxWire(payloadLen int) int { return Overhead + payloadLen }
|
||||
|
||||
// State holds an AEAD instance derived from the shared key.
|
||||
// One State can be shared across many Conns (e.g. server-side listener).
|
||||
type State struct {
|
||||
aead cipher.AEAD
|
||||
}
|
||||
|
||||
func NewState(key []byte) (*State, error) {
|
||||
if len(key) != KeyLen {
|
||||
return nil, fmt.Errorf("wrap: key must be %d bytes (got %d)", KeyLen, len(key))
|
||||
}
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wrap: aead init: %w", err)
|
||||
}
|
||||
return &State{aead: aead}, nil
|
||||
}
|
||||
|
||||
// Conn carries per-stream RTP state (seq/timestamp/SSRC/counter) plus a
|
||||
// reference to the shared AEAD State.
|
||||
type Conn struct {
|
||||
state *State
|
||||
sessionID [4]byte // 4B prefix for nonce; MSB encodes direction
|
||||
ssrc [4]byte // SSRC for RTP header; MSB encodes direction
|
||||
counter atomic.Uint64
|
||||
seq atomic.Uint32 // RTP sequence (used as uint16)
|
||||
timestamp atomic.Uint32 // RTP timestamp
|
||||
}
|
||||
|
||||
// NewConn allocates a fresh State from key and a Conn bound to it.
|
||||
func NewConn(key []byte, isServer bool) (*Conn, error) {
|
||||
s, err := NewState(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewConnFromState(s, isServer)
|
||||
}
|
||||
|
||||
// NewConnFromState builds a Conn with random per-stream RTP fields,
|
||||
// reusing the supplied State.
|
||||
func NewConnFromState(state *State, isServer bool) (*Conn, error) {
|
||||
if state == nil {
|
||||
return nil, errors.New("wrap: nil state")
|
||||
}
|
||||
c := &Conn{state: state}
|
||||
|
||||
var rnd [16]byte
|
||||
if _, err := rand.Read(rnd[:]); err != nil {
|
||||
return nil, fmt.Errorf("wrap: rand init: %w", err)
|
||||
}
|
||||
copy(c.sessionID[:], rnd[0:4])
|
||||
copy(c.ssrc[:], rnd[4:8])
|
||||
if isServer {
|
||||
c.sessionID[0] |= 0x80
|
||||
c.ssrc[0] |= 0x80
|
||||
} else {
|
||||
c.sessionID[0] &^= 0x80
|
||||
c.ssrc[0] &^= 0x80
|
||||
}
|
||||
c.seq.Store(uint32(binary.BigEndian.Uint16(rnd[8:10])))
|
||||
c.timestamp.Store(binary.BigEndian.Uint32(rnd[10:14]))
|
||||
|
||||
var cb [8]byte
|
||||
if _, err := rand.Read(cb[:]); err != nil {
|
||||
return nil, fmt.Errorf("wrap: counter rand: %w", err)
|
||||
}
|
||||
c.counter.Store(binary.BigEndian.Uint64(cb[:]))
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// WrapInto encodes payload into dst (must be at least MaxWire(len(payload)) bytes)
|
||||
// and returns the number of wire bytes written.
|
||||
func (c *Conn) WrapInto(dst, payload []byte) (int, error) {
|
||||
wireLen := Overhead + len(payload)
|
||||
if len(dst) < wireLen {
|
||||
return 0, errors.New("wrap: dst buffer too small")
|
||||
}
|
||||
|
||||
// RTP header.
|
||||
dst[0] = rtpVersion
|
||||
dst[1] = rtpPT
|
||||
seq := uint16(c.seq.Add(1) - 1)
|
||||
binary.BigEndian.PutUint16(dst[2:4], seq)
|
||||
ts := c.timestamp.Add(tsStep) - tsStep
|
||||
binary.BigEndian.PutUint32(dst[4:8], ts)
|
||||
copy(dst[8:12], c.ssrc[:])
|
||||
|
||||
// Explicit nonce.
|
||||
noncePos := rtpHdrLen
|
||||
copy(dst[noncePos:noncePos+4], c.sessionID[:])
|
||||
ctr := c.counter.Add(1) - 1
|
||||
binary.BigEndian.PutUint64(dst[noncePos+4:noncePos+nonceLen], ctr)
|
||||
|
||||
nonce := dst[noncePos : noncePos+nonceLen]
|
||||
aad := dst[:headerLen]
|
||||
ctPos := headerLen
|
||||
copy(dst[ctPos:], payload)
|
||||
c.state.aead.Seal(dst[ctPos:ctPos], nonce, dst[ctPos:ctPos+len(payload)], aad)
|
||||
|
||||
return wireLen, nil
|
||||
}
|
||||
|
||||
// Unwrap decodes a wire packet into dst and returns the plaintext length.
|
||||
func (c *Conn) Unwrap(wire, dst []byte) (int, error) {
|
||||
if len(wire) < Overhead {
|
||||
return 0, errors.New("wrap: packet too short")
|
||||
}
|
||||
nonce := wire[rtpHdrLen : rtpHdrLen+nonceLen]
|
||||
aad := wire[:headerLen]
|
||||
ct := wire[headerLen:]
|
||||
|
||||
plain, err := c.state.aead.Open(ct[:0], nonce, ct, aad)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("wrap: AEAD open: %w", err)
|
||||
}
|
||||
if len(plain) > len(dst) {
|
||||
return 0, errors.New("wrap: dst buffer too small")
|
||||
}
|
||||
copy(dst[:len(plain)], plain)
|
||||
return len(plain), nil
|
||||
}
|
||||
|
||||
// --- Key helpers ---
|
||||
|
||||
// GenKeyHex returns a fresh hex-encoded random key.
|
||||
func GenKeyHex() (string, error) {
|
||||
key := make([]byte, KeyLen)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return "", fmt.Errorf("wrap: key gen: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(key), nil
|
||||
}
|
||||
|
||||
// DecodeKey decodes a hex key and validates length when enabled.
|
||||
// When enabled is false, returns (nil, nil).
|
||||
func DecodeKey(enabled bool, raw string) ([]byte, error) {
|
||||
if !enabled {
|
||||
return nil, nil
|
||||
}
|
||||
if raw == "" {
|
||||
return nil, errors.New("-wrap requires -wrap-key")
|
||||
}
|
||||
key, err := hex.DecodeString(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("-wrap-key invalid hex: %w", err)
|
||||
}
|
||||
if len(key) != KeyLen {
|
||||
return nil, fmt.Errorf("-wrap-key must decode to %d bytes (got %d)", KeyLen, len(key))
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package wrap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConnRoundTrip(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x42}, KeyLen)
|
||||
payload := []byte("dtls record bytes")
|
||||
|
||||
client, err := NewConn(key, false)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConn(client): %v", err)
|
||||
}
|
||||
server, err := NewConn(key, true)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConn(server): %v", err)
|
||||
}
|
||||
|
||||
wire := make([]byte, MaxWire(len(payload)))
|
||||
n, err := client.WrapInto(wire, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("WrapInto: %v", err)
|
||||
}
|
||||
wire = wire[:n]
|
||||
|
||||
if wire[0] != rtpVersion {
|
||||
t.Fatalf("RTP byte0 = 0x%02X, want 0x%02X", wire[0], rtpVersion)
|
||||
}
|
||||
if wire[1] != rtpPT {
|
||||
t.Fatalf("RTP byte1 (PT) = 0x%02X, want 0x%02X", wire[1], rtpPT)
|
||||
}
|
||||
if bytes.Contains(wire, payload) {
|
||||
t.Fatalf("wrapped packet contains plaintext payload")
|
||||
}
|
||||
|
||||
dst := make([]byte, 1600)
|
||||
m, err := server.Unwrap(wire, dst)
|
||||
if err != nil {
|
||||
t.Fatalf("Unwrap: %v", err)
|
||||
}
|
||||
if m != len(payload) {
|
||||
t.Fatalf("unwrapped len = %d, want %d", m, len(payload))
|
||||
}
|
||||
if !bytes.Equal(dst[:m], payload) {
|
||||
t.Fatalf("round trip mismatch: got %q want %q", dst[:m], payload)
|
||||
}
|
||||
|
||||
wire2 := make([]byte, MaxWire(len(payload)))
|
||||
n2, err := server.WrapInto(wire2, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("server WrapInto: %v", err)
|
||||
}
|
||||
m2, err := client.Unwrap(wire2[:n2], dst)
|
||||
if err != nil {
|
||||
t.Fatalf("client Unwrap: %v", err)
|
||||
}
|
||||
if !bytes.Equal(dst[:m2], payload) {
|
||||
t.Fatalf("server→client round trip mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRTPHeaderProgression(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x42}, KeyLen)
|
||||
c, err := NewConn(key, false)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConn: %v", err)
|
||||
}
|
||||
payload := []byte("x")
|
||||
|
||||
wire1 := make([]byte, MaxWire(len(payload)))
|
||||
n1, err := c.WrapInto(wire1, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("WrapInto 1: %v", err)
|
||||
}
|
||||
wire2 := make([]byte, MaxWire(len(payload)))
|
||||
n2, err := c.WrapInto(wire2, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("WrapInto 2: %v", err)
|
||||
}
|
||||
if n1 != n2 {
|
||||
t.Fatalf("wire size variance: %d vs %d", n1, n2)
|
||||
}
|
||||
|
||||
seq1 := binary.BigEndian.Uint16(wire1[2:4])
|
||||
seq2 := binary.BigEndian.Uint16(wire2[2:4])
|
||||
if seq2 != seq1+1 {
|
||||
t.Fatalf("seq did not increment: %d → %d", seq1, seq2)
|
||||
}
|
||||
|
||||
ts1 := binary.BigEndian.Uint32(wire1[4:8])
|
||||
ts2 := binary.BigEndian.Uint32(wire2[4:8])
|
||||
if ts2-ts1 != tsStep {
|
||||
t.Fatalf("timestamp step = %d, want %d", ts2-ts1, tsStep)
|
||||
}
|
||||
|
||||
if !bytes.Equal(wire1[8:12], wire2[8:12]) {
|
||||
t.Fatalf("SSRC changed between packets")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectionBit(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x42}, KeyLen)
|
||||
client, err := NewConn(key, false)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConn(client): %v", err)
|
||||
}
|
||||
server, err := NewConn(key, true)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConn(server): %v", err)
|
||||
}
|
||||
|
||||
if client.sessionID[0]&0x80 != 0 {
|
||||
t.Fatalf("client sessionID MSB should be 0, got 0x%02X", client.sessionID[0])
|
||||
}
|
||||
if server.sessionID[0]&0x80 == 0 {
|
||||
t.Fatalf("server sessionID MSB should be 1, got 0x%02X", server.sessionID[0])
|
||||
}
|
||||
if client.ssrc[0]&0x80 != 0 {
|
||||
t.Fatalf("client SSRC MSB should be 0, got 0x%02X", client.ssrc[0])
|
||||
}
|
||||
if server.ssrc[0]&0x80 == 0 {
|
||||
t.Fatalf("server SSRC MSB should be 1, got 0x%02X", server.ssrc[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeKeyRequiresValidKeyWhenEnabled(t *testing.T) {
|
||||
if key, err := DecodeKey(false, ""); err != nil || key != nil {
|
||||
t.Fatalf("disabled DecodeKey = (%v, %v), want (nil, nil)", key, err)
|
||||
}
|
||||
|
||||
if _, err := DecodeKey(true, ""); err == nil {
|
||||
t.Fatalf("DecodeKey accepted empty key")
|
||||
}
|
||||
|
||||
shortHex := strings.Repeat("ab", KeyLen-1)
|
||||
if _, err := DecodeKey(true, shortHex); err == nil {
|
||||
t.Fatalf("DecodeKey accepted short key")
|
||||
}
|
||||
|
||||
fullHex := strings.Repeat("ab", KeyLen)
|
||||
key, err := DecodeKey(true, fullHex)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeKey returned error: %v", err)
|
||||
}
|
||||
if len(key) != KeyLen {
|
||||
t.Fatalf("decoded key len = %d, want %d", len(key), KeyLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapRejectsShortPacket(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x42}, KeyLen)
|
||||
c, err := NewConn(key, false)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConn: %v", err)
|
||||
}
|
||||
if _, err := c.Unwrap([]byte("short"), make([]byte, 16)); err == nil {
|
||||
t.Fatalf("Unwrap accepted short packet")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapRejectsTamperedPacket(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x42}, KeyLen)
|
||||
client, err := NewConn(key, false)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConn(client): %v", err)
|
||||
}
|
||||
server, err := NewConn(key, true)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConn(server): %v", err)
|
||||
}
|
||||
|
||||
payload := []byte("integrity test")
|
||||
wire := make([]byte, MaxWire(len(payload)))
|
||||
n, err := client.WrapInto(wire, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("WrapInto: %v", err)
|
||||
}
|
||||
wire = wire[:n]
|
||||
|
||||
wire[headerLen+1] ^= 0xFF
|
||||
|
||||
dst := make([]byte, 1600)
|
||||
if _, uerr := server.Unwrap(wire, dst); uerr == nil {
|
||||
t.Fatalf("Unwrap accepted tampered ciphertext")
|
||||
}
|
||||
|
||||
n2, err := client.WrapInto(wire, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("WrapInto: %v", err)
|
||||
}
|
||||
wire = wire[:n2]
|
||||
wire[8] ^= 0x01
|
||||
if _, uerr := server.Unwrap(wire, dst); uerr == nil {
|
||||
t.Fatalf("Unwrap accepted tampered AAD")
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/cacggghp/vk-turn-proxy/internal/bond"
|
||||
"github.com/cacggghp/vk-turn-proxy/internal/stats"
|
||||
"github.com/cacggghp/vk-turn-proxy/internal/wrap"
|
||||
"github.com/cacggghp/vk-turn-proxy/tcputil"
|
||||
"github.com/pion/dtls/v3"
|
||||
"github.com/pion/dtls/v3/pkg/crypto/selfsign"
|
||||
@@ -44,7 +45,7 @@ func main() {
|
||||
isDebug = *debugFlag
|
||||
|
||||
if *genWrapKey {
|
||||
key := make([]byte, wrapKeyLen)
|
||||
key := make([]byte, wrap.KeyLen)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
log.Panicf("gen-wrap-key: rand.Read: %v", err)
|
||||
}
|
||||
@@ -80,8 +81,8 @@ func main() {
|
||||
if err != nil {
|
||||
log.Panicf("-wrap-key invalid hex: %v", err)
|
||||
}
|
||||
if len(wrapKey) != wrapKeyLen {
|
||||
log.Panicf("-wrap-key must decode to %d bytes (got %d)", wrapKeyLen, len(wrapKey))
|
||||
if len(wrapKey) != wrap.KeyLen {
|
||||
log.Panicf("-wrap-key must decode to %d bytes (got %d)", wrap.KeyLen, len(wrapKey))
|
||||
}
|
||||
}
|
||||
log.Printf("Starting server listen=%s connect=%s vless=%t vless-bond=%t wrap=%t bond-autodetect=true", *listen, *connect, *vlessMode, *vlessBond, *wrapMode)
|
||||
@@ -104,7 +105,7 @@ func main() {
|
||||
var listener net.Listener
|
||||
if *wrapMode {
|
||||
log.Printf("WRAP mode enabled: listener only accepts clients with matching -wrap-key")
|
||||
wrapListener, werr := listenWrapped(addr, wrapKey)
|
||||
wrapListener, werr := wrap.Listen(addr, wrapKey)
|
||||
if werr != nil {
|
||||
panic(werr)
|
||||
}
|
||||
|
||||
-198
@@ -1,198 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
dtlsnet "github.com/pion/dtls/v3/pkg/net"
|
||||
pionudp "github.com/pion/transport/v4/udp"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
// Wire format is identical to client — see client/wrap.go. Server sets the
|
||||
// MSB of sessionID/SSRC; client clears it. RTP header fields are per-conn.
|
||||
|
||||
const (
|
||||
wrapKeyLen = 32
|
||||
wrapRTPHdrLen = 12
|
||||
wrapNonceLen = 12
|
||||
wrapTagLen = 16
|
||||
wrapHeaderLen = wrapRTPHdrLen + wrapNonceLen
|
||||
wrapOverhead = wrapHeaderLen + wrapTagLen
|
||||
wrapRTPVersion = 0x80
|
||||
wrapRTPPT = 0x6F
|
||||
wrapTSStep = 960
|
||||
)
|
||||
|
||||
// bufPool eliminates per-packet heap allocation on the hot read/write paths.
|
||||
var bufPool = sync.Pool{
|
||||
New: func() any {
|
||||
b := make([]byte, 1600+wrapOverhead)
|
||||
return &b
|
||||
},
|
||||
}
|
||||
|
||||
// wrapState holds the AEAD instance shared by all connections under one key.
|
||||
type wrapState struct {
|
||||
aead cipher.AEAD
|
||||
}
|
||||
|
||||
func newWrapState(key []byte) (*wrapState, error) {
|
||||
if len(key) != wrapKeyLen {
|
||||
return nil, fmt.Errorf("wrap: key must be %d bytes (got %d)", wrapKeyLen, len(key))
|
||||
}
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wrap: aead init: %w", err)
|
||||
}
|
||||
return &wrapState{aead: aead}, nil
|
||||
}
|
||||
|
||||
// --- Listener ---
|
||||
|
||||
func listenWrapped(addr *net.UDPAddr, key []byte) (dtlsnet.PacketListener, error) {
|
||||
ws, err := newWrapState(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inner, err := pionudp.Listen("udp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wrap: udp listen: %w", err)
|
||||
}
|
||||
return &wrapPacketListener{
|
||||
inner: dtlsnet.PacketListenerFromListener(inner),
|
||||
ws: ws,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type wrapPacketListener struct {
|
||||
inner dtlsnet.PacketListener
|
||||
ws *wrapState
|
||||
}
|
||||
|
||||
func (l *wrapPacketListener) Accept() (net.PacketConn, net.Addr, error) {
|
||||
pc, addr, err := l.inner.Accept()
|
||||
if err != nil {
|
||||
return pc, addr, err
|
||||
}
|
||||
c := &wrapPacketConn{inner: pc, ws: l.ws}
|
||||
|
||||
var rnd [16]byte
|
||||
if _, err := rand.Read(rnd[:]); err != nil {
|
||||
return nil, addr, fmt.Errorf("wrap: rand init: %w", err)
|
||||
}
|
||||
copy(c.sessionID[:], rnd[0:4])
|
||||
copy(c.ssrc[:], rnd[4:8])
|
||||
c.sessionID[0] |= 0x80
|
||||
c.ssrc[0] |= 0x80
|
||||
c.seq.Store(uint32(binary.BigEndian.Uint16(rnd[8:10])))
|
||||
c.timestamp.Store(binary.BigEndian.Uint32(rnd[10:14]))
|
||||
|
||||
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() }
|
||||
func (l *wrapPacketListener) Addr() net.Addr { return l.inner.Addr() }
|
||||
|
||||
// --- Per-peer PacketConn ---
|
||||
|
||||
type wrapPacketConn struct {
|
||||
inner net.PacketConn
|
||||
ws *wrapState
|
||||
sessionID [4]byte
|
||||
ssrc [4]byte
|
||||
counter atomic.Uint64
|
||||
seq atomic.Uint32
|
||||
timestamp atomic.Uint32
|
||||
}
|
||||
|
||||
func (c *wrapPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
bp := bufPool.Get().(*[]byte) //nolint:errcheck // pool New always returns *[]byte
|
||||
buf := *bp
|
||||
need := len(p) + wrapOverhead
|
||||
if cap(buf) < need {
|
||||
buf = make([]byte, need)
|
||||
*bp = buf
|
||||
}
|
||||
defer bufPool.Put(bp)
|
||||
|
||||
n, addr, err := c.inner.ReadFrom(buf[:cap(buf)])
|
||||
if err != nil {
|
||||
return 0, addr, err
|
||||
}
|
||||
wire := buf[:n]
|
||||
if len(wire) < wrapOverhead {
|
||||
return 0, addr, errors.New("wrap: packet too short")
|
||||
}
|
||||
nonce := wire[wrapRTPHdrLen : wrapRTPHdrLen+wrapNonceLen]
|
||||
aad := wire[:wrapHeaderLen]
|
||||
ct := wire[wrapHeaderLen:]
|
||||
|
||||
plain, err := c.ws.aead.Open(ct[:0], nonce, ct, aad)
|
||||
if err != nil {
|
||||
return 0, addr, fmt.Errorf("wrap: AEAD open: %w", err)
|
||||
}
|
||||
if len(plain) > len(p) {
|
||||
return 0, addr, errors.New("wrap: dst buffer too small")
|
||||
}
|
||||
copy(p[:len(plain)], plain)
|
||||
return len(plain), addr, nil
|
||||
}
|
||||
|
||||
func (c *wrapPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||
wireLen := wrapOverhead + len(p)
|
||||
|
||||
bp := bufPool.Get().(*[]byte) //nolint:errcheck // pool New always returns *[]byte
|
||||
out := *bp
|
||||
if cap(out) < wireLen {
|
||||
out = make([]byte, wireLen)
|
||||
*bp = out
|
||||
}
|
||||
out = out[:wireLen]
|
||||
defer bufPool.Put(bp)
|
||||
|
||||
// RTP header.
|
||||
out[0] = wrapRTPVersion
|
||||
out[1] = wrapRTPPT
|
||||
seq := uint16(c.seq.Add(1) - 1)
|
||||
binary.BigEndian.PutUint16(out[2:4], seq)
|
||||
ts := c.timestamp.Add(wrapTSStep) - wrapTSStep
|
||||
binary.BigEndian.PutUint32(out[4:8], ts)
|
||||
copy(out[8:12], c.ssrc[:])
|
||||
|
||||
noncePos := wrapRTPHdrLen
|
||||
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]
|
||||
aad := out[:wrapHeaderLen]
|
||||
ctPos := wrapHeaderLen
|
||||
copy(out[ctPos:], p)
|
||||
c.ws.aead.Seal(out[ctPos:ctPos], nonce, out[ctPos:ctPos+len(p)], aad)
|
||||
|
||||
if _, err := c.inner.WriteTo(out, addr); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (c *wrapPacketConn) Close() error { return c.inner.Close() }
|
||||
func (c *wrapPacketConn) LocalAddr() net.Addr { return c.inner.LocalAddr() }
|
||||
func (c *wrapPacketConn) SetDeadline(t time.Time) error { return c.inner.SetDeadline(t) }
|
||||
func (c *wrapPacketConn) SetReadDeadline(t time.Time) error { return c.inner.SetReadDeadline(t) }
|
||||
func (c *wrapPacketConn) SetWriteDeadline(t time.Time) error { return c.inner.SetWriteDeadline(t) }
|
||||
Reference in New Issue
Block a user