Skip to content

Handle external VPN revocation policy#638

Merged
kasnder merged 1 commit into
masterfrom
agent/vpn-revoke-policy
Jul 12, 2026
Merged

Handle external VPN revocation policy#638
kasnder merged 1 commit into
masterfrom
agent/vpn-revoke-policy

Conversation

@kasnder

@kasnder kasnder commented Jul 11, 2026

Copy link
Copy Markdown
Member

What changed

  • Preserve TrackerControl's enabled state when Android still identifies it as the always-on VPN, allowing recovery from transient system teardowns.
  • Disable watchdog recovery when Android positively reports that TrackerControl is no longer always-on, so a system-settings disable or replacement VPN is respected.
  • Treat an unavailable pre-Android 10 always-on setting as unknown and keep the existing state.
  • Add focused truth-table tests for the policy.

Why this is separate

This lifecycle behavior was split out of #617 because correctness depends on Android's onRevoke() / always-on-state timing and should be validated independently on real devices.

Validation

  • ./gradlew testGithubDebugUnitTest --tests eu.faircode.netguard.VpnRevokePolicyTest

Device validation still needed

  1. A transient revoke while TrackerControl remains configured as always-on recovers.
  2. Disabling TrackerControl in system VPN settings does not get reclaimed by the watchdog.
  3. Replacing TrackerControl with another VPN does not get reclaimed by the watchdog.

@kasnder

kasnder commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

Local emulator test results

Tested the GitHub debug build from this PR on the local Small_Phone AVD running Android 17 / API 37.

Passed: deliberate disable in Android VPN settings

With TrackerControl active and configured as always-on, I disabled Always-on VPN through Android's real VPN settings UI.

Observed:

  • Android invoked ServiceSinkhole.onRevoke().
  • The service logged Revoke always-on=false enabled=false.
  • The persisted enabled preference changed to false.
  • The native tunnel stopped and ServiceSinkhole was destroyed.
  • TrackerControl did not reclaim the VPN afterward.

This validates the safety-critical direction: a user disabling TrackerControl in system settings is respected rather than being undone by recovery/watchdog behavior.

Passed with limitation: recovery while always-on remains configured

I re-enabled TrackerControl and Android's always-on setting, then induced a controlled crash of the debuggable app process.

Observed:

  • Android retained TrackerControl as the configured always-on VPN.
  • The persisted enabled preference remained true.
  • Reopening TrackerControl re-created ServiceSinkhole and successfully re-established the VPN/tunnel.

The emulator did not automatically restart the crashed app, so this confirms that the preserved state permits recovery, but not that Android itself will always trigger that recovery immediately.

Not reproducible in this emulator setup

The emulator exposes no shell command that produces a genuine transient onRevoke() callback while leaving isAlwaysOn() == true. Therefore the central Android timing assumption—what isAlwaysOn() reports during an OS-internal teardown/re-auth callback—was not directly verified.

Replacement by a second VPN was also not exercised because the AVD has no other VPN app installed.

Conclusion

The deliberate-disable path behaves correctly, and recovery remains possible when always-on state is retained. The PR should remain draft until the transient-revoke timing is observed on a physical device (or reproduced with a dedicated second/test VPN).

@kasnder kasnder marked this pull request as ready for review July 12, 2026 09:01
@kasnder kasnder merged commit 80db76b into master Jul 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant