Compare commits
1
Commits
v3.3.8
..
62e968f133
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62e968f133 |
No files matched your search
@@ -5,6 +5,19 @@ Versions follow [semver](https://semver.org/): MAJOR.MINOR.PATCH
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Removed
|
||||
- **The background server check while disconnected.** The app no longer contacts
|
||||
the server every 30 seconds while you are not connected to it. Nothing about
|
||||
the connected session changes.
|
||||
|
||||
### Changed
|
||||
- **The status bar no longer guesses whether the server is up.** It reads
|
||||
"Ready" while disconnected and shows "Online" once a session has synced, so a
|
||||
green dot now means a session that is actually working rather than a
|
||||
reachability check made beforehand.
|
||||
|
||||
## [3.3.8] — 2026-10-17
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -49,15 +49,14 @@ class StatusBar extends StatelessWidget {
|
||||
builder: (context, vpn, timer, _) {
|
||||
final isConnected = vpn.status == VpnStatus.connected;
|
||||
final isConnecting = vpn.status == VpnStatus.connecting;
|
||||
final serverHealthy =
|
||||
vpn.serverReachable || (isConnected && timer.hasSyncedOnce);
|
||||
final sessionOrTunnelHealthy = timer.hasSyncedOnce || vpn.serverReachable;
|
||||
// Rule 60: reachability is known only from a session sync that
|
||||
// succeeded, never from a probe. While disconnected the app knows
|
||||
// nothing about the server, so it claims nothing either way.
|
||||
final serverHealthy = isConnected && timer.hasSyncedOnce;
|
||||
|
||||
final statusLabel = isConnected
|
||||
? (sessionOrTunnelHealthy ? 'Online' : 'Syncing…')
|
||||
: (isConnecting
|
||||
? 'Connecting…'
|
||||
: (vpn.serverReachable ? 'Ready' : 'Offline'));
|
||||
? (timer.hasSyncedOnce ? 'Online' : 'Syncing…')
|
||||
: (isConnecting ? 'Connecting…' : 'Ready');
|
||||
|
||||
final detail = _detail(vpn, timer);
|
||||
|
||||
|
||||
@@ -197,18 +197,6 @@ class HivemindService {
|
||||
throw Exception('Session not activated. Server callback may have timed out.');
|
||||
}
|
||||
|
||||
static Future<bool> checkHealth() async {
|
||||
try {
|
||||
final response = await controlGet(
|
||||
_publicUrl('/health'),
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
return response.statusCode == 200;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static String newNonce() {
|
||||
final random = Random.secure();
|
||||
final high = random.nextInt(0x7FFFFFFF);
|
||||
|
||||
@@ -38,9 +38,6 @@ class VpnConnection extends ChangeNotifier {
|
||||
bool _adoptedRunningRuntime = false;
|
||||
bool get adoptedRunningRuntime => _adoptedRunningRuntime;
|
||||
|
||||
bool _serverReachable = false;
|
||||
bool get serverReachable => _serverReachable;
|
||||
|
||||
ConnectionMode _activeMode = ConnectionMode.tun;
|
||||
ConnectionMode get activeMode => _activeMode;
|
||||
|
||||
@@ -73,7 +70,6 @@ class VpnConnection extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Timer? _healthTimer;
|
||||
StreamSubscription<NetworkSnapshot>? _networkSubscription;
|
||||
|
||||
SecureSocksSession? _lastSecureSocks;
|
||||
@@ -142,11 +138,6 @@ class VpnConnection extends ChangeNotifier {
|
||||
},
|
||||
);
|
||||
|
||||
unawaited(_checkHealth());
|
||||
_healthTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
unawaited(_checkHealth());
|
||||
});
|
||||
|
||||
try {
|
||||
final coreVersion = await _vless.getCoreVersion();
|
||||
if (!_disposed) {
|
||||
@@ -472,14 +463,6 @@ class VpnConnection extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _checkHealth() async {
|
||||
if (_disposed || _status == VpnStatus.connected) return;
|
||||
final reachable = await HivemindService.checkHealth();
|
||||
if (_disposed || _status == VpnStatus.connected) return;
|
||||
_serverReachable = reachable;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
@@ -487,7 +470,6 @@ class VpnConnection extends ChangeNotifier {
|
||||
_suppressNativeConnect = true;
|
||||
_userDisconnecting = true;
|
||||
HivemindService.cancel();
|
||||
_healthTimer?.cancel();
|
||||
_networkSubscription?.cancel();
|
||||
// Provider/UI disposal is not a user disconnect command. Keeping teardown
|
||||
// in disconnect() prevents lifecycle churn from silently dropping the VPN.
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user