add comprehensive testing system with E2E tests and DPI analysis

- E2E test suite: TCP/UDP data integrity, AES-GCM, decoy injection, metrics
- PCAP deep analysis: 7-dimension DPI resistance scoring (protocol, size, timing, entropy, RTP consistency, decoy coverage)
- Docker Compose multi-container test environment
- Traffic stealth analysis with tshark
- Verified: 12/12 E2E tests pass, DPI score 81.9/100 with AES-GCM
This commit is contained in:
uk0
2026-04-08 15:30:20 +08:00
parent 94981e71ea
commit 8852bb7976
12 changed files with 2214 additions and 1 deletions
+12 -1
View File
@@ -33,5 +33,16 @@ go.work.sum
# Ignore everything # Ignore everything
# But not these files... # But not these files...
!/.gitignore !/.gitignore
.idea
.DS_Store
.env
docs/
*.docx
*.doc
*.xlsx
*.csv
userdata/
session/
tests/results/
tests/**/*.pcap
+43
View File
@@ -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
```
+38
View File
@@ -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"]
+16
View File
@@ -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"]
+95
View File
@@ -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()
+372
View File
@@ -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
+197
View File
@@ -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
+822
View File
@@ -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 <pcap_file> [--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()
+2
View File
@@ -0,0 +1,2 @@
scapy
numpy
+34
View File
@@ -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()
+309
View File
@@ -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
+274
View File
@@ -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 ""