Option to disable netfilter bridge firewalling (and/or nf_conntrack_bridge) completely

The introduction of bridge firewalling has brought about a long trail of strange problems that have successively led to the creation of per-protocol workarounds in firewall global-options apply-to-bridged-traffic accept-invalid to make things like ARP and DHCP work.

While figuring out why VyOS receives STP BPDUs from my switch, but my switch does not receive any from VyOS, I eventually realized that the frames are being dropped by ct_state invalid on the bridge.

I tried to work around that by setting up the following rule:

[edit firewall bridge output filter]
pngu@r0# sh r 100
 action accept
 destination {
     /* IEEE 802.1d STP/RSTP/MSTP destination MAC */
     mac-address 01:80:c2:00:00:00
 }

Unfortunately that also doesn’t work, as it is inserted after the conntrack state policy:

root@r0:/home/pngu# nft list chain bridge vyos_filter VYOS_OUTPUT_filter
table bridge vyos_filter {
	chain VYOS_OUTPUT_filter {
		type filter hook output priority filter; policy accept;
		ct state invalid ether type arp counter packets 0 bytes 0 accept
		ct state invalid ether type 8021q counter packets 14 bytes 392 accept
		ct state invalid udp sport 67 udp dport 68 counter packets 0 bytes 0 accept
		ct state invalid ether type 0x0842 counter packets 0 bytes 0 accept
		ct state invalid ether type 0x8864 counter packets 0 bytes 0 accept
		ct state invalid ether type 0x8863 counter packets 0 bytes 0 accept
		jump VYOS_STATE_POLICY
		ether daddr 01:80:c2:00:00:00 counter packets 0 bytes 0 accept comment "bri-OUT-filter-100"
		counter packets 0 bytes 0 accept comment "OUT-filter default-action accept"
	}
}

Once I manually insert that rule ahead of the state policy jump, my switch starts receiving BPDUs:

root@r0:/home/pngu# nft insert rule bridge vyos_filter VYOS_OUTPUT_filter ct state invalid ether daddr 01:80:c2:00:00:00 counter accept

root@r0:/home/pngu# nft list chain bridge vyos_filter VYOS_OUTPUT_filter
table bridge vyos_filter {
	chain VYOS_OUTPUT_filter {
		type filter hook output priority filter; policy accept;
		ct state invalid ether daddr 01:80:c2:00:00:00 counter packets 90 bytes 4680 accept
		ct state invalid ether type arp counter packets 0 bytes 0 accept
		ct state invalid ether type 8021q counter packets 251 bytes 7028 accept
		ct state invalid udp sport 67 udp dport 68 counter packets 0 bytes 0 accept
		ct state invalid ether type 0x0842 counter packets 0 bytes 0 accept
		ct state invalid ether type 0x8864 counter packets 0 bytes 0 accept
		ct state invalid ether type 0x8863 counter packets 0 bytes 0 accept
		jump VYOS_STATE_POLICY
		ether daddr 01:80:c2:00:00:00 counter packets 0 bytes 0 accept comment "bri-OUT-filter-100"
		counter packets 0 bytes 0 accept comment "OUT-filter default-action accept"
	}
}

There are several possible future options to get STP working:

A: Another firewall global-options apply-to-bridged-traffic accept-invalid option, this time for stp

B: An option similar to the above, but for allowing any kind of bridge traffic that conntrack marks as invalid, regardless of “protocol”, e.g. firewall global-options apply-to-bridged-traffic accept-invalid all

C: An option to disable conntrack on bridge interfaces, e.g. firewall global-options apply-to-bridged-traffic disable-conntrack

D: An option to disable all nftables handling of bridge interfaces (i.e. don’t register table bridge at all if the firewall global-options apply-to-bridged-traffic config element is not present)

There are probably other ways to do this, but I’m trying to make a case for option D here, as it would be intuitive behaviour and a sane default. There are valid use cases for filtering bridge traffic when doing things like L2 tunneling, which I believe the original PR introducing bridge firewall was geared towards, but in its current form the feature seems to be doing more harm than good.

As a side note, it seems that all the stuff conntrack complains about as invalid is involving broadcast or link-local addressing of some sort (DHCP, ARP, STP, etc) and it might be possible to fix it by using per-physical-interface conntrack zones instead of the current series of “accept invalid” workarounds, but I’m not familiar enough with conntrack as a whole to be sure, it’s just something that kept coming up during my research.

Can you paste your current malfunctioning config with “show configuration commands” (and perhaps just include the “set firewall” lines)?

Because I have not encountered what you explain.

Im running VyOS Stream 2026.03 currently with this configuration:

set firewall flowtable PROD interface 'eth1'
set firewall flowtable PROD offload 'software'
set firewall global-options all-ping 'enable'
set firewall global-options broadcast-ping 'disable'
set firewall global-options directed-broadcast 'disable'
set firewall global-options ip-src-route 'disable'
set firewall global-options ipv6-receive-redirects 'disable'
set firewall global-options ipv6-source-validation 'strict'
set firewall global-options ipv6-src-route 'disable'
set firewall global-options log-martians 'enable'
set firewall global-options receive-redirects 'disable'
set firewall global-options resolver-cache
set firewall global-options resolver-interval '60'
set firewall global-options send-redirects 'disable'
set firewall global-options source-validation 'strict'
set firewall global-options state-policy established action 'accept'
set firewall global-options state-policy invalid action 'drop'
set firewall global-options state-policy related action 'accept'
set firewall global-options syn-cookies 'enable'
set firewall global-options timeout icmp '10'
set firewall global-options timeout other '600'
set firewall global-options timeout tcp close '30'
set firewall global-options timeout tcp established '600'
set firewall global-options timeout tcp fin-wait '30'
set firewall global-options timeout tcp last-ack '30'
set firewall global-options timeout tcp syn-recv '30'
set firewall global-options timeout tcp syn-sent '30'
set firewall global-options timeout tcp time-wait '30'
set firewall global-options timeout udp other '600'
set firewall global-options timeout udp stream '600'
set firewall global-options twa-hazards-protection 'disable'
set firewall ipv4 forward filter default-action 'drop'
set firewall ipv4 forward filter rule 10 action 'offload'
set firewall ipv4 forward filter rule 10 offload-target 'PROD'
set firewall ipv4 forward filter rule 10 state 'established'
set firewall ipv4 forward filter rule 10 state 'related'
set firewall ipv4 forward filter rule 20 action 'accept'
set firewall ipv4 forward filter rule 20 state 'established'
set firewall ipv4 forward filter rule 20 state 'related'
set firewall ipv4 forward filter rule 999999 action 'drop'
set firewall ipv4 input filter default-action 'accept'
set firewall ipv4 output filter default-action 'accept'
set firewall ipv4 prerouting raw default-action 'accept'
set firewall ipv6 forward filter default-action 'drop'
set firewall ipv6 input filter default-action 'drop'
set firewall ipv6 output filter default-action 'drop'
set firewall ipv6 prerouting raw default-action 'drop'

This particular setup only have two interfaces where eth0 is MGMT and eth1 is PROD (hence why the software flowtable only have a single interface, if/when I add additional interfaces for PROD then this will be added to the existing flowtable aswell).

Hey! It’s possible I did a bad job explaining the problem. The drops show up in the logs like this:

Aug 22 18:57:30 kernel: [STATE-POLICY-INV-D]IN= OUT=eth3 MAC=01:80:c2:00:00:00:52:54:00:08:0c:f8:00:26
Aug 22 18:57:30 kernel: [STATE-POLICY-INV-D]IN= OUT=eth0 MAC=01:80:c2:00:00:00:00:60:e0:96:6d:65:00:26
Aug 22 18:57:32 kernel: [STATE-POLICY-INV-D]IN= OUT=eth3 MAC=01:80:c2:00:00:00:52:54:00:08:0c:f8:00:26
Aug 22 18:57:32 kernel: [STATE-POLICY-INV-D]IN= OUT=eth0 MAC=01:80:c2:00:00:00:00:60:e0:96:6d:65:00:26
Aug 22 18:57:34 kernel: [STATE-POLICY-INV-D]IN= OUT=eth3 MAC=01:80:c2:00:00:00:52:54:00:08:0c:f8:00:26
Aug 22 18:57:34 kernel: [STATE-POLICY-INV-D]IN= OUT=eth0 MAC=01:80:c2:00:00:00:00:60:e0:96:6d:65:00:26
Aug 22 18:57:36 kernel: [STATE-POLICY-INV-D]IN= OUT=eth3 MAC=01:80:c2:00:00:00:52:54:00:08:0c:f8:00:26
Aug 22 18:57:36 kernel: [STATE-POLICY-INV-D]IN= OUT=eth0 MAC=01:80:c2:00:00:00:00:60:e0:96:6d:65:00:26
Aug 22 18:57:38 kernel: [STATE-POLICY-INV-D]IN= OUT=eth3 MAC=01:80:c2:00:00:00:52:54:00:08:0c:f8:00:26
Aug 22 18:57:38 kernel: [STATE-POLICY-INV-D]IN= OUT=eth0 MAC=01:80:c2:00:00:00:00:60:e0:96:6d:65:00:26
Aug 22 18:57:40 kernel: [STATE-POLICY-INV-D]IN= OUT=eth3 MAC=01:80:c2:00:00:00:52:54:00:08:0c:f8:00:26

This user has the same issue with STP, and apparently also RADIUS (which I’m not familiar with): ⚓ T7150 Traffic marked invalid on bridge member interfaces

So what I’m talking about is the bridge firewall specifically, not just the firewall in general. I’d imagine LLDP is subject to the same breakage, I’ll test that tomorrow.

Essentially I’m not reporting a bug, but hoping to open a discussion about how bridge is firewalling currently implemented, with no apparent way to turn it off (unless not having a firewall global-options apply-to-bridged-traffic config element is already meant to turn it off, but doesn’t, in which case I’d indeed go ahead and create a ticket).

I can still try to post the relevant bits of my configuration if that helps build context for the specific STP issue in case you want to reproduce it, but TL;DR is I’d like a way to actually disable bridge firewalling and have a bridge behave like a bridge.

Which is why it would be handy if you could paste your current config through “show config commands” since I dont see the malfunction that you describe.

Of course you’re not seeing what I describe because there’s no bridge in the config you posted, but maybe I have tunnel vision from tinkering too much in the last couple days :')

My config is too much of a wall of text to post here so I’ll attach it as a file. The topology is pretty much VyOS in a VM, with 3 physical NICs passed through and an additional virtio-net interface plugged into a bridge on the hypervisor (Debian running libvirt).

One of the physical ports is plugged into a managed switch, one of them has a WiFi AP attached to it, and the third connects to a DSL modem. Everything except for the port with the modem is wrapped in a single VLAN-aware bridge with STP enabled, which is receiving BPDUs (and using) from the switch just fine, but drops its own BPDUs because of the bridge firewall that can’t be fixed without manual nftables commands.

I am proposing there should be a way to disable the bridge firewall instead of adding yet another very specific accept-invalid setting for STP, like with ARP, DHCP, and whatever else is broken.

Edit: Maybe I wasn’t clear about this, but the bridge itself works and traffic flows fine, it’s just STP itself that does not work because of the firewall. There is an intentional L2 loop in this setup (VyOS bridge is attached to switch, switch is attached to hypervisor bridge, hypervisor bridge is attached to VyOS bridge), and working STP would be nice.

vyos_cmds.txt (31.7 KB)

This actually is a bug, or at the very minimum incorrect behavior. The issue stems from the <defaultValue> schema that VyOS uses. It is used to make sure a value is inserted for the default firewall global options. For instance, if you just configure a blank firewall config::

vyos@vyos# set firewall 
vyos@vyos# commit

vyos@vyos# show | commands 
set firewall

It will insert all of these values:

Default Firewall Config
{
    "ipv6": {
        "prerouting": {
            "raw": {
                "default_action": "accept"
            }
        },
        "output": {
            "raw": {
                "default_action": "accept"
            },
            "filter": {
                "default_action": "accept"
            }
        },
        "input": {
            "filter": {
                "default_action": "accept"
            }
        },
        "forward": {
            "filter": {
                "default_action": "accept"
            }
        }
    },
    "ipv4": {
        "prerouting": {
            "raw": {
                "default_action": "accept"
            }
        },
        "output": {
            "raw": {
                "default_action": "accept"
            },
            "filter": {
                "default_action": "accept"
            }
        },
        "input": {
            "filter": {
                "default_action": "accept"
            }
        },
        "forward": {
            "filter": {
                "default_action": "accept"
            }
        }
    },
    "bridge": {
        "prerouting": {
            "filter": {
                "default_action": "accept"
            }
        },
        "output": {
            "filter": {
                "default_action": "accept"
            }
        },
        "input": {
            "filter": {
                "default_action": "accept"
            }
        },
        "forward": {
            "filter": {
                "default_action": "accept"
            }
        }
    },
    "global_options": {
        "ipv6_src_route": "disable",
        "ipv6_source_validation": "disable",
        "ipv6_receive_redirects": "disable",
        "twa_hazards_protection": "disable",
        "timeout": {
            "udp": {
                "stream": "180",
                "other": "30"
            },
            "tcp": {
                "time_wait": "120",
                "syn_sent": "120",
                "syn_recv": "60",
                "last_ack": "30",
                "fin_wait": "120",
                "established": "432000",
                "close": "10",
                "close_wait": "60"
            },
            "other": "600",
            "icmp": "30"
        },
        "syn_cookies": "enable",
        "source_validation": "disable",
        "send_redirects": "enable",
        "resolver_interval": "300",
        "receive_redirects": "disable",
        "log_martians": "enable",
        "ip_src_route": "disable",
        "geoip": {
            "provider": "db-ip"
        },
        "directed_broadcast": "enable",
        "broadcast_ping": "disable",
        "all_ping": "enable"
    },
    "group_resync": false,
    "geoip_sets": {
        "name": [],
        "ipv6_name": []
    },
    "geoip_updated": false,
    "policy": {},
    "ip_fqdn": {},
    "ip6_fqdn": {},
    "first_install": true
}

The specific issue here being the default_action for all of the chains in the bridge firewall:

Bridge Firewall portion:
    },
    "bridge": {
        "prerouting": {
            "filter": {
                "default_action": "accept"
            }
        },
        "output": {
            "filter": {
                "default_action": "accept"
            }
        },
        "input": {
            "filter": {
                "default_action": "accept"
            }
        },
        "forward": {
            "filter": {
                "default_action": "accept"
            }
        }
    },

The Jinja2 template checks if bridge is present, and won’t configure the bridge firewall if it isn’t present. This is the expected behavior that you have, if you’re not using the bridge firewall it shouldn’t be configured (e.g.{% if bridge is vyos_defined %}), but the <defaultValue> overrides it.

Jinja2 template:
## Bridge Firewall
{% if first_install is not vyos_defined %}
delete table bridge vyos_filter
{% endif %}
table bridge vyos_filter {
{% if bridge is vyos_defined %}

This is being tracked in the below task. @jestabro is working on a scalable solution, since this impacts quite a bit of places, including a lot of the FRR implementation. I should also note this also impacts creating additional processing in a firewall config. For instance, if you only have the input chain configured, it shouldn’t create the forward chain, but that <defaultValue> piece forces it’s creation currently. This will impact users who want to only secure VyOS itself, while not impacting forwarded traffic, such as those that run VyOS in a service provider capactiy.

2 Likes

Thanks for clearing that up, I was too focused on the whole bridge thing to realize this was a general issue for all chains.

Here’s a temporary solution for you. Add this to /config/scripts/vyos-preconfig-bootup.script:

if grep -qF '{% if bridge is vyos_defined %}' /usr/share/vyos/templates/firewall/nftables.j2; then
    sudo sed -i 's/{% if bridge is vyos_defined %}/{% if bridge is not vyos_defined %}/' \
        /usr/share/vyos/templates/firewall/nftables.j2
    sudo systemctl restart vyos-configd.service
    logger -t vyos-preconfig "Patched nftables.j2 (disable bridge firewall)"
fi

This will persist across image upgrades. You can also run that block directly in your running VyOS instance, and then make any arbitrary change to the firewall config and the bridge table chains will be removed. Something like:

set firewall ipv4 name DMZ-LOCAL description 'DMZ -> LOCAL - TEMP'

You can revert that back immediately. You can verify the table was purged with this:

vyos@vyos# sudo nft list table bridge vyos_filter
table bridge vyos_filter {
}

@pngu or anyone interested

I made a v2 of the above script. This is a little better since it leaves room to configure the bridge firewall later if desired.

A quick explanation. All of the current lines like this:

"{%     if ipv4.forward is vyos_defined %}"

Are replaced with a line with additional checks:

"{%     if ipv4.forward is vyos_defined and ((ipv4.forward.filter | length) > 1 or ipv4.forward.filter.default_action == 'drop') %}"

The length check is checking if more than default-action is configured, which is the line that causes a lot of the issues. If only default-action exists, then the length is 1, so that check fails. It also checks if default-action is set to “drop”, which means the user explicitly configured that line since it differs from the default-action of “accept”. So even though the length is still 1, it shows the user intended to create that config, rather than just being part of the <defaultValue> schema.

Script
restart_configd=0

replace_line() {
    file="$1"
    old="$2"
    new="$3"
    message="$4"

    if grep -qF "$old" "$file"; then
        sed -i "s#$old#$new#" "$file"
        logger -t vyos-preconfig "$message"
        restart_configd=1
    fi
}

replace_line \
    "/usr/share/vyos/templates/firewall/nftables.j2" \
    "{%     if ipv4.forward is vyos_defined %}" \
    "{%     if ipv4.forward is vyos_defined and ((ipv4.forward.filter | length) > 1 or ipv4.forward.filter.default_action == 'drop') %}" \
    "Patched nftables.j2 (disable ipv4.forward firewall)"

replace_line \
    "/usr/share/vyos/templates/firewall/nftables.j2" \
    "{%     if ipv4.output is vyos_defined %}" \
    "{%     if ipv4.output is vyos_defined and ((ipv4.output.filter | length) > 1 or ipv4.output.filter.default_action == 'drop') %}" \
    "Patched nftables.j2 (disable ipv4.output firewall)"

replace_line \
    "/usr/share/vyos/templates/firewall/nftables.j2" \
    "{%     if ipv4.input is vyos_defined %}" \
    "{%     if ipv4.input is vyos_defined and ((ipv4.input.filter | length) > 1 or ipv4.input.filter.default_action == 'drop') %}" \
    "Patched nftables.j2 (disable ipv4.input firewall)"
    
replace_line \
    "/usr/share/vyos/templates/firewall/nftables.j2" \
    "{%     if ipv6.forward is vyos_defined %}" \
    "{%     if ipv6.forward is vyos_defined and ((ipv6.forward.filter | length) > 1 or ipv6.forward.filter.default_action == 'drop') %}" \
    "Patched nftables.j2 (disable ipv6.forward firewall)"

replace_line \
    "/usr/share/vyos/templates/firewall/nftables.j2" \
    "{%     if ipv6.output is vyos_defined %}" \
    "{%     if ipv6.output is vyos_defined and ((ipv6.output.filter | length) > 1 or ipv6.output.filter.default_action == 'drop') %}" \
    "Patched nftables.j2 (disable ipv6.output firewall)"

replace_line \
    "/usr/share/vyos/templates/firewall/nftables.j2" \
    "{%     if ipv6.input is vyos_defined %}" \
    "{%     if ipv6.input is vyos_defined and ((ipv6.input.filter | length) > 1 or ipv6.input.filter.default_action == 'drop') %}" \
    "Patched nftables.j2 (disable ipv6.input firewall)"

replace_line \
    "/usr/share/vyos/templates/firewall/nftables.j2" \
    "{%     if bridge.forward is vyos_defined %}" \
    "{%     if bridge.forward is vyos_defined and ((bridge.forward.filter | length) > 1 or bridge.forward.filter.default_action == 'drop') %}" \
    "Patched nftables.j2 (disable bridge.forward firewall)"

replace_line \
    "/usr/share/vyos/templates/firewall/nftables.j2" \
    "{%     if bridge.input is vyos_defined %}" \
    "{%     if bridge.input is vyos_defined and ((bridge.input.filter | length) > 1 or bridge.input.filter.default_action == 'drop') %}" \
    "Patched nftables.j2 (disable bridge.input firewall)"

replace_line \
    "/usr/share/vyos/templates/firewall/nftables.j2" \
    "{%     if bridge.output is vyos_defined %}" \
    "{%     if bridge.output is vyos_defined and ((bridge.output.filter | length) > 1 or bridge.output.filter.default_action == 'drop') %}" \
    "Patched nftables.j2 (disable bridge.output firewall)"

if [ "$restart_configd" -eq 1 ]; then
    logger -t vyos-preconfig "Restarting vyos-configd.service"
    systemctl restart vyos-configd.service
fi
nftables config before
vyos@vyos# set firewall 
vyos@vyos# commit

vyos@vyos# sudo nft list table bridge vyos_filter
table bridge vyos_filter {
        chain VYOS_FORWARD_filter {
                type filter hook forward priority filter; policy accept;
                counter packets 0 bytes 0 accept comment "FWD-filter default-action accept"
        }

        chain VYOS_INPUT_filter {
                type filter hook input priority filter; policy accept;
                counter packets 0 bytes 0 accept comment "INP-filter default-action accept"
        }

        chain VYOS_OUTPUT_filter {
                type filter hook output priority filter; policy accept;
                counter packets 0 bytes 0 accept comment "OUT-filter default-action accept"
        }

        chain VYOS_PREROUTING_filter {
                type filter hook prerouting priority filter; policy accept;
                counter packets 0 bytes 0 accept comment "PRE-filter default-action accept"
        }
}

vyos@vyos# sudo nft list table ip vyos_filter
table ip vyos_filter {
        chain VYOS_FORWARD_filter {
                type filter hook forward priority filter; policy accept;
                counter packets 0 bytes 0 accept comment "FWD-filter default-action accept"
        }

        chain VYOS_INPUT_filter {
                type filter hook input priority filter; policy accept;
                counter packets 0 bytes 0 accept comment "INP-filter default-action accept"
        }

        chain VYOS_OUTPUT_raw {
                type filter hook output priority raw; policy accept;
                counter packets 0 bytes 0 accept comment "OUT-raw default-action accept"
        }

        chain VYOS_OUTPUT_filter {
                type filter hook output priority filter; policy accept;
                counter packets 0 bytes 0 accept comment "OUT-filter default-action accept"
        }

        chain VYOS_PREROUTING_raw {
                type filter hook prerouting priority raw; policy accept;
                counter packets 0 bytes 0 accept comment "PRE-raw default-action accept"
        }

        chain VYOS_FRAG_MARK {
                type filter hook prerouting priority -450; policy accept;
                ip frag-off & 0x3fff != 0x0 meta mark set 0x000ffff1 return
        }
}

vyos@vyos# sudo nft list table ip6 vyos_filter
table ip6 vyos_filter {
        chain VYOS_IPV6_FORWARD_filter {
                type filter hook forward priority filter; policy accept;
                counter packets 0 bytes 0 accept comment "FWD-filter default-action accept"
        }

        chain VYOS_IPV6_INPUT_filter {
                type filter hook input priority filter; policy accept;
                counter packets 0 bytes 0 accept comment "INP-filter default-action accept"
        }

        chain VYOS_IPV6_OUTPUT_raw {
                type filter hook output priority raw; policy accept;
                counter packets 0 bytes 0 accept comment "OUT-raw default-action accept"
        }

        chain VYOS_IPV6_OUTPUT_filter {
                type filter hook output priority filter; policy accept;
                counter packets 0 bytes 0 accept comment "OUT-filter default-action accept"
        }

        chain VYOS_IPV6_PREROUTING_raw {
                type filter hook prerouting priority raw; policy accept;
                counter packets 0 bytes 0 accept comment "PRE-raw default-action accept"
        }

        chain VYOS_FRAG6_MARK {
                type filter hook prerouting priority -450; policy accept;
                exthdr frag exists meta mark set 0x000ffff1 return
        }
}
nftables config after
vyos@vyos# set firewall 
vyos@vyos# commit

vyos@vyos# sudo nft list table bridge vyos_filter
table bridge vyos_filter {
}

vyos@vyos# sudo nft list table ip vyos_filter
table ip vyos_filter {
        chain VYOS_FRAG_MARK {
                type filter hook prerouting priority -450; policy accept;
                ip frag-off & 0x3fff != 0x0 meta mark set 0x000ffff1 return
        }
}

vyos@vyos# sudo nft list table ip6 vyos_filter
table ip6 vyos_filter {
        chain VYOS_FRAG6_MARK {
                type filter hook prerouting priority -450; policy accept;
                exthdr frag exists meta mark set 0x000ffff1 return
        }
}

If I configure anything other than default-action “accept”, you’ll see the chain is created (since the length is now 2):

vyos@vyos# set firewall ipv4 input filter description test
vyos@vyos# commit

vyos@vyos# show firewall | commands 
set ipv4 input filter description 'test'

vyos@vyos# sudo nft list table ip vyos_filter
table ip vyos_filter {
        chain VYOS_INPUT_filter {
                type filter hook input priority filter; policy accept;
                counter packets 0 bytes 0 accept comment "INP-filter default-action accept"
        }

        chain VYOS_FRAG_MARK {
                type filter hook prerouting priority -450; policy accept;
                ip frag-off & 0x3fff != 0x0 meta mark set 0x000ffff1 return
        }
}

Additionally, if I configure a chain with only default-action set, but it is set to “drop”, it also creates the chain.

vyos@vyos# set firewall ipv4 forward filter default-action drop 
vyos@vyos# commit

vyos@vyos# show firewall | commands 
set ipv4 forward filter default-action 'drop'
set ipv4 input filter description 'test'

vyos@vyos# sudo nft list table ip vyos_filter
table ip vyos_filter {
        chain VYOS_FORWARD_filter {
                type filter hook forward priority filter; policy accept;
                counter packets 0 bytes 0 drop comment "FWD-filter default-action drop"
        }

        chain VYOS_INPUT_filter {
                type filter hook input priority filter; policy accept;
                counter packets 0 bytes 0 accept comment "INP-filter default-action accept"
        }

        chain VYOS_FRAG_MARK {
                type filter hook prerouting priority -450; policy accept;
                ip frag-off & 0x3fff != 0x0 meta mark set 0x000ffff1 return
        }
}
3 Likes

Thank you, that works perfectly. I added the following lines to apply the same logic to the prerouting hook for the bridge family:

replace_line \
    "/usr/share/vyos/templates/firewall/nftables.j2" \
    "{%     if bridge.prerouting is vyos_defined %}" \
    "{%     if bridge.prerouting is vyos_defined and ((bridge.prerouting.filter | length) > 1 or bridge.prerouting.filter.default_action == 'drop') %}" \
    "Patched nftables.j2 (disable bridge.prerouting firewall)"

I also tested LLDP before these changes and it turns out it works without having to modify any firewall rules (in my case anyway, since I’m not using XDP, Flowtables or any other stuff that might hook in on L2 traffic). I guess that’s because in-kernel STP uses the MAC of the bridge itself (as opposed to that of individual ports) as source address when transmitting BPDUs (which is correct behaviour) and that confuses nf_conntrack_bridge, because it sees multiple new “connections” with the same source and destination addresses trying to leave multiple interfaces (one for each bridge port) whereas lldpd uses the MAC of each interface it’s configured to transmit LLDPDUs on (which is also correct behaviour but doesn’t confuse conntrack). So maybe for those users that do want to filter bridge traffic, perhaps assigning different conntrack zones to member interfaces of a bridge would allow us to get rid of all the accept-invalid workarounds and have more correct behaviour.

Thanks again :slight_smile:

1 Like