Summary
Committing a second protocols nhrp tunnel that has redirect enabled fails with "Failed to apply NHRP tunnel firewall rules" and the commit is rejected. A single NHRP tunnel commits and works fine.
Root cause is a hardcoded nftables meter name (loglimit-0) in the NHRP firewall template. The redirect chain is rendered in a per-tunnel loop, so the second redirect-enabled tunnel re-declares the same named set and nft aborts the whole (atomic) ruleset load.
Software version
- 2026.07.21-1151-rolling (also reproduces on current rolling)
- Regression introduced by the NHRP-to-FRR migration (T2326, commit 5e8307bf3)
Steps to reproduce
set interfaces tunnel tun100 address '172.16.0.1/32' set interfaces tunnel tun100 encapsulation 'gre' set interfaces tunnel tun100 source-address '198.51.100.1' set interfaces tunnel tun100 enable-multicast set interfaces tunnel tun100 parameters ip key '1' set protocols nhrp tunnel tun100 network-id '1' set protocols nhrp tunnel tun100 redirect set protocols nhrp tunnel tun100 multicast 'dynamic' commit # OK set interfaces tunnel tun101 address '172.16.1.1/32' set interfaces tunnel tun101 encapsulation 'gre' set interfaces tunnel tun101 source-address '198.51.100.1' set interfaces tunnel tun101 enable-multicast set interfaces tunnel tun101 parameters ip key '2' set protocols nhrp tunnel tun101 network-id '2' set protocols nhrp tunnel tun101 redirect set protocols nhrp tunnel tun101 multicast 'dynamic' commit # FAILS
Commit output:
[ protocols nhrp ] Failed to apply NHRP tunnel firewall rules [[protocols nhrp]] failed Commit failed
Expected behavior
Multiple NHRP tunnels, each with redirect (and/or multicast) enabled, commit successfully and each gets its own redirect rate-limit/log rule.
Actual behavior
The commit fails as soon as a second redirect-enabled tunnel is added. The trigger is specifically two or more tunnels with redirect. Tunnels that only use multicast are unaffected.
Root cause
apply() in src/conf_mode/protocols_nhrp.py runs nft --file /run/nftables_nhrp.conf and raises on any non-zero return:
nft_rc = run(f'nft --file {nhrp_nftables_conf}') if nft_rc != 0: raise ConfigError('Failed to apply NHRP tunnel firewall rules')
That file is rendered from data/templates/frr/nhrpd_nftables.conf.j2. The redirect chain loops over every tunnel but hardcodes the meter name loglimit-0:
{% for tun, tunnel_conf in tunnel.items() %}
{% if tunnel_conf.redirect is vyos_defined %}
iifname "{{ tun }}" oifname "{{ tun }}" meter loglimit-0 size 65535 { ... } counter log group {{ redirect }}
{% endif %}
{% endfor %}meter loglimit-0 size 65535 { ... } declares a named dynamic set loglimit-0 in table vyos_nhrp_redirect. With two redirect-enabled tunnels the loop declares it twice. The {{ redirect }} log-group value is a single global constant (1, set in python/vyos/frrender.py), so the two rules are identical except for the interface name — the meter name never varies. The rendered /run/nftables_nhrp.conf becomes:
table ip vyos_nhrp_redirect {
chain VYOS_NHRP_REDIRECT_FORWARD {
type filter hook forward priority filter+10; policy accept;
iifname "tun100" oifname "tun100" meter loglimit-0 size 65535 { ip daddr & 255.255.255.0 . ip saddr & 255.255.255.0 timeout 1m limit rate 4/minute burst 1 packets } counter log group 1
iifname "tun101" oifname "tun101" meter loglimit-0 size 65535 { ip daddr & 255.255.255.0 . ip saddr & 255.255.255.0 timeout 1m limit rate 4/minute burst 1 packets } counter log group 1
}
}nft rejects the second declaration because the named set/meter loglimit-0 already exists in the table, and aborts the atomic load. Reproducing by hand surfaces the exact error:
$ sudo nft --file /run/nftables_nhrp.conf # fails on the second loglimit-0 line, e.g.: # Error: Could not process rule: File exists
The multicast chains use plain rules with no named sets, which is why multicast-only tunnels don't collide.
Why it wasn't caught
smoketest/scripts/cli/test_protocols_nhrp.py only ever configures a single tunnel (tun100) and even asserts the literal meter loglimit-0 string, so the multi-tunnel path has no test coverage.
Proposed fix
Make the meter name unique per loop iteration. Using loop.index0 keeps loglimit-0 for the first tunnel (existing smoketest assertion stays valid) and yields loglimit-1, loglimit-2, … for the rest:
--- a/data/templates/frr/nhrpd_nftables.conf.j2 +++ b/data/templates/frr/nhrpd_nftables.conf.j2 @@ -37,7 +37,7 @@ {% if tunnel is vyos_defined %} {% for tun, tunnel_conf in tunnel.items() %} {% if tunnel_conf.redirect is vyos_defined %} - iifname "{{ tun }}" oifname "{{ tun }}" meter loglimit-0 size 65535 { ip daddr & 255.255.255.0 . ip saddr & 255.255.255.0 timeout 1m limit rate 4/minute burst 1 packets } counter log group {{ redirect }} + iifname "{{ tun }}" oifname "{{ tun }}" meter loglimit-{{ loop.index0 }} size 65535 { ip daddr & 255.255.255.0 . ip saddr & 255.255.255.0 timeout 1m limit rate 4/minute burst 1 packets } counter log group {{ redirect }} {% endif %} {% endfor %} {% endif %}
This matches the convention used throughout the firewall templates, where every named set is parameterized per item (RECENT_{{ set_name }}, A_{{ group_name }}, … in data/templates/firewall/nftables-defines.j2). A more self-documenting alternative is loglimit-{{ tun }} (e.g. loglimit-tun100), but it changes the first tunnel's name and would require updating the smoketest assertion.
Regression test
Extend smoketest/scripts/cli/test_protocols_nhrp.py with two tunnels that both enable redirect and multicast, assert the commit succeeds, and assert both loglimit-0 and loglimit-1 rules exist in table ip vyos_nhrp_redirect.
Workaround
Until fixed, only one NHRP tunnel may have redirect enabled at a time.