replaces functionality of previous 3 relay plugins --- config.sample/norelayclients | 5 + config.sample/plugins | 8 +- plugins/relay | 237 ++++++++++++++++++++++++++++++++++++++++++ t/plugin_tests/relay | 81 +++++++++++++++ 4 files changed, 327 insertions(+), 4 deletions(-) create mode 100644 config.sample/norelayclients create mode 100644 plugins/relay create mode 100644 t/plugin_tests/relay
diff --git a/config.sample/norelayclients b/config.sample/norelayclients new file mode 100644 index 0000000..0ad5e1a --- /dev/null +++ b/config.sample/norelayclients @@ -0,0 +1,5 @@ +# sample entries, used for testing +192.168.99.5 +192.168.99.6 +192.168.98. +# add your own entries below... diff --git a/config.sample/plugins b/config.sample/plugins index 67a85bd..ca92a78 100644 --- a/config.sample/plugins +++ b/config.sample/plugins @@ -27,7 +27,7 @@ quit_fortune #tls check_earlytalker count_unrecognized_commands 4 -check_relay +relay require_resolvable_fromhost @@ -84,6 +84,6 @@ dspam learn_from_sa 7 reject 1 # If you need to run the same plugin multiple times, you can do # something like the following -# check_relay -# check_relay:0 somearg -# check_relay:1 someotherarg +# relay +# relay:0 somearg +# relay:1 someotherarg diff --git a/plugins/relay b/plugins/relay new file mode 100644 index 0000000..d8a643b --- /dev/null +++ b/plugins/relay @@ -0,0 +1,237 @@ +#!perl -w + +=head1 SYNOPSIS + +relay - control whether relaying is permitted + +=head1 DESCRIPTION + +relay - check the following places to see if relaying is allowed: + +I<$ENV{RELAYCLIENT}> + +I<config/norelayclients>, I<config/relayclients>, I<config/morerelayclients> + +The search order is as shown and cascades until a match is found or the list +is exhausted. + +Note that I<norelayclients> is the first file checked. A match there will +override matches in the subsequent files. + +=head1 CONFIG + +Enable this plugin by adding it to config/plugins above the rcpt_* plugins + + # other plugins... + + relay + + # rcpt_* go here + +=head2 relayclients + +A list of IP addresses that are permitted to relay mail through this server. + +Each line in I<relayclients> is one of: + - a full IP address + + - partial IP address terminated by a dot or colon for matching whole networks + 192.168.42. + fdda:b13d:e431:ae06: + ... + + - a network/mask, aka a CIDR block + 10.1.0.0/24 + fdda:b13d:e431:ae06::/64 + ... + +=head2 morerelayclients + +Additional IP addresses that are permitted to relay. The syntax of the config +file is identical to I<relayclients> except that CIDR (net/mask) entries are +not supported. If you have many (>50) IPs allowed to relay, most should likely +be listed in I<morerelayclients> where lookups are faster. + + +=head2 norelayclients + +I<norelayclients> allows specific clients, such as a mail gateway, to be denied +relaying, even though they would be allowed by I<relayclients>. This is most +useful when a block of IPs is allowed in relayclients, but several IPs need to +be excluded. + +The file format is the same as morerelayclients. + +=head2 RELAY ONLY + +The relay only option restricts connections to only clients that have relay +permission. All other connections are denied during the RCPT phase of the +SMTP conversation. + +This option is useful when a server is used as the smart relay host for +internal users and external/authenticated users, but should not be considered +a normal inbound MX server. + +It should be configured to be run before other RCPT hooks! Only clients that +have authenticated or are listed in the relayclient file will be allowed to +send mail. + +To enable relay only mode, set the B<only> option to any true value in +I<config/plugins> as shown: + + relay only 1 + +=head1 AUTHOR + +2012 - Matt Simerson - Merged check_relay, check_norelay, and relayonly + +2005 - check_norelay - Copyright Gordon Rowell <gord...@gormand.com.au> + +200? - check_relay plugin + +200? - relay_only plugin + +=head1 LICENSE + +This software is free software and may be distributed under the same +terms as qpsmtpd itself. + +=cut + +use strict; +use warnings; + +use Qpsmtpd::Constants; +use Net::IP qw(:PROC); + +sub register { + my ($self, $qp) = shift, shift; + $self->log(LOGERROR, "Bad arguments") if @_ % 2; + $self->{_args} = { @_ }; + + if ( $self->{_args}{only} ) { + $self->register_hook('rcpt', 'relay_only'); + }; +}; + +sub is_in_norelayclients { + my $self = shift; + + my %no_relay_clients = map { $_ => 1 } $self->qp->config('norelayclients'); + + my $ip = $self->qp->connection->remote_ip; + + while ( $ip ) { + if ( exists $no_relay_clients{$ip} ) { + $self->log(LOGNOTICE, "$ip in norelayclients"); + return 1; + } + $ip =~ s/(\d|\w)+(:|\.)?$// or last; # strip off another octet + }; + + $self->log(LOGDEBUG, "no match in norelayclients"); + return; +}; + +sub populate_relayclients { + my $self = shift; + + foreach ( $self->qp->config('relayclients') ) { + my ($network, $netmask) = ip_splitprefix($_); + if ( $netmask ) { + push @{ $self->{_cidr_blocks} }, $_; + next; + } + $self->{_octets}{$_} = 1; # no prefix, split + } +}; + +sub is_in_cidr_block { + my $self = shift; + + my $ip = $self->qp->connection->remote_ip; + my $cversion = ip_get_version($ip); + for ( @{ $self->{_cidr_blocks} } ) { + my ($network, $mask) = ip_splitprefix($_); # split IP & CIDR range + my $rversion = ip_get_version($network); # get IP version (4 vs 6) + my ($begin, $end) = ip_normalize($_, $rversion); # get pool start/end + +# expand the client address (zero pad it) before converting to binary + my $bin_ip = ip_iptobin(ip_expand_address($ip, $cversion), $cversion); + + if ( ip_bincomp($bin_ip, 'gt', ip_iptobin($begin, $rversion)) + && ip_bincomp($bin_ip, 'lt', ip_iptobin($end, $rversion)) + ) { + $self->log(LOGINFO, "pass: cidr match ($ip)"); + return 1; + } + } + + $self->log(LOGDEBUG, "no cidr match"); + return; +}; + +sub is_octet_match { + my $self = shift; + + my $ip = $self->qp->connection->remote_ip; + $ip =~ s/::/:/; + + if ( $ip eq ':1' ) { + $self->log(LOGINFO, "pass: octet matched localhost ($ip)"); + return 1; + }; + + my $more_relay_clients = $self->qp->config('morerelayclients', 'map'); + + while ($ip) { + if ( exists $self->{_octets}{$ip} ) { + $self->log(LOGINFO, "pass: octet match in relayclients ($ip)"); + return 1; + }; + + if ( exists $more_relay_clients->{$ip} ) { + $self->log(LOGINFO, "pass: octet match in morerelayclients ($ip)"); + return 1; + }; + $ip =~ s/(\d|\w)+(:|\.)?$// or last; # strip off another 8 bits + } + + $self->log(LOGDEBUG, "no octet match" ); + return; +} + +sub hook_connect { + my ($self, $transaction) = @_; + + if ( $self->is_in_norelayclients() ) { + $self->qp->connection->relay_client(0); + delete $ENV{RELAYCLIENT}; + return (DECLINED); + } + + if ( $ENV{RELAYCLIENT} ) { + $self->qp->connection->relay_client(1); + $self->log(LOGINFO, "pass: enabled by env"); + return (DECLINED); + }; + + $self->populate_relayclients(); + + if ( $self->is_in_cidr_block() || $self->is_octet_match() ) { + $self->qp->connection->relay_client(1); + return (DECLINED); + }; + + $self->log(LOGINFO, "skip: no match"); + return (DECLINED); +} + +sub relay_only { + my $self = shift; + if ( $self->qp->connection->relay_client ) { + return (OK); + }; + return (DENY); +} + diff --git a/t/plugin_tests/relay b/t/plugin_tests/relay new file mode 100644 index 0000000..3d1b91e --- /dev/null +++ b/t/plugin_tests/relay @@ -0,0 +1,81 @@ +#!perl -w + +use strict; +use warnings; + +use Qpsmtpd::Constants; + +sub register_tests { + my $self = shift; + + $self->register_test('test_relay_only', 2); + $self->register_test('test_is_octet_match', 3); + $self->register_test('test_is_in_cidr_block', 4); + $self->register_test('test_is_in_norelayclients', 5); +} + +sub test_relay_only { + my $self = shift; + + $self->qp->connection->relay_client(0); + my $r = $self->relay_only(); + cmp_ok( $r, '==', DENY, "relay_only -"); + + $self->qp->connection->relay_client(1); + $r = $self->relay_only(); + cmp_ok( $r, '==', OK, "relay_only +"); + + $self->qp->connection->relay_client(0); +}; + +sub test_is_octet_match { + my $self = shift; + + $self->populate_relayclients(); + + $self->qp->connection->remote_ip('192.168.1.1'); + ok( $self->is_octet_match(), "match, +"); + + $self->qp->connection->remote_ip('192.169.1.1'); + ok( ! $self->is_octet_match(), "nope, -"); + + $self->qp->connection->remote_ip('10.10.10.10'); + ok( ! $self->is_octet_match(), "nope, -"); +}; + +sub test_is_in_cidr_block { + my $self = shift; + + $self->qp->connection->remote_ip('192.168.1.1'); + $self->{_cidr_blocks} = [ '192.168.1.0/24' ]; + ok( $self->is_in_cidr_block(), "match, +" ); + + $self->{_cidr_blocks} = [ '192.168.0.0/24' ]; + ok( ! $self->is_in_cidr_block(), "nope, -" ); + + + $self->qp->connection->remote_ip('fdda:b13d:e431:ae06:00a1::'); + $self->{_cidr_blocks} = [ 'fdda:b13d:e431:ae06::/64' ]; + ok( $self->is_in_cidr_block(), "match, +" ); + + $self->{_cidr_blocks} = [ 'fdda:b13d:e431:be17::' ]; + ok( ! $self->is_in_cidr_block(), "nope, -" ); +}; + +sub test_is_in_norelayclients { + my $self = shift; + + my @matches = qw/ 192.168.99.5 192.168.98.1 192.168.98.255 /; + my @false = qw/ 192.168.99.7 192.168.109.7 /; + + foreach ( @matches ) { + $self->qp->connection->remote_ip($_); + ok( $self->is_in_norelayclients(), "match, + ($_)"); + }; + + foreach ( @false ) { + $self->qp->connection->remote_ip($_); + ok( ! $self->is_in_norelayclients(), "match, + ($_)"); + }; +}; + -- 1.7.9.6