-
Notifications
You must be signed in to change notification settings - Fork 20
Description
Describe the Bug
The Pangolin iOS client (OLM) fails to register on IPv6-only mobile networks (e.g., T-Mobile Germany 5G with NAT64/DNS64). The WebSocket connection (TCP) establishes correctly over IPv6, but the UDP hole punch never sends a single packet, causing the client to be stuck in "Registering" state indefinitely.
The root cause appears to be that the hole punch code resolves base_endpoint to an IPv4 address and opens an AF_INET UDP socket, which cannot function on an IPv6-only network. On NAT64 networks, there is no native IPv4 stack available — only TCP connections benefit from the system's Happy Eyeballs / NAT64 translation.
Actual Behavior
The iOS client connects the WebSocket (TCP) successfully over IPv6 but gets stuck at "Registering" because the UDP hole punch never completes.
Diagnostic Evidence
1. WebSocket connects, but server receives no hole punch
Pangolin server logs show the WebSocket is established, but the hole punch timestamp is never updated:
[info]: Establishing websocket connection
[info]: Client added to tracking - OLM ID: 72jspdj84knpeu4
[info]: WebSocket connection fully established and ready - OLM ID: 72jspdj84knpeu4
[info]: Handling register olm message!
[info]: Public key mismatch. Updating public key and clearing session info...
[warn]: Client last hole punch is too old and we have sites to send; skipping this register
[warn]: Client last hole punch is too old and we have sites to send; skipping this register
[warn]: Client last hole punch is too old and we have sites to send; skipping this register
... (repeats every 2 seconds indefinitely)
2. tcpdump confirms zero UDP packets from iPhone
Full packet capture on the server's eth0 interface with tcpdump -i eth0 -n 'udp' shows no UDP packets from any T-Mobile IPv6 prefix or NAT64 address while the iPhone is attempting to connect. All captured UDP traffic belongs to other (IPv4) sites that are functioning correctly.
A separate capture filtered for IPv6 UDP (tcpdump -i eth0 -n 'ip6 and udp') shows 0 packets from the iPhone, while UDP from other IPv6 sources (e.g., another Hetzner VPS) arrives correctly.
3. Server infrastructure is correctly configured
- Hetzner Cloud Firewall: UDP 51820 open for both "Any IPv4" and "Any IPv6" ✅
ip6tables: default policy ACCEPT, no DROP rules ✅docker-proxy: listening on both0.0.0.0:51820and[::]:51820✅- IPv6 UDP from other hosts arrives correctly (verified with test from another VPS) ✅
4. tcpdump of the WebSocket (TCP) over IPv6
The TCP connection from the iPhone's IPv6 address works perfectly — full TLS handshake, HTTP 101 upgrade, and the client sends 732-byte keepalive frames every 2 seconds. The server ACKs on TCP level but sends no application data back (because registration is blocked by missing hole punch).
5. WiFi (IPv4) works perfectly
Same iPhone, same app, same Pangolin server — switching from 5G to WiFi (which provides IPv4) immediately connects all sites.
Analysis
The sequence on an IPv6-only/NAT64 network:
- iOS resolves
pangolin.mydomain→ gets both A and AAAA records - TCP/WebSocket: iOS networking stack (NSURLSession/Network.framework) uses Happy Eyeballs, prefers IPv6, connects via AAAA record → ✅ works
- UDP hole punch: The OLM code appears to resolve
base_endpointto the A record (IPv4) and opens anAF_INETUDP socket → ❌ fails silently on IPv6-only network (no native IPv4 stack available)
On NAT64 networks, the correct approach is to use getaddrinfo() with AF_UNSPEC and let the OS synthesize a NAT64 IPv6 address (e.g., 64:ff9b::4d2a:147a) for IPv4-only destinations. Alternatively, the code should attempt IPv6 UDP first when an AAAA record is available.
Note on AAAA records making it worse: With an AAAA record present, DNS64 synthesis does not activate (DNS64 only synthesizes when there is no AAAA record). This means the TCP stack connects via real IPv6, but the UDP stack gets a real IPv4 address it cannot use. Without the AAAA record, DNS64 would at least synthesize a NAT64 address, which might work if the UDP code uses getaddrinfo() properly.
Suggested Fix
In the iOS OLM client's hole punch implementation:
- Use
getaddrinfo()withAF_UNSPECinstead of resolving to IPv4 only - Prefer IPv6 (
AF_INET6) when available, fall back to IPv4 - Or: use
Network.framework'sNWConnectionwith UDP, which handles Happy Eyeballs and NAT64 transparently
This would also fix the issue for all other NAT64/DS-Lite networks, which are extremely common in Germany (Vodafone Kabel, 1&1, Telekom mobile).
Impact
This affects all users on IPv6-only/NAT64 mobile networks, which includes:
| Provider | Network Type | Affected |
|---|---|---|
| T-Mobile DE | NAT64 (5G/LTE) | ✅ Yes |
| Vodafone DE (Kabel) | DS-Lite / CGNAT | ✅ Yes |
| o2/Telefónica DE | DS-Lite | ✅ Yes |
| 1&1 | DS-Lite | ✅ Yes |
This is the majority of German mobile and cable internet users. The issue likely also affects other countries where carriers have deployed IPv6-only with NAT64.
Workaround
Currently none — the only option is to use WiFi with IPv4 connectivity. Removing the AAAA record from DNS does not help because the hole punch code appears to force AF_INET regardless.
Environment
- Pangolin iOS: 0.6.2
- Pangolin Server: running on Hetzner VPS (Debian), dual-stack (IPv4 + IPv6) -> Docker, all images latest
- Gerbil: latest (from
fosrl/gerbilimage) - Mobile Network: T-Mobile Germany, 5G, IPv6-only with NAT64/DNS64
- DNS: host has both A and AAAA records
To Reproduce
- Set up Pangolin server on a dual-stack VPS with both A and AAAA DNS records
- Configure sites/resources via Newt (working correctly)
- Connect the Pangolin iOS app via WiFi (IPv4) → works, all sites show "Connected"
- Switch to mobile data (5G, IPv6-only/NAT64 network) → app shows "Registering", sites never connect
Expected Behavior
The iOS client should complete registration and connect to all sites regardless of whether the client is on an IPv4 or IPv6-only network.



