diff --git a/lib/Qpsmtpd/Constants.pm b/lib/Qpsmtpd/Constants.pm index 0480d58..ccd8440 100644 --- a/lib/Qpsmtpd/Constants.pm +++ b/lib/Qpsmtpd/Constants.pm @@ -29,24 +29,6 @@ my %return_codes = ( YIELD => 911, ); -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/lib/Qpsmtpd/TcpServer.pm b/lib/Qpsmtpd/TcpServer.pm index d79423f..8a1dbd5 100644 --- a/lib/Qpsmtpd/TcpServer.pm +++ b/lib/Qpsmtpd/TcpServer.pm @@ -8,6 +8,23 @@ use strict; use POSIX (); +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; +} + my $first_0; sub start_connection { @@ -104,4 +121,43 @@ sub disconnect { exit; } +# local/remote port and ip address +sub lrpip { + my ($server, $client, $hisaddr) = @_; + + my ($port, $iaddr) = ($server->sockdomain == AF_INET) ? (sockaddr_in($hisaddr)) : (sockaddr_in6($hisaddr)); + my $localsockaddr = getsockname($client); + 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 $nto_laddr = ($server->sockdomain == AF_INET) ? (inet_ntoa($laddr)) : (inet_ntop(AF_INET6(), $laddr)); + $nto_iaddr =~ s/::ffff://; + $nto_laddr =~ s/::ffff://; + + return ($port, $iaddr, $lport, $laddr, $nto_iaddr, $nto_laddr); +} + +sub tcpenv { + my ($nto_laddr, $nto_iaddr, $no_rdns) = @_; + + my $TCPLOCALIP = $nto_laddr; + my $TCPREMOTEIP = $nto_iaddr; + + if ($no_rdns) { + return ($TCPLOCALIP, $TCPREMOTEIP, $TCPREMOTEIP ? "[$ENV{TCPREMOTEIP}]" : "[noip!]"); + } + my $res = new Net::DNS::Resolver; + $res->tcp_timeout(3); + $res->udp_timeout(3); + my $query = $res->query($nto_iaddr); + my $TCPREMOTEHOST; + if($query) { + foreach my $rr ($query->answer) { + next unless $rr->type eq "PTR"; + $TCPREMOTEHOST = $rr->ptrdname; + } + } + return ($TCPLOCALIP, $TCPREMOTEIP, $TCPREMOTEHOST || "Unknown"); +} + 1; diff --git a/plugins/check_relay b/plugins/check_relay index e294c9d..eeec9d8 100644 --- a/plugins/check_relay +++ b/plugins/check_relay @@ -2,25 +2,70 @@ # $ENV{RELAYCLIENT} to see if relaying is allowed. # +use Net::IP qw(:PROC); + sub hook_connect { my ($self, $transaction) = @_; my $connection = $self->qp->connection; # Check if this IP is allowed to relay - my @relay_clients = $self->qp->config("relayclients"); - my $more_relay_clients = $self->qp->config("morerelayclients", "map"); - my %relay_clients = map { $_ => 1 } @relay_clients; my $client_ip = $self->qp->connection->remote_ip; - while ($client_ip) { - if (exists($ENV{RELAYCLIENT}) or - exists($relay_clients{$client_ip}) or - exists($more_relay_clients->{$client_ip})) - { - $connection->relay_client(1); - last; + + # @crelay... for comparing, @srelay... for stripping + my (@crelay_clients, @srelay_clients); + + my @relay_clients = $self->qp->config("relayclients"); + for (@relay_clients) { + my ($range_ip, $range_prefix) = ip_splitprefix($_); + if($range_prefix){ + # has a prefix, so due for comparing + push @crelay_clients, $_; + } + else { + # has no prefix, so due for splitting + push @srelay_clients, $_; } - $client_ip =~ s/(\d|\w|::)+(:|\.)?$//; # strip off another 8 bits } + if (@crelay_clients){ + my ($range_ip, $range_prefix, $rversion, $begin, $end, $bin_client_ip); + my $cversion = ip_get_version($client_ip); + for (@crelay_clients) { + # Get just the IP from the CIDR range, to get the IP version, so we can + # get the start and end of the range + ($range_ip, $range_prefix) = ip_splitprefix($_); + $rversion = ip_get_version($range_ip); + ($begin, $end) = ip_normalize($_, $rversion); + + # expand the client address (zero pad it) before converting to binary + $bin_client_ip = ip_iptobin(ip_expand_address($client_ip, $cversion), $cversion); + + if (ip_bincomp($bin_client_ip, 'gt', ip_iptobin($begin, $rversion)) + && ip_bincomp($bin_client_ip, 'lt', ip_iptobin($end, $rversion))) + { + $connection->relay_client(1); + last; + } + } + } + + # If relay_client is already set, no point checking again + if (@srelay_clients && !$connection->relay_client) { + my $more_relay_clients = $self->qp->config("morerelayclients", "map"); + my %srelay_clients = map { $_ => 1 } @srelay_clients; + $client_ip =~ s/::/:/; + ($connection->relay_client(1) && undef($client_ip)) if $client_ip eq ":1"; + + while ($client_ip) { + if (exists($ENV{RELAYCLIENT}) or + exists($srelay_clients{$client_ip}) or + exists($more_relay_clients->{$client_ip})) + { + $connection->relay_client(1); + last; + } + $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 78579e9..8362acf 100644 --- a/plugins/require_resolvable_fromhost +++ b/plugins/require_resolvable_fromhost @@ -1,9 +1,10 @@ use Qpsmtpd::DSN; use Net::DNS qw(mx); use Socket; +use Net::IP qw(:PROC); my %invalid = (); -my $has_ipv6 = Qpsmtpd::Constants::has_ipv6; +my $has_ipv6 = Qpsmtpd::TcpServer::has_ipv6; sub hook_mail { my ($self, $transaction, $sender, %param) = @_; @@ -99,6 +100,9 @@ sub is_valid { sub mx_valid { my ($self, $name, $host) = @_; my $res = new Net::DNS::Resolver; + # IP in MX + return is_valid($name) if ip_is_ipv4($name) or ip_is_ipv6($name); + my @mx_answers; my $query = $res->search($name, 'A'); if ($query) { diff --git a/qpsmtpd-forkserver b/qpsmtpd-forkserver index 1cedf5d..d2e7aee 100755 --- a/qpsmtpd-forkserver +++ b/qpsmtpd-forkserver @@ -18,7 +18,7 @@ use Net::DNS::Header; use strict; $| = 1; -my $has_ipv6 = Qpsmtpd::Constants::has_ipv6; +my $has_ipv6 = Qpsmtpd::TcpServer::has_ipv6; if ($has_ipv6) { eval 'use Socket6'; @@ -40,8 +40,10 @@ sub usage { print <<"EOT"; usage: qpsmtpd-forkserver [ options ] -l, --listen-address addr : listen on specific address(es); can be specified - multiple times for multiple bindings. Default is - 0.0.0.0 (all interfaces). + multiple times for multiple bindings. IPv6 + addresses must be inside square brackets [], and + don't need to be zero padded. + Default is [::] (if has_ipv6) or 0.0.0.0 (if not) -p, --port P : listen on a specific port; default 2525; can be specified multiple times for multiple bindings. -c, --limit-connections N : limit concurrent connections to N; default 15 @@ -234,14 +236,8 @@ while (1) { next; } IO::Handle::blocking($client, 1); - my ($port, $iaddr) = ($server->sockdomain == AF_INET) ? (sockaddr_in($hisaddr)) : (sockaddr_in6($hisaddr)); - my $localsockaddr = getsockname($client); - 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://; + # get local/remote hostname, port and ip address + my ($port, $iaddr, $lport, $laddr, $nto_iaddr, $nto_laddr) = Qpsmtpd::TcpServer::lrpip($server, $client, $hisaddr); my ($rc, @msg) = $qpsmtpd->run_hooks("pre-connection", remote_ip => $nto_iaddr, @@ -293,28 +289,9 @@ while (1) { ::log(LOGINFO, "Connection Timed Out"); exit; }; - $ENV{TCPLOCALIP} = $nto_laddr; - # my ($port, $iaddr) = sockaddr_in($hisaddr); - $ENV{TCPREMOTEIP} = $nto_iaddr; - - if ($NORDNS) { - $ENV{TCPREMOTEHOST} = $ENV{TCPREMOTEIP} ? "[$ENV{TCPREMOTEIP}]" : "[noip!]"; - } - else { - my $zero = $0; - $0 = "$zero (gethostbyname $ENV{TCPREMOTEIP})"; - - 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"; - } - - $0 = $zero; - } - + # set enviroment variables + ($ENV{TCPLOCALIP}, $ENV{TCPREMOTEIP}, $ENV{TCPREMOTEHOST}) = Qpsmtpd::TcpServer::tcpenv($nto_laddr, $nto_iaddr); + # don't do this! #$0 = "qpsmtpd-forkserver: $ENV{TCPREMOTEIP} / $ENV{TCPREMOTEHOST}"; diff --git a/qpsmtpd-prefork b/qpsmtpd-prefork index 2874054..b9e4e0c 100755 --- a/qpsmtpd-prefork +++ b/qpsmtpd-prefork @@ -19,6 +19,12 @@ use Qpsmtpd::TcpServer::Prefork; use Qpsmtpd::Constants; use Getopt::Long; +my $has_ipv6 = Qpsmtpd::TcpServer::has_ipv6; + +if ($has_ipv6) { + use Socket6; +} + #use Time::HiRes qw(gettimeofday tv_interval); # secure shell @@ -47,7 +53,14 @@ my $d; # socket my $pid_path = '/var/run/qpsmtpd/'; my $PID = $pid_path . "/qpsmtpd.pid"; my $d_port = 25; -my $d_addr = "0.0.0.0"; +my $d_addr; +if ($has_ipv6) { + $d_addr = "[::]"; +} +else { + $d_addr = "0.0.0.0"; +} + my $debug = 0; my $max_children = 15; # max number of child processes to spawn my $idle_children = 5; # number of idle child processes to spawn @@ -67,7 +80,7 @@ sub usage { print <<"EOT"; Usage: qpsmtpd-prefork [ options ] --quiet : Be quiet (even errors are suppressed) ---version : Show version information +--version : Show version information --debug : Enable debug output --interface addr : Interface daemon should listen on (default: $d_addr) --port int : TCP port daemon should listen on (default: $d_port) @@ -134,15 +147,20 @@ sub run { if (!$uuid || !$ugid); } + my @Socket_opts = ( + LocalPort => $d_port, + LocalAddr => $d_addr, + Proto => 'tcp', + Listen => SOMAXCONN, + Reuse => 1, + ); # create new socket (used by clients to communicate with daemon) - $d = - new IO::Socket::INET( - LocalPort => $d_port, - LocalAddr => $d_addr, - Proto => 'tcp', - Listen => SOMAXCONN, - Reuse => 1, - ); + if ($has_ipv6) { + $d = IO::Socket::INET6->new(@Socket_opts); + } + else { + $d = IO::Socket::INET(@Socket_opts); + } die "FATAL: Failed to start daemon.\nReason: $!\n(It may be nessesary to " . "wait 20 secs before starting daemon again)\n" unless $d; @@ -361,7 +379,7 @@ sub new_child { my $sigset = block_signal(SIGHUP); # start a session if connection looks valid - qpsmtpd_session($client, $qpsmtpd) if ($iinfo); + qpsmtpd_session($client, $iinfo, $qpsmtpd) if ($iinfo); # close connection and cleanup $client->shutdown(2); @@ -512,15 +530,16 @@ sub info { # start qpmstpd session # arg0: ref to socket object -# arg1: ref to qpsmtpd instance +# arg1: ref to socket object +# arg2: ref to qpsmtpd instance # ret0: void sub qpsmtpd_session { my $client = shift; #arg0 - my $qpsmtpd = shift; #arg1 + my $iinfo = shift; #arg1 + my $qpsmtpd = shift; #arg2 # get local/remote hostname, port and ip address - my ($port, $iaddr) = sockaddr_in(getpeername($client)); #remote - my ($lport, $laddr) = sockaddr_in(getsockname($client)); #local + my ($port, $iaddr, $lport, $laddr, $nto_iaddr, $nto_laddr) = Qpsmtpd::TcpServer::lrpip($d, $client, $iinfo); # get current connected ip addresses (from shared memory) my %children; @@ -529,9 +548,9 @@ sub qpsmtpd_session { 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 %children], @@ -574,9 +593,7 @@ sub qpsmtpd_session { }; # set enviroment variables - $ENV{TCPLOCALIP} = inet_ntoa($laddr); - $ENV{TCPREMOTEIP} = inet_ntoa($iaddr); - $ENV{TCPREMOTEHOST} = gethostbyaddr($iaddr, AF_INET) || "Unknown"; + ($ENV{TCPLOCALIP}, $ENV{TCPREMOTEIP}, $ENV{TCPREMOTEHOST}) = Qpsmtpd::TcpServer::tcpenv($nto_laddr, $nto_iaddr); # run qpmsptd functions $SIG{__DIE__} = 'DEFAULT';