Under the hood: nftables
VyOS's firewall is a front end: on commit, it writes nftables rules into the Linux kernel. Reading them is the fastest way to answer "what is the router actually doing?", and the skills carry straight over to any Linux firewall. (To write nftables by hand on a Debian router, see nftables and iptables on Debian.)
- Each family gets a table (
ip vyos_filter,ip6 vyos_filter). - Each hook becomes a chain hooked into the kernel (
VYOS_INPUT_filter), and each named ruleset an ordinary chain (NAME_SSH-IN). - Every hook chain starts with a jump to
VYOS_STATE_POLICY, which is where the global state policy lives. - Each group becomes an nftables set (
A_ADMIN-HOMES, whereA_means address group andN_network group), which is why groups stay fast with thousands of entries. - The
recentrate limit turns into a per-source meter:limit rate over 3/minute burst 3 packets. It allows a burst of three, then refills at three a minute. It counts packets in the new state, so a client's retransmitted connection attempts count too. - The comment on each nftables rule (
ipv4-NAM-SSH-IN-20) maps it back to the VyOS rule.
The hook chains have policy accept with an explicit final drop rule, so the default-action you configured is a rule with its own counter.
The output hook
The router's own traffic leaves through the output hook, which this lab leaves open. R1 can ping the scanner, even though the scanner can't reach R1: the request leaves via output, and the reply comes back as established, accepted by the state policy before the blocklist is checked. An output filter is useful on locked-down routers (for example, only allowing the router to reach its DNS, NTP and log servers), but most networks leave it open.
vyos@R1:~$ sudo nft list tables | grep vyos_filter
table ip vyos_filter
table ip6 vyos_filter
table bridge vyos_filter
vyos@R1:~$ sudo nft list chain ip vyos_filter VYOS_STATE_POLICY
table ip vyos_filter {
chain VYOS_STATE_POLICY {
ct state established counter packets 8 bytes 447 accept
ct state invalid counter packets 0 bytes 0 drop
ct state related counter packets 0 bytes 0 accept
return
}
}
vyos@R1:~$ sudo nft list chain ip vyos_filter VYOS_INPUT_filter
table ip vyos_filter {
chain VYOS_INPUT_filter {
type filter hook input priority filter; policy accept;
jump VYOS_STATE_POLICY
counter packets 3 bytes 228 jump NAME_BLOCKLIST-CHECK comment "ipv4-INP-filter-5"
meta l4proto icmp counter packets 0 bytes 0 accept comment "ipv4-INP-filter-10"
tcp dport 22 counter packets 1 bytes 60 jump NAME_SSH-IN comment "ipv4-INP-filter-20"
counter packets 0 bytes 0 drop comment "INP-filter default-action drop"
}
}
vyos@R1:~$ sudo nft list chain ip vyos_filter NAME_SSH-IN
table ip vyos_filter {
chain NAME_SSH-IN {
ip saddr @N_MGMT-NETS counter packets 1 bytes 60 accept comment "ipv4-NAM-SSH-IN-10"
ct state new add @RECENT_NAM_SSH-IN_20 { ip saddr limit rate over 3/minute burst 3 packets } log prefix "[ipv4-NAM-SSH-IN-20-D]" counter packets 0 bytes 0 drop comment "ipv4-NAM-SSH-IN-20"
ip saddr @A_ADMIN-HOMES counter packets 0 bytes 0 accept comment "ipv4-NAM-SSH-IN-30"
counter packets 0 bytes 0 log prefix "[ipv4-NAM-SSH-IN-default-D]" drop comment "NAM-SSH-IN default-action drop"
}
}
vyos@R1:~$ sudo nft list set ip vyos_filter A_ADMIN-HOMES
table ip vyos_filter {
set A_ADMIN-HOMES {
type ipv4_addr
flags interval
auto-merge
elements = { 192.0.2.10 }
}
}- Lines 1–4: One table per family, plus one for bridges.
- Lines 5–13: The global state policy, as a chain of its own.
- Lines 14–24: The input filter: state policy first, then rules 5, 10 and 20, each tagged with a comment naming the VyOS rule, then the default action as a final drop.
- Lines 25–33: The SSH chain, with the rate limit as a per-source meter.
- Lines 34–42: A group, as an nftables set.
vyos@R1:~$ show firewall summary
Ruleset Summary
IPv6 Ruleset:
Ruleset Hook Ruleset Priority Description
-------------- ------------------ -------------
forward filter
input filter
IPv4 Ruleset:
Ruleset Hook Ruleset Priority Description
-------------- ------------------ ---------------------------
forward filter
input filter
name BLOCKLIST-CHECK Shared by input and forward
name SSH-IN Who may SSH to the router
Firewall Groups
Name Type References Members
----------- --------------- ---------------------------- ------------
ADMIN-HOMES address_group ipv4-name-SSH-IN-30 192.0.2.10
BLOCKLIST address_group ipv4-name-BLOCKLIST-CHECK-10 192.0.2.66
LAN-IFS interface_group ipv4-forward-filter-10 eth1
ipv4-forward-filter-30
ipv6-forward-filter-10
MGMT-NETS network_group ipv4-name-SSH-IN-10 10.99.0.0/24
WEB-PORTS port_group ipv4-forward-filter-10 443
ipv4-forward-filter-30 80
ipv6-forward-filter-10- Lines 4–19: Each hook and named ruleset, per family.
- Lines 21–33: Groups and the rules that use them.
vyos@R1:~$ ping 192.0.2.66 count 2
PING 192.0.2.66 (192.0.2.66) 56(84) bytes of data.
64 bytes from 192.0.2.66: icmp_seq=1 ttl=63 time=2.93 ms
64 bytes from 192.0.2.66: icmp_seq=2 ttl=63 time=2.16 ms
--- 192.0.2.66 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1007ms
rtt min/avg/max/mdev = 2.164/2.549/2.934/0.385 ms- Lines 1–4: R1 pings the blocklisted scanner: outbound via the open output hook, and the replies are established traffic.