integrate audio ticker, STUN consent, fix timing and STUN ping-pong

- 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)
This commit is contained in:
uk0
2026-04-08 16:40:08 +08:00
parent 8b001daf0a
commit babf6bd0b8
3 changed files with 125 additions and 22 deletions
+73 -22
View File
@@ -793,6 +793,12 @@ type runtimeCtx struct {
// Shim decoy RPS // Shim decoy RPS
shimUpRL *rateLimiter shimUpRL *rateLimiter
shimDownRL *rateLimiter shimDownRL *rateLimiter
// WebRTC shared state
twccSeq uint32 // shared transport-cc counter (atomic)
// IPCAM shared state
gop *gopState
} }
func runMetrics(addr string) { func runMetrics(addr string) {
@@ -987,29 +993,58 @@ func runUDPClient(ctx context.Context, rt *runtimeCtx) error {
logf(LInfo, "[client][udp] %s -> server %s (sess=%d, wire=%s, %dfps, %dMbps)", logf(LInfo, "[client][udp] %s -> server %s (sess=%d, wire=%s, %dfps, %dMbps)",
rt.cfg.Listen, rt.cfg.ServerAddr, sess, rt.cfg.Wire, rt.cfg.FPS, rt.cfg.BitrateMbps) rt.cfg.Listen, rt.cfg.ServerAddr, sess, rt.cfg.Wire, rt.cfg.FPS, rt.cfg.BitrateMbps)
// WebRTC: simulate DTLS handshake before media
if rt.cfg.Wire == WireWebRTC && rt.cfg.EnableDTLS {
logf(LInfo, "[client] performing DTLS handshake with server...")
if err := performDTLSHandshake(uc, saddr, false, rt.pc); err != nil {
logf(LWarn, "[client] DTLS handshake failed: %v (continuing)", err)
} else {
metricDTLSSent.Add(1)
logf(LInfo, "[client] DTLS handshake complete")
}
}
var appPeerMu sync.RWMutex var appPeerMu sync.RWMutex
var appPeer *net.UDPAddr var appPeer *net.UDPAddr
// RTP 累加
var rtp = rtpState{seq: uint16(grnd.Uint32()), ssrc: grnd.Uint32()} var rtp = rtpState{seq: uint16(grnd.Uint32()), ssrc: grnd.Uint32()}
var rtpTs uint32 var rtpTs uint32
step := 90000 / max(1, rt.cfg.FPS) step := 90000 / max(1, rt.cfg.FPS)
// WebRTC: launch audio ticker and STUN consent freshness
if rt.cfg.Wire == WireWebRTC {
audio := &audioState{
seq: uint16(grnd.Uint32()),
ts: 0,
ssrc: grnd.Uint32(), // different SSRC for audio
twccSeq: &rt.twccSeq,
}
go runAudioTicker(ctx, uc, saddr, audio, rt.tb, rt.pc)
go runSTUNConsent(ctx, uc, saddr, rt.pc)
}
go func() { go func() {
buf := make([]byte, 64<<10) buf := make([]byte, 64<<10)
for { for {
n, from, err := uc.ReadFromUDP(buf) n, from, err := uc.ReadFromUDP(buf)
if err != nil { return } if err != nil { return }
// 来自 server:解壳 -> 发给应用
if from.IP.Equal(saddr.IP) && from.Port == saddr.Port { if from.IP.Equal(saddr.IP) && from.Port == saddr.Port {
raw := append([]byte(nil), buf[:n]...) raw := append([]byte(nil), buf[:n]...)
src := ip4OrLoopback(from.IP) src := ip4OrLoopback(from.IP)
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)
// Skip DTLS/STUN control packets (not data) if len(raw) == 0 { continue }
if isDTLSRange(raw[0]) || isSTUNPacket(raw) { continue } if isDTLSRange(raw[0]) || isSTUNPacket(raw) {
// WebRTC: respond to STUN Binding Requests only (not responses, to avoid ping-pong)
if rt.cfg.Wire == WireWebRTC && isSTUNRequest(raw) {
resp := buildSTUNBindingResponse(raw[8:20])
_, _ = uc.WriteToUDP(resp, from)
rt.pc.WriteUDP(dst, uc.LocalAddr().(*net.UDPAddr).Port, src, from.Port, resp)
}
continue
}
h, payload, e2 := decodeUDPFrame(rt.cfg, rt.ae, raw, sess) h, payload, e2 := decodeUDPFrame(rt.cfg, rt.ae, raw, sess)
if e2 != nil { continue } if e2 != nil { continue }
@@ -1026,13 +1061,11 @@ func runUDPClient(ctx context.Context, rt *runtimeCtx) error {
continue continue
} }
// 来自应用:加壳 -> 先发真实帧,再插播 shim 诱饵(RPS 优先)→ 插播额外伪报文
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) 真实帧 // a) 真实帧(使用共享 twccSeq
var twccSeq uint32 frame := encodeUDPFrame(rt.cfg, rt.ae, &rtp, &rtpTs, step, sess, p, false, &rt.twccSeq)
frame := encodeUDPFrame(rt.cfg, rt.ae, &rtp, &rtpTs, step, sess, p, false, &twccSeq)
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))
@@ -1040,24 +1073,29 @@ 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 诱饵 // 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 {
// 延迟 2-8ms 避免与真实帧突发
time.Sleep(time.Duration(2+grnd.Intn(6)) * time.Millisecond)
djunk := make([]byte, len(p)) djunk := make([]byte, len(p))
_, _ = cryptoRand.Read(djunk) _, _ = cryptoRand.Read(djunk)
df := encodeUDPFrame(rt.cfg, rt.ae, &rtp, &rtpTs, step, sess, djunk, true, &twccSeq) df := encodeUDPFrame(rt.cfg, rt.ae, &rtp, &rtpTs, step, sess, djunk, true, &rt.twccSeq)
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)
rt.pc.WriteUDP(srcIP, uc.LocalAddr().(*net.UDPAddr).Port, dstIP, saddr.Port, df) rt.pc.WriteUDP(srcIP, uc.LocalAddr().(*net.UDPAddr).Port, dstIP, saddr.Port, df)
} }
// c) 额外伪报文(无 shimRTCP / pure RTP / STUN // c) 额外伪报文(WebRTC 用 compound RTCP
sendExtraDecoysUDP(uc, uc.LocalAddr().(*net.UDPAddr), saddr, rt, rtp.ssrc, &rtp.seq, &rtpTs, step, true) if rt.cfg.Wire == WireWebRTC {
sendWebRTCExtraDecoys(uc, uc.LocalAddr().(*net.UDPAddr), saddr, rt, rtp.ssrc, &rtp.seq, &rtpTs, step, true)
} else {
sendExtraDecoysUDP(uc, uc.LocalAddr().(*net.UDPAddr), saddr, rt, rtp.ssrc, &rtp.seq, &rtpTs, step, true)
}
// 节奏(fps
base := 1000 / max(1, rt.cfg.FPS) base := 1000 / max(1, rt.cfg.FPS)
jitterSleep(base, rt.cfg.JitterPct) jitterSleep(base, rt.cfg.JitterPct)
} }
@@ -1120,8 +1158,17 @@ 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)
// Skip DTLS/STUN control packets // Handle DTLS/STUN control packets
if len(raw) > 0 && (isDTLSRange(raw[0]) || isSTUNPacket(raw)) { continue } if len(raw) > 0 && isDTLSRange(raw[0]) { continue }
if len(raw) > 0 && isSTUNPacket(raw) {
// WebRTC: respond to STUN Binding Requests only (not responses)
if rt.cfg.Wire == WireWebRTC && isSTUNRequest(raw) {
resp := buildSTUNBindingResponse(raw[8:20])
_, _ = ln.WriteToUDP(resp, caddr)
rt.pc.WriteUDP(dst, ln.LocalAddr().(*net.UDPAddr).Port, src, caddr.Port, resp)
}
continue
}
h, p, e2 := decodeUDPFrame(rt.cfg, rt.ae, raw, 0) h, p, e2 := decodeUDPFrame(rt.cfg, rt.ae, raw, 0)
if e2 != nil { continue } if e2 != nil { continue }
@@ -1154,9 +1201,8 @@ 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) 真实帧 // a) 真实帧(使用共享 twccSeq
var twccSeq uint32 pkt := encodeUDPFrame(rt.cfg, rt.ae, &rtp, &rtpTs, step, k.sess, pp, false, &rt.twccSeq)
pkt := encodeUDPFrame(rt.cfg, rt.ae, &rtp, &rtpTs, step, k.sess, pp, false, &twccSeq)
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))
@@ -1164,22 +1210,27 @@ 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 诱饵 // 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 {
time.Sleep(time.Duration(2+grnd.Intn(6)) * time.Millisecond)
djunk := make([]byte, len(pp)) djunk := make([]byte, len(pp))
_, _ = cryptoRand.Read(djunk) _, _ = cryptoRand.Read(djunk)
df := encodeUDPFrame(rt.cfg, rt.ae, &rtp, &rtpTs, step, k.sess, djunk, true, &twccSeq) df := encodeUDPFrame(rt.cfg, rt.ae, &rtp, &rtpTs, step, k.sess, djunk, true, &rt.twccSeq)
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)
rt.pc.WriteUDP(src2, ln.LocalAddr().(*net.UDPAddr).Port, dst2, s.client.Port, df) rt.pc.WriteUDP(src2, ln.LocalAddr().(*net.UDPAddr).Port, dst2, s.client.Port, df)
} }
// c) 额外伪报文(无 shimRTCP / pure RTP / STUN),发往 client // c) 额外伪报文(WebRTC 用 compound RTCP
sendExtraDecoysUDP(ln, ln.LocalAddr().(*net.UDPAddr), s.client, rt, rtp.ssrc, &rtp.seq, &rtpTs, step, false) if rt.cfg.Wire == WireWebRTC {
sendWebRTCExtraDecoys(ln, ln.LocalAddr().(*net.UDPAddr), s.client, rt, rtp.ssrc, &rtp.seq, &rtpTs, step, false)
} else {
sendExtraDecoysUDP(ln, ln.LocalAddr().(*net.UDPAddr), s.client, rt, rtp.ssrc, &rtp.seq, &rtpTs, step, false)
}
// 节奏(fps // 节奏(fps
base := 1000 / max(1, rt.cfg.FPS) base := 1000 / max(1, rt.cfg.FPS)
+10
View File
@@ -147,3 +147,13 @@ func isSTUNPacket(data []byte) bool {
cookie := binary.BigEndian.Uint32(data[4:8]) cookie := binary.BigEndian.Uint32(data[4:8])
return cookie == 0x2112A442 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
}
+42
View File
@@ -224,3 +224,45 @@ func runSTUNConsent(ctx context.Context, conn *net.UDPConn, peer *net.UDPAddr, p
} }
} }
} }
// sendWebRTCExtraDecoys sends compound RTCP and STUN with proper WebRTC formatting.
func sendWebRTCExtraDecoys(conn *net.UDPConn, src *net.UDPAddr, dst *net.UDPAddr, rt *runtimeCtx,
ssrc uint32, seq *uint16, ts *uint32, step int, upDirection bool) {
srcIP := ip4OrLoopback(src.IP)
dstIP := ip4OrLoopback(dst.IP)
var rl *decoyRL
if upDirection {
rl = rt.upRL
} else {
rl = rt.downRL
}
const maxBurst = 2
// Compound RTCP (SR+SDES) instead of bare SR
if rl != nil && rl.sr != nil {
n := rl.sr.takeMax(maxBurst)
for i := 0; i < n; i++ {
pktCount := uint32(metricFramesUp.Value())
octCount := uint32(metricBytesUp.Value())
compound := buildCompoundRTCP(ssrc, *ts, pktCount, octCount)
rt.tb.wait(len(compound) + 28)
_, _ = conn.WriteToUDP(compound, dst)
rt.pc.WriteUDP(srcIP, src.Port, dstIP, dst.Port, compound)
metricRTCPsrSent.Add(1)
}
}
// STUN with FINGERPRINT
if rl != nil && rl.stun != nil {
n := rl.stun.takeMax(1)
for i := 0; i < n; i++ {
req := buildSTUNBindingRequestFull()
rt.tb.wait(len(req) + 28)
_, _ = conn.WriteToUDP(req, dst)
rt.pc.WriteUDP(srcIP, src.Port, dstIP, dst.Port, req)
metricSTUNSent.Add(1)
}
}
}