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'),
},
},
},
},
}