Updated on 2026/08/05
A router on a stick is a router with only ethernet port, which is connected to a vlan switch. The single ethernet port is split into two ethernet ports via vlans (e.g., VLAN100 and VLAN500.
Internet /
ISP Router
192.168.66.0/24
| -----------------------
| | Router-on-a-Stick |
| | VLAN100 192.168.66.54 |
| | VLAN300 192.168.77.1 |
| -----------------------
| |
| | Trunk or Stick
| |
| -------------------------------------------------------------
| | 1 | 2 | 3 | 4 | 5 |
| -------------------------------------------------------------
| | | Netgear GS105v2
| in | |
--------------- |
out
192.168.77.0/24
| Port # | 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|---|
| Configure | VLAN 1 | U | U | U | U | U |
| in | VLAN 100 | U | T | |||
| trunk | VLAN 200 | T | ||||
| out | VLAN 300 | T | U | |||
| PVID | 100 | 200 | 300 | 1 | 1 |
Frames eminating from the trunk go to ether Port 1 or Port 3, but not both.
I don't believe I install vlan or the monprobe, and it still worked?
sudo nmcli con add con-name VLAN100 type vlan dev eth0 id 100 ipv4.address 192.168.66.45/24 ipv4.method manual ipv4.gateway 192.168.66.1 ipv4.dns 192.168.66.1 ipv6.method disabled
The output of
sudo cat /etc/NetworkManager/system-connections/VLAN100.nmconnection
is:
[connection]
id=VLAN100
uuid=e63734b8-6ae9-4a93-b768-bc4cad6cc71d
type=vlan
timestamp=1785877335
[ethernet]
[vlan]
flags=1
id=100
parent=eth0
[ipv4]
address1=192.168.66.45/24
dns=192.168.66.1;
gateway=192.168.66.1
method=manual
[ipv6]
addr-gen-mode=default
method=disabled
[proxy]
sudo nmcli con add con-name VLAN300 type vlan dev eth0 id 300 ipv4.address 192.168.77.1/24 ipv6.method disabled ipv4.method shared ipv4.shared-dhcp-range '192.168.77.100,192.168.77.199'
The output of
sudo cat /etc/NetworkManager/system-connections/VLAN300.nmconnection
is:
[connection]
id=VLAN300
uuid=9d08cf28-7d01-4a95-b1b9-f06912c6dc27
type=vlan
timestamp=1785877337
[ethernet]
[vlan]
flags=1
id=300
parent=eth0
[ipv4]
address1=192.168.77.1/24
method=shared
shared-dhcp-range=192.168.77.100,192.168.77.199
[ipv6]
addr-gen-mode=default
method=disabled
[proxy]
The output of
ip a
is:
1: lo: 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 noprefixroute
valid_lft forever preferred_lft forever
2: eth0: mtu 1500 qdisc mq state UP group default qlen 1000
link/ether e4:5f:01:9c:c4:eb brd ff:ff:ff:ff:ff:ff
3: wlan0: mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether e4:5f:01:9c:c4:ed brd ff:ff:ff:ff:ff:ff
4: eth0.300@eth0: mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether e4:5f:01:9c:c4:eb brd ff:ff:ff:ff:ff:ff
inet 192.168.77.1/24 brd 192.168.77.255 scope global noprefixroute eth0.300
valid_lft forever preferred_lft forever
5: eth0.100@eth0: mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether e4:5f:01:9c:c4:eb brd ff:ff:ff:ff:ff:ff
inet 192.168.66.45/24 brd 192.168.66.255 scope global noprefixroute eth0.100
valid_lft forever preferred_lft forever
Because the VLAN300 ipv4.method was shared, the Network Manager will not only implement the DHCP protocol and supply ipv4 address from the specified range, but it will also generate nftables for fowarding and masquerade.
To see the nftables:
sudo systemctl enable nftables.service
sudo reboot
Then execute
sudo nft list ruleset
The output is:
# Generated by Debian 13 (trixie) / Raspberry Pi OS.
table inet filter {
chain input {
type filter hook input priority filter; policy accept;
}
chain forward {
type filter hook forward priority filter; policy accept;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
# Generated by the NetworkManager because ipv4.method was shared.
table ip nm-shared-eth0.300 {
chain nat_postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 192.168.77.0/24 ip daddr != 192.168.77.0/24 masquerade
}
chain filter_forward {
type filter hook forward priority filter; policy accept;
ip daddr 192.168.77.0/24 oifname "eth0.300" ct state { established, related } accept
ip saddr 192.168.77.0/24 iifname "eth0.300" accept
iifname "eth0.300" oifname "eth0.300" accept
iifname "eth0.300" reject
oifname "eth0.300" reject
}
}
The output of sudo nft list rulest, only list rules and supresses all comments. The two comments were added to show which tables were produced by what.
The DHCP protocol allows for static addresses so you do not have to actually implementing the method shared, but you need the method to be shared to get the Network Manager to generate the nftables.
This was implemented on both a Raspberry Pi 5B and a RPi Zero 2W. For the Zero 2W, I had to add a USB-to-Ethernet adapter. The USB port on to Zero 2W is USB 2.0 and limited to 480 Mb/s maximum. It was fast enough for web surfing, but its not as fast as 1GB/s Ehternet.
Maybe the Zero 3w wil have USB 3.0, which has a trasfer rate of 5GB/s.
A Router-on-a-Stick can be expanded to create multiple subnets and seperate traffic.
Internet /
ISP Router
192.168.66.0/24
| -----------------------
| | Router-on-a-stick |
| | VLAN100 192.168.66.54 |
| | VLAN300 192.168.77.1 |
| | VLAN400 192.168.88.1 |
| | VLAN500 192.168.99.1 |
| -----------------------
| |
| | Trunk or Stick
| |
| -------------------------------------------------------------
| | 1 | 2 | 3 | 4 | 5 |
| -------------------------------------------------------------
| | | | |
| in | | | |
--------------- | out 2 |
| 192.168.88.0/24 |
| |
out 1 out 3
192.168.77.0/24 192.166.99.0/24
| Port # | 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|---|
| Configure | VLAN 1 | U | U | U | U | U |
| in | VLAN 100 | U | T | |||
| trunk | VLAN 200 | T | ||||
| out 1 | VLAN 300 | T | U | |||
| out 2 | VLAN 400 | T | U | |||
| out 1 | VLAN 50 | T | U | U | ||
| PVID | 100 | 200 | 300 | 1 | 1 |
There are no changes to VLAN100 and/or VLAN300.
sudo nmcli con add con-name VLAN400 dev eth0 id 300 ipv4.address 192.168.88.45/24 ipv6.method disabled ipv4.method shared ipv4.shared-dhcp-range '192.168.88.100,192.168.88.199'
sudo nmcli con add con-name VLAN500 dev eth0 id 300 ipv4.address 192.168.99.45/24 ipv6.method disabled ipv4.method shared ipv4.shared-dhcp-range '192.168.99.100,192.168.99.199'
The nftable created by the Network Manager only allows traffic that originates behind the router. That is, it reject any unsolicited traffic comming into the router, and this is what a router facing the Internet should do. However, I have a laptop that is in front of the router, and it needs to communicate with host behind the router.
To allow this, I copied the nftable ruleset generated by the Network Manager, disabled the Network Manager from generating nftables, and implemented the copied ruleset manualy, and finally modified the nftable. The are a lots os steps, but they are straight forward:
sudo systemctl enable nftables
sudo reboot
sudo nft list ruleset > /etc/my_router.nft
include "/etc/my_router.nft"
sudo vi no_nft.conf
Place the following two lines in the file:
[main]
firewall-backend=none
sudo vi allow_forwarding.conf
Add the following line to the file:
net.ipv4.ip_forward=1
sudo reboot
table ip nm-shared-end0.300 {
chain nat_postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 192.168.66.0/24 ip daddr != 192.168.66.0/24 masquerade
# added by me on 2026/08/01
ip saddr != 192.168.66.0/24 ip daddr 192.168.66.0/24 masquerade
}
chain filter_forward {
type filter hook forward priority filter; policy accept;
ip daddr 192.168.66.0/24 oifname "end0.300" ct state { established, related } accept
# added by me on 2026/08/01
ip daddr 192.168.66.0/24 oifname "end0.300" ip saddr 192.168.37.67 accept
ip saddr 192.168.66.0/24 iifname "end0.300" accept
iifname "end0.300" oifname "end0.300" accept
iifname "end0.300" reject
oifname "end0.300" reject
}
}
sudo nmcli con mod eth0 +ipv4.routes "192.168.77.0/24 192.168.66.54"