diff --git a/Changes b/Changes index 3368173..469de24 100644 --- a/Changes +++ b/Changes @@ -1,5 +1,7 @@ 0.33 + Experimental IPv6 support (forkserver only). (Mike Williams) + Support "module" plugins ("My::Plugin" in the config/plugins file) Enhance the spamassassin plugin to support connecting to a remote diff --git a/lib/Qpsmtpd/Constants.pm b/lib/Qpsmtpd/Constants.pm index 68bd8f6..4152131 100644 --- a/lib/Qpsmtpd/Constants.pm +++ b/lib/Qpsmtpd/Constants.pm @@ -27,6 +27,24 @@ my %return_codes = ( DONE => 910, ); +my $has_ipv6; + +if ( + eval {require Socket6;} && + # INET6 prior to 2.01 will not work; sorry. + eval {require IO::Socket::INET6; IO::Socket::INET6->VERSION("2.00");} + ) { + import Socket6; + $has_ipv6=1; +} +else { + $has_ipv6=0; +} + +sub has_ipv6 { + return $has_ipv6; +} + use vars qw(@ISA @EXPORT); @ISA = qw(Exporter); @EXPORT = (keys(%return_codes), keys(%log_levels), "return_code", "log_level"); diff --git a/plugins/check_relay b/plugins/check_relay index a79da91..e294c9d 100644 --- a/plugins/check_relay +++ b/plugins/check_relay @@ -19,7 +19,7 @@ sub hook_connect { $connection->relay_client(1); last; } - $client_ip =~ s/\d+\.?$//; # strip off another 8 bits + $client_ip =~ s/(\d|\w|::)+(:|\.)?$//; # strip off another 8 bits } return (DECLINED); diff --git a/plugins/require_resolvable_fromhost b/plugins/require_resolvable_fromhost index 2886b3f..78579e9 100644 --- a/plugins/require_resolvable_fromhost +++ b/plugins/require_resolvable_fromhost @@ -3,6 +3,7 @@ use Net::DNS qw(mx); use Socket; my %invalid = (); +my $has_ipv6 = Qpsmtpd::Constants::has_ipv6; sub hook_mail { my ($self, $transaction, $sender, %param) = @_; @@ -38,6 +39,7 @@ sub hook_mail { sub check_dns { my ($self, $host) = @_; + my @host_answers; # for stuff where we can't even parse a hostname out of the address return 0 unless $host; @@ -53,15 +55,24 @@ sub check_dns { } my $query = $res->search($host); if ($query) { - foreach my $rr ($query->answer) { - if ($rr->type eq "A") { - return is_valid($rr->address); - } - elsif ($rr->type eq "MX") { - return mx_valid($self, $rr->exchange, $host); + foreach my $rrA ($query->answer) { + push(@host_answers, $rrA); + } + } + if ($has_ipv6) { + my $query = $res->search($host, 'AAAA'); + if ($query) { + foreach my $rrAAAA ($query->answer) { + push(@host_answers, $rrAAAA); } } } + if (@host_answers) { + foreach my $rr (@host_answers) { + return is_valid($rr->address) if $rr->type eq "A" or $rr->type eq "AAAA"; + return mx_valid($self, $rr->exchange, $host) if $rr->type eq "MX"; + } + } else { $self->log(LOGWARN, "$$ query for $host failed: ", $res->errorstring) unless $res->errorstring eq "NXDOMAIN"; @@ -88,10 +99,24 @@ sub is_valid { sub mx_valid { my ($self, $name, $host) = @_; my $res = new Net::DNS::Resolver; - my $query = $res->search($name); + my @mx_answers; + my $query = $res->search($name, 'A'); if ($query) { - foreach my $rr ($query->answer) { - next unless $rr->type eq "A"; + foreach my $rrA ($query->answer) { + push(@mx_answers, $rrA); + } + } + if ($has_ipv6) { + my $query = $res->search($name, 'AAAA'); + if ($query) { + foreach my $rrAAAA ($query->answer) { + push(@mx_answers, $rrAAAA); + } + } + } + if (@mx_answers) { + foreach my $rr (@mx_answers) { + next unless $rr->type eq "A" or $rr->type eq "AAAA"; return is_valid($rr->address); } } diff --git a/qpsmtpd-forkserver b/qpsmtpd-forkserver index b836255..e9701a5 100755 --- a/qpsmtpd-forkserver +++ b/qpsmtpd-forkserver @@ -17,6 +17,12 @@ use POSIX qw(:sys_wait_h :errno_h :signal_h); use strict; $| = 1; +my $has_ipv6 = Qpsmtpd::Constants::has_ipv6; + +if ($has_ipv6) { + use Socket6; +} + # Configuration my $MAXCONN = 15; # max simultaneous connections my @PORT; # port number(s) @@ -54,12 +60,17 @@ GetOptions('h|help' => \&usage, ) || &usage; # detaint the commandline -@LOCALADDR = ( '0.0.0.0' ) if !@LOCALADDR; +if ($has_ipv6) { + @LOCALADDR = ( '[::]' ) if !@LOCALADDR; +} +else { + @LOCALADDR = ( '0.0.0.0' ) if !@LOCALADDR; +} @PORT = ( 2525 ) if !@PORT; my @LISTENADDR; for (0..$#LOCALADDR) { - if ($LOCALADDR[$_] =~ /^([\d\w\-.]+)(?::(\d+))?$/) { + if ($LOCALADDR[$_] =~ /^(\[.*\]|[\d\w\-.]+)(?::(\d+))?$/) { if ( defined $2 ) { push @LISTENADDR, { 'addr' => $1, 'port' => $2 }; } else { @@ -106,16 +117,24 @@ $SIG{INT} = \&HUNTSMAN; $SIG{TERM} = \&HUNTSMAN; my $select = new IO::Select; +my $server; # establish SERVER socket(s), bind and listen. for my $listen_addr (@LISTENADDR) { - my $server = IO::Socket::INET->new(LocalPort => $listen_addr->{'port'}, + my @Socket_opts = (LocalPort => $listen_addr->{'port'}, LocalAddr => $listen_addr->{'addr'}, Proto => 'tcp', Reuse => 1, Blocking => 0, - Listen => SOMAXCONN ) - or die "Creating TCP socket $listen_addr->{'addr'}:$listen_addr->{'port'}: $!\n"; + Listen => SOMAXCONN); + if ($has_ipv6) { + $server = IO::Socket::INET6->new(@Socket_opts) + or die "Creating TCP socket $listen_addr->{'addr'}:$listen_addr->{'port'}: $!\n"; + } + else { + $server = IO::Socket::INET->new(@Socket_opts) + or die "Creating TCP socket $listen_addr->{'addr'}:$listen_addr->{'port'}: $!\n"; + } IO::Handle::blocking($server, 0); $select->add($server); } @@ -208,14 +227,19 @@ while (1) { next; } IO::Handle::blocking($client, 1); - my ($port, $iaddr) = sockaddr_in($hisaddr); + my ($port, $iaddr) = ($server->sockdomain == AF_INET) ? (sockaddr_in($hisaddr)) : (sockaddr_in6($hisaddr)); my $localsockaddr = getsockname($client); - my ($lport, $laddr) = sockaddr_in($localsockaddr); + my ($lport, $laddr) = ($server->sockdomain == AF_INET) ? (sockaddr_in($localsockaddr)) : (sockaddr_in6($localsockaddr)); + my $nto_iaddr = ($server->sockdomain == AF_INET) ? (inet_ntoa($iaddr)) : (inet_ntop(AF_INET6, $iaddr)); + my $ton_iaddr = ($server->sockdomain == AF_INET) ? (inet_aton($iaddr)) : (inet_pton(AF_INET6, $iaddr)); + my $nto_laddr = ($server->sockdomain == AF_INET) ? (inet_ntoa($laddr)) : (inet_ntop(AF_INET6, $laddr)); + $nto_iaddr =~ s/::ffff://; + $nto_laddr =~ s/::ffff://; my ($rc, @msg) = $qpsmtpd->run_hooks("pre-connection", - remote_ip => inet_ntoa($iaddr), + remote_ip => $nto_iaddr, remote_port => $port, - local_ip => inet_ntoa($laddr), + local_ip => $nto_laddr, local_port => $lport, max_conn_ip => $MAXCONNIP, child_addrs => [values %childstatus], @@ -259,11 +283,18 @@ while (1) { ::log(LOGINFO, "Connection Timed Out"); exit; }; - $ENV{TCPLOCALIP} = inet_ntoa($laddr); + $ENV{TCPLOCALIP} = $nto_laddr; # my ($port, $iaddr) = sockaddr_in($hisaddr); - $ENV{TCPREMOTEIP} = inet_ntoa($iaddr); - $ENV{TCPREMOTEHOST} = gethostbyaddr($iaddr, AF_INET) || "Unknown"; - + $ENV{TCPREMOTEIP} = $nto_iaddr; + + if ($server->sockdomain == AF_INET) { + $ENV{TCPREMOTEHOST} = gethostbyaddr($iaddr, AF_INET) || "Unknown"; + } + else { + my ($family, $socktype, $proto, $saddr, $canonname, @res) = getaddrinfo($iaddr, $port, AF_UNSPEC); + $ENV{TCPREMOTEHOST} = $canonname || "Unknown"; + } + # don't do this! #$0 = "qpsmtpd-forkserver: $ENV{TCPREMOTEIP} / $ENV{TCPREMOTEHOST}";