Using a network namespace as NAT router for a VPS
As I had hinted at in another article, I am currently building a distributed kubernetes cluster from a variety of machines.
One of that machines is a virtual private server that (of course) has a public IP configured.
Having a node which has a public IP hinders you in using NodePort
easily; you would need to add a DNAT entry for incoming traffic.
But if you want to do any NAT or firewalling, you need cooperation of the kubernetes network plugin you are using (calico in my case). Firewalling is easy, but NAT is not.
Please note, that outgoing traffic from a pod needs to be NATed, as my provider will not handle traffic coming from an internal IP on the public interface.
I had experimented with natOutgoing
, but that messed up traffic from pods running on the node to pods running on other nodes, as they would suddenly come from a public IP…
The solution? Move the network interface of the machine into its own network namespace, add a virtual ethernet pair between the root namespace and the new namespace and configure the network-stack in that namespace to do routing and NAT.
I have wrapped all necessary steps into an ansible role, but I will go through the steps here.
Step 0: Experience linux namespaces
The namespace support of the linux kernel is what allows such technologies as “containers”. There are a number of namespaced resources in the linux kernel (network, process ids, mountpoints and many others). See this article for an in-depth walkthrough.
If you enter into a network namespace there is absolutely no network connectivity of a sudden:
$ sudo ip netns add foo
$ sudo ip netns exec foo bash
# ip addr show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
In this namespace everything network is totally separate from the rest of the system: Interfaces, routes, iptables rules, even the sysctl
parameters controlling IP-forwarding. Basically, it feels like a totally different computer.
Let’s use that “new computer” as a router for our VPS:
Step 1: Create the namespace, move the interface, configure the interface
I use debian on my servers, so I configured the networking in the file /etc/network/interfaces
.
auto ens3
iface ens3 inet manual
First of all, I set the interface to be configured manual
ly.
That means that we have to provide commands to be called for each step of setting up the interface.
Before the interface is configured, the namespace is created and the interface is moved into it.
pre-up ip netns add ens3 && ip link set ens3 netns ens3
down ip netns del ens3
To de-configure the interface, all we need to do is remove the namespace and the interface will pop back into the root namespace as an unconfigured interface.
up ip netns exec ens3 ip addr replace 1.2.3.4/24 dev ens3 && ip netns exec ens3 ip -6 addr replace 2123:2134:abc:ddd::3/64 dev ens3 && ip netns exec ens3 ip link set up dev ens3
Now the IP-addresses is configured.
For IPv6, I choose the IP ending in :3
, where the server had one ending in :2
before.
Next, we configure the routes into the new and old internet.
post-up ip netns exec ens3 ip route replace default via 1.2.3.1
post-up ip netns exec ens3 ip -6 route replace default via 2132:2134:abc:ddd::1
And configure routing in the namespace:
post-up ip netns exec ens3 bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'
post-up ip netns exec ens3 bash -c 'echo 1 > /proc/sys/net/ipv6/conf/all/forwarding'
post-up ip netns exec ens3 iptables -t nat -A POSTROUTING -o ens3 -j MASQUERADE
As the server still needs to answer to the IPv6 address ending in :2
, we ask the namespace to do that:
post-up ip netns exec ens3 bash -c 'echo 1 > /proc/sys/net/ipv6/conf/all/proxy_ndp'
post-up ip netns exec ens3 ip -6 neigh add proxy 2132:2134:abc:ddd::2 dev ens3
If we now add the virtual device (see below), this is all we need to have internet access in the root namespace. I added two more lines:
post-up ip netns exec ens3 iptables -t nat -A PREROUTING -d 1.2.3.4 -p tcp --dport 22 -j DNAT --to-destination 192.168.158.2
post-up ip netns exec ens3 iptables -t nat -A POSTROUTING -o vens3 -s 192.168.158.2 -j MASQUERADE
The first one does a port forwarding for SSH into the root namespace (which has IP 192.168.158.2
, see below).
The second one ensures that when the host itself (from the root namespace) tries to contact its own public ip (1.2.3.4
), the traffic will be sent back through the virtual interface as if it originated from the non-root namespace.
Otherwise the root namespace would send traffic to 1.2.3.4
and receive it back with its own IP address as source.
Step 2: Create virtual interface between the namespaces
auto vens3
iface vens3 inet static
address 192.168.158.2
netmask 255.255.255.0
This time we can actually leverage the operating system to configure our IP addresses!
But first, the interface needs to be created:
pre-up ip link add vens3 type veth peer vens3 netns ens3
And the other half needs to be configured (note that we are also adding a static IPv6 address, that will become important when configuring routing for IPv6):
pre-up ip netns exec ens3 ip addr add 192.168.158.1/24 dev vens3 && ip netns exec ens3 ip -6 addr add fe80::1 dev vens3 && ip netns exec ens3 ip link set up dev vens3
Now we can configure the default route through the namespace:
post-up ip route replace default via 192.168.158.1
And tell the operating system to delete the interface if is it deconfigured:
post-down ip link del vens3
Repeat for IPv6:
iface vens3 inet6 static
address 2132:2134:abc:ddd::2/64
post-up ip -6 addr add fe80::2 dev vens3
We add a static link local IPv6 here as well, as that will be the address the namespace will use to route our public IP:
post-up ip -6 route replace default via fe80::1 dev vens3
post-up ip netns exec ens3 ip -6 route replace 2134:2134:abc:ddd::2/128 via fe80::2 dev vens3
We add a default router via the static link local IPv6 of the namespace and we tell the namespace how to reach us.
Step 3: Add port-forwardings
The ansible role also support adding port forward rules, they are implemented by adding DNAT
iptables rules to the namespace, just as we saw for SSH.
Have fun!