1

I have a working nftables rule-set. However it is very long, and has a lot of repeated code:

  • Exact (just a few characters different) duplicate for ip4 and ip6.
  • Chains of rules, that branch into near identical branches. I feel that some boolean logic would help here.

How can I reduce the repeated code, to make this rule-set more concise?

I was trying various things then realised I can do it like below, but it makes the programmer in me feel dirty. It has too much repeated code.

#!/usr/sbin/nft -f

table ip vnc_table {};
table ip6 vnc_table {};
flush table ip vnc_table;
flush table ip6 vnc_table;

table ip vnc_table {

    # 3 near identical sets
    set richardports {
        type inet_service;
        flags interval;
        elements = { 5910-5919 };
    }
    set henryports {
        type inet_service;
        flags interval;
        elements = { 5920-5929 };
    }
    set sholaports {
        type inet_service;
        flags interval;
        elements = { 5930-5939 };
    }

    chain output {
        type filter hook output priority 0; policy accept;
        ip daddr 127.0.0.1 jump localhost;
    }

    chain localhost {
        tcp dport @richardports jump richard_chain;
        tcp dport @henryports   jump henry_chain;
        tcp dport @sholaports   jump shola_chain;
    }

    # 3 near identical chains
    chain richard_chain {
        skuid "richard" accept;
        reject;
    }
    chain henry_chain {
        skuid "henry" accept;
        reject;
    }
    chain shola_chain {
        skuid "shola" accept;
        reject;
    }

}

#then we do it all again for ip6
table ip6 vnc_table {
    set richardports {
        type inet_service;
        flags interval;
        elements = { 5910-5919 };
    }
    set henryports {
        type inet_service;
        flags interval;
        elements = { 5920-5929 };
    }
    set sholaports {
        type inet_service;
        flags interval;
        elements = { 5930-5939 };
    }

    chain output {
        type filter hook output priority 0; policy accept;
        ip6 daddr ::1 jump localhost;
    }

    chain localhost {
        tcp dport @richardports jump richard_chain;
        tcp dport @henryports   jump henry_chain;
        tcp dport @sholaports   jump shola_chain;
    }

    chain richard_chain {
        skuid "richard" accept;
        reject;
    }
    chain henry_chain {
        skuid "henry" accept;
        reject;
    }
    chain shola_chain {
        skuid "shola" accept;
        reject;
    } 
}
1

Joining IPv4 and IPv6 together

Join tables of families ip and ip6 into a single table of family inet:

  • remove the whole ip6 vnc_table table

  • change the ip vnc_table table into inet vnc_table table

    Family inet can handle both IPv4 and IPv6 at the same time, and can still accept specific IPv4 or IPv6 rules when needed. So replace:

    table ip vnc_table {
    

    with:

    table inet vnc_table {
    
  • adapt the flushes before

    While answering this question, I discovered that flush table isn't adequate to be able to reload the rules (I had to amend my answer on this topic): as documented, it will "Flush all chains and rules of the specified table." but will not delete those objects themselves, leading to leftover or conflicting objects. delete table should be used instead:

    delete table inet vnc_table
    

    and of course ip vnc_table and ip6 vnc_table tables should be deleted manually once:

    # nft delete table ip vnc_table
    # nft delete table ip6 vnc_table
    
  • add the now missing ip6 rule, so that both IPv4 and IPv6 localhost will match

    nft add inet vnc_table output ip6 daddr ::1 jump localhost
    

Bleeding edge required to do better

To go further the sets have to be organised differently but there's a bug/limitation which has been lifted only in nftables 0.9.4 released on 2020-04-01 along with libnftnl 1.1.6, while also requiring kernel 5.6:

  • Support for ranges in concatenations (requires Linux kernel >= 5.6),

Without these versions, nftables can't accept a concatenation which includes a range (here: the ports range). Using only kernel 5.5 rather than 5.6 led to a segmentation fault when handling the ruleset below (this is arguably a bug): kernel too is required.

So two sets are made: the first is to tell what port ranges are filtered and need special treatment. The second (still having to reinclude the previous port ranges) is a concatenation of port ranges and user ids. Those two properties will be checked against the packet: if a port + user association exists, it will be accepted, else the packet is rejected for those filtered ports.

Result:

table inet vnc_table
delete table inet vnc_table

table inet vnc_table {
    set filteredport {
        type inet_service
        flags interval
        elements = { 5910-5919, 5920-5929, 5930-5939 }
    }

    set portuser {
        type inet_service . uid
        flags interval
        elements = {
                 5910-5919 . "richard",
                 5920-5929 . "henry",
                 5930-5939 . "shola"
        }
    }

    chain output {
        type filter hook output priority filter; policy accept;
        ip daddr 127.0.0.1 jump localhost
        ip6 daddr ::1 jump localhost
    }

    chain localhost {
        tcp dport @filteredport jump portuser_chain
    }

    chain portuser_chain {
        tcp dport . meta skuid @portuser accept
        reject with tcp reset
    }
}

Notes:

  • the uid type in a named set is still not documented in the man page, but has been existing for a very long time along many other types. Source code defining it: here and there.

  • replacing reject (which sends ICMP) with reject with tcp reset (which sends a specific TCP RST) speeds up the port rejection detection on the client (and there's no need to specify it's TCP before, the check is added implicitly, matching only for TCP).

3
  • I did the first bit. Kernel too old for 2nd bit. Can you reference any info about delete vs flush. Everything that I read says that delete does a delete, and flush does a flush. I is all a bit MS help "see help for help. Press exit to exit. etc." – ctrl-alt-delor Apr 6 '20 at 17:06
  • it's like with iptables: you can flush the rules with iptables -F but must also delete the now empty chains with iptables -X (except nftables does both with delete) – A.B Apr 6 '20 at 17:07
  • 1
    here's a reference: wiki.nftables.org/wiki-nftables/index.php/… vs wiki.nftables.org/wiki-nftables/index.php/… . For delete to work as expected, kernel >= 3.18 required. – A.B Apr 6 '20 at 17:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.