The phone reboots, always-on is enabled, "block connections without VPN" is on, and the tunnel never comes up. Nothing is misconfigured: the block is doing its job, and the client's own attempt to reach its endpoint is subject to it. The endpoint has to be resolved before the tunnel exists, by a client that is allowed to talk to the network while every other app is not. Understanding which component does that, and in what order, is what turns this from a mystery into a configuration question.

What the VPN builder actually asks for

An Android VPN app does not hand the platform a server address. It builds a local interface: addresses and routes for the tunnel, the resolvers apps should use inside it, and then establish() returns a file descriptor that the app reads and writes as raw IP packets. The remote endpoint never appears in that builder at all.

That is the key structural fact. The platform creates a tun interface and enforces policy on it; the app implements the transport in userspace, which means the app itself resolves its endpoint name and opens the encrypted connection. A hostname endpoint is therefore possible — not because the platform supports names, but because the platform never sees the endpoint.

One builder field does take addresses: the DNS servers. Those must be numeric resolver addresses, usually ones the operator wants queried through the tunnel. That field describes where apps send queries after the tunnel is up. It says nothing about how the client finds the endpoint, and treating the two as the same setting is where the confusion starts.

Two DNS questions, not one

The first question is: where do apps send queries once the tunnel is up? The answer is a resolver address inside the tunnel, reachable through the tun interface, numeric by construction.

The second question is: how does the client find its own endpoint before that tunnel exists? That lookup happens outside the tunnel, on the underlying network, in the client's process. It can use a name, and it must use a resolver the device can reach without the VPN.

StageWho resolvesPathFailure looks like
Endpoint lookupThe VPN appUnderlying network, protected socketNever connects; no tunnel interface traffic
Peer authenticationClient and serverTunnel transportConnects then drops; certificate or key error
Private name lookupApps via the tunnelResolver inside the tunTunnel up, internal sites do not resolve

Three stages, three different symptoms. A single failure message that says "cannot connect" collapses them, which is why people end up editing the DNS-server field to fix a problem that happens before it is ever consulted.

Why the bootstrap can deadlock

Always-on with the block enabled means traffic that is not inside the VPN is dropped, including traffic from the app that owns the VPN. The escape hatch is explicit: the app calls protect() on a socket, which binds it to the underlying network so it bypasses the tunnel and is allowed out. Every VPN client has to do this for its own transport, or it could never reach a server.

Now consider a deployment where the only resolver that knows the endpoint name is itself inside the tunnel. The client needs the tunnel to resolve the name, and the name to build the tunnel. That is a genuine circular dependency, and no amount of client configuration resolves it. It is a design error, not a bug.

Timing exposes the same dependency in milder forms. At boot before the first unlock, the device may have no usable network yet; on a captive portal, the network exists but blocks everything except the portal; on a Wi-Fi to cellular handover, the protected socket's underlying network changes and the client has to notice and re-resolve. Each of those is a bootstrap event, and each is worth testing deliberately rather than discovering at 3 a.m.

Why pinning the answer is not a fix

The tempting workaround is to resolve the name once and paste the resulting address into a numeric field, or to hardcode it in a custom build. It appears to work, and it converts a name into a snapshot. When the operator moves the endpoint — a migration, an anycast change, a new regional address — the client keeps dialling the old one and nothing in the UI explains why.

DNS records carry a time to live precisely so that answers can change; caching is defined in terms of that lifetime rather than forever. Pinning an answer discards the mechanism that makes a name better than an address, while keeping the operational cost of having used a name.

Pinning also tends to travel with a second bad habit: relaxing identity checks because the connection now fails differently. Server identity verification is not part of the bootstrap problem, and weakening it exchanges a DNS inconvenience for an interception risk.

Designs that survive a reboot

The durable arrangement is unglamorous. Use a client that implements protected sockets correctly and resolves its endpoint on the underlying network, and give it an endpoint name that resolves publicly, or through a resolver reachable before the tunnel. Keep the block enabled; disabling it wholesale to make DNS work removes the property the deployment exists to provide.

Where the endpoint must stay private, provide an explicit bootstrap path the platform can use — a resolver reachable without the tunnel, or a management connection the operator maintains — and keep that exception narrow and owned by policy rather than by a user toggle. The Android VpnService documentation is the reference for what the platform guarantees about protection and lifecycle, and the VpnService.Builder reference confirms that the DNS field takes numeric addresses.

Then verify in the three stages, in order, on a fresh connection after a reboot: the endpoint name resolves, the tunnel establishes and the peer authenticates, and a private name resolves through the tunnel's resolver. If stage one fails, nothing later can work, and the fix belongs in the bootstrap path — never in the resolver field.

  • Endpoint resolution happens in the app, outside the tunnel, on a protected socket.
  • The DNS field in the builder is for apps inside the tunnel, not for the client's own endpoint.
  • A resolver only reachable through the tunnel is a deadlock, not a slow start.
  • Test endpoint, authentication and private lookup separately after a reboot.

Pennadoù liammet