Multi-Hop WireGuard

A simple WireGuard Hub and Spoke VPN (Virtual Private Network) allows you to connect two or more endpoints together through a central hub. But you can also marry this basic topology to other topology primitives to create a more sophisticated network that carries traffic through multiple hops from one endpoint to some far-flung site (or the Internet) at the other end of your WireGuard network.

In this article, we’ll explore several scenarios that do this:

This article will build off the basic hub-and-spoke and point-to-site configurations explained by the WireGuard Hub and Spoke Configuration and WireGuard Point to Site Configuration articles, so refer to them for detailed explanations of the core WireGuard configuration settings used in this article.

The Guiding Principle

The guiding principle and most important thing to remember when configuring the routing for WireGuard interfaces (in this article, and everywhere) is this:

Use the AllowedIPs setting of each peer to specify the traffic you want to send to or send through the peer.

Specifically, AllowedIPs should be the list of IP addresses and IP address ranges that are used as the destination address for all packets that should be routed to (or through) the peer.

This means that the AllowedIPs setting is usually not symmetric between two peers: For example, if you want Host A to send all its outgoing Internet traffic through Host β, you would set AllowedIPs = 0.0.0.0/0, ::/0 in Host A’s peer configuration for Host β. But on Host β, if you want to send Host A only traffic returning from the Internet that has Host A as its destination, you would set AllowedIPs = 10.0.0.1/32, fd10:0:0:1::/64 in Host β’s peer configuration for Host A (if 10.0.0.1 was Host A’s IPv4 address and fd10:0:0:1::1 was its IPv6 address within the WireGuard network).

Site Gateway as a Spoke

For our first scenario, we have an endpoint with WireGuard running on it, Endpoint A, from which we want to access several other endpoints not running WireGuard — like Endpoint B, in Site B. To get from Endpoint A to Site B in this scenario, WireGuard traffic needs to go through two hops: one through the VPN hub, Host C; and the second through a spoke of the hub, Host β — the WireGuard gateway to Site B:

Hub with a Site Gateway Spoke

Our WireGuard network uses a subnet of 10.0.0.0/24, within which Endpoint A has an IP address of 10.0.0.1, Host C has an IP address of 10.0.0.3, and Host β has an IP address of 10.0.0.2. Site B uses a subnet of 192.168.200.0/24, with Host β having an IP address of 192.168.200.2 in it, and Endpoint B having an IP address of 192.168.200.22. Host C has a public IP address of 192.0.2.3, allowing Endpoint A and Host β each to establish a WireGuard tunnel to it.

Endpoint A

On Endpoint A, we want to send all the traffic destined for the 10.0.0.0/24 and 192.168.200.0/24 networks through Host C. So those two address blocks are what we put in the AllowedIPs setting for Host C in Endpoint A’s WireGuard config:

# /etc/wireguard/wg0.conf

# local settings for Endpoint A
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32
ListenPort = 51821

# remote settings for Host C
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
Endpoint = 192.0.2.3:51823
AllowedIPs = 10.0.0.0/24, 192.168.200.0/24

Host C

On Host C, we want to send all the traffic destined for 10.0.0.1 to Endpoint A; and all traffic for 10.0.0.2 and the 192.168.200.0/24 network to Host β. So we configure WireGuard on Host C with AllowedIPs = 10.0.0.1/32 in its Endpoint A [Peer] section; and AllowedIPs = 10.0.0.2/32, 192.168.200.0/24 in its Host β [Peer] section:

# /etc/wireguard/wg0.conf

# local settings for Host C
[Interface]
PrivateKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCGA=
Address = 10.0.0.3/32
ListenPort = 51823

PreUp = sysctl -w net.ipv4.ip_forward=1

# remote settings for Endpoint A
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIPs = 10.0.0.1/32

# remote settings for Host β
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
AllowedIPs = 10.0.0.2/32, 192.168.200.0/24

Host B

And on Host β, we want to send all the traffic destined for the 10.0.0.0/24 network through Host C — so that’s what we use as the AllowedIPs setting for Host C in Host β’s WireGuard config:

# /etc/wireguard/wg0.conf

# local settings for Host β
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32
ListenPort = 51822

# IP forwarding
PreUp = sysctl -w net.ipv4.ip_forward=1
# IP masquerading
PreUp = iptables -t mangle -A PREROUTING -i wg0 -j MARK --set-mark 0x30
PreUp = iptables -t nat -A POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE
PostDown = iptables -t mangle -D PREROUTING -i wg0 -j MARK --set-mark 0x30
PostDown = iptables -t nat -D POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE

# remote settings for Host C
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
Endpoint = 192.0.2.3:51823
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25

Note that in Host β’s configuration for Host C, we’ve also included a PersistentKeepalive = 25 setting. This ensures that Host β pokes a hole through any NAT (or egress-only) firewalls between Host C and Host β, allowing Endpoint A to initiate connections through the WireGuard network to Host β. You can omit this setting if there are no NAT or egress-only firewalls between Host C and Host β (but if you do that, make sure you instead set up Host C’s WireGuard config to include the public IP address of Host β in its Endpoint setting for Host β).

Also note that we’ve included some iptables rules in Host β’s WireGuard configuration. These iptables rules masquerade packets from the WireGuard network when Host β forwards them out to Site B. You can omit these rules if the LAN router (or each individual endpoint) at Site B is already configured to route traffic destined for the WireGuard network (10.0.0.0/24) back through Host β. Also, if Host β uses nftables instead of iptables, omit these iptables rules, and instead configure nftables on Host β according to the “Point to Site” section of the How to Use WireGuard With Nftables guide.

Test It Out

In this scenario, we have a webserver running on Endpoint B (TCP port 80 of 192.168.200.22). So after starting up our configured WireGuard interfaces on each host (Endpoint A, Host C, and Host β), we can run cURL (or a regular webrowser) from Endpoint A to access the webserver on Endpoint B at 192.168.200.22:

$ curl 192.168.200.22
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
...

Internet Gateway as a Spoke

In our second scenario, we have an endpoint with WireGuard running on it, Endpoint A, from which we want to access the Internet. To get from Endpoint A to the Internet in this scenario, however, WireGuard traffic needs to go through two hops: one through the VPN hub, Host C; and the second through a spoke of the hub, Host β. However, while we want to use Host β as the Internet gateway for Endpoint A, we don’t want that for Host C — Host C itself can use the default routes at Site C to access the Internet as needed.

Hub with an Internet Gateway Spoke

Within our WireGuard network, Endpoint A has an IPv4 address of 10.0.0.1 and an IPv6 address of fd10:0:0:1::1; Host C has an IPv4 address of 10.0.0.3, and an IPv6 address of fd10:0:0:3::1; and Host β has an IPv4 address of 10.0.0.2 and an IPv6 address of fd10:0:0:2::1. Host C has a public IP address of 192.0.2.3, allowing Endpoint A and Host β to each establish a WireGuard tunnel to it.

Endpoint A

On Endpoint A, when the WireGuard network is up, we want to send all Internet traffic through Host C, so we configure AllowedIPs = 0.0.0.0/0, ::/0 for Host C in Endpoint A’s WireGuard config:

# /etc/wireguard/wg0.conf

# local settings for Endpoint A
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32, fd10:0:0:1::1/64
ListenPort = 51821

# remote settings for Host C
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
Endpoint = 192.0.2.3:51823
AllowedIPs = 0.0.0.0/0, ::/0

0.0.0.0/0 is the entire IPv4 space, and ::/0 is the entire IPv6 space. (If you don’t care about IPv6 traffic, you can omit the IPv6 addresses and address blocks from this and the other WireGuard configurations.)

Tip

If you want to avoid using Endpoint A’s default DNS resolver when its WireGuard interface is up, add the following line to the [Interface] section of its config file (to use the Quad9 DNS resolver instead):

DNS = 9.9.9.9, 149.112.112.112

Note that if, however, Endpoint A uses systemd, you probably will need instead to use a systemd-specific command; see the “Override DNS Globally” section of the WireGuard DNS Configuration for Systemd guide for details.

Host C

On Host C, we want to send all the traffic destined for 10.0.0.1 or fd10:0:0:1::/64 to Endpoint A, and all other traffic that comes through our WireGuard network on to Host β. So we configure WireGuard on Host C with AllowedIPs = 10.0.0.1/32, fd10:0:0:1::/64 for its Endpoint A [Peer] section, and AllowedIPs = 0.0.0.0/0, ::/0 for its Host β [Peer] section.

But since we don’t want all of Host C’s traffic to go to Host β — just traffic forwarded through the WireGuard network — we configure the routes for this WireGuard interface to use a custom routing table, via the interface’s Table = 123 setting. And we use a PreUp command to add a policy routing rule that directs the host to use this table only for traffic coming in from this WireGuard interface (ip rule add iif wg0 table 123 priority 456):

# /etc/wireguard/wg0.conf

# local settings for Host C
[Interface]
PrivateKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCGA=
Address = 10.0.0.3/32, fd10:0:0:3::1/64
ListenPort = 51823
Table = 123

# IPv4 forwarding & routing
PreUp = sysctl -w net.ipv4.ip_forward=1
PreUp = ip rule add iif wg0 table 123 priority 456
PostDown = ip rule del iif wg0 table 123 priority 456

# IPv6 forwarding & routing
PreUp = sysctl -w net.ipv6.conf.all.forwarding=1
PreUp = ip -6 rule add iif wg0 table 123 priority 456
PostDown = ip -6 rule del iif wg0 table 123 priority 456

# remote settings for Endpoint A
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIPs = 10.0.0.1/32, fd10:0:0:1::/64

# remote settings for Host β
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
AllowedIPs = 0.0.0.0/0, ::/0
Note

If you want to connect to Host C itself from one or more of its WireGuard endpoints, instead of merely forwarding traffic through it — for example, to ping Host C or SSH into it through its WireGuard connection — you also need to add one or more routes to Host C’s main routing table that lets it know how to route its own traffic to the WireGuard network.

You could do that by adding routes via PostUp commands in Host C’s WireGuard config:

PostUp = ip route add 10.0.0.0/24 dev wg0
PostUp = ip -6 route add fd10::/56 dev wg0

But if Host C’s own WireGuard address falls within the network range you use for the rest of the WireGuard network, you can simply adjust the netmask on Host C’s WireGuard interface address to include that range:

Address = 10.0.0.3/24, fd10:0:0:3::1/56
Tip

For your own documentation purposes, you may want to add an entry to the host’s /etc/iproute2/rt_tables file for any custom routing tables you use. For example, the following command will define routing table 123 with a name of mywg:

$ echo 123 mywg | sudo tee -a /etc/iproute2/rt_tables

If you do that, you can then also use the name mywg to identify the table in your WireGuard config (and elsewhere on the system):

Table = mywg
PreUp = ip rule add iif wg0 table mywg priority 456
PreUp = ip -6 rule add iif wg0 table mywg priority 456

Host B

Similar to the previous Site Gateway as a Spoke scenario, on Host β, we want to send all the traffic destined for the 10.0.0.0/24 and fd10::/56 networks back through Host C — so that’s what we use as the AllowedIPs setting for Host C in Host β’s WireGuard config:

# /etc/wireguard/wg0.conf

# local settings for Host β
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32, fd10:0:0:2::1/64
ListenPort = 51822

# IPv4 forwarding
PreUp = sysctl -w net.ipv4.ip_forward=1
# IPv4 masquerading
PreUp = iptables -t mangle -A PREROUTING -i wg0 -j MARK --set-mark 0x30
PreUp = iptables -t nat -A POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE
PostDown = iptables -t mangle -D PREROUTING -i wg0 -j MARK --set-mark 0x30
PostDown = iptables -t nat -D POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE

# IPv6 forwarding
PreUp = sysctl -w net.ipv6.conf.all.forwarding=1
# IPv6 masquerading
PreUp = ip6tables -t mangle -A PREROUTING -i wg0 -j MARK --set-mark 0x30
PreUp = ip6tables -t nat -A POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE
PostDown = ip6tables -t mangle -D PREROUTING -i wg0 -j MARK --set-mark 0x30
PostDown = ip6tables -t nat -D POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE

# remote settings for Host C
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
Endpoint = 192.0.2.3:51823
AllowedIPs = 10.0.0.0/24, fd10::/56
PersistentKeepalive = 25

And just like Host β in the previous Site Gateway as a Spoke scenario, we’ve also included an Endpoint and PersistentKeepalive = 25 setting in Host β’s configuration for Host C, to ensure that Host β pokes a hole through the NAT firewall between Host C and it; and we’ve included some iptables rules in Host β’s WireGuard configuration to masquerade traffic from the WireGuard network. If Host β uses nftables instead of iptables, configure nftables on Host β according to the “Point to Site” section of the How to Use WireGuard With Nftables guide instead of using these iptables rules.

Test It Out

After starting up our configured WireGuard interfaces on each host (Endpoint A, Host C, and Host β), we can run cURL (or a regular webrowser) from Endpoint A to access Google (or any other Internet site) through our WireGuard network:

$ curl google.com
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
...

Chain of Hubs

For our third scenario, we have an endpoint with WireGuard running on it, Endpoint A, from which we want to access several other endpoints not running WireGuard — like Endpoint B, in Site B. But unlike our first Site Gateway as a Spoke scenario, to get from Endpoint A to Site B, WireGuard traffic needs to go through multiple hubs: Hub 1, Hub 2, and Hub 3; and then through a spoke of Hub 3 — Host β, the WireGuard gateway to Site B:

Chain of Hubs with a Site Gateway Spoke

Within our WireGuard network, Endpoint A and Hub 1 our on the same 10.0.1.0/24 subnet, with Endpoint A having an IP address of 10.0.1.1 and Hub 1 having an IP address of 10.0.1.3; Hub 2 is on a 10.0.2.0/24 subnet with an IP address of 10.0.2.3; and Hub 3 and Host β are on the same 10.0.3.0/24 subnet, with Hub 3 having an IP address of 10.0.3.3 and Host β having an IP address of 10.0.3.2.

At Site B, which uses a subnet of 192.168.200.0/24, Host β has an IP address of 192.168.200.2 within it, and Endpoint B has an IP address of 192.168.200.22.

Outside of the local sites and our WireGuard network, Hub 1 has a public IP address of 198.51.100.10, Hub 2 has a public IP address of 192.0.2.249, and Hub 3 has a public IP address of 203.0.113.33. These IP addresses allow Endpoint A to establish a WireGuard tunnel with Hub 1, Hub 1 to establish a tunnel with Hub 2, and Hub 2 and Host β to establish a tunnel with Hub 3.

Endpoint A

On Endpoint A, we want to send all the traffic destined for the 10.0.0.0/24, 10.0.2.0/24, 10.0.3.0/24, and 192.168.200.0/24 networks through through the first hop, Hub 1. So two address blocks are what we put in the AllowedIPs setting for Hub 1 in Endpoint A’s WireGuard config:

# /etc/wireguard/wg0.conf

# local settings for Endpoint A
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.1.1/32
ListenPort = 51821

# remote settings for Hub 1
[Peer]
PublicKey = hN+4fjPihcKbEDFQhqrMmW8oPqqaT4CUQYtyg3FUx3k=
Endpoint = 198.51.100.10:51823
AllowedIPs = 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24, 192.168.200.0/24

Hub 1

On Hub 1, we want to send all the traffic destined for 10.0.1.1 to Endpoint A, and all traffic for the 10.0.2.0/24, 10.0.3.0/24, and 192.168.200.0/24 networks to Hub 2. So we configure WireGuard on Hub 1 with AllowedIPs = 10.0.1.1/32 for its Endpoint A [Peer] section, and AllowedIPs = 10.0.2.0/24, 10.0.3.0/24, 192.168.200.0/24 for its Hub 2 [Peer] section:

# /etc/wireguard/wg0.conf

# local settings for Hub 1
[Interface]
PrivateKey = 0F11111111111111111111111111111111111111110=
Address = 10.0.1.3/32
ListenPort = 51823

PreUp = sysctl -w net.ipv4.ip_forward=1

# remote settings for Endpoint A
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIPs = 10.0.1.1/32

# remote settings for Hub 2
[Peer]
PublicKey = 777Ntbnv/AA9Fvd53yVR6Fsrd02CNCM2ySXgzXbbSUI=
Endpoint = 192.0.2.249:51823
AllowedIPs = 10.0.2.0/24, 10.0.3.0/24, 192.168.200.0/24

We’ve also specified the public IP address at which Hub 1 can access Hub 2 outside of WireGuard, via the Endpoint = 192.0.2.249 setting in its Hub 2 [Peer] section, in order to establish the WireGuard tunnel to Hub 2. If Hub 2 didn’t have a static IP address at which Hub 1 could access it, but Hub 1 did have a static IP address at which Hub 2 could access it, we could have omitted this Endpoint setting in Hub 1’s config, and instead configured an Endpoint setting for Hub 1 in Hub 2’s WireGuard config (with a PersistentKeepalive setting to ensure that Hub 2 proactively sets up the WireGuard tunnel between it and Hub 1 for use by connections initiated by Endpoint A).

Hub 2

On Hub 2, we want to send all the traffic destined for the 10.0.1.0/24 network to Hub 1, and all traffic for the 10.0.3.0/24 and 192.168.200.0/24 networks to Hub 3. So we configure WireGuard on Hub 2 with AllowedIPs = 10.0.1.0/24 for its Hub 1 [Peer] section, and AllowedIPs = 10.0.3.0/24, 192.168.200.0/24 for its Hub 3 [Peer] section:

# /etc/wireguard/wg0.conf

# local settings for Hub 2
[Interface]
PrivateKey = 2G22222222222222222222222222222222222222220=
Address = 10.0.2.3/32
ListenPort = 51823

PreUp = sysctl -w net.ipv4.ip_forward=1

# remote settings for Hub 1
[Peer]
PublicKey = hN+4fjPihcKbEDFQhqrMmW8oPqqaT4CUQYtyg3FUx3k=
Endpoint = 198.51.100.10:51823
AllowedIPs = 10.0.1.0/24

# remote settings for Hub 3
[Peer]
PublicKey = IZ2rER/G68c1LCHeXIkpHqKYT7zB5bLkNULSSJ5HI1U=
Endpoint = 203.0.113.33:51823
AllowedIPs = 10.0.3.0/24, 192.168.200.0/24

We’ve also specified the public IP addresses at which Hub 2 can access Hub 1 and Hub 3 outside of WireGuard in order to establish its WireGuard tunnels; for Hub 1 via Endpoint = 198.51.100.10:51823, and Hub 3 via Endpoint = 203.0.113.33:51823. While it’s possible to just specify an endpoint on one side of a tunnel (eg either in Hub 1 or Hub 2’s configuration for the other), it’s better to specify an endpoint for each side of the tunnel when possible (so that both sides can initiate a connection through the tunnel without having to wait for the other side to contact it first).

Hub 3

On Hub 2, we want to send all the traffic destined for the 10.0.1.0/24 and 10.0.2.0/24 networks to Hub 2, and all traffic for the 192.168.200.0/24 network to Host β. Therefore we configure WireGuard on Hub 3 with AllowedIPs = 10.0.1.0/24, 10.0.2.0/24 for its Hub 2 [Peer] section, and AllowedIPs = 192.168.200.0/24 for its Host β [Peer] section:

# /etc/wireguard/wg0.conf

# local settings for Hub 3
[Interface]
PrivateKey = 2H33333333333333333333333333333333333333330=
Address = 10.0.3.3/32
ListenPort = 51823

PreUp = sysctl -w net.ipv4.ip_forward=1

# remote settings for Hub 2
[Peer]
PublicKey = 777Ntbnv/AA9Fvd53yVR6Fsrd02CNCM2ySXgzXbbSUI=
Endpoint = 192.0.2.249:51823
AllowedIPs = 10.0.1.0/24, 10.0.2.0/24

# remote settings for Host β
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
AllowedIPs = 192.168.200.0/24

Host B

On Host β, we want to send all the traffic destined for the 10.0.1.0/24, 10.0.2.0/24, and 10.0.3.0/24 networks through Hub 3 — so that’s what we use as its AllowedIPs setting for Hub 3:

# /etc/wireguard/wg0.conf

# local settings for Host β
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.3.2/32
ListenPort = 51822

# IP forwarding
PreUp = sysctl -w net.ipv4.ip_forward=1
# IP masquerading
PreUp = iptables -t mangle -A PREROUTING -i wg0 -j MARK --set-mark 0x30
PreUp = iptables -t nat -A POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE
PostDown = iptables -t mangle -D PREROUTING -i wg0 -j MARK --set-mark 0x30
PostDown = iptables -t nat -D POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE

# remote settings for Hub 3
[Peer]
PublicKey = IZ2rER/G68c1LCHeXIkpHqKYT7zB5bLkNULSSJ5HI1U=
Endpoint = 203.0.113.33:51823
AllowedIPs = 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24
PersistentKeepalive = 25

Like Host β in our first Site Gateway as a Spoke scenario, we’ve also included an Endpoint and PersistentKeepalive = 25 setting in Host β’s configuration for Hub 3, to ensure that Host β pokes a hole through the NAT firewall between Hub 3 and it; and we’ve included some iptables rules in Host β’s WireGuard configuration to masquerade traffic from the WireGuard network. If Host β uses nftables instead of iptables, configure nftables on Host β according to the “Point to Site” section of the How to Use WireGuard With Nftables guide instead of using these iptables rules (or if Site B is configured to route traffic to Endpoint A, 10.0.1.1, back through Host β, you don’t need any masquerading rules).

Test It Out

In this scenario, we have a webserver running on Endpoint B (TCP port 80 of 192.168.200.22). So after starting up our configured WireGuard interfaces on each host (Endpoint A, Hub 1, Hub 2, Hub 3, and Host β), we can run cURL (or a regular webrowser) from Endpoint A to access the webserver on Endpoint B at 192.168.200.22:

$ curl 192.168.200.22
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
...

Hub is Also a Site Gateway

In our fourth scenario, we have an endpoint with WireGuard running on it, Endpoint A, from which we want to access several other endpoints not running WireGuard, like Endpoint B, in Site B — as well as Endpoint C, in Site C. To get from Endpoint A to Site B, WireGuard traffic needs to go through two hops: one through the VPN hub, Host C; and the second through a spoke of the hub, Host β. But to get from Endpoint A to Site C, WireGuard traffic only needs one hop: just through the VPN hub, Host C:

Hub That is Also a Site Gateway

Our WireGuard network uses a subnet of 10.0.0.0/24, within which Endpoint A has an IP address of 10.0.0.1, Host C has an IP address of 10.0.0.3, and Host β has an IP address of 10.0.0.2. Site B uses a subnet of 192.168.200.0/24, with Host β having an IP address of 192.168.200.2 within it, and Endpoint B having an IP address of 192.168.200.22.

Site C uses a subnet of 192.168.33.0/24, with Host C having an IP address of 192.168.33.3 within it, and Endpoint C having an IP address of 192.168.33.13. Host C also has a public IP address of 192.0.2.3, allowing Endpoint A and Host β each to establish a WireGuard tunnel to it.

Endpoint A

On Endpoint A, we want to send all the traffic destined for the 10.0.0.0/24, 192.168.33.0/24, and 192.168.200.0/24 networks through Host C. So those three address blocks are what we put in the AllowedIPs setting for Host C in Endpoint A’s WireGuard config:

# /etc/wireguard/wg0.conf

# local settings for Endpoint A
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32
ListenPort = 51821

# remote settings for Host C
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
Endpoint = 192.0.2.3:51823
AllowedIPs = 10.0.0.0/24, 192.168.33.0/24, 192.168.200.0/24

Host C

On Host C, we want to send all the traffic destined for 10.0.0.1 to Endpoint A, and all traffic for 10.0.0.2 and the 192.168.200.0/24 network on to Host β. So we configure WireGuard on Host C with AllowedIPs = 10.0.0.1/32 for its Endpoint A [Peer] section, and AllowedIPs = 10.0.0.2/32, 192.168.200.0/24 for its Host β [Peer] section:

# /etc/wireguard/wg0.conf

# local settings for Host C
[Interface]
PrivateKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCGA=
Address = 10.0.0.3/32
ListenPort = 51823

# IP forwarding
PreUp = sysctl -w net.ipv4.ip_forward=1
# IP masquerading
PreUp = iptables -t mangle -A PREROUTING -i wg0 -j MARK --set-mark 0x30
PreUp = iptables -t nat -A POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE
PostDown = iptables -t mangle -D PREROUTING -i wg0 -j MARK --set-mark 0x30
PostDown = iptables -t nat -D POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE

# remote settings for Endpoint A
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIPs = 10.0.0.1/32

# remote settings for Host β
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
AllowedIPs = 10.0.0.2/32, 192.168.200.0/24

But because Host C is also a gateway to Site C, we’re also including some iptables rules in Host C’s WireGuard configuration to masquerade traffic from the WireGuard network when Host C forwards it out to Site C. You can omit these rules if the LAN router (or each individual endpoint) at Site C is configured to route traffic to the WireGuard network (10.0.0.0/24) back through Host C. Also, if Host C uses nftables instead of iptables, configure nftables on Host C according to the “Point to Site” section of the How to Use WireGuard With Nftables guide instead of using these iptables rules.

Host B

On Host β, we want to send all the traffic destined for the 10.0.0.0/24 network through Host C, so that’s what we use for its AllowedIPs setting for Host C in Host β’s WireGuard config:

# /etc/wireguard/wg0.conf

# local settings for Host β
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32
ListenPort = 51822

# IP forwarding
PreUp = sysctl -w net.ipv4.ip_forward=1
# IP masquerading
PreUp = iptables -t mangle -A PREROUTING -i wg0 -j MARK --set-mark 0x30
PreUp = iptables -t nat -A POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE
PostDown = iptables -t mangle -D PREROUTING -i wg0 -j MARK --set-mark 0x30
PostDown = iptables -t nat -D POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE

# remote settings for Host C
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
Endpoint = 192.0.2.3:51823
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25

Like Host β in our first Site Gateway as a Spoke scenario, we’ve also included an Endpoint and PersistentKeepalive = 25 setting in Host β’s configuration for Host C, to ensure that Host β pokes a hole through the NAT firewall between Host C and it; and we’ve included some iptables rules in Host β’s WireGuard configuration to masquerade traffic from the WireGuard network. If Host β uses nftables instead of iptables, configure nftables on Host β according to the “Point to Site” section of the How to Use WireGuard With Nftables guide instead of using these iptables rules (or if Site B is configured to route traffic to the WireGuard network back through Host β, you don’t need any masquerading rules).

Test It Out

In this scenario, we have a webserver running on Endpoint B (TCP port 80 of 192.168.200.22), and another one on Endpoint C (TCP port 80 of 192.168.33.13). So after starting up our configured WireGuard interfaces on each host (Endpoint A, Host C, and Host β), we can run cURL (or a regular webrowser) from Endpoint A to access the webserver on Endpoint B at 192.168.200.22:

$ curl 192.168.200.22
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
...

And we can run cURL (or a regular webrowser) to access the webserver on Endpoint C at 192.168.33.13:

$ curl 192.168.33.13
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
...

Hub is a Site Gateway With an Internet Gateway Spoke

One more bonus scenario: say we have a hub (again Host C) that is also a gateway, like the previous scenario (Hub is Also a Site Gateway), plus a spoke (again Host β) that is an Internet gateway (like the Internet Gateway as a Spoke scenario). How do we handle that?

Hub That is Also a Site Gateway With an Internet Gateway Spoke

We’d actually configure Endpoint A and Host β exactly the same as the Internet Gateway as a Spoke scenario: setting AllowedIPs = 0.0.0.0/0, ::/0 in Endpoint A’s configuration for Host C; and AllowedIPs = 10.0.0.0/24 in Host β’s configuration for Host C.

We’d also configure Host C mostly the same as the Internet Gateway as a Spoke scenario — just with a few extra sets of routes (plus with firewall rules for masquerading like the Hub is Also a Site Gateway scenario).

Host C

Just like the basic Internet Gateway as a Spoke scenario, on Host C in this scenario we want to send all the traffic destined for 10.0.0.1 or fd10:0:0:1::/64 to Endpoint A; and send (almost) all other traffic that comes through our WireGuard network to Host β. So we’d configure WireGuard on Host C with AllowedIPs = 10.0.0.1/32, fd10:0:0:1::/64 for its Endpoint A [Peer] section, and AllowedIPs = 0.0.0.0/0, ::/0 for its Host β [Peer] section.

But since we don’t want all of Host C’s traffic to go to Host β — just (almost) all traffic forwarded through the WireGuard network — we’d configure the routes for this WireGuard interface to use a custom routing table, via the interface’s Table = 123 setting — just like the Internet Gateway as a Spoke scenario. And just like that same scenario, we’d use a PreUp command to add a policy routing rule that directs the host to use this table only for traffic coming in from this WireGuard interface.

But to route traffic destined for Site C to Site C instead of to Host β, we’d also need to add another policy routing rule to carve out an exception for Site C traffic (192.168.33.0/24); plus add a custom route for traffic returning back from Site C to the WireGuard network:

# /etc/wireguard/wg0.conf

# local settings for Host C
[Interface]
PrivateKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCGA=
Address = 10.0.0.3/32, fd10:0:0:3::1/64
ListenPort = 51823
Table = 123

# IP forwarding
PreUp = sysctl -w net.ipv4.ip_forward=1

# default routing for incoming WireGuard packets
PreUp = ip rule add iif wg0 table 123 priority 456
PostDown = ip rule del iif wg0 table 123 priority 456

# routing for packets sent from WireGuard network to Site C
PreUp = ip rule add to 192.168.33.0/24 table main priority 444
PostDown = ip rule del to 192.168.33.0/24 table main priority 444

# routing for packets returning from Site C back to WireGuard network
PostUp = ip route add 10.0.0.0/24 dev wg0

# IP masquerading
PreUp = iptables -t mangle -A PREROUTING -i wg0 -j MARK --set-mark 0x30
PreUp = iptables -t nat -A POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE
PostDown = iptables -t mangle -D PREROUTING -i wg0 -j MARK --set-mark 0x30
PostDown = iptables -t nat -D POSTROUTING ! -o wg0 -m mark --mark 0x30 -j MASQUERADE

# IPv6 forwarding & routing
PreUp = sysctl -w net.ipv6.conf.all.forwarding=1
PreUp = ip -6 rule add iif wg0 table 123 priority 456
PostDown = ip -6 rule del iif wg0 table 123 priority 456

# remote settings for Endpoint A
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIPs = 10.0.0.1/32, fd10:0:0:1::/64

# remote settings for Host β
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
AllowedIPs = 0.0.0.0/0, ::/0

The first policy routing rule in this configuration is identical to the one we used in the basic Internet Gateway as a Spoke scenario:

# default routing for incoming WireGuard packets
PreUp = ip rule add iif wg0 table 123 priority 456

It sends all packets that come in on this WireGuard interface to our custom 123 route table; and the routes in that table (added automatically because of the Table = 123 setting in our WireGuard config) will send those packets back out the same WireGuard interface.

But we don`t want this to happen for packets sent from Endpoint A to Site C. That’s where the second policy routing rule comes in:

# routing for packets sent from WireGuard network to Site C
PreUp = ip rule add to 192.168.33.0/24 table main priority 444

It has a lower priority number (444) that the first rule (456), meaning that it is evaluated before the first rule. It sends all packets destined for Site C (192.168.33.0/24) to the main routing table instead of our custom 123 table. Host C should have a route in its main routing table for Site C, allowing those packets to be routed correctly to Site C (probably routed out some physical Ethernet interface, like eth0).

Finally, for packets returning from Site C back to the WireGuard network, we need to manually add a route to the main routing table that directs them to go out the WireGuard interface:

# routing for packets returning from Site C back to WireGuard network
PostUp = ip route add 10.0.0.0/24 dev wg0

Unlike packets from Endpoint A or Host β, these packets won’t come in to Host C via the WireGuard interface — so the first policy routing rule above won’t apply to them. We could create another policy routing rule just for them; but it’s simplest just to add another route to the main routing table. (And note that we use a PostUp command instead PreUp for the route, since the WireGuard interface needs to be up first before we can add a route for it; and it needs no corresponding PreDown command since the route will be deleted automatically when the interface is shut down.)

Tip

If the host’s WireGuard IP address falls within a single continuous block of addresses that you want to route to the WireGuard network, like above, you can skip adding a route for that block manually, and instead adjust the netmask on the WireGuard interface’s own address to include that block of addresses.

For example, when Host C’s WireGuard address is 10.0.0.3, you can skip manually adding the following route for Host C:

ip route add 10.0.0.0/24 dev wg0

Instead, just specify a /24 netmask (instead of a /32 netmask) when you configure Host C’s WireGuard address:

Address = 10.0.0.3/24

Test It Out

After starting up our configured WireGuard interfaces on each host (Endpoint A, Host C, and Host β), we can run cURL (or a regular webrowser) from Endpoint A to access Google (or any other Internet site) through our WireGuard network:

$ curl google.com
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
...

And we can run cURL (or a regular webrowser) to access the webserver on Endpoint C at 192.168.33.13:

$ curl 192.168.33.13
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
...