Guides, Nov 10, 2026

# 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 Muntaha, 9 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](https://tunnelhq.com/blog/wireguard-handshake-did-not-complete/) 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](https://www.debian.org/releases/trixie/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 `FORWARD` chain's default policy to `DROP`, as its [firewall docs](https://docs.docker.com/engine/network/firewall-iptables/) describe. Run `iptables -S FORWARD`: a first line of `-P FORWARD DROP` means you need the explicit `ACCEPT` rules above.
-   **ufw.** Its default forward policy is `DROP`, and a plain `ufw allow` rule 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 ruleset` and look for a chain with `hook forward` and `policy drop` that has no rule accepting traffic from `wg0`.

## 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](https://www.wireguard.com/) 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 says `AllowedIPs = 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/24` sends only the tunnel subnet into the tunnel, and everything else goes out the normal way. For a full tunnel, the client needs `AllowedIPs = 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.1` needs a resolver on the server that listens on the tunnel address. Many listen only on `127.0.0.1`. Check with `ss -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/24` to the allowed clients, and allow port 53 from `wg0` in 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/24` addresses and the tunnel uses `10.0.0.0/24` too, 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 as `192.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 `MTU` line, 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 = 25` on the client's `[Peer]`, the interval the `wg(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](https://tunnelhq.com/wireguard-monitoring/) 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](https://tunnelhq.com/docs/concepts/monitors/) list each step and every failure message.

To try it once without an account, the [free tester](https://tunnelhq.com/test/) 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.

Source: https://tunnelhq.com/blog/wireguard-handshake-but-no-internet/
