#!/bin/bash # tor-helper.sh — Privileged Tor operations for the Archipelago backend. # Runs as root via systemd (archipelago-tor-helper.service), triggered by # a path unit watching /var/lib/archipelago/tor-config/tor-action. # # The backend writes a JSON action file, the path unit triggers this script. # This avoids calling sudo from within a NoNewPrivileges=yes service. set -euo pipefail ACTION_FILE="/var/lib/archipelago/tor-config/tor-action" TORRC_STAGED="/var/lib/archipelago/tor-config/torrc.staged" RESULT_FILE="/var/lib/archipelago/tor-config/tor-result" HOSTNAMES_DIR="/var/lib/archipelago/tor-hostnames" log() { echo "[tor-helper] $*"; } write_result() { echo "$1" > "$RESULT_FILE" chown archipelago:archipelago "$RESULT_FILE" 2>/dev/null || true } sync_hostnames() { mkdir -p "$HOSTNAMES_DIR" # Clear stale copies first rm -f "$HOSTNAMES_DIR"/* 2>/dev/null || true # Prefer /var/lib/tor (system Tor, authoritative) over /var/lib/archipelago/tor # Only copy from secondary if not already found in primary for base in /var/lib/tor /var/lib/archipelago/tor; do for dir in "$base"/hidden_service_*; do [ -d "$dir" ] || continue svc=$(basename "$dir" | sed 's/^hidden_service_//') echo "$svc" | grep -q '_old_' && continue # Skip if already synced from a higher-priority location [ -f "${HOSTNAMES_DIR}/${svc}" ] && continue if [ -f "$dir/hostname" ]; then cp "$dir/hostname" "${HOSTNAMES_DIR}/${svc}" log "Synced hostname: $svc ($base)" fi done done chown -R archipelago:archipelago "$HOSTNAMES_DIR" 2>/dev/null || true } # ─── Main ───────────────────────────────────────────────────────── if [ ! -f "$ACTION_FILE" ]; then log "No action file found" exit 0 fi ACTION=$(cat "$ACTION_FILE") rm -f "$ACTION_FILE" ACTION_TYPE=$(echo "$ACTION" | python3 -c "import sys,json; print(json.load(sys.stdin).get('action',''))" 2>/dev/null || echo "") case "$ACTION_TYPE" in write-torrc-and-restart) if [ ! -f "$TORRC_STAGED" ]; then log "ERROR: No staged torrc at $TORRC_STAGED" write_result '{"ok":false,"error":"No staged torrc"}' exit 1 fi cp "$TORRC_STAGED" /etc/tor/torrc chown debian-tor:debian-tor /etc/tor/torrc 2>/dev/null || true log "torrc updated from staged file" systemctl restart tor log "Tor restarted" # Wait for SOCKS port for i in $(seq 1 30); do if timeout 1 bash -c 'echo > /dev/tcp/127.0.0.1/9050' 2>/dev/null; then break fi sleep 1 done sync_hostnames write_result '{"ok":true}' ;; restart) systemctl restart tor log "Tor restarted" sleep 3 sync_hostnames write_result '{"ok":true}' ;; delete-service) NAME=$(echo "$ACTION" | python3 -c "import sys,json; print(json.load(sys.stdin).get('name',''))" 2>/dev/null || echo "") if [ -z "$NAME" ]; then write_result '{"ok":false,"error":"Missing service name"}' exit 1 fi if ! echo "$NAME" | grep -qE '^[a-zA-Z0-9_-]+$'; then write_result '{"ok":false,"error":"Invalid service name"}' exit 1 fi rm -rf "/var/lib/tor/hidden_service_${NAME}" 2>/dev/null || true rm -rf "/var/lib/archipelago/tor/hidden_service_${NAME}" 2>/dev/null || true rm -f "${HOSTNAMES_DIR}/${NAME}" 2>/dev/null || true log "Deleted hidden service: $NAME" write_result '{"ok":true}' ;; sync-hostnames) sync_hostnames write_result '{"ok":true}' ;; *) log "Unknown action: $ACTION_TYPE" write_result '{"ok":false,"error":"Unknown action"}' exit 1 ;; esac