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, <redacted> 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 <redacted> [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: <redacted> 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 <redacted> [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: <redacted> 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 <redacted>
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:<redacted> Secret:<redacted>
UserToken:<redacted> MTU:1280 DNS: UpstreamDNS:[] PublicDNS:[] InterfaceName:
MatchDomains:[] Holepunch:true TlsClientCert: PingIntervalDuration:10s
PingTimeoutDuration:30s OrgID:<redacted> FileDescriptorTun:146 FileDescriptorUAPI:0
EnableUAPI:false OverrideDNS:true TunnelDNS:false ... DisableRelay:false
PreferLocalRoutes:false}
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_MISSINGand 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
internet.v6.telekom(IPv6-only APN, Telekom Germany / its MVNOs), DNS servers2a01:598:7ff:0:10:74:210:221/:222internet.telekom(dual-stack), or any network providing an IPv4 resolverHostnames, org id, keys and tokens are redacted below as
pangolin.example.com,<redacted>etc.To Reproduce
Failing case (IPv6-only DNS)
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]:53fails.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:
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 IPv6line; every session configures onlySet IPv4 addresses: [100.90.x.x].Android's
VpnServicemakes 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 withVpnService.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:
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: