Merge pull request #165 from jaredj/p0f-block

Support list of client operating systems to block
This commit is contained in:
Matt Simerson 2014-12-11 15:11:48 -08:00
commit 91b9d026f8
4 changed files with 212 additions and 27 deletions

View File

@ -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) = @_;

View File

@ -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;

View File

@ -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 {

View File

@ -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 {