mirror of
https://github.com/uk0/camostream.git
synced 2026-08-29 08:49:44 +00:00
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:
+12
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Executable
+197
@@ -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
|
||||
Executable
+822
@@ -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()
|
||||
@@ -0,0 +1,2 @@
|
||||
scapy
|
||||
numpy
|
||||
@@ -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()
|
||||
Executable
+309
@@ -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
|
||||
Executable
+274
@@ -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 ""
|
||||
Reference in New Issue
Block a user