ipset

The proposed way to ban IPs using ipset uses two reaction4 and reaction6 sets.

The IPs are banned on all ports, meaning banned IPs will not be able to connect on any service.

There are two methods to using reaction with ipset that you can choose from. Timeouts can be handled with reaction or with ipset. Using ipset for timeouts can help scaling to face larger attacks.

Most distributions have an ipset package that can easily be installed with your package manager.

Timeouts with ipset (ipset-based)

This method is faster to start, because previous IPs are already stored in the set by ipset. However, as reaction does not handle the lifecycle of bans, commands like reaction show and reaction flush can't be used. You must directly use ipset commands to see/modify what IP is banned.

It is recommended to use the ipset-based method if the reaction-based method is too slow for your workload.

Start/Stop

We first need to create ipsets, and add them at the beginning of the INPUT iptables chain.

Docker & LXD users will need to add this rule to the FORWARD chain as well.

Subsequent stops and starts will save and restore the ipsets.

{
  start: [
    // Restore reaction's ipsets and ignore errors.
    // Make sure the files exist.
    // touch /var/lib/reaction/reaction4.ipset
    // touch /var/lib/reaction/reaction6.ipset
    ['touch', '/var/lib/reaction/reaction4.ipset'],
    ['touch', '/var/lib/reaction/reaction6.ipset'],
    // ipset restore -! -f /var/lib/reaction/reaction4.ipset
    // ipset restore -! -f /var/lib/reaction/reaction6.ipset
    ['ipset', 'restore', '-!', '-f', '/var/lib/reaction/reaction4.ipset'],
    ['ipset', 'restore', '-!', '-f', '/var/lib/reaction/reaction6.ipset'],
    // Create reaction ipsets if they do not already exist.
    // ipset -exist -N reaction4 hash:net family inet maxelem 3000000 timeout 0
    // ipset -exist -N reaction6 hash:net family inet6 maxelem 3000000 timeout 0
    ['ipset', '-exist', '-N', 'reaction4', 'hash:net', 'family', 'inet', 'maxelem', '3000000', 'timeout', '0'],
    ['ipset', '-exist', '-N', 'reaction6', 'hash:net', 'family', 'inet6', 'maxelem', '3000000', 'timeout', '0'],
    // Create iptables rules for reaction ipsets.
    // iptables -w 10 -I INPUT 1 -m set --match-set reaction4 src -j DROP
    // iptables -w 10 -I FORWARD 1 -m set --match-set reaction4 src -j DROP
    // ip6tables -w 10 -I INPUT 1 -m set --match-set reaction6 src -j DROP
    // ip6tables -w 10 -I FORWARD 1 -m set --match-set reaction6 src -j DROP
    ['iptables', '-w', '10', '-I', 'INPUT', '1', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['iptables', '-w', '10', '-I', 'FORWARD', '1', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-I', 'INPUT', '1', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-I', 'FORWARD', '1', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
  ],
}

We want reaction to remove them when quitting:

{
  stop: [
    // Remove iptables rules for reaction ipsets.
    // iptables -w 10 -D INPUT -m set --match-set reaction4 src -j DROP
    // iptables -w 10 -D FORWARD -m set --match-set reaction4 src -j DROP
    // ip6tables -w 10 -D INPUT -m set --match-set reaction6 src -j DROP
    // ip6tables -w 10 -D FORWARD -m set --match-set reaction6 src -j DROP
    ['iptables', '-w', '10', '-D', 'INPUT', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['iptables', '-w', '10', '-D', 'FORWARD', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-D', 'INPUT', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-D', 'FORWARD', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
    // Save reaction's ipsets.
    // ipset save reaction4 -f /var/lib/reaction/reaction4.ipset
    // ipset save reaction6 -f /var/lib/reaction/reaction6.ipset
    ['ipset', 'save', 'reaction4', '-f', '/var/lib/reaction/reaction4.ipset'],
    ['ipset', 'save', 'reaction6', '-f', '/var/lib/reaction/reaction6.ipset'],
    // Delete reaction ipsets.
    // ipset -X reaction4
    // ipset -X reaction6
    ['ipset', '-X', 'reaction4'],
    ['ipset', '-X', 'reaction6'],
  ],
}

Ban/Unban

Now we can ban an IPv4 with this command:

{
  cmd: ['ipset', '-exist', '-A', 'reaction4', '<ip>', 'timeout', '172800']
}

and we can ban an IPv6 with this command:

{
  cmd: ['ipset', '-exist', '-A', 'reaction6', '<ip>', 'timeout', '172800']
}

Unbanning with this method is not handled by reaction and is instead normally handled by ipset's timeout value. Manual unbanning is possible with standard ipset commands.

Manually unban an IPv4 address with this command:

ipset del reaction4 <ip>

Manually unban an IPv6 address with this command:

ipset del reaction6 <ip>

A good practice is to wrap the actions in a function with parameters:

local banFor(time) = {
  ban4: {
    // ipset -exist -A reaction4 <ip> timeout 172800
    // 172800 is 48 hours or 2 days.
    cmd: ['ipset', '-exist', '-A', 'reaction4', '<ip>', 'timeout', '172800'],
    ipv4only: true,
j  },
  ban6: {
    // ipset -exist -A reaction6 <ip> timeout 172800
    // 172800 is 48 hours or 2 days.
    cmd: ['ipset', '-exist', '-A', 'reaction6', '<ip>', 'timeout', '172800'],
    ipv6only: true,
  },
  // There is no unban4 and unban6 when ipset handles timeout.
};

See how to merge different actions in JSONnet FAQ

Migrating from reaction's lifecycle to ipset timeouts

If you want to convert from running the reaction-based method to this method, these commands permits to migrate existing IPs to the this method:

# Save the running ipsets.
ipset save reaction4 -f /var/lib/reaction/reaction4.ipset
ipset save reaction6 -f /var/lib/reaction/reaction6.ipset
# Stop reaction.
systemctl stop reaction
# Convert the reaction saves to use timeouts.
sed -i -e '/create/s/$/ timeout 0/g' /var/lib/reaction/reaction4.ipset
sed -i -e '/add/s/$/ timeout 172800/g' /var/lib/reaction/reaction4.ipset
sed -i -e '/create/s/$/ timeout 0/g' /var/lib/reaction/reaction6.ipset
sed -i -e '/add/s/$/ timeout 172800/g' /var/lib/reaction/reaction6.ipset
# Manually update the reaction config for this method.
# Start reaction.
systemctl start reaction

Real-world example

local banFor(time) = {
  ban4: {
    // ipset -exist -A reaction4 <ip> timeout 172800
    // 172800 is 48 hours or 2 days.
    cmd: ['ipset', '-exist', '-A', 'reaction4', '<ip>', 'timeout', '172800'],
    ipv4only: true,
  },
  ban6: {
    // ipset -exist -A reaction6 <ip> timeout 172800
    // 172800 is 48 hours or 2 days.
    cmd: ['ipset', '-exist', '-A', 'reaction6', '<ip>', 'timeout', '172800'],
    ipv6only: true,
  },
  // There is no unban4 and unban6 when ipset handles timeout.
};

{
  state_directory: '/var/lib/reaction',
  patterns: {
    ip: {
      type: 'ip',
      // Block big groups.
      ipv6mask: 64,
      ignorecidr: [
        // Loopback
        '127.0.0.0/8',
        '::1/128',
      ],
    },
  },

  start: [
    // Restore reaction's ipsets and ignore errors.
    // Make sure the files exist.
    // touch /var/lib/reaction/reaction4.ipset
    // touch /var/lib/reaction/reaction6.ipset
    ['touch', '/var/lib/reaction/reaction4.ipset'],
    ['touch', '/var/lib/reaction/reaction6.ipset'],
    // ipset restore -! -f /var/lib/reaction/reaction4.ipset
    // ipset restore -! -f /var/lib/reaction/reaction6.ipset
    ['ipset', 'restore', '-!', '-f', '/var/lib/reaction/reaction4.ipset'],
    ['ipset', 'restore', '-!', '-f', '/var/lib/reaction/reaction6.ipset'],
    // Create reaction ipsets if they do not already exist.
    // ipset -exist -N reaction4 hash:net family inet maxelem 3000000 timeout 0
    // ipset -exist -N reaction6 hash:net family inet6 maxelem 3000000 timeout 0
    ['ipset', '-exist', '-N', 'reaction4', 'hash:net', 'family', 'inet', 'maxelem', '3000000', 'timeout', '0'],
    ['ipset', '-exist', '-N', 'reaction6', 'hash:net', 'family', 'inet6', 'maxelem', '3000000', 'timeout', '0'],
    // Create iptables rules for reaction ipsets.
    // iptables -w 10 -I INPUT 1 -m set --match-set reaction4 src -j DROP
    // iptables -w 10 -I FORWARD 1 -m set --match-set reaction4 src -j DROP
    // ip6tables -w 10 -I INPUT 1 -m set --match-set reaction6 src -j DROP
    // ip6tables -w 10 -I FORWARD 1 -m set --match-set reaction6 src -j DROP
    ['iptables', '-w', '10', '-I', 'INPUT', '1', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['iptables', '-w', '10', '-I', 'FORWARD', '1', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-I', 'INPUT', '1', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-I', 'FORWARD', '1', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
  ],
  stop: [
    // Remove iptables rules for reaction ipsets.
    // iptables -w 10 -D INPUT -m set --match-set reaction4 src -j DROP
    // iptables -w 10 -D FORWARD -m set --match-set reaction4 src -j DROP
    // ip6tables -w 10 -D INPUT -m set --match-set reaction6 src -j DROP
    // ip6tables -w 10 -D FORWARD -m set --match-set reaction6 src -j DROP
    ['iptables', '-w', '10', '-D', 'INPUT', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['iptables', '-w', '10', '-D', 'FORWARD', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-D', 'INPUT', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-D', 'FORWARD', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
    // Save reaction's ipsets.
    // ipset save reaction4 -f /var/lib/reaction/reaction4.ipset
    // ipset save reaction6 -f /var/lib/reaction/reaction6.ipset
    ['ipset', 'save', 'reaction4', '-f', '/var/lib/reaction/reaction4.ipset'],
    ['ipset', 'save', 'reaction6', '-f', '/var/lib/reaction/reaction6.ipset'],
    // Delete reaction ipsets.
    // ipset -X reaction4
    // ipset -X reaction6
    ['ipset', '-X', 'reaction4'],
    ['ipset', '-X', 'reaction6'],
  ],

  streams: {
    // Ban hosts failing to connect via SSH.
    ssh: {
      cmd: ['journalctl', '-fn0', '-u', 'sshd.service'],
      filters: {
        failedlogin: {
          regex: [
            @'authentication failure;.*rhost=<ip>',
            @'Connection reset by authenticating user .* <ip>',
            @'Failed password for .* from <ip>',
          ],
          retry: 3,
          retryperiod: '6h',
          // This time does not matter as ipset is handling the timeout.
          actions: banFor('48h'),
        },
      },
    },
  },
}

Timeouts with reaction (reaction-based lifecycle)

Start/Stop

We first need to create ipsets, and add them at the beginning of the INPUT iptables chain.

Docker & LXD users will need to add this rule to the FORWARD chain as well.

{
  start: [
    // Create reaction ipsets if they do not already exist.
    // ipset -exist -N reaction4 hash:net family inet maxelem 3000000
    // ipset -exist -N reaction6 hash:net family inet6 maxelem 3000000
    ['ipset', '-exist', '-N', 'reaction4', 'hash:net', 'family', 'inet', 'maxelem', '3000000'],
    ['ipset', '-exist', '-N', 'reaction6', 'hash:net', 'family', 'inet6', 'maxelem', '3000000'],
    // Create iptables rules for reaction ipsets.
    // iptables -w 10 -I INPUT 1 -m set --match-set reaction4 src -j DROP
    // iptables -w 10 -I FORWARD 1 -m set --match-set reaction4 src -j DROP
    // ip6tables -w 10 -I INPUT 1 -m set --match-set reaction6 src -j DROP
    // ip6tables -w 10 -I FORWARD 1 -m set --match-set reaction6 src -j DROP
    ['iptables', '-w', '10', '-I', 'INPUT', '1', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['iptables', '-w', '10', '-I', 'FORWARD', '1', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-I', 'INPUT', '1', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-I', 'FORWARD', '1', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
  ],
}

We want reaction to remove them when quitting:

  stop: [
    // Remove iptables rules for reaction ipsets.
    // iptables -w 10 -D INPUT -m set --match-set reaction4 src -j DROP
    // iptables -w 10 -D FORWARD -m set --match-set reaction4 src -j DROP
    // ip6tables -w 10 -D INPUT -m set --match-set reaction6 src -j DROP
    // ip6tables -w 10 -D FORWARD -m set --match-set reaction6 src -j DROP
    ['iptables', '-w', '10', '-D', 'INPUT', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['iptables', '-w', '10', '-D', 'FORWARD', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-D', 'INPUT', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-D', 'FORWARD', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
    // Delete reaction ipsets.
    // ipset -X reaction4
    // ipset -X reaction6
    ['ipset', '-X', 'reaction4'],
    ['ipset', '-X', 'reaction6'],
  ],

Ban/Unban

Now we can ban an IPv4 with this command:

{
  cmd: ['ipset', '-exist', '-A', 'reaction4', '<ip>']
}

and we can ban an IPv6 with this command:

{
  cmd: ['ipset', '-exist', '-A', 'reaction6', '<ip>']
}

And unban the IPv4 with this command:

{
  cmd: ['ipset', '-D', 'reaction4', '<ip>']
}

And unban the IPv6 with this command:

{
  cmd: ['ipset', '-D', 'reaction6', '<ip>']
}

A good practice is to wrap the actions in a function with parameters:

local banFor(time) = {
  ban4: {
    // ipset -exist -A reaction4 <ip>
    cmd: ['ipset', '-exist', '-A', 'reaction4', '<ip>'],
    ipv4only: true,
  },
  ban6: {
    // ipset -exist -A reaction6 <ip>
    cmd: ['ipset', '-exist', '-A', 'reaction6', '<ip>'],
    ipv6only: true,
  },
  unban4: {
    after: time,
    // ipset -D reaction4 <ip>
    cmd: ['ipset', '-D', 'reaction4', '<ip>'],
    ipv4only: true,
  },
  unban6: {
    after: time,
    // ipset -D reaction6 <ip>
    cmd: ['ipset', '-D', 'reaction6', '<ip>'],
    ipv6only: true,
  },
};

See how to merge different actions in JSONnet FAQ

Real-world example

local banFor(time) = {
  ban4: {
    // ipset -exist -A reaction4 <ip>
    cmd: ['ipset', '-exist', '-A', 'reaction4', '<ip>'],
    ipv4only: true,
  },
  ban6: {
    // ipset -exist -A reaction6 <ip>
    cmd: ['ipset', '-exist', '-A', 'reaction6', '<ip>'],
    ipv6only: true,
  },
  unban4: {
    after: time,
    // ipset -D reaction4 <ip>
    cmd: ['ipset', '-D', 'reaction4', '<ip>'],
    ipv4only: true,
  },
  unban6: {
    after: time,
    // ipset -D reaction6 <ip>
    cmd: ['ipset', '-D', 'reaction6', '<ip>'],
    ipv6only: true,
  },
};

{
  patterns: {
    ip: {
      type: 'ip',
      ignorecidr: [
        // Loopback
        '127.0.0.0/8',
        '::1/128',
      ],
    },
  },

  start: [
    // Create reaction ipsets if they do not already exist.
    // ipset -exist -N reaction4 hash:net family inet maxelem 3000000
    // ipset -exist -N reaction6 hash:net family inet6 maxelem 3000000
    ['ipset', '-exist', '-N', 'reaction4', 'hash:net', 'family', 'inet', 'maxelem', '3000000'],
    ['ipset', '-exist', '-N', 'reaction6', 'hash:net', 'family', 'inet6', 'maxelem', '3000000'],
    // Create iptables rules for reaction ipsets.
    // iptables -w 10 -I INPUT 1 -m set --match-set reaction4 src -j DROP
    // iptables -w 10 -I FORWARD 1 -m set --match-set reaction4 src -j DROP
    // ip6tables -w 10 -I INPUT 1 -m set --match-set reaction6 src -j DROP
    // ip6tables -w 10 -I FORWARD 1 -m set --match-set reaction6 src -j DROP
    ['iptables', '-w', '10', '-I', 'INPUT', '1', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['iptables', '-w', '10', '-I', 'FORWARD', '1', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-I', 'INPUT', '1', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-I', 'FORWARD', '1', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
  ],
  stop: [
    // Remove iptables rules for reaction ipsets.
    // iptables -w 10 -D INPUT -m set --match-set reaction4 src -j DROP
    // iptables -w 10 -D FORWARD -m set --match-set reaction4 src -j DROP
    // ip6tables -w 10 -D INPUT -m set --match-set reaction6 src -j DROP
    // ip6tables -w 10 -D FORWARD -m set --match-set reaction6 src -j DROP
    ['iptables', '-w', '10', '-D', 'INPUT', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['iptables', '-w', '10', '-D', 'FORWARD', '-m', 'set', '--match-set', 'reaction4', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-D', 'INPUT', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
    ['ip6tables', '-w', '10', '-D', 'FORWARD', '-m', 'set', '--match-set', 'reaction6', 'src', '-j', 'DROP'],
    // Delete reaction ipsets.
    // ipset -X reaction4
    // ipset -X reaction6
    ['ipset', '-X', 'reaction4'],
    ['ipset', '-X', 'reaction6'],
  ],

  streams: {
    // Ban hosts failing to connect via SSH.
    ssh: {
      cmd: ['journalctl', '-fn0', '-u', 'sshd.service'],
      filters: {
        failedlogin: {
          regex: [
            @'authentication failure;.*rhost=<ip>',
            @'Connection reset by authenticating user .* <ip>',
            @'Failed password for .* from <ip>',
          ],
          retry: 3,
          retryperiod: '6h',
          actions: banFor('48h'),
        },
      },
    },
  },
}