mirror of
https://github.com/uk0/camostream.git
synced 2026-08-29 08:49:44 +00:00
- Launch audio ticker (50pps Opus PT=111) and STUN consent freshness as background goroutines in WebRTC mode - Fix STUN infinite ping-pong: only respond to Binding Requests (0x0001), not Binding Responses (0x0101) - Add decoy send delay (2-8ms) to avoid burst timing fingerprint - Use shared atomic transport-cc counter across all streams - WebRTC mode uses compound RTCP (SR+SDES) instead of bare SR - Server responds to client STUN Binding Requests with proper Response DPI score: 55.6 -> 73.8 (timing 10->69, size CV 0.09->0.93)
160 lines
4.5 KiB
Go
160 lines
4.5 KiB
Go
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
|
|
}
|
|
|
|
// isSTUNRequest checks if a STUN packet is a Binding Request (type 0x0001).
|
|
func isSTUNRequest(data []byte) bool {
|
|
if len(data) < 20 {
|
|
return false
|
|
}
|
|
msgType := binary.BigEndian.Uint16(data[0:2])
|
|
cookie := binary.BigEndian.Uint32(data[4:8])
|
|
return cookie == 0x2112A442 && msgType == 0x0001
|
|
}
|