An Android VPN client that runs a real Tor client in-process, picks a working bridge by racing several pluggable transports at once, and remembers which bridges actually worked.
The whole client is bundled: libtor (Tor), libobfs4proxy (lyrebird) and hev-socks5-tunnel
(tun2socks) are compiled into the APK. There is no external Tor installation and no companion app
to install alongside it.
Grab the APK that matches your device from the releases page:
| Asset | Use it on | Size |
|---|---|---|
deltator-v2.0.0-arm64-v8a.apk |
almost every phone and tablet made in the last decade | ~22 MB |
deltator-v2.0.0-armeabi-v7a.apk |
32-bit ARM devices | ~22 MB |
deltator-v2.0.0-universal.apk |
anything else, carries every ABI | ~38 MB |
x86 and x86_64 payloads only contain tun2socks, because Tor and lyrebird are not built for
x86. The app is therefore meant for real ARM devices; x86 emulators will not work.
Requirements: Android 7.0 (API 24) or newer. The APK is signed with the release key, so Android will ask you to allow installs from your browser the first time.
On connect, four Tor clients are started in parallel, each on its own port, each with its own
torrc:
| Runner | Pluggable transport | Bridges |
|---|---|---|
vanilla |
none | direct bridges |
obfs4 |
lyrebird | obfs4 bridges |
webtunnel |
lyrebird | webtunnel bridges |
memory |
inherited per bridge | the bridges that worked last time |
The first runner that reaches 100% bootstraps wins, the others are stopped, and the VPN is brought up on the winner. Progress is reported per runner in the notification, and a runner that stalls is skipped by the bootstrap timeout.
memory has no bridges of its own. It is populated from what the app learns at runtime:
- Whenever a runner reports
DoneorBootstrapped 100%, the descriptor Tor printed (bridge (cached): …/bridge (fresh): …) is read back out of the log. - Those fingerprints are stored per transport, together with every bridge that already worked for the other transports, in a bounded list (60 per transport).
- The next connect starts a fourth runner over exactly those bridges.
The first connect therefore runs three runners. Every connect after the first one that succeeded runs four. If a memory bridge stops working it simply drops out of the race and the normal transports take over.
Lists come from Delta-Kronecker/Tor-Bridges-Collector and are resolved in this order:
- the cached list on the device,
- the copy bundled inside the APK (so a fresh install works before the first download),
- the network.
snowflake comes from the collector's snowflake.txt, like the others, with a bundled copy as the
offline fallback.
webtunnel is assembled from two files, webtunnel.txt and webtunnel_ipv6.txt. They are merged
by fingerprint, alternating between the two so both sources are represented once Tor's per-runner
line cap applies. A bridge that is republished with a newer ver= replaces its older twin in
place, because a stale webtunnel protocol version is rejected during the handshake.
Bridge counts in the app are therefore the number of distinct bridges, not the number of lines in the files.
ADVANCED has a CONNECT VIA dropdown: auto, vanilla, obfs4, webtunnel, snowflake,
direct (no bridge at all, straight to a guard) or custom, where you paste your own bridge
lines. Every mode except auto runs exactly one transport.
auto races several transports at once and keeps the first that reaches 100%. The AUTO RACERS
list below the dropdown decides which ones take part, so you can, for example, drop webtunnel on
a network without IPv6. At least one must stay ticked. The memory runner joins automatically once
it has bridges that provably worked, and it is not part of that list.
The selection is applied on the next connect.
Any number of countries can be selected; the app writes them as ExitNodes with StrictNodes 0.
The most used countries are offered first. After connecting, the app resolves the real exit IP
through Tor and shows the country and the address, so a bad exit is visible at a glance.
Three things make a pinned exit country actually work, and all three are needed:
StrictNodes 0, not1. With1Tor refuses to build any circuit that does not exit through one of the selected nodes, so when those relays are slow, guarded or unreachable no circuit can ever complete and bootstrap stops at 50%. With0the list is a strong preference: Tor still steers into those countries, but a circuit can always be finished.- A geoip database. Tor can only resolve
ExitNodes {us}if it knows which country a relay is in, and the bundled Tor binary ships no geoip, so the rule used to match nothing and the selection was ignored in silence. The database is generated on demand from the range table the APK already carries (assets/geoip/country.csv.gz), converted to Tor'slow,high,CCformat, for the selected countries only. Bothgeoipandgeoip6are written, and they are rebuilt when the selection changes. LearnCircuitBuildTimeout 1with a 180 secondCircuitBuildTimeout, appended after the template so they win. A circuit whose exit has to come from a chosen country is a much narrower draw than a random one, and the shipped template ends with a 20 second timeout and learning switched off, which abandoned every attempt before it could finish.
- Ring button — connect, or reconnect. It is the only thing you need for day to day use.
- Live speed — shown in the notification, together with the running totals. Swipe the app away and you still see the throughput.
- ADVANCED — transport mode, the auto racer list, custom bridges, exit countries, bridge counts, an in-app log with per-transport filters, and a manual bridge refresh. The screen is a lazy list, so it opens without waiting on the country list or the torrc editor.
- Log filters —
ALL,VANILLA,OBFS4,WEBTUNNEL,SNOWFLAKE,MEMORY,DIRECTandCUSTOM;MEMORYshows what the memory runner was given and whether it updated.
Stopping the VPN and disconnecting are separate actions: stopping tears the tunnel down but keeps Tor alive, disconnecting starts a fresh race.
Everything is built by Gradle and the NDK; the toolchain is pinned in the build files
(compileSdk 36, ndk 29.0.14206865, JDK 17).
./gradlew assembleDebug # per-ABI debug APKs
./gradlew assembleRelease # signed per-ABI release APKs + universalNative code lives in app/src/main/cpp: hev-socks5-tunnel is vendored and built with
ndk-build, and Application.mk sets APP_ABI := arm64-v8a armeabi-v7a x86 x86_64.
Release signing reads the keystore from ANDROID_KEYSTORE_FILE plus the
ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_PASSWORD, ANDROID_KEY_ALIAS and
ANDROID_KEYSTORE_STORE_TYPE environment variables. For a local build, drop an ignored
keystore.properties next to the module instead. Never commit a keystore.
The bridge list files are plain text, one bridge per line, in Tor's own format, and are also
committed under app/src/main/assets/bridges/ so the app has something to work with before its
first download.
| Workflow | Trigger | Does |
|---|---|---|
android-build.yml |
push to main/android, manual |
debug APKs as an artifact |
release.yml |
tag v*, manual with a tag input |
signed APKs, signature verification, GitHub release |
Both workflows install the SDK, build-tools and the pinned NDK themselves, so no runner setup is needed.
- No account, no analytics, no telemetry, no crash reporting.
- The only outbound requests are the bridge list download, the update check, and a single one-time request per installation used to count installs.
- All traffic goes through Tor once the VPN is up.
Source is provided as is. Tor and lyrebird are BSD 3-Clause, hev-socks5-tunnel is MIT; the exact
terms of every vendored component are in its own LICENSE file under
app/src/main/cpp/hev-socks5-tunnel-src/.