Mullvad tunnel with wg-quick and systemd
0. Setup this note refers to
| Item | Value |
|---|---|
| Interface name | mullvad |
| Config | /etc/wireguard/mullvad.conf |
| Machine | Arch, NetworkManager + systemd-resolved |
| Kill switch | iptables rules in PostUp / PreDown |
| Package | wireguard-tools (ships wg-quick@.service) |
The name after wg-quick@ and in every wg-quick command is just the filename in /etc/wireguard/ without .conf.
1. Cheat sheet
sudo systemctl start wg-quick@mullvad # up
sudo systemctl stop wg-quick@mullvad # down
sudo wg show # is it working?
curl -m5 https://am.i.mullvad.net/connected # am I exiting through Mullvad?
journalctl -u wg-quick@mullvad -n 30 --no-pager
2. Manual control (wg-quick)
sudo wg-quick up mullvad # create interface, add routes, run PostUp
sudo wg-quick down mullvad # run PreDown, delete interface
wg-quick up is not a daemon. It creates a kernel interface and exits. Nothing removes the tunnel on its own — not switching Wi-Fi, not suspend. Only an explicit down, ip link delete, or a reboot.
Test a config without installing it
Path must end in .conf:
sudo wg-quick up ~/Downloads/de-ber-wg-001.conf
Install a downloaded config
sudo install -D -m 600 ~/Downloads/<file>.conf /etc/wireguard/mullvad.conf
-m 600— permissions (the file holds a private key)-D— create parent directories
Doing it in one step avoids the window where the file is world-readable.
3. Status and verification
sudo wg show # all interfaces
sudo wg show mullvad # one interface
sudo wg show interfaces # names only
Reading wg show
| What you see | Meaning |
|---|---|
| No output | Interface doesn’t exist |
| Recent handshake, both counters moving | Working |
| Old handshake, no traffic | Idle — normal, re-handshakes on demand |
sent climbing, received frozen | Packets leaving, nothing back: UDP 51820 blocked, captive portal, or account out of time |
| Recent handshake but nothing loads | Routing or DNS problem, not the tunnel |
PersistentKeepalive = 25 in [Interface] keeps the tunnel warm and makes roaming to a new network recover in seconds.
Confirm exit and DNS
curl -m5 https://am.i.mullvad.net/connected
resolvectl status mullvad # want 10.64.0.1 and "~." routing domain
Full check including DNS leaks: https://mullvad.net/check in the browser.
4. systemd
Start / stop
sudo systemctl start wg-quick@mullvad
sudo systemctl stop wg-quick@mullvad
sudo systemctl restart wg-quick@mullvad
sudo systemctl reload wg-quick@mullvad # wg syncconf, no tunnel drop
Boot behaviour
sudo systemctl enable wg-quick@mullvad # start at boot
sudo systemctl disable wg-quick@mullvad # don't start at boot
sudo systemctl enable --now wg-quick@mullvad # both at once
Inspect
systemctl status wg-quick@mullvad
systemctl is-enabled wg-quick@mullvad
journalctl -u wg-quick@mullvad -n 30 --no-pager
journalctl -fu wg-quick@mullvad # follow live
Three independent states
| State | Question | Changed by |
|---|---|---|
| active / inactive | Running right now? | start / stop |
| enabled / disabled | Starts at boot? | enable / disable |
| masked | Forbidden entirely | mask / unmask |
A unit can be active but disabled. list-units only shows the first axis — use is-enabled for the second.
active (exited) is normal: the unit is Type=oneshot, so wg-quick runs once and exits while the tunnel stays up. systemd does not supervise the tunnel afterwards — a dead tunnel never shows as a failed unit.
Don’t mix the two control paths. Once managed by systemd, use
systemctl stop, notwg-quick down, or systemd’s view goes stale.
Removing / cleaning up
sudo systemctl disable --now wg-quick@mullvad
sudo systemctl reset-failed wg-quick@mullvad # clear a "failed" list entry
sudo systemctl mask wg-quick@mullvad # block entirely
sudo systemctl unmask wg-quick@mullvad
There is no file to delete: wg-quick@mullvad is an instance of the template wg-quick@.service, which belongs to the wireguard-tools package. Never rm anything under /usr/lib/systemd/system/.
Own units live in /etc/systemd/system/ — disable before deleting, then sudo systemctl daemon-reload.
5. Listing services
systemctl list-units --type=service # loaded now
systemctl list-units --type=service --all # incl. inactive
systemctl list-unit-files --type=service # everything on disk + state
systemctl list-unit-files --state=enabled # starts at boot
systemctl --failed # broken units
systemctl --user list-units --type=service # user session
systemctl list-unit-files 'wg-quick*'
wg-quick@.service always reads disabled — templates can’t be enabled, only instances. In status, preset: disabled is Arch’s default policy, not your state; the word before it is what counts.
6. Kill switch
Rules live in [Interface] (above [Peer]) and reject anything not leaving via the tunnel, except loopback, 192.168.0.0/16, and Docker bridges.
Verify the rules loaded
ACCEPTs must sit above the REJECT:
sudo iptables -vnL OUTPUT --line-numbers | head -8
Test that it actually closes
Down the interface directly — not wg-quick down, which removes the rules as it goes:
sudo ip link set down dev mullvad
curl -m5 https://example.com # must fail
sudo ip link set up dev mullvad
Gotchas
ufw reloadorsystemctl restart ufwflushes these rules while the tunnel stays up — no kill switch, no warning. Re-cycle the tunnel after touching ufw.- Rules live and die with the interface: they protect against a tunnel that drops while up, but not against the unit failing at boot. A ufw
default deny outgoing+allow out on mullvadbackstop survives both. - A failing
PreDowncommand aborts teardown and leaves the interface up. Append|| trueto thePreDownlines to avoid it.
7. Troubleshooting
| Symptom | Check |
|---|---|
wg-quick: 'x' does not exist | ls /etc/wireguard/ — name must match <name>.conf |
Unit wg-quick@x.service does not exist | ls -l /usr/lib/systemd/system/wg-quick@.service; pacman -Qkk wireguard-tools; reinstall; daemon-reload |
resolvconf: signature mismatch | openresolv can’t write systemd’s resolv.conf → pacman -S systemd-resolvconf (replaces openresolv) |
| Unit fails to start, tunnel already up | wg-quick down first, or reset-failed then start |
| Containers stall on external requests | MTU: tunnel 1420 vs bridge 1500 → {"mtu": 1420} in /etc/docker/daemon.json, recreate networks |
| localhost 502 | Check the app first: docker ps, docker logs --tail=30 <container> |
MTU test
ping -M do -s 1392 -c2 1.1.1.1 # 1420 total — should work
ping -M do -s 1472 -c2 1.1.1.1 # 1500 total — fails through tunnel
8. Files
| Path | What |
|---|---|
/etc/wireguard/mullvad.conf | Config + kill switch (mode 600) |
/usr/lib/systemd/system/wg-quick@.service | Template unit (package-owned) |
/etc/systemd/system/multi-user.target.wants/wg-quick@mullvad.service | Symlink created by enable |
/etc/resolv.conf | Symlink → /run/systemd/resolve/stub-resolv.conf |
/etc/systemd/resolved.conf | Check for an Omarchy-set global DNS= that could bypass the tunnel |