feat: connectivity redesign phase 3+4 — per-connection health, per-peer fallback, connection CRUD API
Unit Tests / test (push) Successful in 13m15s

Health probes (probe_health/refresh_health) are type-aware: WireGuard
checks the last WG handshake timestamp, OpenVPN checks the tun/tap
interface, Tor checks the control-port GETINFO, and sshuttle/proxy
types do a TCP reachability probe to the remote endpoint. Results are
persisted via set_connection_status and wired into the health_monitor_loop
so the UI always has a current health snapshot without polling.

Per-peer fail-open semantics: VPN, SSH, and proxy connections default to
fail-closed (kill-switch stays active even when the tunnel is down).
Tor defaults to fail-open. The default can be overridden per-peer via
set_peer_failopen/effective_failopen. apply_routes skips the fwmark and
kill-switch rules for any fail-open peer whose connection health is not
"working", letting traffic fall back to direct routing transparently.

New generic admin-only connection CRUD endpoints (GET/POST/PUT/DELETE
/api/connectivity/connections, GET /<id>/health, PUT
/api/connectivity/peers/<peer>/failopen) are guarded by the existing
admin role check. connection.create, connection.update, connection.delete,
and peer.failopen are all registered in ROUTE_ACTION_MAP for the audit
hook so every change is recorded in the owner-visible change log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 21:50:45 -04:00
parent 8b50fb1036
commit d39c091cec
6 changed files with 1249 additions and 2 deletions
+33
View File
@@ -110,6 +110,39 @@ def test_unmapped_mutating_endpoint_gets_generic_action(auth_mgr, audit_mgr):
assert match[0]['target_type'] == 'unknown'
# ── connectivity v2 connection routes are audited ─────────────────────────────
def test_connection_create_audited(auth_mgr, audit_mgr):
with _client(auth_mgr, audit_mgr, login_as='admin') as c:
with patch('app.connectivity_manager') as cm:
cm.create_connection.return_value = {'ok': True, 'connection': {'id': 'c'}}
c.post('/api/connectivity/connections',
json={'type': 'tor', 'name': 'T'})
res = audit_mgr.query({'action': 'connection.create'})
assert res['total'] >= 1
assert res['entries'][0]['target_type'] == 'connection'
def test_connection_delete_audited_with_id(auth_mgr, audit_mgr):
with _client(auth_mgr, audit_mgr, login_as='admin') as c:
with patch('app.connectivity_manager') as cm:
cm.delete_connection.return_value = {'ok': True}
c.delete('/api/connectivity/connections/conn_abc')
res = audit_mgr.query({'action': 'connection.delete'})
assert res['total'] >= 1
assert res['entries'][0]['target_id'] == 'conn_abc'
def test_peer_failopen_audited(auth_mgr, audit_mgr):
with _client(auth_mgr, audit_mgr, login_as='admin') as c:
with patch('app.connectivity_manager') as cm:
cm.set_peer_failopen.return_value = {'ok': True, 'peer': 'bob'}
c.put('/api/connectivity/peers/bob/failopen', json={'failopen': True})
res = audit_mgr.query({'action': 'peer.failopen'})
assert res['total'] >= 1
assert res['entries'][0]['target_id'] == 'bob'
# ── auth routes: never write password ─────────────────────────────────────────
def test_change_password_audited_without_value(auth_mgr, audit_mgr):