Since I switched my server from OpenBSD to NixOS, I was missing a feature. The previous server was using iblock, a program I made to block IPs connecting on a list of ports, I don't like people knocking randomly on ports.
iblock is simple, if you connect to any port on which it's listening, you get banned in the firewall.
I reimplemented it using iptables on NixOS.
Iptables provides a feature adding an IP to a set if the address connects n times before s seconds. Let's just set it to ONCE so the address is banned on first connection.
For the record, a "set" is an extra iptables feature allowing to add many IP addresses like an OpenBSD PF table. We need separate sets for IPv4 and IPv6, they don't mix well.
You can create a new nix file with this content and add it to the imports of your configuration file.
{ lib, pkgs, ... }: let wan_interface = "eth0"; ports-to-block = "21,23,53,111,135,137,138,139,445,1433,25565,5432,3389,3306,27019"; # block people 10 days expire = 60 * 60 * 24 * 10; # in seconds, 0 to disable expiration , max is 2147483 rules = table: [ "INPUT -i ${wan_interface} -p tcp -m multiport --dports ${ports-to-block} -m state --state NEW -m recent --set" "INPUT -i ${wan_interface} -p tcp -m multiport --dports ${ports-to-block} -m state --state NEW -m recent --update --seconds 10 --hitcount 1 -j SET --add-set ${table} src" "INPUT -i ${wan_interface} -p tcp -m set --match-set ${table} src -j nixos-fw-refuse" "INPUT -i ${wan_interface} -p udp -m set --match-set ${table} src -j nixos-fw-refuse" ]; create-rules = lib.concatStringsSep "\n" ( builtins.map (rule: "iptables -C " + rule + " || iptables -A " + rule) (rules "blocked") ++ builtins.map (rule: "ip6tables -C " + rule + " || ip6tables -A " + rule) (rules "blocked6") ); delete-rules = lib.concatStringsSep "\n" ( builtins.map (rule: "iptables -C " + rule + " && iptables -D " + rule) (rules "blocked") ++ builtins.map (rule: "ip6tables -C " + rule + " && ip6tables -D " + rule) (rules "blocked6") ); in { networking.firewall = { enable = true; extraPackages = [pkgs.ipset]; extraCommands = '' if test -f /var/lib/ipset.conf then ipset restore -! < /var/lib/ipset.conf else ipset -exist create blocked hash:ip ${ if expire > 0 then "timeout ${toString expire}" else "" } ipset -exist create blocked6 hash:ip family inet6 ${ if expire > 0 then "timeout ${toString expire}" else "" } fi ${create-rules} ''; extraStopCommands = '' ipset -exist create blocked hash:ip ${ if expire > 0 then "timeout ${toString expire}" else "" } ipset -exist create blocked6 hash:ip family inet6 ${ if expire > 0 then "timeout ${toString expire}" else "" } ipset save > /var/lib/ipset.conf ${delete-rules} ''; }; }
To explain this implementation without going into details:
The configuration isn't stateless, it creates a file /var/lib/ipset.conf , so if you want to make changes like expiration time to the sets while they already exist, you will need to use ipset yourself.
And most importantly, because of the way the firewall service is implemented, if you don't use this file anymore, the firewall won't reload.
I've lost a lot of time figuring why: when NixOS reloads the firewall service, it uses the new reload script which doesn't include the cleanup from stopCommand, and this fails because the NixOS service didn't expect anything in the INPUT chain.
sept. 29 23:24:22 interbus systemd[1]: Reloading Firewall... sept. 29 23:24:22 interbus firewall-reload[94376]: iptables: Chain already exists. sept. 29 23:24:22 interbus firewall-reload[94340]: Failed to reload firewall... Stopping sept. 29 23:24:22 interbus systemd[1]: firewall.service: Control process exited, code=exited, status=1/FAILURE sept. 29 23:24:22 interbus systemd[1]: Reload failed for Firewall.
In this case, you have to manually delete the rules in the INPUT chain in for IPv4 and IPv6, or reboot your system that will start with a fresh set, or flush all rules in iptables and restart the firewall service.
I'll be able to publish again a list of IPs scanning my server, and this is also fun to see the list growing every minute.