R6S router project

2026-03


Background

When I finally got access to fiber and considered upgrading my network, several complications reared their heads. My house is old, and the cabling channels in the walls are sized rather restrictively, which meant that since I wanted to avoid surface-mounted cabling as much as possible, I was unable to build a single, completely centrally connected ethernet backhaul. This meant that in addition to a central switch, I needed a router with at least two internal LAN ports to connect the rooms in the two sides of the network.

LAN wiring diagram

Also, since my cabling was new enough to support it, and multiple devices (like my main computer and NAS) in my home had 2.5G NICs, I wanted a speedy LAN to take advantage of them. For wifi, I specifically did not want that functionality in the router, as I was planning to set up separate dedicated wifi access points for each floor. Several options popped up, but none felt reasonable, as wired routers with multiple 2.5G LAN ports seemed to be both rare and expensive. Ubiquity devices for this niche would set one back more than 300 euros, and at the time, I couldn't even find a Mikrotik option that filled my requirements. There were a couple of (ugly) wifi routers by gaming-oriented brands that matched these specifications for €200-€300, but I felt they weren't for me.

It soon became clear that similarly to my NAS project, what I probably wanted was some kind of a "prosumer" device such as those produced by CWWK and FriendlyElec. When trying to find the least overengineered option, I soon came up with the NanoPi R6S, which had exactly the number of ports I needed for $150, coupled with a beefy processor, 8G of RAM and 64GB of EMMC.

Setting up

The device arrived with the case already assembled. Build quality feels solid and plugging in a display and keyboard just worked immediately. The device was initially set up with some kind of a desktop environment, which I didn't investigate further.

In addition to serving as a documentation for myself on what I actually did and how to start fixing stuff later when I've forgotten, this document can be considered a guide on how to manually set up a functional linux-based router.

Installation
Installed on the wall

Image selection

With official images, the installation was trivial - insert an SD card with the image, boot, and when asked, remove it and reboot. Unfortunately I cannot recommend the official images, as they are application images with a layered filesystems that prevent you from updating the kernel and configuring critical components such as filesystem settings. They are clearly intended for fire&forget installations where the device is to be provisioned for some application and then left alone for years instead of being actively maintained.

Instead, one should visit armbian.com and pick an image. I chose minimal Trixie, as Debian has always worked for me and it has an absolutely massive install base, which comes handy if one has to do any problem solving. For this, one does have to run armbian-config from the SD card and install to EMMC manually, which might not be obvious on first glance. After this, it is possible to remove the SD card and reboot, which gives a situation identical to the application images, except with the user in control of everything.

NIC renaming

R6S official images come with an intended setup of 2.5G WAN and 1G and 2.5G LAN ports. Because we are not going to use the 2.5G port as WAN, it is smart to rename the ports in order to avoid confusion down the line. Thus, I suggest opening /etc/udev/rules.d/70-persistent-net.rules and alrering it as follows:

SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", KERNELS=="fe1c0000.ethernet", NAME:="eth0"
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="r8169", KERNELS=="0003:31:00.0", NAME:="eth1"
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="r8169", KERNELS=="0004:41:00.0", NAME:="eth2"

It might be possible to just delete the file instead, which should result in the same naming convention, but I didn't bother to try.

Bridge interface

Instead of configuring systemd-networkd directly, Armbian uses Netplan to manage the network interfaces. At least for me, the configuration was read from /etc/netplan/10-armbian.yaml and I changed it to remove dhcp from eth1/eth2 and combine them to a switch-like br0 interface:

network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      dhcp4: true
      dhcp4-overrides:
        use-dns: true
      dhcp6: false
    eth1:
      dhcp4: false
      dhcp6: false
    eth2:
      dhcp4: false
      dhcp6: false
  bridges:
    br0:
      interfaces:
        - eth1
        - eth2
      addresses:
        - 10.2.0.1/24
      dhcp4: false
      dhcp6: false
      parameters:
        stp: false
If you want a separate VLAN for guest users or IoT shit, consider adding the following or a similar block to the end:
  vlans:
    vlan20:
      id: 20
      link: br0
      addresses:
        - 10.2.20.1/24

In my case, I'm using VLAN ID 20 for IoT devices. The specific ID holds no significance and it can be anything, but based on what I've read, 20 is used for IoT by convention.

dnsmasq

One can always edit dhcpd configuration files directly, and I have done so in the past, but these days we have better options. The dnsmasq service is a one-stop DHCP solution for your LAN which handles DHCP and both NTP and DNS proxying. To set this up, we have to break armbian name resolution a bit, because it makes certain assumptions about how things should work.

First, edit /etc/hosts and bind your gateway hostname to its br0 IP, so that other machines can ping the gateway with that name instead of pinging themselves. Also remove the name from the ::1 address, so that machines in LAN won't try to connect to themselves when you aim for the gateway.

127.0.0.1   localhost
10.2.0.1    your-gateway-name
::1         localhost ip6-localhost ip6-loopback
fe00::0     ip6-localnet
ff00::0     ip6-mcastprefix
ff02::1     ip6-allnodes
ff02::2     ip6-allrouters

Then, in modern Debian, /etc/resolv.conf is symlinked to /run/systemd/resolve/stub-resolv.conf, which does not seem to work for our purposes as using it will create a name resolution loop. We aim to make the gateway machine and its DNS proxy responsible for resolving names in the LAN, so the order must to go from ISP DNS servers to dnsmasq to LAN. Thus, we alter resolv.conf to contain a single line:

nameserver 127.0.0.1

Then, in /etc/dnsmasq.conf, we point the upstream to where names are really resolved from. The following block lists also other settings which you should set avoid angering your ISP by dealing out DHCP addresses from eth0 (this continues to be a constant nuisance in apartment complexes with ethernet cabling).

# Change this line if you want dns to get its upstream servers from
# somewhere other that /etc/resolv.conf
resolv-file=/run/systemd/resolve/resolv.conf

bind-interfaces
expand-hosts

That is enough for the general dnsmasq configuration, but to set up the internal DHCP space, one should also create an additional file in /etc/dnsmasq.d, which contains the actual address dealing information:

# LAN domain for local names
interface=br0
domain=lan
local=/lan/

# Dynamic DHCP range: start at .33
dhcp-range=10.2.0.33,10.2.0.254,255.255.255.0,6h

# Set this as DNS, NTP and GW for clients
dhcp-option=option:router,10.2.0.1
dhcp-option=option:dns-server,10.2.0.1
dhcp-option=option:ntp-server,10.2.0.1

# Static hosts
dhcp-host=C7-DB-0B-C6-1F-3A,10.2.0.5,printer,6h
dhcp-host=A1-DA-B9-C0-95-E9,10.2.0.9,gamingrig,6h

Optionally, add another one for your IoT/guest/whatever vlan, if any:

# Domain for IoT shit
interface=vlan20
domain=iot
local=/iot/

# Dynamic DHCP range: start at .33
dhcp-range=10.2.20.33,10.2.20.254,255.255.255.0,6h

# Set this computer's vlan20 interface as DNS and GW for clients
dhcp-option=option:router,10.2.20.1
dhcp-option=option:dns-server,10.2.20.1
dhcp-option=option:ntp-server,10.2.20.1

# Static hosts
dhcp-host=5F:4E:4D:FC:F2:06,10.2.20.11,spotlight1,6h
dhcp-host=3A:CF:39:7F:54:FA,10.2.20.12,spotlight2,6h
dhcp-host=18:92:89:E1:95:85,10.2.20.13,spotlight3,6h
dhcp-host=0B:C4:08:E4:33:EE,10.2.20.14,spotlight4,6h

After this, NPT, LAN dhcp, and local and global name resolution should work. I have to admit that writing resolv.conf manually seems iffy, but as far as I know, Armbian uses netplan by default and if one does not want to switch away from it, this sidesteps issues with having to disable systemd-resolved and reconfigure absolutely everything.

chrony

Rather minor, but why not - we just wanted to be the master time for devices in the LAN, so install chrony. Then add the following to its configuration file:

# Serve LAN
allow 10.2.0.0/24
local stratum 10

Firewall

Edit the file /etc/sysctl.d/99-router.conf or create it if it does not exist. Personally I do not enable ipv6 forwarding or deal ipv6 addresses into LAN, so your mileage may vary.

# IPv4
net.ipv4.ip_forward = 1
net.ipv4.conf.eth0.rp_filter=1
net.bridge.bridge-nf-call-iptables=0

# IPv6
net.bridge.bridge-nf-call-ip6tables=0

This enables IP4 IP forwarding, and prevents the kernel from immediately throwing packages to trash if they arrive in its external IP but are not targeted for itself.

I still can't speak netfilter, so the following is an iptables script. I'm giving the script as-is without a lot of commenting, but it should be obvious what lines you should edit to configure your internal service ports. Note that the script provides functionality that most commercial routers do not have. Most importantly, it has proper NAT reflection for both UDP and TCP, and it uses deterministic SNAT so that oldschool services such as identd can actually function. You indeed can get a better product by building stuff yourself.

This expert functionality is the reason for why the forwarding blocks appear as complex as they are. Much simpler setups work if one does not have needy local services.

#!/bin/bash
set -eu

WAN=eth0
LAN=br0
VLAN=vlan20
LAN_NET=10.2.0.0/24
VLAN_NET=10.2.20.0/24
PUBLIC_IP="$(ip -4 addr show "$WAN" | awk '/inet / {print $2}' | cut -d/ -f1)"
[ -n "$PUBLIC_IP" ] || { echo "fw: no public IP on $WAN" >&2; exit 1; }

GAMINGRIG="10.2.0.9"

allow() {
  # allow_wan_service  
  local port="$1" protocol="$2" protocols p

  if [ "$protocol" = "both" ]; then
    protocols="tcp udp"
  else
    protocols="$protocol"
  fi

  for p in $protocols; do
    iptables -A INPUT -i "$WAN" -d "$PUBLIC_IP" -p "$p" --dport "$port" -m conntrack --ctstate NEW -j ACCEPT
    iptables -t nat -A PREROUTING -i "$LAN" -d "$PUBLIC_IP" -p udp --dport "$port" -j REDIRECT --to-ports "$port"
  done
}

create_forward() {
  # create_forward   
  local ip="$1" port="$2" protocol="$3"

  if [ "$protocol" = "both" ]; then
    protocols="tcp udp"
  else
    protocols="$protocol"
  fi

  for p in $protocols; do
    # Forwarding
    iptables -t nat -A PREROUTING -i "$WAN" -p "$p" --dport "$port" -j DNAT --to-destination "$ip:$port"
    iptables -A FORWARD -i "$WAN" -o "$LAN" ! -s "$LAN_NET" -p "$p" -d "$ip" --dport "$port" -m conntrack --ctstate NEW -j ACCEPT

    # LAN -> LAN
    iptables -A FORWARD -i "$LAN" -o "$LAN" -s "$LAN_NET" -d "$ip" -p "$p" --dport "$port" -m conntrack --ctstate NEW -j ACCEPT

    # NAT Reflection
    iptables -t nat -A PREROUTING -i "$LAN" -d "$PUBLIC_IP" -p "$p" --dport "$port" -j DNAT --to-destination "$ip:$port"
    iptables -t nat -A POSTROUTING -o "$LAN" -s "$LAN_NET" -d "$ip" -p "$p" --dport "$port" -j MASQUERADE
  done
}

# Flush
iptables -F
iptables -t nat -F
iptables -t mangle -F
iptables -X

# Default policies
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# Allow loopback
iptables -A INPUT -i lo -j ACCEPT

# Allow between interfaces
iptables -A FORWARD -i eth1 -o eth2 -j ACCEPT
iptables -A FORWARD -i eth2 -o eth1 -j ACCEPT
iptables -A FORWARD -i eth1 -o "$VLAN" -j ACCEPT
iptables -A FORWARD -i eth2 -o "$VLAN" -j ACCEPT

# Allow established/related in
iptables -A INPUT   -m conntrack --ctstate INVALID -j DROP
iptables -A FORWARD -m conntrack --ctstate INVALID -j DROP
iptables -A INPUT   -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Allow ping, but ratelimit
iptables -A INPUT -i "$WAN" -p icmp --icmp-type echo-request -m hashlimit --hashlimit 1/second --hashlimit-burst 5 --hashlimit-mode srcip --hashlimit-name ping_limit -j ACCEPT

# Allow LAN and VLAN to reach router services (DHCP/DNS/SSH etc)
iptables -A INPUT -i "$LAN" -j ACCEPT
iptables -A INPUT -i "$VLAN" -j ACCEPT

# Allow LAN -> WAN forwarding
iptables -A FORWARD -i "$LAN" -o "$WAN" -s "$LAN_NET" -j ACCEPT
#iptables -A FORWARD -i "$VLAN" -o "$WAN" -s "$VLAN_NET" -j ACCEPT

# NAT (masquerade) LAN out from WAN
iptables -t nat -A POSTROUTING -o "$WAN" -s "$LAN_NET" -j MASQUERADE
#iptables -t nat -A POSTROUTING -o "$WAN" -s "$VLAN_NET" -j MASQUERADE

################
### Services ###
################

# Uncomment to allow SSH from WAN
# If you do, BE CAREFUL and harden it
#allow 22 tcp

#######################
### Port Forwarding ###
#######################

# Steam shit
create_forward "$GAMINGRIG" 3074 udp
create_forward "$GAMINGRIG" 3478 both
create_forward "$GAMINGRIG" 4379 both
create_forward "$GAMINGRIG" 27015 both

In this script, the VLAN is isolated both from the LAN and the internet, while devices in LAN can reach the VLAN normally and control IoT devices. Uncomment the masquerade and forward lines if you instead want a guest network, where devices in the VLAN are allowed to access the internet.

Note that just doing this is not enough to make the IoT VLAN work out of the box. Any wifi access points you have must expose a separate VLAN network with its own SSID, and redirect DHCP queries in it to the gateway and its vlan20 interface. Technically, if you have a switch behind the router before the wifi access point, the switch also needs to be managed, so that it can be configured to direct VLAN traffic into the correct direction. With unmanaged switches, malicious IoT devices can scan the address space and bypass your gateway just fine. Currently I am unprotected, as managed switches seemed to be around 250€ a piece for 2.5G throughput, but the logic is there if I decide to invest at some point.

Cpupower

I don't know why, but my Armbian experiences with cpupower, (and earlier with cpufrequtils) with respect to the RK3588 chip have not exactly gone well. As far as I know, the CPU isn't detected correctly and power management simply has to be set manually. The following script helps.

#!/bin/sh
cpupower --cpu 0-3 frequency-set --min 408MHz --max 1800MHz --governor schedutil
cpupower --cpu 4-7 frequency-set --min 408MHz --max 2304MHz --governor schedutil

Make stuff persistent

Save the following blocks as /usr/lib/systemd/system/cpupower.service and /usr/lib/systemd/system/firewall.service and then enable them through the normal systemd commands.

[Unit]
Description=cpupower script to run in system start
After=network.target

[Service]
Type=oneshot
ExecStart=/path/to/update-cpupower
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
[Unit]
Description=Iptables firewall
After=network-online.target

[Service]
Type=oneshot
ExecStart=/path/to/update-firewall
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
service cpupower enable
service firewall enable
systemctl daemon-reload

Finishing up

At this stage, the router main setup is basically finished, and one can either restart the whole device or the services that comprise its functionality. It's best to try this stage while still having your keyboard and monitor connected to the router, as applying the netplan or running the firewall script may lock you out.

If you do not want to restart the whole device, refer to the script below and run it line by line, observing the logs of the services and syslog after each step. Look for errors or inconsistencies.

service cpupower restart
netplan apply
service systemd-networkd restart
service systemd-resolved restart
service dnsmasq restart
service firewall restart

Performance

After all this trouble, you can enjoy some network performance benchmarks.
CPU usage does not seem to be affected by transfers that much, so there is ample headroom to make use of the RAM and install whatever other services you may need.

WAN speedtest, maxes out outgoing IF
LAN iperf3 is pretty close to maxing out 2.5G

<return>