{% set all_public_network_interfaces = ( ansible_interfaces | select('match', '^(eth|wlan|(en|wl|ww)[osxpP])[0-9]+') | list ) -%} #!/usr/sbin/nft -f # NOTE: # Dynamic blacklisting: # - https://wiki.nftables.org/wiki-nftables/index.php/Meters # - https://wiki.archlinux.org/title/Nftables#Dynamic_blackhole # - https://unix.stackexchange.com/questions/581964/create-dynamic-blacklist-with-nftables # # Early broken packages dropping in netdev: # - https://wiki.gentoo.org/wiki/Nftables#Family_netdev_and_ingress_hook # - https://serverfault.com/questions/772195/drop-fragmented-packets-in-nftables # # udev's network interface naming scheme: # - https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_networking/consistent-network-interface-device-naming_configuring-and-managing-networking # - https://systemd.io/PREDICTABLE_INTERFACE_NAMES/ # # UDP is stateless, but the kernel can still mark a few conntrack states on the packets: # - https://blog.cloudflare.com/everything-you-ever-wanted-to-know-about-udp-sockets-but-were-afraid-to-ask-part-1/ # - https://www.rigacci.org/wiki/lib/exe/fetch.php/doc/appunti/linux/sa/iptables/conntrack.html # - https://www.frozentux.net/iptables-tutorial/chunkyhtml/x1555.html # - https://github.com/torvalds/linux/blob/master/net/ipv4/udp.c # Clear all prior state flush ruleset table netdev filter { chain ingress { # 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 = { {{ all_public_network_interfaces | 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 { set blackhole { type ipv4_addr; flags dynamic, timeout; size 65535; } set blackhole6 { type ipv6_addr; flags dynamic, timeout; size 65535; } # NOTE: connlimit shouldn't be used with timeout flag (same goes for "update" set statement) set connlimit { type ipv4_addr; flags dynamic; size 65535; } set connlimit6 { type ipv6_addr; flags dynamic; size 65535; } 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)" udp dport mdns ip daddr 224.0.0.251 accept \ comment "Accept mDNS" udp dport mdns ip6 daddr ff02::fb accept \ comment "Accept mDNS" jump input_dhcp_client jump input_icmp # Blacklisting should be done before stateful accept rules ip saddr @blackhole counter drop ip6 saddr @blackhole6 counter drop # Drop future attempts on opened ports if there are already 3 established connections {% if opened_ports.tcp is sequence and opened_ports.tcp | length > 0 %} tcp dport { {{ opened_ports.tcp | join(', ') }} } ct state new \ add @connlimit { ip saddr ct count over 3 } drop tcp dport { {{ opened_ports.tcp | join(', ') }} } ct state new \ add @connlimit6 { ip6 saddr ct count over 3 } drop {% endif %} {% if opened_ports.udp is sequence and opened_ports.udp | length > 0 %} udp dport { {{ opened_ports.udp | join(', ') }} } ct state new \ add @connlimit { ip saddr ct count over 3 } drop udp dport { {{ opened_ports.udp | join(', ') }} } ct state new \ add @connlimit6 { ip6 saddr ct count over 3 } drop {% endif %} # Allow opened ports but also dynamically add them to the blacklist {% if opened_ports.tcp is sequence and opened_ports.tcp | length > 0 %} tcp dport { {{ opened_ports.tcp | join(', ') }} } ct state new \ add @blackhole { ip saddr timeout 60s limit rate 10/second } accept tcp dport { {{ opened_ports.tcp | join(', ') }} } ct state new \ add @blackhole6 { ip6 saddr timeout 60s limit rate 10/second } accept {% endif %} {% if opened_ports.udp is sequence and opened_ports.udp | length > 0 %} udp dport { {{ opened_ports.udp | join(', ') }} } ct state new \ add @blackhole { ip saddr timeout 60s limit rate 10/second } accept udp dport { {{ opened_ports.udp | join(', ') }} } ct state new \ add @blackhole6 { ip6 saddr timeout 60s limit rate 10/second } accept {% endif %} } chain forward { type filter hook forward priority 0; policy drop; } chain output { type filter hook output priority 0; policy accept; } chain input_dhcp_client { udp sport 67 udp dport 68 accept \ comment "Accept DHCP client input traffic" ip6 saddr fe80::/10 udp sport 547 udp dport 546 accept \ comment "Accept DHCPv6 replies from IPv6 link-local addresses" } chain input_icmp { ip protocol igmp accept \ comment "Accept IGMP" 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 10/second burst 4 packets accept \ comment "Accept ICMP" 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 10/second burst 4 packets accept \ comment "Accept basic IPv6 functionality" 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" 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" } } # The state of stateful objects saved on the nftables service stop. include "/var/lib/nftables/*.nft"