added Utils->is_valid_ip, IPv6 ready

resolves Issue #82
This commit is contained in:
Matt Simerson 2014-09-11 13:34:32 -07:00
parent 5960cb4d87
commit ca96ddf4eb
5 changed files with 86 additions and 29 deletions

View File

@ -1,6 +1,8 @@
package Qpsmtpd::Utils; package Qpsmtpd::Utils;
use strict; use strict;
use Net::IP;
sub tildeexp { sub tildeexp {
my ($self, $path) = @_; my ($self, $path) = @_;
$path =~ s{^~([^/]*)} { $path =~ s{^~([^/]*)} {
@ -20,4 +22,18 @@ sub is_localhost {
return; return;
} }
sub is_valid_ip {
my ($self, $ip) = @_;
if (Net::IP::ip_is_ipv4($ip)) {
return if $ip eq '0.0.0.0';
return if $ip eq '255.255.255.255';
return if $ip =~ /255/;
return 1;
};
return 1 if Net::IP::ip_is_ipv6($ip);
return;
}
1; 1;

View File

@ -227,7 +227,10 @@ additional check ideas from Haraka helo plugin
use strict; use strict;
use warnings; use warnings;
use Net::IP;
use Qpsmtpd::Constants; use Qpsmtpd::Constants;
use Qpsmtpd::Utils;
sub register { sub register {
my ($self, $qp) = (shift, shift); my ($self, $qp) = (shift, shift);
@ -354,8 +357,7 @@ sub invalid_localhost {
sub is_plain_ip { sub is_plain_ip {
my ($self, $host) = @_; my ($self, $host) = @_;
return if $host =~ /[^\d\.]+/; # has chars other than digits and a dot return if !Qpsmtpd::Utils->is_valid_ip($host);
return if $host !~ m/^(\d{1,3}\.){3}\d{1,3}$/;
$self->log(LOGDEBUG, "fail, plain IP"); $self->log(LOGDEBUG, "fail, plain IP");
return ("Plain IP is invalid HELO hostname (RFC 2821)", "plain IP"); return ("Plain IP is invalid HELO hostname (RFC 2821)", "plain IP");
@ -363,7 +365,11 @@ sub is_plain_ip {
sub is_address_literal { sub is_address_literal {
my ($self, $host) = @_; my ($self, $host) = @_;
return if $host !~ m/^\[(\d{1,3}\.){3}\d{1,3}\]$/;
my ($ip) = $host =~ /^\[(.*)\]/; # strip off any brackets
return if !$ip; # no brackets, not a literal
return if !Qpsmtpd::Utils->is_valid_ip($ip);
$self->log(LOGDEBUG, "fail, bracketed IP"); $self->log(LOGDEBUG, "fail, bracketed IP");
return ("RFC 2821 allows an address literal, but we do not", return ("RFC 2821 allows an address literal, but we do not",
@ -372,9 +378,9 @@ sub is_address_literal {
sub is_forged_literal { sub is_forged_literal {
my ($self, $host) = @_; my ($self, $host) = @_;
return if $host !~ m/^\[(\d{1,3}\.){3}\d{1,3}\]$/; return if !Qpsmtpd::Utils->is_valid_ip($host);
# should we add exceptions for reserved internal IP space? (192.168,10., etc?) # should we add exceptions for reserved internal IP space? (192.168,10., etc)
$host = substr $host, 1, -1; $host = substr $host, 1, -1;
return if $host eq $self->qp->connection->remote_ip; return if $host eq $self->qp->connection->remote_ip;
return ("Forged IPs not accepted here", "forged IP literal"); return ("Forged IPs not accepted here", "forged IP literal");
@ -486,15 +492,25 @@ sub check_ip_match {
my $self = shift; my $self = shift;
my $ip = shift or return; my $ip = shift or return;
if ($ip eq $self->qp->connection->remote_ip) { my $rip = $self->qp->connection->remote_ip;
if ($ip eq $rip) {
$self->log(LOGDEBUG, "forward ip match"); $self->log(LOGDEBUG, "forward ip match");
$self->connection->notes('helo_forward_match', 1); $self->connection->notes('helo_forward_match', 1);
return; return;
} }
my $dns_net = join('.', (split(/\./, $ip))[0, 1, 2]); my ($dns_net, $rem_net);
my $rem_net = if ($ip =~ /:/) {
join('.', (split(/\./, $self->qp->connection->remote_ip))[0, 1, 2]); if ($ip =~ /::/) { $ip = Net::IP::ip_expand_address($ip, 6); }
if ($rip =~ /::/) { $rip = Net::IP::ip_expand_address($rip, 6); }
$dns_net = join(':', (split(/:/, $ip ))[0, 1, 2, 3, 4, 5]);
$rem_net = join(':', (split(/:/, $rip))[0, 1, 2, 3, 4, 5]);
}
else {
$dns_net = join('.', (split(/\./, $ip))[0, 1, 2]);
$rem_net = join('.', (split(/\./, $rip))[0, 1, 2]);
}
if ($dns_net eq $rem_net) { if ($dns_net eq $rem_net) {
$self->log(LOGNOTICE, "forward network match"); $self->log(LOGNOTICE, "forward network match");

View File

@ -183,13 +183,12 @@ sub check_dns {
sub ip_is_valid { sub ip_is_valid {
my ($self, $ip) = @_; my ($self, $ip) = @_;
my ($net, $mask);
### while (($net,$mask) = each %invalid) { ### while (($net,$mask) = each %invalid) {
### ... does NOT reset to beginning, will start on ### ... does NOT reset to beginning, will start on
### 2nd invocation after where it denied the first time..., so ### 2nd invocation after where it denied the first time..., so
### 2nd time the same "MAIL FROM" would be accepted! ### 2nd time the same "MAIL FROM" would be accepted!
foreach $net (keys %invalid) { foreach my $net (keys %invalid) {
$mask = $invalid{$net}; my $mask = $invalid{$net};
$mask = pack "B32", "1" x ($mask) . "0" x (32 - $mask); $mask = pack "B32", "1" x ($mask) . "0" x (32 - $mask);
return if $net eq join('.', unpack("C4", inet_aton($ip) & $mask)); return if $net eq join('.', unpack("C4", inet_aton($ip) & $mask));
} }

View File

@ -18,7 +18,7 @@ sub register_tests {
$self->register_test('test_no_reverse_dns', 3); $self->register_test('test_no_reverse_dns', 3);
$self->register_test('test_no_matching_dns', 2); $self->register_test('test_no_matching_dns', 2);
$self->register_test('test_helo_handler', 1); $self->register_test('test_helo_handler', 1);
$self->register_test('test_check_ip_match', 3); $self->register_test('test_check_ip_match', 6);
$self->register_test('test_check_name_match', 3); $self->register_test('test_check_name_match', 3);
} }
@ -82,26 +82,26 @@ sub test_invalid_localhost {
sub test_is_plain_ip { sub test_is_plain_ip {
my $self = shift; my $self = shift;
my ($err, $why) = $self->is_plain_ip('0.0.0.0'); my ($err, $why) = $self->is_plain_ip('1.0.0.0');
ok( $err, "plain IP, $why"); ok( $err, "plain IP, $why");
($err, $why) = $self->is_plain_ip('255.255.255.255'); ($err, $why) = $self->is_plain_ip('254.254.254.254');
ok( $err, "plain IP, $why"); ok( $err, "plain IP, $why");
($err, $why) = $self->is_plain_ip('[255.255.255.255]'); ($err, $why) = $self->is_plain_ip('[254.254.254.254]');
ok( ! $err, "address literal"); ok( ! $err, "address literal");
}; };
sub test_is_address_literal { sub test_is_address_literal {
my $self = shift; my $self = shift;
my ($err, $why) = $self->is_address_literal('[0.0.0.0]'); my ($err, $why) = $self->is_address_literal('[1.0.0.0]');
ok( $err, "plain IP, $why"); ok( $err, "plain IP, $why");
($err, $why) = $self->is_address_literal('[255.255.255.255]'); ($err, $why) = $self->is_address_literal('[254.254.254.254]');
ok( $err, "plain IP, $why"); ok( $err, "plain IP, $why");
($err, $why) = $self->is_address_literal('255.255.255.255'); ($err, $why) = $self->is_address_literal('254.254.254.254');
ok( ! $err, "address literal"); ok( ! $err, "address literal");
}; };
@ -146,19 +146,32 @@ sub test_no_matching_dns {
sub test_check_ip_match { sub test_check_ip_match {
my $self = shift; my $self = shift;
$self->qp->connection->remote_ip('192.0.2.1'); my @good_tests = (
{ ip => '192.0.2.1', ip2 => '192.0.2.1', r => 'exact' },
{ ip => '192.0.2.1', ip2 => '192.0.2.2', r => 'network' },
{ ip => '2001:db8::1', ip2 => '2001:db8::1', r => 'exact' },
{ ip => '2001:db8::1', ip2 => '2001:db8::2', r => 'network' },
);
my @bad_tests = (
{ ip => '192.0.2.1', ip2 => '192.0.1.1', r => 'miss' },
{ ip => '2001:db8::1', ip2 => '2001:db7::1', r => 'miss' },
);
foreach my $t ( @good_tests ) {
$self->qp->connection->remote_ip($t->{ip});
$self->connection->notes('helo_forward_match', 0);
$self->check_ip_match($t->{ip2});
ok( $self->connection->notes('helo_forward_match'), $t->{r});
};
foreach my $t ( @bad_tests ) {
$self->qp->connection->remote_ip($t->{ip});
$self->connection->notes('helo_forward_match', 0); $self->connection->notes('helo_forward_match', 0);
$self->check_ip_match('192.0.2.1'); $self->check_ip_match($t->{ip2});
ok( $self->connection->notes('helo_forward_match'), "exact"); ok( ! $self->connection->notes('helo_forward_match'), $t->{r});
};
$self->connection->notes('helo_forward_match', 0);
$self->check_ip_match('192.0.2.2');
ok( $self->connection->notes('helo_forward_match'), "network");
$self->connection->notes('helo_forward_match', 0);
$self->check_ip_match('192.0.1.1');
ok( ! $self->connection->notes('helo_forward_match'), "miss");
}; };
sub test_check_name_match { sub test_check_name_match {

View File

@ -12,9 +12,22 @@ my $utils = bless {}, 'Qpsmtpd::Utils';
__tildeexp(); __tildeexp();
__is_localhost(); __is_localhost();
__is_valid_ip();
done_testing(); done_testing();
sub __is_valid_ip {
my @good = qw/ 1.2.3.4 1.0.0.0 254.254.254.254 2001:db8:ffff:ffff:ffff:ffff:ffff:ffff /;
foreach my $ip ( @good ) {
ok( $utils->is_valid_ip($ip), "is_valid_ip: $ip");
}
my @bad = qw/ 1.2.3.256 256.1.1.1 2001:db8:ffff:ffff:ffff:ffff:ffff:fffj /;
foreach my $ip ( @bad ) {
ok( !$utils->is_valid_ip($ip), "is_valid_ip, neg: $ip");
}
};
sub __is_localhost { sub __is_localhost {
for my $local_ip (qw/ 127.0.0.1 ::1 2607:f060:b008:feed::127.0.0.1 127.0.0.2 /) { for my $local_ip (qw/ 127.0.0.1 ::1 2607:f060:b008:feed::127.0.0.1 127.0.0.2 /) {