{"id":49550438,"url":"https://github.com/kokosaaaa/netguard","last_synced_at":"2026-05-02T22:06:06.558Z","repository":{"id":350954932,"uuid":"1208918388","full_name":"KOKosaaaa/NetGuard","owner":"KOKosaaaa","description":"Android VPN client built for privacy, security, and stealth. Powered by xray-core.","archived":false,"fork":false,"pushed_at":"2026-04-30T22:13:10.000Z","size":494,"stargazers_count":10,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-01T00:15:43.829Z","etag":null,"topics":["android","anti-censorship","censorship-circumvention","hysteria2","kotlin","privacy","proxy","reality","shadowsocks","stealth-vpn","trojan","tun2socks","v2ray","vless","vmess","vpn","xray-core"],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/KOKosaaaa.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-12T22:59:42.000Z","updated_at":"2026-04-30T22:13:14.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/KOKosaaaa/NetGuard","commit_stats":null,"previous_names":["kokosaaaa/netguard"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/KOKosaaaa/NetGuard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KOKosaaaa%2FNetGuard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KOKosaaaa%2FNetGuard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KOKosaaaa%2FNetGuard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KOKosaaaa%2FNetGuard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/KOKosaaaa","download_url":"https://codeload.github.com/KOKosaaaa/NetGuard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KOKosaaaa%2FNetGuard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32550945,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-02T21:31:48.061Z","status":"ssl_error","status_checked_at":"2026-05-02T21:31:46.574Z","response_time":132,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","anti-censorship","censorship-circumvention","hysteria2","kotlin","privacy","proxy","reality","shadowsocks","stealth-vpn","trojan","tun2socks","v2ray","vless","vmess","vpn","xray-core"],"created_at":"2026-05-02T22:05:52.423Z","updated_at":"2026-05-02T22:06:06.550Z","avatar_url":"https://github.com/KOKosaaaa.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NetGuard\n\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)\n\nAndroid VPN client built for privacy, security, and stealth. Powered by xray-core.\n\nLicensed under the **Apache License, Version 2.0** — see [`LICENSE`](LICENSE). You are free to use, modify, distribute, and sublicense the code (including for commercial use), provided you preserve the copyright notice. The Apache-2.0 patent grant also protects downstream users from patent claims by contributors.\n\n## What makes NetGuard different\n\nMost VPN clients (v2rayNG, Hiddify, v2rayTUN) are great tools, but they share common weaknesses: credentials sitting on disk, open local SOCKS ports, IP leaks during network switches, and package names that scream \"VPN\" to any DPI system. NetGuard was designed to fix all of that.\n\n### Zero-leak network switching\n\nWhen you switch between WiFi and mobile data, typical clients tear down the entire VPN tunnel and reconnect from scratch. During that window your real IP leaks. NetGuard keeps the TUN interface alive and only restarts the internal xray + tun2socks processes. Packets are black-holed until the tunnel is back up — zero leak window.\n\n### Ephemeral authenticated SOCKS5\n\nEvery connection generates a fresh random port, a UUID username, and a 32-character random password for the local SOCKS5 bridge between tun2socks and xray. Credentials are cleared from memory on disconnect or network reconnect (they are kept alive during a session so that internal speed-test / service-test requests can reauthenticate through the HTTP bridge). Even if another app scans localhost ports, it cannot authenticate without the ephemeral password.\n\n### Config minimally exposed on disk\n\nThe xray JSON config (containing server credentials, UUIDs, passwords) is written to app-private internal storage only long enough for xray to read it, then immediately `unlink()`ed once the SOCKS inbound is up. No config.json is kept across sessions, and it is never visible to other apps. Note that on ext4/F2FS the underlying blocks may persist until overwritten — this is a brief-exposure design, not a never-on-disk one.\n\n### Built-in security self-test\n\n9 automated checks that run against your own device:\n\n- Open SOCKS5/HTTP proxy ports (10808, 1080, 8080, etc.)\n- Xray gRPC API exposure\n- Clash REST API exposure\n- `/proc/net/tcp` analysis for unexpected listeners\n- VPN transport flag detection\n- MTU informational report (non-decisive — see self-test details)\n- Package name stealth analysis\n\n### Evil Twin WiFi protection\n\nStores SSID+BSSID pairs for trusted WiFi networks. When connecting to a WiFi with a known SSID but unknown BSSID (possible Evil Twin attack), automatically enables VPN and sends a warning notification.\n\n### Service availability testing\n\nTest which services actually work through each server — YouTube, Telegram, Instagram, ChatGPT, Discord, Google, X/Twitter, Spotify. See a score like \"3/8\" before you connect.\n\n### Stealth branding\n\nPackage name `com.smarttools.netguard`, notification says \"Connection active / Network service is running\" — no mention of VPN or proxy anywhere visible to system-level inspection.\n\n## Features\n\n**Protocols:** VLESS (+ REALITY), VMess, Trojan, Shadowsocks, Hysteria2\n\n**Transports:** TCP, WebSocket, gRPC, HTTP/2, HTTP Upgrade, SplitHTTP, KCP, QUIC\n\n| Feature | Details |\n|---------|---------|\n| Connection Map | Animated world map showing user-to-server connection arc |\n| Speed Test | Download/upload/ping through VPN tunnel (OkHttp + raw SOCKS5) |\n| WiFi Auto-Connect | Auto-enable VPN on untrusted WiFi + Evil Twin detection |\n| Material You | Dynamic color theme on Android 12+ |\n| Per-app routing | Whitelist / Blacklist / Disabled |\n| Auto-select best server | TCP ping all servers, connect to fastest |\n| Subscription management | Auto-update via WorkManager (6/12/24/48h) |\n| QR code | Scan (ML Kit + CameraX) and generate (ZXing) |\n| Deep link import | `vless://`, `vmess://`, `trojan://`, `ss://`, `hy2://` |\n| Traffic stats | Real-time speed, session/daily/weekly/total counters |\n| Home screen widget | One-tap connect/disconnect |\n| Quick Settings tile | Android 7.0+ notification panel toggle |\n| Boot auto-connect | Reconnect to last server on device restart |\n| DNS | Custom primary/secondary, optional DoH through proxy |\n| Routing modes | Global proxy / Rule-based (RU direct) / Direct |\n| LAN bypass | Access local network devices while connected |\n| Themes | Dark, Light, OLED Black, Ocean, Dynamic (Material You) |\n| Languages | 17 languages |\n| Backup/Restore | Export/import full config as JSON |\n| Log viewer | Real-time xray logs with auto-redaction of credentials |\n\n## Security hardening\n\n- **Not vulnerable to the April 2026 VLESS local-SOCKS leak** affecting Happ, v2rayTUN, Hiddify, v2rayNG, NekoBox and others. No unauthenticated local SOCKS5 inbound is ever exposed — both internal bridges (SOCKS5 for tun2socks, HTTP for internal speed/service tests) require the ephemeral 32-char password. See *Ephemeral authenticated SOCKS5* above.\n- **Honest caveat on password surface.** The SOCKS5 username and password are passed to the `tun2socks` helper process via command-line arguments, so they appear in `/proc/\u003ctun2socks_pid\u003e/cmdline`. On modern Android this file is protected by SELinux `app_data_file` contexts and hidepid, so other apps cannot read it, but a rooted attacker or the same-uid process can. The password is ephemeral per session, so disclosure only compromises the current tunnel's local bridge, not the server credentials. Migration to stdin / fd-based credential passing is tracked as a future hardening step.\n- EncryptedSharedPreferences via `androidx.security:security-crypto` for small secrets (DB key material placeholder, credentials cache). Full database-level encryption via SQLCipher is on the roadmap — see *Known limitations* below.\n- Log redaction — UUIDs, passwords, Bearer tokens masked automatically\n- SSRF protection — private/loopback/link-local IPv4 and IPv6 blocked in profile parser\n- Tapjacking protection on critical buttons (filterTouchesWhenObscured)\n- Deep link validation with confirmation dialog\n- Atomic file writes (temp + rename pattern)\n- No cleartext traffic (except speed test domains through VPN tunnel)\n- No backup (`android:allowBackup=\"false\"`)\n- DNS leak prevention — all port 53 traffic forced through proxy\n- DNS address validation — loopback, private ranges and garbage strings rejected before they reach xray\n- Input size limits on URIs, subscriptions, imports\n- Evil Twin WiFi detection (SSID+BSSID pair validation)\n\n## Known limitations\n\n- **Room database is currently plain**, despite earlier wording. The SQLCipher dependency was declared but never wired as the Room `SupportFactory`, and the app deletes any previously-encrypted DB on first launch of a new version (see `AppDatabase.deleteEncryptedIfNeeded`). Profile data is already re-fetchable from subscriptions, so this is low-impact, but a proper SQLCipher integration is tracked as future work.\n- **`androidx.security:security-crypto` was deprecated by Google in 2024.** The 1.1.0-alpha06 release still works on current Android, but Android 15/16 may change backing-store behaviour without backward-compatibility guarantees. Migration path: move to `java.security.KeyStore.getInstance(\"AndroidKeyStore\")` directly, generate the AES-256 key via `KeyGenerator`, and store ciphertext in plain `SharedPreferences`. Tracked, not urgent.\n- **tun2socks credential exposure.** See *Security hardening → honest caveat on password surface* above.\n\n## Acknowledgements\n\nNetGuard is composed of a small original glue layer wired around a stack of open-source components that do the heavy lifting. The honest credit list — without these projects, this app could not exist.\n\n### Tunnel core (native)\n\n- **[xray-core](https://github.com/XTLS/Xray-core)** — Mozilla Public License 2.0. Project X. The actual proxy engine that speaks VLESS / VMess / Trojan / Shadowsocks / Hysteria2 and handles TLS / REALITY / uTLS fingerprinting. Shipped as `libxray.so` in `jniLibs/arm64-v8a/`.\n- **[badvpn / tun2socks](https://github.com/ambrop72/badvpn)** — BSD-3-Clause. Ambroz Bizjak. Userspace TUN-to-SOCKS5 helper that turns the Android `VpnService` TUN file descriptor into TCP/UDP streams that xray can consume. Shipped as `libtun2socks.so`.\n\n### Android libraries\n\n- **[AndroidX](https://developer.android.com/jetpack/androidx)** (Core, AppCompat, ConstraintLayout, SwipeRefreshLayout, Navigation, Lifecycle, Room, Preference, WorkManager, CameraX, Security-Crypto) — Apache-2.0. Google.\n- **[Material Components for Android](https://github.com/material-components/material-components-android)** — Apache-2.0. Google. Material You theming, dialogs, bottom-nav, dynamic colors.\n- **[OkHttp](https://github.com/square/okhttp)** — Apache-2.0. Square. HTTP client used for subscription fetching, certificate pinning, and DNS-rebinding-safe resolution.\n- **[Gson](https://github.com/google/gson)** — Apache-2.0. Google. JSON serialisation for xray config generation and profile import/export.\n- **[kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)** — Apache-2.0. JetBrains. Async runtime for `TunnelVpnService`, watchdogs, network callbacks.\n- **[ZXing Core](https://github.com/zxing/zxing)** — Apache-2.0. Generates QR codes for sharing profiles.\n- **[ML Kit Barcode Scanning](https://developers.google.com/ml-kit/vision/barcode-scanning)** — Apache-2.0 (Google Play Services component). Scans QR-coded subscription / profile URIs.\n- **[JUnit 4](https://github.com/junit-team/junit4)** — EPL-1.0. Test runner.\n\n### Inspiration / prior art\n\nThe architecture of NetGuard's `TunnelVpnService` (TUN file descriptor handover via Unix-domain socket, watchdog loop, no-leak network switching) draws from the broad Android-VPN-with-xray prior art established by:\n\n- **[v2rayNG](https://github.com/2dust/v2rayNG)** — GPL-3.0. The reference implementation for an Android Xray client; defined many of the patterns we still use.\n- **[Hiddify](https://github.com/hiddify/hiddify-app)** — GPL-3.0. Subscription / profile parser conventions.\n- **[NekoBoxForAndroid](https://github.com/MatsuriDayo/NekoBoxForAndroid)** — GPL-3.0. UI patterns for per-app routing and traffic stats.\n\nNetGuard does **not** reuse code from these projects directly (NetGuard is Apache-2.0; copying GPL-3.0 code would force a relicense), but ideas and protocol-level conventions are owed to them.\n\n### Tooling\n\n- **[Claude Code](https://claude.com/claude-code)** — Anthropic. Performed the v1.1.4 / v1.1.5 / v1.1.6 security audit, wrote the `ServerPreflight` / `PackageInstallReceiver` / TLS-rotation / MTU-probing changes, the JVM unit-test suite, and most of this README. AI-assisted commits are tagged with `Co-Authored-By: Claude` in their trailer when material code was generated by the assistant.\n\nIf we missed your project here, please open an issue — credit is the one thing we can give back, and we want the list to be complete.\n\n## Release notes\n\n### v1.1.9 (2026-05-02)\n\nSprint-1 + Sprint-2 of the post-v1.1.8 code-review fix list. Closes 6 P0 (critical) and 9 P1 (high) findings around SSRF guards, network-level fingerprint, log redaction, and confirmation flows.\n\n**P0 — SSRF / leak surface**\n- `parseVmess` now runs the same `AddressValidator.requirePublicAddress` check as the other URI parsers, plus `1..65535` port range. The base64-encoded JSON could previously slip in `127.0.0.1` / private / CGNAT addresses that the rest of the app trusted.\n- `ProfileEditViewModel.isValidAddress` delegates to `AddressValidator.isPrivateOrReserved` and validates IPv6 via `InetAddress.getByName(\"[…]\")`. Manual save of `127.0.0.1`, `192.168.x`, `100.64.x`, hex/octal IPv4 or IPv4-mapped IPv6 is now rejected.\n- `ServerPreflight.check` re-validates the resolved IP after `InetAddress.getByName`. Closes a DNS-rebinding window where a hostname could resolve to a private IP between profile parsing and TCP probe.\n- `MainViewModel.autoSelectAndConnect` no longer fires N parallel TCP SYNs from the user's real IP. Uses cached `lastPingMs` if any profile has a fresh value; otherwise probes only 3 random candidates sequentially with an early stop on `\u003c200ms`. Eliminates the \"burst of SYNs to scattered cloud IPs\" fingerprint that ISP/corp DPI uses to flag VPN clients in the auto-select phase.\n- xray and tun2socks output is now run through `LogBuffer.redactPublic` *before* `Log.d`, not just inside the in-app Log viewer. ProGuard now strips `Log.d` / `Log.v` from release builds entirely.\n- QR scan no longer imports profiles or subscriptions silently. Mirrors `MainActivity.handleDeepLink`'s confirmation dialog: VPN URIs go through `ProfileParser.parseSingleUri` + a `MaterialAlertDialog`; HTTPS subscriptions show a confirmation with the host preview; HTTP and unrecognised QR content are rejected with a Toast.\n\n**P1 — high-priority hardening**\n- `LogBuffer.redact` rebuilt: covers `password`/`passwd`/`pwd`/`pass`/`secret`/`api_key`/`token`/`bearer`/`authorization` (case-insensitive, both `=` and `:`, optional quotes), JSON form `\"key\":\"value\"`, Reality `pbk`/`sid` long base64, Shadowsocks URL base64 between `://` and `@`, hostname/IP in network-error lines (`dial`/`connect to`/`dns`/`resolve`). Quick-check keywords removed (the `-` keyword matched almost every line).\n- `LogBuffer` switched from `ArrayList.removeAt(0)` to `ArrayDeque.removeFirst()` (O(1) vs O(n)). The \"throttle\" flag was a no-op; replaced with a real CONFLATED `Channel` debounced at 120ms for `StateFlow` updates.\n- `SubscriptionRepository.validateUrl` is now public, called *before* DB insert in `SubscriptionViewModel.addSubscription` and `SettingsViewModel.importConfig`. Junk subscription URLs no longer persist and are not retried by the periodic `SubscriptionUpdateWorker`. Added 256-char limit to subscription names (mirrors `ProfileParser.MAX_NAME_LENGTH`).\n- `ProfileEditViewModel.testConnection` refuses to TCP-ping when the tunnel is down. When connected, performs a SOCKS5 CONNECT through the local authenticated bridge and times the handshake. No more direct probe from the user's real IP.\n- `HomeFragment` skips `GeoLookup.fetchUserLocation()` when the tunnel is down — the `ipwho.is` request previously went out from the user's real IP on every Home open, and once a week even with a warm cache.\n- Subscription list masks the URL: shown as `host/…/abcd` (last 4 chars of path). The full token-bearing URL is no longer rendered into a `RecyclerView` where any screenshot leaks it.\n- `SettingsFragment.isValidDns` now goes through `AddressValidator.isPrivateOrReserved`, covering CGNAT, IPv6, hex/octal IPv4, IPv4-mapped IPv6 — all of which the previous regex missed.\n- `SecuritySelfTest` adds an \"Own SOCKS5 auth\" check that connects to our own ephemeral SOCKS port and asserts that the no-auth handshake is rejected. Catches future regressions in `XrayConfigGenerator.buildInbounds` automatically.\n- `ServiceTester` returns uniform error results when the tunnel is down instead of testing youtube.com / openai.com / instagram.com / discord.com from the user's real IP. `buildClient()` now returns `null` if credentials aren't ready — there's no direct-connection fallback path left.\n- All interactive elements in the onboarding wizard (Back / Next / Grant VPN / Grant Usage Stats / Paste / Import / Skip) carry `android:filterTouchesWhenObscured=\"true\"`. Onboarding is the highest-stakes UI flow (VPN permission, mode selection, first profile) and was the only place README's tapjacking-protection claim didn't apply.\n\n**Internals**\n- New strings: `import_subscription_confirm` (en + ru). Other locales fall back to English via Android's locale resolution.\n- `versionCode` 33, `versionName` \"1.1.9\".\n\n### v1.1.8 (2026-05-02)\n\n**First-launch onboarding wizard** — the app now opens with a 7-step setup flow on first install: language → welcome (with feature cards) → routing-mode picker → VPN permission → Usage Stats permission (only when Trigger mode is picked) → first-server import → done. State is persisted in `SharedPreferences` (`onboarding_done`), so the wizard is shown exactly once. Subsequent launches go straight to the main UI. Profile import accepts `vless://` / `vmess://` / `trojan://` / `ss://` / `hy2://` links via paste-from-clipboard or manual paste, with an explicit \"Add later\" skip button if the user has no profile yet. The bottom progress bar uses Material 3 `LinearProgressIndicator` with `trackCornerRadius`, animated via `ObjectAnimator` between steps; transitions use a slide+fade choreography (180ms out, 220ms in).\n\n- **Live language switching, no flicker.** `OnboardingActivity` declares `configChanges=\"locale|layoutDirection|uiMode|fontScale\"` and overrides `onConfigurationChanged` so a locale switch via `AppCompatDelegate.setApplicationLocales` does NOT recreate the activity. Instead, a `refreshMap` of `(TextView, R.string.X)` pairs is walked and every translated string is re-set in place — including the `TextInputLayout` hint and the bottom Back/Next button labels. Result: pick a language, the entire wizard updates instantly without a single frame of black.\n- **Selection cards rebuilt without `isCheckable`.** `MaterialCardView` with `isCheckable=true` + `checkedIcon=null` crashes on Android 14 with NPE in `c5.b.onAnimationUpdate` (Material's checked-icon animator tries to set alpha on a null `Drawable`). The language cards are now plain `MaterialCardView` with manual stroke-color toggling (`colorOutlineVariant` ↔ `colorPrimary`); selection is conveyed by the `RadioButton` plus the colored outline. No checkmark icon, no crash.\n- **Translations.** All onboarding + trigger-mode strings have been added to **all 16 supported locales**: `ar`, `de`, `en`, `es`, `fr`, `hi`, `in`, `it`, `ja`, `ko`, `pt`, `ru`, `th`, `tr`, `vi`, `zh-rCN`. Around 50 new strings × 14 non-base translations = ~700 new translations.\n\n**Flexible trigger routing mode** — new `triggerStrictMode` setting (default `true` to preserve v1.1.7 behavior). When `false`, the trigger watcher acts as a launcher only: opening a trigger app brings up the regular global tunnel governed by the user's normal `perAppMode` (`DISABLED` = global, `WHITELIST`, `BLACKLIST`); closing it (with `triggerAutoStop`) calls `TunnelVpnService.stop()`. Strict mode keeps the v1.1.7 allow-list semantics (only trigger apps route through the VPN, blackholed when down). The new switch is in *Settings → Trigger apps* with an inline explainer.\n\n- `App.onCreate`: skips `startQuarantine` when flexible mode is enabled.\n- `App.updateTriggerWatcher`: only pre-warms when strict.\n- `ForegroundAppWatcher.onForegroundChanged`: branches between `activateTrigger`/`deactivateTrigger` (strict) and `start(profileId)`/`stop()` (flexible).\n- `TriggerAppsFragment.save`: stops force-disabling per-app routing and stops the BLACKLIST overlap auto-cleanup when flexible mode is on — composing trigger detection with per-app rules is the whole point.\n- Always-on auto-start path in `TunnelVpnService.onStartCommand` also respects the flag.\n\n**Dual-app warning surface.** Cloned variants of an app (Telegram via MIUI Dual Apps, Samsung Dual Messenger, Parallel Space) cannot be added to the VPN allow list — Android's public `addAllowedApplication(packageName)` only resolves the calling user's UID, while the clone runs in a separate user space (UID ≥ 999000 on MIUI). The Trigger Apps screen now shows a red Material 3 card explaining this, with a \"Why?\" dialog detailing the limitation and three workarounds (use original Telegram, disable \"Block connections without VPN\" in system settings, or add the cloned package separately if the OEM exposes it).\n\n**Internals**\n- `OnboardingActivity` lives in `ui/onboarding/`; registered in the manifest with `singleTask` launch mode and the configChanges flags above.\n- `MainActivity.onCreate` short-circuits to `OnboardingActivity` on first launch (`onboarding_done` pref absent), and respects an `EXTRA_OPEN_TRIGGER` extra to navigate directly to the trigger picker after onboarding when the user picked Trigger mode.\n- Wizard state (current step, picked routing mode, picked language, profile-import counter) survives configuration-change events via `onSaveInstanceState`.\n- `versionCode` 32, `versionName` \"1.1.8\".\n\n### v1.1.7 (2026-05-02)\n\n**App-launch trigger mode** — the headline feature. Pick which apps go through the VPN; everything else (banking, maps, your usual browser) keeps using the regular connection. The selected apps have no fallback path: if the tunnel is down, their packets are black-holed in the TUN — they never see your real IP, not even for a moment.\n\n- New `ForegroundAppWatcher` service polls `UsageStatsManager` (250ms) to detect when a trigger app comes to the foreground.\n- Pre-warm: when trigger mode is enabled, the tunnel comes up immediately and stays ready, so opening a trigger app is instant — no \"Connecting…\" delay.\n- Optional auto-stop on background to save battery (xray killed, TUN keeps the apps black-holed).\n- Boot-survival: `BootReceiver` re-arms the watcher and tunnel after device reboot.\n- Auto-recover: if xray crashes, up to 3 silent restarts before any user-visible failure. In trigger mode the TUN is preserved as a quarantine even after xray gives up — never falls back to \"no VPN\" for trigger apps.\n- Network sanity: skipping activation when no underlying network is available, surfacing it as \"Нет сети\" / \"No network\" instead of a stale \"Connecting…\".\n- UI: dedicated screen with Material 3 switches, collapsible explainer, search box, system-app filter, save/cancel. Big red banner explains the Always-on VPN sweet spot (\"turn ON Always-on, leave Block-without-VPN OFF\").\n\n**Bug fixes**\n- Backup / Restore: profiles now keep their subscription link. Old format (v1, plain URI list) is still accepted; new exports use v2 with `{uri, subscription}` per profile.\n- Settings: scroll position is preserved when you navigate into a sub-screen and back.\n- Settings: bottom-nav tap always returns to the root of the tab — no more \"I went into Trigger then tapped Servers and came back to Trigger with the wrong tab highlighted\".\n- Quick Settings tile: when there is no saved profile, opening the tile now triggers auto-select (best non-RU server) instead of silently doing nothing.\n- DNS: when a non-empty Per-App blacklist is active, the VPN no longer overrides system DNS for the excluded apps. Previously they were forced onto Cloudflare/Google DNS, which is filtered by some Russian ISPs and broke their resolution.\n- Connect button now triggers auto-select when no profile is picked, instead of doing nothing.\n- Per-App routing description rewritten to make the relationship to Trigger mode obvious; toggling Trigger automatically disables Per-App routing to avoid silent conflict.\n\n**Internals**\n- Tunnel start re-ordered: TUN comes up before xray now, eliminating the leak window during start.\n- Faster activation: tighter polling in `sendTunFd` and `waitForPort`, 100ms watcher tick, 50ms post-establish delay — saves ~600-700ms on cold open of a trigger app.\n- `setUnderlyingNetworks` now passes ALL non-VPN networks (cellular + wifi if both available) instead of just the first one.\n\n### v1.1.6 — 2026-04-30\n\n**Security audit (P0/P1 fixes)**\n- **No more HTTP fallback in geo-IP lookup.** `GeoLookup.kt` previously fell back to `http://ip-api.com/json/` when the HTTPS `ipwho.is` provider was unreachable. Any on-path observer would have seen the real source IP in plaintext during that fallback. The HTTP path is now removed entirely; if HTTPS lookup fails, the result is simply `null`.\n- **Geo cache moved to `EncryptedSharedPreferences`.** User coordinates were previously cached in plain `SharedPreferences` (`/data/data/\u003cpkg\u003e/shared_prefs/geo_cache.xml`). A privileged co-resident process or root could read them. The cache is now stored under `geo_cache_enc` with `MasterKey` AES-256-GCM, with a one-shot migration that wipes the legacy plaintext on first launch.\n- **Profile validation before xray config build.** `XrayConfigGenerator.validateProfile()` now rejects `port` outside `1..65535` and any control-character (`\\x00..\\x1f`, `\\x7f`) in `address`, `host`, `path`, `sni`, `serviceName`, `authority`, `publicKey`, `shortId`, `alpn` *before* the JSON is fed to xray. A corrupt stored profile no longer kills the tunnel through the watchdog loop; instead a single readable error surfaces.\n- **No more `Thread.sleep` on Dispatchers.IO.** `sendTunFd()` and `startTun2socksProcess()` are now `suspend` and use `delay()` while waiting for the tun2socks Unix-domain socket. The previous `Thread.sleep(200)` calls were blocking IO worker threads up to 2 s per startup.\n- **Concurrency: `@Volatile` on cross-thread service state.** `vpnFd`, `serviceScope`, `trafficMonitor`, `xrayProcess`, `tun2socksProcess`, `networkCallback`, `currentProfileId`, `showSpeedNotification`, `xrayWatchdogJob`, `tun2socksWatchdogJob`, `lastNotificationUpdate` are now all `@Volatile` (or `synchronized(fdLock)` for `vpnFd`). Writes from the main thread are now visible immediately to the watchdogs running on `Dispatchers.IO`, so a watchdog cannot see a stale `null` and skip recovery.\n- **Faster network-switch reconnect.** WiFi↔LTE handover debounce reduced from 2000 ms to 500 ms — the TUN black-hole window during a clean handover is now sub-second.\n- **`ACCESS_FINE_LOCATION` capped at API 32.** `NEARBY_WIFI_DEVICES` (`neverForLocation`) covers SSID/BSSID reads on Android 13+, so the privacy-sensitive permission is no longer requested on devices where it is no longer needed.\n\n**VPN-detection bypass (per-app)**\n- **New \"Exclude all Russian apps\" / \"Исключить приложения РФ\" button** in *Settings → Per-app routing*. One tap sweeps every installed non-system app whose `packageName` starts with `ru.` and adds them to the per-app list, plus a curated set of Russian apps that publish under `com.*` / `io.*` (Tinkoff, Sberbank, Otkritie, Wildberries, Ozon, ICBC, AliExpress AER, the full Yandex stack, etc.). When `PerAppMode.BLACKLIST` is active, those apps see the real underlying network instead of the tunnel — their IP / GeoIP / `TRANSPORT_VPN` flag stays consistent with the SIM/Wi-Fi region. This is the only client-side mitigation available without root against the published \"VPN/Proxy detection on client devices\" methodology, which relies on `ConnectivityManager.NetworkCapabilities.TRANSPORT_VPN`, `VpnTransportInfo`, and `dumpsys vpn_management` — none of which can be hidden by an app from another app at the OS level. The button is locale-gated (visible only on `ru` / `en`) so non-Russian-locale users do not see an unhelpful action.\n- **Auto-bypass for newly installed RU apps.** A `PACKAGE_ADDED` `BroadcastReceiver` (registered dynamically because Android 8+ disallows static manifest `PACKAGE_ADDED` for regular apps) appends every freshly-installed non-system app whose `packageName` starts with `ru.` to `perAppList`, but only when the new `autoBypassRuPackages` toggle is on. Without this the one-shot button only protects apps that exist at the moment the user taps it.\n\n**Tunnel reliability**\n- **MTU-probing.** `VpnService.Builder.setMtu()` and `tun2socks --tunmtu` are now derived from `ConnectivityManager.getLinkProperties().getMtu()` of the active non-VPN network, capped to `[1280, 1500]`. On many mobile carriers the LTE/5G link MTU is 1428 or 1450 — the previous hard-coded `1500` caused silent IP fragmentation and a 5–15 % throughput drop.\n- **Pre-flight reachability check.** Before xray + tun2socks are spun up, `ServerPreflight` does a 1.5 s DNS + 2 s TCP probe of the target address. A dead server short-circuits to `ConnectionState.Error` in \u003c2 s instead of burning the full 30 s tunnel-establishment budget. UDP-only protocols (Hysteria2) fall back to DNS-success-only since we cannot meaningfully ping UDP without speaking the protocol.\n- **Auto-failover.** When `startTunnel()` fails (timeout, preflight dead, xray crash, tun2socks exit) the service automatically tries the next untried profile from the same subscription (or the next one in the database if the current profile has no subscription), with a budget of 3 attempts. The ledger resets on a successful `Connected` state and on user-initiated `stopTunnel`.\n- **TLS fingerprint randomisation.** New `TlsFingerprintMode` setting (`CHROME` / `FIREFOX` / `SAFARI` / `IOS` / `EDGE` / `RANDOM`) feeds the uTLS fingerprint string used for VLESS / VMess / Trojan TLS and REALITY outbounds. In `RANDOM` mode a fresh fingerprint is picked per connect, so DPI systems cannot correlate consecutive sessions by ClientHello shape. Per-profile override (set by `ProfileParser` when the URL carries `fp=…`) still wins over the global setting. Fixed a long-standing bug where `ServerProfile.fingerprint` defaulted to `\"chrome\"` instead of `\"\"`, which made any global rotation a no-op for stored profiles.\n\n**Tests**\n- New JVM unit tests under `app/src/test/java/com/smarttools/netguard/core/`:\n  - `XrayConfigValidationTest` — port range, control-character rejection in 9 string fields, fingerprint resolver (default / global / random / per-profile override), SOCKS port + credentials sanity (11 cases).\n  - `ServerPreflightTest` — empty / unresolvable / closed-port / listening-port / UDP-only paths (5 cases).\n- `app/build.gradle.kts` enables `testOptions.unitTests.isReturnDefaultValues = true` so `android.util.Log` calls in code under test do not throw `Method d in android.util.Log not mocked.`\n\n### v1.1.4 — 2026-04-18\n\n**Security**\n- **SSRF hardening.** All private / loopback / link-local / CGNAT (`100.64.0.0/10`) / reserved (`240.0.0.0/4`) / IPv4-mapped-IPv6 / hex-and-octal IPv4 literals (`0x7f.0.0.1`, `0177.0.0.1`) are now rejected by a single `AddressValidator`. The old spot checks on `startsWith(\"10.\")`, `\"192.168.\"` etc. were trivially defeated by alternative textual forms; the new validator resolves literals numerically via `InetAddress` and inspects the resulting bytes.\n- **DNS rebinding guard.** Subscription fetches now go through a custom OkHttp `Dns` resolver that re-validates every address returned at connect time, so an attacker who controls the subscription host's DNS cannot swap the validated public IP for a private LAN IP between validation and connection.\n- **Ephemeral SOCKS credentials.** The local SOCKS5 / HTTP bridge credentials are now stored as `CharArray` inside `CredentialManager` and actively zeroed on `clear()`, instead of lingering as immutable `String` heap residue until GC.\n- **No-leak network switching.** `VpnService.setUnderlyingNetworks()` is now pinned to the active non-VPN network both at tunnel establishment and on every WiFi↔LTE switch, eliminating the narrow window where xray's outbound socket could transiently route over the old interface.\n- **Honest README.** Claims that were not strictly true in the code — \"credentials wiped from memory immediately after handshake\", \"config never touches disk\", \"encrypted database\" — have been rewritten to reflect what actually happens. The matching *Known limitations* section documents tracked items (SQLCipher integration, `security-crypto` deprecation, tun2socks credential argv exposure).\n\n**Platform compat**\n- **Android 14 FGS.** Foreground-service type migrated to `specialUse | systemExempted` with the required `PROPERTY_SPECIAL_USE_FGS_SUBTYPE` declaration. The previous `specialUse` on Android 14 raised a runtime `SecurityException` at `startForeground()`; a naive migration to `connectedDevice` also fails because that type requires holding one of BLUETOOTH/USB/NFC permissions that a VPN legitimately cannot claim.\n- **Android 13+ WiFi.** `NEARBY_WIFI_DEVICES` permission (with `neverForLocation`) is now declared and requested at runtime when the user opens the Trusted WiFi dialog or flips the auto-connect toggle; without it, SSID read silently returned `\u003cunknown ssid\u003e` on stock Android 13+.\n- **IPv6 route is now conditional on the `Enable IPv6` setting.** Previously `addRoute(\"::\", 0)` was claimed unconditionally — on IPv4-only servers, AAAA-bound traffic disappeared into tun2socks instead of falling back gracefully.\n\n**WiFi auto-connect reliability**\n- SSID/BSSID lookup falls back to `WifiManager.connectionInfo` when Android strips `transportInfo` from background callbacks.\n- Events are de-duplicated by `(SSID, BSSID)` so the throttled `onCapabilitiesChanged` firehose (RSSI / link-speed / validation updates) no longer spams `TunnelVpnService.start()`.\n- If the first capability event arrives with `\u003cunknown ssid\u003e` (Wi-Fi still associating), the manager retries at 2 s / 5 s / 10 s via `WifiManager` instead of losing the event.\n- An initial probe runs after `registerNetworkCallback` so sessions that start while already on Wi-Fi are handled, not only subsequent network switches.\n- Trusted WiFi dialog works even before enabling auto-connect — it uses a transient manager instance to read SSID/BSSID.\n\n**Code quality**\n- New `Security self-test`: \"Neighboring VPNs\" check scans for other installed VPN clients (v2rayNG, Hiddify, Happ, SFA, Karing, WireGuard, OpenVPN, strongSwan, …) that may be vulnerable to the April 2026 local-SOCKS leak.\n- `MTU` self-test is now correctly reported as informational: flagging `MTU != 1500` as a \"VPN anomaly\" was both false-positive on well-behaved VPNs and contradicted NetGuard's own stealth choice of MTU=1500.\n- Speed-test URLs switched to HTTPS where the endpoint supports it; cleartext exception narrowed to `speedtest.tele2.net` only (was three domains).\n- Removed unused `ConfigBuilder.kt` wrapper, cleaned out imaginary `com.github.nicknob.*` entries from `KNOWN_VPN_PACKAGES`, removed the dead `SQLCipher` dependency (was declared but never wired into Room).\n- First unit tests: `AddressValidatorTest` covers the SSRF-critical logic (16 cases, including CGNAT, hex/octal IPv4, IPv4-mapped IPv6).\n- CI: GitHub Actions workflow runs `./gradlew testDebugUnitTest` on push / PR.\n- `SECURITY.md` with responsible-disclosure policy.\n- `User-Agent` now reads from `BuildConfig.VERSION_NAME` instead of the hard-coded `\"NetGuard/1.0\"`.\n\n### v1.1.3 — 2026-04-17\n\n**Security**\n- Dropped the unauthenticated local SOCKS5 inbound (`speedtest-in`). That same pattern was disclosed as a leak for Happ, v2rayTUN, Hiddify and v2rayNG in April 2026: any app on the device could reach `127.0.0.1` and tunnel traffic through the VPN to learn the real server IP. Internal speed/service tests now talk to the authenticated `http-in` bridge instead.\n- Restored DNS validation on the settings save path. It had regressed when the explicit \"Save\" button was removed, so `127.0.0.1`, `localhost`, private ranges and random typos were being silently accepted.\n- TLS fragment and routing fields reject malformed input with a short toast instead of passing garbage through to xray.\n\n**New features**\n- **TLS Fragment.** Splits the TLS ClientHello into smaller pieces to slip past DPI that matches on SNI. Packets / length / interval are configurable in Settings.\n- **Favorites.** Star a server to pin it to the top of the list. Room migrates v2 → v3 on first launch.\n- **Domain / IP bypass list.** Lines of domains and IPs that go direct, regardless of the global routing mode.\n- **Traffic statistics graph.** 7-day chart on the Home tab. 30 days of history retained.\n- **Widget.** The home-screen widget is now 3×1 and shows the selected server + connection state. Tap anywhere on it to toggle the VPN.\n\n**UI / cosmetic**\n- Subscription list: the Share / Update / Delete buttons no longer overlap the subscription name and URL. Icons are theme-tinted so they stay visible on both light and dark backgrounds.\n- Flat vector favorite stars in place of the legacy 3D Android star drawable.\n- Connection map: adapts to light theme (bitmap is grayscale-inverted, land/ocean tints are swapped) and the dashed connection arc animates from the user to the server while the VPN is up.\n\n**Reliability**\n- The widget no longer runs a blocking Room query on the main broadcast thread. It reads the selected server name from a small SharedPreferences cache that's refreshed on profile select and on VPN start. Removes an ANR risk when the DB was locked during a subscription sync.\n- Traffic history cleanup is one `apply()` per day archive (was 30) and the cleanup window is wide enough to recover history after the app was idle for months.\n\n## Setup\n\n1. Clone the repository.\n2. Drop the libXray AAR into `app/libs/` — see [`app/libs/README.md`](app/libs/README.md) for the exact download link and version. Without this step the build will fail with unresolved native symbols.\n3. (Release builds only) copy `keystore.properties.template` → `keystore.properties` and point it at your signing key. Debug builds auto-sign with the SDK debug key and do not need this.\n4. Build:\n\n   ```bash\n   # Requires Android Studio with JBR (JetBrains Runtime)\n   JAVA_HOME=\"/path/to/android-studio/jbr\" ./gradlew assembleDebug\n   ```\n\n**Requirements:**\n- Android SDK 34\n- Kotlin 1.9+\n- Min SDK 26 (Android 8.0)\n\n## Architecture\n\n```\nxray-core (VLESS/VMess/Trojan/SS/Hy2)\n    |\n    | authenticated SOCKS5 (random port, ephemeral creds)\n    |\nbadvpn-tun2socks\n    |\n    | TUN file descriptor\n    |\nAndroid VpnService (TUN interface)\n    |\n    | per-app routing rules\n    |\napps\n```\n\n## License\n\nThis project is provided as-is for personal use.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkokosaaaa%2Fnetguard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkokosaaaa%2Fnetguard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkokosaaaa%2Fnetguard/lists"}