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
|
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
|
=head1 Environment requirements
|
||||||
|
|
||||||
p0f v3 requires only the remote IP.
|
p0f v3 requires only the remote IP.
|
||||||
@ -166,12 +182,63 @@ sub register {
|
|||||||
foreach (keys %args) {
|
foreach (keys %args) {
|
||||||
$self->{_args}->{$_} = $args{$_};
|
$self->{_args}->{$_} = $args{$_};
|
||||||
}
|
}
|
||||||
|
$self->register_headers();
|
||||||
|
$self->register_genre_blocking();
|
||||||
|
}
|
||||||
|
|
||||||
|
sub register_headers {
|
||||||
|
my ( $self ) = @_;
|
||||||
my $enabled = $self->{_args}{add_headers};
|
my $enabled = $self->{_args}{add_headers};
|
||||||
$enabled = 'true' if ! defined $enabled;
|
$enabled = 'true' if ! defined $enabled;
|
||||||
return if $enabled =~ /false/i;
|
return if $enabled =~ /false/i;
|
||||||
$self->register_hook( data_post => 'add_headers' );
|
$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 {
|
sub hook_connect {
|
||||||
my ($self, $qp) = @_;
|
my ($self, $qp) = @_;
|
||||||
|
|
||||||
|
@ -84,4 +84,27 @@ sub validate_password {
|
|||||||
return $deny, "$file - wrong 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;
|
1;
|
||||||
|
@ -16,12 +16,11 @@ sub test_content_log_file {
|
|||||||
is( $self->content_log_file,
|
is( $self->content_log_file,
|
||||||
strftime('mail/%Y%m%d', localtime(time)),
|
strftime('mail/%Y%m%d', localtime(time)),
|
||||||
'content_log_file() returns the right default value' );
|
'content_log_file() returns the right default value' );
|
||||||
$self->save_hook;
|
$self->fake_config( content_log_file => '/path/to/%Y%m%d%H%M' );
|
||||||
$self->fake_config('/path/to/%Y%m%d%H%M');
|
|
||||||
is( $self->content_log_file,
|
is( $self->content_log_file,
|
||||||
strftime('/path/to/%Y%m%d%H%M', localtime(time)),
|
strftime('/path/to/%Y%m%d%H%M', localtime(time)),
|
||||||
'content_log_file config changes content_log_file() output' );
|
'content_log_file config changes content_log_file() output' );
|
||||||
$self->restore_hook;
|
$self->unfake_config;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub test_content_log_enabled {
|
sub test_content_log_enabled {
|
||||||
@ -65,37 +64,15 @@ sub test_content_log_enabled {
|
|||||||
expected => 0,
|
expected => 0,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
$self->save_hook;
|
|
||||||
for ( @test_data ) {
|
for ( @test_data ) {
|
||||||
my $descr = "content_log_enabled="
|
my $descr = "content_log_enabled="
|
||||||
. ( defined $_->{config } ? "'$_->{config }'" : 'undef' )
|
. ( defined $_->{config } ? "'$_->{config }'" : 'undef' )
|
||||||
. ( $_->{expected} ? ' enables' : ' disables' )
|
. ( $_->{expected} ? ' enables' : ' disables' )
|
||||||
. ' content logging';
|
. ' content logging';
|
||||||
$self->fake_config( $_->{config } );
|
$self->fake_config( content_log_enabled => $_->{config } );
|
||||||
is( $self->content_log_enabled, $_->{expected}, $descr );
|
is( $self->content_log_enabled, $_->{expected}, $descr );
|
||||||
}
|
}
|
||||||
$self->restore_hook;
|
$self->unfake_config;
|
||||||
}
|
|
||||||
|
|
||||||
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 },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub test_exclude {
|
sub test_exclude {
|
||||||
|
@ -13,6 +13,124 @@ sub register_tests {
|
|||||||
$self->register_test('test_get_v3_query');
|
$self->register_test('test_get_v3_query');
|
||||||
$self->register_test('test_store_v2_results');
|
$self->register_test('test_store_v2_results');
|
||||||
$self->register_test('test_store_v3_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 {
|
sub test_add_headers {
|
||||||
|
Loading…
Reference in New Issue
Block a user