Pages: Welcome | Projects

Playing with netfilter: like fail2ban

2021/1/16
Tags: [ GNU/Linux ] [ network ] [ security ]

It has been a long time since my last blog entry. Lots of things happened, that kept me away from my playful explorations. Nonetheless, I've got a list of projects that I'd like to work on, even though the time for it is going to be rare and precious.

Two of these projects require some firewall management skills, one under FreeBSD, and one under GNU/Linux. This little post concerns the second.

In short, I started to do some prototyping on my (yet undisclosed) idea. That's when I realized it is a precious opportunity to learn some nft(8), since I only know the outdated iptables(8). I decided to upgrade my knowledge by means of some exercises.

So here's a nice exercise involving named sets: temporarily ban a given IP address, as fail2ban does.

#!/bin/sh

set -e

# The script will modify the firewall configuration, so a back-up copy
# of it is taken, and gets restored when the script is terminated.
atexit() {
  local e="$?"
  set +e

  [ ! -e "$td/backup.nft" ] || {
    echo >&2 "RESTORE RULESET:"

    # nft -f <file> allows for an atomic replacement of the ruleset
    # with the content of file.
    <"$td/backup.nft" tee /dev/stderr | nft -f -
  }
  [ ! -e "$td" ] || rm -fr "$td"

  exit "$e"
}
trap atexit EXIT

td="$(mktemp -d)"

# The backup file begins with 'flush ruleset', since applying a file
# via nft -f achieves merging of the ruleset, and not replacing.
{ echo flush ruleset; nft list ruleset; } >"$td/backup.nft"

nft -f - <<'EOF'

flush ruleset;

# I'm defining a single table called 'Bastion' where there will be a
# chain based on the 'input' hook (that is, hooks in packets that are
# directed to the local host).
create table inet Bastion;
add chain inet Bastion Input {
  type filter hook input priority 0;
  policy accept;
}

# Two twin 'sets': one for ipv4 and one for ipv6, but with the same
# characteristics: a host that is inserted will be kept for 10
# seconds and then removed automatically.
create set inet Bastion Banned_ip4 {
  type ipv4_addr;
  timeout 10s;
  gc-interval 2s;
}
create set inet Bastion Banned_ip6 {
  type ipv6_addr;
  timeout 10s;
  gc-interval 2s;
}

# For each of the two sets there's going to be a rule for the
# 'Bastion' chain.  Both rules will log and drop the catched packets..
add rule inet Bastion Input ip daddr @Banned_ip4 log drop;
add rule inet Bastion Input ip6 daddr @Banned_ip6 log drop;

EOF

# This loop emulates the work of the daemon inserting elements into
# the banned host list.  Each iteration requires the address family (4
# or 6, for ipv4 and ipv6 respectively), and the address of a host to
# ban. 
while read -p "enter family/ip> " -r family banned; do

  case "$family" in
  4 | 6) set="Banned_ip$family";;

  # If nothing is provided, print the two sets.  This is a quick way
  # to keep an eye on the sets, and the neat part is that the output
  # will in fact mention the timeout for each host of the set.
  '')
    nft 'list set inet Bastion Banned_ip4'
    nft 'list set inet Bastion Banned_ip6'
    continue
    ;;

  # And then some help, just to be nice.
  *)
    echo >&2 "bad family '$family': use 4 or 6"
    continue;;
  esac

  # Once a set is defined, elements can be inserted just witn a nft(8)
  # invocation.
  nft "add element inet Bastion $set { $banned }" || :
done

Then the behaviour can be verified with nc(1), the poket knife of TCP/IP: