TTR Networks

Intermediate · VyOS firewall in depth · section 6 of 8

Under the hood: nftables

✓ Lab-tested 21 October 2026 on VyOS 2026.10.16-0024-rolling, Debian 12 hosts

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, where A_ means address group and N_ network group), which is why groups stay fast with thousands of entries.
  • The recent rate 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.

What VyOS wrote into the kernel (real output)
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.
Everything at a glance (real output)
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.
The router's own traffic (real output)
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.