English | 简体中文
An Android client that turns an HTTP / SOCKS5 proxy into a system-wide VPN.
Traffic from every app — or only the ones you pick — is captured by VpnService as raw IP
packets, reassembled into TCP / UDP sessions in userspace, and forwarded through the upstream
proxy. Apps with no proxy support of their own end up going through it anyway.
- Profile management: multiple SOCKS5 / HTTP CONNECT profiles with username/password auth; duplicate, edit and delete, plus a connectivity test that performs a real handshake before saving
- Per-app routing: three modes — all apps / allow list (only the selected ones are proxied) / bypass list (the selected ones go direct)
- Dark mode: follow system, force light, force dark, plus Android 12+ dynamic colour
- Notification status: an ongoing notification with live upload/download counters and a one-tap disconnect
- Activity log: live throughput bar plus a colour-coded event stream recording DNS lookups and the outcome of every connection, labelled with the app that opened it
- DNS resolution mode: each profile can send DNS through the proxy (privacy) or resolve locally (speed)
- Tablets and large screens: at 600dp of window width the bottom bar becomes a navigation rail, content is centred within a readable width, and the editor becomes a centred card
- English and Simplified Chinese, start on boot, stop when the network drops, bypass private networks, IPv6 leak blocking
Requires JDK 17+ and Android SDK Platform 36.
./gradlew assembleDebug # debug build
./gradlew assembleRelease # release build (R8 enabled)
./gradlew testDebugUnitTest # unit testsArtifacts land in app/build/outputs/apk/.
sdk.dir in local.properties points at the local SDK — change it on another machine.
The usual approach is to bundle a native library such as tun2socks, which drags in the NDK
toolchain. This project instead implements a trimmed-down userspace TCP/IP stack in Kotlin: no
NDK, no native artifacts, one build for every ABI.
The key simplification: packets written to the TUN device are handed to the local kernel and
never cross a lossy link, so congestion control is unnecessary. As long as the peer's advertised
receive window is respected nothing is dropped, and retransmission exists only as a backstop.
That keeps TcpSession far simpler than a complete TCP implementation while staying reliable
in this local setting.
app → system routing → TUN device
↓ reader thread
Ipv4Header / TcpHeader / UdpHeader parsing
↓
TcpSession (userspace state machine) / UdpSession
↓ protect()-ed socket
ProxyClient (SOCKS5 handshake / HTTP CONNECT)
↓
upstream proxy → destination
Return traffic is re-encapsulated into IP packets by PacketBuilder and sent back to the TUN
device through a write queue.
- The proxy hostname must be resolved up front: once the tunnel is up, DNS queries are routed
into it — and the tunnel itself depends on that lookup.
FreeProxyVpnServicetherefore resolves the proxy host beforeestablish()and reuses the address for the whole session. - Bypassing LAN traffic is routing, not dropping:
RouteCalculatorrecursively subtracts RFC 1918 and friends from0.0.0.0/0and declares only the complement to the TUN device, so private addresses keep using the physical interface. - Automatic DNS downgrade: when the proxy cannot carry UDP (an HTTP proxy, or UDP ASSOCIATE turned off), DNS falls back to DNS-over-TCP (RFC 7766) through the proxy, so name resolution keeps working.
- An empty allow list degrades on purpose: never calling Android's
addAllowedApplicationis equivalent to "allow everything", which is easy to misread, so an empty allow list falls back to global mode. - Where the hostnames in the log come from: inside the tunnel only IP addresses are visible.
HostRegistryparses the DNS responses passing through and builds a reverse IP → hostname table, which is why connection entries can showgithub.com:443instead of a bare IP. - App attribution in the log:
ConnectivityManager.getConnectionOwnerUidmaps the 4-tuple back to an owning UID and then to a package name. That API arrived in Android 10; on older systems the column stays empty. - Large screens are centred, not margin-computed:
ResponsiveContainerconstrains content to 640dp by centring it. Computing left/right margins from the screen width was tried first, but the navigation rail eats part of the width and the result drifts to one side. The breakpoint measuresLocalWindowInfo.containerSizerather than the screen, so split-screen and foldables are handled correctly.
Neither Hilt nor Room is used: this app has one process, two repositories, one long-running service, and a few dozen profiles at most. A DataStore-backed JSON blob plus a hand-written container is enough — dropping the annotation processors makes builds faster and version conflicts rarer.
The toolchain is pinned at AGP 8.13.2 + compileSdk 36. Newer androidx.core:core (1.19+) and
androidx.lifecycle (2.11+) require AGP 9.1 / compileSdk 37, so Android SDK Platform 37 has to be
installed before upgrading.
| Limitation | Detail |
|---|---|
| No IPv6 forwarding | IPv4 only. "Block IPv6" pulls IPv6 into the tunnel and drops it so it cannot leak around the proxy |
| No ICMP | Proxy protocols cannot carry it, so ping inside the tunnel failing is expected |
| UDP depends on the proxy | Needs SOCKS5 with UDP ASSOCIATE; otherwise only DNS works (over TCP) |
| No window scaling | TCP Window Scaling is not negotiated, so per-connection throughput is capped by the 64KB window |
| No fragment reassembly | Fragmented IP packets are dropped; the TUN MTU is set by this app, so normal traffic never fragments |
| Credentials stored in plaintext | Proxy passwords live unencrypted in the app's private directory, excluded from cloud backup |
| App attribution needs Android 10+ | It relies on getConnectionOwnerUid; older systems leave the column empty |
Store assets live in fastlane/metadata/android/{en-US,zh-CN}/; F-Droid picks up the
descriptions, icon and screenshots from this repository automatically. Every release needs a new
changelogs/<versionCode>.txt. After changing the icon, run art/render-icon.sh to re-export the
512×512 images/icon.png.
To submit: tag the release as vX.Y.Z, then copy fdroid/tech.xvanturing.freeproxy.yml into
metadata/ in fdroiddata and open a merge request. Once
merged, F-Droid's build server compiles from source and signs with their own key — meaning the
F-Droid build and any self-signed APK are not upgrade-compatible.
app/src/test/ holds 19 unit tests covering route-complement calculation (including boundary
addresses and overlap checks), IP/TCP/UDP packet build-parse round trips, checksum self-consistency,
sequence-number wraparound comparison, and DNS message parsing (compression pointers, malformed
messages, and infinite-loop defence against self-referencing pointers).
The full path needs a real device: install, add a proxy profile, tap connect and grant the VPN permission.