fosrl / fosrl/android

Client cannot resolve exit node endpoint on IPv6-only networks — hole punch never starts, registration fails with HOLEPUNCH_MISSING

Open
#42 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Kotlin
Stars
43
Forks
10
Avg merge
1m
Merged PRs (30d)
1

Description

### Describe the Bug

## Summary

On a mobile network that hands out **only IPv6 DNS servers**, the Android client fails to resolve the exit node hostname for UDP hole punching. No hole punch packet is ever sent, so the server aborts registration with `HOLEPUNCH_MISSING` and the app hangs in the "registering" state indefinitely.

The same device, app version and server work fine as soon as the network provides an IPv4 DNS server — including full relay fallback.

The server's error message ("ensure your client can reach the server on UDP port 21820") is misleading here: the client never attempts to reach it, because name resolution failed first.

### Environment

| | |
|---|---|
| App | Pangolin Android **0.4.1** |
| olm version reported | 0.4.1 |
| Device | Pixel 8a, Android 17, aarch64 |
| Pangolin server | 1.21.1 (enterprise build) |
| newt (site) | 1.15.0 |
| Exit node | hostname, UDP 21820 |
| Failing network | mobile data on APN `internet.v6.telekom` (IPv6-only APN, Telekom Germany / its MVNOs), DNS servers `2a01:598:7ff:0:10:74:210:221` / `:222` |
| Working network | same SIM on APN `internet.telekom` (dual-stack), or any network providing an IPv4 resolver |

Hostnames, org id, keys and tokens are redacted below as `pangolin.example.com`, `` etc.

### To Reproduce

## Failing case (IPv6-only DNS)

```
12:59:30.608 Applying system DNS: [[2a01:598:7ff:0:10:74:210:221]:53 [2a01:598:7ff:0:10:74:210:222]:53]
12:59:30.612 Created shared UDP socket on port 63563 (refcount: 2)
12:59:30.662 Got exit nodes for hole punching: [{pangolin.example.com 21820 [1]}]
12:59:30.662 Starting hole punch for 1 exit nodes
12:59:30.662 Starting UDP hole punch to 1 exit nodes with shared bind
12:59:30.666 Failed to resolve endpoint pangolin.example.com: DNS lookup failed using all
upstream servers: lookup pangolin.example.com on [::1]:53:
dial udp [2a01:598:7ff:0:10:74:210:222]:53: connect: no route to host
12:59:30.666 No exit nodes available yet, waiting for nodes to be added
12:59:30.849 Websocket Connected
12:59:31.350 Sending registration message to server with public key: and relay: false
... olm/wg/register repeated every 2s ...
13:00:05.556 Received olm error message: map[code:HOLEPUNCH_MISSING
message:Unable to coordinate client P2P connection. Please ensure your client
can reach the server on UDP port 21820 and try registering again.]
```

**The key detail:** the websocket connects to the *same hostname* 183 ms after the resolver failed on it. The platform resolver works; only the client's internal resolver on `[::1]:53` fails.

This reproduced 8 times in a row over ~40 minutes, byte-identical each time.

## Working case (IPv4 DNS available)

Same device, same app version, same server, same session of the log, ~45 minutes earlier:

```
12:13:21.175 Applying system DNS: [9.9.9.9:53 149.112.112.112:53]
12:13:21.639 Got exit nodes for hole punching: [{pangolin.example.com 21820 [1]}]
12:13:21.639 Starting hole punch for 1 exit nodes ← no resolve error
12:13:21.737 Websocket Connected
12:13:22.239 Sending registration message to server with public key: and relay: false
12:13:22.603 The tunnel IP is: 100.90.128.2/20 ← registration completes
12:13:22.603 Set IPv4 addresses: [100.90.128.2], subnet masks: [255.255.240.0]
12:13:22.603 Set MTU: 1280
12:13:22.613 Configured peer
12:13:33.081 Local endpoint 192.168.178.200:63131 for site 1 failed 3 times,
falling back to public/relay logic
12:13:35.899 Rapid fallback test: site 1 unreachable on public endpoint after local
fallback, requesting relay
12:13:35.943 Received relay-peer message: map[relayEndpoint:pangolin.example.com
relayPort:21820 siteId:1]
12:13:35.987 Adjusted peer 1 to point to relay!
12:13:41.138 WireGuard connection to site 1 is CONNECTED
```

The device was **not** on the site's LAN in this session — the local endpoint probe failed and the client correctly fell back to relay. So relay works; the only variable between working and failing is whether an IPv4 DNS server is present.

## Suspected cause

The tunnel interface is **IPv4-only**. Across the whole 9119-line log there is not a single `Set IPv6` line; every session configures only `Set IPv4 addresses: [100.90.x.x]`.

Android's `VpnService` makes IPv6 destinations unreachable (`no route to host`) for traffic subject to a VPN that has no IPv6 address/route. The internal DNS proxy's upstream socket appears not to be protected with `VpnService.protect()`, so once the TUN is established its queries to IPv6 resolvers are blackholed.

On a dual-stack network this is invisible, because the IPv4 resolver in the same list still answers. On an IPv6-only network there is no fallback and hole punching never starts.

### Expected Behavior

The exit node hostname should resolve regardless of the network's address family — e.g. by protecting the resolver socket, resolving before the TUN is established, or falling back to the platform resolver (which demonstrably succeeds, as the websocket connection proves).

## Additional note: the DNS setting in the app does not reach this code path

Setting a custom DNS server (9.9.9.9) in the app's settings does not help. The value never appears in the tunnel config. Across all 8 failing attempts:

```
UpstreamDNS:[] PublicDNS:[] DNS:
```

The only field that changed after editing the setting was `TunnelDNS: true → false`. So there is currently no user-side workaround within the app.

Full tunnel config of a failing session:

```
{Endpoint:https://pangolin.example.com ID: Secret:
UserToken: MTU:1280 DNS: UpstreamDNS:[] PublicDNS:[] InterfaceName:
MatchDomains:[] Holepunch:true TlsClientCert: PingIntervalDuration:10s
PingTimeoutDuration:30s OrgID: FileDescriptorTun:146 FileDescriptorUAPI:0
EnableUAPI:false OverrideDNS:true TunnelDNS:false ... DisableRelay:false
PreferLocalRoutes:false}
```

Contributor guide

Open the contributing guide

Research direction

Start by tracing the internal DNS proxy and UDP hole-punch endpoint resolution around the Android VpnService setup, then compare it with the websocket hostname resolution that succeeds. Verify the resolver works on IPv6-only networks after the TUN is established, and confirm that hole punching starts and registration completes without HOLEPUNCH_MISSING.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, kotlin
Domain
mobile-dev, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.