From bf1d6baf49b4445ab457bb92e8868536445bc73e Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Thu, 11 Dec 2014 13:48:27 -0600 Subject: [PATCH 01/14] Modularize registering optional p0f hooks --- plugins/ident/p0f | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/ident/p0f b/plugins/ident/p0f index d8d2597..aa6c9a3 100644 --- a/plugins/ident/p0f +++ b/plugins/ident/p0f @@ -166,6 +166,11 @@ sub register { foreach (keys %args) { $self->{_args}->{$_} = $args{$_}; } + $self->register_headers(); +} + +sub register_headers { + my ( $self ) = @_; my $enabled = $self->{_args}{add_headers}; $enabled = 'true' if ! defined $enabled; return if $enabled =~ /false/i; From cb2f0ca104c6ed48269912a10ddec6bb2acd6aa2 Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Thu, 11 Dec 2014 14:35:54 -0600 Subject: [PATCH 02/14] Block clients with OS matching phrases and regexes Not yet tested --- plugins/ident/p0f | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/plugins/ident/p0f b/plugins/ident/p0f index aa6c9a3..cd75e47 100644 --- a/plugins/ident/p0f +++ b/plugins/ident/p0f @@ -119,6 +119,19 @@ 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. + +Example entries: + +Windows XP +/windows/i + +Default: none (p0f rejections disabled) + =head1 Environment requirements p0f v3 requires only the remote IP. @@ -167,6 +180,7 @@ sub register { $self->{_args}->{$_} = $args{$_}; } $self->register_headers(); + $self->register_genre_blocking(); } sub register_headers { @@ -177,6 +191,51 @@ sub register_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 ( @pattern ) { + if ( $pattern =~ /^\/(.*)\/$/ ) { + push @{ $self->{os_block_re} }, qr/$1/; + } else { + push @{ $self->{os_block} }, qr/$1/; + } + } + $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) = @_; From 4547677135654040fafd3fa0e457aeb93ebe73a2 Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Thu, 11 Dec 2014 15:11:51 -0600 Subject: [PATCH 03/14] Tests for ident/p0f register_headers() --- t/plugin_tests/ident/p0f | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/t/plugin_tests/ident/p0f b/t/plugin_tests/ident/p0f index 8379140..fd25ded 100644 --- a/t/plugin_tests/ident/p0f +++ b/t/plugin_tests/ident/p0f @@ -13,6 +13,64 @@ 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 { +} + +sub test_rcpt_handler { +} + +sub test_check_genre { +} + +sub test_exclude_connection { +} + +sub test_exclude_recipient { } sub test_add_headers { From 45da3778971f051211fb36e871c6d3844341b91a Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Thu, 11 Dec 2014 15:12:17 -0600 Subject: [PATCH 04/14] Fix variable name in ident/p0f that caused a crash --- plugins/ident/p0f | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ident/p0f b/plugins/ident/p0f index cd75e47..13a7feb 100644 --- a/plugins/ident/p0f +++ b/plugins/ident/p0f @@ -195,7 +195,7 @@ sub register_genre_blocking { my ( $self ) = @_; my @patterns = $self->qp->config('p0f_blocked_operating_systems'); return unless @patterns; - for my $pattern ( @pattern ) { + for my $pattern ( @patterns ) { if ( $pattern =~ /^\/(.*)\/$/ ) { push @{ $self->{os_block_re} }, qr/$1/; } else { From 4f4e19ba0a242d37c3a13bcd7daa651b7809ecb6 Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Thu, 11 Dec 2014 15:31:23 -0600 Subject: [PATCH 05/14] Better fake_config() in t/plugin_tests/content_log --- t/plugin_tests/content_log | 40 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/t/plugin_tests/content_log b/t/plugin_tests/content_log index 63ad855..af1b26c 100644 --- a/t/plugin_tests/content_log +++ b/t/plugin_tests/content_log @@ -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,39 +64,40 @@ 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; + $self->unfake_config; } sub fake_config { - my ( $self, $value ) = @_; + my $self = shift; + my $fake_config = {@_}; $self->qp->hooks->{config} = [ { - name => 'test hook', - code => sub { return OK, $value }, + 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} || [] } + ]; +} + sub test_exclude { my ( $self ) = @_; ok( ! $self->exclude, 'exclude() default method returns false' ); From 0d5ec185a8615f56d2ef38e2d48dadb111e03ac9 Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Thu, 11 Dec 2014 15:33:34 -0600 Subject: [PATCH 06/14] Make fake_config() available to all plugin tests --- t/Test/Qpsmtpd/Plugin.pm | 23 +++++++++++++++++++++++ t/plugin_tests/content_log | 23 ----------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/t/Test/Qpsmtpd/Plugin.pm b/t/Test/Qpsmtpd/Plugin.pm index 0aedb78..6693824 100644 --- a/t/Test/Qpsmtpd/Plugin.pm +++ b/t/Test/Qpsmtpd/Plugin.pm @@ -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; diff --git a/t/plugin_tests/content_log b/t/plugin_tests/content_log index af1b26c..c2aba0c 100644 --- a/t/plugin_tests/content_log +++ b/t/plugin_tests/content_log @@ -75,29 +75,6 @@ sub test_content_log_enabled { $self->unfake_config; } -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} || [] } - ]; -} - sub test_exclude { my ( $self ) = @_; ok( ! $self->exclude, 'exclude() default method returns false' ); From 0a7c6f0f08c8796120e9f9e3740c81f32faaed2c Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Thu, 11 Dec 2014 16:06:54 -0600 Subject: [PATCH 07/14] Tests for ident/p0f register_genre_blocking() --- t/plugin_tests/ident/p0f | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/t/plugin_tests/ident/p0f b/t/plugin_tests/ident/p0f index fd25ded..509d14c 100644 --- a/t/plugin_tests/ident/p0f +++ b/t/plugin_tests/ident/p0f @@ -59,6 +59,34 @@ sub test_register_headers { } 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 { From 52c46cfec3d81e4ce61e0ee2e01c4e566fff2709 Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Thu, 11 Dec 2014 16:07:32 -0600 Subject: [PATCH 08/14] Fixes for ident/p0f register_genre_blocking - Make regexes case-insensitive (I don't want to mess with modifiers) - Don't compile non-regexes as regexes --- plugins/ident/p0f | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/ident/p0f b/plugins/ident/p0f index 13a7feb..3947d3e 100644 --- a/plugins/ident/p0f +++ b/plugins/ident/p0f @@ -123,7 +123,10 @@ Default: true =head2 p0f_blocked_operating_systems -If populated, systems that match the phrases and regular expressions in this list will be rejected. +If populated, systems that match the phrases and regular expressions +in this list will be rejected. + +Regular expressions are case-insensitive. Example entries: @@ -197,9 +200,9 @@ sub register_genre_blocking { return unless @patterns; for my $pattern ( @patterns ) { if ( $pattern =~ /^\/(.*)\/$/ ) { - push @{ $self->{os_block_re} }, qr/$1/; + push @{ $self->{os_block_re} }, qr/$1/i; } else { - push @{ $self->{os_block} }, qr/$1/; + push @{ $self->{os_block} }, $pattern; } } $self->register_hook( rcpt => 'rcpt_handler' ); From ec470ed08e60deddf7d3c3f7b32223e2ad3d880c Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Thu, 11 Dec 2014 16:29:07 -0600 Subject: [PATCH 09/14] Tests for ident/p0f rcpt_handler() --- t/plugin_tests/ident/p0f | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/t/plugin_tests/ident/p0f b/t/plugin_tests/ident/p0f index 509d14c..42039aa 100644 --- a/t/plugin_tests/ident/p0f +++ b/t/plugin_tests/ident/p0f @@ -90,6 +90,18 @@ sub test_register_genre_blocking { } 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' ); } sub test_check_genre { From 1e2c0a6f78f0710a91ea39683f4556444654d84f Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Thu, 11 Dec 2014 16:29:26 -0600 Subject: [PATCH 10/14] Fix missing assignment in ident/p0f rcpt_handler() --- plugins/ident/p0f | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ident/p0f b/plugins/ident/p0f index 3947d3e..22ab995 100644 --- a/plugins/ident/p0f +++ b/plugins/ident/p0f @@ -215,7 +215,7 @@ sub rcpt_handler { } sub check_genre { - my ( $self, $rcpt ); + 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); From dad4fb1d29413080272e364788e216353dff1cc6 Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Thu, 11 Dec 2014 16:49:49 -0600 Subject: [PATCH 11/14] Remove unnecessary // from check_genre() --- plugins/ident/p0f | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ident/p0f b/plugins/ident/p0f index 22ab995..cc2d91e 100644 --- a/plugins/ident/p0f +++ b/plugins/ident/p0f @@ -223,7 +223,7 @@ sub check_genre { return 1 if $p0f->{genre} eq $phrase; } for my $re ( @{ $self->{os_block_re} || [] } ) { - return 1 if $p0f->{genre} =~ /$re/; + return 1 if $p0f->{genre} =~ $re; } return 0; } From 4ad56e41173d8ffe9922ce88332b85b809f3ce87 Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Thu, 11 Dec 2014 16:50:06 -0600 Subject: [PATCH 12/14] Add tests for check_genre() --- t/plugin_tests/ident/p0f | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/t/plugin_tests/ident/p0f b/t/plugin_tests/ident/p0f index 42039aa..1f7cb97 100644 --- a/t/plugin_tests/ident/p0f +++ b/t/plugin_tests/ident/p0f @@ -102,9 +102,20 @@ sub test_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 { From a9e7d00177bc22e26a8f77577fdb4b4849d846de Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Thu, 11 Dec 2014 16:55:59 -0600 Subject: [PATCH 13/14] Tests exclude_connection() and exclude_recipient() --- t/plugin_tests/ident/p0f | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/t/plugin_tests/ident/p0f b/t/plugin_tests/ident/p0f index 1f7cb97..4f50a96 100644 --- a/t/plugin_tests/ident/p0f +++ b/t/plugin_tests/ident/p0f @@ -13,7 +13,6 @@ 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'); @@ -119,9 +118,19 @@ sub test_check_genre { } 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 { From 9f93087675a096be9543278af8a9491c0a6bb991 Mon Sep 17 00:00:00 2001 From: Jared Johnson Date: Thu, 11 Dec 2014 16:57:22 -0600 Subject: [PATCH 14/14] Remove modifier from POD --- plugins/ident/p0f | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ident/p0f b/plugins/ident/p0f index cc2d91e..59fe7c0 100644 --- a/plugins/ident/p0f +++ b/plugins/ident/p0f @@ -131,7 +131,7 @@ Regular expressions are case-insensitive. Example entries: Windows XP -/windows/i +/windows/ Default: none (p0f rejections disabled)