WireGuard iOS Troubleshooting Guide
Overview
This guide addresses handshake failures when connecting iOS devices to a WireGuard VPN server. iOS WireGuard clients have specific requirements and quirks that differ from other platforms.
Background
WireGuard uses a simple handshake protocol:
- Client sends handshake initiation to server
- Server responds with handshake response
- Both sides derive session keys
- Data packets can flow
When handshakes fail, you’ll see:
- “Handshake did not complete” in iOS WireGuard app
- No “latest handshake” timestamp on server (
wg show) - Connection timeout without error details
Common iOS-Specific Causes
1. DNS Configuration Issues (MOST COMMON)
Problem: iOS handles DNS differently than other platforms. The WireGuard iOS app may refuse to connect or fail silently if DNS is misconfigured.
Symptoms:
- Connection appears to activate but no traffic flows
- Works on some networks but not others
- Interface goes “up” but handshake never completes
Root Causes:
- No DNS server specified in configuration
- Multiple DNS servers listed (iOS sometimes rejects this)
- DNS server is within VPN tunnel range (10.100.0.x) but unreachable during handshake
- DNS64/NAT64 networks (some cellular carriers) conflict with WireGuard DNS
Solution:
[Interface]
DNS = 8.8.8.8
Best Practices:
- Use a single, external DNS server (8.8.8.8, 1.1.1.1, or 9.9.9.9)
- Avoid using multiple DNS servers
- Don’t use VPN-internal DNS during initial connection
- For privacy, Cloudflare (1.1.1.1) or Quad9 (9.9.9.9) are good alternatives to Google
2. MTU Size Mismatches
Problem: iOS devices, especially on cellular networks, are sensitive to MTU (Maximum Transmission Unit) size.
Symptoms:
- Handshake completes but no data transfers
- Works on WiFi but fails on cellular
- Small packets work but large ones don’t (can ping but can’t browse)
Root Causes:
- Default WireGuard MTU is 1420 bytes
- Cellular networks often have lower MTU (1280-1400)
- WireGuard adds 80 bytes of overhead to IP packets
- Path MTU discovery doesn’t work well with iOS + cellular
Solution:
[Interface]
MTU = 1280
Testing Different MTU Values:
# On server after connection established:
ping -M do -s 1232 10.100.0.5 # Test with 1280 MTU (1232 + 28 ICMP + 20 IP = 1280)
ping -M do -s 1372 10.100.0.5 # Test with 1420 MTU (1372 + 28 + 20 = 1420)
If lower values work but higher ones fail, reduce MTU in iOS config.
Recommended Values:
- Start with: 1280 (works on most cellular networks)
- If problems persist: try 1280, 1360, 1400, 1412
- WiFi-only: can usually use 1420 (default)
3. Cellular Network Routing Issues
Problem: iOS on cellular data routes traffic differently than WiFi, and some carriers block or NAT UDP traffic aggressively.
Symptoms:
- Works perfectly on home WiFi
- Fails immediately on cellular data
- No traffic appears at server firewall
- Sometimes works, then stops working
Root Causes:
- Carrier-grade NAT (CGNAT) blocks or mangles UDP
- Carrier firewall blocks UDP port 51820
- iOS routing table prioritizes cellular routes incorrectly
- Mobile data restrictions or VPN policies
Diagnosis:
# On iOS device, check if UDP packets reach server
# Have someone run on server while you connect:
sudo tcpdump -i any udp port 51820
# If you see no packets from your IP, it's a carrier issue
Solutions:
- Test on different cellular network if possible
- Try alternative WireGuard port (some carriers block 51820)
- Use TCP-based tunnel (Shadowsocks/V2Ray) if UDP is completely blocked
- Contact carrier if VPN is required for work
Workarounds:
- Enable VPN only on WiFi (on-demand rules)
- Use backup VPN provider for cellular
- Switch to different carrier with better UDP support
4. Server NAT/Masquerading Misconfiguration
Problem: Server firewall rules don’t properly NAT traffic from VPN clients.
Symptoms:
- Handshake completes (shows “latest handshake” on server)
- Can ping server VPN IP (10.100.0.1) but nothing else
- No internet access through VPN
Root Causes:
- Missing or incorrect MASQUERADE rule
- Wrong source subnet in iptables rule
- Missing IP forwarding (net.ipv4.ip_forward = 0)
Check Server Config:
# Verify IP forwarding is enabled
sysctl net.ipv4.ip_forward
# Should return: net.ipv4.ip_forward = 1
# Check NAT rule exists
sudo iptables -t nat -L POSTROUTING -v -n | grep 10.100.0.0
# Should see something like:
# MASQUERADE all -- * * 10.100.0.0/24 0.0.0.0/0
Fix in NixOS (modules/wireguard-server.nix):
networking.firewall.extraCommands = ''
iptables -t nat -A POSTROUTING -s 10.100.0.0/24 -j MASQUERADE
iptables -A FORWARD -i wg+ -j ACCEPT
'';
Common Mistakes:
-s10.100.0.0/24(missing space after -s)-s 10.100.0.0/32(wrong subnet mask - /32 is single IP, should be /24)- Using SNAT instead of MASQUERADE on dynamic IP servers
5. Key Mismatches
Problem: Public/private keys don’t match between client and server config.
Symptoms:
- “Handshake did not complete” immediately
- No handshake attempts visible on server
- iOS app shows “Invalid key” or just fails silently
Root Causes:
- Typo when copying public key
- QR code scanning error
- Key regeneration on one side but not updated on other
- PreSharedKey mismatch (if using PSK)
Verification:
On iOS device:
- Open WireGuard app
- Tap on tunnel configuration
- Note the Public key shown under Interface
On server:
# Check what public key is configured for this peer
sudo wg show wg0 peers | grep -A5 "1wzFG60hlrAoSYcRKApsH+WK3Zyz8ljdLglb/8JbuW0="
In your NixOS config (globals.nix):
hokkaido = {
net = {
vpn = {
pubkey = "1wzFG60hlrAoSYcRKApsH+WK3Zyz8ljdLglb/8JbuW0=";
ips = [ "10.100.0.5" ];
};
};
};
Solutions:
- Delete and recreate tunnel on iOS (generates new key pair)
- Update globals.nix with new public key
- Rebuild and deploy server config
- Double-check keys match exactly (case-sensitive, watch for l vs 1, O vs 0)
6. Server Firewall Blocking UDP
Problem: Server firewall doesn’t allow UDP port 51820.
Symptoms:
- Connection times out
- No errors, just hangs
- Works from some networks but not others (if server IP is geo-blocked)
Check Firewall:
# On NixOS server
sudo iptables -L INPUT -v -n | grep 51820
# Should see:
# ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:51820
Cloud Provider Firewalls:
If running on DigitalOcean/AWS/GCP, check:
- DigitalOcean: Cloud Firewalls in dashboard
- AWS: Security Groups must allow UDP 51820 inbound
- GCP: VPC Firewall Rules must allow UDP 51820
Fix in NixOS:
networking.firewall.allowedUDPPorts = [ 51820 ];
7. iOS App-Specific Bugs
Problem: Bugs in specific versions of WireGuard iOS app.
Symptoms:
- Works on other devices but not iOS
- Works on older iOS version but not after update
- Inconsistent behavior
Solutions:
- Update WireGuard iOS app to latest version
- Check WireGuard iOS GitHub issues for known bugs
- Try toggling “On-Demand” activation off and on
- Force-quit and restart WireGuard app
- Reboot iOS device (clears network stack caches)
- Delete and recreate tunnel configuration
Recommended iOS Client Configuration
For device hokkaido (10.100.0.5) connecting to server kerkouane:
[Interface]
PrivateKey = <GENERATE_ON_IOS_DEVICE>
Address = 10.100.0.5/24
DNS = 8.8.8.8
MTU = 1280
[Peer]
PublicKey = +H3fxErP9HoFUrPgU19ra9+GDLQw+VwvLWx3lMct7QI=
Endpoint = 167.99.17.238:51820
AllowedIPs = 10.100.0.0/24
PersistentKeepalive = 25
Field Explanations:
- PrivateKey: Generated automatically by iOS app (never share this)
- Address: Your VPN IP from globals.nix
- DNS: Single external DNS server (Google, Cloudflare, or Quad9)
- MTU: Reduced for cellular compatibility
- PublicKey: Server’s public key from globals.nix (kerkouane)
- Endpoint: Server’s public IP and WireGuard port
- AllowedIPs: VPN subnet to route through tunnel
- PersistentKeepalive: Keep NAT mappings alive (important for mobile)
For Full Tunnel (Route all traffic through VPN):
AllowedIPs = 0.0.0.0/0, ::/0
For Split Tunnel (Only VPN subnet):
AllowedIPs = 10.100.0.0/24
Step-by-Step Troubleshooting
Step 1: Verify Server Configuration
# SSH to kerkouane
ssh kerkouane
# Check WireGuard interface is up
sudo wg show
# Should see:
# interface: wg0
# public key: +H3fxErP9HoFUrPgU19ra9+GDLQw+VwvLWx3lMct7QI=
# private key: (hidden)
# listening port: 51820
# Check peer is configured
sudo wg show wg0 peers
# Should include:
# peer: 1wzFG60hlrAoSYcRKApsH+WK3Zyz8ljdLglb/8JbuW0=
# allowed ips: 10.100.0.5/32
# Check firewall allows UDP 51820
sudo iptables -L INPUT -v -n | grep 51820
# Check NAT rule
sudo iptables -t nat -L POSTROUTING -v -n | grep 10.100.0
Step 2: Test Server Connectivity
# From any other machine (not iOS yet)
nc -u -v 167.99.17.238 51820
# Or test with another WireGuard client if available
Step 3: Configure iOS Client
- Open WireGuard app on iOS
- Tap “Add a tunnel” → “Create from scratch”
- Set name:
kerkouane-vpn - Copy Public key shown (you’ll need this for server config)
- Tap “Add peer”
- Enter server configuration
- Save tunnel
Step 4: Update Server with iOS Public Key
# In globals.nix, update hokkaido's pubkey if changed
hokkaido = {
net = {
vpn = {
pubkey = "PASTE_PUBLIC_KEY_FROM_IOS_APP_HERE";
ips = [ "10.100.0.5" ];
};
};
};
# Rebuild server config
make host/kerkouane/switch
Step 5: Attempt Connection from iOS
- Activate tunnel in WireGuard app
- Watch for status change
- If it shows “Connected”, check connection
# On iOS, open Safari and try:
http://10.100.0.1 # Server VPN IP
http://whoami.sbr.pm # Public service through VPN
Step 6: Monitor Server Logs
# On server, watch for handshake attempts
sudo journalctl -u systemd-networkd -f
# Or watch WireGuard interface
watch -n 1 sudo wg show
# Look for:
# peer: 1wzFG60hlrAoSYcRKApsH+WK3Zyz8ljdLglb/8JbuW0=
# endpoint: <YOUR_IP>:<PORT>
# latest handshake: X seconds ago # This should appear!
# transfer: X.XX KiB received, Y.YY KiB sent
Step 7: Test Different Networks
# Try connection on:
1. Home WiFi
2. Mobile data (cellular)
3. Different WiFi network (coffee shop, etc.)
# Note which networks work/fail
# This helps identify carrier or firewall issues
Diagnostic Commands
On Server (kerkouane)
# Show full WireGuard status
sudo wg show all
# Show only latest handshakes
sudo wg show wg0 latest-handshakes
# Capture WireGuard traffic (run during connection attempt)
sudo tcpdump -i any -n udp port 51820
# Show connected peers
sudo wg show wg0 endpoints
# Check if packets are forwarded
sudo iptables -L FORWARD -v -n | grep wg
# Test routing from server to client (after handshake)
ping 10.100.0.5
On iOS Device
# No direct CLI on iOS, but you can:
1. Check WireGuard app logs (tap on tunnel, scroll down)
2. Use Network utility apps (iNetTools, Ping)
3. Check "Latest handshake" timestamp in app
4. Test with Safari: http://10.100.0.1
On Another Machine (for comparison)
# Create test config on Linux/Mac
sudo wg-quick up wg0
# Or with temporary interface
sudo ip link add dev wg0 type wireguard
sudo wg set wg0 private-key <(echo "TEST_PRIVATE_KEY")
sudo wg set wg0 peer +H3fxErP9HoFUrPgU19ra9+GDLQw+VwvLWx3lMct7QI= \
endpoint 167.99.17.238:51820 \
allowed-ips 10.100.0.0/24 \
persistent-keepalive 25
sudo ip addr add 10.100.0.99/24 dev wg0
sudo ip link set wg0 up
# Test handshake
sudo wg show
Verification Checklist
After configuration changes, verify:
- Server shows peer configured:
sudo wg show wg0 peers - Firewall allows UDP 51820:
sudo iptables -L INPUT -v -n | grep 51820 - NAT rule is correct:
sudo iptables -t nat -L POSTROUTING -v -n - IP forwarding enabled:
sysctl net.ipv4.ip_forwardreturns 1 - iOS config has DNS set: Check WireGuard app interface config
- iOS config has MTU=1280: Check WireGuard app interface config
- iOS public key matches server config: Compare app and globals.nix
- Server public key matches iOS peer config: Compare globals.nix and app
- Endpoint IP is correct: 167.99.17.238:51820
- AllowedIPs is correct: 10.100.0.0/24 (or 0.0.0.0/0 for full tunnel)
Working Configuration Example
Server (kerkouane) - modules/wireguard-server.nix
{
services.wireguard.server = {
enable = true;
ips = libx.wg-ips globals.machines.kerkouane.net.vpn.ips;
peers = libx.generateWireguardPeers globals.machines;
};
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
networking.firewall.extraCommands = ''
iptables -t nat -A POSTROUTING -s 10.100.0.0/24 -j MASQUERADE
iptables -A FORWARD -i wg+ -j ACCEPT
'';
networking.firewall.allowedUDPPorts = [ 51820 ];
networking.firewall.trustedInterfaces = [ "wg0" ];
}
Client (iOS) - WireGuard App Config
[Interface]
PrivateKey = oK8qaDg9FXYSVwGp9NM8xz8VQm7JwLN7CwTQMb7Hv1E=
Address = 10.100.0.5/24
DNS = 8.8.8.8
MTU = 1280
[Peer]
PublicKey = +H3fxErP9HoFUrPgU19ra9+GDLQw+VwvLWx3lMct7QI=
Endpoint = 167.99.17.238:51820
AllowedIPs = 10.100.0.0/24
PersistentKeepalive = 25
Server Config (globals.nix)
hokkaido = {
net = {
vpn = {
pubkey = "1wzFG60hlrAoSYcRKApsH+WK3Zyz8ljdLglb/8JbuW0=";
ips = [ "10.100.0.5" ];
};
};
};
kerkouane = {
net = {
vpn = {
pubkey = "+H3fxErP9HoFUrPgU19ra9+GDLQw+VwvLWx3lMct7QI=";
ips = [ "10.100.0.1" ];
};
};
};
Advanced Troubleshooting
Packet Capture Analysis
# On server, capture handshake initiation
sudo tcpdump -i any -vvv -X udp port 51820
# Look for:
# - Packets FROM client IP TO server:51820 (initiation)
# - Packets FROM server:51820 TO client IP (response)
# - If you see initiation but no response → server issue
# - If you see no packets at all → network/firewall issue
Testing with Different Endpoints
# Try different port (if carrier blocks 51820)
# On server:
networking.wireguard.interfaces.wg0.listenPort = 443;
networking.firewall.allowedUDPPorts = [ 443 ];
# On iOS:
Endpoint = 167.99.17.238:443
Enable Debug Logging (iOS)
- Export tunnel config from WireGuard app
- Edit .conf file, add to [Interface]:
# No debug flag in iOS, but you can: # - Check system Settings → Privacy → Analytics → Analytics Data # - Look for WireGuard crash logs
Test with Minimal Config
# Minimal iOS config to isolate issues
[Interface]
PrivateKey = <GENERATED>
Address = 10.100.0.5/32
[Peer]
PublicKey = +H3fxErP9HoFUrPgU19ra9+GDLQw+VwvLWx3lMct7QI=
Endpoint = 167.99.17.238:51820
AllowedIPs = 10.100.0.1/32
PersistentKeepalive = 25
If this works, gradually add: DNS, full subnet AllowedIPs, MTU changes.
Performance Tuning
Once working, optimize for performance:
Increase MTU (if stable)
# Test progressively higher MTU values
MTU = 1280 # Start here
MTU = 1360 # Try this
MTU = 1400 # Or this
MTU = 1420 # Default (best performance if it works)
Adjust Keepalive
# Default
PersistentKeepalive = 25
# For unstable cellular (more aggressive)
PersistentKeepalive = 15
# For stable WiFi only (reduce bandwidth)
PersistentKeepalive = 60
On-Demand Rules (iOS-specific)
Configure automatic VPN activation:
- In WireGuard app, tap tunnel
- Tap “Edit”
- Tap “On-Demand Activation”
- Configure rules:
- WiFi: Connect to VPN when on untrusted WiFi
- Cellular: Always connect on cellular
- Ethernet: Connect when on any wired network
Example rules:
Connect On Demand:
☑ Cellular
☑ WiFi (except: Home_WiFi_SSID)
☐ Ethernet
Common Error Messages
“Handshake did not complete after 5 seconds”
Causes:
- Server not reachable (firewall, wrong endpoint)
- Key mismatch
- Server not running WireGuard
Solutions: Check server status, verify endpoint IP/port, verify keys
“Unable to resolve endpoint”
Causes:
- DNS resolution failure
- No internet connection on iOS device
- Invalid endpoint hostname
Solutions: Use IP address instead of hostname, check iOS internet connectivity
“Invalid key”
Causes:
- Malformed base64 key
- Wrong key length (WireGuard keys are 32 bytes, 44 chars in base64)
Solutions: Regenerate keys, check for copy-paste errors
Connection works but no internet
Causes:
- NAT/masquerade misconfigured
- DNS not set or incorrect
- AllowedIPs too restrictive
Solutions: Check server MASQUERADE rule, add DNS to iOS config, verify AllowedIPs
References
Documentation
Troubleshooting Resources
- WireGuard VPN - Handshake error - MikroTik
- iOS iPhone not connecting on WireGuard - Super User
- Wireguard no handshake on iOS - MikroTik
- Wireguard handshake problem with Apple devices - OpenWrt
- Wireguard problems on NixOS Discourse
Tools
- WireGuard Configuration Generator
- Network testing: iNetTools (iOS app)
- Packet capture: tcpdump, Wireshark
Summary
Most common iOS WireGuard issues:
- Missing or incorrect DNS configuration
- MTU too high for cellular networks
- Server NAT/masquerade misconfigured
- Carrier blocking UDP or using aggressive NAT
Quick fixes that solve 90% of issues:
- Add
DNS = 8.8.8.8to iOS config - Add
MTU = 1280to iOS config - Fix server NAT rule:
-s 10.100.0.0/24not/32 - Verify keys match between client and server
Systematic troubleshooting:
- Verify server configuration and logs
- Test from known working client first
- Configure iOS with minimal settings
- Add features incrementally (DNS, MTU, full routing)
- Test on different networks (WiFi vs cellular)
- Monitor server during connection attempts
When in doubt, start fresh with minimal config and build up from there.