feat(session): every reconnect starts a fresh session
Android CI / android (push) Canceled after 0s

SOCKS5 Reconnect re-attached to the session the device already held. It
was the only path where a session began without an ad and without a
nonce binding it to one, and supporting it cost three parameters -
resume, release, resetSupportClaim - plus a carve-out in rule 4 that let
_fetchActiveSession accept whatever nonce the server happened to return.

It also wasn't the kindness it looked like. _handle_main calls
create_or_replace with a fresh full window, so a new session hands the
user *more* time, not less; re-attaching only preserved the remainder of
a session whose proxy had just died.

Reconnect now disconnects and connects again exactly as the button does,
ad included. All four special cases are deleted and the nonce check is
unconditional.

Costs one rewarded ad to repair an orphaned proxy once ads are enabled.
esef's call, and it buys a single way to start a session.

Also fixes the 3.3.7 regression where Reconnect released the session and
then asked for it back: there is no longer a path that does either.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
esefxdzandClaude Opus 5 committed 2026-10-17 18:53:42 +03:00
1 parent 73d76c04f2
commit 571c07bca8
7 files changed
+75 -51

No files matched your search

+9 -13
View File
@@ -146,14 +146,13 @@ class HivemindService {
_expectedNonce = null;
}
// Fetches this device's VLESS URL.
// adNonce — wait for the session that ad created (SSV can lag, so poll longer)
// resume — re-attach to the session already held; any nonce accepted
// neither — ads off: mint a nonce and fire the bypass (rule 87)
// Fetches this device's VLESS URL. Every session starts the same way, so
// there is always a nonce to bind it to: either the one the ad minted, or one
// we mint here before firing the bypass (rule 87). The re-attach path that
// used to accept any nonce is gone - see rule 4.
static Future<String> fetchConfigDirectly({
void Function(int attempt, int total)? onAttempt,
String? adNonce,
bool resume = false,
}) async {
// Capture the generation before the first await. Otherwise a disconnect
// that lands while secure storage is resolving the device ID can be lost
@@ -162,11 +161,11 @@ class HivemindService {
final deviceId = await CryptoService.getDeviceId();
_throwIfCancelled(callId);
final nonce = resume ? null : (adNonce ?? newNonce());
final nonce = adNonce ?? newNonce();
_expectedNonce = nonce;
if (!resume && adNonce == null) {
await _runConfiguredBypass(deviceId, nonce!);
if (adNonce == null) {
await _runConfiguredBypass(deviceId, nonce);
_throwIfCancelled(callId);
}
@@ -229,10 +228,9 @@ class HivemindService {
}
}
// nonce == null accepts whatever nonce the server holds (resume path).
static Future<_HivemindSessionConfig?> _fetchActiveSession(
String deviceId,
String? nonce,
String nonce,
) async {
final statusUri = _publicUrl('/session/status').replace(
queryParameters: {'device_id': deviceId},
@@ -245,9 +243,7 @@ class HivemindService {
final serverNonce = decoded['nonce'];
if (_expectedNonce != nonce) return null;
if (nonce != null && (serverNonce is! String || serverNonce != nonce)) {
return null;
}
if (serverNonce is! String || serverNonce != nonce) return null;
if (decoded['active'] != true || decoded['vless_uuid'] == null) {
return null;