I want to run my own network gear. I suppose that’s just because my day job is writing network software for Linux, and I feel like I should be able to do it. I’m not great at hardware, but I know a decent bit about using Linux networking, for moving packets around. And it shouldn’t be much harder than iproute2 and nftables, right?

I don’t care too much about speed; I remember dial-up and live in a household of two. Even hitting 100 Mbps would be plenty. So I went cheap and bought a [NanoPi R2S Plus]1, a tiny, Linux-capable SBC with two RJ-45 ports.

Flashing

I really wanted to flash the on-board eMMC module via USB, but I just couldn’t figure out the right invocation of upgrade_tool2 or rkdeveloptool3 that got the board to boot. And I sunk many hours into studying their documentation and trying things. It was just too opaque and outside my expertise. Fortunately, I have a microSD card laying around, so I used FriendlyElec’s eflasher image instead.

$ gunzip rk3328-eflasher-debian-bookworm-core-6.1-arm64-20231213.img.gz
$ sudo dd if=rk3328-eflasher-debian-bookworm-core-6.1-arm64-20231213.img of=/dev/mmcblk0 bs=4M status=progress

Put the card in the board, plug in the power, and connect to the serial console from my desktop:

$ picocom -b 150_0000 /dev/ttyUSB0
...

Ubuntu 22.04 LTS NanoPi-R2S-Plus ttyS2

Default Login:
Username = pi
Password = pi

NanoPi-R2S-Plus login: pi
Password: 

pi@NanoPi-R2S-Plus:~$ head -n1 /etc/os-release 
PRETTY_NAME="Ubuntu 22.04.3 LTS"
pi@NanoPi-R2S-Plus:~$ sudo eflasher -i /mnt/sdcard/debian-bookworm-core-arm64/
Using config file:  "/tmp/eflasher.conf" 
------------------------------------------------------- 
>>Debian 12 Core
Ready to Go with Debian,Total size: 1.3 GB,
Installing Debian ..., , ,                 
Installing Debian, ,Formatting,Done        
Installing Debian, ,Formatting,Done        
Finish!,Speed: 48 MB/s                                                     
RunCmd:  "/bin/sh" "-c /usr/bin/sd_monitor" pid:  724 
^C
pi@NanoPi-R2S-Plus:~$ poweroff

Remove the SD card and hit the reset button. This time, the serial console shows

Debian GNU/Linux 12 NanoPi-R2S-Plus ttyS2

NanoPi-R2S-Plus login: pi
Password: 
Linux NanoPi-R2S-Plus 6.1.63 #218 SMP Thu Nov 30 20:48:04 CST 2023 aarch64

pi@NanoPi-R2S-Plus:~$ head -n1 /etc/os-release 
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"

Perfect.

Configuration

Now on to the part I’m more comfortable with. First, I want the network interfaces to have sane names so they’re easier to refer to as I admin the system.

$ cat <<EOF > /etc/systemd/network/25-lan.link
[Match]
MACAddress=c6:f7:85:65:33:77
Type=ether

[Link]
Name=lan

$ cat <<EOF > /etc/systemd/network/25-wan.link
[Match]
MACAddress=c6:f7:ba:a5:59:5b
Type=ether

[Link]
Name=wan

$ cat <<EOF /etc/systemd/network/75-lan.network
[Match]
Name=lan

[Network]
Address=192.168.0.1/24

$ cat <<EOF /etc/systemd/network/75-wan.network
[Match]
Name=wan

[Network]
DHCP=ipv4

[DHCPv4]
UseDNS=false

Now onto the pieces that make this system a router.

$ cat <<EOF > /etc/sysctl.d/local.conf
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
EOF
$ apt install nftables dnsmasq
$ cat <<EOF > /etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

table inet router {
    flowtable f {
        hook ingress priority 0
        devices = { lan, wan }
        counter
    }

    chain input {
        type filter hook input priority filter; policy drop;
        iifname "lan" accept
        iifname "wan" ct state vmap { established : accept, related : accept, invalid : drop }
        counter accept
    }

    chain output {
        type filter hook output priority filter; policy drop;
        oifname "lan" accept
        udp dport 53 counter accept comment "dns"
        udp sport 123 udp dport 123 counter accept comment "ntp"
        ip daddr 8.45.176.225-8.45.176.232 tcp dport { 80, 443 } counter accept comment "mirrors.aliyun.com"
        counter accept
    }

    chain prerouting {
        type filter hook prerouting priority filter; policy drop;
        iifname "lan" accept
        iifname "wan" ct state established,related accept
    }

    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        oifname "wan" masquerade
    }
}
EOF
$ cat <<EOF > /etc/dnsmasq.d/lan.conf
# Run DNS server on LAN IP
listen-address=127.0.0.1,192.168.0.1
cache-size=1000
no-resolv
domain-needed
server=1.1.1.1

local=/lan/
domain=lan
expand-hosts

# Only listen to routers' LAN NIC.  Doing so opens up tcp/udp port 53 to localhost and udp port 67 to world:
interface=wan
bind-interfaces

dhcp-lease-max=100

# Set default gateway
dhcp-option=3,192.168.0.1

# Set DNS servers to announce
dhcp-option=6,192.168.0.1

# If your dnsmasq server is also doing the routing for your network, you can use option 121 to push a static route out.
dhcp-option=121,0.0.0.0/24,192.168.0.1

# Dynamic range of IPs to make available to LAN PC and the lease time. 
# The range of addresses here must lie within the address range assigned to the virtual interface.
dhcp-range=192.168.0.2,192.168.0.200,1h
EOF

These two services and their configuration files turn my new little box into a functional home router.

https://speed.cloudflare.com/ says I’m getting around 90 Mbps, which is good enough for me. I don’t even pay for much more than that. The devices on my desk are wired up like this:

network

Totally worth the $50.