Three related cell-link/peer-config fixes (the peer and cell endpoints were
showing the raw external IP, which confused public-vs-internal addressing):
1. Peer WireGuard configs now embed the cell's effective domain (DDNS/ACME
modes) instead of the detected external IP, via the new
WireGuardManager.get_advertised_endpoint(). A name that resolves to the
public IP survives IP changes and lets the datacenter forward each cell's
WG port to the right host. LAN mode still falls back to the IP; an admin
wireguard_endpoint override still wins.
2. Cell invites advertise <effective-domain>:<this cell's WG port> (was the
external IP + a default/possibly-wrong port), so a remote cell pairs to the
right host and port over the public path.
3. Cross-cell peer-sync no longer targets http://<ip>:3000 (the API binds
127.0.0.1 and is unreachable across cells). It targets the remote's Caddy on
HTTPS/443 — which the WireGuard server already DNATs over the tunnel — and the
initial pre-tunnel invite push goes to https://<endpoint-host>/... ; legacy
http://<ip>:3000 link URLs migrate to https on load.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three independent bugs surfaced during pic1 clean-install testing:
1. Tor _exit_status hardcoded configured=True regardless of whether Tor was
actually installed. Status now flows through the same store-installed /
container-running bridge used by every other optional service, so Tor only
reports installed when the container is present and running.
2. check_port_open compared the port from wg0.conf against the kernel-reported
listening port, causing false "port closed" results whenever the conf and the
running container were momentarily out of sync. The function is now an honest
liveness check: any wg0 interface that is up and has a "listening port:" line
in `wg show` is considered open. The check-port API endpoint now also returns
the actual kernel listening_port and a port_mismatch flag so the UI can inform
the user when a container recreate is needed. (The recreate machinery already
exists via the port-change pending-restart path; this fix makes the mismatch
visible rather than silently lying about reachability.)
3. upload_backup only handled .zip archives; encrypted .age blobs were rejected
with a generic error. The endpoint now calls backup_crypto.is_encrypted() to
detect Age-encrypted blobs and stores them verbatim as <id>.tar.gz.age with
mode 0600 so they can be uploaded and then restored with a passphrase. The
plaintext zip path is unchanged.
Tests added/updated: test_connectivity_manager.py (Tor status bridge),
test_wireguard_manager.py + test_wireguard_endpoints.py (port-check liveness
and mismatch flag), test_config_backup_restore_http.py (encrypted upload
round-trip).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause: write_env_file used os.replace() which creates a new inode.
Docker file bind-mounts track the original inode at mount time, so the
container's /app/.env.compose never saw updates — docker compose always
read the stale port value and skipped container recreation.
Fixes:
- ip_utils.write_env_file: write in-place (open 'w') instead of os.replace()
so Docker bind-mounted files see the update immediately
- apply_pending_config: add --force-recreate to docker compose up for
specific-container restarts, bypassing config-hash comparison as a
belt-and-suspenders measure
Tests added:
- TestWriteEnvFileInPlace: verifies inode is preserved across writes
- TestApplyPendingConfigForceRecreate: verifies --force-recreate is in the
docker compose command for specific-container restarts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug 1 — port not propagated to wg0.conf:
The identity update path (wireguard_port via PUT /api/config) was calling
wireguard_manager.update_config() which only saves to a JSON file via
BaseServiceManager. wg0.conf was never updated, so after a container
restart the WireGuard interface would still listen on the old port.
Fix: call apply_config() instead — it writes ListenPort into wg0.conf.
Bug 2 — check_port_open ignored configured port:
check_port_open() checked for 'listening port' in wg show output but
never compared it against the configured port. A port-mismatch (e.g.
after config change but before restart) would return True — misleading.
Fix: require 'listening port: {configured_port}' to match exactly.
Tests added:
- test_check_port_open_wrong_port_returns_false
- test_check_port_open_explicit_port_matches
- test_check_port_open_explicit_port_mismatch
- test_wireguard_port_identity_change_calls_apply_config
- test_wireguard_port_same_value_does_not_call_apply_config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>