How to tell if WireGuard is working
WireGuard is working when a peer has a recent latest handshake in wg show and its received counter keeps growing while you send traffic. On the server that is a two-command check. From outside it is harder, because WireGuard never answers a packet it can't authenticate: the only real outside test is a client that completes a handshake with a valid config and gets traffic back.
ST Muntaha9 min read
The commands below are from wireguard-tools 1.0.20260223, and I checked the output formats against its show.c and config.c. If you already see Handshake did not complete after 5 seconds in a log, skip ahead to the handshake troubleshooting guide, which goes through the causes one by one.
"Working" means three different things, and it helps to keep them apart. The interface is up. A peer has completed a handshake. Traffic goes through the tunnel and comes back. Each check below answers one of them.
How do I check WireGuard is working on the server?
Run wg show and look at each peer's latest handshake and transfer lines. A peer with a handshake in the last two or three minutes and a received counter that grows is working.
$ sudo wg show wg0
interface: wg0
public key: xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=
private key: (hidden)
listening port: 51820
peer: Hk3vR8bPq2LmNw5sYt7uZx9aCd1eFg3hJk5lMn7oPqE=
endpoint: 203.0.113.45:41237
allowed ips: 10.0.0.2/32
latest handshake: 48 seconds ago
transfer: 18.42 MiB received, 211.07 MiB sent
Three things to read:
- No
interface: wg0at all. The interface isn't up. Checksystemctl status wg-quick@wg0and its log withjournalctl -u wg-quick@wg0. - No
latest handshakeline under a peer.wg showleaves the line out when that peer has never completed a handshake since the interface came up. transferstuck at the same numbers. Runwg show wg0 transfertwice, a few seconds apart, while the client sends traffic. If received doesn't move, packets aren't arriving.
Then confirm that traffic really passes. From the client, ping the server's tunnel address:
$ ping -c 3 10.0.0.1
If that works but the internet doesn't, the tunnel is fine and the problem is forwarding or NAT on the server:
sysctl net.ipv4.ip_forward
sudo nft list ruleset | grep -i masquerade
sudo iptables -t nat -S POSTROUTING
What does latest handshake tell me, and what doesn't it?
It tells you when this peer last proved it holds the right keys. It does not tell you that the peer is connected now, or that traffic flows.
WireGuard only makes a new handshake when there is something to send and the current session is older than 2 minutes. So:
| What you see | What it usually means |
|---|---|
| Under 2 to 3 minutes ago | The peer is active, or has PersistentKeepalive set |
| Hours ago | The peer is idle. That is normal for a phone or laptop that isn't sending anything |
| No line at all | No handshake since the interface came up: wrong key, wrong endpoint, or the client never tried |
| Recent, but no internet on the client | Keys are right. The problem is routing, NAT, DNS or MTU, after the handshake |
The last row catches people. A handshake is a few small packets, so it succeeds on paths where real traffic fails. A recent handshake proves the keys and the UDP path, not the tunnel.
For scripts, use the machine-readable form. It prints each peer's public key and a Unix timestamp, and the timestamp is 0 when there has never been a handshake:
$ sudo wg show wg0 latest-handshakes
Hk3vR8bPq2LmNw5sYt7uZx9aCd1eFg3hJk5lMn7oPqE= 1794385210
Qm7tY2wE9rT4uI6oP1aS3dF5gH8jK0lZ2xC4vB6nM8E= 0
wg show wg0 dump gives every field for every peer on one line each, which is easier to parse than the human output.
Can I test a WireGuard server from outside?
Yes, but only with a valid client config. WireGuard is built to stay silent: a server that gets a packet it can't authenticate drops it and sends nothing back. So from outside, a dead server, a filtered port and a working server all look the same unless you can complete a handshake.
A real outside test therefore needs three things: a private key that the server knows as a peer, the server's public key, and a machine on a different network. Bring up the tunnel there and check the same two things as on the server:
$ sudo wg-quick up ./test.conf
$ sudo wg show test latest-handshakes
$ ping -c 3 10.0.0.1
$ curl -s https://ifconfig.me
$ sudo wg-quick down ./test.conf
If curl prints the server's public address, the tunnel works end to end from that network. Use a config made for testing, not a copy of a user's config. If the same private key is used on two devices at once, each new handshake from one replaces the other's session, and you break the user's tunnel while you test.
Why can't a port scan tell if WireGuard is up?
Because WireGuard never replies to a packet that isn't a valid handshake. A scanner sends something, gets nothing back, and has to guess:
$ sudo nmap -sU -p 51820 vpn.example.com
PORT STATE SERVICE
51820/udp open|filtered unknown
open|filtered means "no answer". You get the same result when WireGuard is running, when the process is stopped behind an open firewall, and when a firewall drops the packet. The scan can't tell them apart, so it can't tell you if WireGuard is up. Why WireGuard port monitoring fails goes into this in more detail, including what a port check does catch.
How do I validate a wg0.conf file before using it?
Load it into a throwaway interface and let wg parse it. There is no dedicated validator, but wg setconf checks every key and every line, and it fails loudly.
wg itself doesn't understand the lines that only wg-quick uses (Address, DNS, MTU, Table, PreUp, PostUp, PreDown, PostDown, SaveConfig), so strip them first:
$ sudo wg-quick strip ./wg0.conf > /tmp/wg0-stripped.conf
$ sudo ip link add wgtest type wireguard
$ sudo wg setconf wgtest /tmp/wg0-stripped.conf
$ sudo wg showconf wgtest
$ sudo ip link del wgtest
Two details save time. Write ./wg0.conf, not wg0.conf: without a slash, wg-quick treats a short name as an interface name and looks for /etc/wireguard/wg0.conf.conf. And wg-quick runs itself with sudo even for strip. The test interface is never brought up, so this sends nothing to the server.
The errors you may see:
| Error | What is wrong |
|---|---|
Key is not the correct length or format |
A key was cut short, or has a stray character. Keys are 44 characters of base64 ending in = |
Line unrecognized: `...' |
A typo in a key name, or a wg-quick line you didn't strip |
Unable to parse IP address: `...' |
A bad AllowedIPs entry |
Unable to parse empty endpoint |
Endpoint = with no value |
Then check that the keys match the server, which no parser can do for you. Derive the client's public key and look for it on the server:
$ grep PrivateKey wg0.conf | cut -d= -f2- | tr -d ' ' | wg pubkey
$ sudo wg show wg0 peers # on the server
And compare the PublicKey in the client's [Peer] section with sudo wg show wg0 public-key on the server. A wrong key here is the most common reason a config parses fine and never gets a handshake.
How do I test a WireGuard config online safely?
Use a peer made only for testing, and a tester that runs the full handshake and then sends traffic, not one that only pings the port. The TunnelHQ free tester works this way. Paste the config or upload the .conf file, and one of its checkers brings up the tunnel from your config, sends a packet to start the handshake, and waits for it to complete. When the handshake works, it sends traffic through the tunnel to well-known public addresses and shows the exit IP it got back.
What it does with the lines in your config:
PrivateKey,PublicKey,PresharedKey,Endpoint,AllowedIPsare applied as written. The endpoint may be a host name.MTUis used when you set it. Otherwise the checker sizes the tunnel from the path to your server.DNS,Table,PostUpand the other wg-quick hooks are ignored. No script in a pasted config runs.
Because the test traffic goes to public internet addresses, the config needs AllowedIPs that covers them, such as 0.0.0.0/0. A split-tunnel config that only routes 10.0.0.0/24 never sends a packet to the server, so the test fails without a handshake.
The result is either "WireGuard handshake worked" or "WireGuard handshake failed" with a reason:
- Server not connected: no handshake. The checker sent initiations and got no reply. From outside, a wrong key and an unreachable server look exactly the same, because the server answers neither, so check both.
- Internet stuck: the handshake worked, but traffic through the tunnel didn't come back. Look at forwarding, NAT and the server's firewall, as in the first section.
On safety: the private key and the rest of the config are used for that one test and not stored or logged. Even so, use a dedicated test peer. It keeps a test from taking over a real user's session, and if the config ever leaks, you remove one peer and nobody else is affected. The tester gives each IP address 5 tests every 24 hours.
How do I get alerted when handshakes stop?
Check the handshake age of peers that should always be up, and alert when it goes over about three minutes. That works for site-to-site links and anything with PersistentKeepalive. It doesn't work for phones and laptops, whose handshakes are old whenever they are idle.
This script prints any peer on wg0 with no handshake in the last 180 seconds and exits with an error if it finds one:
#!/bin/sh
# /usr/local/bin/check-wg-handshakes.sh
now=$(date +%s)
status=0
wg show wg0 latest-handshakes | {
while read -r key ts; do
age=$((now - ts))
if [ "$ts" -eq 0 ] || [ "$age" -gt 180 ]; then
echo "stale: $key"
status=1
fi
done
exit $status
}
Run it from cron and have it report to a heartbeat that alerts when the pings stop:
*/5 * * * * /usr/local/bin/check-wg-handshakes.sh && curl -fsS -m 10 --retry 3 -o /dev/null "$HEARTBEAT_URL"
TunnelHQ Uptime heartbeats work for this, and so does any other heartbeat service. The weak spot is obvious: the script runs on the server, so when the whole server is down or unreachable, the heartbeat goes quiet instead of reporting a specific failure. You still find out, but not what broke.
For a check from outside, the TunnelHQ WireGuard monitor runs the same handshake and traffic test as the free tester, every 5 minutes on Starter, every 2 minutes on Pro and every minute on Business, and alerts when it fails. A failed handshake shows as "Tunnel unreachable", and a tunnel that connects without passing traffic as "Tunnel established - Internet is stuck". The WireGuard docs page has the setup. Every plan has a 14-day free trial. The VPN server monitoring guide covers what else is worth watching on the server itself.


