fix: Tor management system, bug fixes, federation name sync
Major changes: - Full Tor hidden service management via systemd path unit pattern (tor-helper.sh + archipelago-tor-helper.path/service) — respects NoNewPrivileges=yes, no sudo needed from backend - Container doctor: prefer system Tor over container, remove archy-tor - Deploy script: fix torrc generation (read correct services.json path), web apps map port 80→local port, enable both tor and tor@default - Federation: server rename pushes name to peers via background sync - Server name: fix root-owned file, optimistic store update - Mesh: local echo for sent messages, sendingArch loading state - Web5: Message button → Mesh redirect, node name lookup in messages - PeerFiles: show DID not onion in header - Connected Nodes: flex-1 instead of fixed max-h - Toast notifications route to Mesh - Deploy script: fix single-quote syntax in SSH block Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -85,45 +85,26 @@ fix_orphaned_conmon() {
|
||||
$fixed && return 0 || return 1
|
||||
}
|
||||
|
||||
# ── Fix 3: System tor conflict ───────────────────────────────
|
||||
# ── Fix 3: Ensure system Tor is running (preferred over container) ──
|
||||
fix_system_tor_conflict() {
|
||||
# Only relevant if we have a container tor on host network
|
||||
local has_container_tor=false
|
||||
# System Tor is preferred over container Tor.
|
||||
# If archy-tor container exists, remove it and use system Tor instead.
|
||||
if podman ps -a --format '{{.Names}}' 2>/dev/null | grep -qE '^archy-tor$'; then
|
||||
local net_mode
|
||||
net_mode=$(podman inspect archy-tor --format '{{.HostConfig.NetworkMode}}' 2>/dev/null || true)
|
||||
if [ "$net_mode" = "host" ]; then
|
||||
has_container_tor=true
|
||||
podman stop archy-tor 2>/dev/null || true
|
||||
podman rm -f archy-tor 2>/dev/null || true
|
||||
log "Removed archy-tor container (system Tor is preferred)"
|
||||
fi
|
||||
|
||||
# Ensure system Tor is enabled and running
|
||||
if command -v tor >/dev/null 2>&1; then
|
||||
if ! systemctl is-active tor@default >/dev/null 2>&1; then
|
||||
systemctl enable tor tor@default 2>/dev/null || true
|
||||
systemctl start tor tor@default 2>/dev/null || true
|
||||
log "Started system Tor"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! $has_container_tor; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if system tor is binding port 9050
|
||||
local system_tor_pid
|
||||
system_tor_pid=$(ss -tlnp 2>/dev/null | grep ':9050 ' | grep -oP 'pid=\K\d+' | head -1)
|
||||
if [ -z "$system_tor_pid" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if it's the system tor (not container tor)
|
||||
local exe
|
||||
exe=$(readlink /proc/"$system_tor_pid"/exe 2>/dev/null || true)
|
||||
if [[ "$exe" == */tor ]] && ! grep -q "container" /proc/"$system_tor_pid"/cgroup 2>/dev/null; then
|
||||
log "System tor (pid=$system_tor_pid) conflicts with container tor on port 9050"
|
||||
systemctl stop tor@default 2>/dev/null || true
|
||||
systemctl stop tor 2>/dev/null || true
|
||||
systemctl disable tor@default 2>/dev/null || true
|
||||
systemctl disable tor 2>/dev/null || true
|
||||
sleep 2
|
||||
# Restart container tor now that port is free
|
||||
podman restart archy-tor 2>/dev/null || true
|
||||
log "Disabled system tor, restarted container tor"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -147,9 +128,9 @@ fix_tor_permissions() {
|
||||
done < <(find "$base" -maxdepth 1 -name "hidden_service_*" -type d 2>/dev/null)
|
||||
done
|
||||
|
||||
# If we fixed permissions and tor container exists, restart it
|
||||
# If we fixed permissions, restart system Tor to pick up the changes
|
||||
if $fixed; then
|
||||
podman restart archy-tor 2>/dev/null || true
|
||||
systemctl restart tor@default 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
|
||||
@@ -685,8 +685,9 @@ PYEOF
|
||||
# Fix secrets directory ownership (must be readable by archipelago user, not root)
|
||||
sudo chown -R archipelago:archipelago /var/lib/archipelago/secrets 2>/dev/null || true
|
||||
sudo chmod 700 /var/lib/archipelago/secrets 2>/dev/null || true
|
||||
# Fix any root-owned config files in data dir (dead man's switch, sessions, etc.)
|
||||
sudo find /var/lib/archipelago -maxdepth 1 -name '*.json' -user root -exec chown archipelago:archipelago {} \; 2>/dev/null || true
|
||||
# Fix any root-owned files in data dir - dead mans switch, sessions, server-name
|
||||
sudo find /var/lib/archipelago -maxdepth 1 -name "*.json" -user root -exec chown archipelago:archipelago {} \; 2>/dev/null || true
|
||||
sudo chown archipelago:archipelago /var/lib/archipelago/server-name 2>/dev/null || true
|
||||
echo " Data directories OK"
|
||||
|
||||
# Rootless podman UID mapping: fix data dir ownership so container processes
|
||||
@@ -716,6 +717,26 @@ PYEOF
|
||||
scp $SSH_OPTS "$PROJECT_DIR/neode-ui/public/nostr-provider.js" "$TARGET_HOST:/tmp/nostr-provider.js" 2>/dev/null && \
|
||||
ssh $SSH_OPTS "$TARGET_HOST" 'sudo cp /tmp/nostr-provider.js /opt/archipelago/web-ui/nostr-provider.js && echo " nostr-provider.js deployed"' 2>/dev/null || echo " (nostr-provider.js not found, skipping)"
|
||||
|
||||
# Deploy tor-helper: script + systemd path unit for privileged Tor management
|
||||
progress "Deploying tor-helper"
|
||||
scp $SSH_OPTS \
|
||||
"$PROJECT_DIR/scripts/tor-helper.sh" \
|
||||
"$PROJECT_DIR/image-recipe/configs/archipelago-tor-helper.path" \
|
||||
"$PROJECT_DIR/image-recipe/configs/archipelago-tor-helper.service" \
|
||||
"$TARGET_HOST:/tmp/" 2>/dev/null && \
|
||||
ssh $SSH_OPTS "$TARGET_HOST" '
|
||||
sudo mkdir -p /opt/archipelago/scripts
|
||||
sudo cp /tmp/tor-helper.sh /opt/archipelago/scripts/tor-helper.sh
|
||||
sudo chmod 755 /opt/archipelago/scripts/tor-helper.sh
|
||||
sudo chown root:root /opt/archipelago/scripts/tor-helper.sh
|
||||
sudo cp /tmp/archipelago-tor-helper.path /etc/systemd/system/
|
||||
sudo cp /tmp/archipelago-tor-helper.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable archipelago-tor-helper.path
|
||||
sudo systemctl start archipelago-tor-helper.path
|
||||
echo " tor-helper deployed with systemd path unit"
|
||||
' 2>/dev/null || echo " (tor-helper deploy skipped)"
|
||||
|
||||
# Sync nginx config (second pass — includes HTTPS snippets)
|
||||
scp $SSH_OPTS "$PROJECT_DIR/image-recipe/configs/nginx-archipelago.conf" "$TARGET_HOST:/tmp/nginx-archipelago.conf" 2>/dev/null && \
|
||||
ssh $SSH_OPTS "$TARGET_HOST" '
|
||||
@@ -1201,32 +1222,54 @@ print("services.json created")
|
||||
'
|
||||
fi
|
||||
|
||||
# Generate torrc — use /var/lib/tor/ for hidden services (AppArmor-safe)
|
||||
# Generate torrc from services.json — use /var/lib/tor/ for hidden services
|
||||
sudo python3 -c '
|
||||
import json
|
||||
lines = ["SocksPort 9050", "ControlPort 0", ""]
|
||||
try:
|
||||
with open("/var/lib/archipelago/tor/services.json") as f:
|
||||
cfg = json.load(f)
|
||||
extra_ports = {"lnd": [8080]} # LND REST API over Tor
|
||||
import json, os
|
||||
|
||||
# Protocol services get direct port mapping; web apps map port 80 to their local port
|
||||
PROTOCOL_SERVICES = {"bitcoin", "bitcoin-knots", "electrs", "electrumx", "lnd"}
|
||||
|
||||
lines = ["# Auto-generated by Archipelago deploy", "SocksPort 9050", "# ControlPort disabled", ""]
|
||||
|
||||
# Try reading services config (check both paths for compatibility)
|
||||
cfg = None
|
||||
for path in ["/var/lib/archipelago/tor-config/services.json", "/var/lib/archipelago/tor/services.json"]:
|
||||
try:
|
||||
with open(path) as f:
|
||||
cfg = json.load(f)
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if cfg:
|
||||
for svc in cfg.get("services", []):
|
||||
if svc.get("enabled", True):
|
||||
n = svc["name"]
|
||||
p = svc["local_port"]
|
||||
lines.append("HiddenServiceDir /var/lib/tor/hidden_service_%s" % n)
|
||||
lines.append("HiddenServicePort %d 127.0.0.1:%d" % (p, p))
|
||||
for ep in extra_ports.get(n, []):
|
||||
lines.append("HiddenServicePort %d 127.0.0.1:%d" % (ep, ep))
|
||||
lines.append("")
|
||||
except Exception:
|
||||
for n, ports in [("archipelago",[80]),("bitcoin",[8333]),("electrumx",[50001]),("lnd",[9735,8080]),("btcpay",[23000]),("mempool",[4080]),("fedimint",[8175])]:
|
||||
if not svc.get("enabled", True):
|
||||
continue
|
||||
n = svc["name"]
|
||||
p = svc["local_port"]
|
||||
lines.append("HiddenServiceDir /var/lib/tor/hidden_service_%s" % n)
|
||||
for p in ports:
|
||||
if n in PROTOCOL_SERVICES:
|
||||
# Protocol: direct port mapping
|
||||
lines.append("HiddenServicePort %d 127.0.0.1:%d" % (p, p))
|
||||
if n == "lnd":
|
||||
lines.append("HiddenServicePort 9735 127.0.0.1:9735")
|
||||
lines.append("HiddenServicePort 10009 127.0.0.1:10009")
|
||||
else:
|
||||
# Web app: map port 80 on .onion to local app port (access via app.onion without port)
|
||||
lines.append("HiddenServicePort 80 127.0.0.1:%d" % p)
|
||||
lines.append("")
|
||||
else:
|
||||
# Fallback: default services
|
||||
for n, mappings in [("archipelago",[(80,80)]),("bitcoin",[(8333,8333)]),("electrs",[(50001,50001)]),("lnd",[(8080,8080),(9735,9735),(10009,10009)]),("btcpay",[(80,23000)]),("mempool",[(80,4080)]),("fedimint",[(80,8175)])]:
|
||||
lines.append("HiddenServiceDir /var/lib/tor/hidden_service_%s" % n)
|
||||
for remote_p, local_p in mappings:
|
||||
lines.append("HiddenServicePort %d 127.0.0.1:%d" % (remote_p, local_p))
|
||||
lines.append("")
|
||||
|
||||
with open("/etc/tor/torrc", "w") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
print("torrc generated with %d services" % (len(lines) // 3))
|
||||
enabled = sum(1 for s in (cfg or {}).get("services", []) if s.get("enabled", True))
|
||||
print("torrc generated with %d services" % (enabled or 7))
|
||||
'
|
||||
|
||||
# Remove any old Tor container (system Tor is preferred)
|
||||
@@ -1238,6 +1281,8 @@ print("torrc generated with %d services" % (len(lines) // 3))
|
||||
# Use system Tor (preferred — no AppArmor issues with default paths)
|
||||
if command -v tor >/dev/null 2>&1; then
|
||||
sudo systemctl enable tor 2>/dev/null
|
||||
sudo systemctl enable tor@default 2>/dev/null
|
||||
sudo systemctl restart tor 2>/dev/null
|
||||
sudo systemctl restart tor@default 2>/dev/null
|
||||
echo ' Using system Tor daemon'
|
||||
else
|
||||
@@ -1245,6 +1290,8 @@ print("torrc generated with %d services" % (len(lines) // 3))
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq tor 2>/dev/null || true
|
||||
if command -v tor >/dev/null 2>&1; then
|
||||
sudo systemctl enable tor 2>/dev/null
|
||||
sudo systemctl enable tor@default 2>/dev/null
|
||||
sudo systemctl restart tor 2>/dev/null
|
||||
sudo systemctl restart tor@default 2>/dev/null
|
||||
echo ' System Tor installed and started'
|
||||
else
|
||||
|
||||
117
scripts/tor-helper.sh
Executable file
117
scripts/tor-helper.sh
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user