NAME

Net::Firewall::BlockerHelper::backends::pf - pf backend for Net::Firewall::BlockerHelper.

VERSION

Version 0.1.0

SYNOPSIS

The rules and table this backend creates all live under the anchor <prefix>/<name>, which is what allows them to be managed dynamically without ever touching the main ruleset. pf will only evaluate the anchor if pf.conf contains an anchor rule for it though, so a bit of one time setup is required. With the default prefix of kur that means adding the like of the following and reloading pf.conf.

anchor "kur/*"

The rules loaded into the anchor are block drop quick rules, so they will win out over any non-quick pass rules regardless of placement, but the anchor line does need to come before any quick pass rules that would otherwise match the traffic in question.

use Net::Firewall::BlockerHelper::backends::pf;

my $backend1;
my $backend2;
eval {
    $backend1 = Net::Firewall::BlockerHelper::backends::pf->new(
            backend => 'pf',
            name => 'all',
            options=>{ kill=>1 },
        );
    $backend2 = Net::Firewall::BlockerHelper::backends::pf->new(
            backend => 'pf',
            ports => ['143'],
            protocols => ['tcp'],
            name => 'imap',
            options=>{ kill=>0 },
        );
};
if ($@) {
    print 'Error: '
        . $Error::Helper::error
        . "\nError String: "
        . $Error::Helper::errorString
        . "\nError Flag: "
        . $Error::Helper::errorFlag . "\n";
}

print `pfctl -sr`

$backend1->init;
$backend2->init;

print `pfctl -sr`

$backend1->ban(ban=>'1.2.3.4');
$backend1->ban(ban=>'4.3.2.1');
$backend2->ban(ban=>'4.3.2.1');

use Data::Dumper;
print Dumper($backend1->list);
print Dumper($backend2->list);

$backend1->unban(ban=>'4.3.2.1');

use Data::Dumper;
print Dumper($backend1->list);
print Dumper($backend2->list);

$backend1->teardown;

print `pfctl -sr`

$backend2->teardown;

print `pfctl -sr`

METHODS

new

Initiates the the object.

- options :: Backend specific options that will be passed to the backend unchecked
        outside of making sure it is a hash ref if defined. See below for furhter info.
    - Default :: {}

- ports :: A array of ports to block. Checked to make sure they are positive ints or a valid
        service name via getservbyname. All ports will be blocked if non are specified. If
        duplicates are removed.
    - Default :: []

- protocols :: A array of protocols to block. This is checked against
        /etc/protocols via the function getprotobyname. Duplicates will be
        discarded. If no protocols are given, tcp, udp, icmp, and icmp6 are
        blocked, unless ports are given, in which case it defaults to tcp
        and udp. Ports are only attached to port-capable protocols
        (tcp/udp/sctp); other protocols are blocked without a port.
    - Default :: ['tcp','udp','icmp','icmp6'], or ['tcp','udp'] when ports are given

- prefix :: Prefix to use. Must match the regex /^[a-zA-Z0-9]+$/
    - default :: kur

- name :: Name of this specific instance. This must be specified.
    - default :: undef

The options hash accepts the following.

- kill :: If it should kill states for the banned IP or not. Handles
        both IPv4 and IPv6 and is scoped to what is being blocked. With
        protocols and/or ports configured the state table is searched and
        matching states killed by ID, filtered to the blocked protocols
        and ports (pf keeps state for UDP as well, so blocking only udp
        kills only udp states and leaves tcp alone); the state matching
        follows the family of the banned IP as pf prints IPv4 states as
        addr:port and IPv6 ones as addr[port]. With nothing configured
        everything is being blocked and pfctl -k is used, killing all
        states for the IP.
    - Default :: 0

All errors are considered fatal, meaning if new fails it will die.

my $backend;
eval {
    $backend = Net::Firewall::BlockerHelper::backends::pf->new(
            backend => 'pf',
            ports => ['22'],
            protocols => ['tcp'],
            name => 'ssh',
        );
};
if ($@) {
    print 'Error: '
        . $Error::Helper::error
        . "\nError String: "
        . $Error::Helper::errorString
        . "\nError Flag: "
        . $Error::Helper::errorFlag . "\n";
}

init

Initiates the backend. Best effort cleanup commands are run first, flushing and killing the table and flushing the anchor rules, so remnants of a previous run do not linger. Then the table <prefix>_<name> is created (persist, with counters) and block drop quick rules for the configured protocols and ports are loaded into the anchor <prefix>/<name> via pfctl -f-.

Note that pf.conf must contain a matching anchor rule for any of this to have an effect. See "SYNOPSIS".

No arguments are taken.

May called a second time, it will error.

$backend->init;

ban

Bans an IP. The value of ban is validated as being a IPv4 or IPv6 address and lowercased, then added to the pf table via pfctl -a <prefix>/<name> -t <prefix>_<name> -T add. If the option kill is true, states for the IP are killed as well, scoped to the configured protocols and ports. Banning an already banned IP is a noop.

$backend->ban(ban => $ip);

unban

Unbans an IP. The value of ban is validated as being a IPv4 or IPv6 address and lowercased, then removed from the pf table via pfctl -a <prefix>/<name> -t <prefix>_<name> -T delete. Unbanning an IP that is not banned is a noop.

$backend->unban(ban => $ip);

ban_cidr

Bans a CIDR range by adding it to the pf table via pfctl -T add. pf tables accept a network prefix in the same manner as a single address. The value of ban is validated as being a IPv4 or IPv6 CIDR range and lowercased. Banning an already banned range is a noop.

$backend->ban_cidr(ban => '1.2.3.0/24');

unban_cidr

Unbans a CIDR range by deleting it from the pf table via pfctl -T delete. The value of ban is validated as being a IPv4 or IPv6 CIDR range and lowercased. Unbanning a range that is not banned is a noop.

$backend->unban_cidr(ban => '1.2.3.0/24');

list_cidr

List banned CIDR ranges. Returns an array of the currently banned CIDR ranges. Single IPs are not included; for those see "list".

my @banned_cidrs = $backend->list_cidr;

list

List banned IPs. Returns an array of the currently banned single IPs. CIDR ranges are not included; for those see "list_cidr".

my @banned = $backend->list;

re_init

Tells the backend to re-init it's self.

This will call teardown and init again. After that it will re-added all previously added bans.

Will error if already inited.

$backend->re_init;

teardown

Tears down the setup for the backend.

This flushes and kills the pf table and flushes the rules from the anchor, removing both the table and the block rules.

If called prior to calling init, this will error. It won't check if it has been inited or not.

$backend->teardown;

stop

Alias for "teardown", provided for parity with the fail2ban actionstop concept.

$backend->stop;

check

Verifies that the pf table is still present under the anchor and that the anchor still contains the block rules. Returns a true value if the setup is intact and a false value if any part of it appears to have been removed. This is the equivalent of fail2ban's actioncheck.

if ( !$backend->check ) {
    $backend->re_init;
}

flush

Removes all currently banned IPs at once by flushing the pf table, leaving the table and rules in place. This is the equivalent of fail2ban's actionflush.

$backend->flush;

ERROR CODES / FLAGS

Error handling is provided by Error::Helper. All errors are considered fatal.

1, notInited

The backend has not been inited yet.

2, invalidPortSpecified

Port is either not an int within the range 1 to 65535 or a name that can be resolved by getservbyname.

3, portsNotArray

The data passed to new for ports is not an array.

4, protocolsNotArray

The data passed to new for protocols is not an array.

5, invalidPortSpecified

Port is either not an int within the range 1 to 65535 or a name that can be resolved by getservbyname.

6, invalidPrefixSpecified

The specified prefix did not match /^[a-zA-Z0-9]+$/.

7, invalidName

The name is either undef or does not match /^[a-zA-Z0-9\-]+$/.

8, optionsNotHash

The item passed to new for options is not a hash.

9, noBanItem

No IP or CIDR range specified to ban or unban.

10, banItemNotIP

The item to ban is not an IP. Either wrong ref type or regexp test using Regexp::IPv4 and Regexp::IPv6 failed.

11, invalidBackend

The specified backend failed to pass a basic sanity check of making sure it matches the regexp /^[a-zA-Z0-9\_]+$/.

12, backendInitError

Failed to init the backend.

13, banFailed

Failed to ban the item.

14, unbanFailed

Failed to unban the item.

15, listFailed

Failed to get a list of bans.

16, reInitFailed

Failed to re_init the backend.

17, teardownFailed

Failed to teardown the backend.

18, alreadyInited

init called, but the backend has already been inited.

19, nameTooLong

The combined prefix and name is longer than 31 characters, the max pf table name length.

23, initFailed

One of the required commands for init failed.

24, checkFailed

The backend check raised an error.

25, flushFailed

Failed to flush the bans.

26, banCidrFailed

Failed to ban the CIDR range.

27, unbanCidrFailed

Failed to unban the CIDR range.

28, cidrItemNotCidr

The item to ban is not a CIDR range. Either wrong ref type or it is not an IPv4 or IPv6 address followed by a prefix length valid for its family.

29, cidrNotSupported

The backend does not support CIDR bans.

30, listCidrFailed

Failed to get a list of CIDR bans.

AUTHOR

Zane C. Bowers-Hadley, <vvelox at vvelox.ent>

BUGS

Please report any bugs or feature requests to bug-net-firewall-blockerhelper at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Net-Firewall-BlockerHelper. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

perldoc Net::Firewall::BlockerHelper

You can also look for information at:

ACKNOWLEDGEMENTS

LICENSE AND COPYRIGHT

This software is Copyright (c) 2023 by Zane C. Bowers-Hadley.

This is free software, licensed under:

The GNU Lesser General Public License, Version 2.1, February 1999