new relay plugin, with tests!
replaces functionality of previous 3 relay plugins
This commit is contained in:
parent
bf5f1db436
commit
974f1a95e8
5
config.sample/norelayclients
Normal file
5
config.sample/norelayclients
Normal file
@ -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...
|
@ -32,7 +32,7 @@ quit_fortune
|
||||
#tls
|
||||
check_earlytalker
|
||||
count_unrecognized_commands 4
|
||||
check_relay
|
||||
relay
|
||||
|
||||
require_resolvable_fromhost
|
||||
|
||||
@ -85,6 +85,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
|
||||
|
237
plugins/relay
Normal file
237
plugins/relay
Normal file
@ -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 <gordonr@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);
|
||||
}
|
||||
|
81
t/plugin_tests/relay
Normal file
81
t/plugin_tests/relay
Normal file
@ -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, + ($_)");
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user