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
|
## [3.3.8] — 2026-10-17
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
# Privacy Policy
|
# Privacy Policy
|
||||||
|
|
||||||
**Last updated:** September 2026
|
**Last updated:** October 2026
|
||||||
|
|
||||||
ReVoltVPN is a free VPN service built and operated by ReVolt Team.
|
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:
|
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).
|
- 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
|
## What our server sees
|
||||||
@@ -27,14 +27,14 @@ Support ads (the "Support us" button) extend an active session by 30 minutes and
|
|||||||
|
|
||||||
## Operational logs
|
## 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
|
## What we do not collect
|
||||||
|
|
||||||
- We do not collect your name, email, or any personal information.
|
- 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 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 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
|
## Ads & Consent
|
||||||
|
|
||||||
|
|||||||
@@ -49,15 +49,14 @@ class StatusBar extends StatelessWidget {
|
|||||||
builder: (context, vpn, timer, _) {
|
builder: (context, vpn, timer, _) {
|
||||||
final isConnected = vpn.status == VpnStatus.connected;
|
final isConnected = vpn.status == VpnStatus.connected;
|
||||||
final isConnecting = vpn.status == VpnStatus.connecting;
|
final isConnecting = vpn.status == VpnStatus.connecting;
|
||||||
final serverHealthy =
|
// Rule 60: reachability is known only from a session sync that
|
||||||
vpn.serverReachable || (isConnected && timer.hasSyncedOnce);
|
// succeeded, never from a probe. While disconnected the app knows
|
||||||
final sessionOrTunnelHealthy = timer.hasSyncedOnce || vpn.serverReachable;
|
// nothing about the server, so it claims nothing either way.
|
||||||
|
final serverHealthy = isConnected && timer.hasSyncedOnce;
|
||||||
|
|
||||||
final statusLabel = isConnected
|
final statusLabel = isConnected
|
||||||
? (sessionOrTunnelHealthy ? 'Online' : 'Syncing…')
|
? (timer.hasSyncedOnce ? 'Online' : 'Syncing…')
|
||||||
: (isConnecting
|
: (isConnecting ? 'Connecting…' : 'Ready');
|
||||||
? 'Connecting…'
|
|
||||||
: (vpn.serverReachable ? 'Ready' : 'Offline'));
|
|
||||||
|
|
||||||
final detail = _detail(vpn, timer);
|
final detail = _detail(vpn, timer);
|
||||||
|
|
||||||
|
|||||||
@@ -197,18 +197,6 @@ class HivemindService {
|
|||||||
throw Exception('Session not activated. Server callback may have timed out.');
|
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() {
|
static String newNonce() {
|
||||||
final random = Random.secure();
|
final random = Random.secure();
|
||||||
final high = random.nextInt(0x7FFFFFFF);
|
final high = random.nextInt(0x7FFFFFFF);
|
||||||
|
|||||||
@@ -38,9 +38,6 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
bool _adoptedRunningRuntime = false;
|
bool _adoptedRunningRuntime = false;
|
||||||
bool get adoptedRunningRuntime => _adoptedRunningRuntime;
|
bool get adoptedRunningRuntime => _adoptedRunningRuntime;
|
||||||
|
|
||||||
bool _serverReachable = false;
|
|
||||||
bool get serverReachable => _serverReachable;
|
|
||||||
|
|
||||||
ConnectionMode _activeMode = ConnectionMode.tun;
|
ConnectionMode _activeMode = ConnectionMode.tun;
|
||||||
ConnectionMode get activeMode => _activeMode;
|
ConnectionMode get activeMode => _activeMode;
|
||||||
|
|
||||||
@@ -73,7 +70,6 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer? _healthTimer;
|
|
||||||
StreamSubscription<NetworkSnapshot>? _networkSubscription;
|
StreamSubscription<NetworkSnapshot>? _networkSubscription;
|
||||||
|
|
||||||
SecureSocksSession? _lastSecureSocks;
|
SecureSocksSession? _lastSecureSocks;
|
||||||
@@ -142,11 +138,6 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
unawaited(_checkHealth());
|
|
||||||
_healthTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
|
||||||
unawaited(_checkHealth());
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final coreVersion = await _vless.getCoreVersion();
|
final coreVersion = await _vless.getCoreVersion();
|
||||||
if (!_disposed) {
|
if (!_disposed) {
|
||||||
@@ -472,14 +463,6 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
notifyListeners();
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
@@ -487,7 +470,6 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
_suppressNativeConnect = true;
|
_suppressNativeConnect = true;
|
||||||
_userDisconnecting = true;
|
_userDisconnecting = true;
|
||||||
HivemindService.cancel();
|
HivemindService.cancel();
|
||||||
_healthTimer?.cancel();
|
|
||||||
_networkSubscription?.cancel();
|
_networkSubscription?.cancel();
|
||||||
// Provider/UI disposal is not a user disconnect command. Keeping teardown
|
// Provider/UI disposal is not a user disconnect command. Keeping teardown
|
||||||
// in disconnect() prevents lifecycle churn from silently dropping the VPN.
|
// in disconnect() prevents lifecycle churn from silently dropping the VPN.
|
||||||
|
|||||||
@@ -164,4 +164,24 @@ void main() {
|
|||||||
reason: 'the nonce check must not be skippable',
|
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