WireGuard handshake works but no internet
A WireGuard handshake only proves that the keys match and that UDP gets through. What happens to the packets after that is up to the server's routing and firewall. When there's a handshake but no internet, the cause is almost always IP forwarding or NAT on the server, a wrong AllowedIPs on one side, DNS, or MTU, and wg show's transfer counters tell you which one to look at first.
ST Muntaha9 min read
This is the WireGuard report that confuses people most: the client says connected, wg show has a fresh handshake, and no website loads. The tunnel is up, so it feels like WireGuard should be working. It is. The problem is in what the server does with the packets once WireGuard has decrypted them.
If the handshake itself never completes, this is the wrong post. The handshake checklist covers keys, ports and blocked UDP.
The examples below use a server with the tunnel address 10.0.0.1, a client at 10.0.0.2, the tunnel subnet 10.0.0.0/24, and eth0 as the server's internet interface. Replace them with yours.
Why is there a handshake but no traffic?
Because the handshake and the traffic are handled by different layers. WireGuard does the handshake, and the Linux kernel's forwarding, NAT and firewall rules decide whether the decrypted packets go anywhere.
Start on the server with the peer's counters:
# wg show wg0
interface: wg0
public key: <server public key>
listening port: 51820
peer: <client public key>
endpoint: 203.0.113.25:49152
allowed ips: 10.0.0.2/32
latest handshake: 14 seconds ago
transfer: 18.42 KiB received, 2.11 KiB sent
Then make the client send something, for example ping 1.1.1.1, and run wg show again. The counters point at the layer that's failing:
| What you see | Look at |
|---|---|
| Server's "received" grows, "sent" barely moves | Forwarding, NAT or the server's firewall |
Server's "received" grows, and the client's address isn't in allowed ips |
AllowedIPs on the server |
ping 1.1.1.1 works, ping example.com fails |
DNS |
| Pings work, HTTPS pages hang | MTU |
| The client's "sent" grows, the server's "received" doesn't | The client isn't routing traffic into the tunnel, or packets are lost before WireGuard |
To see where a packet stops, watch both interfaces on the server while the client pings:
# tcpdump -ni wg0 icmp
# tcpdump -ni eth0 icmp and host 1.1.1.1
If the ping shows on wg0 but not on eth0, the server isn't forwarding it. If it leaves eth0 with the source 10.0.0.2 instead of the server's public address, NAT is missing, and the reply can never come back. If the reply arrives on eth0 but never appears on wg0, a firewall rule is dropping the return traffic.
Is IP forwarding on, and is NAT set up?
For a tunnel that carries internet traffic, the server has to forward packets from wg0 to its internet interface and masquerade them behind its own address. Neither happens by default on Debian or Ubuntu, and this pair is the most common cause of a handshake with no internet.
Check forwarding first:
$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
Turn it on now, and in a file under /etc/sysctl.d/ so it survives a reboot:
# sysctl -w net.ipv4.ip_forward=1
# echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-wireguard.conf
# sysctl --system
Use /etc/sysctl.d/, not /etc/sysctl.conf. The Debian 13 release notes say that /etc/sysctl.conf is no longer honored, so a setting that worked on Debian 12 can silently stop applying after an upgrade. If clients get an IPv6 tunnel address and ::/0 in AllowedIPs, also set net.ipv6.conf.all.forwarding = 1.
Next, find the real name of the internet interface. It's often ens3 or enp1s0, not eth0, and a masquerade rule for the wrong name does nothing:
$ ip route show default
default via 203.0.113.1 dev ens3 proto static
With nftables, add a NAT table and a masquerade rule for the tunnel subnet:
# nft add table ip wgnat
# nft add chain ip wgnat postrouting '{ type nat hook postrouting priority 100 ; }'
# nft add rule ip wgnat postrouting ip saddr 10.0.0.0/24 oifname "eth0" masquerade
With iptables, the same rule plus two forwarding rules:
# iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
# iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT
# iptables -A FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
Rules typed at the shell are lost on reboot. The usual place for them is the server's wg0.conf, so they come and go with the interface:
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <server private key>
PostUp = iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
If forwarding and NAT are both right and packets still stop at the server, something has set the forward policy to drop. Three things do this often:
- Docker. When Docker turns on IP forwarding, it also sets the iptables
FORWARDchain's default policy toDROP, as its firewall docs describe. Runiptables -S FORWARD: a first line of-P FORWARD DROPmeans you need the explicitACCEPTrules above. - ufw. Its default forward policy is
DROP, and a plainufw allowrule doesn't cover forwarded traffic. Add a route rule:ufw route allow in on wg0 out on eth0. - Your own nftables ruleset. Run
nft list rulesetand look for a chain withhook forwardandpolicy dropthat has no rule accepting traffic fromwg0.
Are AllowedIPs right on both sides?
AllowedIPs is a routing table and a filter at the same time, so a wrong value drops packets silently, after a perfectly good handshake. The server needs the client's tunnel address, and the client needs the destinations it wants to reach.
The WireGuard site calls this cryptokey routing. For outgoing packets, WireGuard compares the destination with each peer's allowed IPs to pick the peer. For incoming packets, it decrypts them and then checks the source: a packet whose source address isn't in that peer's allowed IPs is dropped.
That gives two common mistakes:
- The server's AllowedIPs doesn't match the client's Address. The client config says
Address = 10.0.0.3/32, and the server's[Peer]for it saysAllowedIPs = 10.0.0.2/32. Every packet from the client is authentic, has the wrong source, and is dropped. The server's "received" counter grows, and nothing else happens. Copy-pasted peers often have this. - The client's AllowedIPs doesn't cover the internet.
AllowedIPs = 10.0.0.0/24sends only the tunnel subnet into the tunnel, and everything else goes out the normal way. For a full tunnel, the client needsAllowedIPs = 0.0.0.0/0, ::/0.
One address can belong to only one peer. If two [Peer] sections on the server list the same /32, only one of those clients gets traffic back, and which one can change when you reload. Give every client its own address.
To compare the two sides:
# wg show wg0 allowed-ips # on the server: each peer and its addresses
$ ip -4 addr show wg0 # on the client: the address it actually uses
Is DNS the problem, not the tunnel?
If an IP address works through the tunnel and a name doesn't, the tunnel is fine and name resolution is broken. Test the two separately on the client:
$ ping -c 3 1.1.1.1
$ getent hosts example.com
$ dig @1.1.1.1 example.com +short
If the first and third work and the second doesn't, the resolver the client is using is the problem. The usual causes:
- The config points DNS at the server, and nothing answers there.
DNS = 10.0.0.1needs a resolver on the server that listens on the tunnel address. Many listen only on127.0.0.1. Check withss -lunp 'sport = :53'on the server. - The resolver refuses the tunnel subnet. Resolvers such as unbound and dnsmasq only answer the networks they're told to. Add
10.0.0.0/24to the allowed clients, and allow port 53 fromwg0in the server's firewall. - The config has no DNS line at all. The client keeps the resolver of the network it's on, which may not be reachable once all traffic goes into the tunnel. Set one explicitly.
The quickest way to confirm a DNS cause is to set DNS = 1.1.1.1 in the client config and reconnect. If pages load, go back and fix the resolver you meant to use.
Is it the MTU?
It's MTU if small things work and big things don't: pings and DNS succeed, but HTTPS pages hang partway through and large downloads stall. If nothing at all works, not even a small ping through the tunnel, it isn't MTU.
WireGuard adds up to 80 bytes to every packet. A path that can't carry the result, such as a PPPoE line or some mobile networks, drops the large packets while the small handshake and keepalive packets pass. For a quick test, set MTU = 1280 in the client's [Interface] and reconnect. If pages load, MTU is the cause, and you can raise the value again once you've measured the real path.
Why does it work on one network and not another?
If the same config works at home and fails at a hotel or on mobile data, the difference is on that network. With a good handshake on both, the usual causes are a subnet clash, a smaller path MTU, or a NAT that forgets the connection.
- The local network uses the tunnel's subnet. If the client's Wi-Fi hands out
10.0.0.0/24addresses and the tunnel uses10.0.0.0/24too, the client has two routes for the same addresses, and traffic for the tunnel goes to the wrong place. Pick a tunnel subnet that home and office networks rarely use. The same applies to a site-to-site AllowedIPs such as192.168.1.0/24, which many home routers use. - The path MTU is smaller. This is the same problem as the previous section, and it shows up only on some networks. Phone apps are less affected: the official Android and iOS apps use 1280 when the config has no
MTUline, so a config can work on a phone and fail on a laptop on the same Wi-Fi. - The NAT forgets idle connections. Some networks drop UDP mappings after a short idle time. Traffic the client starts still works, because it opens a new mapping, but anything the server sends after a quiet spell is lost. Set
PersistentKeepalive = 25on the client's[Peer], the interval thewg(8)man page suggests for most firewalls.
How do I check traffic actually passes, not just the handshake?
Send real traffic through the tunnel and check where it leaves. On the client, with the tunnel up:
$ curl -4 -m 10 -s https://icanhazip.com
198.51.100.7
That should print the server's public address, not the client's own. A timeout here, with a fresh handshake in wg show, is exactly the problem this post is about.
A monitor that only checks for a handshake, or that only checks the UDP port, stays green through all of it. Full disclosure: this is the gap a TunnelHQ WireGuard check is built for. It brings a tunnel up from your client config, completes the handshake, then sends real requests to well-known sites through the tunnel and records the exit IP they left from. If the handshake fails, the check reports Tunnel unreachable. If the handshake works and the traffic doesn't reach the internet, it reports Tunnel established - Internet is stuck and marks the monitor down. The monitor docs list each step and every failure message.
To try it once without an account, the free tester runs the same kind of check on a pasted config: 5 tests per IP address per 24 hours, and nothing is saved. It needs a full client config, private key included, so use a test peer you can remove afterwards.
The limit is the same as for any outside check: it runs from TunnelHQ's checkers, not from your users' networks. A server with no NAT fails from everywhere and shows up at once. A subnet clash or a small MTU on one user's network won't, and for those the commands above, run on that user's machine, are still the way to find it.

