firewalld/NFT tables vs. address families

I’m in the process of learning how to create advanced firewall rules and am having a hard time understanding the basic structure of the tables used in the NFT backend.

Among others, there are these three address families (see man NFT(8)):

ADDRESS FAMILIES
Address families determine the type of packets which are processed. For each address family, the kernel contains so called hooks at specific stages of the packet processing paths, which invoke nftables if rules for these hooks exist.

   ip       IPv4 address family.
   ip6      IPv6 address family.
   inet     Internet (IPv4/IPv6) address family.

There are exactly three tables that match these address families:

# nft list tables
table inet firewalld
table ip firewalld
table ip6 firewalld

Question #1: Since when do we define table names with spaces?!
Question #2: Since when do we not have to use quotation marks around names with spaces in between?!

Anyway. So one would assume that table ip firewalld has the chains and rules for IPv4, table ip6 firewalld has the chains and rules for IPv6, and table inet firewalld has the chains and rules that apply to both IPv4 and IPv6, correct? Wrong!

What you find is that all rules, even if they apply only to IPv4 or only to IPv6, appear in the table inet firewalld. The other two tables are basically unused.

Worse, the table ip firewalld has chains that apply only to IPv6:

    chain nat_PRE_policy_allow-host-ipv6 {
            jump nat_PRE_policy_allow-host-ipv6_pre
            jump nat_PRE_policy_allow-host-ipv6_log
            jump nat_PRE_policy_allow-host-ipv6_deny
            jump nat_PRE_policy_allow-host-ipv6_allow
            jump nat_PRE_policy_allow-host-ipv6_post
    }

Question #3: Why has that been set up like this?
Question #4: What is the logic behind it?

Question #5: How would one define/insert a low priority rule that does what this old iptables rule has done: Drop all outbound traffic that has not explictely been allowed?

$ iptables -P OUTPUT DROP

Where do you see spaces?

Legacy. Historically firewalld used separate rules/tables for IPv4 and IPv6, later it was changed to use common inet table.

If the question is “how you do it using firewalld” - there are multiple possibilities.

a) use direct rules, it still works
b) create policy with ingress zone HOST and egress zone ANY with suitable target

Sorry, I was wrong, it won’t work together with nft for whitelisting.

Where do you see spaces?

inet firewalld
    ^here

Note that you don’t quote the blank:
# nft list table inet firewalld

a) use direct rules, it still works

In this thread you wrote it can’t be done with firewalld:

No. You can set firewalld OUTPUT policy target to DROP, but this still configures “last” rule in set of chains firewalld creates, netfilter policy remains ACCEPT

(scroll to the last post in thread)

b) create policy with ingress zone HOST and egress zone ANY with suitable target

Not sure I want to go that far low level.

I mean it is straightforward to insert generic ALLOW rules:

    chain filter_INPUT {
            ...
            iifname "lo" accept

So I was hoping one can do something similar for OUTPUT using oifname "eth0" drop as the very last (catchall) rule:

    chain filter_OUTPUT {
           type filter hook output priority filter + 10; policy accept;
           oifname "lo" accept
           ...
           jump filter_OUTPUT_POLICIES_post
           oifname "eth0" drop
    }

What’s wrong with this approach?

Table name is firewalld - where do you see a blank here?

Actually, that is pretty high level - certainly for more than individual nftables rules.

Anything you added here will be lost when firewalld is reloaded.

I understood your question about where I see spaces when you first raised it. But the command nft list tables does list three entries or tables:

# nft list tables
table inet firewalld
table ip firewalld
table ip6 firewalld

If there was just one table named firewalld why would nft list it three times?
So logic tells us that the address family has to be considered as part of the table name. :grin:

My personal impression is that NFT implements a few very good ideas, but on the other hand, they didn’t think it all the way through in some places. Perhaps it is due to my lack of understanding that I see it that way.

BTW, I tried the following and immediately got kicked out of PuTTY because there’s no rule for outgoing SSH traffic:

# nft add rule inet firewalld filter_OUTPUT_POLICIES_post 'oifname "eth0" drop'

Logging in on the console I could see that the rule indeed was applied and active. Too bad this can’t be made persistent. Seems like I have to continue with direct rules and the iptables backend.

Because there are three tables belonging to different address families. Address family is not part of table name.

Three different tables with identical name, ok :thinking:

BTW, I found two different ways to make a modified ruleset persistent:

  1. arvidjaar wrote:

Or one can simply execute again “firewall-cmd --runtime-to-permanent” to save complete current configuration including newly added rules as permanent configuration.

  1. I just tried:
# systemctl stop firewalld
# systemctl mask firewalld
# zypper ar -f https://download.opensuse.org/repositories/network/15.4/ network
# zypper in nftables.service
# nft list ruleset > /etc/nftables.conf
# systemctl daemon-reload
# systemctl start nftables
# systemctl enable nftables

My understanding is that the main purpose of the /usr/sbin/nftables-service program is to read in the saved ruleset from /etc/nftables.conf on service start and write it back to disk on service termination.

Do not quote what I said out of context. This was about firewalld configuration, not about arbitrary manual additions to tables, managed by firewalld.

You can add your own tables and firewalld will not touch them when reloading.

If are happy to maintain the whole rule sets manually, fine. Your question was about firewalld.

One would wonder why programming languages were invented when one could write anything in assembler (and even this is not needed, any program is just a sequence of zeroes and ones).

No, certainly not. But if firewalld and/or the NFT backend do not offer a way to make changes in the ruleset done with nft persistent what alternatives do you have?

Stay inside of firewalld and use policies which were invented exactly for such use case.

Correction: If you don’t want to start from scratch and continue to use the firewalld ruleset with nftables:

# nft list ruleset > /etc/nftables.fwd
# systemctl stop firewalld
# systemctl mask firewalld
# zypper ar -f https://download.opensuse.org/repositories/network/15.4/ network
# zypper in nftables.service
# cp /etc/nftables.fwd /var/lib/nftables/auto.conf
# systemctl daemon-reload
# systemctl start nftables
# systemctl enable nftables

Excerpt from /usr/share/doc/packages/nftables-service/README.md:

nftables auto support

If you want to preserve the current ruleset on shutdown, e.g. because you are
filling named sets from tools like fail2ban, then you can create
/var/lib/nftables/auto.conf.

The start and reload actions will prefer this file, if the file is not empty.
The stop action of the service will save the current ruleset to the file if
it is non-empty.

The NFT developers haven’t defined table names with spaces …

  • For each NFT family – inet / ip / ip6 – there’s a table named “firewalld” …
 # nft list tables
table inet firewalld
table ip firewalld
table ip6 firewalld
 # nft list tables inet
table inet firewalld
 # nft list tables ip
table ip firewalld
 # nft list tables ip6
table ip6 firewalld
 #

Looking into the contents of one of the NFT tables (the one for the family “inet”) which happens to be named “firewalld” –

 # nft list table inet firewalld
table inet firewalld {
        chain raw_PREROUTING {
                type filter hook prerouting priority raw + 10; policy accept;
                icmpv6 type { nd-router-advert, nd-neighbor-solicit } accept
                meta nfproto ipv6 fib saddr . iif oif missing drop
        }

        chain mangle_PREROUTING {
                type filter hook prerouting priority mangle + 10; policy accept;
                jump mangle_PREROUTING_POLICIES_pre
                jump mangle_PREROUTING_ZONES
                jump mangle_PREROUTING_POLICIES_post
        }

        chain mangle_PREROUTING_POLICIES_pre {
                jump mangle_PRE_policy_allow-host-ipv6
        }

        chain mangle_PREROUTING_ZONES {
                iifname "eth0" goto mangle_PRE_trusted
                iifname "docker0" goto mangle_PRE_docker
                goto mangle_PRE_trusted
        }

        chain mangle_PREROUTING_POLICIES_post {
        }

        chain filter_INPUT {
                type filter hook input priority filter + 10; policy accept;
                ct state { established, related } accept
                ct status dnat accept
                iifname "lo" accept
                jump filter_INPUT_POLICIES_pre
                jump filter_INPUT_ZONES
                jump filter_INPUT_POLICIES_post
                ct state { invalid } drop
                reject with icmpx type admin-prohibited
        }

        chain filter_FORWARD {
                type filter hook forward priority filter + 10; policy accept;
                ct state { established, related } accept
                ct status dnat accept
                iifname "lo" accept
                ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 type addr-unreachable
                jump filter_FORWARD_POLICIES_pre
                jump filter_FORWARD_IN_ZONES
                jump filter_FORWARD_OUT_ZONES
                jump filter_FORWARD_POLICIES_post
                ct state { invalid } drop
                reject with icmpx type admin-prohibited
        }

        chain filter_OUTPUT {
                type filter hook output priority filter + 10; policy accept;
                oifname "lo" accept
                ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 type addr-unreachable
                jump filter_OUTPUT_POLICIES_pre
                jump filter_OUTPUT_POLICIES_post
        }

        chain filter_INPUT_POLICIES_pre {
                jump filter_IN_policy_allow-host-ipv6
        }

        chain filter_INPUT_ZONES {
                iifname "eth0" goto filter_IN_trusted
                iifname "docker0" goto filter_IN_docker
                goto filter_IN_trusted
        }

        chain filter_INPUT_POLICIES_post {
        }

        chain filter_FORWARD_POLICIES_pre {
        }

        chain filter_FORWARD_IN_ZONES {
                iifname "eth0" goto filter_FWDI_trusted
                iifname "docker0" goto filter_FWDI_docker
                goto filter_FWDI_trusted
        }

        chain filter_FORWARD_OUT_ZONES {
                oifname "eth0" goto filter_FWDO_trusted
                oifname "docker0" goto filter_FWDO_docker
                goto filter_FWDO_trusted
        }

        chain filter_FORWARD_POLICIES_post {
        }

        chain filter_OUTPUT_POLICIES_pre {
        }

        chain filter_OUTPUT_POLICIES_post {
        }

        chain filter_IN_docker {
                jump filter_IN_docker_pre
                jump filter_IN_docker_log
                jump filter_IN_docker_deny
                jump filter_IN_docker_allow
                jump filter_IN_docker_post
                accept
        }

        chain filter_IN_docker_pre {
        }

        chain filter_IN_docker_log {
        }

        chain filter_IN_docker_deny {
        }

        chain filter_IN_docker_allow {
        }

        chain filter_IN_docker_post {
        }

        chain filter_FWDO_docker {
                jump filter_FWDO_docker_pre
                jump filter_FWDO_docker_log
                jump filter_FWDO_docker_deny
                jump filter_FWDO_docker_allow
                jump filter_FWDO_docker_post
                accept
        }

        chain filter_FWDO_docker_pre {
        }

        chain filter_FWDO_docker_log {
        }

        chain filter_FWDO_docker_deny {
        }

        chain filter_FWDO_docker_allow {
        }

        chain filter_FWDO_docker_post {
        }

        chain filter_FWDI_docker {
                jump filter_FWDI_docker_pre
                jump filter_FWDI_docker_log
                jump filter_FWDI_docker_deny
                jump filter_FWDI_docker_allow
                jump filter_FWDI_docker_post
                accept
        }

        chain filter_FWDI_docker_pre {
        }

        chain filter_FWDI_docker_log {
        }

        chain filter_FWDI_docker_deny {
        }

        chain filter_FWDI_docker_allow {
        }

        chain filter_FWDI_docker_post {
        }

        chain mangle_PRE_docker {
                jump mangle_PRE_docker_pre
                jump mangle_PRE_docker_log
                jump mangle_PRE_docker_deny
                jump mangle_PRE_docker_allow
                jump mangle_PRE_docker_post
        }

        chain mangle_PRE_docker_pre {
        }

        chain mangle_PRE_docker_log {
        }

        chain mangle_PRE_docker_deny {
        }

        chain mangle_PRE_docker_allow {
        }

        chain mangle_PRE_docker_post {
        }

        chain filter_IN_trusted {
                jump filter_IN_trusted_pre
                jump filter_IN_trusted_log
                jump filter_IN_trusted_deny
                jump filter_IN_trusted_allow
                jump filter_IN_trusted_post
                accept
        }

        chain filter_IN_trusted_pre {
        }

        chain filter_IN_trusted_log {
        }

        chain filter_IN_trusted_deny {
        }

        chain filter_IN_trusted_allow {
        }

        chain filter_IN_trusted_post {
        }

        chain filter_FWDO_trusted {
                jump filter_FWDO_trusted_pre
                jump filter_FWDO_trusted_log
                jump filter_FWDO_trusted_deny
                jump filter_FWDO_trusted_allow
                jump filter_FWDO_trusted_post
                accept
        }

        chain filter_FWDO_trusted_pre {
        }

        chain filter_FWDO_trusted_log {
        }

        chain filter_FWDO_trusted_deny {
        }

        chain filter_FWDO_trusted_allow {
        }

        chain filter_FWDO_trusted_post {
        }

        chain filter_FWDI_trusted {
                jump filter_FWDI_trusted_pre
                jump filter_FWDI_trusted_log
                jump filter_FWDI_trusted_deny
                jump filter_FWDI_trusted_allow
                jump filter_FWDI_trusted_post
                accept
        }

        chain filter_FWDI_trusted_pre {
        }

        chain filter_FWDI_trusted_log {
        }

        chain filter_FWDI_trusted_deny {
        }

        chain filter_FWDI_trusted_allow {
        }

        chain filter_FWDI_trusted_post {
        }

        chain mangle_PRE_trusted {
                jump mangle_PRE_trusted_pre
                jump mangle_PRE_trusted_log
                jump mangle_PRE_trusted_deny
                jump mangle_PRE_trusted_allow
                jump mangle_PRE_trusted_post
        }

        chain mangle_PRE_trusted_pre {
        }

        chain mangle_PRE_trusted_log {
        }

        chain mangle_PRE_trusted_deny {
        }

        chain mangle_PRE_trusted_allow {
        }

        chain mangle_PRE_trusted_post {
        }

        chain filter_IN_policy_allow-host-ipv6 {
                jump filter_IN_policy_allow-host-ipv6_pre
                jump filter_IN_policy_allow-host-ipv6_log
                jump filter_IN_policy_allow-host-ipv6_deny
                jump filter_IN_policy_allow-host-ipv6_allow
                jump filter_IN_policy_allow-host-ipv6_post
        }

        chain filter_IN_policy_allow-host-ipv6_pre {
        }

        chain filter_IN_policy_allow-host-ipv6_log {
        }

        chain filter_IN_policy_allow-host-ipv6_deny {
        }

        chain filter_IN_policy_allow-host-ipv6_allow {
                icmpv6 type nd-neighbor-advert accept
                icmpv6 type nd-neighbor-solicit accept
                icmpv6 type nd-router-advert accept
                icmpv6 type nd-redirect accept
        }

        chain filter_IN_policy_allow-host-ipv6_post {
        }

        chain mangle_PRE_policy_allow-host-ipv6 {
                jump mangle_PRE_policy_allow-host-ipv6_pre
                jump mangle_PRE_policy_allow-host-ipv6_log
                jump mangle_PRE_policy_allow-host-ipv6_deny
                jump mangle_PRE_policy_allow-host-ipv6_allow
                jump mangle_PRE_policy_allow-host-ipv6_post
        }

        chain mangle_PRE_policy_allow-host-ipv6_pre {
        }

        chain mangle_PRE_policy_allow-host-ipv6_log {
        }

        chain mangle_PRE_policy_allow-host-ipv6_deny {
        }

        chain mangle_PRE_policy_allow-host-ipv6_allow {
        }

        chain mangle_PRE_policy_allow-host-ipv6_post {
        }
}
 #

I could have chosen a table named “firewalld” which has less content but, I didn’t … :sunglasses:

The NFT developers haven’t defined any names with spaces and therefore, there’s no need to use quotation marks or, any other delimiter characters, for the strings which happen to be names …

And, those chains are empty –

 # nft list chain ip firewalld nat_PRE_policy_allow-host-ipv6
table ip firewalld {
        chain nat_PRE_policy_allow-host-ipv6 {
                jump nat_PRE_policy_allow-host-ipv6_pre
                jump nat_PRE_policy_allow-host-ipv6_log
                jump nat_PRE_policy_allow-host-ipv6_deny
                jump nat_PRE_policy_allow-host-ipv6_allow
                jump nat_PRE_policy_allow-host-ipv6_post
        }
}
 # nft list chain ip firewalld nat_PRE_policy_allow-host-ipv6_pre
table ip firewalld {
        chain nat_PRE_policy_allow-host-ipv6_pre {
        }
}
 # nft list chain ip firewalld nat_PRE_policy_allow-host-ipv6_allow
table ip firewalld {
        chain nat_PRE_policy_allow-host-ipv6_allow {
        }
}
 # nft list chain ip firewalld nat_PRE_policy_allow-host-ipv6_deny
table ip firewalld {
        chain nat_PRE_policy_allow-host-ipv6_deny {
        }
}
 # nft list chain inet firewalld nat_PRE_policy_allow-host-ipv6_deny
Error: No such file or directory; did you mean chain ‘nat_PRE_policy_allow-host-ipv6_deny’ in table ip ‘firewalld’?
list chain inet firewalld nat_PRE_policy_allow-host-ipv6_deny
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 # nft list chain ip6 firewalld nat_PRE_policy_allow-host-ipv6_deny
table ip6 firewalld {
        chain nat_PRE_policy_allow-host-ipv6_deny {
        }
}
 #

Possibly needed due to the incoming Packet processing order – <https://unix.stackexchange.com/questions/607358/packet-processing-order-in-nftables>
Further information is here – <https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains>
And, here: <https://kernelnewbies.org/nftables_examples>