Session state machine
A well-defined state machine (IDLE, ACTIVE, EXPIRED, CLOSED) prevents invalid transitions and enforces single-session-at-a-time. No accidental double tunnels.
Open source project
Porthole gives security-conscious Android users a safe, controlled way to authenticate with captive portals. It opens an isolated, time-limited browser session that operates outside your VPN, authenticates with the portal, and shuts down cleanly when you are done.
The problem
On Android with a VPN kill switch active, connecting to a captive portal network leaves you stuck. The VPN blocks the portal from loading, but disabling the VPN exposes all your traffic. Porthole bridges this gap by acting as a split-tunnel exception — only Porthole's traffic bypasses the VPN to reach the portal gateway. All other apps remain protected.
Features
A well-defined state machine (IDLE, ACTIVE, EXPIRED, CLOSED) prevents invalid transitions and enforces single-session-at-a-time. No accidental double tunnels.
13 security-hardening settings applied to every session. JavaScript off by default, no file access, no geolocation, no password persistence, no databases. The WebView is destroyed and recreated each session.
Strict mode restricts navigation to private addresses only (10.x, 172.16.x, 192.168.x). Permissive mode lets you approve external domains one at a time.
Polls a connectivity check endpoint every 5 seconds. Detects portal authentication success via HTTP 204 response and updates status in real time.
On session end, all cookies, cache, and web storage are wiped and the WebView instance is destroyed. Zero data persists between sessions.
A non-dismissible foreground notification with countdown timer runs during active sessions. You cannot forget you are outside the tunnel.
Architecture
Porthole relies on Android's per-app VPN exclusion. When added to your VPN's split tunnel list, the kernel routes only Porthole's traffic outside the tunnel. The app itself never implements VPN logic.
Session start:
1. Check WiFi connected ──▶ reject if no WiFi
2. Resolve gateway IP ──▶ WifiManager.getDhcpInfo()
3. Create sandboxed WebView ──▶ 13 security settings applied
4. Load gateway URL ──▶ RFC 1918 allowlist enforced
5. Start countdown timer ──▶ max 10 minutes
6. Show foreground notification
During session:
Portal traffic ──▶ [Porthole only] ──▶ outside VPN tunnel
All other apps ──▶ [VPN tunnel] ──▶ encrypted as normal
Connectivity ──▶ poll /generate_204 every 5 seconds
Session end:
1. Wipe cookies, cache, storage
2. Destroy WebView instance
3. Dismiss notification
4. Return to IDLE Under the hood
Single-activity architecture with Jetpack Navigation Compose. Reactive state via Kotlin Flows and Compose's collectAsState(). Hilt for dependency injection.
No external HTTP client, no JSON parsing libraries, no analytics. Connectivity checks use java.net.HttpURLConnection directly. Smaller attack surface, smaller APK.
No browsing history, no cookies to disk, no form data. Only user preferences (timeout, JS toggle, strict mode) are stored via DataStore.
Private address detection delegates to Android's InetAddress.isSiteLocalAddress() rather than regex parsing — more reliable and handles edge cases correctly.
License
Porthole is licensed under the Apache License 2.0. Free to use, modify, and distribute for any purpose.
Install
./gradlew assembleDebug