fix: clean-install bugs — Tor false-installed, WG port-check honesty, encrypted backup upload
Unit Tests / test (push) Successful in 13m7s

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>
This commit is contained in:
2026-06-11 01:52:26 -04:00
parent 743b026b01
commit 8d904b1b8f
9 changed files with 207 additions and 26 deletions
+31 -6
View File
@@ -984,19 +984,44 @@ class WireGuardManager(BaseServiceManager):
pass
return ip
def check_port_open(self, port: int = None) -> bool:
"""Check if WireGuard is running and listening on the configured UDP port."""
configured_port = port if port is not None else self._get_configured_port()
# Primary: verify wg0 is up AND listening on the configured port
def _kernel_listening_port(self) -> Optional[int]:
"""Return the UDP port wg0 is actually bound to per `wg show`, or None.
This reads the live kernel state, which is the source of truth for what
port traffic must reach — it may differ from wg0.conf's ListenPort if the
container has not been recreated since the port was changed.
"""
try:
result = subprocess.run(
['docker', 'exec', 'cell-wireguard', 'wg', 'show', 'wg0'],
capture_output=True, text=True, timeout=5,
)
if result.returncode == 0 and f'listening port: {configured_port}' in result.stdout.lower():
return True
if result.returncode != 0:
return None
for line in result.stdout.lower().splitlines():
line = line.strip()
if line.startswith('listening port:'):
try:
return int(line.split(':', 1)[1].strip())
except (ValueError, IndexError):
return None
except Exception:
pass
return None
def check_port_open(self, port: int = None) -> bool:
"""True when WireGuard is up and bound to a UDP port (reachable).
This is a liveness check, not a strict equality check against the
configured port: an interface that is up with a `listening port:` line
is serving traffic on that bound port. The bound port may differ from
wg0.conf's ListenPort if the container has not yet been recreated — that
is surfaced separately via the endpoint's actual-port field, not by
reporting the port closed.
"""
# Primary: wg0 is up and has a listening port → reachable on that port.
if self._kernel_listening_port() is not None:
return True
# Fallback: recent peer handshake confirms external reachability
try:
statuses = self.get_all_peer_statuses()