fix(app): stop probing the control plane while disconnected
Android CI / android (push) Canceled after 0s

VpnConnection._init() started a 30 s /health poll from the constructor
with no lifecycle gating, so it ran for the life of the app process -
backgrounded, and whether or not the user ever connected. The app is
excluded from its own tunnel, so those requests left in the clear. A
periodic request that fires while the user is deliberately not connected
is an avoidable fingerprint, and rule 60 already ruled probes out:
reachability is meant to be passive.

All it bought was the status dot before the first connect, and it got
that wrong in both directions - "Offline" on a transient resolver
failure against a server that was fine.

Reachability now comes from a session sync that succeeded. serverHealthy
is isConnected && hasSyncedOnce, so a green dot means a working session
rather than a check made beforehand; while disconnected the bar reads
"Ready" and claims nothing either way.

checkHealth() goes with it. Nothing calls it, and leaving a probe helper
in place invites its reuse.

Pinned in test/session_lifecycle_regression_test.dart so it cannot come
back as a tidy-up. flutter analyze clean, 10/10 tests pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
esefxdzandClaude Opus 5 committed 2026-10-17 21:17:17 +03:00
1 parent 571c07bca8
commit 62e968f133
5 files changed
+39 -37

No files matched your search

@@ -164,4 +164,24 @@ void main() {
reason: 'the nonce check must not be skippable',
);
});
test('nothing probes the control plane while disconnected - rule 60', () {
final vpn = File('lib/logic/vpn_connection.dart').readAsStringSync();
final hivemind = File('lib/logic/hivemind_service.dart').readAsStringSync();
final bar = File('lib/components/status_bar.dart').readAsStringSync();
// The runtime excludes the app from its own tunnel
// (XrayVPNService.kt addDisallowedApplication), so a periodic /health probe
// was a cleartext-SNI metronome to the API host - which resolves to the
// tunnel IP. It fed step 1 of the kill chain in KNOWN_SECURITY_ISSUES #3,
// in exchange for a green dot before the user had connected to anything.
expect(vpn, isNot(contains('_healthTimer')));
expect(vpn, isNot(contains('_checkHealth')));
expect(hivemind, isNot(contains('checkHealth')));
expect(hivemind, isNot(contains("'/health'")));
// Reachability is passive now: a session sync that succeeded, nothing else.
expect(bar, isNot(contains('serverReachable')));
expect(bar, contains('isConnected && timer.hasSyncedOnce'));
});
}