Open source project

Captive portal auth without breaking your VPN

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

VPN kill switch vs. captive portal

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

Security by default

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.

WebView sandboxing

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.

RFC 1918 navigation allowlist

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.

Automatic connectivity detection

Polls a connectivity check endpoint every 5 seconds. Detects portal authentication success via HTTP 204 response and updates status in real time.

Comprehensive session cleanup

On session end, all cookies, cache, and web storage are wiped and the WebView instance is destroyed. Zero data persists between sessions.

Persistent warning notification

A non-dismissible foreground notification with countdown timer runs during active sessions. You cannot forget you are outside the tunnel.

Architecture

How it works

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

Technical decisions

Kotlin + Jetpack Compose

Single-activity architecture with Jetpack Navigation Compose. Reactive state via Kotlin Flows and Compose's collectAsState(). Hilt for dependency injection.

Minimal dependency footprint

No external HTTP client, no JSON parsing libraries, no analytics. Connectivity checks use java.net.HttpURLConnection directly. Smaller attack surface, smaller APK.

Zero data persistence

No browsing history, no cookies to disk, no form data. Only user preferences (timeout, JS toggle, strict mode) are stored via DataStore.

InetAddress for RFC 1918

Private address detection delegates to Android's InetAddress.isSiteLocalAddress() rather than regex parsing — more reliable and handles edge cases correctly.

License

Open source

Porthole is licensed under the Apache License 2.0. Free to use, modify, and distribute for any purpose.

Install

Get the APK

  • GitHub Releases — signed APKs coming soon
  • Build from source./gradlew assembleDebug
  • F-Droid and Google Play — planned for future releases

Stop choosing between WiFi and privacy

Next time you are stuck at a hotel login page with your VPN kill switch on, Porthole gets you through the portal without exposing the rest of your traffic. One tap, time-limited, and everything is wiped when you are done.