Merge pull request #165 from jaredj/p0f-block
Support list of client operating systems to block
This commit is contained in:
commit
91b9d026f8
@ -119,6 +119,22 @@ Example entry disabling header addition
|
||||
|
||||
Default: true
|
||||
|
||||
=head1 CONFIGURATION FILES
|
||||
|
||||
=head2 p0f_blocked_operating_systems
|
||||
|
||||
If populated, systems that match the phrases and regular expressions
|
||||
in this list will be rejected.
|
||||
|
||||
Regular expressions are case-insensitive.
|
||||
|
||||
Example entries:
|
||||
|
||||
Windows XP
|
||||
/windows/
|
||||
|
||||
Default: none (p0f rejections disabled)
|
||||
|
||||
=head1 Environment requirements
|
||||
|
||||
p0f v3 requires only the remote IP.
|
||||
@ -166,12 +182,63 @@ sub register {
|
||||
foreach (keys %args) {
|
||||
$self->{_args}->{$_} = $args{$_};
|
||||
}
|
||||
$self->register_headers();
|
||||
$self->register_genre_blocking();
|
||||
}
|
||||
|
||||
sub register_headers {
|
||||
my ( $self ) = @_;
|
||||
my $enabled = $self->{_args}{add_headers};
|
||||
$enabled = 'true' if ! defined $enabled;
|
||||
return if $enabled =~ /false/i;
|
||||
$self->register_hook( data_post => 'add_headers' );
|
||||
}
|
||||
|
||||
sub register_genre_blocking {
|
||||
my ( $self ) = @_;
|
||||
my @patterns = $self->qp->config('p0f_blocked_operating_systems');
|
||||
return unless @patterns;
|
||||
for my $pattern ( @patterns ) {
|
||||
if ( $pattern =~ /^\/(.*)\/$/ ) {
|
||||
push @{ $self->{os_block_re} }, qr/$1/i;
|
||||
} else {
|
||||
push @{ $self->{os_block} }, $pattern;
|
||||
}
|
||||
}
|
||||
$self->register_hook( rcpt => 'rcpt_handler' );
|
||||
}
|
||||
|
||||
sub rcpt_handler {
|
||||
my ( $self, $txn, $rcpt ) = @_;
|
||||
return DECLINED if ! $self->check_genre($rcpt);
|
||||
return DENY, 'OS Blocked';
|
||||
}
|
||||
|
||||
sub check_genre {
|
||||
my ( $self, $rcpt ) = @_;
|
||||
my $p0f = $self->connection->notes('p0f') or return 0;
|
||||
return 0 if $self->exclude_connection();
|
||||
return 0 if $self->exclude_recipient($rcpt);
|
||||
for my $phrase ( @{ $self->{os_block} || [] } ) {
|
||||
return 1 if $p0f->{genre} eq $phrase;
|
||||
}
|
||||
for my $re ( @{ $self->{os_block_re} || [] } ) {
|
||||
return 1 if $p0f->{genre} =~ $re;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub exclude_connection {
|
||||
my ( $self ) = @_;
|
||||
my $cxn = $self->connection;
|
||||
return $cxn->notes('p0f_exclude') if defined $cxn->notes('p0f_exclude');
|
||||
return $cxn->notes('p0f_exclude',1) if $self->is_immune();
|
||||
return $cxn->notes('p0f_exclude',0);
|
||||
}
|
||||
|
||||
# This sub exists to be overridden by plugins that inherit from this one
|
||||
sub exclude_recipient { return 0 }
|
||||
|
||||
sub hook_connect {
|
||||
my ($self, $qp) = @_;
|
||||
|
||||
|
@ -84,4 +84,27 @@ sub validate_password {
|
||||
return $deny, "$file - wrong password";
|
||||
}
|
||||
|
||||
sub fake_config {
|
||||
my $self = shift;
|
||||
my $fake_config = {@_};
|
||||
$self->qp->hooks->{config} = [
|
||||
{
|
||||
name => '___FakeHook___',
|
||||
code => sub {
|
||||
my ( $self, $txn, $conf ) = @_;
|
||||
return DECLINED if ! exists $fake_config->{$conf};
|
||||
return OK, $fake_config->{$conf};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
sub unfake_config {
|
||||
my ( $self ) = @_;
|
||||
$self->qp->hooks->{config} = [
|
||||
grep { $_->{name} ne '___FakeHook___' }
|
||||
@{ $self->qp->hooks->{config} || [] }
|
||||
];
|
||||
}
|
||||
|
||||
1;
|
||||
|
@ -16,12 +16,11 @@ sub test_content_log_file {
|
||||
is( $self->content_log_file,
|
||||
strftime('mail/%Y%m%d', localtime(time)),
|
||||
'content_log_file() returns the right default value' );
|
||||
$self->save_hook;
|
||||
$self->fake_config('/path/to/%Y%m%d%H%M');
|
||||
$self->fake_config( content_log_file => '/path/to/%Y%m%d%H%M' );
|
||||
is( $self->content_log_file,
|
||||
strftime('/path/to/%Y%m%d%H%M', localtime(time)),
|
||||
'content_log_file config changes content_log_file() output' );
|
||||
$self->restore_hook;
|
||||
$self->unfake_config;
|
||||
}
|
||||
|
||||
sub test_content_log_enabled {
|
||||
@ -65,37 +64,15 @@ sub test_content_log_enabled {
|
||||
expected => 0,
|
||||
},
|
||||
);
|
||||
$self->save_hook;
|
||||
for ( @test_data ) {
|
||||
my $descr = "content_log_enabled="
|
||||
. ( defined $_->{config } ? "'$_->{config }'" : 'undef' )
|
||||
. ( $_->{expected} ? ' enables' : ' disables' )
|
||||
. ' content logging';
|
||||
$self->fake_config( $_->{config } );
|
||||
$self->fake_config( content_log_enabled => $_->{config } );
|
||||
is( $self->content_log_enabled, $_->{expected}, $descr );
|
||||
}
|
||||
$self->restore_hook;
|
||||
}
|
||||
|
||||
our $oldhook;
|
||||
sub save_hook {
|
||||
my ( $self ) = @_;
|
||||
$oldhook = $self->qp->hooks->{config};
|
||||
}
|
||||
|
||||
sub restore_hook {
|
||||
my ( $self ) = @_;
|
||||
$self->qp->hooks->{config} = $oldhook;
|
||||
}
|
||||
|
||||
sub fake_config {
|
||||
my ( $self, $value ) = @_;
|
||||
$self->qp->hooks->{config} = [
|
||||
{
|
||||
name => 'test hook',
|
||||
code => sub { return OK, $value },
|
||||
},
|
||||
];
|
||||
$self->unfake_config;
|
||||
}
|
||||
|
||||
sub test_exclude {
|
||||
|
@ -13,6 +13,124 @@ sub register_tests {
|
||||
$self->register_test('test_get_v3_query');
|
||||
$self->register_test('test_store_v2_results');
|
||||
$self->register_test('test_store_v3_results');
|
||||
$self->register_test('test_register_headers');
|
||||
$self->register_test('test_register_genre_blocking');
|
||||
$self->register_test('test_rcpt_handler');
|
||||
$self->register_test('test_check_genre');
|
||||
$self->register_test('test_exclude_connection');
|
||||
$self->register_test('test_exclude_recipient');
|
||||
}
|
||||
|
||||
sub register_hook {
|
||||
# Override normal register_hook() to record behavior
|
||||
my $self = shift;
|
||||
$self->{_lastreg} = join ',', @_;
|
||||
}
|
||||
|
||||
sub test_register_headers {
|
||||
my ( $self ) = @_;
|
||||
|
||||
# reset last_register_args
|
||||
$self->register_hook();
|
||||
delete $self->{_args}{add_headers};
|
||||
$self->register_headers();
|
||||
is( $self->{_lastreg}, 'data_post,add_headers',
|
||||
'register_headers() registers data_post hook by default' );
|
||||
|
||||
$self->register_hook();
|
||||
$self->{_args}{add_headers} = 'asdf';
|
||||
$self->register_headers();
|
||||
is( $self->{_lastreg}, 'data_post,add_headers',
|
||||
'register_headers() registers data_post hook on invalid input' );
|
||||
|
||||
$self->register_hook();
|
||||
$self->{_args}{add_headers} = 'true';
|
||||
$self->register_headers();
|
||||
is( $self->{_lastreg}, 'data_post,add_headers',
|
||||
'register_headers() registers data_post hook when explicitly enabled' );
|
||||
|
||||
$self->register_hook();
|
||||
$self->{_args}{add_headers} = 'false';
|
||||
$self->register_headers();
|
||||
is( $self->{_lastreg}, '',
|
||||
'register_headers() does not register data_post hook when disabled' );
|
||||
|
||||
}
|
||||
|
||||
sub test_register_genre_blocking {
|
||||
my ( $self ) = @_;
|
||||
|
||||
$self->register_hook();
|
||||
$self->register_genre_blocking();
|
||||
is( $self->{_lastreg}, '',
|
||||
'rcpt_handler not registered when no blocked genres are configured' );
|
||||
|
||||
$self->register_hook();
|
||||
$self->fake_config( p0f_blocked_operating_systems => 'Crapple Macintawsh' );
|
||||
$self->register_genre_blocking();
|
||||
is( $self->{_lastreg}, 'rcpt,rcpt_handler',
|
||||
'rcpt_handler registered when blocked genre phrases are configured' );
|
||||
is( join('|', @{ delete $self->{os_block} || [] }), 'Crapple Macintawsh',
|
||||
'blocked OS phrases processed correctly' );
|
||||
is( join('|', @{ delete $self->{os_block_re} || [] }), '',
|
||||
'no blocked OS regexes' );
|
||||
|
||||
$self->register_hook();
|
||||
$self->fake_config( p0f_blocked_operating_systems => '/windoze/' );
|
||||
$self->register_genre_blocking();
|
||||
is( $self->{_lastreg}, 'rcpt,rcpt_handler',
|
||||
'rcpt_handler registered when blocked genre regexes are configured' );
|
||||
is( join('|', @{ delete $self->{os_block} || [] }), '',
|
||||
'no blocked OS phrases' );
|
||||
is( join('|', @{ delete $self->{os_block_re} || [] }), qr/windoze/i,
|
||||
'blocked OS regexes processed correctly' );
|
||||
|
||||
$self->unfake_config;
|
||||
}
|
||||
|
||||
sub test_rcpt_handler {
|
||||
my ( $self ) = @_;
|
||||
|
||||
$self->{os_block} = ['Windows 7 or 8'];
|
||||
my ( $r, $msg ) = $self->rcpt_handler();
|
||||
is( return_code($r) . "/$msg", "DENY/OS Blocked",
|
||||
'rcpt_handler rejects on p0f genre match' );
|
||||
|
||||
$self->{os_block} = ['Commodor 16'];
|
||||
( $r, $msg ) = $self->rcpt_handler();
|
||||
$msg = 'undef' if ! defined $msg;
|
||||
is( return_code($r) . "/$msg", "DECLINED/undef",
|
||||
'rcpt_handler returns DECLINED on no p0f genre match' );
|
||||
|
||||
delete $self->{os_block};
|
||||
}
|
||||
|
||||
sub test_check_genre {
|
||||
my ( $self ) = @_;
|
||||
|
||||
$self->{os_block_re} = [qr/windows/i];
|
||||
ok( $self->check_genre, 'check_genre() returns true on OS match' );
|
||||
|
||||
$self->{os_block_re} = [qr/windoze/i];
|
||||
ok( ! $self->check_genre, 'check_genre() returns false for no OS match' );
|
||||
|
||||
delete $self->{os_block_re};
|
||||
}
|
||||
|
||||
sub test_exclude_connection {
|
||||
my ( $self ) = @_;
|
||||
$self->connection->relay_client(0);
|
||||
ok( ! $self->exclude_connection,
|
||||
'default: exclude no connections from genre check' );
|
||||
$self->connection->notes( p0f_exclude => undef );
|
||||
$self->connection->relay_client(1);
|
||||
ok( $self->exclude_connection, 'relay clients excluded from genre check' );
|
||||
}
|
||||
|
||||
sub test_exclude_recipient {
|
||||
my ( $self ) = @_;
|
||||
ok( ! $self->exclude_recipient({}),
|
||||
'default: exclude no recipients from genre check' );
|
||||
}
|
||||
|
||||
sub test_add_headers {
|
||||
|
Loading…
Reference in New Issue
Block a user