About blacklistd(8)
blacklistd(8) provides an API that can be used by network daemons to communicate with a packet filter via a daemon to enforce opening and closing ports dynamically based on policy.
The interface to the packet filter is in /libexec/blacklistd-helper
(this is currently designed for npf) and the configuration file (inspired from inetd.conf) is in etc/blacklistd.conf
Kernel SecureLevel
As we're likely planning to expose our workstation to Internet, it is advisable to raise the kernel securelevel at least to 1 - Secure mode (default= -1, Permanently insecure mode)... please refer to secmodel_securelevel(9).
Let's enable secure mode by specifying the securelevel=n variable inside /etc/rc.conf/; this will let init(8) take care of it upon boot.
$ sed -i '/hostname/a \
securelevel=1"
' /etc/rc.conf
At next boot we should see:
$ sysctl kern.securelevel
kern.securelevel = 1
Enabling NPF, bpfjit and npflog in secure mode
Among the other restrictions securelevel=1 implies:
- Kernel modules may not be loaded or unloaded.
- Adding or removing sysctl(9) nodes is denied.
Now, blacklistd(8) will require bpfjit(4) (Just-In-Time compiler for Berkeley Packet Filter) in order to properly work, in addition to, naturally, npf(7) as frontend and syslogd(8), as a backend to print diagnostic messages. Also remember npf shall rely on the npflog* virtual network interface to provide logging for tcpdump() to use.
Unfortunately (dont' ask me why 😛) in 8.1 all the required kernel components are still not compiled by default in the GENERIC kernel (though they are in HEAD), and are rather provided as modules. Enabling NPF and blacklistd services would normally result in them being automatically loaded as root, but predictably on securelevel=1 this is not going to happen. We have 2 ways out to deal with this:
1) enable them in kernel config
If you're used to running a custom kernel, you can uncomment:
pseudo-device bpfilter # Berkeley packet filter
pseudo-device npf # NPF packet filter
options NPF_EXT_LOG
options NPF_EXT_NORMALISE
options BPFJIT
options SLJIT
2) make init(8) load them at boot by resorting to modules.conf(5):
$ cat >> /etc/modules.conf << EOF
> #additional kernel modules
> bpfjit
> if_npflog
> npf
> npf_alg_icmp
> npf_ext_log
> npf_ext_normalize
> sljit
> EOF
$ echo modules=YES >> /etc/rc.conf
After rebooting, let's check everything's in place:
$ modstat | egrep "npf|jit"
NAME CLASS SOURCE^[1] FLAG REFS SIZE REQUIRES
bpfjit misc filesys - 0 8491 sljit
if_npflog driver filesys - 0 516 -
npf driver filesys - 3 40811 bpf
npf_alg_icmp misc filesys - 0 1312 npf
npf_ext_log misc filesys - 0 669 npf
npf_ext_normalize misc filesys a 0 751 npf
sljit misc filesys a 1 28794 -
[1] Notice the "filesys" value (in place of "builtin") for the $SOURCE variable, indicating those modules are not built in the currently loaded kernel
In both cases we may want to edit /etc/sysctl.conf by adding:
# Toggle JIT compilation of new filter programs on
net.bpf.jit=1
# 4x BPF buffer max size
net.bpf.maxbufsize=4194304
....and run:
$ echo create > /etc/ifconfig.npflog0
thus to force ifconfig()
to create npflog0 at boot.
Configure blacklistd
Let's write a suitable /etc/blacklistd.conf^[2], please refer to blacklistd.conf(5)
# Ban addresses for $disable time on $service after $nfail unsuccessful connection attempts
# service type proto owner name nfail disable
[local]
domain dgram udp named -desk 3 24h
ftp stream tcp root -desk 3 24h
http stream tcp root -desk 3 24h
https stream tcp root -desk 3 24h
smtp stream tcp postfix -desk 3 24h
submission stream tcp postfix -desk 3 24h
ssh stream tcp root -desk 3 24h
# use [remote] table to introduce special policies for $addr/mask:port
# addr/mask:port type proto owner name nfail disable^[3]
[remote]
192.168.1.0/24:ftp stream tcp root -desk 9 =
192.168.1.0/24:ssh stream tcp root -desk 9 =
[2] Disclaimer: at the current state, postfix (and proftpd) support is available only in HEAD
[3] *The "=" value for the $disable variable indicates that the [remote] policy will follow the same behavior as the corresponding [local] one (in this case 24 hours). A wildcard value ('*') would have meant permanent ban *
Make NPF use blacklistd
It's wise to back up your former npf.conf revision beforehand:
$ cp -pi /etc/npf.conf /etc/npf.conf.`date +%s`
Now, let's update our NPF config; in particular, referring to my stateful firewall example, here's the relevant /etc/npf.conf diff:
--- npf.conf 2019-06-08 00:38:31.517536136 +0200
+++ /etc/npf.conf 2019-06-08 01:20:33.955449984 +0200
@@ -13,6 +13,10 @@
$LAN = { 192.168.1.0/24 }
$localhost= 127.0.0.1
+ # Enable Just-In-Time compilation of filter pro-
+ # grams sent to the bpf(4) node
+ set bpf.jit on;
+
# Load ICMP application-level gateway[4]
alg "icmp"
@@ -30,6 +34,10 @@
group default {
#Pass everything on loop interface
pass final on lo0 all
+
+ #Block and release ports on demand to avoid DoS abuse,
+ #according to blacklistd(8) policies
+ ruleset "blacklistd-desk"
#Block blacklisted IPs
block in final from <blacklist>
Restrict r/w permissions to root only
$ chmod 0400 /etc/npf.conf /etc/npf_blacklist /etc/blacklistd.conf
And subsequently update the NPF ruleset:
$ npfctl flush
$ npfctl reload
Enable blacklistd
$ echo blacklistd=YES >> /etc/rc.conf
$ service blacklistd start
Perform some tests
According to the my [local]
table, WAN clients shall be banned for 24h after 3 failed authentication attempts, while the [remote]
table (which takes precedence) introduces milder policies for LAN clients, implying they can try to authenticate up to 9 times before being rejected.
$ blacklistctl dump -ab
address/ma:port id nfail last access
152.19.207.76/32:22 9 11/3 2019/06/08 12:51:25
192.168.1.31/32:21 d 12/9 2019/06/08 12:54:19
The first address is me trying to SSH into my laptop from my phone while on Mobile Data connection (correctly banned after 3 failed auth attempts); the second address is me trying to FTP into my laptop from phone while connected to Wifi (correctly banned after 9 failed auth attempts)
I'm factually negated access
That's all folks, hope you enjoyed it! 🙂