On this page
A script that checks whether wg0 exists can report success while every useful request times out. An interface, an authenticated peer, a working route, and a reachable application are separate signals. Reliable automation makes those distinctions explicit.
This example uses a Linux WireGuard client managed by wg-quick and systemd. It creates a small, read-only health probe, schedules it, and explains how to add bounded recovery without replacing a real firewall kill switch.
Define states before writing actions
Use explicit states instead of a single connected flag. For a basic client, distinguish an absent interface, a reachable tunnel endpoint, and a failed application probe. More advanced checks can separately record DNS resolution, recent peer handshakes, and public egress identity.
Use the JSON Formatter & Syntax Linter below to validate a sample status document before integrating it with monitoring. Consistent field names and types prevent downstream tools from treating the string false as a Boolean value. Test with invented interface names and generic error messages rather than real credentials.
Define success criteria for your policy. In this walkthrough, success means an HTTPS health endpoint under your control answers through the tunnel interface with a successful HTTP status. That is deliberately narrower than proving every application is protected. A split-tunnel client may need different checks for private services and public internet traffic.
Establish a manual baseline
First verify the existing VPN by hand. Install the distribution's WireGuard tools and curl if necessary. Do not create a second network manager for an interface already controlled by a desktop VPN client; choose one owner for lifecycle operations.
For a wg-quick profile at /etc/wireguard/wg0.conf, inspect its service and peer state:
sudo systemctl status wg-quick@wg0.service --no-pager
sudo wg show wg0 latest-handshakes
ip route get 192.0.2.10
sudo curl --interface wg0 --fail --silent --show-error --connect-timeout 5 --max-time 10 https://health.example.com/vpnReplace the health URL with an endpoint you operate that returns a 2xx response without authentication. The documentation hostname is a placeholder, not a public test service. Use a small response body, stable certificate, and no sensitive query parameters.
The route lookup uses a documentation address only to inspect routing; it does not test an actual server. Curl's interface binding checks this particular request, not the entire routing policy. DNS resolution may follow a separate system path, so treat resolver behavior as an additional test. Review WireGuard's quick start before changing peer routes or addresses.
Write a probe with predictable output
Create /usr/local/sbin/vpn-health as a root-owned executable. The script below emits fixed JSON fields and uses no untrusted text in its output. It intentionally does not restart anything, change routes, or alter firewall rules.
#!/usr/bin/env bash
set -u
PATH=/usr/sbin:/usr/bin:/sbin:/bin
iface=wg0
url=https://health.example.com/vpn
state=interface_down
ok=false
if ip link show dev "$iface" >/dev/null 2>&1; then
state=probe_failed
if curl --interface "$iface" --fail --silent --show-error --connect-timeout 5 --max-time 10 "$url" >/dev/null 2>&1; then
state=healthy
ok=true
fi
fi
printf '{"interface":"wg0","ok":%s,"state":"%s"}
' "$ok" "$state"
if [ "$ok" = true ]; then
exit 0
fi
exit 1Set ownership to root and permissions to 0750 using an administrator account. Keep the script directory unwritable by ordinary users. Do not let a web request choose the interface, URL, or command arguments for a privileged health check.
A timeout is ambiguous evidence: the remote application might be down, the hotspot may require sign-in, or the route might be broken. Do not translate every failure into a tunnel restart. Consider a second independent endpoint for diagnosis, while remembering that multiple probes add traffic and operational dependencies.
For scripts that include arbitrary error text, construct JSON with a real serializer such as jq instead of interpolating strings. Quotes, backslashes, and newlines can otherwise invalidate the status document or mislead log processors.
Schedule checks without overlapping jobs
Create a systemd service and timer using the following content in the two named files. The first unit runs one probe; the timer starts it after boot and repeats after the previous activation completes. A oneshot unit does not run overlapping copies of itself.
# /etc/systemd/system/vpn-health.service
[Unit]
Description=Read-only WireGuard health probe
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/vpn-health
TimeoutStartSec=20
User=root
UMask=0077
# /etc/systemd/system/vpn-health.timer
[Unit]
Description=Periodic WireGuard health probe
[Timer]
OnBootSec=45s
OnUnitInactiveSec=60s
Unit=vpn-health.service
[Install]
WantedBy=timers.targetReload systemd's configuration with systemctl daemon-reload, then enable and start vpn-health.timer. Inspect results with journalctl -u vpn-health.service and check scheduling with systemctl list-timers. A nonzero probe exit is expected for an unhealthy state, so a failed service entry does not necessarily mean the script itself crashed.
The unit uses minimal assumptions, not a complete sandbox. Root is used for interface binding compatibility; review whether a less privileged account works on your system. Keep network access available to the probe when adding service hardening. The systemd project documentation provides background on unit behavior and service management.
Add recovery only after observing failures
Start with notification-only automation for several real network transitions: boot, Wi-Fi roaming, sleep and resume, and temporary endpoint downtime. Preserve timestamps and state changes, but do not collect private keys or packet payloads. Reduce repetitive healthy messages if they overwhelm useful evidence.
If recovery is needed, require several consecutive failures and a cooldown before one authorized restart. Count attempts persistently enough to avoid a restart storm. Repeated failure should stop automated action and alert an operator, not endlessly reset networking. Ensure the script cannot terminate the SSH session used to repair a remote machine without an alternate access path.
Keep traffic blocking independent from health reporting. A script that runs once a minute leaves gaps if it is the only protection against direct egress. Use the VPN client's documented kill switch or a carefully tested firewall policy, following the kill-switch configuration guide. For firewall concepts, consult Ubuntu's UFW documentation.
Finally, copy a sanitized status line into the JSON validation tool and confirm its shape matches monitoring expectations. Test both healthy and unhealthy output. Good automation reports uncertainty honestly instead of promising protection from one green indicator.
Key takeaways
- Model interface state and application reachability as different observations.
- Use short timeouts, fixed JSON fields, and an endpoint under your control.
- Schedule read-only checks before adding restart privileges or recovery actions.
- Bound retries and preserve an alternate administrative access path.
- Enforce leak prevention independently of the monitoring script.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor