Tether
Tether is a personal residential proxy we designed and built — a native Android app and relay server that turn your own phone into a secure HTTP/HTTPS proxy, so cloud workloads can reach IP-restricted APIs from your home IP with just two environment variables.
- Year
- 2025
- Service
- Product Design & Engineering
- Industry
- Networking & Developer Tools
- Size
- In-house product

Introduction
Tether is an internal security product we engineered at Keplaris to solve a problem that kept surfacing in our own cloud work: serverless workloads live on datacenter IP addresses, and a growing number of APIs simply refuse to talk to datacenter IP addresses. Tether is our answer: a personal residential proxy system in which a native Android app turns one of our own phones into a secure HTTP and HTTPS proxy egress point. A function on GCP Cloud Run, Cloud Functions, Netlify, or Vercel routes its outbound requests through the phone and reaches IP-restricted APIs from a home or mobile IP instead of a cloud provider's address block. The system is two pieces of software written from scratch: an Android app built in one hundred percent Kotlin with Jetpack Compose and Material 3, and a relay server in Node.js and TypeScript, packaged as a Docker multi-stage Alpine build that runs on any small cloud VM.
We built Tether for ourselves, on our own infrastructure, with our own threat model in mind. This case study walks through the problem, the architecture we landed on, and the engineering that makes a phone a dependable piece of network infrastructure.
The challenge
Anyone who runs serverless workloads eventually collides with the same wall. Some APIs geo-restrict by IP. Others rate-limit or outright block traffic from known cloud provider ranges, because that is where the bots live. Your code is correct, your credentials are valid, and the request still fails because of where it came from. The existing workarounds are all unappealing in their own way.
- Commercial residential-proxy services are expensive, and worse, they route your traffic through strangers' devices. For anything touching credentials, that is a trust decision we were not willing to make.
- Self-hosted alternatives fight NAT. A proxy running on a residential connection usually cannot accept inbound connections at all, because home routers and carrier-grade NAT make the device unreachable from the public internet.
- Routing everything through a VPN or a static egress gateway moves the problem rather than solving it: the egress IP is still a datacenter IP, just a different one.
What we actually wanted was a bring-your-own-IP setup: your phone, your internet connection, your APIs, with nobody else's hardware in the path. The hard part is reachability. A phone on WiFi or mobile data sits behind layers of NAT with no stable public address, so the conventional model of a proxy that listens for connections does not apply.
Our approach
We inverted the architecture. Instead of cloud workloads connecting to the phone, the phone dials out. The Android app initiates a single persistent encrypted WebSocket tunnel over WSS to a small relay server we control, and holds that tunnel open indefinitely. Because the relay never dials the phone, the design works through any NAT, including carrier-grade NAT, with no port forwarding or router configuration at all. The phone stays unreachable from the internet; reachability flows entirely through the outbound tunnel.
The relay is the only component with a public address, and it is deliberately small: a Node.js and TypeScript service in a Docker multi-stage Alpine image, deployable on any modest cloud VM. To the cloud workload, it presents a completely standard HTTP and HTTPS proxy interface using the CONNECT method with Basic authentication. Because that interface is the proxy protocol every HTTP client already understands, integration is two environment variables, HTTP_PROXY and HTTPS_PROXY, and zero code changes in the workload. There is no SDK to write or maintain. A Cloud Run service, a Netlify function, and a Vercel deployment all pick up the proxy the same way, and the destination API sees every request arriving from a residential or mobile IP.
On the device side, we treated the app as infrastructure rather than as an app. It is written entirely in Kotlin with a two-screen Jetpack Compose UI: a Setup screen for the relay host, pairing token, credentials, and certificate fingerprint, and a Status screen showing live connection state, an egress-IP self-test, bytes in and out, and the active stream count. It runs on anything from Android 8.0 up.
Inside the tunnel: framing, multiplexing, and staying alive
A custom binary protocol over one socket
One phone has to serve many concurrent proxy connections, but the architecture allows exactly one outbound tunnel. So we designed a custom binary framing protocol that multiplexes everything over that single WebSocket. Each frame carries a one-byte type and a four-byte stream identifier, letting the relay and the app interleave many independent streams on one socket. Four frame types carry the protocol: AUTH establishes the session, OPEN starts a proxied connection, DATA moves payload bytes in either direction, and CLOSE tears a stream down without disturbing its neighbors. PING and PONG heartbeats flow roughly every twenty-five seconds, detecting dead tunnels quickly and keeping NAT mappings from silently expiring.
Security at every hop
A system that bridges cloud traffic into a home network has to be paranoid by default, so we layered the defenses. Devices join the relay through a pairing-token handshake, and per-device credentials are stored bcrypt-hashed on the relay, so a compromised relay disk yields no usable secrets. The app pins the relay's TLS certificate, either trust-on-first-use or against an explicitly configured fingerprint, shutting down man-in-the-middle attacks. On the device, all secrets live in EncryptedSharedPreferences under AES-256-GCM. On the proxy side, optional client IP allowlists restrict who may use the egress, and connection rate limiting bounds abuse: a global cap of 512 connections, 64 per client IP, and at most 120 new connections per IP in any 60-second window.
Making a phone behave like a server
Android aggressively kills background work, which is exactly wrong for network infrastructure. The app runs as a foreground service holding a wakelock, requests a battery-optimization exemption, restarts sticky if the system reclaims it, and can optionally auto-start on boot. When the tunnel drops, reconnection uses exponential backoff starting at one second and doubling to a thirty-second cap, with jitter so devices never stampede the relay. A ConnectivityManager hook triggers an instant reconnect the moment the phone switches between WiFi and mobile data, rather than waiting out a backoff timer.
Key capabilities
Tether's surface area is intentionally small, but every capability in it earns its place.
- Phone-initiated tunneling over a single persistent encrypted WebSocket, working through any NAT or carrier-grade NAT with no router or carrier configuration.
- Custom binary framing with a one-byte type and four-byte stream id, multiplexing many concurrent proxy connections as independent streams over one socket.
- Standard CONNECT-method HTTP and HTTPS proxying with Basic auth, so integration is just HTTP_PROXY and HTTPS_PROXY environment variables and zero workload code changes.
- Defense in depth: pairing-token handshake, bcrypt-hashed per-device credentials, TLS certificate pinning, AES-256-GCM encrypted on-device secret storage, optional IP allowlists, and layered connection rate limits.
- Always-on reliability through a foreground service with wakelock, sticky restart, optional boot auto-start, jittered exponential-backoff reconnect, and instant recovery on WiFi-to-mobile transitions.
- A two-screen Compose UI covering setup and live status, including an egress-IP self-test, byte counters, and the active stream count.
- A relay that deploys anywhere: Node.js and TypeScript in a multi-stage Alpine Docker image sized for any small cloud VM.
Results
Tether does exactly what we built it to do: our cloud workloads now reach IP-restricted APIs through an egress IP we own, on hardware we own, over a network we pay for, with no third-party proxy vendor in the path. The integration promise held up in practice: pointing a workload at Tether really is two environment variables, with no application code written to accommodate it. The phone-initiated design has proven itself against the messy realities of residential and mobile networking, where heartbeats, jittered backoff, and the connectivity hook keep the tunnel current as devices roam. Both halves are unit-tested, with Vitest on the relay and JUnit on the app's JVM logic, and the egress-IP self-test on the Status screen answers the only question that matters at a glance: is traffic actually leaving from the right address.
What's next
Tether remains an internal Keplaris product, and we intend to keep sharpening it as our own usage grows. The directions we are exploring follow naturally from the architecture: deeper operational visibility on the relay, smoother management as more devices come online, and continued hardening of the protocol and its limits under real traffic. More broadly, Tether has become a reference point for how we build at Keplaris: take a problem the market answers with expensive, trust-compromised services, and engineer a small, owned, auditable system that solves it outright.
Get in touch.
Whether you have questions or just want to explore what's possible, we're here to help.
