#!/usr/bin/env bash # test-tor-rotation.sh — Validate Tor address rotation end-to-end # # Tests: rotation, old/new address comparison, cache update, cleanup, # federation propagation (fire-and-forget), Nostr publish (fire-and-forget). # # Usage: ./scripts/test-tor-rotation.sh [target-ip] set -uo pipefail TARGET="${1:-192.168.1.228}" SSH_KEY="${ARCHIPELAGO_SSH_KEY:-$HOME/.ssh/archipelago-deploy}" SSH="ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o ConnectTimeout=10 archipelago@$TARGET" PASS=0 FAIL=0 check() { local name="$1" local ok="$2" if [ "$ok" = "true" ]; then echo " ✅ $name" ((PASS++)) else echo " ❌ $name" ((FAIL++)) fi } json_get() { python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',{}); print(r.get('$1','') if isinstance(r,dict) else '')" 2>/dev/null } echo "🔄 Tor Address Rotation Test — $TARGET" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" # Login echo "" echo "Authenticating..." $SSH "curl -s -c /tmp/cookiejar http://localhost:5678/rpc/v1 -H 'Content-Type: application/json' -d '{\"method\":\"auth.login\",\"params\":{\"password\":\"password123\"}}'" >/dev/null 2>&1 CSRF=$($SSH "grep csrf_token /tmp/cookiejar 2>/dev/null | awk '{print \$NF}'" 2>/dev/null) rpc() { local method="$1" local params="${2:-}" local body if [ -n "$params" ]; then body="{\"method\":\"$method\",\"params\":$params}" else body="{\"method\":\"$method\"}" fi $SSH "curl -s -b /tmp/cookiejar -H 'Content-Type: application/json' -H 'X-CSRF-Token: $CSRF' http://localhost:5678/rpc/v1 -d '$body'" 2>/dev/null } # 1. Record current address echo "" echo "1. Current Tor address" BEFORE_RESP=$(rpc "node.tor-address") OLD_ADDR=$(echo "$BEFORE_RESP" | json_get "tor_address") check "Has valid .onion address" "$(echo "$OLD_ADDR" | grep -q '.onion$' && echo true || echo false)" echo " Address: ${OLD_ADDR:-}" # 2. Rotate service echo "" echo "2. Rotating address (may take up to 60s)..." ROTATE_RESP=$(rpc "tor.rotate-service" "{\"name\":\"archipelago\"}") ROTATED=$(echo "$ROTATE_RESP" | json_get "rotated") NEW_ADDR=$(echo "$ROTATE_RESP" | json_get "new_onion") OLD_REPORTED=$(echo "$ROTATE_RESP" | json_get "old_onion") check "Rotation succeeded" "$([ "$ROTATED" = "True" ] || [ "$ROTATED" = "true" ] && echo true || echo false)" check "New address different from old" "$([ -n "$NEW_ADDR" ] && [ "$NEW_ADDR" != "$OLD_ADDR" ] && echo true || echo false)" check "Old address reported correctly" "$([ "$OLD_REPORTED" = "$OLD_ADDR" ] && echo true || echo false)" echo " Old: $OLD_ADDR" echo " New: $NEW_ADDR" # 3. Verify address updated in node.tor-address echo "" echo "3. Address updated everywhere" AFTER_RESP=$(rpc "node.tor-address") AFTER_ADDR=$(echo "$AFTER_RESP" | json_get "tor_address") check "node.tor-address returns new address" "$([ "$AFTER_ADDR" = "$NEW_ADDR" ] && echo true || echo false)" # Check tor-hostnames cache CACHE_ADDR=$($SSH "cat /var/lib/archipelago/tor-hostnames/archipelago 2>/dev/null" 2>/dev/null | tr -d '[:space:]') check "tor-hostnames cache updated" "$([ "$CACHE_ADDR" = "$NEW_ADDR" ] && echo true || echo false)" # Check actual hostname file ACTUAL_ADDR=$($SSH "sudo cat /var/lib/archipelago/tor/hidden_service_archipelago/hostname 2>/dev/null" 2>/dev/null | tr -d '[:space:]') check "Actual hostname file matches" "$([ "$ACTUAL_ADDR" = "$NEW_ADDR" ] && echo true || echo false)" # 4. Old directory preserved for transition echo "" echo "4. Transition period" OLD_DIRS=$($SSH "sudo ls /var/lib/archipelago/tor/ 2>/dev/null | grep '_old_' | wc -l" 2>/dev/null | tr -d '[:space:]') check "Old service directory preserved" "$([ "$OLD_DIRS" -ge 1 ] && echo true || echo false)" # 5. Cleanup (should not remove non-expired dirs) echo "" echo "5. Cleanup (non-expired)" CLEANUP_RESP=$(rpc "tor.cleanup-rotated") CLEANED=$(echo "$CLEANUP_RESP" | json_get "count") check "Cleanup skips non-expired dirs" "$([ "$CLEANED" = "0" ] && echo true || echo false)" # 6. Federation peer propagation (verify it was attempted) echo "" echo "6. Propagation (fire-and-forget)" FED_RESP=$(rpc "federation.list-nodes") PEER_COUNT=$(echo "$FED_RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('result',{}).get('nodes',[])))" 2>/dev/null) check "Federation peers exist for propagation ($PEER_COUNT peers)" "$([ "$PEER_COUNT" -ge 1 ] && echo true || echo false)" echo " (Propagation is fire-and-forget — peers notified via old Tor address)" echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Results: $PASS passed, $FAIL failed" [ $FAIL -eq 0 ] && exit 0 || exit 1