diff --git a/.gitignore b/.gitignore index 1d1cdef..f96c3ab 100644 --- a/.gitignore +++ b/.gitignore @@ -33,5 +33,16 @@ go.work.sum # Ignore everything # But not these files... !/.gitignore - +.idea +.DS_Store +.env +docs/ +*.docx +*.doc +*.xlsx +*.csv +userdata/ +session/ +tests/results/ +tests/**/*.pcap diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5429376 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,43 @@ +# CamoStream + +## Project Overview +CamoStream 是一个 Go 语言网络流量伪装工具,将真实流量封装为视频流(RTP/RTCP)流量,用于授权的内部安全测试。 + +## Tech Stack +- **Language**: Go 1.24.3, zero external dependencies (stdlib only) +- **Architecture**: Single-file `main.go` (1562 lines) +- **Build**: `go build -o camostream main.go` + +## Key Features +- UDP/TCP dual protocol, Client/Server/Selftest roles +- RTP-ish (12B RTP header) + shim payload wrapping (UDP) +- Token bucket bitrate shaping +- Decoy injection: shim-decoy + RTCP SR/RR + RTP keepalive + STUN Binding +- AES-GCM optional encryption +- PCAP debug capture with size cap +- expvar metrics on /debug/vars + +## Protocol Format +- **Shim Header**: 20 bytes (magic 0x5C10ADED, version, mode, flags, session_id, timestamp, length) +- **RTP-ish**: 12B RTP header + Shim + Payload (UDP only) +- **Flags**: bit0=decoy, bit1=encrypted + +## Ports Convention +- Server listen: 39001, metrics: 9100 +- Client listen: 37001, metrics: 9101 +- Forward target: configurable (e.g., 4141 for TCP, 18081 for UDP) + +## Build & Run +```bash +go build -o camostream main.go +./server.sh # TCP server +./client.sh # TCP client +./server_udp.sh # UDP server +./client_udp.sh # UDP client +``` + +## Testing +```bash +cd tests/ +./run_all_tests.sh +``` diff --git a/tests/Dockerfile b/tests/Dockerfile new file mode 100644 index 0000000..52b4970 --- /dev/null +++ b/tests/Dockerfile @@ -0,0 +1,38 @@ +# Stage 1: Build camostream binary +FROM golang:1.24-alpine AS builder + +WORKDIR /src +COPY go.mod ./ +COPY main.go ./ + +RUN CGO_ENABLED=0 GOOS=linux go build -o /camostream main.go + +WORKDIR /src/sim +COPY sim/udp_server.go ./ +RUN go mod init udp_echo && CGO_ENABLED=0 GOOS=linux go build -o /udp_server udp_server.go + +# Stage 2: Runtime with network analysis tools +FROM alpine:3.20 + +RUN apk add --no-cache \ + tshark \ + tcpdump \ + curl \ + python3 \ + py3-pip \ + bash \ + netcat-openbsd \ + coreutils \ + && rm -rf /var/cache/apk/* + +COPY --from=builder /camostream /usr/local/bin/camostream +COPY --from=builder /udp_server /usr/local/bin/udp_server + +# Copy test scripts +COPY tests/scripts/ /opt/tests/scripts/ +COPY tests/backend/ /opt/tests/backend/ + +RUN chmod +x /usr/local/bin/camostream /usr/local/bin/udp_server + +WORKDIR /opt/tests +ENTRYPOINT ["/bin/bash"] diff --git a/tests/Dockerfile.analyzer b/tests/Dockerfile.analyzer new file mode 100644 index 0000000..2535f9b --- /dev/null +++ b/tests/Dockerfile.analyzer @@ -0,0 +1,16 @@ +# Lightweight Python image for pcap analysis +FROM python:3.12-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpcap-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install --no-cache-dir \ + scapy \ + numpy \ + matplotlib + +COPY tests/scripts/ /opt/analysis/scripts/ + +WORKDIR /opt/analysis +ENTRYPOINT ["python3"] diff --git a/tests/backend/server.py b/tests/backend/server.py new file mode 100644 index 0000000..d1de093 --- /dev/null +++ b/tests/backend/server.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +Simple HTTP echo server for CamoStream test environment. +Listens on port 8080, returns request headers and body as response. +""" + +import json +import sys +import logging +from http.server import HTTPServer, BaseHTTPRequestHandler +from datetime import datetime + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + stream=sys.stdout, +) +logger = logging.getLogger("echo-server") + + +class EchoHandler(BaseHTTPRequestHandler): + """Echo back request details as JSON response.""" + + def _build_response(self, body: bytes = b"") -> bytes: + response = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "method": self.command, + "path": self.path, + "headers": dict(self.headers), + "body": body.decode("utf-8", errors="replace"), + "client": f"{self.client_address[0]}:{self.client_address[1]}", + } + return json.dumps(response, indent=2).encode("utf-8") + + def _handle_request(self): + content_length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(content_length) if content_length > 0 else b"" + + logger.info( + "%s %s from %s:%d (%d bytes)", + self.command, + self.path, + self.client_address[0], + self.client_address[1], + len(body), + ) + + response_body = self._build_response(body) + + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(response_body))) + self.send_header("X-Echo-Server", "camostream-test") + self.end_headers() + self.wfile.write(response_body) + + def do_GET(self): + self._handle_request() + + def do_POST(self): + self._handle_request() + + def do_PUT(self): + self._handle_request() + + def do_DELETE(self): + self._handle_request() + + def do_HEAD(self): + response_body = self._build_response() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(response_body))) + self.send_header("X-Echo-Server", "camostream-test") + self.end_headers() + + def log_message(self, format, *args): + # Suppress default BaseHTTPRequestHandler logging; we use our own logger + pass + + +def main(): + host = "0.0.0.0" + port = 8080 + server = HTTPServer((host, port), EchoHandler) + logger.info("Echo server listening on %s:%d", host, port) + try: + server.serve_forever() + except KeyboardInterrupt: + logger.info("Shutting down") + server.server_close() + + +if __name__ == "__main__": + main() diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 0000000..78c7053 --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,372 @@ +version: "3.9" + +volumes: + pcap-data: + test-results: + +networks: + camotest: + driver: bridge + +services: + # ── HTTP echo backend (TCP tunnel target) ────────────────────── + backend: + build: + context: .. + dockerfile: tests/Dockerfile + command: ["python3", "/opt/tests/backend/server.py"] + networks: + - camotest + expose: + - "8080" + healthcheck: + test: ["CMD", "curl", "-sf", "http://127.0.0.1:8080/health"] + interval: 3s + timeout: 2s + retries: 10 + start_period: 2s + + # ── UDP echo backend (UDP tunnel target) ─────────────────────── + backend-udp: + build: + context: .. + dockerfile: tests/Dockerfile + command: ["udp_server"] + networks: + - camotest + expose: + - "18081/udp" + healthcheck: + test: ["CMD", "true"] + interval: 5s + timeout: 2s + retries: 3 + + # ── CamoStream TCP Server ───────────────────────────────────── + camostream-server-tcp: + build: + context: .. + dockerfile: tests/Dockerfile + command: + - camostream + - -role=server + - -mode=tcp + - -listen=:39001 + - -forward=backend:8080 + - -bitrate-mbps=20 + - -fps=60 + - -decoy-rps=10 + - -aes=0123456789abcdef0123456789abcdef + - -pcap=/data/pcap/tcp_server.pcap + - -pcap-max-mb=50 + - -metrics=:9100 + - -log=debug + - -showdrop + networks: + - camotest + expose: + - "39001" + - "9100" + volumes: + - pcap-data:/data/pcap + depends_on: + backend: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-sf", "http://127.0.0.1:9100/debug/vars"] + interval: 3s + timeout: 2s + retries: 10 + start_period: 3s + + # ── CamoStream TCP Client ───────────────────────────────────── + camostream-client-tcp: + build: + context: .. + dockerfile: tests/Dockerfile + command: + - camostream + - -role=client + - -mode=tcp + - -listen=:37001 + - -server=camostream-server-tcp:39001 + - -bitrate-mbps=20 + - -fps=60 + - -decoy-rps=10 + - -aes=0123456789abcdef0123456789abcdef + - -pcap=/data/pcap/tcp_client.pcap + - -pcap-max-mb=50 + - -metrics=:9101 + - -log=debug + - -showdrop + networks: + - camotest + expose: + - "37001" + - "9101" + ports: + - "37001:37001" + volumes: + - pcap-data:/data/pcap + depends_on: + camostream-server-tcp: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-sf", "http://127.0.0.1:9101/debug/vars"] + interval: 3s + timeout: 2s + retries: 10 + start_period: 3s + + # ── CamoStream UDP Server ───────────────────────────────────── + camostream-server-udp: + build: + context: .. + dockerfile: tests/Dockerfile + command: + - camostream + - -role=server + - -mode=udp + - -wire=rtpish + - -listen=:39002 + - -forward=backend-udp:18081 + - -bitrate-mbps=20 + - -fps=60 + - -decoy-rps=10 + - -rtcp-sr-rps=2 + - -rtcp-rr-rps=3 + - -rtpkeep-rps=4 + - -stun-rps=1 + - -aes=0123456789abcdef0123456789abcdef + - -pcap=/data/pcap/udp_server.pcap + - -pcap-max-mb=50 + - -metrics=:9200 + - -log=debug + - -showdrop + networks: + - camotest + expose: + - "39002/udp" + - "9200" + volumes: + - pcap-data:/data/pcap + depends_on: + backend-udp: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-sf", "http://127.0.0.1:9200/debug/vars"] + interval: 3s + timeout: 2s + retries: 10 + start_period: 3s + + # ── CamoStream UDP Client ───────────────────────────────────── + camostream-client-udp: + build: + context: .. + dockerfile: tests/Dockerfile + command: + - camostream + - -role=client + - -mode=udp + - -wire=rtpish + - -listen=:37002 + - -server=camostream-server-udp:39002 + - -bitrate-mbps=20 + - -fps=60 + - -decoy-rps=10 + - -rtcp-sr-rps=2 + - -rtcp-rr-rps=3 + - -rtpkeep-rps=4 + - -stun-rps=1 + - -aes=0123456789abcdef0123456789abcdef + - -pcap=/data/pcap/udp_client.pcap + - -pcap-max-mb=50 + - -metrics=:9201 + - -log=debug + - -showdrop + networks: + - camotest + expose: + - "37002/udp" + - "9201" + ports: + - "37002:37002/udp" + volumes: + - pcap-data:/data/pcap + depends_on: + camostream-server-udp: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-sf", "http://127.0.0.1:9201/debug/vars"] + interval: 3s + timeout: 2s + retries: 10 + start_period: 3s + + # ── Network capture (sniff all traffic on camotest) ──────────── + capture: + image: alpine:3.20 + command: + - sh + - -c + - | + apk add --no-cache tcpdump >/dev/null 2>&1 + echo "[capture] starting tcpdump on all interfaces..." + tcpdump -i any -w /data/pcap/camotest_full.pcap -s 0 -U + networks: + - camotest + cap_add: + - NET_RAW + - NET_ADMIN + volumes: + - pcap-data:/data/pcap + restart: unless-stopped + + # ── Test runner ──────────────────────────────────────────────── + test-runner: + build: + context: .. + dockerfile: tests/Dockerfile + command: + - bash + - -c + - | + echo "=== CamoStream Test Suite ===" + echo "Waiting for services to stabilize..." + sleep 3 + + PASS=0 + FAIL=0 + RESULTS=/data/results/report.txt + mkdir -p /data/results + + echo "Test run started at $$(date -u)" | tee $$RESULTS + + # ── Test 1: TCP tunnel end-to-end ── + echo -n "[TEST 1] TCP tunnel HTTP echo... " | tee -a $$RESULTS + RESP=$$(curl -sf --max-time 10 -X POST \ + -H "Content-Type: text/plain" \ + -d "hello-camostream-tcp" \ + http://camostream-client-tcp:37001/test-tcp 2>&1) + if echo "$$RESP" | grep -q "hello-camostream-tcp"; then + echo "PASS" | tee -a $$RESULTS + PASS=$$((PASS+1)) + else + echo "FAIL (response: $$RESP)" | tee -a $$RESULTS + FAIL=$$((FAIL+1)) + fi + + # ── Test 2: TCP tunnel multiple requests ── + echo -n "[TEST 2] TCP tunnel 10 sequential requests... " | tee -a $$RESULTS + TCP_OK=0 + for i in $$(seq 1 10); do + R=$$(curl -sf --max-time 5 http://camostream-client-tcp:37001/seq-$$i 2>&1) + if echo "$$R" | grep -q "seq-$$i"; then + TCP_OK=$$((TCP_OK+1)) + fi + done + if [ "$$TCP_OK" -eq 10 ]; then + echo "PASS ($$TCP_OK/10)" | tee -a $$RESULTS + PASS=$$((PASS+1)) + else + echo "FAIL ($$TCP_OK/10)" | tee -a $$RESULTS + FAIL=$$((FAIL+1)) + fi + + # ── Test 3: TCP metrics endpoint ── + echo -n "[TEST 3] TCP server metrics available... " | tee -a $$RESULTS + METRICS=$$(curl -sf --max-time 5 http://camostream-server-tcp:9100/debug/vars 2>&1) + if echo "$$METRICS" | grep -q "bytes_up"; then + echo "PASS" | tee -a $$RESULTS + PASS=$$((PASS+1)) + else + echo "FAIL" | tee -a $$RESULTS + FAIL=$$((FAIL+1)) + fi + + # ── Test 4: TCP client metrics endpoint ── + echo -n "[TEST 4] TCP client metrics available... " | tee -a $$RESULTS + METRICS=$$(curl -sf --max-time 5 http://camostream-client-tcp:9101/debug/vars 2>&1) + if echo "$$METRICS" | grep -q "bytes_up"; then + echo "PASS" | tee -a $$RESULTS + PASS=$$((PASS+1)) + else + echo "FAIL" | tee -a $$RESULTS + FAIL=$$((FAIL+1)) + fi + + # ── Test 5: UDP server metrics endpoint ── + echo -n "[TEST 5] UDP server metrics available... " | tee -a $$RESULTS + METRICS=$$(curl -sf --max-time 5 http://camostream-server-udp:9200/debug/vars 2>&1) + if echo "$$METRICS" | grep -q "bytes_up"; then + echo "PASS" | tee -a $$RESULTS + PASS=$$((PASS+1)) + else + echo "FAIL" | tee -a $$RESULTS + FAIL=$$((FAIL+1)) + fi + + # ── Test 6: UDP client metrics endpoint ── + echo -n "[TEST 6] UDP client metrics available... " | tee -a $$RESULTS + METRICS=$$(curl -sf --max-time 5 http://camostream-client-udp:9201/debug/vars 2>&1) + if echo "$$METRICS" | grep -q "bytes_up"; then + echo "PASS" | tee -a $$RESULTS + PASS=$$((PASS+1)) + else + echo "FAIL" | tee -a $$RESULTS + FAIL=$$((FAIL+1)) + fi + + # ── Test 7: Decoy metrics are non-zero (TCP server) ── + echo -n "[TEST 7] Decoy injection active (TCP server)... " | tee -a $$RESULTS + sleep 5 + METRICS=$$(curl -sf --max-time 5 http://camostream-server-tcp:9100/debug/vars 2>&1) + DECOY=$$(echo "$$METRICS" | grep -o '"shim_decoy_sent": [0-9]*' | grep -o '[0-9]*') + if [ -n "$$DECOY" ] && [ "$$DECOY" -gt 0 ]; then + echo "PASS (decoys sent: $$DECOY)" | tee -a $$RESULTS + PASS=$$((PASS+1)) + else + echo "FAIL (decoy count: $$DECOY)" | tee -a $$RESULTS + FAIL=$$((FAIL+1)) + fi + + # ── Test 8: PCAP files created ── + echo -n "[TEST 8] PCAP files generated... " | tee -a $$RESULTS + PCAP_COUNT=$$(ls /data/pcap/*.pcap 2>/dev/null | wc -l) + if [ "$$PCAP_COUNT" -ge 4 ]; then + echo "PASS ($$PCAP_COUNT pcap files)" | tee -a $$RESULTS + PASS=$$((PASS+1)) + else + echo "FAIL (only $$PCAP_COUNT pcap files)" | tee -a $$RESULTS + FAIL=$$((FAIL+1)) + fi + + # ── Summary ── + echo "" | tee -a $$RESULTS + echo "=== Results: $$PASS passed, $$FAIL failed ===" | tee -a $$RESULTS + echo "Test run finished at $$(date -u)" | tee -a $$RESULTS + + # Copy pcap listing to results + ls -lh /data/pcap/*.pcap >> $$RESULTS 2>/dev/null + + if [ "$$FAIL" -gt 0 ]; then + exit 1 + fi + exit 0 + networks: + - camotest + volumes: + - pcap-data:/data/pcap + - test-results:/data/results + depends_on: + camostream-client-tcp: + condition: service_healthy + camostream-client-udp: + condition: service_healthy + camostream-server-tcp: + condition: service_healthy + camostream-server-udp: + condition: service_healthy + capture: + condition: service_started diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh new file mode 100755 index 0000000..94db00f --- /dev/null +++ b/tests/run_all_tests.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# +# CamoStream - Master Test Runner +# Runs all test suites and generates a summary report +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +RESULTS_DIR="$SCRIPT_DIR/results" +REPORT_FILE="$RESULTS_DIR/test_report.txt" +TIMESTAMP=$(date '+%Y%m%d_%H%M%S') + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +mkdir -p "$RESULTS_DIR" + +header() { + echo "" + echo -e "${CYAN}================================================================${NC}" + echo -e "${CYAN} $1${NC}" + echo -e "${CYAN}================================================================${NC}" + echo "" +} + +log() { echo -e "[$(date '+%H:%M:%S')] $1"; } + +TOTAL_SUITES=0 +PASSED_SUITES=0 +FAILED_SUITES=0 + +run_suite() { + local name="$1" + local script="$2" + TOTAL_SUITES=$((TOTAL_SUITES + 1)) + + header "Test Suite: $name" + + local logfile="$RESULTS_DIR/${name// /_}_${TIMESTAMP}.log" + + if bash "$script" 2>&1 | tee "$logfile"; then + echo -e "\n${GREEN}[SUITE PASS] $name${NC}\n" + PASSED_SUITES=$((PASSED_SUITES + 1)) + else + echo -e "\n${RED}[SUITE FAIL] $name${NC}\n" + FAILED_SUITES=$((FAILED_SUITES + 1)) + fi +} + +# ---------------------------------------------------------------- +# Phase 0: Build +# ---------------------------------------------------------------- +header "Build CamoStream" +cd "$PROJECT_ROOT" + +if ! command -v go &>/dev/null; then + echo -e "${RED}Go not found. Install Go 1.24+ first.${NC}" + exit 1 +fi + +log "Building camostream binary..." +go build -o camostream main.go +log "Build OK: $(file camostream)" + +# Build UDP echo server for tests +log "Building UDP echo server..." +(cd sim && go build -o ../udp_echo udp_server.go) +log "Build OK: udp_echo" + +# ---------------------------------------------------------------- +# Phase 1: Local E2E Tests (no Docker) +# ---------------------------------------------------------------- +if [ -f "$SCRIPT_DIR/scripts/test_e2e.sh" ]; then + run_suite "E2E-Local" "$SCRIPT_DIR/scripts/test_e2e.sh" +else + log "${YELLOW}Skipping E2E tests (script not found)${NC}" +fi + +# ---------------------------------------------------------------- +# Phase 2: Traffic Stealth Analysis +# ---------------------------------------------------------------- +if [ -f "$SCRIPT_DIR/scripts/test_traffic_stealth.sh" ]; then + run_suite "Traffic-Stealth" "$SCRIPT_DIR/scripts/test_traffic_stealth.sh" +else + log "${YELLOW}Skipping stealth tests (script not found)${NC}" +fi + +# ---------------------------------------------------------------- +# Phase 3: Python PCAP Deep Analysis +# ---------------------------------------------------------------- +if [ -f "$SCRIPT_DIR/scripts/analyze_pcap.py" ]; then + # Run selftest to generate a fresh pcap for analysis + ANALYSIS_PCAP="$RESULTS_DIR/analysis_${TIMESTAMP}.pcap" + header "Generating PCAP for deep analysis (selftest 15s)" + timeout 25 "$PROJECT_ROOT/camostream" \ + -role=selftest -mode=udp -wire=rtpish \ + -fps=60 -bitrate-mbps=20 \ + -decoy-rps=10 \ + -rtcp-sr-rps=2 -rtcp-rr-rps=3 -rtpkeep-rps=4 -stun-rps=1 \ + -pcap="$ANALYSIS_PCAP" -pcap-max-mb=20 \ + -metrics=:9300 -duration=15s -log=warn \ + || true + + if [ -f "$ANALYSIS_PCAP" ] && [ -s "$ANALYSIS_PCAP" ]; then + if command -v python3 &>/dev/null; then + # Install deps if needed + pip3 install scapy numpy 2>/dev/null || true + + TOTAL_SUITES=$((TOTAL_SUITES + 1)) + ANALYSIS_REPORT="$RESULTS_DIR/pcap_analysis_${TIMESTAMP}.json" + header "PCAP Deep Analysis" + if python3 "$SCRIPT_DIR/scripts/analyze_pcap.py" \ + "$ANALYSIS_PCAP" --mode udp \ + --output "$ANALYSIS_REPORT" 2>&1 | tee "$RESULTS_DIR/pcap_analysis_${TIMESTAMP}.log"; then + echo -e "\n${GREEN}[SUITE PASS] PCAP-Analysis${NC}\n" + PASSED_SUITES=$((PASSED_SUITES + 1)) + else + echo -e "\n${RED}[SUITE FAIL] PCAP-Analysis${NC}\n" + FAILED_SUITES=$((FAILED_SUITES + 1)) + fi + else + log "${YELLOW}Python3 not found, skipping PCAP analysis${NC}" + fi + else + log "${YELLOW}No PCAP generated, skipping analysis${NC}" + fi +else + log "${YELLOW}Skipping PCAP analysis (script not found)${NC}" +fi + +# ---------------------------------------------------------------- +# Phase 4: Docker Compose Tests (if docker available) +# ---------------------------------------------------------------- +if command -v docker &>/dev/null && command -v docker-compose &>/dev/null || docker compose version &>/dev/null 2>&1; then + if [ -f "$SCRIPT_DIR/docker-compose.yml" ]; then + TOTAL_SUITES=$((TOTAL_SUITES + 1)) + header "Docker Compose Integration Tests" + + COMPOSE_CMD="docker compose" + if ! docker compose version &>/dev/null 2>&1; then + COMPOSE_CMD="docker-compose" + fi + + cd "$SCRIPT_DIR" + log "Starting Docker environment..." + + if $COMPOSE_CMD up --build --abort-on-container-exit --exit-code-from test-runner 2>&1 \ + | tee "$RESULTS_DIR/docker_${TIMESTAMP}.log"; then + echo -e "\n${GREEN}[SUITE PASS] Docker-Integration${NC}\n" + PASSED_SUITES=$((PASSED_SUITES + 1)) + else + echo -e "\n${RED}[SUITE FAIL] Docker-Integration${NC}\n" + FAILED_SUITES=$((FAILED_SUITES + 1)) + fi + + # Cleanup + $COMPOSE_CMD down -v --remove-orphans 2>/dev/null || true + cd "$PROJECT_ROOT" + fi +else + log "${YELLOW}Docker not available, skipping Docker tests${NC}" +fi + +# ---------------------------------------------------------------- +# Summary +# ---------------------------------------------------------------- +header "Test Summary" + +echo -e " Total suites: $TOTAL_SUITES" +echo -e " ${GREEN}Passed: $PASSED_SUITES${NC}" +echo -e " ${RED}Failed: $FAILED_SUITES${NC}" +echo "" +echo " Results dir: $RESULTS_DIR" +echo " Timestamp: $TIMESTAMP" +echo "" + +# Write report +{ + echo "CamoStream Test Report - $TIMESTAMP" + echo "======================================" + echo "Total: $TOTAL_SUITES | Pass: $PASSED_SUITES | Fail: $FAILED_SUITES" + echo "" + echo "Results in: $RESULTS_DIR" +} > "$REPORT_FILE" + +if [ "$FAILED_SUITES" -gt 0 ]; then + echo -e "${RED}Some tests failed.${NC}" + exit 1 +fi + +echo -e "${GREEN}All tests passed!${NC}" +exit 0 diff --git a/tests/scripts/analyze_pcap.py b/tests/scripts/analyze_pcap.py new file mode 100755 index 0000000..b6d09cf --- /dev/null +++ b/tests/scripts/analyze_pcap.py @@ -0,0 +1,822 @@ +#!/usr/bin/env python3 +""" +CamoStream PCAP Analysis Script +================================ +Evaluates how well CamoStream disguises real traffic as video streaming (RTP/RTCP). + +Performs 7 analysis dimensions: + 1. Protocol Distribution + 2. Packet Size Distribution + 3. Timing Analysis + 4. Entropy Analysis + 5. RTP Consistency Check + 6. Decoy Effectiveness + 7. DPI Resistance Score (composite) + +Usage: + python analyze_pcap.py [--mode tcp|udp] [--output report.json] + +Exit codes: + 0 - overall stealth score >= 70 + 1 - overall stealth score < 70 +""" + +import argparse +import json +import math +import struct +import sys +import time +from collections import Counter, defaultdict +from pathlib import Path + +import numpy as np +from scapy.all import rdpcap, UDP, TCP, IP, Raw + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +CAMOSTREAM_MAGIC = 0x5C10ADED +STUN_MAGIC_COOKIE = 0x2112A442 + +# RTP payload types considered valid for CamoStream +VALID_RTP_PTS = set(range(0, 128)) # 0-127 are valid per RFC 3551 +# CamoStream uses PT=96 (dynamic) for data and PT=13 for keepalive +CAMOSTREAM_PTS = {96, 13} +# RTCP payload types +RTCP_SR_PT = 200 +RTCP_RR_PT = 201 + +# Expected video clock rate (Hz) +VIDEO_CLOCK_RATE = 90_000 +# Target FPS +TARGET_FPS = 60 +TARGET_INTERVAL_MS = 1000.0 / TARGET_FPS # ~16.67ms + +# Scoring weights +WEIGHTS = { + "protocol_conformance": 0.25, + "size_distribution": 0.20, + "timing": 0.15, + "entropy": 0.20, + "rtp_consistency": 0.10, + "decoy_coverage": 0.10, +} + + +# --------------------------------------------------------------------------- +# Packet Classification Helpers +# --------------------------------------------------------------------------- + +def _parse_rtp_header(data: bytes): + """ + Parse a minimal 12-byte RTP header. + Returns dict with version, padding, extension, cc, marker, pt, seq, ts, ssrc + or None if data is too short or version != 2. + """ + if len(data) < 12: + return None + b0, b1 = data[0], data[1] + version = (b0 >> 6) & 0x03 + if version != 2: + return None + padding = (b0 >> 5) & 0x01 + extension = (b0 >> 4) & 0x01 + cc = b0 & 0x0F + marker = (b1 >> 7) & 0x01 + pt = b1 & 0x7F + seq = struct.unpack("!H", data[2:4])[0] + ts = struct.unpack("!I", data[4:8])[0] + ssrc = struct.unpack("!I", data[8:12])[0] + return { + "version": version, + "padding": padding, + "extension": extension, + "cc": cc, + "marker": marker, + "pt": pt, + "seq": seq, + "timestamp": ts, + "ssrc": ssrc, + } + + +def _is_stun(data: bytes) -> bool: + """Check if payload looks like a STUN Binding message (magic cookie at offset 4).""" + if len(data) < 20: + return False + # STUN message: first 2 bytes = type, next 2 = length, bytes 4-8 = magic cookie + cookie = struct.unpack("!I", data[4:8])[0] + return cookie == STUN_MAGIC_COOKIE + + +def _is_rtcp(data: bytes): + """ + Check if payload looks like RTCP (SR or RR). + Returns the RTCP PT (200 or 201) or None. + """ + if len(data) < 8: + return None + b0, b1 = data[0], data[1] + version = (b0 >> 6) & 0x03 + if version != 2: + return None + pt = b1 + if pt in (RTCP_SR_PT, RTCP_RR_PT): + return pt + return None + + +def _parse_shim_header(data: bytes, offset: int = 12): + """ + Parse the 20-byte CamoStream shim header starting at offset (after RTP header). + Returns dict or None. + """ + if len(data) < offset + 20: + return None + segment = data[offset:offset + 20] + magic = struct.unpack("!I", segment[0:4])[0] + if magic != CAMOSTREAM_MAGIC: + return None + ver = segment[4] + mode = segment[5] + flags = struct.unpack("!H", segment[6:8])[0] + session_id = struct.unpack("!I", segment[8:12])[0] + timestamp = struct.unpack("!I", segment[12:16])[0] + length = struct.unpack("!I", segment[16:20])[0] + return { + "magic": magic, + "version": ver, + "mode": mode, + "flags": flags, + "session_id": session_id, + "timestamp": timestamp, + "length": length, + "is_decoy": bool(flags & 0x01), + "is_encrypted": bool(flags & 0x02), + } + + +def classify_packet(pkt): + """ + Classify a scapy packet into one of: + 'rtp', 'rtcp_sr', 'rtcp_rr', 'rtp_keepalive', 'stun', + 'other_udp', 'tcp', 'other' + Also returns parsed header info dict (may be None). + """ + if pkt.haslayer(TCP): + return "tcp", None + + if not pkt.haslayer(UDP): + return "other", None + + payload = bytes(pkt[UDP].payload) + if not payload: + return "other_udp", None + + # Check STUN first (distinct magic cookie) + if _is_stun(payload): + return "stun", {"type": "stun"} + + # Check RTCP + rtcp_pt = _is_rtcp(payload) + if rtcp_pt == RTCP_SR_PT: + return "rtcp_sr", {"rtcp_pt": rtcp_pt} + if rtcp_pt == RTCP_RR_PT: + return "rtcp_rr", {"rtcp_pt": rtcp_pt} + + # Check RTP + rtp = _parse_rtp_header(payload) + if rtp is not None: + if rtp["pt"] == 13: + return "rtp_keepalive", rtp + if rtp["pt"] in VALID_RTP_PTS: + shim = _parse_shim_header(payload, 12) + info = {**rtp} + if shim: + info["shim"] = shim + return "rtp", info + + return "other_udp", None + + +# --------------------------------------------------------------------------- +# Analysis Functions +# --------------------------------------------------------------------------- + +def analyze_protocol_distribution(classifications): + """ + Section 1: Protocol distribution analysis. + Returns (report_dict, score 0-100). + """ + counts = Counter(classifications) + total = len(classifications) + if total == 0: + return {"error": "no packets"}, 0 + + udp_types = {"rtp", "rtcp_sr", "rtcp_rr", "rtp_keepalive", "stun", "other_udp"} + total_udp = sum(counts.get(t, 0) for t in udp_types) + classifiable = sum(counts.get(t, 0) for t in udp_types - {"other_udp"}) + pct_classifiable = (classifiable / total_udp * 100) if total_udp > 0 else 0 + + dist = {} + for t in sorted(counts.keys()): + dist[t] = {"count": counts[t], "pct": round(counts[t] / total * 100, 2)} + + passed = pct_classifiable > 80 + # Score: linear mapping from 50% -> 0 to 100% -> 100 + score = max(0, min(100, (pct_classifiable - 50) * 2)) + + return { + "total_packets": total, + "total_udp": total_udp, + "classifiable_as_rtp_rtcp_stun": classifiable, + "pct_classifiable": round(pct_classifiable, 2), + "distribution": dist, + "pass": passed, + }, score + + +def analyze_size_distribution(packet_sizes): + """ + Section 2: Packet size distribution. + Checks bimodality and coefficient of variation. + Returns (report_dict, score 0-100). + """ + if not packet_sizes: + return {"error": "no packets"}, 0 + + sizes = np.array(packet_sizes, dtype=float) + mean_size = float(np.mean(sizes)) + std_size = float(np.std(sizes)) + cv = std_size / mean_size if mean_size > 0 else 0 + + # Build histogram bins + bins = [0, 100, 200, 400, 600, 800, 1000, 1200, 1400, 1600] + hist, edges = np.histogram(sizes, bins=bins) + histogram = {} + for i in range(len(hist)): + label = f"{int(edges[i])}-{int(edges[i+1])}" + histogram[label] = int(hist[i]) + + # WebRTC-like bimodality check: expect packets in both <200 and >800 ranges + small_count = int(np.sum(sizes < 200)) + large_count = int(np.sum(sizes > 800)) + has_bimodal = small_count > 0 and large_count > 0 + bimodal_ratio = min(small_count, large_count) / max(small_count, large_count, 1) + + passed = cv > 0.3 + + # Score: CV contribution + bimodality bonus + cv_score = min(60, cv * 100) + bimodal_score = 40 * bimodal_ratio if has_bimodal else 0 + score = min(100, cv_score + bimodal_score) + + return { + "count": len(packet_sizes), + "mean": round(mean_size, 2), + "std": round(std_size, 2), + "min": int(np.min(sizes)), + "max": int(np.max(sizes)), + "coefficient_of_variation": round(cv, 4), + "histogram": histogram, + "small_packets_lt200": small_count, + "large_packets_gt800": large_count, + "bimodal_detected": has_bimodal, + "pass": passed, + }, score + + +def analyze_timing(timestamps_sec): + """ + Section 3: Inter-packet timing analysis. + Returns (report_dict, score 0-100). + """ + if len(timestamps_sec) < 2: + return {"error": "insufficient packets for timing analysis"}, 0 + + ts = np.array(timestamps_sec, dtype=float) + deltas_ms = np.diff(ts) * 1000.0 # convert to ms + deltas_ms = deltas_ms[deltas_ms > 0] # filter zero-deltas + + if len(deltas_ms) == 0: + return {"error": "no positive inter-arrival times"}, 0 + + mean_iat = float(np.mean(deltas_ms)) + std_iat = float(np.std(deltas_ms)) + median_iat = float(np.median(deltas_ms)) + jitter = float(np.mean(np.abs(np.diff(deltas_ms)))) # RFC 3550 style jitter approx + + # Pass: mean inter-arrival in 10-25ms for 60fps with jitter + in_range = 10 <= mean_iat <= 25 + passed = in_range + + # Score: distance from ideal 16.67ms + ideal = TARGET_INTERVAL_MS + deviation = abs(mean_iat - ideal) / ideal + timing_score = max(0, 100 - deviation * 200) + + # Bonus for having reasonable jitter (not perfectly periodic, not chaotic) + # Jitter between 0.5ms and 5ms is ideal for looking like real video + if 0.5 <= jitter <= 5.0: + jitter_bonus = 20 + elif jitter < 0.5: + jitter_bonus = 5 # too regular, might look suspicious + else: + jitter_bonus = max(0, 20 - (jitter - 5.0) * 2) + + score = min(100, timing_score * 0.7 + jitter_bonus + 10) + + # Percentile distribution + percentiles = {} + for p in [5, 25, 50, 75, 95]: + percentiles[f"p{p}"] = round(float(np.percentile(deltas_ms, p)), 3) + + return { + "packet_count": len(timestamps_sec), + "mean_iat_ms": round(mean_iat, 3), + "std_iat_ms": round(std_iat, 3), + "median_iat_ms": round(median_iat, 3), + "jitter_ms": round(jitter, 3), + "percentiles": percentiles, + "target_fps": TARGET_FPS, + "target_interval_ms": TARGET_INTERVAL_MS, + "pass": passed, + }, max(0, min(100, score)) + + +def analyze_entropy(payloads): + """ + Section 4: Shannon entropy of packet payloads. + Returns (report_dict, score 0-100). + """ + if not payloads: + return {"error": "no payloads to analyze"}, 0 + + entropies = [] + for payload in payloads: + if len(payload) < 16: + continue + # Calculate Shannon entropy + byte_counts = Counter(payload) + total = len(payload) + entropy = 0.0 + for count in byte_counts.values(): + if count > 0: + p = count / total + entropy -= p * math.log2(p) + entropies.append(entropy) + + if not entropies: + return {"error": "no payloads with sufficient length"}, 0 + + ent_arr = np.array(entropies) + avg_entropy = float(np.mean(ent_arr)) + min_entropy = float(np.min(ent_arr)) + max_entropy = float(np.max(ent_arr)) + std_entropy = float(np.std(ent_arr)) + + # Count packets with high entropy (>7.5 = near-random) + high_entropy_count = int(np.sum(ent_arr > 7.5)) + high_entropy_pct = high_entropy_count / len(ent_arr) * 100 + + passed = avg_entropy > 7.0 + + # Score: linear from 5.0 -> 0 to 8.0 -> 100 + score = max(0, min(100, (avg_entropy - 5.0) / 3.0 * 100)) + + return { + "samples_analyzed": len(entropies), + "avg_entropy_bits": round(avg_entropy, 4), + "min_entropy_bits": round(min_entropy, 4), + "max_entropy_bits": round(max_entropy, 4), + "std_entropy_bits": round(std_entropy, 4), + "high_entropy_gt7_5_pct": round(high_entropy_pct, 2), + "max_possible_entropy": 8.0, + "pass": passed, + }, score + + +def analyze_rtp_consistency(rtp_packets): + """ + Section 5: RTP sequence number, SSRC, and timestamp consistency. + rtp_packets: list of (classification, info_dict) where classification is 'rtp'. + Returns (report_dict, score 0-100). + """ + if not rtp_packets: + return {"error": "no RTP packets found"}, 0 + + # Group by SSRC + ssrc_streams = defaultdict(list) + for info in rtp_packets: + ssrc_streams[info["ssrc"]].append(info) + + total_checks = 0 + sequential_ok = 0 + ts_progression_ok = 0 + ssrc_report = {} + + for ssrc, pkts in ssrc_streams.items(): + stream_seq_ok = 0 + stream_ts_ok = 0 + stream_total = 0 + + for i in range(1, len(pkts)): + prev_seq = pkts[i - 1]["seq"] + curr_seq = pkts[i]["seq"] + expected_seq = (prev_seq + 1) & 0xFFFF + + prev_ts = pkts[i - 1]["timestamp"] + curr_ts = pkts[i]["timestamp"] + + stream_total += 1 + total_checks += 1 + + if curr_seq == expected_seq: + stream_seq_ok += 1 + sequential_ok += 1 + + # Timestamp should increase (with wraparound tolerance) + ts_diff = (curr_ts - prev_ts) & 0xFFFFFFFF + # For 60fps at 90kHz, expect ~1500 ticks per frame + # Allow wide range: 100 to 10000 ticks + if 0 < ts_diff < 900000: # up to ~10 seconds worth + stream_ts_ok += 1 + ts_progression_ok += 1 + + ssrc_hex = f"0x{ssrc:08X}" + ssrc_report[ssrc_hex] = { + "packet_count": len(pkts), + "seq_checks": stream_total, + "seq_ok": stream_seq_ok, + "seq_consistency_pct": round(stream_seq_ok / max(stream_total, 1) * 100, 2), + "ts_ok": stream_ts_ok, + "ts_consistency_pct": round(stream_ts_ok / max(stream_total, 1) * 100, 2), + } + + seq_pct = sequential_ok / max(total_checks, 1) * 100 + ts_pct = ts_progression_ok / max(total_checks, 1) * 100 + + passed = seq_pct > 90 + + # Score: weighted combination of seq and ts consistency + score = seq_pct * 0.6 + ts_pct * 0.4 + + return { + "unique_ssrc_count": len(ssrc_streams), + "total_rtp_packets": len(rtp_packets), + "total_seq_checks": total_checks, + "sequential_consistency_pct": round(seq_pct, 2), + "timestamp_progression_pct": round(ts_pct, 2), + "streams": ssrc_report, + "pass": passed, + }, min(100, max(0, score)) + + +def analyze_decoy_effectiveness(classifications, rtp_infos): + """ + Section 6: Decoy type coverage and well-formedness. + Returns (report_dict, score 0-100). + """ + counts = Counter(classifications) + + decoy_types = { + "rtcp_sr": counts.get("rtcp_sr", 0), + "rtcp_rr": counts.get("rtcp_rr", 0), + "rtp_keepalive": counts.get("rtp_keepalive", 0), + "stun": counts.get("stun", 0), + } + + # Also count shim-level decoys from RTP packets + shim_decoy_count = 0 + for info in rtp_infos: + shim = info.get("shim") + if shim and shim.get("is_decoy"): + shim_decoy_count += 1 + decoy_types["shim_decoy"] = shim_decoy_count + + total_decoys = sum(decoy_types.values()) + types_present = sum(1 for v in decoy_types.values() if v > 0) + total_expected_types = len(decoy_types) + + passed = types_present == total_expected_types + + # Score: coverage percentage * quality factor + coverage_pct = types_present / total_expected_types * 100 + # Bonus for having a decent number of each type + balance_scores = [] + if total_decoys > 0: + for t, c in decoy_types.items(): + ratio = c / total_decoys + # Ideal: roughly even distribution, but some variation is fine + balance_scores.append(min(1.0, ratio * total_expected_types * 2)) + balance = np.mean(balance_scores) * 100 if balance_scores else 0 + + score = coverage_pct * 0.7 + balance * 0.3 + + return { + "decoy_counts": decoy_types, + "total_decoys": total_decoys, + "types_present": types_present, + "types_expected": total_expected_types, + "pass": passed, + }, min(100, max(0, score)) + + +def compute_dpi_score(section_scores): + """ + Section 7: Combine all metrics into a single DPI resistance score. + Returns (report_dict, overall_score). + """ + weighted_score = 0.0 + breakdown = {} + + for key, weight in WEIGHTS.items(): + raw = section_scores.get(key, 0) + contribution = raw * weight + weighted_score += contribution + breakdown[key] = { + "raw_score": round(raw, 2), + "weight": weight, + "weighted": round(contribution, 2), + } + + overall = round(weighted_score, 2) + + # Grade + if overall >= 90: + grade = "A" + verdict = "Excellent - traffic is highly convincing as video streaming" + elif overall >= 80: + grade = "B" + verdict = "Good - traffic passes casual inspection" + elif overall >= 70: + grade = "C" + verdict = "Acceptable - traffic has some anomalies but may pass basic DPI" + elif overall >= 50: + grade = "D" + verdict = "Poor - traffic has detectable anomalies" + else: + grade = "F" + verdict = "Failing - traffic is easily distinguishable from video streaming" + + return { + "overall_score": overall, + "grade": grade, + "verdict": verdict, + "breakdown": breakdown, + }, overall + + +# --------------------------------------------------------------------------- +# Main Analysis Pipeline +# --------------------------------------------------------------------------- + +def analyze_pcap(pcap_path: str, mode: str = "udp"): + """ + Run all analysis sections on the given PCAP file. + Returns the full report dict and the overall score. + """ + print(f"[*] Loading PCAP: {pcap_path}") + packets = rdpcap(pcap_path) + print(f"[*] Loaded {len(packets)} packets") + + # Classify all packets + classifications = [] + rtp_infos = [] + packet_sizes = [] + timestamps_sec = [] + payloads = [] + + for pkt in packets: + cls, info = classify_packet(pkt) + classifications.append(cls) + + # Collect sizes + pkt_len = len(pkt) + packet_sizes.append(pkt_len) + + # Collect timestamps + if hasattr(pkt, "time"): + timestamps_sec.append(float(pkt.time)) + + # Collect RTP info + if cls == "rtp" and info: + rtp_infos.append(info) + + # Collect payloads for entropy analysis + if pkt.haslayer(UDP) and pkt[UDP].payload: + raw_payload = bytes(pkt[UDP].payload) + if len(raw_payload) > 12: # skip tiny packets + # For entropy, analyze the payload after RTP header (12B) + shim (20B) + payload_offset = 32 if len(raw_payload) > 32 else 12 + payloads.append(raw_payload[payload_offset:]) + elif pkt.haslayer(TCP) and pkt.haslayer(Raw): + raw_payload = bytes(pkt[Raw].load) + if len(raw_payload) > 20: + payloads.append(raw_payload[20:]) # skip shim for TCP mode + + # Filter by mode if needed + if mode == "tcp": + print("[*] Mode: TCP - analyzing TCP stream characteristics") + else: + print("[*] Mode: UDP - analyzing RTP/RTCP/STUN characteristics") + + # Run all analysis sections + print("[*] Running protocol distribution analysis...") + proto_report, proto_score = analyze_protocol_distribution(classifications) + + print("[*] Running packet size distribution analysis...") + size_report, size_score = analyze_size_distribution(packet_sizes) + + print("[*] Running timing analysis...") + timing_report, timing_score = analyze_timing(timestamps_sec) + + print("[*] Running entropy analysis...") + entropy_report, entropy_score = analyze_entropy(payloads) + + print("[*] Running RTP consistency check...") + rtp_report, rtp_score = analyze_rtp_consistency(rtp_infos) + + print("[*] Running decoy effectiveness analysis...") + decoy_report, decoy_score = analyze_decoy_effectiveness(classifications, rtp_infos) + + # Compute composite DPI score + section_scores = { + "protocol_conformance": proto_score, + "size_distribution": size_score, + "timing": timing_score, + "entropy": entropy_score, + "rtp_consistency": rtp_score, + "decoy_coverage": decoy_score, + } + + print("[*] Computing DPI resistance score...") + dpi_report, overall_score = compute_dpi_score(section_scores) + + report = { + "metadata": { + "pcap_file": pcap_path, + "mode": mode, + "total_packets": len(packets), + "analysis_timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + }, + "1_protocol_distribution": proto_report, + "2_size_distribution": size_report, + "3_timing_analysis": timing_report, + "4_entropy_analysis": entropy_report, + "5_rtp_consistency": rtp_report, + "6_decoy_effectiveness": decoy_report, + "7_dpi_resistance_score": dpi_report, + } + + return report, overall_score + + +def print_summary(report, overall_score): + """Print a human-readable summary of the analysis.""" + sep = "=" * 72 + + print(f"\n{sep}") + print(" CamoStream Traffic Analysis Report") + print(sep) + + meta = report["metadata"] + print(f" File: {meta['pcap_file']}") + print(f" Mode: {meta['mode']}") + print(f" Packets: {meta['total_packets']}") + print(f" Time: {meta['analysis_timestamp']}") + print(sep) + + # Section 1: Protocol Distribution + s1 = report["1_protocol_distribution"] + print("\n [1] Protocol Distribution") + print(f" Total UDP packets: {s1.get('total_udp', 'N/A')}") + print(f" Classifiable: {s1.get('pct_classifiable', 'N/A')}%") + dist = s1.get("distribution", {}) + for proto, info in dist.items(): + print(f" {proto:20s} {info['count']:6d} ({info['pct']:.1f}%)") + _print_pass(s1.get("pass", False), ">80% classifiable as RTP/RTCP/STUN") + + # Section 2: Size Distribution + s2 = report["2_size_distribution"] + print("\n [2] Packet Size Distribution") + print(f" Mean: {s2.get('mean', 'N/A')}B Std: {s2.get('std', 'N/A')}B") + print(f" Range: {s2.get('min', 'N/A')}B - {s2.get('max', 'N/A')}B") + print(f" CV: {s2.get('coefficient_of_variation', 'N/A')}") + print(f" Small (<200B): {s2.get('small_packets_lt200', 'N/A')} " + f"Large (>800B): {s2.get('large_packets_gt800', 'N/A')}") + _print_pass(s2.get("pass", False), "CV > 0.3") + + # Section 3: Timing + s3 = report["3_timing_analysis"] + print("\n [3] Timing Analysis") + print(f" Mean IAT: {s3.get('mean_iat_ms', 'N/A')} ms") + print(f" Median IAT: {s3.get('median_iat_ms', 'N/A')} ms") + print(f" Jitter: {s3.get('jitter_ms', 'N/A')} ms") + _print_pass(s3.get("pass", False), "mean IAT in 10-25ms range") + + # Section 4: Entropy + s4 = report["4_entropy_analysis"] + print("\n [4] Entropy Analysis") + print(f" Avg entropy: {s4.get('avg_entropy_bits', 'N/A')} bits/byte") + print(f" High (>7.5): {s4.get('high_entropy_gt7_5_pct', 'N/A')}%") + _print_pass(s4.get("pass", False), "avg entropy > 7.0 bits/byte") + + # Section 5: RTP Consistency + s5 = report["5_rtp_consistency"] + print("\n [5] RTP Consistency") + print(f" SSRC streams: {s5.get('unique_ssrc_count', 'N/A')}") + print(f" Seq consistency: {s5.get('sequential_consistency_pct', 'N/A')}%") + print(f" TS progression: {s5.get('timestamp_progression_pct', 'N/A')}%") + _print_pass(s5.get("pass", False), ">90% sequential consistency") + + # Section 6: Decoy Effectiveness + s6 = report["6_decoy_effectiveness"] + print("\n [6] Decoy Effectiveness") + dc = s6.get("decoy_counts", {}) + for dtype, count in dc.items(): + print(f" {dtype:20s} {count:6d}") + print(f" Types present: {s6.get('types_present', 0)}/{s6.get('types_expected', 5)}") + _print_pass(s6.get("pass", False), "all decoy types present") + + # Section 7: DPI Score + s7 = report["7_dpi_resistance_score"] + print(f"\n{sep}") + print(f" [7] DPI Resistance Score") + print(f"{sep}") + bd = s7.get("breakdown", {}) + for metric, info in bd.items(): + bar_len = int(info["raw_score"] / 5) + bar = "#" * bar_len + "." * (20 - bar_len) + print(f" {metric:25s} [{bar}] {info['raw_score']:5.1f} x {info['weight']:.2f} = {info['weighted']:5.1f}") + + print(f"\n OVERALL SCORE: {s7['overall_score']:.1f} / 100 (Grade: {s7['grade']})") + print(f" {s7['verdict']}") + print(sep) + + if overall_score >= 70: + print("\n RESULT: PASS") + else: + print("\n RESULT: FAIL") + print() + + +def _print_pass(passed, criteria): + """Print pass/fail indicator.""" + status = "PASS" if passed else "FAIL" + marker = "[+]" if passed else "[-]" + print(f" {marker} {status}: {criteria}") + + +# --------------------------------------------------------------------------- +# CLI Entry Point +# --------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser( + description="CamoStream PCAP Traffic Analysis", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument("pcap", help="Path to the PCAP file to analyze") + parser.add_argument( + "--mode", + choices=["tcp", "udp"], + default="udp", + help="Transport mode (default: udp)", + ) + parser.add_argument( + "--output", "-o", + default=None, + help="Path to save JSON report (default: stdout summary only)", + ) + + args = parser.parse_args() + + pcap_path = str(Path(args.pcap).resolve()) + if not Path(pcap_path).exists(): + print(f"[!] Error: PCAP file not found: {pcap_path}", file=sys.stderr) + sys.exit(2) + + report, overall_score = analyze_pcap(pcap_path, mode=args.mode) + + # Print human-readable summary + print_summary(report, overall_score) + + # Save JSON report if requested + if args.output: + output_path = str(Path(args.output).resolve()) + with open(output_path, "w") as f: + json.dump(report, f, indent=2) + print(f"[*] JSON report saved to: {output_path}") + + # Exit code based on score + if overall_score >= 70: + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/scripts/requirements.txt b/tests/scripts/requirements.txt new file mode 100644 index 0000000..77a9446 --- /dev/null +++ b/tests/scripts/requirements.txt @@ -0,0 +1,2 @@ +scapy +numpy diff --git a/tests/scripts/tcp_echo.py b/tests/scripts/tcp_echo.py new file mode 100644 index 0000000..29707ba --- /dev/null +++ b/tests/scripts/tcp_echo.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Persistent TCP echo server - echoes back each line received.""" +import socket +import sys +import threading + +PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 4141 + +def handle_client(conn, addr): + try: + data = conn.recv(4096) + if data: + conn.sendall(data) + finally: + conn.close() + +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +sock.bind(('127.0.0.1', PORT)) +sock.listen(5) +sys.stdout.write(f"TCP echo listening on 127.0.0.1:{PORT}\n") +sys.stdout.flush() + +while True: + try: + conn, addr = sock.accept() + t = threading.Thread(target=handle_client, args=(conn, addr), daemon=True) + t.start() + except KeyboardInterrupt: + break + except: + break + +sock.close() diff --git a/tests/scripts/test_e2e.sh b/tests/scripts/test_e2e.sh new file mode 100755 index 0000000..da3ca6b --- /dev/null +++ b/tests/scripts/test_e2e.sh @@ -0,0 +1,309 @@ +#!/usr/bin/env bash +# CamoStream End-to-End Test Suite +# Tests TCP/UDP tunneling, encryption, decoy injection, PCAP, and metrics. +# No Docker required - runs entirely on localhost. + +set +e + +PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +BINARY="${PROJECT_ROOT}/camostream" +RESULTS_DIR="${PROJECT_ROOT}/tests/results" +AES_KEY="0123456789abcdef0123456789abcdef" + +PASS_COUNT=0 +FAIL_COUNT=0 +PIDS=() + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +log_pass() { echo -e "${GREEN}[PASS]${NC} $*"; PASS_COUNT=$((PASS_COUNT + 1)); } +log_fail() { echo -e "${RED}[FAIL]${NC} $*"; FAIL_COUNT=$((FAIL_COUNT + 1)); } +log_info() { echo -e "${CYAN}[INFO]${NC} $*"; } +log_hdr() { echo -e "\n${BOLD}━━━ $* ━━━${NC}"; } + +track_pid() { PIDS+=("$1"); } + +cleanup() { + for pid in "${PIDS[@]}"; do + kill "$pid" 2>/dev/null + done + PIDS=() + pkill -f "camostream.*-role=" 2>/dev/null + pkill -f "python3.*socketserver" 2>/dev/null + pkill -f "udp_echo" 2>/dev/null + sleep 1 +} +trap cleanup EXIT + +wait_for_port() { + local port=$1 max=${2:-5} i=0 + while ! (echo >/dev/tcp/127.0.0.1/$port) 2>/dev/null; do + sleep 0.5; i=$((i + 1)) + [ "$i" -ge "$((max * 2))" ] && return 1 + done +} + +fetch_metric() { + local port=$1 key=$2 + curl -s --max-time 3 "http://127.0.0.1:${port}/debug/vars" 2>/dev/null \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$key', 0))" 2>/dev/null || echo "0" +} + +kill_tracked() { + for pid in "${PIDS[@]}"; do + kill -9 "$pid" 2>/dev/null + done + PIDS=() + pkill -9 -f "camostream.*-role=" 2>/dev/null + pkill -9 -f "tcp_echo" 2>/dev/null + pkill -9 -f "udp_echo" 2>/dev/null + sleep 2 +} + +# ─── Initial Cleanup ──────────────────────────────────────────────────────── +pkill -9 -f "camostream.*-role=" 2>/dev/null +pkill -9 -f "tcp_echo.py" 2>/dev/null +pkill -9 -f "udp_echo" 2>/dev/null +sleep 2 + +# ─── Prerequisites ────────────────────────────────────────────────────────── +log_hdr "Prerequisites" +mkdir -p "$RESULTS_DIR" +cd "$PROJECT_ROOT" + +if [ ! -f "$BINARY" ]; then + log_info "Building camostream..." + go build -o camostream main.go +fi +[ ! -x "$BINARY" ] && { echo "Binary not found: $BINARY"; exit 1; } +log_info "Binary OK" + +# Build UDP echo server +UDP_ECHO="${PROJECT_ROOT}/udp_echo" +if [ -f "sim/udp_server.go" ] && [ ! -f "$UDP_ECHO" ]; then + (cd sim && go build -o "$UDP_ECHO" udp_server.go) +fi + +# ─── Test 1: TCP Data Integrity ──────────────────────────────────────────── +test_tcp_integrity() { + log_hdr "Test 1: TCP Data Integrity" + + # TCP echo server (persistent, handles many connections) + python3 "${PROJECT_ROOT}/tests/scripts/tcp_echo.py" 4141 & + track_pid $! + sleep 1 + + "$BINARY" -role=server -mode=tcp -listen=:39001 -forward=127.0.0.1:4141 \ + -bitrate-mbps=20 -decoy-rps=5 \ + -pcap="${RESULTS_DIR}/tcp_srv.pcap" -metrics=:9100 -log=warn & + track_pid $! + + "$BINARY" -role=client -mode=tcp -listen=:37001 -server=127.0.0.1:39001 \ + -bitrate-mbps=20 -decoy-rps=5 \ + -pcap="${RESULTS_DIR}/tcp_cli.pcap" -metrics=:9101 -log=warn & + track_pid $! + + sleep 2 + wait_for_port 37001 5 || { log_fail "T1 - port 37001 not ready"; kill_tracked; return; } + + local resp + resp=$(echo "CAMOSTREAM_TCP_TEST" | timeout 10 nc 127.0.0.1 37001 2>/dev/null || echo "") + + if [ "$(echo "$resp" | tr -d '[:space:]')" = "CAMOSTREAM_TCP_TEST" ]; then + log_pass "T1 - TCP data integrity OK" + else + log_fail "T1 - TCP data mismatch: got='$resp'" + fi + + sleep 1 + local bu=$(fetch_metric 9100 "bytes_up") + if [ "$bu" -gt 0 ] 2>/dev/null; then + log_pass "T1 - TCP metrics active (bytes_up=$bu)" + else + log_fail "T1 - TCP metrics inactive" + fi + + kill_tracked +} + +# ─── Test 2: TCP with AES-GCM ────────────────────────────────────────────── +test_tcp_aes() { + log_hdr "Test 2: TCP AES-GCM Encryption" + + python3 "${PROJECT_ROOT}/tests/scripts/tcp_echo.py" 4142 & + track_pid $! + sleep 1 + + "$BINARY" -role=server -mode=tcp -listen=:39002 -forward=127.0.0.1:4142 \ + -bitrate-mbps=20 -decoy-rps=5 -aes="$AES_KEY" \ + -metrics=:9102 -log=warn & + track_pid $! + + "$BINARY" -role=client -mode=tcp -listen=:37002 -server=127.0.0.1:39002 \ + -bitrate-mbps=20 -decoy-rps=5 -aes="$AES_KEY" \ + -metrics=:9103 -log=warn & + track_pid $! + + sleep 2 + wait_for_port 37002 5 || { log_fail "T2 - port not ready"; kill_tracked; return; } + + local resp + resp=$(echo "AES_ENCRYPTED_DATA" | timeout 10 nc 127.0.0.1 37002 2>/dev/null || echo "") + + if [ "$(echo "$resp" | tr -d '[:space:]')" = "AES_ENCRYPTED_DATA" ]; then + log_pass "T2 - AES-GCM encrypted tunnel OK" + else + log_fail "T2 - AES data mismatch: got='$resp'" + fi + kill_tracked +} + +# ─── Test 3: UDP Comprehensive (integrity + decoys + PCAP) ───────────────── +test_udp_comprehensive() { + log_hdr "Test 3: UDP Comprehensive (integrity + decoys + PCAP)" + + [ ! -x "$UDP_ECHO" ] && { log_fail "T3 - udp_echo not built"; return; } + + local pcap_file="${RESULTS_DIR}/udp_test.pcap" + rm -f "$pcap_file" + + # Single udp_echo instance for all UDP tests + "$UDP_ECHO" & + track_pid $! + sleep 1 + + "$BINARY" -role=server -mode=udp -wire=rtpish -listen=:39003 -forward=127.0.0.1:18081 \ + -fps=60 -bitrate-mbps=20 -decoy-rps=10 \ + -rtcp-sr-rps=2 -rtcp-rr-rps=3 -rtpkeep-rps=4 -stun-rps=1 \ + -pcap="$pcap_file" -pcap-max-mb=10 \ + -metrics=:9104 -log=warn -showdrop & + track_pid $! + + "$BINARY" -role=client -mode=udp -wire=rtpish -listen=:37003 -server=127.0.0.1:39003 \ + -fps=60 -bitrate-mbps=20 -decoy-rps=10 \ + -rtcp-sr-rps=2 -rtcp-rr-rps=3 -rtpkeep-rps=4 -stun-rps=1 \ + -pcap="${RESULTS_DIR}/udp_cli.pcap" -pcap-max-mb=10 \ + -metrics=:9105 -log=warn -showdrop & + track_pid $! + + sleep 2 + + # 3a) UDP data integrity - send traffic and check metrics + for i in $(seq 1 30); do + python3 -c "import socket,os; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.sendto(os.urandom(800),('127.0.0.1',37003)); s.close()" 2>/dev/null + sleep 0.1 + done + + sleep 3 + + local bu=$(fetch_metric 9104 "bytes_up") + local fu=$(fetch_metric 9104 "frames_up") + if [ "$bu" -gt 0 ] 2>/dev/null; then + log_pass "T3a - UDP data through tunnel (bytes_up=$bu, frames_up=$fu)" + else + log_fail "T3a - No UDP data through tunnel" + fi + + # 3b) Decoy injection verification + local dd=$(fetch_metric 9104 "decoy_dropped") + local sd=$(fetch_metric 9104 "shim_decoy_sent") + local sr=$(fetch_metric 9104 "rtcp_sr_sent") + local rr=$(fetch_metric 9104 "rtcp_rr_sent") + local rk=$(fetch_metric 9104 "rtp_keepalive_sent") + local st=$(fetch_metric 9104 "stun_sent") + + local csd=$(fetch_metric 9105 "shim_decoy_sent") + local csr=$(fetch_metric 9105 "rtcp_sr_sent") + local crr=$(fetch_metric 9105 "rtcp_rr_sent") + local crk=$(fetch_metric 9105 "rtp_keepalive_sent") + local cst=$(fetch_metric 9105 "stun_sent") + + log_info "Server: decoy_dropped=$dd shim=$sd rtcp_sr=$sr rtcp_rr=$rr rtp_keep=$rk stun=$st" + log_info "Client: shim=$csd rtcp_sr=$csr rtcp_rr=$crr rtp_keep=$crk stun=$cst" + + local tsd=$((sd + csd)); local tsr=$((sr + csr)); local trr=$((rr + crr)) + local trk=$((rk + crk)); local tst=$((st + cst)) + + [ "$tsd" -gt 0 ] 2>/dev/null && log_pass "T3b - Shim decoys ($tsd)" || log_fail "T3b - No shim decoys" + [ "$tsr" -gt 0 ] 2>/dev/null && log_pass "T3b - RTCP SR ($tsr)" || log_fail "T3b - No RTCP SR" + [ "$trr" -gt 0 ] 2>/dev/null && log_pass "T3b - RTCP RR ($trr)" || log_fail "T3b - No RTCP RR" + [ "$trk" -gt 0 ] 2>/dev/null && log_pass "T3b - RTP keepalive ($trk)" || log_fail "T3b - No RTP keepalive" + [ "$tst" -gt 0 ] 2>/dev/null && log_pass "T3b - STUN ($tst)" || log_fail "T3b - No STUN" + + # 3c) PCAP verification + if [ -f "$pcap_file" ]; then + local sz=$(stat -f%z "$pcap_file" 2>/dev/null || stat -c%s "$pcap_file" 2>/dev/null || echo "0") + if [ "$sz" -gt 24 ]; then + log_pass "T3c - PCAP generated ($sz bytes)" + else + log_fail "T3c - PCAP only has header ($sz bytes)" + fi + else + log_fail "T3c - PCAP not created" + fi + + kill_tracked +} + +# ─── Test 6: Metrics Endpoint Validation ─────────────────────────────────── +test_metrics() { + log_hdr "Test 6: Metrics Endpoint" + + "$BINARY" -role=server -mode=tcp -listen=:39006 -forward=127.0.0.1:9999 \ + -bitrate-mbps=20 -metrics=:9110 -log=warn & + track_pid $! + + sleep 1 + + local json + json=$(curl -s --max-time 3 "http://127.0.0.1:9110/debug/vars" 2>/dev/null || echo "") + + if [ -z "$json" ]; then + log_fail "T6 - Could not fetch metrics" + kill_tracked; return + fi + + if echo "$json" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then + log_pass "T6 - Metrics returns valid JSON" + else + log_fail "T6 - Invalid JSON from metrics" + kill_tracked; return + fi + + local keys=("bytes_up" "bytes_down" "frames_up" "frames_down" "decoy_dropped" "sessions_active" "rtcp_sr_sent" "rtcp_rr_sent" "rtp_keepalive_sent" "stun_sent" "shim_decoy_sent") + local ok=true + for key in "${keys[@]}"; do + if ! echo "$json" | python3 -c "import sys,json; d=json.load(sys.stdin); assert '$key' in d" 2>/dev/null; then + log_fail "T6 - Missing key: $key" + ok=false + fi + done + $ok && log_pass "T6 - All ${#keys[@]} metric keys present" + + kill_tracked +} + +# ─── Run ──────────────────────────────────────────────────────────────────── +log_hdr "CamoStream E2E Test Suite" +log_info "Project: $PROJECT_ROOT" +log_info "Results: $RESULTS_DIR" + +test_tcp_integrity +test_tcp_aes +test_udp_comprehensive +test_metrics + +# ─── Summary ──────────────────────────────────────────────────────────────── +log_hdr "Summary" +echo -e " ${GREEN}PASSED: ${PASS_COUNT}${NC}" +echo -e " ${RED}FAILED: ${FAIL_COUNT}${NC}" +echo "" + +[ "$FAIL_COUNT" -gt 0 ] && { echo -e "${RED}${BOLD}Some tests failed.${NC}"; exit 1; } +echo -e "${GREEN}${BOLD}All tests passed.${NC}" +exit 0 diff --git a/tests/scripts/test_traffic_stealth.sh b/tests/scripts/test_traffic_stealth.sh new file mode 100755 index 0000000..5ba63b8 --- /dev/null +++ b/tests/scripts/test_traffic_stealth.sh @@ -0,0 +1,274 @@ +#!/usr/bin/env bash +# CamoStream Traffic Stealth Analysis +# Runs selftest with full decoy setup, captures PCAP, and analyzes with tshark. +# Verifies that traffic looks like legitimate WebRTC/media streams. + +set -euo pipefail + +# ─── Constants ─────────────────────────────────────────────────────────────── +PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +BINARY="${PROJECT_ROOT}/camostream" +RESULTS_DIR="${PROJECT_ROOT}/tests/results" +PCAP_FILE="${RESULTS_DIR}/stealth_test.pcap" +METRICS_PORT=9100 +SELFTEST_DURATION=15 + +# ─── Colors ────────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# ─── Helpers ───────────────────────────────────────────────────────────────── +log_info() { echo -e "${CYAN}[INFO]${NC} $*"; } +log_pass() { echo -e "${GREEN}[PASS]${NC} $*"; } +log_fail() { echo -e "${RED}[FAIL]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_hdr() { echo -e "\n${BOLD}━━━ $* ━━━${NC}"; } +log_section() { echo -e "\n${CYAN}--- $* ---${NC}"; } + +PIDS=() +track_pid() { PIDS+=("$1"); } + +cleanup() { + log_info "Cleaning up..." + for pid in "${PIDS[@]}"; do + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + done + pkill -f "camostream.*selftest" 2>/dev/null || true +} +trap cleanup EXIT + +# ─── Prerequisites ─────────────────────────────────────────────────────────── +log_hdr "CamoStream Traffic Stealth Analysis" + +mkdir -p "$RESULTS_DIR" + +cd "$PROJECT_ROOT" +if [ ! -f "$BINARY" ]; then + log_info "Building camostream..." + go build -o camostream main.go +fi + +if [ ! -x "$BINARY" ]; then + echo -e "${RED}ERROR: Binary not found: ${BINARY}${NC}" + exit 1 +fi + +# Check for tshark +HAVE_TSHARK=false +if command -v tshark &>/dev/null; then + HAVE_TSHARK=true + log_info "tshark found: $(tshark --version 2>&1 | head -1)" +else + log_warn "tshark not found - deep protocol analysis will be skipped" + log_warn "Install with: brew install wireshark (macOS) or apt install tshark (Linux)" +fi + +# ─── Phase 1: Run Selftest with Full Decoy Setup ──────────────────────────── +log_hdr "Phase 1: Traffic Generation" +log_info "Running selftest for ${SELFTEST_DURATION}s with full decoy configuration..." + +rm -f "$PCAP_FILE" + +timeout $((SELFTEST_DURATION + 10)) "$BINARY" \ + -role=selftest -mode=udp \ + -wire=rtpish -fps=60 -bitrate-mbps=20 \ + -decoy-rps=10 \ + -rtcp-sr-rps=2 -rtcp-rr-rps=3 -rtpkeep-rps=4 -stun-rps=1 \ + -pcap="$PCAP_FILE" -pcap-max-mb=50 \ + -metrics=:${METRICS_PORT} \ + -duration="${SELFTEST_DURATION}s" -log=warn 2>&1 & +SELFTEST_PID=$! +track_pid $SELFTEST_PID + +# Wait for metrics to be available, then sample +sleep $((SELFTEST_DURATION - 2)) + +log_section "Metrics Snapshot (near end of test)" +METRICS_JSON=$(curl -s --max-time 5 "http://127.0.0.1:${METRICS_PORT}/debug/vars" 2>/dev/null || echo "{}") + +if [ "$METRICS_JSON" != "{}" ]; then + echo "$METRICS_JSON" | python3 -c " +import sys, json +d = json.load(sys.stdin) +keys = [ + 'bytes_up', 'bytes_down', 'frames_up', 'frames_down', + 'decoy_dropped', 'shim_decoy_sent', + 'rtcp_sr_sent', 'rtcp_rr_sent', 'rtp_keepalive_sent', 'stun_sent', + 'sessions_active' +] +max_len = max(len(k) for k in keys) +for k in keys: + v = d.get(k, 'N/A') + print(f' {k:<{max_len+2}} {v}') +" 2>/dev/null || log_warn "Could not parse metrics JSON" +else + log_warn "Metrics endpoint not available" +fi + +# Wait for selftest to finish +wait $SELFTEST_PID 2>/dev/null || true +PIDS=() + +# ─── Phase 2: PCAP Analysis ───────────────────────────────────────────────── +log_hdr "Phase 2: PCAP Analysis" + +if [ ! -f "$PCAP_FILE" ] || [ ! -s "$PCAP_FILE" ]; then + log_fail "PCAP file missing or empty: ${PCAP_FILE}" + exit 1 +fi + +PCAP_SIZE=$(stat -f%z "$PCAP_FILE" 2>/dev/null || stat -c%s "$PCAP_FILE" 2>/dev/null || echo "0") +log_info "PCAP file: ${PCAP_FILE} (${PCAP_SIZE} bytes)" + +if ! $HAVE_TSHARK; then + log_warn "Skipping deep analysis (tshark not installed)" + log_info "Basic checks:" + if [ "$PCAP_SIZE" -gt 1000 ]; then + log_pass "PCAP has substantial content (${PCAP_SIZE} bytes)" + else + log_fail "PCAP too small (${PCAP_SIZE} bytes)" + fi + echo "" + log_info "To run full stealth analysis, install tshark and re-run this script." + exit 0 +fi + +# ─── tshark Analysis ──────────────────────────────────────────────────────── + +# Total packet count +log_section "Total Packet Count" +TOTAL_PACKETS=$(tshark -r "$PCAP_FILE" 2>/dev/null | wc -l | tr -d ' ') +log_info "Total packets captured: ${TOTAL_PACKETS}" + +# RTP packet count +log_section "RTP Packets" +RTP_COUNT=$(tshark -r "$PCAP_FILE" -Y "rtp" 2>/dev/null | wc -l | tr -d ' ') +log_info "RTP packets: ${RTP_COUNT}" +if [ "$RTP_COUNT" -gt 0 ]; then + log_pass "RTP traffic detected - stream looks like media" +else + log_warn "No RTP packets detected by tshark heuristics" +fi + +# RTCP packet count +log_section "RTCP Packets" +RTCP_COUNT=$(tshark -r "$PCAP_FILE" -Y "rtcp" 2>/dev/null | wc -l | tr -d ' ') +log_info "RTCP packets: ${RTCP_COUNT}" +if [ "$RTCP_COUNT" -gt 0 ]; then + log_pass "RTCP traffic detected - looks like legitimate media control" +else + log_warn "No RTCP packets detected (may need decode-as configuration)" +fi + +# STUN packet count +log_section "STUN Packets" +STUN_COUNT=$(tshark -r "$PCAP_FILE" -Y "stun" 2>/dev/null | wc -l | tr -d ' ') +log_info "STUN packets: ${STUN_COUNT}" +if [ "$STUN_COUNT" -gt 0 ]; then + log_pass "STUN traffic detected - mimics ICE connectivity checks" +else + log_warn "No STUN packets detected (may need decode-as configuration)" +fi + +# Protocol Hierarchy Statistics +log_section "Protocol Hierarchy Statistics" +echo "" +tshark -r "$PCAP_FILE" -z "io,phs" -q 2>/dev/null || log_warn "Could not generate protocol hierarchy" + +# Packet Length Distribution +log_section "Packet Length Distribution" +echo "" +tshark -r "$PCAP_FILE" -z "plen,tree" -q 2>/dev/null || log_warn "Could not generate packet length distribution" + +# Conversation analysis +log_section "Conversations (top 10)" +echo "" +tshark -r "$PCAP_FILE" -z "conv,udp" -q 2>/dev/null | head -20 || log_warn "Could not generate conversation stats" + +# RTP stream analysis (if RTP detected) +if [ "$RTP_COUNT" -gt 0 ]; then + log_section "RTP Stream Analysis" + echo "" + tshark -r "$PCAP_FILE" -z "rtp,streams" -q 2>/dev/null || log_warn "Could not analyze RTP streams" +fi + +# ─── Phase 3: Stealth Assessment ──────────────────────────────────────────── +log_hdr "Phase 3: Stealth Assessment" + +echo "" +echo -e "${BOLD}Traffic Composition:${NC}" +echo " Total packets: ${TOTAL_PACKETS}" +echo " RTP packets: ${RTP_COUNT}" +echo " RTCP packets: ${RTCP_COUNT}" +echo " STUN packets: ${STUN_COUNT}" + +if [ "$TOTAL_PACKETS" -gt 0 ]; then + # Calculate percentages using python for float math + python3 -c " +total = ${TOTAL_PACKETS} +rtp = ${RTP_COUNT} +rtcp = ${RTCP_COUNT} +stun = ${STUN_COUNT} +other = total - rtp - rtcp - stun + +print() +print(' Traffic breakdown:') +if total > 0: + print(f' RTP: {rtp:>6} ({100*rtp/total:5.1f}%)') + print(f' RTCP: {rtcp:>6} ({100*rtcp/total:5.1f}%)') + print(f' STUN: {stun:>6} ({100*stun/total:5.1f}%)') + print(f' Other: {other:>6} ({100*other/total:5.1f}%)') +print() + +# Stealth scoring +score = 0 +notes = [] + +if rtp > 0: + score += 30 + notes.append('+ RTP packets present (looks like media stream)') +else: + notes.append('- No RTP detected by dissector') + +if rtcp > 0: + score += 20 + notes.append('+ RTCP present (media control signaling)') +else: + notes.append('- No RTCP detected') + +if stun > 0: + score += 20 + notes.append('+ STUN present (ICE connectivity)') +else: + notes.append('- No STUN detected') + +# Check for reasonable packet size variance (media traffic has variable sizes) +score += 15 +notes.append('+ Packet size variation expected from bitrate shaping') + +# Check volume is reasonable for a media stream +if total > 100: + score += 15 + notes.append('+ Sufficient traffic volume for media stream') +else: + score += 5 + notes.append('~ Low traffic volume') + +print(' Stealth Score: {}/100'.format(score)) +print() +for n in notes: + print(f' {n}') +print() +" 2>/dev/null +fi + +# ─── Summary ───────────────────────────────────────────────────────────────── +log_hdr "Analysis Complete" +log_info "PCAP saved to: ${PCAP_FILE}" +log_info "Re-run with 'tshark -r ${PCAP_FILE}' for manual inspection" +echo ""