mirror of
https://github.com/uk0/camostream.git
synced 2026-08-29 08:49:44 +00:00
add WebRTC and IPCAM wire formats, encrypt shim header, simulate DTLS
Security fixes: - Encrypt shim header inside AES-GCM (eliminates 0x5C10ADED fingerprint) - XOR magic with session-derived mask when AES disabled - Unified encode/decode path via wire.go New wire formats: - webrtc: SRTP-style 24B RTP header with 0xBEDE extensions (abs-send-time, transport-cc), 10B auth tag, compound RTCP SR+SDES, STUN with FINGERPRINT - ipcam: H.264/RTP surveillance camera simulation with FU-A fragmentation, STAP-A SPS/PPS, GOP state machine (I/P frames) DTLS handshake simulation: - Fake DTLS 1.2 ClientHello/ServerHello/ChangeCipherSpec/Finished - Realistic cipher suites, use_srtp extension, supported_groups DPI score improvement: 34.6 -> 70.4 (webrtc+AES mode)
This commit is contained in:
@@ -0,0 +1,138 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
cryptoRand "crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type aeadBox2 struct{ aead cipher.AEAD }
|
||||||
|
|
||||||
|
func newAEAD2(hexKey string) (*aeadBox2, error) {
|
||||||
|
if strings.TrimSpace(hexKey) == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
kb, err := hex.DecodeString(hexKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch len(kb) {
|
||||||
|
case 16, 24, 32:
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("aes key must be 16/24/32 bytes")
|
||||||
|
}
|
||||||
|
block, err := aes.NewCipher(kb)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
a, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &aeadBox2{aead: a}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *aeadBox2) seal(p []byte) (out []byte, nonce []byte, err error) {
|
||||||
|
if b == nil {
|
||||||
|
return p, nil, nil
|
||||||
|
}
|
||||||
|
nonce = make([]byte, b.aead.NonceSize())
|
||||||
|
if _, err = cryptoRand.Read(nonce); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
c := b.aead.Seal(nil, nonce, p, nil)
|
||||||
|
return c, nonce, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *aeadBox2) open(nonce, c []byte) ([]byte, error) {
|
||||||
|
if b == nil {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
return b.aead.Open(nil, nonce, c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonceLen = 12
|
||||||
|
|
||||||
|
// magicMask derives a 4-byte XOR mask from the session ID so the magic constant
|
||||||
|
// never appears as a fixed pattern on the wire when encryption is disabled.
|
||||||
|
func magicMask(sessionID uint32) uint32 {
|
||||||
|
// simple deterministic mixing: multiply by a large odd prime, rotate
|
||||||
|
m := sessionID * 0x9E3779B9
|
||||||
|
m ^= m >> 16
|
||||||
|
m *= 0x45D9F3B
|
||||||
|
m ^= m >> 16
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// sealFrame encrypts (or obfuscates) the shim header + payload into a wire frame.
|
||||||
|
//
|
||||||
|
// When ae != nil (AES-GCM enabled):
|
||||||
|
//
|
||||||
|
// output = nonce(12) || AEAD(shimHeader || payload)
|
||||||
|
//
|
||||||
|
// When ae == nil (no encryption):
|
||||||
|
//
|
||||||
|
// output = shimHeader(magic XORed with session mask) || payload
|
||||||
|
func sealFrame(ae *aeadBox, hdr shimHeader, payload []byte) ([]byte, error) {
|
||||||
|
inner := make([]byte, shimLen+len(payload))
|
||||||
|
copy(inner, hdr.Marshal())
|
||||||
|
copy(inner[shimLen:], payload)
|
||||||
|
|
||||||
|
if ae != nil {
|
||||||
|
nonce := make([]byte, nonceLen)
|
||||||
|
if _, err := cryptoRand.Read(nonce); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ct := ae.aead.Seal(nil, nonce, inner, nil)
|
||||||
|
out := make([]byte, nonceLen+len(ct))
|
||||||
|
copy(out, nonce)
|
||||||
|
copy(out[nonceLen:], ct)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// No encryption: XOR the magic so it is not a static fingerprint.
|
||||||
|
mask := magicMask(hdr.SessionID)
|
||||||
|
binary.BigEndian.PutUint32(inner[0:4], hdr.Magic^mask)
|
||||||
|
return inner, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// openFrame decrypts (or de-obfuscates) a wire frame back into shimHeader + payload.
|
||||||
|
//
|
||||||
|
// When ae != nil:
|
||||||
|
//
|
||||||
|
// expects data = nonce(12) || ciphertext
|
||||||
|
//
|
||||||
|
// When ae == nil:
|
||||||
|
//
|
||||||
|
// expects data = shimHeader(magic XORed) || payload
|
||||||
|
func openFrame(ae *aeadBox, data []byte, sessionHint uint32) (shimHeader, []byte, error) {
|
||||||
|
if ae != nil {
|
||||||
|
if len(data) < nonceLen {
|
||||||
|
return shimHeader{}, nil, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
nonce := data[:nonceLen]
|
||||||
|
ct := data[nonceLen:]
|
||||||
|
plain, err := ae.aead.Open(nil, nonce, ct, nil)
|
||||||
|
if err != nil {
|
||||||
|
return shimHeader{}, nil, fmt.Errorf("aead open: %w", err)
|
||||||
|
}
|
||||||
|
return parseShimFull(plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No encryption: un-XOR the magic first.
|
||||||
|
if len(data) < shimLen {
|
||||||
|
return shimHeader{}, nil, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
// work on a copy so we don't mutate the caller's buffer
|
||||||
|
buf := make([]byte, len(data))
|
||||||
|
copy(buf, data)
|
||||||
|
mask := magicMask(sessionHint)
|
||||||
|
raw := binary.BigEndian.Uint32(buf[0:4])
|
||||||
|
binary.BigEndian.PutUint32(buf[0:4], raw^mask)
|
||||||
|
return parseShimFull(buf)
|
||||||
|
}
|
||||||
@@ -0,0 +1,407 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
cryptoRand "crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DTLS record header offsets and constants
|
||||||
|
const (
|
||||||
|
dtlsContentHandshake = 22
|
||||||
|
dtlsContentChangeCipher = 20
|
||||||
|
dtlsContentAppData = 23
|
||||||
|
dtlsVersion12 = 0xFEFD // DTLS 1.2
|
||||||
|
dtlsRecordHeaderLen = 13
|
||||||
|
dtlsHandshakeHeaderLen = 12
|
||||||
|
dtlsHandshakeTimeout = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handshake types
|
||||||
|
const (
|
||||||
|
dtlsHSClientHello = 1
|
||||||
|
dtlsHSServerHello = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// putDTLSRecordHeader writes a DTLS record header into dst (must be >= 13 bytes).
|
||||||
|
func putDTLSRecordHeader(dst []byte, contentType uint8, epoch uint16, seq uint64, payloadLen int) {
|
||||||
|
dst[0] = contentType
|
||||||
|
binary.BigEndian.PutUint16(dst[1:3], dtlsVersion12)
|
||||||
|
binary.BigEndian.PutUint16(dst[3:5], epoch)
|
||||||
|
// 48-bit sequence number
|
||||||
|
dst[5] = byte(seq >> 40)
|
||||||
|
dst[6] = byte(seq >> 32)
|
||||||
|
dst[7] = byte(seq >> 24)
|
||||||
|
dst[8] = byte(seq >> 16)
|
||||||
|
dst[9] = byte(seq >> 8)
|
||||||
|
dst[10] = byte(seq)
|
||||||
|
binary.BigEndian.PutUint16(dst[11:13], uint16(payloadLen))
|
||||||
|
}
|
||||||
|
|
||||||
|
// putHandshakeHeader writes a DTLS handshake message header.
|
||||||
|
func putHandshakeHeader(dst []byte, hsType uint8, length int, msgSeq uint16, fragOff, fragLen int) {
|
||||||
|
dst[0] = hsType
|
||||||
|
// 24-bit length
|
||||||
|
dst[1] = byte(length >> 16)
|
||||||
|
dst[2] = byte(length >> 8)
|
||||||
|
dst[3] = byte(length)
|
||||||
|
binary.BigEndian.PutUint16(dst[4:6], msgSeq)
|
||||||
|
// 24-bit fragment offset
|
||||||
|
dst[6] = byte(fragOff >> 16)
|
||||||
|
dst[7] = byte(fragOff >> 8)
|
||||||
|
dst[8] = byte(fragOff)
|
||||||
|
// 24-bit fragment length
|
||||||
|
dst[9] = byte(fragLen >> 16)
|
||||||
|
dst[10] = byte(fragLen >> 8)
|
||||||
|
dst[11] = byte(fragLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDTLSClientHello constructs a realistic DTLS 1.2 ClientHello (~250 bytes).
|
||||||
|
func buildDTLSClientHello(random []byte) []byte {
|
||||||
|
// Body: version(2) + random(32) + sessionID(1+32) + cookie(1+0) +
|
||||||
|
// cipherSuites(2+N*2) + compressionMethods(1+1) + extensions
|
||||||
|
var body []byte
|
||||||
|
|
||||||
|
// client version
|
||||||
|
body = append(body, 0xFE, 0xFD)
|
||||||
|
|
||||||
|
// random (32 bytes); pad/truncate caller input
|
||||||
|
r := make([]byte, 32)
|
||||||
|
copy(r, random)
|
||||||
|
body = append(body, r...)
|
||||||
|
|
||||||
|
// session id: 32-byte random
|
||||||
|
sid := make([]byte, 32)
|
||||||
|
cryptoRand.Read(sid)
|
||||||
|
body = append(body, 32)
|
||||||
|
body = append(body, sid...)
|
||||||
|
|
||||||
|
// cookie: empty
|
||||||
|
body = append(body, 0)
|
||||||
|
|
||||||
|
// cipher suites
|
||||||
|
suites := []uint16{
|
||||||
|
0xC02B, // TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||||
|
0xC02F, // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||||||
|
0xC02C, // TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
|
||||||
|
0xCCA9, // TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
|
||||||
|
0xCCA8, // TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
|
||||||
|
0x00FF, // TLS_EMPTY_RENEGOTIATION_INFO_SCSV
|
||||||
|
}
|
||||||
|
binary.BigEndian.AppendUint16(nil, 0) // placeholder
|
||||||
|
sLen := len(suites) * 2
|
||||||
|
body = append(body, byte(sLen>>8), byte(sLen))
|
||||||
|
for _, s := range suites {
|
||||||
|
body = append(body, byte(s>>8), byte(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// compression methods: null only
|
||||||
|
body = append(body, 1, 0)
|
||||||
|
|
||||||
|
// extensions
|
||||||
|
var exts []byte
|
||||||
|
|
||||||
|
// use_srtp extension (type 0x000E)
|
||||||
|
srtpProfiles := []uint16{0x0001} // SRTP_AES128_CM_HMAC_SHA1_80
|
||||||
|
srtpBody := make([]byte, 2+len(srtpProfiles)*2+1)
|
||||||
|
binary.BigEndian.PutUint16(srtpBody[0:2], uint16(len(srtpProfiles)*2))
|
||||||
|
for i, p := range srtpProfiles {
|
||||||
|
binary.BigEndian.PutUint16(srtpBody[2+i*2:4+i*2], p)
|
||||||
|
}
|
||||||
|
srtpBody[len(srtpBody)-1] = 0 // mki length
|
||||||
|
exts = appendExtension(exts, 0x000E, srtpBody)
|
||||||
|
|
||||||
|
// supported_groups (type 0x000A): x25519, secp256r1, secp384r1
|
||||||
|
groups := []uint16{0x001D, 0x0017, 0x0018}
|
||||||
|
gBody := make([]byte, 2+len(groups)*2)
|
||||||
|
binary.BigEndian.PutUint16(gBody[0:2], uint16(len(groups)*2))
|
||||||
|
for i, g := range groups {
|
||||||
|
binary.BigEndian.PutUint16(gBody[2+i*2:4+i*2], g)
|
||||||
|
}
|
||||||
|
exts = appendExtension(exts, 0x000A, gBody)
|
||||||
|
|
||||||
|
// ec_point_formats (type 0x000B)
|
||||||
|
exts = appendExtension(exts, 0x000B, []byte{1, 0}) // uncompressed
|
||||||
|
|
||||||
|
// signature_algorithms (type 0x000D)
|
||||||
|
sigAlgs := []uint16{0x0403, 0x0503, 0x0603, 0x0804, 0x0805, 0x0806, 0x0401, 0x0501, 0x0601}
|
||||||
|
saBody := make([]byte, 2+len(sigAlgs)*2)
|
||||||
|
binary.BigEndian.PutUint16(saBody[0:2], uint16(len(sigAlgs)*2))
|
||||||
|
for i, sa := range sigAlgs {
|
||||||
|
binary.BigEndian.PutUint16(saBody[2+i*2:4+i*2], sa)
|
||||||
|
}
|
||||||
|
exts = appendExtension(exts, 0x000D, saBody)
|
||||||
|
|
||||||
|
// extensions length prefix
|
||||||
|
body = append(body, byte(len(exts)>>8), byte(len(exts)))
|
||||||
|
body = append(body, exts...)
|
||||||
|
|
||||||
|
// wrap in handshake header + record header
|
||||||
|
hsPayload := make([]byte, dtlsHandshakeHeaderLen+len(body))
|
||||||
|
putHandshakeHeader(hsPayload, dtlsHSClientHello, len(body), 0, 0, len(body))
|
||||||
|
copy(hsPayload[dtlsHandshakeHeaderLen:], body)
|
||||||
|
|
||||||
|
pkt := make([]byte, dtlsRecordHeaderLen+len(hsPayload))
|
||||||
|
putDTLSRecordHeader(pkt, dtlsContentHandshake, 0, 0, len(hsPayload))
|
||||||
|
copy(pkt[dtlsRecordHeaderLen:], hsPayload)
|
||||||
|
return pkt
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDTLSServerHello constructs a DTLS 1.2 ServerHello (~120 bytes).
|
||||||
|
func buildDTLSServerHello(random []byte) []byte {
|
||||||
|
var body []byte
|
||||||
|
|
||||||
|
// server version
|
||||||
|
body = append(body, 0xFE, 0xFD)
|
||||||
|
|
||||||
|
// random (32 bytes)
|
||||||
|
r := make([]byte, 32)
|
||||||
|
copy(r, random)
|
||||||
|
body = append(body, r...)
|
||||||
|
|
||||||
|
// session id (32 bytes, echo a random one)
|
||||||
|
sid := make([]byte, 32)
|
||||||
|
cryptoRand.Read(sid)
|
||||||
|
body = append(body, 32)
|
||||||
|
body = append(body, sid...)
|
||||||
|
|
||||||
|
// selected cipher suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||||
|
body = append(body, 0xC0, 0x2B)
|
||||||
|
|
||||||
|
// compression method: null
|
||||||
|
body = append(body, 0)
|
||||||
|
|
||||||
|
// extensions: use_srtp
|
||||||
|
var exts []byte
|
||||||
|
srtpBody := []byte{0x00, 0x02, 0x00, 0x01, 0x00} // profile=0x0001, mki=0
|
||||||
|
exts = appendExtension(exts, 0x000E, srtpBody)
|
||||||
|
|
||||||
|
body = append(body, byte(len(exts)>>8), byte(len(exts)))
|
||||||
|
body = append(body, exts...)
|
||||||
|
|
||||||
|
hsPayload := make([]byte, dtlsHandshakeHeaderLen+len(body))
|
||||||
|
putHandshakeHeader(hsPayload, dtlsHSServerHello, len(body), 1, 0, len(body))
|
||||||
|
copy(hsPayload[dtlsHandshakeHeaderLen:], body)
|
||||||
|
|
||||||
|
pkt := make([]byte, dtlsRecordHeaderLen+len(hsPayload))
|
||||||
|
putDTLSRecordHeader(pkt, dtlsContentHandshake, 0, 1, len(hsPayload))
|
||||||
|
copy(pkt[dtlsRecordHeaderLen:], hsPayload)
|
||||||
|
return pkt
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDTLSChangeCipherSpec builds a DTLS ChangeCipherSpec record (14 bytes total).
|
||||||
|
func buildDTLSChangeCipherSpec() []byte {
|
||||||
|
pkt := make([]byte, dtlsRecordHeaderLen+1)
|
||||||
|
putDTLSRecordHeader(pkt, dtlsContentChangeCipher, 0, 2, 1)
|
||||||
|
pkt[dtlsRecordHeaderLen] = 0x01
|
||||||
|
return pkt
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDTLSFinished builds a fake encrypted Finished record (~50-60 bytes).
|
||||||
|
func buildDTLSFinished(random []byte) []byte {
|
||||||
|
// simulate encrypted payload (40-60 bytes random data)
|
||||||
|
pLen := 40 + int(random[0]%21) // 40..60
|
||||||
|
payload := make([]byte, pLen)
|
||||||
|
cryptoRand.Read(payload)
|
||||||
|
|
||||||
|
pkt := make([]byte, dtlsRecordHeaderLen+pLen)
|
||||||
|
putDTLSRecordHeader(pkt, dtlsContentAppData, 1, 0, pLen)
|
||||||
|
copy(pkt[dtlsRecordHeaderLen:], payload)
|
||||||
|
return pkt
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDTLSPacket checks if the first byte indicates a DTLS record (content types 20-63).
|
||||||
|
func isDTLSPacket(data []byte) bool {
|
||||||
|
if len(data) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return data[0] >= 20 && data[0] <= 63
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendExtension appends a TLS extension (type + length-prefixed data).
|
||||||
|
func appendExtension(buf []byte, extType uint16, data []byte) []byte {
|
||||||
|
buf = append(buf, byte(extType>>8), byte(extType))
|
||||||
|
buf = append(buf, byte(len(data)>>8), byte(len(data)))
|
||||||
|
buf = append(buf, data...)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// performDTLSHandshake runs a fake DTLS 1.2 handshake over the given UDP connection.
|
||||||
|
// isServer: true for the responder side, false for the initiator.
|
||||||
|
// pcap: optional pcap writer (may be nil).
|
||||||
|
func performDTLSHandshake(conn *net.UDPConn, peer *net.UDPAddr, isServer bool, pcap *pcapWriter) error {
|
||||||
|
rnd := make([]byte, 32)
|
||||||
|
cryptoRand.Read(rnd)
|
||||||
|
|
||||||
|
if !isServer {
|
||||||
|
return dtlsClientHandshake(conn, peer, rnd, pcap)
|
||||||
|
}
|
||||||
|
return dtlsServerHandshake(conn, peer, rnd, pcap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dtlsClientHandshake(conn *net.UDPConn, peer *net.UDPAddr, rnd []byte, pcap *pcapWriter) error {
|
||||||
|
// 1. Send ClientHello
|
||||||
|
ch := buildDTLSClientHello(rnd)
|
||||||
|
if _, err := conn.WriteToUDP(ch, peer); err != nil {
|
||||||
|
return fmt.Errorf("dtls: send ClientHello: %w", err)
|
||||||
|
}
|
||||||
|
dtlsWritePcap(pcap, conn.LocalAddr(), peer, ch)
|
||||||
|
logf(LDebug, "dtls: sent ClientHello (%d bytes)", len(ch))
|
||||||
|
|
||||||
|
// 2. Wait for ServerHello
|
||||||
|
conn.SetReadDeadline(time.Now().Add(dtlsHandshakeTimeout))
|
||||||
|
buf := make([]byte, 2048)
|
||||||
|
for {
|
||||||
|
n, from, err := conn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dtls: wait ServerHello: %w", err)
|
||||||
|
}
|
||||||
|
if from.String() != peer.String() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data := buf[:n]
|
||||||
|
dtlsWritePcap(pcap, from, conn.LocalAddr(), data)
|
||||||
|
if len(data) >= dtlsRecordHeaderLen && data[0] == dtlsContentHandshake {
|
||||||
|
logf(LDebug, "dtls: recv ServerHello (%d bytes)", n)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Wait for server ChangeCipherSpec
|
||||||
|
conn.SetReadDeadline(time.Now().Add(dtlsHandshakeTimeout))
|
||||||
|
for {
|
||||||
|
n, from, err := conn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dtls: wait ChangeCipherSpec: %w", err)
|
||||||
|
}
|
||||||
|
if from.String() != peer.String() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data := buf[:n]
|
||||||
|
dtlsWritePcap(pcap, from, conn.LocalAddr(), data)
|
||||||
|
if len(data) >= dtlsRecordHeaderLen && data[0] == dtlsContentChangeCipher {
|
||||||
|
logf(LDebug, "dtls: recv ChangeCipherSpec (%d bytes)", n)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Wait for server Finished
|
||||||
|
conn.SetReadDeadline(time.Now().Add(dtlsHandshakeTimeout))
|
||||||
|
for {
|
||||||
|
n, from, err := conn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dtls: wait server Finished: %w", err)
|
||||||
|
}
|
||||||
|
if from.String() != peer.String() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data := buf[:n]
|
||||||
|
dtlsWritePcap(pcap, from, conn.LocalAddr(), data)
|
||||||
|
if len(data) >= dtlsRecordHeaderLen && data[0] == dtlsContentAppData {
|
||||||
|
logf(LDebug, "dtls: recv server Finished (%d bytes)", n)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Send client ChangeCipherSpec + Finished
|
||||||
|
ccs := buildDTLSChangeCipherSpec()
|
||||||
|
if _, err := conn.WriteToUDP(ccs, peer); err != nil {
|
||||||
|
return fmt.Errorf("dtls: send ChangeCipherSpec: %w", err)
|
||||||
|
}
|
||||||
|
dtlsWritePcap(pcap, conn.LocalAddr(), peer, ccs)
|
||||||
|
logf(LDebug, "dtls: sent ChangeCipherSpec (%d bytes)", len(ccs))
|
||||||
|
|
||||||
|
fin := buildDTLSFinished(rnd)
|
||||||
|
if _, err := conn.WriteToUDP(fin, peer); err != nil {
|
||||||
|
return fmt.Errorf("dtls: send Finished: %w", err)
|
||||||
|
}
|
||||||
|
dtlsWritePcap(pcap, conn.LocalAddr(), peer, fin)
|
||||||
|
logf(LDebug, "dtls: sent Finished (%d bytes)", len(fin))
|
||||||
|
|
||||||
|
conn.SetReadDeadline(time.Time{})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dtlsServerHandshake(conn *net.UDPConn, peer *net.UDPAddr, rnd []byte, pcap *pcapWriter) error {
|
||||||
|
buf := make([]byte, 2048)
|
||||||
|
|
||||||
|
// 1. Wait for ClientHello
|
||||||
|
conn.SetReadDeadline(time.Now().Add(dtlsHandshakeTimeout))
|
||||||
|
for {
|
||||||
|
n, from, err := conn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dtls: wait ClientHello: %w", err)
|
||||||
|
}
|
||||||
|
if from.String() != peer.String() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data := buf[:n]
|
||||||
|
dtlsWritePcap(pcap, from, conn.LocalAddr(), data)
|
||||||
|
if len(data) >= dtlsRecordHeaderLen && data[0] == dtlsContentHandshake {
|
||||||
|
logf(LDebug, "dtls: recv ClientHello (%d bytes)", n)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Send ServerHello
|
||||||
|
sh := buildDTLSServerHello(rnd)
|
||||||
|
if _, err := conn.WriteToUDP(sh, peer); err != nil {
|
||||||
|
return fmt.Errorf("dtls: send ServerHello: %w", err)
|
||||||
|
}
|
||||||
|
dtlsWritePcap(pcap, conn.LocalAddr(), peer, sh)
|
||||||
|
logf(LDebug, "dtls: sent ServerHello (%d bytes)", len(sh))
|
||||||
|
|
||||||
|
// 3. Send ChangeCipherSpec + Finished
|
||||||
|
ccs := buildDTLSChangeCipherSpec()
|
||||||
|
if _, err := conn.WriteToUDP(ccs, peer); err != nil {
|
||||||
|
return fmt.Errorf("dtls: send ChangeCipherSpec: %w", err)
|
||||||
|
}
|
||||||
|
dtlsWritePcap(pcap, conn.LocalAddr(), peer, ccs)
|
||||||
|
logf(LDebug, "dtls: sent ChangeCipherSpec (%d bytes)", len(ccs))
|
||||||
|
|
||||||
|
fin := buildDTLSFinished(rnd)
|
||||||
|
if _, err := conn.WriteToUDP(fin, peer); err != nil {
|
||||||
|
return fmt.Errorf("dtls: send Finished: %w", err)
|
||||||
|
}
|
||||||
|
dtlsWritePcap(pcap, conn.LocalAddr(), peer, fin)
|
||||||
|
logf(LDebug, "dtls: sent Finished (%d bytes)", len(fin))
|
||||||
|
|
||||||
|
// 4. Wait for client ChangeCipherSpec + Finished
|
||||||
|
conn.SetReadDeadline(time.Now().Add(dtlsHandshakeTimeout))
|
||||||
|
got := 0
|
||||||
|
for got < 2 {
|
||||||
|
n, from, err := conn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dtls: wait client finish: %w", err)
|
||||||
|
}
|
||||||
|
if from.String() != peer.String() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data := buf[:n]
|
||||||
|
dtlsWritePcap(pcap, from, conn.LocalAddr(), data)
|
||||||
|
if len(data) >= dtlsRecordHeaderLen {
|
||||||
|
if data[0] == dtlsContentChangeCipher || data[0] == dtlsContentAppData {
|
||||||
|
got++
|
||||||
|
logf(LDebug, "dtls: recv client handshake pkt type=%d (%d bytes)", data[0], n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.SetReadDeadline(time.Time{})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dtlsWritePcap records a DTLS packet to pcap if the writer is available.
|
||||||
|
func dtlsWritePcap(pcap *pcapWriter, src, dst net.Addr, data []byte) {
|
||||||
|
if pcap == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
srcU, okS := src.(*net.UDPAddr)
|
||||||
|
dstU, okD := dst.(*net.UDPAddr)
|
||||||
|
if !okS || !okD {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pcap.WriteUDP(srcU.IP, srcU.Port, dstU.IP, dstU.Port, data)
|
||||||
|
}
|
||||||
@@ -46,6 +46,8 @@ var (
|
|||||||
metricRTPkeepSent = expvar.NewInt("rtp_keepalive_sent")
|
metricRTPkeepSent = expvar.NewInt("rtp_keepalive_sent")
|
||||||
metricSTUNSent = expvar.NewInt("stun_sent")
|
metricSTUNSent = expvar.NewInt("stun_sent")
|
||||||
metricShimDecoySent = expvar.NewInt("shim_decoy_sent")
|
metricShimDecoySent = expvar.NewInt("shim_decoy_sent")
|
||||||
|
metricDTLSSent = expvar.NewInt("dtls_handshake_sent")
|
||||||
|
metricAudioSent = expvar.NewInt("audio_packets_sent")
|
||||||
)
|
)
|
||||||
|
|
||||||
/* ============ CLI / Config ============ */
|
/* ============ CLI / Config ============ */
|
||||||
@@ -64,6 +66,8 @@ const (
|
|||||||
|
|
||||||
WireShim Wire = "shim" // ShimHeader + Payload
|
WireShim Wire = "shim" // ShimHeader + Payload
|
||||||
WireRTPish Wire = "rtpish" // RTP(12) + Shim + Payload (UDP only)
|
WireRTPish Wire = "rtpish" // RTP(12) + Shim + Payload (UDP only)
|
||||||
|
WireWebRTC Wire = "webrtc" // DTLS+SRTP(24)+Shim+Payload+AuthTag (UDP only)
|
||||||
|
WireIPCam Wire = "ipcam" // H.264/RTP surveillance camera (UDP only)
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -106,6 +110,15 @@ type Config struct {
|
|||||||
SelfDur time.Duration
|
SelfDur time.Duration
|
||||||
LogLevel string
|
LogLevel string
|
||||||
LogDrop bool
|
LogDrop bool
|
||||||
|
|
||||||
|
// WebRTC mode options
|
||||||
|
EnableDTLS bool // simulate DTLS handshake at session start
|
||||||
|
AudioRps int // audio packets per second (default 50 for Opus 20ms)
|
||||||
|
STUNInterval int // STUN consent freshness interval in seconds (default 5)
|
||||||
|
|
||||||
|
// IPCAM mode options
|
||||||
|
IPCAMFPS int // surveillance camera FPS (default 25)
|
||||||
|
IPCAMGop int // GOP size in frames (default 50 = 2 seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultConfig() *Config {
|
func defaultConfig() *Config {
|
||||||
@@ -149,6 +162,12 @@ func defaultConfig() *Config {
|
|||||||
SelfDur: 15 * time.Second,
|
SelfDur: 15 * time.Second,
|
||||||
LogLevel: "info",
|
LogLevel: "info",
|
||||||
LogDrop: false,
|
LogDrop: false,
|
||||||
|
|
||||||
|
EnableDTLS: true,
|
||||||
|
AudioRps: 50,
|
||||||
|
STUNInterval: 5,
|
||||||
|
IPCAMFPS: 25,
|
||||||
|
IPCAMGop: 50,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +178,7 @@ func parseFlags() *Config {
|
|||||||
flag.StringVar(&cfg.Listen, "listen", cfg.Listen, "listen addr")
|
flag.StringVar(&cfg.Listen, "listen", cfg.Listen, "listen addr")
|
||||||
flag.StringVar(&cfg.ServerAddr, "server", cfg.ServerAddr, "server addr (client)")
|
flag.StringVar(&cfg.ServerAddr, "server", cfg.ServerAddr, "server addr (client)")
|
||||||
flag.StringVar(&cfg.ForwardAddr, "forward", cfg.ForwardAddr, "forward addr (server)")
|
flag.StringVar(&cfg.ForwardAddr, "forward", cfg.ForwardAddr, "forward addr (server)")
|
||||||
flag.StringVar((*string)(&cfg.Wire), "wire", string(cfg.Wire), "shim|rtpish (udp)")
|
flag.StringVar((*string)(&cfg.Wire), "wire", string(cfg.Wire), "shim|rtpish|webrtc|ipcam (udp)")
|
||||||
flag.IntVar(&cfg.FPS, "fps", cfg.FPS, "60|120")
|
flag.IntVar(&cfg.FPS, "fps", cfg.FPS, "60|120")
|
||||||
flag.IntVar(&cfg.GOPMs, "gop", cfg.GOPMs, "keyframe interval ms")
|
flag.IntVar(&cfg.GOPMs, "gop", cfg.GOPMs, "keyframe interval ms")
|
||||||
flag.IntVar(&cfg.BitrateMbps, "bitrate-mbps", cfg.BitrateMbps, "20|40 etc.")
|
flag.IntVar(&cfg.BitrateMbps, "bitrate-mbps", cfg.BitrateMbps, "20|40 etc.")
|
||||||
@@ -198,12 +217,21 @@ func parseFlags() *Config {
|
|||||||
flag.DurationVar(&cfg.SelfDur, "duration", cfg.SelfDur, "selftest duration")
|
flag.DurationVar(&cfg.SelfDur, "duration", cfg.SelfDur, "selftest duration")
|
||||||
flag.StringVar(&cfg.LogLevel, "log", cfg.LogLevel, "debug|info|warn|error")
|
flag.StringVar(&cfg.LogLevel, "log", cfg.LogLevel, "debug|info|warn|error")
|
||||||
flag.BoolVar(&cfg.LogDrop, "showdrop", cfg.LogDrop, "log when dropping decoy")
|
flag.BoolVar(&cfg.LogDrop, "showdrop", cfg.LogDrop, "log when dropping decoy")
|
||||||
|
|
||||||
|
// WebRTC mode options
|
||||||
|
flag.BoolVar(&cfg.EnableDTLS, "dtls", cfg.EnableDTLS, "simulate DTLS handshake (webrtc mode)")
|
||||||
|
flag.IntVar(&cfg.AudioRps, "audio-rps", cfg.AudioRps, "audio packets per second (webrtc mode, default 50)")
|
||||||
|
flag.IntVar(&cfg.STUNInterval, "stun-interval", cfg.STUNInterval, "STUN consent interval seconds (webrtc mode)")
|
||||||
|
|
||||||
|
// IPCAM mode options
|
||||||
|
flag.IntVar(&cfg.IPCAMFPS, "ipcam-fps", cfg.IPCAMFPS, "surveillance camera FPS (ipcam mode, default 25)")
|
||||||
|
flag.IntVar(&cfg.IPCAMGop, "ipcam-gop", cfg.IPCAMGop, "GOP size in frames (ipcam mode, default 50)")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
cfg.SessID = uint32(sessFlag)
|
cfg.SessID = uint32(sessFlag)
|
||||||
if cfg.Mode == ModeTCP && cfg.Wire == WireRTPish {
|
if cfg.Mode == ModeTCP && cfg.Wire != WireShim {
|
||||||
// 仅提示:RTP-ish 只在 UDP 生效
|
fmt.Printf("[WARN] wire=%s is only effective in UDP mode, falling back to shim for TCP.\n", cfg.Wire)
|
||||||
fmt.Println("[WARN] wire=rtpish is ignored in TCP mode.")
|
|
||||||
}
|
}
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
@@ -980,26 +1008,15 @@ func runUDPClient(ctx context.Context, rt *runtimeCtx) error {
|
|||||||
dst := ip4OrLoopback(uc.LocalAddr().(*net.UDPAddr).IP)
|
dst := ip4OrLoopback(uc.LocalAddr().(*net.UDPAddr).IP)
|
||||||
rt.pc.WriteUDP(src, from.Port, dst, uc.LocalAddr().(*net.UDPAddr).Port, raw)
|
rt.pc.WriteUDP(src, from.Port, dst, uc.LocalAddr().(*net.UDPAddr).Port, raw)
|
||||||
|
|
||||||
var payload []byte
|
// Skip DTLS/STUN control packets (not data)
|
||||||
if rt.cfg.Wire == WireRTPish {
|
if isDTLSRange(raw[0]) || isSTUNPacket(raw) { continue }
|
||||||
_, _, rest, e := stripRTP(raw); if e != nil { continue }
|
|
||||||
h, p, e2 := parseShimFull(rest); if e2 != nil { continue }
|
h, payload, e2 := decodeUDPFrame(rt.cfg, rt.ae, raw, sess)
|
||||||
if (h.Flags & flagDecoy) != 0 { metricDecoyDropped.Add(1); if rt.dropLog { logf(LDebug, "[client] drop decoy %dB", len(p)) }; continue }
|
if e2 != nil { continue }
|
||||||
payload = p
|
if (h.Flags & flagDecoy) != 0 {
|
||||||
if (h.Flags & flagEnc) != 0 && rt.ae != nil {
|
metricDecoyDropped.Add(1)
|
||||||
ns := rt.ae.aead.NonceSize(); if len(payload) >= ns {
|
if rt.dropLog { logf(LDebug, "[client] drop decoy %dB", len(payload)) }
|
||||||
if plain, e := rt.ae.open(payload[:ns], payload[ns:]); e == nil { payload = plain }
|
continue
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
h, p, e2 := parseShimFull(raw); if e2 != nil { continue }
|
|
||||||
if (h.Flags & flagDecoy) != 0 { metricDecoyDropped.Add(1); if rt.dropLog { logf(LDebug, "[client] drop decoy %dB", len(p)) }; continue }
|
|
||||||
payload = p
|
|
||||||
if (h.Flags & flagEnc) != 0 && rt.ae != nil {
|
|
||||||
ns := rt.ae.aead.NonceSize(); if len(payload) >= ns {
|
|
||||||
if plain, e := rt.ae.open(payload[:ns], payload[ns:]); e == nil { payload = plain }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
appPeerMu.RLock(); dstPeer := appPeer; appPeerMu.RUnlock()
|
appPeerMu.RLock(); dstPeer := appPeer; appPeerMu.RUnlock()
|
||||||
if dstPeer != nil {
|
if dstPeer != nil {
|
||||||
@@ -1013,21 +1030,9 @@ func runUDPClient(ctx context.Context, rt *runtimeCtx) error {
|
|||||||
appPeerMu.Lock(); appPeer = from; appPeerMu.Unlock()
|
appPeerMu.Lock(); appPeer = from; appPeerMu.Unlock()
|
||||||
p := append([]byte(nil), buf[:n]...)
|
p := append([]byte(nil), buf[:n]...)
|
||||||
|
|
||||||
// a) 真实帧(带 shim)
|
// a) 真实帧
|
||||||
flags := uint8(0)
|
var twccSeq uint32
|
||||||
pp := p
|
frame := encodeUDPFrame(rt.cfg, rt.ae, &rtp, &rtpTs, step, sess, p, false, &twccSeq)
|
||||||
if rt.ae != nil {
|
|
||||||
if c, nonce, e := rt.ae.seal(p); e == nil { pp = append(nonce, c...); flags |= flagEnc }
|
|
||||||
}
|
|
||||||
h := shimHeader{Magic: magicConst, Version: version, Mode: modeUDP, Flags: flags,
|
|
||||||
SessionID: sess, TsMs: uint32(time.Now().UnixMilli()), Len: uint32(len(pp))}
|
|
||||||
frame := append(h.Marshal(), pp...)
|
|
||||||
if rt.cfg.Wire == WireRTPish {
|
|
||||||
rtpTs += uint32(step)
|
|
||||||
rtpHdr := buildRTPHeader(rtp.seq, rtpTs, 96, false, rtp.ssrc)
|
|
||||||
rtp.seq++
|
|
||||||
frame = append(rtpHdr, frame...)
|
|
||||||
}
|
|
||||||
rt.tb.wait(len(frame) + 28)
|
rt.tb.wait(len(frame) + 28)
|
||||||
_, _ = uc.WriteToUDP(frame, saddr)
|
_, _ = uc.WriteToUDP(frame, saddr)
|
||||||
metricFramesUp.Add(1); metricBytesUp.Add(int64(n))
|
metricFramesUp.Add(1); metricBytesUp.Add(int64(n))
|
||||||
@@ -1035,30 +1040,14 @@ func runUDPClient(ctx context.Context, rt *runtimeCtx) error {
|
|||||||
dstIP := ip4OrLoopback(saddr.IP)
|
dstIP := ip4OrLoopback(saddr.IP)
|
||||||
rt.pc.WriteUDP(srcIP, uc.LocalAddr().(*net.UDPAddr).Port, dstIP, saddr.Port, frame)
|
rt.pc.WriteUDP(srcIP, uc.LocalAddr().(*net.UDPAddr).Port, dstIP, saddr.Port, frame)
|
||||||
|
|
||||||
// b) shim 诱饵(带 shim,RPS 优先,否则按百分比)
|
// b) shim 诱饵
|
||||||
sendDecoy := false
|
sendDecoy := false
|
||||||
if rt.shimUpRL != nil && rt.shimUpRL.takeMax(1) > 0 { sendDecoy = true
|
if rt.shimUpRL != nil && rt.shimUpRL.takeMax(1) > 0 { sendDecoy = true
|
||||||
} else if rt.cfg.DecoyRps <= 0 && grnd.Intn(100) < rt.cfg.DecoyPct { sendDecoy = true }
|
} else if rt.cfg.DecoyRps <= 0 && grnd.Intn(100) < rt.cfg.DecoyPct { sendDecoy = true }
|
||||||
if sendDecoy {
|
if sendDecoy {
|
||||||
djunk := make([]byte, len(p))
|
djunk := make([]byte, len(p))
|
||||||
_, _ = cryptoRand.Read(djunk)
|
_, _ = cryptoRand.Read(djunk)
|
||||||
decFlags := uint8(flagDecoy)
|
df := encodeUDPFrame(rt.cfg, rt.ae, &rtp, &rtpTs, step, sess, djunk, true, &twccSeq)
|
||||||
decPayload := djunk
|
|
||||||
if rt.ae != nil {
|
|
||||||
if c, nonce, e := rt.ae.seal(decPayload); e == nil {
|
|
||||||
decPayload = append(nonce, c...)
|
|
||||||
decFlags |= flagEnc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hd := shimHeader{Magic: magicConst, Version: version, Mode: modeUDP, Flags: decFlags,
|
|
||||||
SessionID: sess, TsMs: uint32(time.Now().UnixMilli()), Len: uint32(len(decPayload))}
|
|
||||||
df := append(hd.Marshal(), decPayload...)
|
|
||||||
if rt.cfg.Wire == WireRTPish {
|
|
||||||
rtpTs += uint32(step)
|
|
||||||
rtpHdr := buildRTPHeader(rtp.seq, rtpTs, 96, false, rtp.ssrc)
|
|
||||||
rtp.seq++
|
|
||||||
df = append(rtpHdr, df...)
|
|
||||||
}
|
|
||||||
rt.tb.wait(len(df) + 28)
|
rt.tb.wait(len(df) + 28)
|
||||||
_, _ = uc.WriteToUDP(df, saddr)
|
_, _ = uc.WriteToUDP(df, saddr)
|
||||||
metricFramesUp.Add(1); metricShimDecoySent.Add(1)
|
metricFramesUp.Add(1); metricShimDecoySent.Add(1)
|
||||||
@@ -1131,31 +1120,18 @@ func runUDPServer(ctx context.Context, rt *runtimeCtx) error {
|
|||||||
dst := ip4OrLoopback(ln.LocalAddr().(*net.UDPAddr).IP)
|
dst := ip4OrLoopback(ln.LocalAddr().(*net.UDPAddr).IP)
|
||||||
rt.pc.WriteUDP(src, caddr.Port, dst, ln.LocalAddr().(*net.UDPAddr).Port, raw)
|
rt.pc.WriteUDP(src, caddr.Port, dst, ln.LocalAddr().(*net.UDPAddr).Port, raw)
|
||||||
|
|
||||||
// 剥线(RTP+Shim 或 Shim)
|
// Skip DTLS/STUN control packets
|
||||||
var h shimHeader
|
if len(raw) > 0 && (isDTLSRange(raw[0]) || isSTUNPacket(raw)) { continue }
|
||||||
var p []byte
|
|
||||||
if rt.cfg.Wire == WireRTPish {
|
h, p, e2 := decodeUDPFrame(rt.cfg, rt.ae, raw, 0)
|
||||||
_, _, rest, e := stripRTP(raw); if e != nil { continue }
|
if e2 != nil { continue }
|
||||||
hh, pp, e2 := parseShimFull(rest); if e2 != nil { continue }
|
|
||||||
h, p = hh, pp
|
|
||||||
} else {
|
|
||||||
hh, pp, e2 := parseShimFull(raw); if e2 != nil { continue }
|
|
||||||
h, p = hh, pp
|
|
||||||
}
|
|
||||||
k := key{ip: caddr.IP.String(), port: caddr.Port, sess: h.SessionID}
|
k := key{ip: caddr.IP.String(), port: caddr.Port, sess: h.SessionID}
|
||||||
|
|
||||||
// 丢诱饵 & 解密
|
|
||||||
if (h.Flags & flagDecoy) != 0 {
|
if (h.Flags & flagDecoy) != 0 {
|
||||||
metricDecoyDropped.Add(1)
|
metricDecoyDropped.Add(1)
|
||||||
if rt.dropLog { logf(LDebug, "[server] drop shim-decoy %dB", len(p)) }
|
if rt.dropLog { logf(LDebug, "[server] drop shim-decoy %dB", len(p)) }
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (h.Flags & flagEnc) != 0 && rt.ae != nil {
|
|
||||||
ns := rt.ae.aead.NonceSize()
|
|
||||||
if len(p) >= ns {
|
|
||||||
if plain, e := rt.ae.open(p[:ns], p[ns:]); e == nil { p = plain }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
sess := smap[k]
|
sess := smap[k]
|
||||||
@@ -1178,24 +1154,9 @@ func runUDPServer(ctx context.Context, rt *runtimeCtx) error {
|
|||||||
if e2 != nil { return }
|
if e2 != nil { return }
|
||||||
pp := append([]byte(nil), b[:n2]...)
|
pp := append([]byte(nil), b[:n2]...)
|
||||||
|
|
||||||
// a) 真实帧(带 shim)
|
// a) 真实帧
|
||||||
flags := uint8(0)
|
var twccSeq uint32
|
||||||
payload := pp
|
pkt := encodeUDPFrame(rt.cfg, rt.ae, &rtp, &rtpTs, step, k.sess, pp, false, &twccSeq)
|
||||||
if rt.ae != nil {
|
|
||||||
if c, nonce, e := rt.ae.seal(payload); e == nil {
|
|
||||||
payload = append(nonce, c...)
|
|
||||||
flags |= flagEnc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h := shimHeader{Magic: magicConst, Version: version, Mode: modeUDP, Flags: flags,
|
|
||||||
SessionID: k.sess, TsMs: uint32(time.Now().UnixMilli()), Len: uint32(len(payload))}
|
|
||||||
pkt := append(h.Marshal(), payload...)
|
|
||||||
if rt.cfg.Wire == WireRTPish {
|
|
||||||
rtpTs += uint32(step)
|
|
||||||
rtpHdr := buildRTPHeader(rtp.seq, rtpTs, 96, false, rtp.ssrc)
|
|
||||||
rtp.seq++
|
|
||||||
pkt = append(rtpHdr, pkt...)
|
|
||||||
}
|
|
||||||
rt.tb.wait(len(pkt) + 28)
|
rt.tb.wait(len(pkt) + 28)
|
||||||
_, _ = ln.WriteToUDP(pkt, s.client)
|
_, _ = ln.WriteToUDP(pkt, s.client)
|
||||||
metricFramesDown.Add(1); metricBytesDown.Add(int64(n2))
|
metricFramesDown.Add(1); metricBytesDown.Add(int64(n2))
|
||||||
@@ -1203,30 +1164,14 @@ func runUDPServer(ctx context.Context, rt *runtimeCtx) error {
|
|||||||
dst2 := ip4OrLoopback(s.client.IP)
|
dst2 := ip4OrLoopback(s.client.IP)
|
||||||
rt.pc.WriteUDP(src2, ln.LocalAddr().(*net.UDPAddr).Port, dst2, s.client.Port, pkt)
|
rt.pc.WriteUDP(src2, ln.LocalAddr().(*net.UDPAddr).Port, dst2, s.client.Port, pkt)
|
||||||
|
|
||||||
// b) shim 诱饵(RPS 优先,否则百分比)
|
// b) shim 诱饵
|
||||||
sendDecoy := false
|
sendDecoy := false
|
||||||
if rt.shimDownRL != nil && rt.shimDownRL.takeMax(1) > 0 { sendDecoy = true
|
if rt.shimDownRL != nil && rt.shimDownRL.takeMax(1) > 0 { sendDecoy = true
|
||||||
} else if rt.cfg.DecoyRps <= 0 && grnd.Intn(100) < rt.cfg.DecoyPct { sendDecoy = true }
|
} else if rt.cfg.DecoyRps <= 0 && grnd.Intn(100) < rt.cfg.DecoyPct { sendDecoy = true }
|
||||||
if sendDecoy {
|
if sendDecoy {
|
||||||
djunk := make([]byte, len(pp))
|
djunk := make([]byte, len(pp))
|
||||||
_, _ = cryptoRand.Read(djunk)
|
_, _ = cryptoRand.Read(djunk)
|
||||||
decFlags := uint8(flagDecoy)
|
df := encodeUDPFrame(rt.cfg, rt.ae, &rtp, &rtpTs, step, k.sess, djunk, true, &twccSeq)
|
||||||
decPayload := djunk
|
|
||||||
if rt.ae != nil {
|
|
||||||
if c, nonce, e := rt.ae.seal(decPayload); e == nil {
|
|
||||||
decPayload = append(nonce, c...)
|
|
||||||
decFlags |= flagEnc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hd := shimHeader{Magic: magicConst, Version: version, Mode: modeUDP, Flags: decFlags,
|
|
||||||
SessionID: k.sess, TsMs: uint32(time.Now().UnixMilli()), Len: uint32(len(decPayload))}
|
|
||||||
df := append(hd.Marshal(), decPayload...)
|
|
||||||
if rt.cfg.Wire == WireRTPish {
|
|
||||||
rtpTs += uint32(step)
|
|
||||||
rtpHdr := buildRTPHeader(rtp.seq, rtpTs, 96, false, rtp.ssrc)
|
|
||||||
rtp.seq++
|
|
||||||
df = append(rtpHdr, df...)
|
|
||||||
}
|
|
||||||
rt.tb.wait(len(df) + 28)
|
rt.tb.wait(len(df) + 28)
|
||||||
_, _ = ln.WriteToUDP(df, s.client)
|
_, _ = ln.WriteToUDP(df, s.client)
|
||||||
metricFramesDown.Add(1); metricShimDecoySent.Add(1)
|
metricFramesDown.Add(1); metricShimDecoySent.Add(1)
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// encodeUDPFrame builds a complete wire frame for the configured wire format.
|
||||||
|
// Returns the ready-to-send bytes.
|
||||||
|
func encodeUDPFrame(cfg *Config, ae *aeadBox, rtp *rtpState, rtpTs *uint32,
|
||||||
|
step int, sess uint32, payload []byte, isDecoy bool, twccSeq *uint32) []byte {
|
||||||
|
|
||||||
|
// Build shim header
|
||||||
|
flags := uint8(0)
|
||||||
|
if isDecoy {
|
||||||
|
flags |= flagDecoy
|
||||||
|
}
|
||||||
|
if ae != nil {
|
||||||
|
flags |= flagEnc
|
||||||
|
}
|
||||||
|
hdr := shimHeader{
|
||||||
|
Magic: magicConst,
|
||||||
|
Version: version,
|
||||||
|
Mode: modeUDP,
|
||||||
|
Flags: flags,
|
||||||
|
SessionID: sess,
|
||||||
|
TsMs: uint32(time.Now().UnixMilli()),
|
||||||
|
Len: uint32(len(payload)),
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cfg.Wire {
|
||||||
|
case WireWebRTC:
|
||||||
|
// Encrypt shim header + payload together (eliminates magic fingerprint)
|
||||||
|
sealed, err := sealFrame(ae, hdr, payload)
|
||||||
|
if err != nil {
|
||||||
|
// fallback: raw
|
||||||
|
sealed = append(hdr.Marshal(), payload...)
|
||||||
|
}
|
||||||
|
// WebRTC RTP header (24 bytes) with extensions
|
||||||
|
*rtpTs += uint32(step)
|
||||||
|
absTime := webrtcAbsSendTime()
|
||||||
|
twcc := uint16(0)
|
||||||
|
if twccSeq != nil {
|
||||||
|
twcc = uint16(atomic.AddUint32(twccSeq, 1))
|
||||||
|
}
|
||||||
|
rtpHdr := buildWebRTCRTPHeader(rtp.seq, *rtpTs, 96, !isDecoy, rtp.ssrc, absTime, twcc)
|
||||||
|
rtp.seq++
|
||||||
|
frame := append(rtpHdr, sealed...)
|
||||||
|
return appendSRTPAuthTag(frame)
|
||||||
|
|
||||||
|
case WireIPCam:
|
||||||
|
// For IPCAM mode: encrypt shim + payload, then wrap in single RTP with H.264 FU-A first-frag indicator
|
||||||
|
sealed, err := sealFrame(ae, hdr, payload)
|
||||||
|
if err != nil {
|
||||||
|
sealed = append(hdr.Marshal(), payload...)
|
||||||
|
}
|
||||||
|
*rtpTs += uint32(step)
|
||||||
|
rtpHdr := buildRTPHeader(rtp.seq, *rtpTs, ipcamPT, true, rtp.ssrc)
|
||||||
|
rtp.seq++
|
||||||
|
// Prepend a FU-A indicator+header to look like H.264 fragment
|
||||||
|
nalType := uint8(nalNonIDR) // default P-frame
|
||||||
|
fuIndicator := byte(0x5C) // F=0 NRI=10 Type=28(FU-A)
|
||||||
|
fuHeader := byte(0x80 | nalType) // S=1 E=0 R=0 Type=1 (start+end for single)
|
||||||
|
fuHeader |= 0x40 // set E bit too (single fragment)
|
||||||
|
h264Hdr := []byte{fuIndicator, fuHeader}
|
||||||
|
frame := append(rtpHdr, h264Hdr...)
|
||||||
|
frame = append(frame, sealed...)
|
||||||
|
return frame
|
||||||
|
|
||||||
|
case WireRTPish:
|
||||||
|
// Legacy: encrypt only payload, shim header exposed (but XOR magic if no AES)
|
||||||
|
sealed, err := sealFrame(ae, hdr, payload)
|
||||||
|
if err != nil {
|
||||||
|
sealed = append(hdr.Marshal(), payload...)
|
||||||
|
}
|
||||||
|
*rtpTs += uint32(step)
|
||||||
|
rtpHdr := buildRTPHeader(rtp.seq, *rtpTs, 96, false, rtp.ssrc)
|
||||||
|
rtp.seq++
|
||||||
|
return append(rtpHdr, sealed...)
|
||||||
|
|
||||||
|
default: // WireShim
|
||||||
|
sealed, err := sealFrame(ae, hdr, payload)
|
||||||
|
if err != nil {
|
||||||
|
sealed = append(hdr.Marshal(), payload...)
|
||||||
|
}
|
||||||
|
return sealed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeUDPFrame strips wire framing and decrypts to recover shimHeader + payload.
|
||||||
|
func decodeUDPFrame(cfg *Config, ae *aeadBox, raw []byte, sessionHint uint32) (shimHeader, []byte, error) {
|
||||||
|
switch cfg.Wire {
|
||||||
|
case WireWebRTC:
|
||||||
|
_, _, rest, err := stripWebRTCRTP(raw)
|
||||||
|
if err != nil {
|
||||||
|
return shimHeader{}, nil, err
|
||||||
|
}
|
||||||
|
return openFrame(ae, rest, sessionHint)
|
||||||
|
|
||||||
|
case WireIPCam:
|
||||||
|
// Strip RTP header (12 bytes) + FU-A header (2 bytes)
|
||||||
|
if len(raw) < 14 {
|
||||||
|
return shimHeader{}, nil, errors.New("ipcam frame too short")
|
||||||
|
}
|
||||||
|
if (raw[0]>>6)&0x3 != 2 {
|
||||||
|
return shimHeader{}, nil, errors.New("rtp ver")
|
||||||
|
}
|
||||||
|
rest := raw[14:] // skip 12B RTP + 2B FU-A
|
||||||
|
return openFrame(ae, rest, sessionHint)
|
||||||
|
|
||||||
|
case WireRTPish:
|
||||||
|
// Strip 12-byte RTP header
|
||||||
|
if len(raw) < 12 {
|
||||||
|
return shimHeader{}, nil, errors.New("rtpish too short")
|
||||||
|
}
|
||||||
|
if (raw[0]>>6)&0x3 != 2 {
|
||||||
|
return shimHeader{}, nil, errors.New("rtp ver")
|
||||||
|
}
|
||||||
|
rest := raw[12:]
|
||||||
|
return openFrame(ae, rest, sessionHint)
|
||||||
|
|
||||||
|
default: // WireShim
|
||||||
|
return openFrame(ae, raw, sessionHint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// webrtcAbsSendTime computes a 24-bit abs-send-time (6.18 fixed-point NTP fraction).
|
||||||
|
func webrtcAbsSendTime() uint32 {
|
||||||
|
now := time.Now()
|
||||||
|
sec := uint64(now.Unix()) + ntpEpochOffset
|
||||||
|
frac := uint64(now.Nanosecond()) * (1 << 18) / 1_000_000_000
|
||||||
|
return uint32(((sec & 0x3F) << 18) | (frac & 0x3FFFF))
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDTLSRange checks if the first byte falls in the DTLS content type range (20-63).
|
||||||
|
func isDTLSRange(b byte) bool {
|
||||||
|
return b >= 20 && b <= 63
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSTUNPacket checks if the packet looks like STUN (magic cookie at offset 4).
|
||||||
|
func isSTUNPacket(data []byte) bool {
|
||||||
|
if len(data) < 20 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cookie := binary.BigEndian.Uint32(data[4:8])
|
||||||
|
return cookie == 0x2112A442
|
||||||
|
}
|
||||||
+404
@@ -0,0 +1,404 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
cryptoRand "crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ============ IPCAM Wire Format: H.264 over RTP (RFC 6184) ============ */
|
||||||
|
// Simulates surveillance camera (Hikvision/Dahua style) streaming to NVR/VMS.
|
||||||
|
// Single video stream, PT=96, FU-A fragmentation, STAP-A for SPS/PPS.
|
||||||
|
|
||||||
|
const (
|
||||||
|
ipcamPT uint8 = 96 // dynamic payload type for H.264
|
||||||
|
ipcamClockHz uint32 = 90000
|
||||||
|
ipcamMTU = 1400 // max RTP payload per fragment
|
||||||
|
ipcamRTPHdrSz = 12
|
||||||
|
)
|
||||||
|
|
||||||
|
// H.264 NAL unit types
|
||||||
|
const (
|
||||||
|
nalNonIDR uint8 = 1 // P-frame slice
|
||||||
|
nalIDR uint8 = 5 // I-frame (IDR)
|
||||||
|
nalSPS uint8 = 7
|
||||||
|
nalPPS uint8 = 8
|
||||||
|
nalSTAPA uint8 = 24 // STAP-A aggregation
|
||||||
|
nalFUA uint8 = 28 // FU-A fragmentation
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ---------- GOP State Machine ---------- */
|
||||||
|
|
||||||
|
type gopState struct {
|
||||||
|
frameNum int // frame within GOP (0 = I-frame)
|
||||||
|
gopSize int // frames per GOP (default 50 for 25fps * 2s)
|
||||||
|
fps int // typically 25
|
||||||
|
ssrc uint32
|
||||||
|
seq uint16
|
||||||
|
ts uint32
|
||||||
|
tsStep uint32 // 90000/fps = 3600 for 25fps
|
||||||
|
pktCount uint32
|
||||||
|
octCount uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGopState(fps int, ssrc uint32) *gopState {
|
||||||
|
if fps <= 0 {
|
||||||
|
fps = 25
|
||||||
|
}
|
||||||
|
return &gopState{
|
||||||
|
frameNum: 0,
|
||||||
|
gopSize: fps * 2, // I-frame every 2 seconds
|
||||||
|
fps: fps,
|
||||||
|
ssrc: ssrc,
|
||||||
|
seq: uint16(rand.Intn(0xFFFF)),
|
||||||
|
ts: uint32(rand.Intn(0xFFFFFF)),
|
||||||
|
tsStep: ipcamClockHz / uint32(fps),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- RTP Header builder (IPCAM, no extensions) ---------- */
|
||||||
|
|
||||||
|
func buildIPCAMRTPHeader(seq uint16, ts uint32, marker bool, ssrc uint32) []byte {
|
||||||
|
b := make([]byte, ipcamRTPHdrSz)
|
||||||
|
b[0] = 0x80 // V=2, P=0, X=0, CC=0
|
||||||
|
b[1] = ipcamPT
|
||||||
|
if marker {
|
||||||
|
b[1] |= 0x80
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint16(b[2:4], seq)
|
||||||
|
binary.BigEndian.PutUint32(b[4:8], ts)
|
||||||
|
binary.BigEndian.PutUint32(b[8:12], ssrc)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- SPS/PPS STAP-A Packet ---------- */
|
||||||
|
|
||||||
|
func buildSTAPA_SPS_PPS() []byte {
|
||||||
|
// Fake SPS: High profile, Level 4.0, 1080p indicators (26 bytes)
|
||||||
|
sps := make([]byte, 26)
|
||||||
|
sps[0] = 0x67 // SPS NAL header (forbidden=0, NRI=3, Type=7)
|
||||||
|
sps[1] = 0x64 // profile_idc = High (100)
|
||||||
|
sps[2] = 0x00 // constraint flags
|
||||||
|
sps[3] = 0x28 // level_idc = 4.0
|
||||||
|
_, _ = cryptoRand.Read(sps[4:]) // plausible bitstream tail
|
||||||
|
|
||||||
|
// Fake PPS: 4 bytes
|
||||||
|
pps := []byte{0x68, 0xEE, 0x3C, 0x80}
|
||||||
|
|
||||||
|
// STAP-A: indicator(1) + spsLen(2) + sps + ppsLen(2) + pps
|
||||||
|
payload := make([]byte, 0, 1+2+len(sps)+2+len(pps))
|
||||||
|
payload = append(payload, 0x78) // F=0, NRI=11, Type=24 (STAP-A)
|
||||||
|
payload = append(payload, byte(len(sps)>>8), byte(len(sps)))
|
||||||
|
payload = append(payload, sps...)
|
||||||
|
payload = append(payload, byte(len(pps)>>8), byte(len(pps)))
|
||||||
|
payload = append(payload, pps...)
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- FU-A Fragmentation ---------- */
|
||||||
|
|
||||||
|
func (g *gopState) buildFUAFragments(nalType uint8, totalSize int) [][]byte {
|
||||||
|
if totalSize < 1 {
|
||||||
|
totalSize = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// FU indicator: forbidden(0) | NRI(2bits) | Type=28(FU-A)
|
||||||
|
var fuIndicator uint8
|
||||||
|
switch nalType {
|
||||||
|
case nalIDR:
|
||||||
|
fuIndicator = 0x7C // NRI=11, Type=28
|
||||||
|
default:
|
||||||
|
fuIndicator = 0x5C // NRI=10, Type=28
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate fake NAL payload (random bytes simulating H.264 bitstream)
|
||||||
|
nalPayload := make([]byte, totalSize)
|
||||||
|
_, _ = cryptoRand.Read(nalPayload)
|
||||||
|
|
||||||
|
maxFrag := ipcamMTU - 2 // 2 bytes for FU indicator + FU header
|
||||||
|
var packets [][]byte
|
||||||
|
off := 0
|
||||||
|
|
||||||
|
for off < len(nalPayload) {
|
||||||
|
end := off + maxFrag
|
||||||
|
if end > len(nalPayload) {
|
||||||
|
end = len(nalPayload)
|
||||||
|
}
|
||||||
|
isFirst := off == 0
|
||||||
|
isLast := end == len(nalPayload)
|
||||||
|
|
||||||
|
// FU header: S(1) | E(1) | R(0) | Type(5)
|
||||||
|
var fuHeader uint8
|
||||||
|
fuHeader = nalType & 0x1F
|
||||||
|
if isFirst {
|
||||||
|
fuHeader |= 0x80 // S=1
|
||||||
|
}
|
||||||
|
if isLast {
|
||||||
|
fuHeader |= 0x40 // E=1
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTP header (marker=1 on last fragment of frame)
|
||||||
|
rtp := buildIPCAMRTPHeader(g.seq, g.ts, isLast, g.ssrc)
|
||||||
|
g.seq++
|
||||||
|
|
||||||
|
// Assemble: RTP header + FU indicator + FU header + payload fragment
|
||||||
|
pkt := make([]byte, 0, ipcamRTPHdrSz+2+end-off)
|
||||||
|
pkt = append(pkt, rtp...)
|
||||||
|
pkt = append(pkt, fuIndicator, fuHeader)
|
||||||
|
pkt = append(pkt, nalPayload[off:end]...)
|
||||||
|
|
||||||
|
packets = append(packets, pkt)
|
||||||
|
g.pktCount++
|
||||||
|
g.octCount += uint32(len(pkt) - ipcamRTPHdrSz)
|
||||||
|
|
||||||
|
off = end
|
||||||
|
}
|
||||||
|
return packets
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Frame Size Simulation ---------- */
|
||||||
|
|
||||||
|
func ipcamFrameSize(isIFrame bool, bitrateMbps int, fps int) int {
|
||||||
|
if fps <= 0 {
|
||||||
|
fps = 25
|
||||||
|
}
|
||||||
|
if bitrateMbps <= 0 {
|
||||||
|
bitrateMbps = 4
|
||||||
|
}
|
||||||
|
bytesPerSec := bitrateMbps * 1024 * 1024 / 8
|
||||||
|
|
||||||
|
if isIFrame {
|
||||||
|
// I-frame ~8x average P-frame
|
||||||
|
base := bytesPerSec / fps * 8
|
||||||
|
if base < 40960 {
|
||||||
|
base = 40960
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
// P-frame: average bitrate portion with ±30% VBR
|
||||||
|
base := float64(bytesPerSec) / float64(fps) * 0.8
|
||||||
|
jitter := base * 0.3 * (rand.Float64()*2 - 1) // ±30%
|
||||||
|
|
||||||
|
// Occasional motion spike (10% chance, 1.5x)
|
||||||
|
if rand.Intn(10) == 0 {
|
||||||
|
base *= 1.5
|
||||||
|
}
|
||||||
|
|
||||||
|
size := int(base + jitter)
|
||||||
|
if size < 200 {
|
||||||
|
size = 200
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Generate One Video Frame ---------- */
|
||||||
|
|
||||||
|
func (g *gopState) generateFrame(isIFrame bool) [][]byte {
|
||||||
|
var packets [][]byte
|
||||||
|
|
||||||
|
if isIFrame {
|
||||||
|
// SPS+PPS as STAP-A before IDR
|
||||||
|
stapa := buildSTAPA_SPS_PPS()
|
||||||
|
rtp := buildIPCAMRTPHeader(g.seq, g.ts, false, g.ssrc)
|
||||||
|
g.seq++
|
||||||
|
g.pktCount++
|
||||||
|
g.octCount += uint32(len(stapa))
|
||||||
|
pkt := append(rtp, stapa...)
|
||||||
|
packets = append(packets, pkt)
|
||||||
|
|
||||||
|
// IDR slice fragments
|
||||||
|
idrSize := ipcamFrameSize(true, 4, g.fps)
|
||||||
|
packets = append(packets, g.buildFUAFragments(nalIDR, idrSize)...)
|
||||||
|
} else {
|
||||||
|
// Non-IDR (P-frame) fragments
|
||||||
|
pSize := ipcamFrameSize(false, 4, g.fps)
|
||||||
|
packets = append(packets, g.buildFUAFragments(nalNonIDR, pSize)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance timestamp for next frame
|
||||||
|
g.ts += g.tsStep
|
||||||
|
|
||||||
|
return packets
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Wrap Shim Inside FU-A Payload ---------- */
|
||||||
|
|
||||||
|
func (g *gopState) wrapPayloadAsIPCAM(shimFrame []byte) [][]byte {
|
||||||
|
isIFrame := g.frameNum == 0
|
||||||
|
|
||||||
|
// Determine target frame size for camouflage
|
||||||
|
var targetSize int
|
||||||
|
if isIFrame {
|
||||||
|
targetSize = ipcamFrameSize(true, 4, g.fps)
|
||||||
|
} else {
|
||||||
|
targetSize = ipcamFrameSize(false, 4, g.fps)
|
||||||
|
}
|
||||||
|
// Ensure target is large enough for the shim + 4-byte length prefix
|
||||||
|
minSize := len(shimFrame) + 4
|
||||||
|
if targetSize < minSize {
|
||||||
|
targetSize = minSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build padded NAL body: [shimLen(4)] [shimFrame] [random padding...]
|
||||||
|
nalBody := make([]byte, targetSize)
|
||||||
|
binary.BigEndian.PutUint32(nalBody[0:4], uint32(len(shimFrame)))
|
||||||
|
copy(nalBody[4:4+len(shimFrame)], shimFrame)
|
||||||
|
_, _ = cryptoRand.Read(nalBody[4+len(shimFrame):])
|
||||||
|
|
||||||
|
// Choose NAL type based on GOP position
|
||||||
|
var nalType uint8
|
||||||
|
if isIFrame {
|
||||||
|
nalType = nalIDR
|
||||||
|
} else {
|
||||||
|
nalType = nalNonIDR
|
||||||
|
}
|
||||||
|
|
||||||
|
// FU indicator
|
||||||
|
var fuIndicator uint8
|
||||||
|
if nalType == nalIDR {
|
||||||
|
fuIndicator = 0x7C
|
||||||
|
} else {
|
||||||
|
fuIndicator = 0x5C
|
||||||
|
}
|
||||||
|
|
||||||
|
var packets [][]byte
|
||||||
|
|
||||||
|
// If I-frame, prepend SPS/PPS STAP-A
|
||||||
|
if isIFrame {
|
||||||
|
stapa := buildSTAPA_SPS_PPS()
|
||||||
|
rtp := buildIPCAMRTPHeader(g.seq, g.ts, false, g.ssrc)
|
||||||
|
g.seq++
|
||||||
|
g.pktCount++
|
||||||
|
g.octCount += uint32(len(stapa))
|
||||||
|
packets = append(packets, append(rtp, stapa...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fragment the padded NAL body as FU-A
|
||||||
|
maxFrag := ipcamMTU - 2
|
||||||
|
off := 0
|
||||||
|
for off < len(nalBody) {
|
||||||
|
end := off + maxFrag
|
||||||
|
if end > len(nalBody) {
|
||||||
|
end = len(nalBody)
|
||||||
|
}
|
||||||
|
isFirst := off == 0
|
||||||
|
isLast := end == len(nalBody)
|
||||||
|
|
||||||
|
var fuHeader uint8
|
||||||
|
fuHeader = nalType & 0x1F
|
||||||
|
if isFirst {
|
||||||
|
fuHeader |= 0x80
|
||||||
|
}
|
||||||
|
if isLast {
|
||||||
|
fuHeader |= 0x40
|
||||||
|
}
|
||||||
|
|
||||||
|
rtp := buildIPCAMRTPHeader(g.seq, g.ts, isLast, g.ssrc)
|
||||||
|
g.seq++
|
||||||
|
|
||||||
|
pkt := make([]byte, 0, ipcamRTPHdrSz+2+end-off)
|
||||||
|
pkt = append(pkt, rtp...)
|
||||||
|
pkt = append(pkt, fuIndicator, fuHeader)
|
||||||
|
pkt = append(pkt, nalBody[off:end]...)
|
||||||
|
|
||||||
|
packets = append(packets, pkt)
|
||||||
|
g.pktCount++
|
||||||
|
g.octCount += uint32(len(pkt) - ipcamRTPHdrSz)
|
||||||
|
|
||||||
|
off = end
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance GOP state
|
||||||
|
g.ts += g.tsStep
|
||||||
|
g.frameNum = (g.frameNum + 1) % g.gopSize
|
||||||
|
|
||||||
|
return packets
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Extract Shim from IPCAM Packets ---------- */
|
||||||
|
|
||||||
|
func extractPayloadFromIPCAM(packets [][]byte) ([]byte, error) {
|
||||||
|
if len(packets) == 0 {
|
||||||
|
return nil, errors.New("ipcam: no packets")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find FU-A fragments (skip STAP-A if present)
|
||||||
|
var fuPackets [][]byte
|
||||||
|
for _, pkt := range packets {
|
||||||
|
if len(pkt) < ipcamRTPHdrSz+2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fuIndicator := pkt[ipcamRTPHdrSz]
|
||||||
|
nalType := fuIndicator & 0x1F
|
||||||
|
if nalType == nalFUA {
|
||||||
|
fuPackets = append(fuPackets, pkt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fuPackets) == 0 {
|
||||||
|
return nil, errors.New("ipcam: no FU-A fragments found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reassemble FU-A payload (strip RTP header + FU indicator + FU header)
|
||||||
|
var assembled []byte
|
||||||
|
for _, pkt := range fuPackets {
|
||||||
|
if len(pkt) < ipcamRTPHdrSz+2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
payload := pkt[ipcamRTPHdrSz+2:]
|
||||||
|
assembled = append(assembled, payload...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract shim: first 4 bytes = length, then that many bytes of shim data
|
||||||
|
if len(assembled) < 4 {
|
||||||
|
return nil, errors.New("ipcam: reassembled too short")
|
||||||
|
}
|
||||||
|
shimLen := binary.BigEndian.Uint32(assembled[0:4])
|
||||||
|
if shimLen == 0 || int(shimLen) > len(assembled)-4 {
|
||||||
|
return nil, errors.New("ipcam: invalid shim length")
|
||||||
|
}
|
||||||
|
return assembled[4 : 4+shimLen], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- RTCP SR for IPCAM ---------- */
|
||||||
|
|
||||||
|
func (g *gopState) buildIPCAMRTCPSR() []byte {
|
||||||
|
// Standard RTCP SR: V=2, P=0, RC=0, PT=200, length=6 (28 bytes)
|
||||||
|
b := make([]byte, 28)
|
||||||
|
b[0] = 0x80 // V=2, P=0, RC=0
|
||||||
|
b[1] = 200 // SR
|
||||||
|
binary.BigEndian.PutUint16(b[2:4], 6) // length in 32-bit words minus 1
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint32(b[4:8], g.ssrc)
|
||||||
|
|
||||||
|
// NTP timestamp
|
||||||
|
now := time.Now()
|
||||||
|
sec := uint32(uint64(now.Unix()) + 2208988800) // NTP epoch offset
|
||||||
|
frac := uint32(uint64(now.Nanosecond()) * (1 << 32) / 1_000_000_000)
|
||||||
|
binary.BigEndian.PutUint32(b[8:12], sec)
|
||||||
|
binary.BigEndian.PutUint32(b[12:16], frac)
|
||||||
|
|
||||||
|
// RTP timestamp, packet count, octet count
|
||||||
|
binary.BigEndian.PutUint32(b[16:20], g.ts)
|
||||||
|
binary.BigEndian.PutUint32(b[20:24], g.pktCount)
|
||||||
|
binary.BigEndian.PutUint32(b[24:28], g.octCount)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Packet Identification ---------- */
|
||||||
|
|
||||||
|
func isIPCAMRTP(b []byte) bool {
|
||||||
|
if len(b) < ipcamRTPHdrSz {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// V=2
|
||||||
|
if (b[0]>>6)&0x3 != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// PT=96 (ignore marker bit)
|
||||||
|
pt := b[1] & 0x7F
|
||||||
|
return pt == ipcamPT
|
||||||
|
}
|
||||||
+226
@@ -0,0 +1,226 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
cryptoRand "crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"hash/crc32"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildWebRTCRTPHeader(seq uint16, ts uint32, pt uint8, marker bool, ssrc uint32, absTime uint32, twccSeq uint16) []byte {
|
||||||
|
b := make([]byte, 24)
|
||||||
|
b[0] = 0x90 // V=2, X=1
|
||||||
|
b[1] = pt
|
||||||
|
if marker {
|
||||||
|
b[1] |= 0x80
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint16(b[2:4], seq)
|
||||||
|
binary.BigEndian.PutUint32(b[4:8], ts)
|
||||||
|
binary.BigEndian.PutUint32(b[8:12], ssrc)
|
||||||
|
binary.BigEndian.PutUint16(b[12:14], 0xBEDE)
|
||||||
|
binary.BigEndian.PutUint16(b[14:16], 0x0002)
|
||||||
|
b[16] = 0x32 // id=3, len=2 (abs-send-time)
|
||||||
|
b[17] = byte(absTime >> 16)
|
||||||
|
b[18] = byte(absTime >> 8)
|
||||||
|
b[19] = byte(absTime)
|
||||||
|
b[20] = 0x51 // id=5, len=1 (transport-cc)
|
||||||
|
binary.BigEndian.PutUint16(b[21:23], twccSeq)
|
||||||
|
b[23] = 0x00 // padding
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendSRTPAuthTag(packet []byte) []byte {
|
||||||
|
tag := make([]byte, 10)
|
||||||
|
_, _ = cryptoRand.Read(tag)
|
||||||
|
return append(packet, tag...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripWebRTCRTP(b []byte) (pt uint8, marker bool, rest []byte, err error) {
|
||||||
|
if len(b) < 24+10 {
|
||||||
|
return 0, false, nil, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
if (b[0]>>6)&0x3 != 2 {
|
||||||
|
return 0, false, nil, errors.New("rtp ver")
|
||||||
|
}
|
||||||
|
marker = (b[1] & 0x80) != 0
|
||||||
|
pt = b[1] & 0x7F
|
||||||
|
rest = b[24 : len(b)-10]
|
||||||
|
return pt, marker, rest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type audioState struct {
|
||||||
|
seq uint16
|
||||||
|
ts uint32
|
||||||
|
ssrc uint32
|
||||||
|
twccSeq *uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *audioState) buildAudioPacket() []byte {
|
||||||
|
twcc := uint16(atomic.AddUint32(a.twccSeq, 1))
|
||||||
|
now := time.Now()
|
||||||
|
sec := uint32(uint64(now.Unix()) + ntpEpochOffset)
|
||||||
|
frac := uint32(uint64(now.Nanosecond()) * (1 << 18) / 1_000_000_000)
|
||||||
|
absTime := (sec << 18) | frac
|
||||||
|
|
||||||
|
h := buildWebRTCRTPHeader(a.seq, a.ts, 111, true, a.ssrc, absTime, twcc)
|
||||||
|
a.seq++
|
||||||
|
a.ts += 960
|
||||||
|
|
||||||
|
payloadLen := 80 + rand.Intn(41) // 80-120
|
||||||
|
payload := make([]byte, payloadLen)
|
||||||
|
_, _ = cryptoRand.Read(payload)
|
||||||
|
|
||||||
|
pkt := append(h, payload...)
|
||||||
|
return appendSRTPAuthTag(pkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCompoundRTCP(ssrc uint32, rtpTs uint32, pktCount uint32, octCount uint32) []byte {
|
||||||
|
now := time.Now()
|
||||||
|
sec := uint32(uint64(now.Unix()) + ntpEpochOffset)
|
||||||
|
frac := uint32(uint64(now.Nanosecond()) * (1 << 32) / 1_000_000_000)
|
||||||
|
|
||||||
|
// SR: 28 bytes
|
||||||
|
sr := make([]byte, 28)
|
||||||
|
sr[0] = 0x80 // V=2, P=0, RC=0
|
||||||
|
sr[1] = 200 // SR
|
||||||
|
binary.BigEndian.PutUint16(sr[2:4], 6)
|
||||||
|
binary.BigEndian.PutUint32(sr[4:8], ssrc)
|
||||||
|
binary.BigEndian.PutUint32(sr[8:12], sec)
|
||||||
|
binary.BigEndian.PutUint32(sr[12:16], frac)
|
||||||
|
binary.BigEndian.PutUint32(sr[16:20], rtpTs)
|
||||||
|
binary.BigEndian.PutUint32(sr[20:24], pktCount)
|
||||||
|
binary.BigEndian.PutUint32(sr[24:28], octCount)
|
||||||
|
|
||||||
|
// SDES: CNAME = "{hex-ssrc}@webrtc.local"
|
||||||
|
cname := []byte(hexSSRC(ssrc) + "@webrtc.local")
|
||||||
|
// SDES header(4) + SSRC(4) + CNAME item(2+len) + END(1) + padding
|
||||||
|
sdesPayload := make([]byte, 0, 4+len(cname)+3)
|
||||||
|
sdesPayload = append(sdesPayload, byte(ssrc>>24), byte(ssrc>>16), byte(ssrc>>8), byte(ssrc))
|
||||||
|
sdesPayload = append(sdesPayload, 1, byte(len(cname)))
|
||||||
|
sdesPayload = append(sdesPayload, cname...)
|
||||||
|
sdesPayload = append(sdesPayload, 0x00) // END
|
||||||
|
for len(sdesPayload)%4 != 0 {
|
||||||
|
sdesPayload = append(sdesPayload, 0x00)
|
||||||
|
}
|
||||||
|
|
||||||
|
sdesHdr := make([]byte, 4)
|
||||||
|
sdesHdr[0] = 0x81 // V=2, SC=1
|
||||||
|
sdesHdr[1] = 202 // SDES
|
||||||
|
binary.BigEndian.PutUint16(sdesHdr[2:4], uint16(len(sdesPayload)/4))
|
||||||
|
|
||||||
|
compound := append(sr, sdesHdr...)
|
||||||
|
compound = append(compound, sdesPayload...)
|
||||||
|
|
||||||
|
// SRTCP index (E-flag set, index=0)
|
||||||
|
idx := make([]byte, 4)
|
||||||
|
idx[0] = 0x80 // E-flag
|
||||||
|
compound = append(compound, idx...)
|
||||||
|
|
||||||
|
return appendSRTPAuthTag(compound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexSSRC(ssrc uint32) string {
|
||||||
|
b := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(b, ssrc)
|
||||||
|
const hex = "0123456789abcdef"
|
||||||
|
out := make([]byte, 8)
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
out[i*2] = hex[b[i]>>4]
|
||||||
|
out[i*2+1] = hex[b[i]&0x0f]
|
||||||
|
}
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stunCRC32(data []byte) uint32 {
|
||||||
|
return crc32.ChecksumIEEE(data) ^ 0x5354554E
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSTUNBindingRequestFull() []byte {
|
||||||
|
b := make([]byte, 28)
|
||||||
|
binary.BigEndian.PutUint16(b[0:2], 0x0001) // Binding Request
|
||||||
|
binary.BigEndian.PutUint16(b[2:4], 8) // length: FINGERPRINT attr (8 bytes)
|
||||||
|
binary.BigEndian.PutUint32(b[4:8], 0x2112A442)
|
||||||
|
_, _ = cryptoRand.Read(b[8:20])
|
||||||
|
|
||||||
|
// FINGERPRINT attribute
|
||||||
|
binary.BigEndian.PutUint16(b[20:22], 0x8028) // type
|
||||||
|
binary.BigEndian.PutUint16(b[22:24], 4) // length
|
||||||
|
fp := stunCRC32(b[:20])
|
||||||
|
binary.BigEndian.PutUint32(b[24:28], fp)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSTUNBindingResponse(transactionID []byte) []byte {
|
||||||
|
b := make([]byte, 44)
|
||||||
|
binary.BigEndian.PutUint16(b[0:2], 0x0101) // Binding Success Response
|
||||||
|
binary.BigEndian.PutUint16(b[2:4], 24) // length: XOR-MAPPED-ADDRESS(12) + FINGERPRINT(8)
|
||||||
|
binary.BigEndian.PutUint32(b[4:8], 0x2112A442)
|
||||||
|
copy(b[8:20], transactionID)
|
||||||
|
|
||||||
|
// XOR-MAPPED-ADDRESS
|
||||||
|
binary.BigEndian.PutUint16(b[20:22], 0x0020) // type
|
||||||
|
binary.BigEndian.PutUint16(b[22:24], 8) // length
|
||||||
|
b[24] = 0x00 // reserved
|
||||||
|
b[25] = 0x01 // IPv4
|
||||||
|
binary.BigEndian.PutUint16(b[26:28], 0x2112^0xD903) // XOR'd port
|
||||||
|
binary.BigEndian.PutUint32(b[28:32], 0x2112A442^0xC0A80101) // XOR'd 192.168.1.1
|
||||||
|
|
||||||
|
// FINGERPRINT
|
||||||
|
binary.BigEndian.PutUint16(b[32:34], 0x8028)
|
||||||
|
binary.BigEndian.PutUint16(b[34:36], 4)
|
||||||
|
fp := stunCRC32(b[:32])
|
||||||
|
binary.BigEndian.PutUint32(b[36:40], fp)
|
||||||
|
|
||||||
|
// pad remaining
|
||||||
|
return b[:40]
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAudioTicker(ctx context.Context, conn *net.UDPConn, peer *net.UDPAddr, audio *audioState, tb *tokenBucket, pcap *pcapWriter) {
|
||||||
|
ticker := time.NewTicker(20 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
localAddr := conn.LocalAddr().(*net.UDPAddr)
|
||||||
|
srcIP := ip4OrLoopback(localAddr.IP)
|
||||||
|
dstIP := ip4OrLoopback(peer.IP)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
jitter := time.Duration(rand.Intn(4000)-2000) * time.Microsecond
|
||||||
|
time.Sleep(jitter)
|
||||||
|
|
||||||
|
pkt := audio.buildAudioPacket()
|
||||||
|
tb.wait(len(pkt) + 28)
|
||||||
|
_, _ = conn.WriteToUDP(pkt, peer)
|
||||||
|
pcap.WriteUDP(srcIP, localAddr.Port, dstIP, peer.Port, pkt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSTUNConsent(ctx context.Context, conn *net.UDPConn, peer *net.UDPAddr, pcap *pcapWriter) {
|
||||||
|
localAddr := conn.LocalAddr().(*net.UDPAddr)
|
||||||
|
srcIP := ip4OrLoopback(localAddr.IP)
|
||||||
|
dstIP := ip4OrLoopback(peer.IP)
|
||||||
|
|
||||||
|
for {
|
||||||
|
jitter := time.Duration(rand.Intn(2000)-1000) * time.Millisecond
|
||||||
|
wait := 5*time.Second + jitter
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(wait):
|
||||||
|
req := buildSTUNBindingRequestFull()
|
||||||
|
_, _ = conn.WriteToUDP(req, peer)
|
||||||
|
pcap.WriteUDP(srcIP, localAddr.Port, dstIP, peer.Port, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user