#!/usr/sbin/nft -f
# You can find examples in /usr/share/nftables/.

# Clear all prior state
flush ruleset

# https://wiki.gentoo.org/wiki/Nftables#Family_netdev_and_ingress_hook
table netdev filter {
    chain ingress {
        # https://serverfault.com/questions/772195/drop-fragmented-packets-in-nftables
        # Hook priority list: https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks
        # The priority needs to be lower than -400 (NF_IP_PRI_CONNTRACK_DEFRAG) to see fragments
        type filter hook ingress devices = { {{ network_interfaces | map(attribute='name') | join(', ') }} } priority -500; policy accept;

        # Drop all fragments.
        ip frag-off & 0x1fff != 0 drop

        # Drop XMAS packets.
        tcp flags & (fin|syn|rst|psh|ack|urg) == fin|syn|rst|psh|ack|urg drop

        # Drop NULL packets.
        tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 drop

        # Drop uncommon MSS values.
        tcp flags syn tcp option maxseg size 1-535 drop
    }
}

# Basic IPv4/IPv6 stateful firewall for server/workstation.
table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        iif lo accept \
        comment "Accept any localhost traffic"

        ct state { established, related } accept \
        comment "Accept traffic originated from us"

        ct state invalid drop \
        comment "Drop invalid connections"

        tcp flags & (fin|syn|rst|ack) != syn ct state new drop \
        comment "Drop non-SYN packets"

        tcp dport 113 reject with icmpx type port-unreachable \
        comment "Reject AUTH to make it fail fast"

        iif != lo ip daddr 127.0.0.1/8 drop \
        comment "Block spoofing as localhost (IPv4)"
        iif != lo ip6 daddr ::1/128 drop \
        comment "Block spoofing as localhost (IPv6)"
{% for interface in network_interfaces %}
{% if opened_ports.tcp is sequence and opened_ports.tcp | length > 0 %}

        iif {{ interface. name }} tcp dport { {{ opened_ports.tcp | join(', ') }} } ct state new accept \
        comment "Open specified TCP ports"
{% endif %}
{% if opened_ports.udp is sequence and opened_ports.udp | length > 0 %}

        iif {{ interface.name }} udp dport { {{ opened_ports.udp | join(', ') }} } ct state new accept \
        comment "Open specified UDP ports"
{% endif %}
{% endfor %}

        jump input_icmp
        jump input_libvirt
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
        jump forward_libvirt
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }

    chain input_icmp {
        # ICMPv4

        ip protocol icmp icmp type {
            echo-reply,  # type 0
            destination-unreachable,  # type 3
            echo-request,  # type 8
            time-exceeded,  # type 11
            parameter-problem,  # type 12
        } limit rate over 1/second burst 4 packets accept \
        comment "Accept ICMP"

        # ICMPv6

        ip6 nexthdr icmpv6 icmpv6 type {
            destination-unreachable,  # type 1
            packet-too-big,  # type 2
            time-exceeded,  # type 3
            parameter-problem,  # type 4
            echo-request,  # type 128
            echo-reply,  # type 129
        } limit rate over 1/second burst 4 packets accept \
        comment "Accept basic IPv6 functionality"

        ip6 nexthdr icmpv6 icmpv6 type {
            nd-router-solicit,  # type 133
            nd-router-advert,  # type 134
            nd-neighbor-solicit,  # type 135
            nd-neighbor-advert,  # type 136
        } ip6 hoplimit 255 accept \
        comment "Allow IPv6 SLAAC"

        ip6 nexthdr icmpv6 icmpv6 type {
            mld-listener-query,  # type 130
            mld-listener-report,  # type 131
            mld-listener-reduction,  # type 132
            mld2-listener-report,  # type 143
        } ip6 saddr fe80::/10 accept \
        comment "Allow IPv6 multicast listener discovery on link-local"

        ip6 saddr fe80::/10 udp sport 547 udp dport 546 accept \
        comment "Accept DHCPv6 replies from IPv6 link-local addresses"
}

    chain input_libvirt {
{% for bridge in libvirt_bridges %}
        # Open DHCP and DNS on {{ bridge.name }}
        iifname "{{ bridge.name }}" udp dport { 53, 67 } accept
        iifname "{{ bridge.name }}" tcp dport { 53, 67 } accept
{% endfor %}
    }

    chain forward_libvirt {
{% for bridge in libvirt_bridges %}
        # Allow forwarding on {{ bridge.name }}
        oifname "{{ bridge.name }}" ip daddr {{ bridge.ip4 }} ct state { established, related } accept
        iifname "{{ bridge.name }}" ip saddr {{ bridge.ip4 }} accept
{% if bridge.ip6 is defined %}
        oifname "{{ bridge.name }}" ip6 daddr {{ bridge.ip6 }} ct state { established, related } accept
        iifname "{{ bridge.name }}" ip6 saddr {{ bridge.ip6 }} accept
{% endif %}
        iifname "{{ bridge.name }}" oifname "{{ bridge.name }}" accept
        oifname "{{ bridge.name }}" reject with icmpx type port-unreachable
        iifname "{{ bridge.name }}" reject with icmpx type port-unreachable
{% endfor %}
    }
}

table inet nat {
    chain postrouting {
        type nat hook postrouting priority 100; policy accept;
        jump postrouting_libvirt
    }

    chain postrouting_libvirt {
{% for bridge in libvirt_bridges %}
        ip saddr {{ bridge.ip4 }} ip daddr != {{ bridge.ip4 }} masquerade
{% endfor %}
    }
}
