Adventures in Rolling Your Own Router: Part III

In my previous post, I described my plan for the new router. In this post, I’ll create a virtual router, LAN, and client in order to test out my plan for the Debian router.

This is easy to accomplish using Cockpit on RHEL, but you can do this on any Linux distribution.

Debian Installation

I downloaded the net installer for Debian 10.7 “buster” and created the first VM called debrouter with two network interfaces. The first interface is bridged with my RHEL server’s Ethernet interface and will get its own IP address via DHCP on my real LAN in the 192.168.10.0/24 block. The second network interface is connected to the virtual LAN privatelan. These will be the WAN and LAN ports respectively.

At the software selection page, I have disabled all but the SSH server. The fewer packages installed, the better!

After the installation is complete, we can ssh into the new VM.

Set Up sudo

Since we didn’t install anything, we unsurprisingly don’t have sudo.

jmp@debrouter:/etc/network$ su -
Password:
root@debrouter:~# apt install sudo
root@debrouter:~# usermod -aG sudo jmp

Now after logging out and logging back in again, jmp can use sudo.

Network Interface Configuration

We will first set up some configuration for systemd-networkd, then we will disable the default networking service.

Rename Network Interfaces

I like to give logical persistent names for each network interface. We can do this using systemd.link.

First, we need to get the MAC addresses of each interface:

jmp@debrouter:~$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 00:0c:29:5e:8e:fa brd ff:ff:ff:ff:ff:ff
3: ens34: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 00:0c:29:5e:8e:04 brd ff:ff:ff:ff:ff:ff

Now, we can create a .link file for each interface with our desired names:

jmp@debrouter:~$ cat /etc/systemd/network/10-extern0.link /etc/systemd/network/20-intern0.link
[Match]
MACAddress=00:0c:29:5e:8e:fa

[Link]
Name=extern0
[Match]
MACAddress=00:0c:29:5e:8e:04

[Link]
Name=intern0

Configure Network Interfaces

Finally we can define our .network files:

jmp@debrouter:~$ cat /etc/systemd/network/external.network
[Match]
Name=extern0

[Network]
DHCP=ipv4

jmp@debrouter:~$ cat /etc/systemd/network/internal.network
[Match]
Name=intern0

[Network]
Address=192.168.77.1/24
IPMasquerade=true

Disable networking and Enable systemd-networkd

By default, Debian uses the /etc/network/interfaces configuration file and the systemd unit networking.service to configure the network interfaces on boot.

Since we want systemd-networkd to control this instead, we need to rename the configuration file and disable the service.

Note: the Debian docs don’t suggest disabling the service, but I haven’t had any issues doing so.

jmp@debrouter:~$ sudo mv /etc/network/interfaces /etc/network/interfaces.save
jmp@debrouter:~$ sudo systemctl disable networking

Be careful not to stop the service yet, or else our interface will go down and we will lose the SSH session.

Now, we can enable systemd-networkd.service.

jmp@debrouter:~$ sudo systemctl enable systemd-networkd

Finishing Up

Now we can reboot and see our network interfaces are configured correctly! Our extern0 interface got a dynamic address 192.168.10.18 (on my real LAN) and the intern0 interface has the static address 192.168.77.1 as requested.

jmp@debrouter:~$ ip ad
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: extern0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 52:54:00:c7:ca:a6 brd ff:ff:ff:ff:ff:ff
    inet 192.168.10.18/24 brd 192.168.10.255 scope global dynamic extern0
       valid_lft 3567sec preferred_lft 3567sec
    inet6 fe80::5054:ff:fec7:caa6/64 scope link
       valid_lft forever preferred_lft forever
3: intern0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 52:54:00:e3:bb:78 brd ff:ff:ff:ff:ff:ff
    inet 192.168.77.1/24 brd 192.168.77.255 scope global intern0
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fee3:bb78/64 scope link
       valid_lft forever preferred_lft forever

Where is nftables?

At this point, because we added IPMasquerade=true in our LAN network file, we should have some nftables rules automatically added. However, to my suprise, nftables is not installed by default in Debian 10. Furthermore, some iptables-legacy rules get created instead!

jmp@debrouter:~$ su -
Password:
root@debrouter:~# nft
-bash: nft: command not found
root@debrouter:~# iptables-legacy-save
# Generated by iptables-save v1.8.2 on Mon Jan 18 00:18:25 2021
*nat
:PREROUTING ACCEPT [61:10854]
:INPUT ACCEPT [57:9808]
:OUTPUT ACCEPT [6:447]
:POSTROUTING ACCEPT [6:447]
-A POSTROUTING -s 192.168.77.0/24 -j MASQUERADE
COMMIT
# Completed on Mon Jan 18 00:18:25 2021

Looks like the version of systemd which ships with Debian buster is out of date. Let’s install the latest systemd from buster-backports

jmp@debrouter:~$ sudo sh -c 'echo "deb http://deb.debian.org/debian buster-backports main" >>/etc/apt/sources.list'
jmp@debrouter:~$ sudo apt update
jmp@debrouter:~$ sudo apt -t buster-backports install systemd

Reboot, aaaand… nope. No dice. Looking in the commit history, it seems like the nftables backend hasn’t even been released yet.

For the time being, let’s continue with the legacy rules, but we will need to replace these later on when we install nftables.

Testing With A Client

At this point, a client should be able to connect to the internal network with a static IP and connect to the Internet through our virtual router.

I’ll now set up another Debian VM called debclient, this time with only one interface connected to the privatelan network.

During the installation process, automatic network configuration fails (expectedly, because there is no DHCP server) so we can manually set up a static IP of 192.168.77.2, netmask 255.255.255.0, and gateway 192.168.77.1. Since we don’t have a nameserver set up yet, I’m using Google’s DNS 8.8.8.8 since it’s easy to remember.

After the installation is complete, I set up tcpdump to listen for ICMP packets on both of the router’s interfaces and then pinged Google’s DNS server from the client.

jmp@debrouter:~$ sudo tcpdump -i any icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
00:40:17.258834 IP 192.168.77.2 > dns.google: ICMP echo request, id 434, seq 1, length 64
00:40:17.258872 IP debrouter.crbj.io > dns.google: ICMP echo request, id 434, seq 1, length 64
00:40:17.276588 IP dns.google > debrouter.crbj.io: ICMP echo reply, id 434, seq 1, length 64
00:40:17.276607 IP dns.google > 192.168.77.2: ICMP echo reply, id 434, seq 1, length 64

Looks like it’s routing packets!

In the next post, I’ll investigate what’s going on with nftables and remove these iptables-legacy rules.