mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Implement Tor transport with operator visibility
Add TorTransport in src/transport/tor/ supporting three operating modes: Outbound (socks5 mode): - Non-blocking SOCKS5 connect via tokio-socks with per-destination circuit isolation (IsolateSOCKSAuth) - TorAddr enum for .onion and clearnet address types - Connection pool with per-connection receive tasks, reuses TCP stream FMP framing - connect_async()/connection_state_sync()/promote_connection() follow the same non-blocking polling pattern as TCP transport Inbound (directory mode — recommended for production): - Tor manages the onion service via HiddenServiceDir in torrc - FIPS reads .onion address from hostname file at startup - No control port needed — enables Tor Sandbox 1 (seccomp-bpf) - Accept loop mirrors TCP pattern with DirectoryServiceConfig Monitoring (control_port mode and optional in directory mode): - Async control port client supporting TCP and Unix socket connections via Box<dyn AsyncRead/Write> trait objects - AUTHENTICATE with cookie or password auth - 8 GETINFO queries: bootstrap, circuits, traffic, liveness, version, dormant state, SOCKS listeners - Background monitoring task polls every 10s, caches TorMonitoringInfo in Arc<RwLock> for synchronous query access - Bootstrap milestone logging (25/50/75/100%), stall warning (>60s), network liveness transitions, dormant mode entry - Directory mode optionally connects to control port when control_addr is configured (non-fatal on failure) Operator visibility: - show_transports query exposes tor_mode, onion_address, tor_monitoring (bootstrap, circuit_established, traffic, liveness, version, dormant) - fipstop transport detail view: Tor mode, onion address, SOCKS5/control errors, connection stats, Tor daemon status section - fipstop table view: tor(mode) label with truncated onion address hint Security hardening: - Per-destination circuit isolation via IsolateSOCKSAuth - Unix socket default for control port (/run/tor/control) - Reference torrc with HiddenServiceDir, VanguardsLiteEnabled, ConnectionPadding, DoS protections (PoW + intro rate limiting) Config: - TorConfig with socks5, control_port, and directory modes - DirectoryServiceConfig: hostname_file, bind_addr - control_addr, control_auth, cookie_path, connect_timeout, max_inbound_connections Testing: - 69 unit + integration tests with mock SOCKS5 and control servers - Docker tests: socks5-outbound (clearnet via Tor) and directory-mode (HiddenServiceDir onion service) Documentation: - Transport layer design doc: Tor architecture, directory mode - Configuration doc: Tor config tables and examples
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# Release binaries (copied at test time)
|
||||
**/fips
|
||||
**/fipsctl
|
||||
**/fipstop
|
||||
|
||||
# Generated configs (created per-run from templates)
|
||||
*/configs/node-*.yaml
|
||||
!*/configs/node-*.yaml.tmpl
|
||||
@@ -0,0 +1,31 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
iproute2 iputils-ping dnsutils openssh-client openssh-server iperf3 \
|
||||
dnsmasq curl python3 rsync && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Setup SSH server with no authentication (test only!)
|
||||
RUN mkdir -p /var/run/sshd && \
|
||||
ssh-keygen -A && \
|
||||
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \
|
||||
sed -i 's/#PermitEmptyPasswords no/PermitEmptyPasswords yes/' /etc/ssh/sshd_config && \
|
||||
sed -i 's/UsePAM yes/UsePAM no/' /etc/ssh/sshd_config && \
|
||||
passwd -d root
|
||||
|
||||
# dnsmasq: forward .fips to FIPS daemon, everything else to Docker DNS
|
||||
RUN printf '%s\n' \
|
||||
'port=53' \
|
||||
'listen-address=127.0.0.1' \
|
||||
'bind-interfaces' \
|
||||
'server=/fips/127.0.0.1#5354' \
|
||||
'server=127.0.0.11' \
|
||||
'no-resolv' \
|
||||
>> /etc/dnsmasq.conf
|
||||
|
||||
COPY fips fipsctl /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/fips /usr/local/bin/fipsctl
|
||||
|
||||
# Start dnsmasq, SSH server, and run FIPS
|
||||
ENTRYPOINT ["/bin/bash", "-c", "dnsmasq && /usr/sbin/sshd && exec fips --config /etc/fips/fips.yaml"]
|
||||
@@ -0,0 +1 @@
|
||||
nameserver 127.0.0.1
|
||||
@@ -0,0 +1,21 @@
|
||||
# Tor configuration for FIPS socks5-outbound integration testing.
|
||||
# Provides a SOCKS5 proxy on port 9050 accessible from the Docker network.
|
||||
|
||||
# Bind to all interfaces (Docker bridge network requires non-localhost)
|
||||
# IsolateSOCKSAuth is required because FIPS uses SOCKS5 username/password
|
||||
# for per-destination circuit isolation. Without it, Tor rejects auth method 0x02.
|
||||
SocksPort 0.0.0.0:9050 IsolateSOCKSAuth
|
||||
|
||||
# Security hardening
|
||||
ExitRelay 0
|
||||
ExitPolicy reject *:*
|
||||
VanguardsLiteEnabled 1
|
||||
ConnectionPadding 1
|
||||
SafeLogging 1
|
||||
|
||||
# Reduce startup time by not waiting for full circuit build
|
||||
# (we're testing SOCKS5 connectivity, not anonymity properties)
|
||||
__DisablePredictedCircuits 1
|
||||
|
||||
# Logging
|
||||
Log notice stderr
|
||||
@@ -0,0 +1,37 @@
|
||||
# Dockerfile for directory-mode test: Tor + FIPS co-located in one container.
|
||||
#
|
||||
# Tor manages the onion service via HiddenServiceDir. FIPS reads the
|
||||
# .onion hostname from /var/lib/tor/fips_onion_service/hostname at startup.
|
||||
# This requires Tor to bootstrap and create the hostname file before FIPS
|
||||
# starts, so the entrypoint script waits for it.
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
ARG TORRC=torrc
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
tor iproute2 iputils-ping dnsutils \
|
||||
dnsmasq python3 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# dnsmasq: forward .fips to FIPS daemon, everything else to Docker DNS
|
||||
RUN printf '%s\n' \
|
||||
'port=53' \
|
||||
'listen-address=127.0.0.1' \
|
||||
'bind-interfaces' \
|
||||
'server=/fips/127.0.0.1#5354' \
|
||||
'server=127.0.0.11' \
|
||||
'no-resolv' \
|
||||
>> /etc/dnsmasq.conf
|
||||
|
||||
COPY fips fipsctl /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/fips /usr/local/bin/fipsctl
|
||||
|
||||
COPY ${TORRC} /etc/tor/torrc
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
COPY resolv.conf /etc/resolv.conf
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,26 @@
|
||||
# FIPS directory-mode test — node A (onion service via HiddenServiceDir)
|
||||
#
|
||||
# Runs in directory mode: Tor manages the onion service via HiddenServiceDir
|
||||
# in torrc. FIPS reads the .onion address from the hostname file.
|
||||
# No control port needed. Enables Tor Sandbox 1.
|
||||
|
||||
node:
|
||||
identity:
|
||||
nsec: "{{NSEC_A}}"
|
||||
|
||||
tun:
|
||||
enabled: true
|
||||
name: fips0
|
||||
mtu: 1280
|
||||
|
||||
dns:
|
||||
enabled: true
|
||||
bind_addr: "127.0.0.1"
|
||||
|
||||
transports:
|
||||
tor:
|
||||
mode: "directory"
|
||||
socks5_addr: "127.0.0.1:9050"
|
||||
directory_service:
|
||||
hostname_file: "/var/lib/tor/fips_onion_service/hostname"
|
||||
bind_addr: "127.0.0.1:8443"
|
||||
@@ -0,0 +1,30 @@
|
||||
# FIPS directory-mode test — node B (outbound connector)
|
||||
#
|
||||
# Runs in socks5 mode: connects outbound to node A's .onion address
|
||||
# via the shared Tor daemon's SOCKS5 proxy. The .onion address and
|
||||
# npub are injected by the test script at runtime.
|
||||
|
||||
node:
|
||||
identity:
|
||||
nsec: "{{NSEC_B}}"
|
||||
|
||||
tun:
|
||||
enabled: true
|
||||
name: fips0
|
||||
mtu: 1280
|
||||
|
||||
dns:
|
||||
enabled: true
|
||||
bind_addr: "127.0.0.1"
|
||||
|
||||
transports:
|
||||
tor:
|
||||
socks5_addr: "127.0.0.1:9050"
|
||||
|
||||
peers:
|
||||
- npub: "{{NPUB_A}}"
|
||||
alias: "dir-a"
|
||||
addresses:
|
||||
- transport: tor
|
||||
addr: "{{ONION_ADDR_A}}"
|
||||
connect_policy: auto_connect
|
||||
@@ -0,0 +1,56 @@
|
||||
# Tor transport integration test — directory mode
|
||||
#
|
||||
# Topology:
|
||||
# [fips-a] — Tor + FIPS co-located, HiddenServiceDir onion service
|
||||
# [fips-b] — Tor + FIPS co-located, socks5-only, connects to fips-a's .onion
|
||||
#
|
||||
# Both nodes run Tor and FIPS in the same container. Node A's Tor manages
|
||||
# the onion service via HiddenServiceDir with Sandbox 1. Node B connects
|
||||
# outbound through its local Tor's SOCKS5 proxy.
|
||||
|
||||
networks:
|
||||
dir-test:
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
fips-a:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.colocated
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
sysctls:
|
||||
- net.ipv6.conf.all.disable_ipv6=0
|
||||
restart: "no"
|
||||
container_name: fips-dir-a
|
||||
hostname: fips-dir-a
|
||||
volumes:
|
||||
- ./configs/node-a.yaml:/etc/fips/fips.yaml:ro
|
||||
environment:
|
||||
- RUST_LOG=info,fips::transport::tor=debug
|
||||
networks:
|
||||
dir-test:
|
||||
|
||||
fips-b:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.colocated
|
||||
args:
|
||||
TORRC: torrc.socks5
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
sysctls:
|
||||
- net.ipv6.conf.all.disable_ipv6=0
|
||||
restart: "no"
|
||||
container_name: fips-dir-b
|
||||
hostname: fips-dir-b
|
||||
volumes:
|
||||
- ./configs/node-b.yaml:/etc/fips/fips.yaml:ro
|
||||
environment:
|
||||
- RUST_LOG=info,fips::transport::tor=debug
|
||||
networks:
|
||||
dir-test:
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# Entrypoint for directory-mode test container.
|
||||
# Starts Tor, optionally waits for hostname file, then starts FIPS.
|
||||
|
||||
set -e
|
||||
|
||||
echo "Starting dnsmasq..."
|
||||
dnsmasq
|
||||
|
||||
# Check if this node uses directory mode (match the YAML value, not comments)
|
||||
IS_DIRECTORY_MODE=false
|
||||
if grep -qE '^\s+mode:\s+"directory"' /etc/fips/fips.yaml 2>/dev/null; then
|
||||
IS_DIRECTORY_MODE=true
|
||||
fi
|
||||
|
||||
# Pre-create HiddenServiceDir with correct permissions.
|
||||
# Tor requires 0700 on the directory.
|
||||
HIDDEN_SERVICE_DIR="/var/lib/tor/fips_onion_service"
|
||||
if [ "$IS_DIRECTORY_MODE" = true ]; then
|
||||
mkdir -p "$HIDDEN_SERVICE_DIR"
|
||||
chmod 700 "$HIDDEN_SERVICE_DIR"
|
||||
fi
|
||||
|
||||
echo "Starting Tor daemon..."
|
||||
tor -f /etc/tor/torrc &
|
||||
|
||||
# If this node uses directory mode, wait for Tor to create the hostname file
|
||||
if [ "$IS_DIRECTORY_MODE" = true ]; then
|
||||
HOSTNAME_FILE="${HIDDEN_SERVICE_DIR}/hostname"
|
||||
echo "Waiting for Tor to create ${HOSTNAME_FILE}..."
|
||||
for i in $(seq 1 120); do
|
||||
if [ -f "$HOSTNAME_FILE" ]; then
|
||||
echo "Tor hostname file ready after ${i}s: $(cat "$HOSTNAME_FILE")"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ ! -f "$HOSTNAME_FILE" ]; then
|
||||
echo "FATAL: Tor did not create hostname file within 120s"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Starting FIPS daemon..."
|
||||
exec fips --config /etc/fips/fips.yaml
|
||||
@@ -0,0 +1 @@
|
||||
nameserver 127.0.0.1
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
#!/bin/bash
|
||||
# Tor directory-mode integration test.
|
||||
#
|
||||
# Validates end-to-end connectivity through a Tor onion service managed
|
||||
# by HiddenServiceDir (directory mode) with Sandbox 1:
|
||||
# fips-a creates onion service via Tor-managed HiddenServiceDir
|
||||
# fips-b connects outbound to fips-a's .onion address via SOCKS5
|
||||
#
|
||||
# Both containers run Tor + FIPS co-located. This is the recommended
|
||||
# production deployment mode.
|
||||
#
|
||||
# Requires internet — the Tor daemon must bootstrap to the network
|
||||
# and publish the onion service descriptor for .onion routing.
|
||||
#
|
||||
# Usage: ./directory-test.sh
|
||||
|
||||
set -e
|
||||
trap 'echo ""; echo "Test interrupted — cleaning up..."; docker compose down 2>/dev/null; exit 130' INT
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TEST_DIR="$SCRIPT_DIR/.."
|
||||
DERIVE_KEYS="$SCRIPT_DIR/../../../static/scripts/derive-keys.py"
|
||||
cd "$TEST_DIR"
|
||||
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
TIMEOUT_PING=15
|
||||
MAX_WAIT_ONION=120
|
||||
MAX_WAIT_PEER=180
|
||||
|
||||
# Count connected peers for a node using fipsctl show peers JSON output
|
||||
count_connected_peers() {
|
||||
local container="$1"
|
||||
docker exec "$container" fipsctl show peers 2>/dev/null \
|
||||
| python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
print(sum(1 for p in data.get('peers', []) if p.get('connectivity') == 'connected'))
|
||||
except:
|
||||
print(0)
|
||||
" 2>/dev/null || echo 0
|
||||
}
|
||||
|
||||
echo "=== FIPS Tor Directory-Mode Integration Test ==="
|
||||
echo ""
|
||||
|
||||
# ── Phase 0: Setup ───────────────────────────────────────────────
|
||||
echo "Phase 0: Setup..."
|
||||
|
||||
# Copy binaries from common (built externally)
|
||||
if [ ! -f fips ] || [ ! -f fipsctl ]; then
|
||||
if [ -f ../common/fips ] && [ -f ../common/fipsctl ]; then
|
||||
cp ../common/fips ../common/fipsctl .
|
||||
echo " Copied binaries from ../common/"
|
||||
else
|
||||
echo " ERROR: fips and fipsctl binaries not found."
|
||||
echo " Build them first: cargo build --release"
|
||||
echo " Then copy to testing/tor/common/ or testing/tor/directory-mode/"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Generate ephemeral identities
|
||||
MESH_NAME="dir-test-$(date +%s)-$$"
|
||||
echo " Mesh name: $MESH_NAME"
|
||||
|
||||
KEYS_A=$(python3 "$DERIVE_KEYS" "$MESH_NAME" "a")
|
||||
NSEC_A=$(echo "$KEYS_A" | grep "^nsec=" | cut -d= -f2)
|
||||
NPUB_A=$(echo "$KEYS_A" | grep "^npub=" | cut -d= -f2)
|
||||
|
||||
KEYS_B=$(python3 "$DERIVE_KEYS" "$MESH_NAME" "b")
|
||||
NSEC_B=$(echo "$KEYS_B" | grep "^nsec=" | cut -d= -f2)
|
||||
NPUB_B=$(echo "$KEYS_B" | grep "^npub=" | cut -d= -f2)
|
||||
|
||||
echo " Node A: $NPUB_A"
|
||||
echo " Node B: $NPUB_B"
|
||||
|
||||
# Generate node-a config
|
||||
sed "s/{{NSEC_A}}/$NSEC_A/" configs/node-a.yaml.tmpl > configs/node-a.yaml
|
||||
echo " Node A config generated"
|
||||
echo ""
|
||||
|
||||
# ── Phase 1: Start node A (Tor + FIPS co-located) ────────────────
|
||||
echo "Phase 1: Starting node A (Tor+FIPS, directory-mode onion service)..."
|
||||
docker compose down 2>/dev/null || true
|
||||
docker compose up -d --build fips-a
|
||||
echo ""
|
||||
|
||||
# ── Phase 2: Wait for onion service creation ─────────────────────
|
||||
echo "Phase 2: Waiting for node A's onion service (up to ${MAX_WAIT_ONION}s)..."
|
||||
echo " (Tor bootstrap + HiddenServiceDir publication)"
|
||||
ONION_ADDR=""
|
||||
elapsed=0
|
||||
while [ "$elapsed" -lt "$MAX_WAIT_ONION" ]; do
|
||||
# Extract .onion address from structured log: onion_address=<addr>.onion
|
||||
# Strip ANSI color codes before matching (tracing emits them by default)
|
||||
ONION_ADDR=$(docker logs fips-dir-a 2>&1 \
|
||||
| sed 's/\x1b\[[0-9;]*m//g' \
|
||||
| grep -oE 'onion_address=[a-z2-7]{56}\.onion' \
|
||||
| head -1 \
|
||||
| cut -d= -f2)
|
||||
|
||||
if [ -n "$ONION_ADDR" ]; then
|
||||
echo " Onion service active after ${elapsed}s"
|
||||
echo " Address: $ONION_ADDR"
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
elapsed=$((elapsed + 5))
|
||||
echo " ${elapsed}s..."
|
||||
done
|
||||
|
||||
if [ -z "$ONION_ADDR" ]; then
|
||||
echo " FAIL: Onion service not created within ${MAX_WAIT_ONION}s"
|
||||
echo ""
|
||||
echo "Node A logs (last 30 lines):"
|
||||
docker logs fips-dir-a 2>&1 | tail -30
|
||||
docker compose down
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ── Phase 3: Start node B with .onion address ────────────────────
|
||||
echo "Phase 3: Starting node B (Tor+FIPS, socks5-only)..."
|
||||
|
||||
# Generate node-b config with discovered .onion address + virtual port
|
||||
ONION_CONNECT="${ONION_ADDR}:8443"
|
||||
sed -e "s/{{NSEC_B}}/$NSEC_B/" \
|
||||
-e "s/{{NPUB_A}}/$NPUB_A/" \
|
||||
-e "s/{{ONION_ADDR_A}}/$ONION_CONNECT/" \
|
||||
configs/node-b.yaml.tmpl > configs/node-b.yaml
|
||||
|
||||
echo " Node B config generated (target: $ONION_CONNECT)"
|
||||
docker compose up -d fips-b
|
||||
echo ""
|
||||
|
||||
# ── Phase 4: Wait for peer connection ────────────────────────────
|
||||
echo "Phase 4: Waiting for peer connection (up to ${MAX_WAIT_PEER}s)..."
|
||||
echo " (SOCKS5 circuit setup + .onion routing may take a while)"
|
||||
|
||||
peers_a=0
|
||||
peers_b=0
|
||||
elapsed=0
|
||||
while [ "$elapsed" -lt "$MAX_WAIT_PEER" ]; do
|
||||
peers_a=$(count_connected_peers fips-dir-a)
|
||||
peers_b=$(count_connected_peers fips-dir-b)
|
||||
|
||||
if [ "$peers_a" -ge 1 ] && [ "$peers_b" -ge 1 ]; then
|
||||
echo " Both nodes connected after ${elapsed}s (A: ${peers_a}, B: ${peers_b})"
|
||||
break
|
||||
fi
|
||||
sleep 10
|
||||
elapsed=$((elapsed + 10))
|
||||
echo " ${elapsed}s... (A peers: ${peers_a}, B peers: ${peers_b})"
|
||||
done
|
||||
|
||||
if [ "$peers_a" -lt 1 ] || [ "$peers_b" -lt 1 ]; then
|
||||
echo " FAIL: Peers not established within ${MAX_WAIT_PEER}s"
|
||||
echo ""
|
||||
echo "Node A logs (last 30 lines):"
|
||||
docker logs fips-dir-a 2>&1 | tail -30
|
||||
echo ""
|
||||
echo "Node B logs (last 30 lines):"
|
||||
docker logs fips-dir-b 2>&1 | tail -30
|
||||
docker compose down
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extra convergence time for routing
|
||||
echo " Waiting 10s for routing convergence..."
|
||||
sleep 10
|
||||
echo ""
|
||||
|
||||
# ── Phase 5: Connectivity tests ──────────────────────────────────
|
||||
echo "Phase 5: Connectivity tests"
|
||||
|
||||
PING_COUNT=11
|
||||
|
||||
ping_series() {
|
||||
local from="$1"
|
||||
local to_npub="$2"
|
||||
local label="$3"
|
||||
|
||||
echo " $label ($PING_COUNT pings, dropping first):"
|
||||
local rtts=()
|
||||
local fails=0
|
||||
for i in $(seq 1 "$PING_COUNT"); do
|
||||
local output
|
||||
if output=$(docker exec "$from" ping6 -c 1 -W "$TIMEOUT_PING" "${to_npub}.fips" 2>&1); then
|
||||
local rtt
|
||||
rtt=$(echo "$output" | grep -oE 'time=[0-9.]+' | cut -d= -f2)
|
||||
if [ -n "$rtt" ]; then
|
||||
printf " %2d: %s ms\n" "$i" "$rtt"
|
||||
rtts+=("$rtt")
|
||||
else
|
||||
printf " %2d: OK (no rtt)\n" "$i"
|
||||
fi
|
||||
else
|
||||
printf " %2d: FAIL\n" "$i"
|
||||
fails=$((fails + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$fails" -gt 0 ]; then
|
||||
FAILED=$((FAILED + fails))
|
||||
fi
|
||||
|
||||
# Drop first ping, compute average of remaining
|
||||
if [ "${#rtts[@]}" -ge 2 ]; then
|
||||
local avg
|
||||
local csv
|
||||
csv=$(IFS=,; echo "${rtts[*]}")
|
||||
avg=$(python3 -c "
|
||||
rtts = [$csv]
|
||||
trimmed = rtts[1:]
|
||||
print(f'{sum(trimmed)/len(trimmed):.1f}')
|
||||
")
|
||||
echo " Avg (excluding first): ${avg} ms"
|
||||
PASSED=$((PASSED + ${#rtts[@]}))
|
||||
elif [ "${#rtts[@]}" -eq 1 ]; then
|
||||
echo " Only 1 successful ping, no average"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo " No successful pings"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo " Ping via Tor onion service (directory mode, Sandbox 1):"
|
||||
ping_series fips-dir-a "$NPUB_B" "A → B"
|
||||
ping_series fips-dir-b "$NPUB_A" "B → A"
|
||||
|
||||
echo ""
|
||||
|
||||
# ── Phase 6: Log analysis ────────────────────────────────────────
|
||||
echo "Phase 6: Log analysis"
|
||||
|
||||
for node in fips-dir-a fips-dir-b; do
|
||||
panics=$(docker logs "$node" 2>&1 | grep -ci "panic" || true)
|
||||
errors=$(docker logs "$node" 2>&1 | grep -ci "error" || true)
|
||||
onion=$(docker logs "$node" 2>&1 | grep -ci "onion" || true)
|
||||
directory=$(docker logs "$node" 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -ci "directory.mode" || true)
|
||||
echo " $node: panics=$panics errors=$errors onion_mentions=$onion directory_mentions=$directory"
|
||||
if [ "$panics" -gt 0 ]; then
|
||||
echo " WARNING: panics detected in $node logs"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
|
||||
# ── Cleanup ──────────────────────────────────────────────────────
|
||||
echo "Cleaning up..."
|
||||
docker compose down
|
||||
rm -f configs/node-a.yaml configs/node-b.yaml
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $PASSED passed, $FAILED failed ==="
|
||||
[ "$FAILED" -eq 0 ] && exit 0 || exit 1
|
||||
@@ -0,0 +1,23 @@
|
||||
# Tor configuration for FIPS directory-mode integration test.
|
||||
# Uses HiddenServiceDir — Tor manages the onion service and key.
|
||||
#
|
||||
# NOTE: Sandbox 1 is omitted in Docker tests (requires specific seccomp
|
||||
# profiles and pre-existing directory ownership). Production deployments
|
||||
# on bare metal should enable Sandbox 1 per packaging/torrc.fips.
|
||||
|
||||
# IsolateSOCKSAuth is required because FIPS uses SOCKS5 username/password
|
||||
# for per-destination circuit isolation.
|
||||
SocksPort 127.0.0.1:9050 IsolateSOCKSAuth
|
||||
|
||||
# Onion service — Tor manages key and hostname file.
|
||||
HiddenServiceDir /var/lib/tor/fips_onion_service
|
||||
HiddenServicePort 8443 127.0.0.1:8443
|
||||
|
||||
# Security hardening (subset safe for Docker)
|
||||
ExitRelay 0
|
||||
ExitPolicy reject *:*
|
||||
VanguardsLiteEnabled 1
|
||||
SafeLogging 1
|
||||
|
||||
# Logging
|
||||
Log notice stderr
|
||||
@@ -0,0 +1,17 @@
|
||||
# Tor configuration for directory-mode test node B (socks5-only).
|
||||
# No onion service — outbound connections only.
|
||||
|
||||
# IsolateSOCKSAuth is required because FIPS uses SOCKS5 username/password
|
||||
# for per-destination circuit isolation.
|
||||
SocksPort 127.0.0.1:9050 IsolateSOCKSAuth
|
||||
|
||||
# Security hardening
|
||||
ExitRelay 0
|
||||
ExitPolicy reject *:*
|
||||
Sandbox 1
|
||||
VanguardsLiteEnabled 1
|
||||
ConnectionPadding 1
|
||||
SafeLogging 1
|
||||
|
||||
# Logging
|
||||
Log notice stderr
|
||||
@@ -0,0 +1,29 @@
|
||||
# FIPS Tor test node A — socks5-outbound
|
||||
#
|
||||
# Connects outbound to vps-chi (217.77.8.91:443) via Tor SOCKS5 proxy.
|
||||
# Identity generated per-run to avoid mesh clashes with parallel tests.
|
||||
|
||||
node:
|
||||
identity:
|
||||
nsec: "{{NSEC_A}}"
|
||||
|
||||
tun:
|
||||
enabled: true
|
||||
name: fips0
|
||||
mtu: 1280
|
||||
|
||||
dns:
|
||||
enabled: true
|
||||
bind_addr: "127.0.0.1"
|
||||
|
||||
transports:
|
||||
tor:
|
||||
socks5_addr: "tor-daemon:9050"
|
||||
|
||||
peers:
|
||||
- npub: "npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98"
|
||||
alias: "vps-chi"
|
||||
addresses:
|
||||
- transport: tor
|
||||
addr: "217.77.8.91:443"
|
||||
connect_policy: auto_connect
|
||||
@@ -0,0 +1,29 @@
|
||||
# FIPS Tor test node B — socks5-outbound
|
||||
#
|
||||
# Connects outbound to vps-chi (217.77.8.91:443) via Tor SOCKS5 proxy.
|
||||
# Identity generated per-run to avoid mesh clashes with parallel tests.
|
||||
|
||||
node:
|
||||
identity:
|
||||
nsec: "{{NSEC_B}}"
|
||||
|
||||
tun:
|
||||
enabled: true
|
||||
name: fips0
|
||||
mtu: 1280
|
||||
|
||||
dns:
|
||||
enabled: true
|
||||
bind_addr: "127.0.0.1"
|
||||
|
||||
transports:
|
||||
tor:
|
||||
socks5_addr: "tor-daemon:9050"
|
||||
|
||||
peers:
|
||||
- npub: "npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98"
|
||||
alias: "vps-chi"
|
||||
addresses:
|
||||
- transport: tor
|
||||
addr: "217.77.8.91:443"
|
||||
connect_policy: auto_connect
|
||||
@@ -0,0 +1,66 @@
|
||||
# Tor transport integration test — socks5-outbound
|
||||
#
|
||||
# Topology:
|
||||
# [fips-a] --tor/socks5--> vps-chi (217.77.8.91:443) <--tor/socks5-- [fips-b]
|
||||
#
|
||||
# Both FIPS nodes connect outbound through a local Tor daemon's SOCKS5
|
||||
# proxy to vps-chi's TCP listener. vps-chi routes between them.
|
||||
# Ping between fips-a and fips-b validates the full Tor transport path.
|
||||
|
||||
networks:
|
||||
tor-test:
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
tor-daemon:
|
||||
image: osminogin/tor-simple:latest
|
||||
container_name: tor-daemon
|
||||
restart: "no"
|
||||
volumes:
|
||||
- ../common/torrc:/etc/tor/torrc:ro
|
||||
networks:
|
||||
tor-test:
|
||||
|
||||
fips-a:
|
||||
build:
|
||||
context: ../common
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
sysctls:
|
||||
- net.ipv6.conf.all.disable_ipv6=0
|
||||
restart: "no"
|
||||
container_name: fips-tor-a
|
||||
hostname: fips-tor-a
|
||||
depends_on:
|
||||
- tor-daemon
|
||||
volumes:
|
||||
- ../common/resolv.conf:/etc/resolv.conf:ro
|
||||
- ./configs/node-a.yaml:/etc/fips/fips.yaml:ro
|
||||
environment:
|
||||
- RUST_LOG=info,fips::transport::tor=debug
|
||||
networks:
|
||||
tor-test:
|
||||
|
||||
fips-b:
|
||||
build:
|
||||
context: ../common
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
sysctls:
|
||||
- net.ipv6.conf.all.disable_ipv6=0
|
||||
restart: "no"
|
||||
container_name: fips-tor-b
|
||||
hostname: fips-tor-b
|
||||
depends_on:
|
||||
- tor-daemon
|
||||
volumes:
|
||||
- ../common/resolv.conf:/etc/resolv.conf:ro
|
||||
- ./configs/node-b.yaml:/etc/fips/fips.yaml:ro
|
||||
environment:
|
||||
- RUST_LOG=info,fips::transport::tor=debug
|
||||
networks:
|
||||
tor-test:
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
#!/bin/bash
|
||||
# Tor transport integration test.
|
||||
#
|
||||
# Validates end-to-end connectivity through a real Tor network:
|
||||
# fips-a --tor/socks5--> vps-chi <--tor/socks5-- fips-b
|
||||
#
|
||||
# Both local FIPS nodes connect outbound through a local Tor daemon
|
||||
# to vps-chi's TCP listener (217.77.8.91:443). Once both are peered
|
||||
# with vps-chi, traffic between fips-a and fips-b is routed through it.
|
||||
#
|
||||
# Each run generates ephemeral identities to avoid mesh clashes when
|
||||
# multiple instances of this test run concurrently.
|
||||
#
|
||||
# Usage: ./tor-test.sh
|
||||
#
|
||||
# Timings (approximate):
|
||||
# Tor bootstrap: 10-30s
|
||||
# First SOCKS5 attempt: may timeout at 60s (circuits not ready)
|
||||
# Retry + circuit setup: 10-30s
|
||||
# FIPS handshake: ~1s per peer
|
||||
# Routing convergence: ~5s
|
||||
# Total: ~90-180s
|
||||
#
|
||||
# This test requires internet access for the Tor daemon.
|
||||
|
||||
set -e
|
||||
trap 'echo ""; echo "Test interrupted — cleaning up..."; docker compose down 2>/dev/null; exit 130' INT
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TOR_DIR="$SCRIPT_DIR/.."
|
||||
DERIVE_KEYS="$SCRIPT_DIR/../../../static/scripts/derive-keys.py"
|
||||
cd "$TOR_DIR"
|
||||
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
TIMEOUT_PING=15
|
||||
MAX_WAIT_TOR=90
|
||||
MAX_WAIT_PEER=180
|
||||
|
||||
# Count connected peers for a node using fipsctl show peers JSON output
|
||||
count_connected_peers() {
|
||||
local container="$1"
|
||||
docker exec "$container" fipsctl show peers 2>/dev/null \
|
||||
| python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
print(sum(1 for p in data.get('peers', []) if p.get('connectivity') == 'connected'))
|
||||
except:
|
||||
print(0)
|
||||
" 2>/dev/null || echo 0
|
||||
}
|
||||
|
||||
echo "=== FIPS Tor Transport Integration Test ==="
|
||||
echo ""
|
||||
|
||||
# ── Phase 0: Generate ephemeral identities ───────────────────────
|
||||
echo "Phase 0: Generating ephemeral identities..."
|
||||
|
||||
MESH_NAME="tor-test-$(date +%s)-$$"
|
||||
echo " Mesh name: $MESH_NAME"
|
||||
|
||||
KEYS_A=$(python3 "$DERIVE_KEYS" "$MESH_NAME" "a")
|
||||
NSEC_A=$(echo "$KEYS_A" | grep "^nsec=" | cut -d= -f2)
|
||||
NPUB_A=$(echo "$KEYS_A" | grep "^npub=" | cut -d= -f2)
|
||||
|
||||
KEYS_B=$(python3 "$DERIVE_KEYS" "$MESH_NAME" "b")
|
||||
NSEC_B=$(echo "$KEYS_B" | grep "^nsec=" | cut -d= -f2)
|
||||
NPUB_B=$(echo "$KEYS_B" | grep "^npub=" | cut -d= -f2)
|
||||
|
||||
echo " Node A: $NPUB_A"
|
||||
echo " Node B: $NPUB_B"
|
||||
|
||||
# Generate configs from templates
|
||||
sed "s/{{NSEC_A}}/$NSEC_A/" configs/node-a.yaml.tmpl > configs/node-a.yaml
|
||||
sed "s/{{NSEC_B}}/$NSEC_B/" configs/node-b.yaml.tmpl > configs/node-b.yaml
|
||||
echo " Configs generated"
|
||||
echo ""
|
||||
|
||||
# ── Phase 1: Build and start ─────────────────────────────────────
|
||||
echo "Phase 1: Starting Tor daemon and FIPS nodes..."
|
||||
docker compose down 2>/dev/null || true
|
||||
docker compose up -d --build
|
||||
echo ""
|
||||
|
||||
# ── Phase 2: Wait for Tor bootstrap ─────────────────────────────
|
||||
echo "Phase 2: Waiting for Tor daemon to bootstrap (up to ${MAX_WAIT_TOR}s)..."
|
||||
elapsed=0
|
||||
while [ "$elapsed" -lt "$MAX_WAIT_TOR" ]; do
|
||||
if docker logs tor-daemon 2>&1 | grep -q "Bootstrapped 100%"; then
|
||||
echo " Tor bootstrapped after ${elapsed}s"
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
elapsed=$((elapsed + 5))
|
||||
echo " ${elapsed}s..."
|
||||
done
|
||||
|
||||
if [ "$elapsed" -ge "$MAX_WAIT_TOR" ]; then
|
||||
echo " FAIL: Tor daemon did not bootstrap within ${MAX_WAIT_TOR}s"
|
||||
echo ""
|
||||
echo "Tor daemon logs:"
|
||||
docker logs tor-daemon 2>&1 | tail -20
|
||||
docker compose down
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ── Phase 3: Wait for FIPS peers via Tor ─────────────────────────
|
||||
echo "Phase 3: Waiting for FIPS nodes to peer with vps-chi via Tor (up to ${MAX_WAIT_PEER}s)..."
|
||||
echo " (First SOCKS5 attempt may timeout while Tor builds circuits)"
|
||||
|
||||
peers_a=0
|
||||
peers_b=0
|
||||
elapsed=0
|
||||
while [ "$elapsed" -lt "$MAX_WAIT_PEER" ]; do
|
||||
peers_a=$(count_connected_peers fips-tor-a)
|
||||
peers_b=$(count_connected_peers fips-tor-b)
|
||||
|
||||
if [ "$peers_a" -ge 1 ] && [ "$peers_b" -ge 1 ]; then
|
||||
echo " Both nodes have connected peers after ${elapsed}s (A: ${peers_a}, B: ${peers_b})"
|
||||
break
|
||||
fi
|
||||
sleep 10
|
||||
elapsed=$((elapsed + 10))
|
||||
echo " ${elapsed}s... (A peers: ${peers_a}, B peers: ${peers_b})"
|
||||
done
|
||||
|
||||
if [ "$peers_a" -lt 1 ] || [ "$peers_b" -lt 1 ]; then
|
||||
echo " FAIL: Peers not established within ${MAX_WAIT_PEER}s"
|
||||
echo ""
|
||||
echo "Node A logs (last 30 lines):"
|
||||
docker logs fips-tor-a 2>&1 | tail -30
|
||||
echo ""
|
||||
echo "Node B logs (last 30 lines):"
|
||||
docker logs fips-tor-b 2>&1 | tail -30
|
||||
docker compose down
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extra convergence time for routing
|
||||
echo " Waiting 10s for routing convergence..."
|
||||
sleep 10
|
||||
echo ""
|
||||
|
||||
# ── Phase 4: Connectivity tests ──────────────────────────────────
|
||||
echo "Phase 4: Connectivity tests"
|
||||
|
||||
PING_COUNT=11
|
||||
|
||||
ping_series() {
|
||||
local from="$1"
|
||||
local to_npub="$2"
|
||||
local label="$3"
|
||||
|
||||
echo " $label ($PING_COUNT pings, dropping first):"
|
||||
local rtts=()
|
||||
local fails=0
|
||||
for i in $(seq 1 "$PING_COUNT"); do
|
||||
local output
|
||||
if output=$(docker exec "$from" ping6 -c 1 -W "$TIMEOUT_PING" "${to_npub}.fips" 2>&1); then
|
||||
local rtt
|
||||
rtt=$(echo "$output" | grep -oE 'time=[0-9.]+' | cut -d= -f2)
|
||||
if [ -n "$rtt" ]; then
|
||||
printf " %2d: %s ms\n" "$i" "$rtt"
|
||||
rtts+=("$rtt")
|
||||
else
|
||||
printf " %2d: OK (no rtt)\n" "$i"
|
||||
fi
|
||||
else
|
||||
printf " %2d: FAIL\n" "$i"
|
||||
fails=$((fails + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$fails" -gt 0 ]; then
|
||||
FAILED=$((FAILED + fails))
|
||||
fi
|
||||
|
||||
# Drop first ping, compute average of remaining
|
||||
if [ "${#rtts[@]}" -ge 2 ]; then
|
||||
local avg
|
||||
local csv
|
||||
csv=$(IFS=,; echo "${rtts[*]}")
|
||||
avg=$(python3 -c "
|
||||
rtts = [$csv]
|
||||
trimmed = rtts[1:]
|
||||
print(f'{sum(trimmed)/len(trimmed):.1f}')
|
||||
")
|
||||
echo " Avg (excluding first): ${avg} ms"
|
||||
PASSED=$((PASSED + ${#rtts[@]}))
|
||||
elif [ "${#rtts[@]}" -eq 1 ]; then
|
||||
echo " Only 1 successful ping, no average"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo " No successful pings"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo " Ping via Tor (routed through vps-chi):"
|
||||
ping_series fips-tor-a "$NPUB_B" "A → B"
|
||||
ping_series fips-tor-b "$NPUB_A" "B → A"
|
||||
|
||||
echo ""
|
||||
|
||||
# ── Phase 5: Log analysis ────────────────────────────────────────
|
||||
echo "Phase 5: Log analysis"
|
||||
|
||||
for node in fips-tor-a fips-tor-b; do
|
||||
panics=$(docker logs "$node" 2>&1 | grep -ci "panic" || true)
|
||||
errors=$(docker logs "$node" 2>&1 | grep -ci "error" || true)
|
||||
socks5=$(docker logs "$node" 2>&1 | grep -ci "socks5\|socks" || true)
|
||||
echo " $node: panics=$panics errors=$errors socks5_mentions=$socks5"
|
||||
if [ "$panics" -gt 0 ]; then
|
||||
echo " WARNING: panics detected in $node logs"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
|
||||
# ── Cleanup ──────────────────────────────────────────────────────
|
||||
echo "Cleaning up..."
|
||||
docker compose down
|
||||
rm -f configs/node-a.yaml configs/node-b.yaml
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $PASSED passed, $FAILED failed ==="
|
||||
[ "$FAILED" -eq 0 ] && exit 0 || exit 1
|
||||
Reference in New Issue
Block a user