A minimal, lightweight Tor client for Android
VPN mode · Snowflake · obfs4 · Censorship circumvention
| Feature | Description |
|---|---|
| One-tap Tor | Connect/disconnect with a single button |
| VPN Mode | Route ALL device traffic through Tor (no root required) |
| IPv6 Leak Protection | IPv6 is captured into the tunnel (::/0) or fails closed — apps can never bypass Tor on dual-stack networks |
| Self-Healing Reconnect | Unexpected tor crashes auto-restart with backoff before showing an error |
| SOCKS5 Proxy | 127.0.0.1:9050 for apps that support proxy settings |
| HTTP Proxy | 127.0.0.1:8118 (Tor's HTTPTunnelPort) |
| Snowflake Bridge | Censorship circumvention via WebRTC proxies |
| obfs4 Bridges | Built-in default bridges for additional circumvention |
| WebTunnel | Mimics HTTPS traffic to defeat deep packet inspection |
| CI-Verified Bridges | GitHub Actions health-checks bridges every hour; the app fetches alive bridges at runtime |
| Per-App Routing | Select which apps use Tor; rules apply live to a running VPN |
| .onion Access | Automatic via Tor routing |
| Bandwidth Monitor | Real-time speed + cumulative totals (upload & download) |
| Kill Switch | Block all internet if Tor disconnects unexpectedly |
| Auto-start on Boot | Optional, configurable in app |
| Battery-Optimization Prompt | One-time system dialog so OEM battery managers don't kill tor after screen-off |
| Low-RAM Design | Bounded icon memory, capped tor buffers — targets 1.5GB devices |
| Camouflage Icons | Disguise the app with alternate launcher icons |
| Kindness Mode | Run a Snowflake proxy to help censored users |
- Not a browser (use Firefox with your Tor connection)
- Not a root app (uses Android's VpnService API)
- Not a hidden service host
- Not affiliated with the Tor Project or Orbot
VPN Mode (all apps):
App → Android VPN (TUN) → hev-socks5-tunnel → Tor SOCKS5 (9050) → Tor Network → Internet
Manual Proxy (per-app):
App (configured) → Tor SOCKS5 (9050) / HTTP (8118) → Tor Network → Internet
+--------------------------------------------+
| UI (MainActivity) |
| [Connect] [Snowflake] [VPN] [Bandwidth] |
+---------------------+----------------------+
| Broadcasts
+---------------------v----------------------+
| TorService (Foreground) |
| +---------------+ +------------------+ |
| |ProcessManager | |ControlConnection | |
| | - tor | | - BW events | |
| | - obfs4 | | - CIRC events | |
| | - snowflake | | - STATUS events | |
| +---------------+ +--------+---------+ |
| +---------------+ | |
| | KillSwitch | +--------v---------+ |
| | Monitor | | BandwidthTracker | |
| +---------------+ +------------------+ |
+--------------------------------------------+
+--------------------------------------------+
| TorVpnService (VpnService) |
| +--------------------------------------+ |
| | hev-socks5-tunnel (native) | |
| | TUN interface -> SOCKS5 :9050 | |
| | (Blocks UDP, handles kill switch) | |
| +--------------------------------------+ |
+--------------------------------------------+
| Tool | Version | Notes |
|---|---|---|
| Android SDK | API 34 | Compile target |
| Android NDK | r26d | Native binary cross-compilation |
| Go | 1.21+ | For obfs4proxy and snowflake |
| Gradle | 8.5 | Included via wrapper |
| JDK | 17 | Temurin distribution |
| Build host | Linux | Tested on Ubuntu 22.04 |
# Set NDK path
export ANDROID_NDK_HOME=/path/to/android-ndk-r26d
# Build all native binaries for all ABIs
chmod +x scripts/*.sh
./scripts/build-all.sh all "$ANDROID_NDK_HOME"
# Or build for a specific ABI
./scripts/build-all.sh armeabi-v7a "$ANDROID_NDK_HOME"This produces the following in app/src/main/jniLibs/<abi>/:
| Binary | Size (approx) | Source |
|---|---|---|
libtor.so |
~6 MB | tor 0.4.8.13 |
libobfs4proxy.so |
~6 MB | obfs4proxy 0.0.14 |
libsnowflake.so |
~5 MB | snowflake 2.10.1 |
libsnowflake-proxy.so |
~33 MB | snowflake proxy 2.10.1 |
libtun2socks.so |
~100 KB | hev-socks5-tunnel 3.0.5 |
libwebtunnel.so |
~8 MB | webtunnel client (required for the WebTunnel toggle) |
# Debug build
./gradlew assembleFdroidDebug
# Release build
./gradlew assembleFdroidRelease# Install on connected device
adb install app/build/outputs/apk/fdroid/debug/*.apk
# Or reinstall
adb install -r app/build/outputs/apk/fdroid/release/*.apkThis app is designed for F-Droid submission from the ground up:
| Requirement | Status |
|---|---|
| 100% FLOSS | GPL-3.0-or-later, no proprietary dependencies |
| No Google Play Services | No Firebase, no analytics, no tracking |
| Reproducible Builds | Consistent build config, frozen timestamps |
| Native Binaries Built from Source | Build scripts in scripts/ directory |
| No Runtime Binary Downloads | All binaries bundled at build time |
| Minimal Dependencies | Only AndroidX, Material, and Kotlin Coroutines |
| Bundled Fonts | No Google Fonts Provider dependency (OFL 1.1 licensed) |
| No NonFreeNet AntiFeature | Uses FOSS-friendly STUN servers only |
- Fork fdroiddata
- Copy
metadata/org.onionroute.lite.ymltometadata/ - Submit a merge request
- F-Droid maintainers will review and build
The kill switch works by keeping the VPN interface active but stopping the SOCKS5 forwarding. Since all traffic must go through the VPN (Android routes everything to TUN), dead forwarding = no traffic. This is more reliable than iptables-based approaches and doesn't require root.
All DNS queries are routed through Tor's DNSPort (127.0.0.1:5400) when VPN mode is active. This prevents your ISP from seeing which domains you resolve. Verify with dnsleaktest.com.
The TUN interface routes both 0.0.0.0/0 and ::/0. On dual-stack (IPv4+IPv6) networks — most LTE and modern WiFi — an IPv4-only VPN would let apps silently bypass Tor over IPv6 while everything looked routed. OnionRoute Lite captures IPv6 into the tunnel too: v6 traffic goes through tun2socks → SOCKS5 → tor, or fails closed. It never leaks around the VPN.
If the tor process dies unexpectedly (OOM killer, OEM battery manager, network switch), the service automatically restarts it with linear backoff (3s, 6s) while the UI shows "Connecting…". Only after repeated failures does the app surface an error. Combined with the one-time battery-optimization prompt on first connect, this addresses the most common real-world failure mode on aggressive Chinese-OEM ROMs (MIUI, EMUI, ColorOS).
Tor only supports TCP. All UDP traffic is blocked at the VPN layer. This means:
- VoIP apps won't work through VPN mode
- Some streaming services may fail
- Online gaming won't work
This is a fundamental Tor limitation, not a bug.
Built for Samsung Galaxy Grand Prime Plus (SM-G532F):
| Aspect | Detail |
|---|---|
| RAM | 1.5 GB — App uses ~30-50 MB; tor daemon ~20-40 MB (queue buffers capped at 32 MB) |
| Storage | 8 GB — APK is ~38 MB; leave room for tor data |
| CPU | Spreadtrum SC9830E — First connect may take 30-60s |
| Battery | Foreground service drains faster; the app asks once to be excluded from battery optimization |
Memory is bounded by design so the app stays smooth on 1.5GB devices:
| Technique | Effect |
|---|---|
| App icons rasterized to fixed 96px bitmaps | Per-app routing list costs ~15 MB instead of 30-80 MB of full-res adaptive icons |
MaxMemInQueue 32 MBytes in generated torrc |
Tor's channel/cell buffers can't balloon under multi-app VPN load |
No largeHeap flag |
Normal heap ceiling keeps the app friendly to co-running apps |
| Speed-history capped at 60 samples; R8 full-mode shrinking | Small state footprint, minimal DEX |
Phone/tablet only. The release APK ships only ARM ABIs (armeabi-v7a + arm64-v8a). x86/x86_64 (Intel-based tablets and emulators) are not supported in release builds — see Supported ABIs for details.
- Settings → Battery → Battery Optimization → Set OnionRoute Lite to Don't optimize (the app shows this system dialog once on your first manual connect — accepting is recommended)
- Settings → Apps → OnionRoute Lite → Enable Allow background activity
onionroute-lite/
+-- app/
| +-- build.gradle # App-level build config
| +-- proguard-rules.pro # ProGuard rules
| +-- src/main/
| +-- AndroidManifest.xml
| +-- assets/tor/torrc.template # Base tor configuration
| +-- java/org/onionroute/lite/
| | +-- Constants.kt # All app constants
| | +-- MainActivity.kt # Main UI (Jetpack Compose)
| | +-- KillSwitch.kt # Kill switch logic
| | +-- service/
| | | +-- TorService.kt # Foreground service (core)
| | | +-- TorVpnService.kt # VPN routing service (IPv4+IPv6)
| | | +-- KindnessService.kt # Snowflake proxy service
| | | +-- AppSelectionManager.kt # Per-app routing rules
| | | +-- BootReceiver.kt # Auto-start on boot
| | | +-- ConnectTileService.kt
| | | +-- RefreshTileService.kt
| | +-- tor/
| | | +-- TorProcessManager.kt # Manages tor binary + torrc generation
| | | +-- TorControlConnection.kt # Control port client
| | | +-- BandwidthTracker.kt # Bandwidth stats
| | | +-- BridgeUpdater.kt # Runtime CI-verified bridge fetcher
| | | +-- BridgeConfig.kt # Bridge configuration
| | | +-- WebTunnelConfig.kt # WebTunnel bridge config
| | +-- ui/
| | +-- screens/ # Compose screens
| | +-- theme/Theme.kt # OnionRoute design system
| +-- res/ # UI resources
| +-- font/ # Bundled fonts (OFL 1.1)
| +-- mipmap-*/ # Launcher icon (per-density)
| +-- drawable-*/ # Adaptive icon foreground + camouflage icons
| +-- values/ # Strings, colors, styles
| +-- xml/ # Network security, VPN config
| +-- jniLibs/ # Native binaries (built from source)
+-- scripts/
| +-- build-all.sh # Build all native binaries
| +-- build-tor.sh # Build tor daemon
| +-- build-obfs4.sh # Build obfs4proxy
| +-- build-snowflake.sh # Build snowflake client + proxy
| +-- build-tun2socks.sh # Build hev-socks5-tunnel
| +-- build-webtunnel.sh # Build webtunnel-client
+-- metadata/
| +-- org.onionroute.lite.yml # F-Droid metadata
+-- fastlane/
| +-- metadata/android/en-US/ # F-Droid store listing
+-- .github/workflows/
| +-- android-build.yml # CI/CD pipeline
+-- LICENSE # GPL-3.0-or-later
+-- THIRD-PARTY-LICENSES.md # Font & dependency licenses
+-- PRIVACY_POLICY.md # Privacy policy
+-- CHANGELOG.md # Version changelog
+-- README.md # This file
OnionRoute Lite ships only ARM ABIs in the release APK to keep the file size small (~38 MB instead of ~77 MB). x86 and x86_64 are emulator-only ABIs and would double the APK size with no benefit to real Android users — ~99% of Android phones and tablets are ARM-based.
| ABI | In Release APK? | Target Devices |
|---|---|---|
armeabi-v7a |
✅ Yes | Older 32-bit ARM phones (Samsung Grand Prime Plus, etc.) |
arm64-v8a |
✅ Yes | Modern 64-bit ARM phones and tablets |
x86 |
❌ No | Android emulators, rare Intel tablets |
x86_64 |
❌ No | Android emulators (x86_64 images) |
Need x86/x86_64 for emulator testing? Build the debug variant locally:
./gradlew assembleFdroidDebug— the debug CI job builds armeabi-v7a + x86_64 so the app can be tested on standard Android emulators. The release APK is for production phone/tablet installs only.
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit your changes (
git commit -am 'Add my feature') - Push to the branch (
git push origin feature/my-feature) - Open a Pull Request
All contributions must be compatible with GPL-3.0-or-later.
OnionRoute Lite — A minimal Tor client for Android
Copyright (C) 2024 darkmaster0345
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Full license: LICENSE
| Component | License |
|---|---|
| OnionRoute Lite | GPL-3.0-or-later |
| Tor | BSD-3-Clause |
| obfs4proxy | BSD-3-Clause |
| Snowflake | BSD-3-Clause |
| hev-socks5-tunnel | GPL-3.0 |
| Bundled fonts | SIL Open Font License v1.1 |
A GitHub Actions workflow tests every bundled bridge approximately every 1-4 hours (scheduled for every 15 min, but GitHub Actions throttles scheduled workflows on free repos). Dead bridges (2+ consecutive failures) are automatically replaced with verified-alive bridges from Tor Project's BridgeDB. If no same-transport replacement is available, cross-transport fallback kicks in immediately (e.g. a dead webtunnel gets replaced with an alive obfs4). The app fetches fresh bridges at runtime from alive_bridges.json — no app update needed.
Last check: 2026-07-13T16:09:14+00:00
| Bridge | Type | Status | Bootstrap | Failures | History |
|---|---|---|---|---|---|
80.96.58.89:9130 |
obfs4 | ✅ alive | 0.13s | 0 | 🟩🟩🟩🟩🟩🟩 |
129.151.27.14:3721 |
obfs4 | ✅ alive | 0.06s | 0 | 🟩🟩🟩🟩🟩🟩 |
82.64.193.83:12867 |
obfs4 | ✅ alive | 0.09s | 0 | 🟩🟩🟨🟩🟩🟩 |
89.40.206.72:37171 |
obfs4 | ✅ alive | 0.02s | 0 | 🟩🟩🟩🟩🟩🟩 |
159.203.18.175:5846 |
obfs4 | ✅ alive | 0.02s | 0 | 🟩🟩🟩🟩🟩🟩 |
159.69.22.241:443 |
webtunnel | ✅ alive | 0.1s | 0 | 🟩🟩🟩🟩🟩🟩 |
67.205.142.190:9889 |
webtunnel | ✅ alive | 0.02s | 0 | 🟩🟩🟩🟩🟩🟩 |
192.0.2.3:80 |
snowflake | ✅ alive | 1.29s | 0 | 🟩🟩🟩🟩🟩🟩 |
History: 🟩 alive · 🟨 timeout · 🟥 dead · 🟧 error · ⬜ no data (last 6 checks)
How it works
- Approximately every 1-4 hours (scheduled every 15 min, throttled by GitHub Actions), a runner tests all bridges in
Constants.kt:- obfs4 / webtunnel: TCP connectivity check to the bridge's IP:port
- snowflake: HTTPS HEAD to the broker URL + UDP STUN Binding Request to each STUN server (the bridge line's IP is a virtual placeholder)
- If any bridge has 2+ consecutive failures:
- Fetch replacement bridges from Tor Project's BridgeDB (multiple requests per transport type)
- Filter out documentation/test addresses (RFC 3849 IPv6, RFC 5737 IPv4)
- TCP-test each candidate (5s timeout)
- Swap dead bridges for verified-alive ones of the same transport
- Cross-transport fallback: if no same-transport replacement exists, swap for an alive bridge of any transport immediately (e.g. dead webtunnel → alive obfs4)
- Snowflake is never swapped (the bridge line is a fixed virtual placeholder pointing at the Tor Project broker; if snowflake is dead, it's an infrastructure issue, not a bridge-line issue)
- Write
alive_bridges.jsonwith all alive bridges (fetched by the app at runtime) - Commit changes to
main— the app picks up new bridges automatically
The app uses a 3-layer bridge sourcing strategy:
- Runtime JSON — fetches
alive_bridges.jsonfrom GitHub (freshest, CI-verified) - Local cache — last successful fetch (works offline, 24h validity)
- Hardcoded —
Constants.ktfallback (auto-swapped by CI)
Workflow: .github/workflows/bridge-health.yml
