Skip to content

Latest commit

 

History

19 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NetLocker

A per-app Wi-Fi / Mobile Data firewall for Android — no root, no Shizuku, no remote server. You choose, per installed app, whether it can use Wi-Fi, Mobile Data, both, or neither, and NetLocker enforces it live.

This README leads with the honest technical story (what's actually possible on stock Android/Samsung, and why), because that's what the rest of the code is built around.


1. What NetLocker is

NetLocker lets you individually allow/block Wi-Fi and Mobile Data for every installed app, without rooting the phone or installing Shizuku/ADB tooling. You get:

  • Apps tab — every installed app with quick Wi-Fi/Mobile-Data switches, category chips (All / Games / Social / System), search, a sort menu (Name A–Z/Z–A, Restricted first, Most/Least data used today — the data-usage sorts need the same "Usage access" permission as App Details and prompt for it the first time), and a loading state while the first scan of installed apps finishes
  • Rules tab — only the apps you have a rule for, with status filters (Blocked / Wi-Fi Only / Mobile Only / Allowed), search, and per-rule Edit / Disable / Delete. Add a rule from a picker of apps that don't have one yet, filterable by the same category chips as the Apps tab
  • Data usage today (Apps tab's overflow menu) — every app that used Wi-Fi or Mobile Data today in one list, most-used first, with a totals card at the top. Same NetworkStatsManager source and "Usage access" permission as App Details and the Apps tab's usage sorts; the totals are a sum across identifiable installed apps, not a claim of the phone's literal system-wide total
  • A details screen per app showing its exact current access state
  • Rules that persist across app restarts and device reboots
  • Honest status reporting — a rule is never shown as "applied" unless it actually is: the firewall banner and each rule's "Enforced" / "Not enforced" label come from the real VPN state, not from what is saved

Apps and Rules share one Room table, so a switch flipped on either tab appears on the other immediately. A rule exists for an app if it has a row; deleting the row returns the app to default (fully allowed). Disabling a rule keeps its saved Wi-Fi/Mobile values but stops enforcing it — the app behaves exactly as if it had no rule (verified on a real device: a disabled "Blocked" rule produced zero firewall decisions and the app went online; re-enabling it resumed dropping its traffic).

The database is at schema v7 (v2 added isEnabled and createdAt; v3 adds the blocked_stats table; v4 adds the per-rule schedule window; v5 adds which days of the week it applies to; v6 splits DNS lookups out of the blocked-attempt count; v7 adds the optional blocked-destination log). Upgrading keeps every saved rule via explicit migrations — there is deliberately no destructive fallback, since silently wiping a firewall's rules would be a security regression.

Schedule. Settings has an off-by-default "Schedule" master switch; only while it's on does each rule show a "Schedule" section (Edit Rule, Add Rule, App Details) to block that app during a time window, optionally repeating only on chosen days (default: every day). Turning the master off again doesn't delete any app's schedule, it just stops enforcing it — the same "paused, not lost" pattern as a disabled rule. An app with a live schedule stays inside the VPN tunnel even outside its block window (it can't use the zero-overhead excluded-app path), so the small relay cost is paid only by apps a user opts into scheduling.

Blocked attempts. While the firewall runs, each connection a rule blocks is counted per app per day and shown on the Rules cards ("N connections blocked today") and App Details, split into connections and DNS lookups since a busy blocked app's DNS retries can vastly outnumber its actual connection attempts. A "blocked attempt" is one new flow (TCP SYN / UDP flow): retransmits of the same flow are counted once per 30 seconds, so a retrying app doesn't inflate the number. Only flows attributed to an app with a restrictive rule are counted; counts are kept for 7 days.

Blocked destinations (optional, off by default). Settings has a "Show blocked destinations" switch; while it's on, each blocked attempt's destination address is logged and can be viewed ("View recent attempts") on the app's details page — up to 50 per app, kept for 7 days. This is sensitive (close to a connection log), so nothing is written unless the switch is on, and turning it back off deletes everything already logged immediately rather than just hiding it.

Keeping the firewall running. A Quick Settings tile toggles the firewall (it reflects the real status, and opens the app if the VPN permission hasn't been granted yet). Settings' "Quick Settings tile" card shows "Added" once it's actually in the panel and an "Add tile" button otherwise — driven only by real signals (TileService.onTileAdded/onTileRemoved, and the result of requestAddTileService), never assumed (verified on-device: adding the tile flips the card to "Added", and the tile is confirmed genuinely present by opening the Quick Settings panel itself). The optional "Start when phone turns on" setting restarts it after a reboot or an app update, only if the VPN permission is still granted. Android's own "Always-on VPN" is linked from Settings but has not been verified with NetLocker on a device. Settings' "Battery optimization" card shows whether NetLocker is currently exempt from Android's Doze/App Standby battery management (verified on a real Samsung device: tapping "Allow unrestricted battery use" shows the real system dialog, and confirms exempt via dumpsys deviceidle whitelist afterwards) and links to the phone's own app-battery settings for whatever extra, manufacturer-specific restrictions Android's own API doesn't cover (the card's wording is deliberately phone-brand-neutral, since NetLocker runs on more than one manufacturer's devices).

What's new. Right after an update, NetLocker shows the new version's release notes once (recorded as "seen" so it never repeats for the same version, and a fresh install never shows anything — there's nothing to announce yet). Settings → About NetLocker has a "What's new" row to see them again anytime. Both reuse the same GitHub Releases endpoint checkForUpdate() already calls, fetching the real published notes — never invented text — and a silent failure (no connection) just means it quietly retries on the next launch rather than showing an unprompted error.

Clear cache. Settings has a "Clear cache" card with a "Clear app cache" button that deletes the contents of NetLocker's own cacheDir (verified on-device: writing a known-size file into the cache directory and clearing it reports the exact size freed, and the file is gone afterwards). It never touches the Room database or DataStore preferences, so rules and settings are unaffected — verified by clearing the cache and confirming all saved rules were still present afterwards. A second row opens the app's own App Info page, since Android has no direct deep-link into the "Storage" sub-screen where the full system-level "Clear data"/"Clear cache" buttons live.

2. Why this needs a local VPN (read this before anything else)

Android sandboxes every app into its own UID. The only APIs that can change another app's network permissions (NetworkPolicyManager.setUidPolicy, iptables/netd rules) require a signature-level permission (MANAGE_NETWORK_POLICY) or root — neither of which a normal, sideloaded/Play app can obtain. This is a deliberate Android security boundary, not an oversight, and there is no way around it without root or an MDM Device-Owner provisioning flow (which requires a factory reset to set up — impractical on a personal phone).

The one public, documented API that lets a third-party app see and gate other apps' traffic without root is VpnService. NetLocker uses it as a local firewall: it builds a virtual network interface on the device, and for apps it needs to restrict, it inspects their packets and decides per-packet whether to forward them — all on-device, with no remote server, no data ever leaving the phone.

Consequence you cannot avoid on stock Android: the OS will show the standard 🔑 "VPN connected" status-bar icon and system notification whenever NetLocker's firewall is active. This is not a bug and cannot be hidden — it's how Android tells the user any app is intercepting traffic, and that's the correct, honest signal for what NetLocker is doing.

Apps you leave fully allowed (Wi-Fi + Mobile Data both on) never enter the tunnel at all — they're excluded via VpnService.Builder.addDisallowedApplication(), so their traffic is completely unaffected, at native speed, with zero NetLocker overhead.

3. Architecture actually used

NetLocker
 ├── ui/            Jetpack Compose (Material 3) screens + ViewModels
 ├── domain/        Models, repository interfaces, use cases (pure Kotlin)
 ├── data/          Room (rules) + PackageManager (installed apps)
 └── network/       The firewall itself
      ├── NetLockerVpnService   the VpnService, foreground notification, tunnel lifecycle
      ├── FirewallEngine        reads the tun device, makes the allow/drop decision
      ├── TransportMonitor      tracks live Wi-Fi/Cellular Network handles independently
      ├── ConnectionOwnerResolver  packet -> owning app, via getConnectionOwnerUid()
      ├── RuleIndex             fast uid -> rule lookup for the packet hot path
      ├── packet/               IPv4/IPv6/TCP/UDP header parsing + checksums
      └── relay/                UDP + TCP session relay for partially-restricted apps

How a packet is actually handled

App's rule What happens Reliability
Wi-Fi ✓ + Mobile ✓ (fully allowed) Excluded from the tunnel entirely — normal OS routing, NetLocker never sees its traffic Guaranteed, zero risk
Wi-Fi ✕ + Mobile ✕ (fully blocked) Packets enter the tunnel and are simply never forwarded Guaranteed
Only one of Wi-Fi/Mobile allowed The flow is relayed through a real socket bound to that specific transport's Network object (via TransportMonitor + Network.bindSocket()); if that transport isn't currently up, it's blocked Best-effort — see §7

TransportMonitor holds independent Network handles for Wi-Fi and Cellular (via two parallel registerNetworkCallback requests), not just "whatever the phone currently defaults to". That means a "Wi-Fi-only" app really is routed over Wi-Fi specifically — even if, say, cellular happened to be the system default at that moment — as long as Wi-Fi is actually connected.

4. Supported Android versions / Samsung compatibility

Status
minSdk 29 (Android 10 / One UI 2.0+)
targetSdk / compileSdk 36 (Android 16)
Android 13, 14, 15, 16 Supported
Samsung One UI (2.0 and newer) Supported — standard AOSP APIs, not modified by One UI
Samsung battery management ("Put unused apps to sleep", Sleeping/Deep sleeping apps) Can kill the background firewall service. You must exclude NetLocker from battery optimization (Settings → Apps → NetLocker → Battery → Unrestricted) for reliable always-on enforcement. This is a Samsung OS behavior NetLocker cannot override from inside the app (documented widely at dontkillmyapp.com).
Knox-managed / enterprise-restricted devices If your organization's MDM sets DISALLOW_CONFIG_VPN, NetLocker detects this (VpnSupportChecker) and shows an honest "not supported on this device" screen instead of silently failing

Why minSdk 29: per-app UID attribution relies on ConnectivityManager.getConnectionOwnerUid() (API 29+). Below that, direct /proc/net inspection would be the only fallback, and that's been access-restricted for non-privileged apps since Android 11 anyway — there's no reliable non-root path below API 29.

5. Permissions required

Permission Why
BIND_VPN_SERVICE (system-granted via consent dialog, not requested in-app) The firewall tunnel itself
INTERNET, ACCESS_NETWORK_STATE Relaying traffic, watching Wi-Fi/Cellular availability
FOREGROUND_SERVICE, FOREGROUND_SERVICE_SPECIAL_USE Keeps the firewall alive in the background
POST_NOTIFICATIONS (Android 13+) Shows the "firewall active" status notification — requested explicitly, right before you enable the firewall, never silently
RECEIVE_BOOT_COMPLETED Only used by the optional "Start when phone turns on" setting
PACKAGE_USAGE_STATS ("Usage access", granted by you in system settings) Shows an app's data use today on its details page
QUERY_ALL_PACKAGES Lists all installed apps, not just ones NetLocker declares an intent filter for. This is a Google Play "sensitive permission" requiring a declaration form — see §9

NetLocker never requests a permission without a visible reason shown first (spec requirement: no silent permission requests, no fake success if one is denied).

6. VPN required?

Yes — see §2. There is no non-VPN, non-root, non-Shizuku way to do this on stock Android. NetLocker uses VpnService purely as a local packet gate; no VPN server, no remote endpoint, no traffic leaves your device for any reason.

7. Root required?

No. NetLocker never requests root and contains no root-only code path.

8. Shizuku required?

No. Shizuku/ADB access was evaluated (see the original feasibility discussion) and rejected as the primary path: it cannot express a "Wi-Fi off, Mobile Data on" rule (the relevant shell-level network-policy commands are metered-data-oriented, with no Wi-Fi equivalent), its exact command surface varies by Android version/OEM, and it requires non-trivial one-time setup (wireless debugging or a PC) that most users won't do. It remains a possible future optional enhancement, not something this build depends on.

9. Known limitations (read before relying on this in production)

  • The partial-restriction relay is best-effort, not a full TCP/IP stack. See network/relay/TcpNatSession.kt's doc comment for the exact scope: correct 3-way handshake/teardown and cumulative ACKs, but single-outstanding-segment ("stop-and-wait") flow control, no SACK, no window scaling, no reassembly buffer. Expect reduced throughput on large transfers for apps in "Wi-Fi only" or "Mobile Data only" mode. Fully-allowed and fully-blocked apps have none of these caveats.
  • IPv6 is relayed, but only for ordinary TCP/UDP. The tunnel carries both IPv4 and IPv6 (an IPv6-only destination used to be simply unreachable for a restricted app — not just unfiltered, genuinely broken; this is now fixed). The one remaining scope limit: a packet using an IPv6 extension header (Hop-by-Hop Options, Routing, Fragment, etc.) is detected and dropped for restricted apps rather than mis-handled, the same fail-closed treatment as an unrecognized protocol — see IPv6Packet's doc comment. Ordinary TCP/UDP traffic, the overwhelming majority of real apps, does not use these.
  • ICMP (ping) is not attributed — getConnectionOwnerUid() only supports TCP/UDP, so ICMP from an app inside the tunnel (partial/blocked apps) is dropped rather than guessed at.
  • The 🔑 VPN icon is unavoidable (see §2), and NetLocker cannot run alongside another VPN app — Android only allows one active VpnService at a time.
  • Samsung background-kill behavior (§4) can silently stop enforcement unless you disable battery optimization for NetLocker.
  • Google Play distribution is uncertain. Play's Developer Policy requires a "Prominent Disclosure" declaration for VPN-permission apps, and historically similar local-firewall apps (e.g. NetGuard) have not been distributed via Play, only via GitHub/F-Droid-style sideloading. Budget for this if Play distribution matters to you. Because of this, NetLocker ships its own Settings → Check for updates: it checks this repo's latest GitHub Release and, if newer, downloads the APK (DownloadManager) and hands it to the system Package Installer (ACTION_VIEW) — the same "Install unknown apps" consent and install confirmation screens you'd see installing any sideloaded APK by hand. No silent installation; Android does not allow that for a non-privileged app, and NetLocker doesn't try to.
  • Rule changes take effect immediately for the exact case that matters most (blocking/unblocking), including tearing down already-open connections for a just-restricted app (FirewallEngine.invalidateSessionsForUid) — but toggling an app into or out of "fully allowed" briefly rebuilds the whole tunnel (NetLockerVpnService.reconfigureAndEstablish), which very briefly interrupts other partially-restricted apps' in-flight relayed connections (fully-allowed apps are never affected, since their traffic never touches the tunnel).

Build verification (what has actually been run, not just written)

This project was built and tested end-to-end outside Android Studio, using a manually installed Android SDK (cmdline-tools, platform 36, build-tools 36.0.0) and Gradle 8.9:

  • gradle test — all unit tests pass (checksum/packet round-trips, rule logic, use-case combining, RuleIndex uid→rule snapshot building)
  • gradle assembleDebug — succeeds, producing a real, installable app-debug.apk (manifest merge, resource compilation, Room/KSP annotation processing, the Compose compiler, and D8 dexing all completed without error)

Two real bugs were found and fixed this way (not hypothetical — both reproduced with a real compiler/test run before being fixed):

  1. Kotlin backtick test names containing -> are illegal JVM method names (> isn't allowed) — renamed to plain English.
  2. Two test files each declared a top-level private class FakeNetworkRuleRepository in the same package — Kotlin/JVM still requires unique class names per package regardless of the private modifier, causing a redeclaration clash. Renamed one.
  3. A backgroundScope-launched flow collector was not being flushed by advanceUntilIdle() in this AGP/kotlinx-coroutines-test combination (confirmed with a minimal, isolated repro before assuming it was environment-specific rather than a real logic bug); runCurrent() reliably flushes it and was used instead.

What this does not verify: actual runtime behavior on a device or emulator — the VPN permission dialog, the tun interface actually intercepting traffic, per-app enforcement working against a real Wi-Fi/cellular radio, Samsung One UI's background behavior. None of that can be exercised without a real device/emulator, which this environment doesn't have. That's still on you — see the manual checklist below.

10. Build instructions

  1. Android Studio (a recent stable release with AGP 8.7+/Kotlin 2.1 support).
  2. Open the project root (E:\AI Project\NetLocker) — it's a standard Gradle project, no special setup.
  3. gradle/wrapper/gradle-wrapper.properties (pointing at Gradle 8.9) is included, but the wrapper's binary jar/gradlew/gradlew.bat launcher scripts are not — this repo was assembled outside Android Studio, which can't emit that binary. On first open, Android Studio will offer to generate/repair the wrapper automatically; if it doesn't, run gradle wrapper once from a system-installed Gradle to create it.
  4. Let Gradle sync (it will fetch the AndroidX/Compose/Room dependencies declared in gradle/libs.versions.toml).
  5. Run on a device or emulator running Android 10 (API 29) or newer. A real device is strongly recommended for anything network-related — see §11.

11. Testing instructions

Automated (run in Android Studio or ./gradlew test)

Pure-JVM unit tests cover the parts that don't need a real device/network stack:

  • NetworkRuleTest — the four Wi-Fi/Mobile-Data combinations map to the correct NetworkAccessState (spec Tests 1–4)
  • ChecksumTest, ParsedPacketTest — IPv4/TCP/UDP header build+parse round-trips and the RFC 1071 checksum self-consistency property
  • RuleIndexTest — uid→rule snapshot building and live updates
  • ObserveAppsWithRulesUseCaseTest, UpdateNetworkRuleUseCaseTest — app-list sorting/defaulting and the persist-then-notify-firewall flow

These were written and reasoned through carefully, but this conversation has no Android device/emulator to actually execute ./gradlew test against — please run them yourself and treat that as the real first checkpoint before trusting anything below.

Manual, on-device (spec Tests 5–8 — these need a real phone, not a unit test)

  1. Reboot test: set a rule, reboot the phone, confirm the rule is still shown correctly (Room persists it; note the firewall itself does not auto-start after reboot unless you open the app and re-grant/re-confirm — NetLocker does not use a BOOT_COMPLETED receiver to silently restart a VPN without you present, matching the "no silent permission use" rule).
  2. App restart test: kill NetLocker from Recents, reopen, confirm rules and firewall status are exactly as left.
  3. Network-switch test: with an app set to "Wi-Fi only", start a download, then turn off Wi-Fi mid-transfer — confirm it stops (falls back to blocked, not silently switching to mobile). Then reverse it for a "Mobile Data only" app.
  4. Samsung One UI test: verify the battery-optimization exemption prompt/flow works as described in §4, and that the firewall survives at least a few hours in the background with the screen off.

Project status

This is a from-scratch implementation, not a fork of an existing firewall — the enforcement engine (packet parsing, checksum, relay sessions) is original code written for this project, scoped and documented as described above.

About

Per-app Wi-Fi/Mobile Data firewall for Android — no root, no Shizuku. A local VpnService-based packet gate (no remote server) built with Kotlin, Compose, Room and MVVM/Clean Architecture.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages