Andrey's Blog

Mullvad tunnel with wg-quick and systemd

0. Setup this note refers to

ItemValue
Interface namemullvad
Config/etc/wireguard/mullvad.conf
MachineArch, NetworkManager + systemd-resolved
Kill switchiptables rules in PostUp / PreDown
Packagewireguard-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

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 seeMeaning
No outputInterface doesn’t exist
Recent handshake, both counters movingWorking
Old handshake, no trafficIdle — normal, re-handshakes on demand
sent climbing, received frozenPackets leaving, nothing back: UDP 51820 blocked, captive portal, or account out of time
Recent handshake but nothing loadsRouting 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

StateQuestionChanged by
active / inactiveRunning right now?start / stop
enabled / disabledStarts at boot?enable / disable
maskedForbidden entirelymask / 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, not wg-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

7. Troubleshooting

SymptomCheck
wg-quick: 'x' does not existls /etc/wireguard/ — name must match <name>.conf
Unit wg-quick@x.service does not existls -l /usr/lib/systemd/system/wg-quick@.service; pacman -Qkk wireguard-tools; reinstall; daemon-reload
resolvconf: signature mismatchopenresolv can’t write systemd’s resolv.confpacman -S systemd-resolvconf (replaces openresolv)
Unit fails to start, tunnel already upwg-quick down first, or reset-failed then start
Containers stall on external requestsMTU: tunnel 1420 vs bridge 1500 → {"mtu": 1420} in /etc/docker/daemon.json, recreate networks
localhost 502Check 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

PathWhat
/etc/wireguard/mullvad.confConfig + kill switch (mode 600)
/usr/lib/systemd/system/wg-quick@.serviceTemplate unit (package-owned)
/etc/systemd/system/multi-user.target.wants/wg-quick@mullvad.serviceSymlink created by enable
/etc/resolv.confSymlink → /run/systemd/resolve/stub-resolv.conf
/etc/systemd/resolved.confCheck for an Omarchy-set global DNS= that could bypass the tunnel