Well, that was surprisingly easy.
I moved the check for Socket6 and IO::Socket::INET6 to
lib/Qpsmtpd/Constants.pm and added a has_ipv6 function to return the status
of support.
Constants seemed the most universally used module.
require_resolvable_fromhost had it's own check for IPv6 removed, and is
included in this patch.
I'm not fully convinced check_relay is good enough.
It appears to work fine. However I guess IPv6 addresses could come in in
either upper or lower case.
I use forkserver, so that's what I modified.
IPv6 address must be specified within [].
It can listen on both v6 and v4 address, at the same time.
The LOCALADDR regexp should be better.
I can't get getaddrinfo to work, it always seems to return gibberish, so
unless qpsmtpd is listening on a v4 address TCPREMOTEHOST will always be
Unknown.
So far I've run several tests against it, sending mails from v4 addresses to
v4 and v6 ([::]) servers, and from v6 to v6 ([::] and [2001:...]) servers.
dns_bl doesn't have any issue, but there isn't much point in running that on
v6 addresses.
I'm not yet confident enough to run it in production :) Although I am going to
relay this via my local copy over IPv6 to our main server over v4
The patch is against branches/0.3x.
Ignore the SWALLOW in Constants.pm, it's not relevant to IPv6.
--
Mike Williams
Index: plugins/require_resolvable_fromhost
===================================================================
--- plugins/require_resolvable_fromhost (revision 656)
+++ plugins/require_resolvable_fromhost (working copy)
@@ -3,6 +3,7 @@
use Socket;
my %invalid = ();
+my $has_ipv6 = Qpsmtpd::Constants::has_ipv6;
sub hook_mail {
my ($self, $transaction, $sender, %param) = @_;
@@ -38,6 +39,7 @@
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 @@
}
my $query = $res->search($host);
if ($query) {
- foreach my $rr ($query->answer) {
- if ($rr->type eq "A") {
- return is_valid($rr->address);
+ 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);
}
- elsif ($rr->type eq "MX") {
- return mx_valid($self, $rr->exchange, $host);
- }
}
}
+ 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 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);
}
}
Index: plugins/check_relay
===================================================================
--- plugins/check_relay (revision 656)
+++ plugins/check_relay (working copy)
@@ -19,7 +19,7 @@
$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);
Index: lib/Qpsmtpd/Constants.pm
===================================================================
--- lib/Qpsmtpd/Constants.pm (revision 656)
+++ lib/Qpsmtpd/Constants.pm (working copy)
@@ -23,10 +23,28 @@
DENYHARD => 903, # 550 + disconnect (deprecated in 0.29)
DENY_DISCONNECT => 903, # 550 + disconnect
DENYSOFT_DISCONNECT => 904, # 450 + disconnect
+ SWALLOW => 905, # Lie, say it was queued, without queuing
DECLINED => 909,
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");
Index: qpsmtpd-forkserver
===================================================================
--- qpsmtpd-forkserver (revision 656)
+++ qpsmtpd-forkserver (working copy)
@@ -17,6 +17,12 @@
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 @@
) || &usage;
# detaint the commandline
[EMAIL PROTECTED] = ( '0.0.0.0' ) if [EMAIL PROTECTED];
+if ($has_ipv6) {
+ @LOCALADDR = ( '[::]' ) if [EMAIL PROTECTED];
+}
+else {
+ @LOCALADDR = ( '0.0.0.0' ) if [EMAIL PROTECTED];
+}
@PORT = ( 2525 ) if [EMAIL PROTECTED];
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{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 @@
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 @@
::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}";