A local proxy has two different network relationships: the client-to-phone connection and the phone-to-destination connection. Treating them as one route creates the wrong mental model.

The client Socket belongs to ingress

When a laptop reaches CellRoute over Wi-Fi, Android accepts that TCP connection on the Wi-Fi-facing local interface. In hotspot mode, the accepted connection belongs to the hotspot LAN path. That Socket must remain reachable over the same ingress network so bytes can continue flowing between the client and phone.

Trying to bind the accepted client Socket to cellular does not turn its traffic into internet egress. It risks breaking the local path that made the connection possible.

The target Socket chooses egress

After parsing and authorizing a proxy request, CellRoute creates a second Socket for the requested destination. In simplified Kotlin, the operation looks like this:

val socket = cellularNetwork.socketFactory.createSocket()
socket.connect(targetAddress, timeout)

The production path also handles DNS, destination ACLs, timeouts, failures, accounting and cleanup. The important point is scope: each outbound target Socket is created on the cellular Network. The app does not need to bind the entire process or change Android’s route table.

DNS should follow the same Network

When the client supplies a host name, resolving it through cellularNetwork.getAllByName(host) keeps the lookup aligned with the intended egress. CellRoute checks the resulting IP addresses before opening the target connection.

If the cellular Network disappears, new target connections must wait for a usable Network. Existing TCP connections may close; they cannot be assumed to migrate to a replacement Network.