WireGuard client router on Arch Linux


Return to index

This guide assumes that you already set up a WireGuard server, or that you otherwise have access to one, for internet service.

A guide was written prior to this one, showing you how to set up your own WireGuard server, on any VPS provider. See: WireGuard server on Debian Linux.

I happened to have an Arch Linux machine running, that I already used for a few things, so I decided to re-purpose it. It is now my WireGuard client router, on my network. Let’s get started.

Regarding firewall configuration

Older versions of this guide used wg-quick, but note that we are using networkd now to configure WireGuard, so global routing tables are now handled declaratively, instead of having WireGuard itself modify them at runtime.

The firewall rules are no longer affected by WireGuard. This means you can, with the current setup documented here, safely reconfigure your firewall while the tunnel is online!

Dependencies

Install these dependencies:

pacman -S resolvconf wireguard-tools

If pacman asks which resolvconf implementation to use, just pick the systemd one. It will do nicely. (if you’re using a non-systemd Arch variant, then please adapt accordingly)

systemd-networkd

We will assume that you have systemd-networkd. If you have something else, such as NetworkManager, then you should adapt accordingly. All of the network management daemons are pretty much the same thing anyway, and they give you roughly the same features.

To enable and start networkd, do this:

systemctl enable --now systemd-networkd systemd-resolved

Also do this:

ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

NOTE: You could alternatively point it to /run/systemd/resolve/resolv.conf which contains the real upstream servers. The two are usually functionally equivalent for normal use, but the stub keeps all queries going through systemd-resolved. This is a matter of preference.

This ensures that /etc/resolv.conf points to systemd-resolved’s stub resolver, so DNS settings provided by systemd-networkd are actually used.

If you were already using it, then this is not necessary. You need both systemd-resolved and systemd-networkd for this configuration, at least if you follow our guide; if you adapt according to your own software choices, that’s fine. This guide mentions specific things like systemd-networkd, but the principles behind how this router works can be adapted to many other daemons and even operating systems.

Firstly, make sure you have two network interfaces. I have two ethernet cards in my machine, with names eno0 and enp2s0.

I used eno0 for LAN, and enp2s0 for my WAN (wireguard client).

Here is the setup, on my machine:

/etc/systemd/network/10-eno0.network

Again, this is my LAN setup:

[Match]
Name=eno0

[Network]
Address=10.69.0.1/24
Address=fd69:69:69::1/64
DHCP=no
IPv6AcceptRA=no
LinkLocalAddressing=no
ConfigureWithoutCarrier=yes

[Link]
IgnoreCarrierLoss=10s

The ConfigureWithoutCarrier setting is only used on LAN. This ensures that addresses are never torn down due to intermittent carrier issues.

Disabling DHCP explicitly will ensure that DHCP isn’t used. Ditto IPv6AcceptRA, disabling it ensures that your router won’t honour IPv6 router advertisements. We want to ensure that no auto-configuration shall occur. Ditto ’LinkLocalAddressing`. We want to ensure absolute control. Wireguard will do the rest, when the tunnel is up.

You don’t want surprise routes / DNS links being established automatically. You will control this with wireguard.

Note the use of 69 instead of 66 as before; the latter is on the tunnel side.

IgnoreCarrierLoss set to 10 seconds means we ignore loss of carrier link, for 10 seconds. Normally when hotplugging, everything is fine and your link gets set up again by networkd. However, split second lapses can wreak havoc on your network.

EEE/powersave/driver quirks can all cause split-second drops, so we introduce a but of insensitivity into networkd, to compensate. This is especially useful on Realtek NICs for example, or otherwise buggy NICs.

Now for WAN:

/etc/systemd/network/10-enp2s0.network

[Match]
Name=enp2s0

[Network]
Address=10.42.0.81/24
DHCP=no
IPv6AcceptRA=no
LinkLocalAddressing=no
DNSDefaultRoute=no
ConfigureWithoutCarrier=no

[Route]
Gateway=10.42.0.1
Destination=IPv4-address-of-your-VPS-goes-here/32

[Link]
IgnoreCarrierLoss=10s

Here, we want to configure with a carrier link, because this is required for the WAN. We want there to be no ambiguity here. Setting it to ConfigureWithoutCarrier=no ensures networkd will only apply configuration when carrier is present, so routes/addresses aren’t considered “up” during a physical disconnect.

Here, 10.42.0.81 is a LAN address too, but it is to the router that I use for normal internet (no tunnel). You might set it up differently.

Note how we are only specifying a subnet size of /24, but we have not specified a default route.

We have, in this example, specific a static route under the [Route] section. This static route goes through the gateway address 10.42.0.1, giving us routing for our WireGuard server’s IPv4 address, but only that will be routed.

No other IP addresses will be routed. There is no default route.

When the wireGuard tunnel is online, you will then have a default route, giving you internet. This is the assumption behind our setup. Continue reading!

WHY!? - simple. Doing it this way prevents DNS leaks. All traffic is routed through the tunnel by default.

Packet forwarding

I create a file, /etc/sysctl.d/10-pf.conf - with these contents:

net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1

You can apply these straight away without reboot, by doing:

sysctl --system

This enables packets to traverse between interfaces. Without it, your router will not work at all.

NOTE:

On non-systemd systems, you might just use the classic /etc/sysctl.conf instead, and run sysctl -p if you specifically want to load sysctl.conf from /etc - some variants of Arch do exist that use other system software e.g. s6, openrc.

This enables packets to traverse interfaces.

You are advised to use .d directories instead, as in the above example. This is the most common paradigm nowadays, with all sorts of Linux software.

Automatic re-connection

Run this when you links are up:

networkctl status enp2s0

Do it per interface, changing the network interface name shown here. You should see:

                       State: routable (configured)
                Online state: online

It should say configured, not unmanaged.

You don’t need anything special, because networkd automatically reconfigures an interface after reestablishing the physical link. The 10-second rule mitigates momentary losses.

Firewall

Make sure that no firewall is currently configured.

Disable/remove other firewall managers (ufw/firewalld) so that they will not conflict with your setup.

We will be using iptables manually.

Make sure that you have iptables and ip6tables commands available when logged in as root.

Note that modern iptables implementations typically just use nftables behind the scenes. I prefer the old iptables syntax, so I will just use that.

Wireguard client config

Note that we will be using systemd-networkd to configure it, not wg-quick. This avoids polluting the routing tables and firewall rules at runtime, enabling easier hotfixes during operation, and reconfiguration after the fact.

Create /etc/systemd/network/wg0.netdev with something like:

[NetDev]
Name=wg0
Kind=wireguard

[WireGuard]
PrivateKey=CLIENT PRIVATE KEY GOES HERE

[WireGuardPeer]
PublicKey=SERVER PUBLIC KEY GOES HERE
Endpoint=SERVER PUBLIC IP HERE:8999
AllowedIPs=0.0.0.0/0,::/0
PersistentKeepalive=25

PersistentKeepalive makes the tunnel reconnect after downtime.

The AllowedIPs entry defines which IP ranges WireGuard will carry; in this case, we’re saying all of them, because the zeroed ranges make all IP addresses be handled. Actual routing is still configured separately, in the [Route] sections of .network files. The .netdev file just configures Wireguard itself, irrespective of actual routing.

Create /etc/systemd/network/10-wg0.network with something like:

[Match]
Name=wg0

[Network]
Address=10.66.66.4/8
Address=fd66:66:66::4/64
DNS=COMMA SEPARATED NAME RESOLVERS HERE
Domains=~.

[Route]
Destination=0.0.0.0/0

[Route]
Destination=::/0

This file configures the actual routing, and sets IPs on the link. This is cleaner than the standard wg0.conf file you might have otherwise placed inside /etc/wireguard (which we didn’t create in this guide!)

The Destination lines set routes. In this case, we specify every destination as going through the wireguard tunnel, because these zeroed IP masks specify every IP address on the internet.

Note that the IPs shown above in Address are simply what I might have used at one point. Adapt according to your setup.

Enable WireGuard client

First, ping your WireGuard server IP to make sure that it works.

Ensure that there is no other route. If you didn’t reboot already, or didn’t reset the network, do this:

networkctl reconfigure eno0 enp2s0 wg0

After that, check:

ip route show table all

If you see any default routes at this point, you should fix that. There should only be a static route to your VPS IP address. Ping that to make sure it works.

It is recommended that you use IPv4 for connecting to the VPN server, from your client router, because IPv4 will have a lower MTU overhead than IPv6; you can use IPv6 through the tunnel just fine.

Enable packet forwarding and NAT/NAT66

If you haven’t applied the network configuration yet, do the firewall steps first. If wg0 is already up, that’s fine — just be aware you may temporarily lose connectivity while adjusting firewall rules.

This will enable NAT and NAT66:

iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
ip6tables -t nat -A POSTROUTING -o wg0 -j MASQUERADE

This will clamp TCP MSS, to mitigate packet fragmentation on TCP connections:

iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1380
ip6tables -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360

Wireguard typically has an MTU overhead of 80 bytes on IPv4, leaving you with an MTU of 1420 on Ethernet MTU 1500. This is a rule of thumb, but you can check the MTU size on your tunnel specifically, by running ip a to check the reported MTU size.

MSS size is set without including the TCP header (20 bytes) and IP header (20 bytes for IPv4 and 40 bytes on IPv6).

We combine the TCP header and the IP header, subtracting from MTU, to decide MTU size. In so doing, other hosts will more reliably know to send smaller packets; the alternative is that you would have to deal with packet fragmentation, which can harm performance a lot more. Note that this change only pertains to TCP.

Many applications are designed to accomodate smaller MTUs, because lots of people use tunnel connections; it was even more common back in the day, and lots of people used much smaller MTUs, so applications tend to be quite resilient to this. Otherwise, just let Linux handle fragmentation. It’s what Linux was born to do!

If you want to, you can figure out precisely what your MTU is, by doing ping tests; consult the manual for your ping implementation, which will tell you how to set the packet size, when sending a ping. You test it on higher settings first until you find the lowest MTU that works.

Note that hardcoding the MSS size can be wrong here, depending on the MTU in an actual given routing path.

You could do these instead:

iptables  -A FORWARD -o wg0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
ip6tables -A FORWARD -o wg0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

But in our network, PMTU discovery can be buggy.

In practise, we can give this one example:

If you see stalls on HTTPS or large downloads, try switching between fixed MSS and --clamp-mss-to-pmtu, as in the above examples.

Stateful firewall

Optionally, you may disable incoming unsolicited traffic, by setting up a stateful packet filter. This is pretty basic, and standard for most networks.

I didn’t see the point in writing this section, since there is already an excellent guide on the Arch Linux wiki:

https://wiki.archlinux.org/title/Simple_stateful_firewall

You can adapt this for your needs. Note that some of this was already configured, if you followed this WireGuard guide, e.g. MASQUERADE rule for NAT.

Save firewall setup

When you’re done configuring your firewall, you can check it like so:

iptables-save
ip6tables-save

If the output satisfies you, you should then save it to a file. You can use these commands.

For IPv4:

iptables-save > /etc/iptables/iptables.rules

For IPv6:

ip6tables-save > /etc/iptables/ip6tables.rules

You should make the rules persistent across reboots. You can do this quite simply:

systemctl enable iptables
systemctl enable ip6tables

You might open/forward ports as well, if you wish. Note that you would also have to open and forward them on your WireGuard server, and the previous WireGuard server guide provides some guidance for that.

This does something similar to iptables-persistent like on Debian. It just applies the iptables config at boot time.

Start wireguard!

Just restart networking:

networkctl reconfigure eno0 enp2s0 wg0

We reconfigure with networkctl, rather than restarting systemd-networkd, to avoid tearing down unrelated interfaces if you happen to have any. This also reduces the chance of being kicked out if you’re logged in remotely.

If all went well, you should have internet on your client router.

You could perhaps run a few simple tests, such as:

ip route get 1.1.1.1
ip -6 route get 2001:4860:4860::8888
ip route get <VPS_PUBLIC_IP>

Expected:

You can replace the IPs with other examples, for your purposes. Perhaps you might also check that DNS works, e.g.:

host google.com
ping google.com

Note that we only checked the route and checked ICMP; let’s check TCP! Perhaps you could wget a web page (if you have wget):

wget https://google.com/

Check the status of wg0 (will report to you any problems):

networkctl status wg0
wg show

Check routes:

ip route show table all

You want to ensure that only the VPS(or VPN server, presumably a VPS) IP goes through your normal gateway; all other IPs must go through the tunnel.

Now connect your clients to the client router.

Your client router has these LAN IP ranges:

Your gateway address for clients, on LAN side, shall be that of the LAN IPs of your WireGuard router, specifically:

So a client behind the router might use e.g. 10.69.0.2 and fd69:69:69::2.

IPv6 ULA GetAddressInfo

As before, in the previous guide, you are using NAT66 which means you have only a ULA, no GUA, assigned on your client machines. This means that DNS functions may resolve IPv4 first, even if v6 is available; it is better to resolve IPv6 first, when available.

To mitigate this, you can use a custom priority for ULA ranges, with a modified /etc/gai.conf like so:

## RFC 6724 Default
label ::1/128        0
label ::/0           1
label ::ffff:0:0/96  4
label 2002::/16      2
label 2001::/32      5
# label fc00::/7      13
# Changed the above line for IPv6-ULA-first workaround.
label fc00::/7       1
label ::/96          3
label fec0::/10     11
label 3ffe::/16     12

precedence ::1/128       50
precedence ::/0          40
precedence ::ffff:0:0/96 35
precedence 2002::/16     30
precedence 2001::/32      5
precedence fc00::/7       3
precedence ::/96          1
precedence fec0::/10      1
precedence 3ffe::/16      1

A bit hacky, but it does work. The above example assumes Linux; adapt according to whatever operating system you use.

Automatically restart on failure

If something happens and WireGuard (client) fails, sometimes the tunnel can stay “up” even if there’s no connection. It can happen sometimes. Sometimes the tunnel can go stale.

To mitigate this, we can periodically ping inside the tunnel. Remember that the WAN side of the tunnel router routes via 10.66.66.1 in this setup; this assumes that you used the setup described in WireGuard VPN server on Debian Linux - please adapt accordingly, if you use a different setup (e.g. other VPN provider).

Create the file /usr/local/sbin/wg-watchdog.sh with these contents:

#!/bin/sh
TARGET="10.66.66.1"

if ! ping -I wg0 -c 1 -W 2 "$TARGET" >/dev/null 2>&1; then
	networkctl reconfigure wg0
fi

If you want it to be a bit less sensitive to temporary blips, perhaps do this:

#!/bin/sh
TARGET="10.66.66.1"

if ! ping -I wg0 -c 1 -W 2 "$TARGET" >/dev/null 2>&1; then
if ! ping -I wg0 -c 1 -W 2 "$TARGET" >/dev/null 2>&1; then
	networkctl reconfigure wg0
fi
fi

On some links, momentary ping blips may be likely, so adding more pings reduces the chance of a false positive restarting your network without good reason.

Make it executable:

chmod +x /usr/local/sbin/wg-watchdog.sh

Make sure also that you do have the networkctl command on your system!

Now create a corresponding systemd timer by creating the file /etc/systemd/system/wg-watchdog.service with these contents:

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/wg-watchdog.sh

And create the file /etc/systemd/system/wg-watchdog.timer with these contents:

[Timer]
OnBootSec=20
OnUnitActiveSec=10

[Install]
WantedBy=timers.target

Pay attention here to the OnUnitActiveSec time; the general rule of thumb is that you should count the maximum amount of time ping will spend during tests, adding up -W if you do multiple pings (always specify -W in ping tests). For example, if you did two pings with -W 2, that’s nominally 4 seconds, but we’ll allow for 5. That leaves 5 seconds or so of slack in the ten-second interval as above, before restarting pings. Please adapt according to whatever setup you decide to use.

NOTE: An earlier version of this guide set OnUnitActiveSec to 15, but now we set it to 10. We absolutely want the tunnel to be online as much as possible. The watchdog script should take no more than 2-3 seconds to finish, so running it every 10 seconds is acceptable, intended to reduce downtime. This reduces the chance that time-sensitive applications (on your clients) will break. For example, a shorter downtime reduces the chance of VOIP calls ending.

Note that 10 seconds is a bit aggressive. You might set it to 15 seconds, if you’re a little bit more conservative than I am.

NOTE: Some VPS providers may rate-limit ICMP or your path drops occasional pings; you may wish to increase the interval, to prevent unnecessary reconfigurations at runtime, but 10 seconds is probably pretty safe.

The 2 second rule set by -W 2 means that momentary blips in lost ping are less likely to affect you, in this context. It’s still quite fast, but you might tighten all of it; you might set a 5 second OnUnitActiveSec time, and set 1 second on the -W switch in the ping command.

It really just depends on how fast you want this to be. A two-second ping timeout is ideal, because it’s short enough to not be too slow, but long enough to tolerate some intermittent blips. We want resiliency, but we don’t want it to be so sensitive as to cause downtime during momentary blips. You should play with these numbers, testing in the real world on your own network.

Now enable it at boot time:

systemctl enable --now wg-watchdog.timer

Note that the --now option here also starts the timer, so you should be all set after running this.

Firewall woes

It should be noted that this iptables-based setup is extremely fragile, if you are running anything else that manipulates packet filtering. It is of the utmost importance that you do not let that happen.

You are applying the iptables rules at startup, as described in this guide.

You could adapt accordingly, and just use standard nftables syntax.

If you happened to have standalone iptables and you had nftables, that could be bad, if you had conflicting rules. You should make sure that the iptables version you have on your system is the nftables one (modern iptables just uses nftables under the hood).

As I said, I prefer the iptables syntax. I see no reason to invest time in re-learning something when what I’m using still works perfectly.

DHCP

Pretty pointless if it’s just you on it. You might set up something like isc-dhcp-server or similar.

The ISC one is the old (read: good) one. Simple for most needs. ISC has a new DHCP server now but I never bothered with it, the old one suits me just fine when I need it.

In my case, I don’t bother. At this point, I’m happy and I just use my router. I plug an OpenWRT router into this, for WiFi and such. A bit over-engineered at that point, but consider that most OpenWRT routers are quite under-powered and might not get good speed on WireGuard, which is precisely why I made my own dedicated WireGuard box. I want to reliably have 1Gbps WireGuard tunnels, so I’m using a faster x86_64 machine to do the job (I use Libreboot btw).

Yes. If this sounds easy, it’s because it is. Enjoy!

REBOOT

After you’ve verified everything, please REBOOT your router. This is important, because you want to make sure everything works when turning it back on.

If everything works fine after rebooting the router, then you now have a fully functional new router. Otherwise, check your configuration.

Markdown file for this page: https://fedfree.org/docs/router/wireguard-client.md

Site map

This HTML page was generated by the Libreboot Static Site Generator.