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

Reply via email to