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.
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
NetworkStatsManagersource 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.
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.
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
| 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.
| 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.
| 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).
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.
No. NetLocker never requests root and contains no root-only code path.
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.
- 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
VpnServiceat 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).
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,RuleIndexuid→rule snapshot building)gradle assembleDebug— succeeds, producing a real, installableapp-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):
- Kotlin backtick test names containing
->are illegal JVM method names (>isn't allowed) — renamed to plain English. - Two test files each declared a top-level
private class FakeNetworkRuleRepositoryin the same package — Kotlin/JVM still requires unique class names per package regardless of theprivatemodifier, causing a redeclaration clash. Renamed one. - A
backgroundScope-launched flow collector was not being flushed byadvanceUntilIdle()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.
- Android Studio (a recent stable release with AGP 8.7+/Kotlin 2.1 support).
- Open the project root (
E:\AI Project\NetLocker) — it's a standard Gradle project, no special setup. gradle/wrapper/gradle-wrapper.properties(pointing at Gradle 8.9) is included, but the wrapper's binary jar/gradlew/gradlew.batlauncher 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, rungradle wrapperonce from a system-installed Gradle to create it.- Let Gradle sync (it will fetch the AndroidX/Compose/Room dependencies declared in
gradle/libs.versions.toml). - 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.
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 correctNetworkAccessState(spec Tests 1–4)ChecksumTest,ParsedPacketTest— IPv4/TCP/UDP header build+parse round-trips and the RFC 1071 checksum self-consistency propertyRuleIndexTest— uid→rule snapshot building and live updatesObserveAppsWithRulesUseCaseTest,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.
- 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_COMPLETEDreceiver to silently restart a VPN without you present, matching the "no silent permission use" rule). - App restart test: kill NetLocker from Recents, reopen, confirm rules and firewall status are exactly as left.
- 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.
- 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.
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.