diff --git a/README.md b/README.md index 369a7e1..22001af 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,209 @@ -### CamoStreamPro +# CamoStream ->我也不知道有什么用 +Network traffic obfuscation tool that disguises real UDP/TCP traffic as legitimate video streaming protocols. Supports multiple wire formats for different camouflage scenarios. -#### build +> For authorized internal security testing only. -```bash -go build -o camostream main.go +## Wire Modes + +| Mode | Disguise As | Protocol Stack | +|------|-------------|----------------| +| `rtpish` | Generic RTP video | RTP(12B) + Shim + Payload | +| `webrtc` | WebRTC video call | SRTP(24B) + Extensions + Auth Tag + Opus Audio + Compound RTCP + STUN | +| `ipcam` | Surveillance camera | H.264 FU-A over RTP + SPS/PPS + GOP I/P frames | +| `shim` | Raw tunnel (no disguise) | ShimHeader + Payload | + +## Architecture + +``` +[App] --UDP--> [CamoStream Client :37001] + | + | encrypted + disguised tunnel + v + [CamoStream Server :39001] --UDP--> [Real Backend :18081] ``` -#### tcp +## Build ```bash +go build -o camostream . +``` -./camostream -role=client -mode=tcp -listen=:37001 -server=127.0.0.1:39001 \ - -bitrate-mbps=20 -decoy-rps=10 \ - -pcap=tcp_client.pcap -pcap-max-mb=50 -metrics=:9101 -log=info - - +Requires Go 1.24+, zero external dependencies (stdlib only). + +## Quick Start + +### WebRTC Mode (Recommended) + +```bash +# Server side +./camostream -role=server -mode=udp -wire=webrtc \ + -listen=:39001 -forward=127.0.0.1:18081 \ + -bitrate-mbps=20 -fps=30 \ + -aes=0123456789abcdef0123456789abcdef \ + -decoy-rps=5 -rtcp-sr-rps=1 -stun-rps=0.2 \ + -metrics=:9100 -log=info + +# Client side +./camostream -role=client -mode=udp -wire=webrtc \ + -listen=:37001 -server=:39001 \ + -bitrate-mbps=20 -fps=30 \ + -aes=0123456789abcdef0123456789abcdef \ + -decoy-rps=5 -rtcp-sr-rps=1 -stun-rps=0.2 \ + -metrics=:9101 -log=info +``` + +### IPCAM Mode (Surveillance Camera) + +```bash +# Server +./camostream -role=server -mode=udp -wire=ipcam \ + -listen=:39001 -forward=127.0.0.1:18081 \ + -ipcam-fps=25 -ipcam-gop=50 -bitrate-mbps=4 \ + -aes=0123456789abcdef0123456789abcdef \ + -metrics=:9100 + +# Client +./camostream -role=client -mode=udp -wire=ipcam \ + -listen=:37001 -server=:39001 \ + -ipcam-fps=25 -ipcam-gop=50 -bitrate-mbps=4 \ + -aes=0123456789abcdef0123456789abcdef \ + -metrics=:9101 +``` + +### TCP Mode + +```bash +# Server ./camostream -role=server -mode=tcp -listen=:39001 -forward=127.0.0.1:4141 \ - -bitrate-mbps=20 -decoy-rps=10 \ - -pcap=tcp_server.pcap -pcap-max-mb=50 -metrics=:9100 -log=info + -bitrate-mbps=20 -decoy-rps=10 -aes=0123456789abcdef0123456789abcdef +# Client +./camostream -role=client -mode=tcp -listen=:37001 -server=127.0.0.1:39001 \ + -bitrate-mbps=20 -decoy-rps=10 -aes=0123456789abcdef0123456789abcdef ``` +## SpeedTest Demo - - - - - - -#### udp +Built-in CRC32 integrity verification and throughput measurement: ```bash -./camostream -role=server -mode=udp -listen=:39001 -forward=127.0.0.1:18081 \ - -wire=rtpish -fps=60 -bitrate-mbps=20 \ - -decoy-rps=12 \ - -rtcp-sr-rps=2 -rtcp-rr-rps=3 -rtpkeep-rps=4 -stun-rps=1 \ - -pcap=udp_server.pcap -pcap-max-mb=100 -metrics=:9100 -log=info - - -./camostream -role=client -mode=udp -listen=:37001 -server=127.0.0.1:39001 \ - -wire=rtpish -fps=60 -bitrate-mbps=20 \ - -decoy-rps=12 \ - -rtcp-sr-rps=2 -rtcp-rr-rps=3 -rtpkeep-rps=4 -stun-rps=1 \ - -pcap=udp_client.pcap -pcap-max-mb=100 -metrics=:9101 -log=info - +# Start tunnel (webrtc mode) +./camostream -role=server -mode=udp -wire=webrtc -listen=:39001 -forward=127.0.0.1:18081 \ + -bitrate-mbps=50 -aes=0123456789abcdef0123456789abcdef -dtls=false -log=warn & +./camostream -role=client -mode=udp -wire=webrtc -listen=:37001 -server=127.0.0.1:39001 \ + -bitrate-mbps=50 -aes=0123456789abcdef0123456789abcdef -dtls=false -log=warn & + +# Start receiver +go run demo/speedtest.go -mode=server -recv=:18081 & + +# Run speedtest (200 pps, 1000 byte packets, 10 seconds) +go run demo/speedtest.go -mode=client -send=127.0.0.1:37001 -size=1000 -pps=200 -duration=10 ``` +Or run all wire modes: +```bash +bash demo/run_speedtest.sh +``` +### SpeedTest Results (Local, WebRTC Mode) -#### feat +``` +Sent: 1997 packets, 1.60 Mbps +Received: 818 packets through tunnel +CRC32 OK: 818 FAIL: 0 (100% integrity) +OOO: 0 DUP: 0 +``` -* ✅ UDP/TCP 双协议、Client/Server 双角色 -* ✅ UDP:RTP-ish(12B RTP 头)+ shim 载荷、60/120fps、GOP 峰谷、抖动 -* ✅ 码率整形(令牌桶) -* ✅ 诱饵插播(shim‑decoy)+ AES‑GCM 可选 -* ✅ 额外伪报文(UDP 无 shim):RTCP SR、RTCP RR、纯 RTP keepalive、STUN Binding -* ✅ 自测模式(UDP Echo + Client/Server + 负载) -* ✅ PCAP(UDP RAW)与指标(/debug/vars) +## Security Features +### Encryption +- **AES-GCM** encrypts the entire shim header + payload together +- Magic bytes (`0x5C10ADED`) never appear on the wire when encryption is enabled +- Without AES: magic is XOR-masked with session-derived key to prevent static fingerprinting -#### 增强 +### WebRTC Camouflage +- 24-byte SRTP headers with `0xBEDE` extensions (abs-send-time, transport-cc) +- 10-byte SRTP authentication tag on every packet +- Opus audio stream at 50 pps (PT=111) with separate SSRC +- Compound RTCP (SR + SDES with CNAME) per RFC 3550 +- STUN Binding Request/Response with FINGERPRINT attribute +- STUN consent freshness every 5 seconds +- DTLS 1.2 handshake simulation at session start (optional) -* UDP 方向新增无 shim 的额外伪报文(中间盒可见,但业务端不感知): -* RTCP SR(PT=200):包含 sender SSRC、NTP 时间戳、RTP 时间戳、包/字节计数。 -* RTCP RR(PT=201):简单接收者报告,无 report block。 -* 纯 RTP keepalive:PT=13(CN 习惯),小 payload/可零 payload。 -* STUN Binding Request:标准 20B 报文,含 Magic Cookie 和 Transaction ID。 -这些报文不带 shim,因此服务端在解析 RTP-ish+shim 失败时直接 continue 丢弃;同样客户端也会丢弃,从而只起到“流量伪装/背景噪声”作用。 -* 仍保持诱饵为“插播”(不替代真实帧): -* TCP:真实帧 → (可选)插播 shim‑decoy。 -* UDP:真实帧(RTP-ish+shim 或 shim)→ (可选)插播 shim‑decoy → (可选)插播 RTCP/RTP/STUN 等额外无壳伪报文。 -* 新增 CLI 控制这类伪报文注入概率(UDP only): -* -rtcp-sr-pct:插播 RTCP SR 的概率(默认 4) -* -rtcp-rr-pct:插播 RTCP RR 的概率(默认 6) -* -rtpkeep-pct:插播纯 RTP keepalive 的概率(默认 5) -* -stun-pct:插播 STUN Binding 的概率(默认 3) +### Decoy System +- **Shim decoys**: encrypted fake frames injected at configurable RPS +- **RTCP SR/RR**: realistic sender/receiver reports +- **RTP keepalive**: comfort noise (PT=13) packets +- **STUN Binding**: ICE connectivity checks with proper responses +- Decoys sent with 2-8ms random delay to avoid burst timing fingerprint +## DPI Resistance ->备注:-wire=rtpish 仅对 UDP 生效;TCP 会打印一个 WARN 并忽略 +Tested with automated 7-dimension analysis: +| Dimension | Score | Description | +|-----------|-------|-------------| +| Protocol Conformance | 100/100 | All packets classify as RTP/RTCP/STUN | +| Packet Size Distribution | 69/100 | Bimodal (audio small + video large) | +| Timing Analysis | 69/100 | Consistent with video call FPS | +| Entropy | 53/100 | High entropy from AES-GCM | +| RTP Consistency | 100/100 | Perfect sequence/timestamp progression | +| Decoy Coverage | 40/100 | Multiple decoy types present | +| **Overall** | **73.8/100** | **Grade C - Passes basic DPI** | +## CLI Reference +``` +-wire shim|rtpish|webrtc|ipcam Wire format (UDP only) +-role server|client|selftest Role +-mode udp|tcp Transport mode +-listen :port Listen address +-server host:port Server address (client mode) +-forward host:port Forward target (server mode) +-aes hex-key AES-GCM 128/192/256 bit key +-bitrate-mbps N Bitrate cap in Mbps +-fps N Frames per second +-decoy-rps N Shim decoy frames per second +-rtcp-sr-rps N RTCP SR decoys per second +-rtcp-rr-rps N RTCP RR decoys per second +-stun-rps N STUN decoys per second +-dtls bool DTLS handshake (webrtc, default true) +-audio-rps N Audio packets/sec (webrtc, default 50) +-ipcam-fps N Camera FPS (ipcam, default 25) +-ipcam-gop N GOP size (ipcam, default 50) +-pcap path PCAP output file +-pcap-max-mb N Max PCAP size in MB +-metrics :port Metrics HTTP endpoint +-log debug|info|warn|error Log level +``` -#### 指标 +## Metrics -* http://127.0.0.1:9100/debug/vars(server) -* http://127.0.0.1:9101/debug/vars(client) -* 关注:bytes_*、frames_*、decoy_dropped、shim_decoy_sent、rtcp_sr_sent、rtcp_rr_sent、rtp_keepalive_sent、stun_sent +Available at `http://host:port/debug/vars`: +``` +bytes_up, bytes_down, frames_up, frames_down, +decoy_dropped, sessions_active, shim_decoy_sent, +rtcp_sr_sent, rtcp_rr_sent, rtp_keepalive_sent, +stun_sent, dtls_handshake_sent, audio_packets_sent +``` -#### Warn +## Project Structure -仅用于授权的内部安全测试。涉及伪装/混淆的功能,请严格遵循公司及法律合规要求。 -TCP 的 PCAP 是伪造的网络层帧,用于 debug 观察我们应用层写入/读到的 shim 帧,不代表内核真实的 TCP 会话(没有三次握手、窗口/ACK 真实演进),但校验和正确,可在 Wireshark 中查看和过滤。 \ No newline at end of file +``` +├── main.go Core framework, UDP/TCP client/server, CLI +├── crypto.go sealFrame/openFrame (full shim+payload encryption) +├── dtls.go DTLS 1.2 handshake simulation +├── wire.go Unified encode/decode path for all wire formats +├── wire_webrtc.go WebRTC: SRTP, audio ticker, compound RTCP, STUN +├── wire_ipcam.go IPCAM: H.264 FU-A, GOP state, SPS/PPS +├── demo/ SpeedTest demo with CRC32 verification +├── sim/ UDP echo server for testing +└── tests/ E2E tests, PCAP analysis, Docker environment +``` + +## Disclaimer + +This tool is designed for authorized internal security testing only. Traffic obfuscation capabilities must be used in compliance with applicable laws and organizational policies. diff --git a/demo/run_speedtest.sh b/demo/run_speedtest.sh new file mode 100755 index 0000000..0767bdb --- /dev/null +++ b/demo/run_speedtest.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# CamoStream SpeedTest Demo +# Tests all wire modes with CRC32 data integrity verification +set -uo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BIN="$ROOT/camostream" +DEMO="$ROOT/demo/speedtest.go" +AES_KEY="0123456789abcdef0123456789abcdef" + +RED='\033[0;31m' +GREEN='\033[0;32m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +PIDS=() +cleanup() { + for p in "${PIDS[@]}"; do kill -9 "$p" 2>/dev/null; done + PIDS=() + pkill -9 -f "camostream.*-role=" 2>/dev/null + pkill -9 -f "speedtest.*-mode=" 2>/dev/null + sleep 1 +} +trap cleanup EXIT + +track() { PIDS+=("$1"); } + +build() { + echo -e "${CYAN}Building camostream...${NC}" + cd "$ROOT" && go build -o camostream . || exit 1 + echo -e "${GREEN}Build OK${NC}" +} + +run_test() { + local wire="$1" + local label="$2" + local extra_server="${3:-}" + local extra_client="${4:-}" + local pps="${5:-200}" + local size="${6:-1000}" + local dur="${7:-10}" + + echo "" + echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BOLD} Wire Mode: ${CYAN}${label}${NC}" + echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + + cleanup 2>/dev/null + + # Start CamoStream server + "$BIN" -role=server -mode=udp -wire="$wire" -listen=:39001 -forward=127.0.0.1:18081 \ + -bitrate-mbps=50 -fps=60 -decoy-rps=5 -aes="$AES_KEY" \ + -rtcp-sr-rps=1 -stun-rps=0.2 -dtls=false \ + -metrics=:9100 -log=warn $extra_server & + track $! + sleep 0.5 + + # Start CamoStream client + "$BIN" -role=client -mode=udp -wire="$wire" -listen=:37001 -server=127.0.0.1:39001 \ + -bitrate-mbps=50 -fps=60 -decoy-rps=5 -aes="$AES_KEY" \ + -rtcp-sr-rps=1 -stun-rps=0.2 -dtls=false \ + -metrics=:9101 -log=warn $extra_client & + track $! + sleep 2 + + # Start speedtest server (receiver behind tunnel) + go run "$DEMO" -mode=server -recv=:18081 & + track $! + sleep 1 + + # Run speedtest client (sender through tunnel) + go run "$DEMO" -mode=client -send=127.0.0.1:37001 \ + -size="$size" -pps="$pps" -duration="$dur" + + sleep 2 + + # Grab metrics + echo "" + echo -e "${CYAN}CamoStream Metrics:${NC}" + curl -s http://127.0.0.1:9100/debug/vars 2>/dev/null | python3 -c " +import sys,json +try: + d=json.load(sys.stdin) + for k in sorted(d.keys()): + if isinstance(d[k],(int,float)) and d[k]>0: + print(f' {k}: {d[k]}') +except: pass +" 2>/dev/null + + # Kill the speedtest server (send SIGINT for final report) + for p in "${PIDS[@]}"; do + if ps -p "$p" -o comm= 2>/dev/null | grep -q "go\|speedtest"; then + kill -INT "$p" 2>/dev/null + sleep 1 + fi + done + + cleanup 2>/dev/null +} + +# ─── Main ─────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}╔══════════════════════════════════════════════════╗${NC}" +echo -e "${BOLD}║ CamoStream SpeedTest - All Wire Modes ║${NC}" +echo -e "${BOLD}╚══════════════════════════════════════════════════╝${NC}" + +build + +# Test 1: rtpish (legacy) +run_test "rtpish" "RTP-ish (Legacy)" "" "" 200 1000 10 + +# Test 2: webrtc +run_test "webrtc" "WebRTC (SRTP + Audio)" "" "" 200 1000 10 + +# Test 3: ipcam +run_test "ipcam" "IPCAM (H.264 Surveillance)" "" "" 200 1000 10 + +echo "" +echo -e "${GREEN}${BOLD}All speedtests complete.${NC}" diff --git a/demo/speedtest.go b/demo/speedtest.go new file mode 100644 index 0000000..62ad2dc --- /dev/null +++ b/demo/speedtest.go @@ -0,0 +1,299 @@ +// demo/speedtest.go +// UDP throughput & integrity test through CamoStream tunnel. +// +// Architecture: +// [speedtest sender] --UDP--> [:37001 CamoStream Client] ==tunnel==> +// [:39001 CamoStream Server] --UDP--> [:18081 speedtest receiver] +// +// Usage: +// 1. Start CamoStream server + client (any wire mode) +// 2. go run demo/speedtest.go -mode=server (listens on :18081) +// 3. go run demo/speedtest.go -mode=client (sends to :37001) +// +// The client sends numbered packets with CRC32 checksums. +// The server verifies each packet and reports stats. + +package main + +import ( + "encoding/binary" + "flag" + "fmt" + "hash/crc32" + "math/rand" + "net" + "os" + "os/signal" + "sync/atomic" + "syscall" + "time" +) + +// Packet layout: +// [0:4] sequence number (uint32 BE) +// [4:8] CRC32 of payload portion (uint32 BE) +// [8:16] send timestamp nanoseconds (int64 BE) +// [16:] random payload + +const headerSize = 16 + +func main() { + mode := flag.String("mode", "client", "client|server|both") + sendAddr := flag.String("send", "127.0.0.1:37001", "send to (CamoStream client listen)") + recvAddr := flag.String("recv", ":18081", "listen on (CamoStream server forwards here)") + pktSize := flag.Int("size", 1000, "packet payload size in bytes") + duration := flag.Int("duration", 10, "test duration in seconds") + pps := flag.Int("pps", 100, "packets per second") + flag.Parse() + + switch *mode { + case "server": + runServer(*recvAddr) + case "client": + runClient(*sendAddr, *pktSize, *duration, *pps) + case "both": + go runServer(*recvAddr) + time.Sleep(500 * time.Millisecond) + runClient(*sendAddr, *pktSize, *duration, *pps) + default: + fmt.Fprintf(os.Stderr, "unknown mode: %s\n", *mode) + os.Exit(1) + } +} + +func runClient(addr string, pktSize, durSec, pps int) { + dst, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + fmt.Fprintf(os.Stderr, "resolve %s: %v\n", addr, err) + os.Exit(1) + } + conn, err := net.DialUDP("udp", nil, dst) + if err != nil { + fmt.Fprintf(os.Stderr, "dial: %v\n", err) + os.Exit(1) + } + defer conn.Close() + + if pktSize < headerSize+1 { + pktSize = headerSize + 1 + } + + fmt.Printf("╔══════════════════════════════════════════════════╗\n") + fmt.Printf("║ CamoStream UDP SpeedTest - Client ║\n") + fmt.Printf("╠══════════════════════════════════════════════════╣\n") + fmt.Printf("║ Target: %-36s ║\n", addr) + fmt.Printf("║ Pkt Size: %-4d bytes ║\n", pktSize) + fmt.Printf("║ PPS: %-4d ║\n", pps) + fmt.Printf("║ Duration: %-2d seconds ║\n", durSec) + fmt.Printf("║ Expected: %-6.2f Mbps ║\n", float64(pktSize*pps*8)/1e6) + fmt.Printf("╚══════════════════════════════════════════════════╝\n\n") + + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + buf := make([]byte, pktSize) + interval := time.Second / time.Duration(pps) + ticker := time.NewTicker(interval) + defer ticker.Stop() + + deadline := time.After(time.Duration(durSec) * time.Second) + var seq uint32 + var totalBytes int64 + start := time.Now() + + for { + select { + case <-deadline: + elapsed := time.Since(start).Seconds() + mbps := float64(totalBytes*8) / elapsed / 1e6 + fmt.Printf("\n[Client] Sent %d packets, %d bytes in %.1fs\n", seq, totalBytes, elapsed) + fmt.Printf("[Client] Throughput: %.2f Mbps\n", mbps) + return + case <-ticker.C: + // Fill random payload + rng.Read(buf[headerSize:]) + // Header: seq + CRC + timestamp + binary.BigEndian.PutUint32(buf[0:4], seq) + csum := crc32.ChecksumIEEE(buf[headerSize:]) + binary.BigEndian.PutUint32(buf[4:8], csum) + binary.BigEndian.PutUint64(buf[8:16], uint64(time.Now().UnixNano())) + + n, err := conn.Write(buf) + if err != nil { + fmt.Fprintf(os.Stderr, "[Client] write error: %v\n", err) + continue + } + totalBytes += int64(n) + seq++ + + if seq%uint32(pps) == 0 { + elapsed := time.Since(start).Seconds() + mbps := float64(totalBytes*8) / elapsed / 1e6 + fmt.Printf("[Client] %ds: sent %d pkts, %.2f Mbps\n", int(elapsed), seq, mbps) + } + } + } +} + +func runServer(addr string) { + laddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + fmt.Fprintf(os.Stderr, "resolve %s: %v\n", addr, err) + os.Exit(1) + } + conn, err := net.ListenUDP("udp", laddr) + if err != nil { + fmt.Fprintf(os.Stderr, "listen: %v\n", err) + os.Exit(1) + } + defer conn.Close() + + fmt.Printf("╔══════════════════════════════════════════════════╗\n") + fmt.Printf("║ CamoStream UDP SpeedTest - Server ║\n") + fmt.Printf("╠══════════════════════════════════════════════════╣\n") + fmt.Printf("║ Listening: %-36s ║\n", addr) + fmt.Printf("╚══════════════════════════════════════════════════╝\n\n") + + var ( + totalPkts uint64 + totalBytes uint64 + crcOK uint64 + crcFail uint64 + outOfOrder uint64 + duplicate uint64 + latencySum int64 + latencyMin int64 = 1<<62 + latencyMax int64 + expectedSeq uint32 + started bool + startTime time.Time + ) + + // Echo response back (so CamoStream server->client path also works) + buf := make([]byte, 64*1024) + + // Stats printer + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + + go func() { + t := time.NewTicker(2 * time.Second) + defer t.Stop() + for { + select { + case <-t.C: + if !started { + continue + } + elapsed := time.Since(startTime).Seconds() + p := atomic.LoadUint64(&totalPkts) + b := atomic.LoadUint64(&totalBytes) + ok := atomic.LoadUint64(&crcOK) + fail := atomic.LoadUint64(&crcFail) + ooo := atomic.LoadUint64(&outOfOrder) + dup := atomic.LoadUint64(&duplicate) + mbps := float64(b*8) / elapsed / 1e6 + fmt.Printf("[Server] %.0fs: %d pkts, %.2f Mbps | CRC OK:%d FAIL:%d | OOO:%d DUP:%d\n", + elapsed, p, mbps, ok, fail, ooo, dup) + case <-sig: + printFinalReport(startTime, totalPkts, totalBytes, crcOK, crcFail, + outOfOrder, duplicate, latencySum, latencyMin, latencyMax) + os.Exit(0) + } + } + }() + + for { + n, from, err := conn.ReadFromUDP(buf) + if err != nil { + continue + } + if n < headerSize { + continue + } + + if !started { + started = true + startTime = time.Now() + fmt.Printf("[Server] First packet from %s\n", from) + } + + atomic.AddUint64(&totalPkts, 1) + atomic.AddUint64(&totalBytes, uint64(n)) + + // Parse header + seq := binary.BigEndian.Uint32(buf[0:4]) + expectedCRC := binary.BigEndian.Uint32(buf[4:8]) + sendTS := int64(binary.BigEndian.Uint64(buf[8:16])) + + // CRC verification + actualCRC := crc32.ChecksumIEEE(buf[headerSize:n]) + if actualCRC == expectedCRC { + atomic.AddUint64(&crcOK, 1) + } else { + atomic.AddUint64(&crcFail, 1) + } + + // Sequence check + if seq == expectedSeq { + expectedSeq++ + } else if seq < expectedSeq { + atomic.AddUint64(&duplicate, 1) + } else { + atomic.AddUint64(&outOfOrder, 1) + expectedSeq = seq + 1 + } + + // Latency + lat := time.Now().UnixNano() - sendTS + atomic.AddInt64(&latencySum, lat) + if lat < atomic.LoadInt64(&latencyMin) { + atomic.StoreInt64(&latencyMin, lat) + } + if lat > atomic.LoadInt64(&latencyMax) { + atomic.StoreInt64(&latencyMax, lat) + } + + // Echo back (trimmed to small ack) + ack := buf[:headerSize] + conn.WriteToUDP(ack, from) + } +} + +func printFinalReport(start time.Time, pkts, bytes, ok, fail, ooo, dup uint64, + latSum, latMin, latMax int64) { + elapsed := time.Since(start).Seconds() + if elapsed < 0.001 { + elapsed = 0.001 + } + mbps := float64(bytes*8) / elapsed / 1e6 + avgLat := float64(0) + if pkts > 0 { + avgLat = float64(latSum) / float64(pkts) / 1e6 + } + + fmt.Printf("\n") + fmt.Printf("╔══════════════════════════════════════════════════╗\n") + fmt.Printf("║ SpeedTest Final Report ║\n") + fmt.Printf("╠══════════════════════════════════════════════════╣\n") + fmt.Printf("║ Duration: %8.1f s ║\n", elapsed) + fmt.Printf("║ Packets: %8d ║\n", pkts) + fmt.Printf("║ Bytes: %8d ║\n", bytes) + fmt.Printf("║ Throughput: %8.2f Mbps ║\n", mbps) + fmt.Printf("╠══════════════════════════════════════════════════╣\n") + fmt.Printf("║ CRC32 OK: %8d ║\n", ok) + fmt.Printf("║ CRC32 FAIL: %8d ║\n", fail) + fmt.Printf("║ Integrity: %7.1f%% ║\n", float64(ok)/float64(max64(pkts,1))*100) + fmt.Printf("╠══════════════════════════════════════════════════╣\n") + fmt.Printf("║ Out-of-Order: %8d ║\n", ooo) + fmt.Printf("║ Duplicates: %8d ║\n", dup) + fmt.Printf("║ Avg Latency: %7.2f ms ║\n", avgLat) + fmt.Printf("║ Min Latency: %7.2f ms ║\n", float64(latMin)/1e6) + fmt.Printf("║ Max Latency: %7.2f ms ║\n", float64(latMax)/1e6) + fmt.Printf("╚══════════════════════════════════════════════════╝\n") +} + +func max64(a, b uint64) uint64 { + if a > b { + return a + } + return b +}