Ip/ipv6 source-validation at two places in config?

Nowadays (using stream 2026.03) you can set ip/ipv6 source-validation both in firewall global context aswell as interface ethernet context like so:

set firewall global-options ipv6-source-validation 'strict'
set firewall global-options source-validation 'strict'

set interfaces ethernet eth0 ip source-validation strict
set interfaces ethernet eth0 ipv6 source-validation strict

Which one is which?

Like if I keep them for interface but disable them for firewall this would mean?

Or the other way around, I disable them for interface but keep them for firewall?

Or do I have to have this enabled at both places to have effect?

I’d say

  • global is the default for all interfaces
  • per interface overrides the global setting

But then the global wouldnt be needed since the default at interface level is that no source-validation is enabled?

I don’t understand.

If the default is “no validation enabled”, you have the option of either enable it per interface, or globally for all interfaces, saving you to repeat it for every interface individually.

Or am I missing something?

If local setting of interfaces override the global setting in firewall then the global setting in firewall wouldnt be needed because enabled in firewall but disabled in interfaces would mean that the feature for this interface is disabled as result.

Just confusing to have both settings at different places without any clear information on which is which in the docs.

Unless its something like if set then firewall will override interfaces. But if not set then its the interfaces who decides and if that isnt set either then the result is that this setting for this interface is disabled?

Because we have like these combos (ill ignore strict/loose for now):

  • firewall: not set + interfaces: not set = ?
  • firewall: not set + interfaces: disabled = ?
  • firewall: not set + interfaces: enabled = ?
  • firewall: disabled + interfaces: not set = ?
  • firewall: disabled + interfaces: disabled = ?
  • firewall: disabled + interfaces: enabled = ?
  • firewall: enabled + interfaces: not set = ?
  • firewall: enabled + interfaces: disabled = ?
  • firewall: enabled + interfaces: enabled = ?

Not mentioned but I assume that firewall setting is “disabled” by default when not set:

https://docs.vyos.io/en/1.5/configuration/firewall/global-options.html#cfgcmd-set-firewall-global-options-source-validation-strict-loose-disable

Same with the interfaces setting that even if not mentioned then when not set the default is “disabled”:

https://docs.vyos.io/en/1.5/configuration/interfaces/ethernet.html#cfgcmd-set-interfaces-ethernet-interface-ip-source-validation-strict-loose-disable

From what I can see in the code, an nftables table inet vyos_global_rpfilter is created.

The global option inserts a rule in there for all interfaces, if defined:

{% if global_options.source_validation is vyos_defined('loose') %}
        fib saddr oif 0 counter drop
{% elif global_options.source_validation is vyos_defined('strict') %}
        fib saddr . iif oif 0 counter drop
{% endif %}

Adding a rule at interface level just adds another rule to that table:

        nft_prefix = f'nft add rule ip6 raw vyos_rpfilter iifname "{self.ifname}"'
        if mode == 'strict':
            self._cmd(f"{nft_prefix} fib saddr . iif oif 0 counter drop")
        elif mode == 'loose':
            self._cmd(f"{nft_prefix} fib saddr oif 0 counter drop")

So this suggests that you either set it globally for all interfaces, OR for every interface indivudually as I don’t see an option that would add an “accept” before the global “drop”.

Thanks!

To me that looks at bit odd.

Because first we have this (firewall) who acts on vyos_global_rpfilter:

If loose than add this line, else if strict than add that line, else do nothing.

But then we also have this (interface) that acts on vyos_rpfilter iifname:

And not only that they seem to be independent as in will do double work but also for example at global doing loose for all traffic and at the same time doing strict for a specific interface will result in two checks being performed first loose and then in another chain strict?

But also that the logic as I interpret it seems a bit bogus in the interface code?

Its like:

if mode strict or loose then do nothing, else if mode strict add this line, else if mode loose add that line.

So like the two else if’s will never be reached or will they?

I filed that as a bug report or rather asked for a second evaluation specially regarding that interface code but also since it seems to do double the work.

Like if you add strict in firewall and strict in interface then it will check the packet for being strict two times?

I think one time is enough :-1:

nftables processes rules in order of definition.

Assuming the chain vyos_global_rpfilter is defined before vyos_rpfilter and you add a global rule (either loose or strict) it will add a drop rule to that chain, which means it will never get to the interface specific rules, they won’t do anything.

In other words, as soon as you add a global definition, the interface rules don’t do anything anymore.

At least, that is how I interpret the code with my limited knowledge.

Well the output of “sudo nft list ruleset” tells a different story.

This is with having “strict” both at firewall and interface level (and IPv6 routing being disabled), I would expect only one of them to be executed (wasting CPU-cycles) but clearly they are both being used?

table ip raw {
	chain vyos_global_rpfilter {
		fib saddr . iif oif 0 counter packets 408 bytes 79938 drop
		return
	}

	chain vyos_rpfilter {
		type filter hook prerouting priority raw; policy accept;
		iifname "eth1" fib saddr . iif oif 0 counter packets 4823 bytes 965273 drop
		iifname "eth1" counter packets 867069 bytes 115745227 return
		iifname "eth0" fib saddr . iif oif 0 counter packets 202 bytes 36669 drop
		iifname "eth0" counter packets 948811 bytes 52940605 return
		counter packets 1815328 bytes 166689781 jump vyos_global_rpfilter
	}
}
table ip6 raw {
	chain vyos_global_rpfilter {
		fib saddr . iif oif 0 counter packets 0 bytes 0 drop
		return
	}

	chain vyos_rpfilter {
		type filter hook prerouting priority raw; policy accept;
		iifname "eth1" fib saddr . iif oif 0 counter packets 0 bytes 0 drop
		iifname "eth1" counter packets 37 bytes 2384 return
		iifname "eth0" fib saddr . iif oif 0 counter packets 0 bytes 0 drop
		iifname "eth0" counter packets 38 bytes 2480 return
		counter packets 89 bytes 6752 jump vyos_global_rpfilter
	}
}

Or is the global hitting some other interface like lo0 ?

Assuming this system doesn’t have any other interfaces than eth0 and eth1 (dummy? tunnels? etc), that might be an explanation, as nft processing stops when a drop is reached.

If you want to be sure, you probably have to trace the chains: Ruleset debug/tracing - nftables wiki

Im not worried about the drops which hopefully will not be that many.

Im worried about wasted CPU-cycles for the allows since evaluating the same packet twice against the same thing is just waste of resources.

The return rules are an early exit mechanism, so if traffic comes in on either eth0 or eth1 and passes the uRPF check, then it won’t hit the global rule. The return will cause it to hit the “policy accept” default for the hook. You can see that it’s not being processed twice with the packet count delta between the first 4 rules and the jump rule in your ruleset output. Using return is odd since it should just be accept, but that is done a lot in VyOS’ nftables implementation, which I was told was carry overs from the iptables implementation.

With that said, there is room for improvement in the implementation. For instance, in your config, if traffic comes in on eth0 and passes the uRPF checks, then it has to hit 4 rules. This scales even worse if you had say 5 interfaces configured for ‘strict’. Then if traffic hits the last interface in the chain and passes checks, then it would have hit 10 rules before being accepted.

A better way would be to use a vmap for strict/loose. For example, this would only be hitting 1 rule (the VMAP call) for most of your traffic, but only 2 rules at most if traffic needs to hit the global rule:

table ip rawtest {
    map rpfilter_strict {
        typeof fib saddr . iif oif : verdict
        counter
        elements = { 0 : drop, "eth0" : accept, "eth1" : accept }
    }

    chain vyos_rpfilter {
        type filter hook prerouting priority raw; policy accept;
        iifname { "eth0", "eth1" } fib saddr . iif oif vmap @rpfilter_strict
        fib saddr . iif oif 0 counter drop
    }
}

So if you had 5 interfaces configured for strict, it’d only hit 1 rule instead of 10. You can save that config snippet to a file and load it with nft -f <name of file if you want to test it. You’ll see the counters incrementing correctly with nft list table ip rawtest. Here’s the output from one of my routers with that table configured:

table ip rawtest {
        map rpfilter_strict {
                typeof fib saddr . iif oif : verdict
                counter
                elements = { 0 counter packets 23 bytes 1737 : drop, 
                "eth0" counter packets 489 bytes 38068 : accept, 
                "eth1" counter packets 0 bytes 0 : accept }
        }

        chain vyos_rpfilter {
                type filter hook prerouting priority raw; policy accept;
                iifname { "eth0", "eth1" } fib saddr . iif oif vmap @rpfilter_strict
                fib saddr . iif oif 0 counter packets 5 bytes 1360 drop
        }
}