Ever wanted to run multiple VPNs at the same time from the same device? Route a client down a different VPN at a boundary linux router?

Introduction

Linux has a wealth of networking capabilities, to the degree that it can operate as quite a powerful router, just without the hardware acceleration you get with a commercial enterprise offering (and the enterprise price tag).

In addition to VRFs (think of it as a separate routing table providing layer3 segregation), linux has the concept of network namespaces (netns). A netns is a separate copy not only of a routing table, but the whole network stack, including firewall rules and network devices. It can handle performing filesystem mounts to present namespace specific networking files such as resolv.conf to applications that use it but are not netns aware.

What we are going to do is set up two VPNs using provider openvpn profiles. These will be segregated using network namespaces, and selectively accessible via our iptables/nft and policy routing directives.

We will then use policy routing for both local host situations, and source devices (where the linux box is a router). This allows us to dictate what uses which VPN and when.

Setup

We will document the steps to manually set up each part. Hooking into your system startup scripts is likely unique for your environment and left as an exercise for the reader, as is the variation for IPv6.

In this diagram we link the main network namespace with the segregated ones via a p2p bridging link. Here you need to choose small IP address ranges that don’t clash with others you or the VPN provider use. We chose /30 ranges out of 192.168.0.0/24 as that is a common commodity router range we don’t use on our net.

If our device isn’t authoritative and own this range then, as we do in this article, we need to deploy NAT to mask this issue.

NamespaceOutside NIC (NS)Inside NIC (main)
piaNL0192.168.0.1/30192.168.0.2/30
piaCA0192.168.0.5/30192.168.0.6/30

We will go through setting up the configuration for netns piaCA0. You can then repeat the steps for all the other VPN’s you wish to add.

First, we need to decide on factors such as which DNS nameservers we are going to use to perform lookups from within the netns. Once decided we can create a file /etc/netns/NAME/resolv.conf. So, in this case, we create one called /etc/netns/piaCA0/resolv.conf.

We want to create the netns and the bridging link. The “ip link” command allows us to specify the namespace each side resides in. As we are linking into the default namespace, the vpnCA0in side does not specify a namespace. Note also that we can supply the “-n” parameter to specify a namespace to operate in.

Next, we assign addresses to the endpoints.

To see the interfaces in the netns, such as piaCA0out, you need to run the “ip” commands in the context of the netns. For example:

We can verify connectivity via ping. Here we use the “ip netns” command to run arbitrary commands in the context of the netns.

As the netns is a complete network stack, it currently does not have a routing table configured. We need to set the default route back to our main namespace.

We now need to set up the iptables/nft rules to firewall things off, but also handle things such as NAT translation. This is critical as it allows each VPN process to use whatever private IP ranges it wants (except our p2p link range), even the same ones as the other VPN’s.

We will assume you have a basic firewall setup and are using nft rather than iptables, though the translation between the two is straightforward.

This is also where we quickly get into environment specific quirks. In my environment pretty much everything is denied access to the Internet. You have to use internal DNS servers, proxy servers, and so on. There are exceptions, but we don’t want to add to that list unless needed.

When the VPN is up, anything running in that context should use the specified DNS servers. However, to bootstrap things, the openvpn client needs to resolve the address of the VPN server endpoint. To do that we use destination NAT to redirect packets to the internal DNS servers.

We then need a rule to masquerade (aka NAT based on our servers interface) anything we send to the VPN namespace. This is important for when we are acting as a router.

The opposite direction is also true. Traffic originating from inside the VPN namespace needs to be NAT’ed, for example VPN packets heading to the our internal DNS servers on int0. You may be able to bind this to specific interfaces as, for example, routes out to the Internet will likely already be NAT’ed if ppp0 is your Internet link.

Next we need to define the firewall rules. The rigorousness of this (how far you lock it down) is a risk-based decision for your environment.

In our setup we create a new chain for traffic outbound from the netns. We limit it to DNS access to the specified DNS servers (we may need to use different ones if we are performing prerouting translations). It can also access the VPN endpoints using the protocol and port specified in the openvpn configuration (look for the “remote” and “proto” directives). As VPN DNS name may be in-flux/rotated we don’t limit destinations IP addresses.

In our setup we create a new chain to allow access from external devices that we have approved.

In both cases we may change “drop” to a new chain that performs logging as well. We then link these rules to our main “vpn” chain, which we already created and exists within the base FORWARD chain (this is hooked into the networking stack).

Next, as the netns is a completely new networking stack, it doesn’t have any firewall setup at all. So we need to create the base chain and its hook. Also, as this is more straightforward, we add the details straight into the base chain. tun0 here is the name of the tunnel interface the openvpn process creates. If it is different for your setup, we need to change it.

If you are not already acting as a router you need to activate forwarding. Again, you may choose specific interfaces via net.ipv4.conf.INTERFACE.forwarding.

Finally, we can set up the routing policy.

Policy Routing

As we are using policy routing, we need to create a separate routing table within the main host (separate from the netns). These are either well-known or numbered (the normal one you see is known as ‘main’ and has an ID of 254).

To create a name we can create a file specific for our netns (the name just has to end with .conf). In our case we assign table 11 to this VPN.

We can then create the policy. First, the obligatory default route points to the VPN netns via the p2p link.

If we are not changing the DNS nameserver remote devices are using, we need to add an exemption for that.

We can do other things like add local users into specific netns. Here we have UID 1002. Anything this user does will be over the netns (but not using the netns specific resolv.conf). For example, separate BurpSuite instances.

To get the netns specific resolver we need to run in the namespace context itself. It depends whether you wish to tunnel the main traffic but monitor DNS or tunnel everything. You can adapt to all these things by tweaking the setup we have just described.

Starting the VPN

We now run the openvpn client with the appropriate openvpn config and other parameters for our VPN.

The log file shows the VPN is up. We can also check the routing table. Note that if we were using 10.2.110.0/24 internally we would have had issues with a normal setup. This way represents no issue as it is isolated within the netns.

A quick test of our policy routing for users (Netherlands, then Canada). As we are IPv6 aware but haven’t set this up for IPv6 yet, we force use over IPv4. In this case, if we don’t do anything for IPv6 it will use the normal routing for IPv6; which may be not what you want.

As mentioned earlier, if we want full isolation for the process, it will need to run within the context of the netns.

For our remote boxes, we can see the effect of the policy routing.

In the case of remote devices, it depends if it is setup to access the network over IPv6. If it does, then you need to handle that via the ip6 family nft rules.

Finally

We repeat the whole thing for each VPN we wish to run. Once set up we can quickly tweak what accesses the VPN by changing the policy routing rules and, if locked down, the filter rules.

If you are looking at scripting this for your environment, the nft “include” directive supports wildcards just like the shell, so if you have a naming format the VPN script can generate the relevant configuration files that will then get included during next load.

Leave a Comment

Your email address will not be published. Required fields are marked *