Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f28379f73f | ||
|
|
8260c7cc0a | ||
|
|
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
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
# Privacy Policy
|
||||
|
||||
**Last updated:** September 2026
|
||||
**Last updated:** October 2026
|
||||
|
||||
ReVoltVPN is a free VPN service built and operated by ReVolt Team.
|
||||
|
||||
@@ -12,7 +12,7 @@ When you first open the app, we show a brief summary of this policy before you c
|
||||
|
||||
We collect the minimum required to operate the service:
|
||||
|
||||
- A randomly generated device ID created on your phone. This is not linked to your identity in any way.
|
||||
- A randomly generated device ID created on your phone. It is stored on your device and reused between sessions, and it is not linked to your identity in any way.
|
||||
- Your connection duration and data usage for the current session, linked to your device ID. This is used solely to enforce the per-session quota (see below).
|
||||
|
||||
## What our server sees
|
||||
@@ -27,14 +27,14 @@ Support ads (the "Support us" button) extend an active session by 30 minutes and
|
||||
|
||||
## Operational logs
|
||||
|
||||
Server logs are written to the systemd journal (`journalctl -u hivemind`). These logs include truncated device IDs (first 8 characters only), session start/stop events, and error messages. Logs stay on the server and are accessible only to the server operator via local console; retention follows the server's systemd journal settings. We also maintain an aggregate total of all data ever transferred (in GB) for capacity planning — this counter is not tied to any individual device.
|
||||
Server logs are written to the system journal. These logs include truncated device IDs (first 8 characters only), session start/stop events, and error messages. Logs stay on the server and are accessible only to the server operator via local console; retention follows the server's journal settings. We also maintain an aggregate total of all data ever transferred (in GB) for capacity planning — this counter is not tied to any individual device.
|
||||
|
||||
## What we do not collect
|
||||
|
||||
- We do not collect your name, email, or any personal information.
|
||||
- We do not log your internet traffic, browsing history, DNS queries, or destination IPs.
|
||||
- We do not inspect or store the content of your traffic.
|
||||
- We do not sell or share any data with third parties.
|
||||
- We do not sell your data. We do not share it with anyone, apart from what Google's ad SDK collects for ad delivery — see “Ads & Consent” below.
|
||||
|
||||
## Ads & Consent
|
||||
|
||||
|
||||
@@ -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