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:
Dorian
2026-03-20 02:59:29 +00:00
parent f7872e2914
commit cffcc9f665
15 changed files with 904 additions and 250 deletions

View File

@@ -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

View File

@@ -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
View 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