Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2-3: Accessing Networks

Our Lighthouse server allowed Peers to connect across network boundaries, so long as all Peers had line-of-sight to the Lighthouse. We accomplished this with IP forwarding on the Lighthouse, so that traffic intended for Peers connected to the Lighthouse could reach their destination.

But IP forwarding can be used for more than just Wireguard traffic. This same technique can allow a Wireguard Peer to provide access to an entire subnet.

It works like this:

flowchart
  subgraph internet [Internet]
  C[Peer C] <--> L[Lighthouse]
  end
  subgraph home1 [Home Network]
  A[Peer A] <--> L
  S[Webserver] <--> A
  end
  subgraph roaming [Roaming Network]
  B[Peer B] <--> L
  end

  style internet fill:blue
  style home1 fill:purple
  style roaming fill:maroon

Here we have the Lighthouse, same as before, as well as a home Peer. In the home network, we also have a web server that we’d like to access remotely. If the home Peer is IP forwarding just like the Lighthouse, and we configure the AllowedIPs for all Peers correctly, all nodes will be able to access IPs in that home subnet from anywhere, while connected to Wireguard. Tailscale calls this kind of Peer a “subnet router,” and so will we. But we don’t need Tailscale to pull this off.

The Magic of AllowedIPs

Recall that AllowedIPs determines both what traffic is allowed in from a Wireguard session, and what traffic is sent out over that session. It’s the latter we take advantage of here to provide subnet access.

The home “subnet router” doesn’t need to change anything in its AllowedIPs configuration, surprisingly. If we’re following a Lighthouse model, even the subnet router will have only a single Peer entry: the Lighthouse. And because we set that AllowedIPs value to the entire Wireguard subnet (so the Peer can access all other Wireguard Peers), we have also stated that traffic from the whole subnet may come over this connection.

The other Peers, on the other hand, need to add each subnet they want handled by the subnet router to their AllowedIPs entry. Subnets are separated by commas. So let’s imagine our home network is the 192.168.1.0/24 subnet, and the Wireguard network itself is 172.16.100.0/24. In order to send traffic for the home network over the Wireguard session, a Peer’s AllowedIPs entry would need to look like:

AllowedIPs = 172.16.100.0/24, 192.168.1.0/24

When the Wireguard session is up, packets destined for either subnet will be sent over the session. In the case of roaming Peers, that means the Lighthouse receives the packets first.

The Lighthouse’s AllowedIPs entry also needs to change. In order to send traffic from other Peers intended for the home network to the subnet router (and to allow replies from those IP addresses), the home subnet needs to be added to the list. So the Lighthouse AllowedIPs for the subnet router looks like:

AllowedIPs = 172.16.100.2/32, 192.168.1.0/24

Note the subtle difference between the Peer and Lighthouse. We’re only allowing a single Wireguard IP address (/32) to/from the subnet router, whereas other Peers allow/route the entire Wireguard subnet (/24). But in both cases, the entire home subnet is allowed.

Warning

This setting could very easily break other local network connections if they use the same subnet as home. If, say, a hotel LAN uses the same IP space as your home network, this configuration will break your hotel LAN connection.

But once the Wireguard traffic from other Peers has reached the subnet router, we have another problem: how to forward the traffic on in such a way that the traffic comes back to the router, and then back to the original source.

For that, we need firewall rules.

PostUp and NFTables

The Lighthouse’s “routing” responsibilities are limited to the Wireguard network, but the subnet router has a harder job to do.

Let’s imagine there’s a web server on my home network—maybe a Jellyfin server or similar service—that I want to access remotely. I know the IP of the server on my home network, so I make a request to that server from a roaming device, connected to the Wireguard network via the Lighthouse. The Lighthouse, configured via its AllowedIPs entry for the subnet router, sends the traffic along to that Peer.

Now the subnet router has the packet, which has a destination intended for the home server. If IP forwarding is enabled on the subnet router, then the packet can be sent along to that server. However, once the server receives it, it will not have any way to reply—the source IP is in the Wireguard subnet, which the home server has no route for.

To solve this problem, the subnet router needs to perform a type of network address translation known as “masquerading.” The router needs to present the packet with itself as the source. Then, upon receiving the reply from the internal server, send the response back to the true source.

Luckily, the ability to do this is built-in to most Linux distributions, via the older iptables or the newer nftables. We’ll be using the latter.

Nftables firewall rules are configured with the nft command line tool.

Here’s the full set of commands we need to configure a masquerading, NAT router in our home subnet.

nft add table ip filter
nft add table ip nat
nft add chain ip filter forward '{type filter hook forward priority filter; policy accept;}'
nft add rule ip filter forward iifname %i counter accept
nft add chain ip nat postrouting '{type nat hook postrouting priority srcnat; policy accept;}'
nft add rule ip nat postrouting oifname <<HOME_INTERFACE>> counter masquerade

That is…quite a lot, I know. Let’s go line by line.

nft add table ip filter: Firewall rules are stored in tables. Tables have multiple types. Here we make a new rules table for IPv4 addresses called filter.

nft add table ip nat: And we make another called nat. Next up, we build rule chains.

nft add chain ip filter forward '{type filter hook forward priority filter; policy accept;}': This monster creates a new chain—a ruleset—called forward to the filter table. The definition inside the curly braces describes the type of rules. These are filter rules (naturally), to be applied on the forward hook. This matches packets that are intended for other hosts. The priority is the standard filter, and and packets not matched by any rules in the chain will be accepted.

nft add rule ip filter forward iifname %i counter accept: Here we add a rule to the chain we just made. This rule applies to any packet coming in on interface %i, which is a variable used by Wireguard to refer to the specific Wireguard interface created by the config. counter means that hits on this rule will be counted, and the packets will be accepted.

nft add chain ip nat postrouting '{ type nat hook postrouting priority srcnat; policy accept; }': Another chain, this one for NAT called postrouting. This is how we’ll send the packets on.

nft add rule ip nat postrouting oifname <<HOME_INTERFACE>> counter masquerade: Just as before, we’re adding a new rule. Before we were catching packets inbound on the Wireguard interface. Now we’re catching packets on the way out on the home network interface, counting them and masquerading the packets. In this way, the packet will be able to find its way back to the router. We’ll need to fill in the <<HOME_INTERFACE>> with a real one, once we know its name.

Put it all together, and we have a way to send packets into the subnet and get them back.

So where do all these commands live? If we want them to be linked to the Wireguard tunnel itself—and we do—we can take advantage of the PostUp directives in the [Interface] config.

The PreUp, PostUp, PreDown, and PostDown directives allow us to run arbitrary commands before and after bringing up and down the Wireguard interface, respectively. We can use the directive as many times as we want for multiple commands. In this way, we can contain all the above nft commands in the Wireguard config and have Wireguard itself set up the firewall rules.

Oh, and remember that %i up there for the interface name? Wireguard will fill in its own when presented with that variable.

We can use PostDown to clean up when we’re done with Wireguard, to keep our firewall rules clean.

For that, we’ll use only two commands:

nft delete chain ip filter forward
nft delete chain ip nat postrouting

So if we put that all together, it looks like this:

PostUp = nft add table ip filter
PostUp = nft add table ip nat
PostUp = nft add chain ip filter forward '{type filter hook forward priority filter; policy accept;}'
PostUp = nft add rule ip filter forward iifname %i counter accept
PostUp = nft add chain ip nat postrouting '{type nat hook postrouting priority srcnat; policy accept;}'
PostUp = nft add rule ip nat postrouting oifname <<HOME_INTERFACE>> counter masquerade
PostDown = nft delete chain ip filter forward
PostDown = nft delete chain ip nat postrouting

That lives up top with the rest of our interface configuration. Is it a bit clunky? It is, but it’s the cleanest way to integrate nftables with Wireguard.

Note

nft can be used as a scripting language in dedicated files. The reason we’re not putting all this into separate files is twofold. First, we’d still need to delete the rules when bringing down the interface; and second, then we’d have to sed two separate files to fill on variables rather than one. I’d prefer fewer files to wrangle.

I know this is a lot, but luckily it’s one of those things that, once you have it set up, you can mostly ignore it. And if you ever need to make another subnet router, the steps will nearly identical. But it’s practice that will cement your memory, so let’s bring up our subnet router lab!

Check For Understanding

  • Explain the difference in how the Lighthouse, home router, and other Peers use AllowedIPs.
  • Take each PostUp nft command in turn. Explain what each one does and why it matters for our subnet router.