I know that IPv6 is not officially supported, but using it has exposed two bugs in qpsmtpd plugins - "hosts_allow" and "peers". The peers one puts qpsmtpd into an infinite loop trying to strip octets from addresses presumed to be ipv4, while hosts_allow tries to use inet_ntoa on the remote IP.
Below are two patches:
--- peers.orig 2025-11-24 15:09:52.000000000 +1100
+++ peers 2026-02-11 14:53:24.183413301 +1100
@@ -102,9 +102,10 @@
if (-f "config/peers/$client_ip") {
_peer_plugins($qp, "set_hooks", "peers/$client_ip");
return (DECLINED);
}
- $client_ip =~ s/\.?\d+$//; # strip off another 8 bits
+ $client_ip =~ s/(\.?\d+|:?[0-9a-f]|:)$//; # strip off 8 bits (ipv4) or 4 bits (ipv6)
+ # $client_ip =~ s/\.?\d+$//; # strip off another 8 bits
}
if (-f "config/peers/0") {
_peer_plugins($qp, "set_hooks", "peers/0");
return (DECLINED);
--- hosts_allow.orig 2025-11-10 17:37:50.000000000 +1100
+++ hosts_allow 2026-02-11 15:12:39.490724723 +1100
@@ -55,27 +55,29 @@
use Qpsmtpd::Constants;
use Socket;
+use Net::IP;
+
sub hook_pre_connection {
my ($self, $transaction, %args) = @_;
- # remote_ip => inet_ntoa($iaddr),
+ # remote_ip => inet_ntop(AF, $iaddr),
# remote_port => $port,
- # local_ip => inet_ntoa($laddr),
+ # local_ip => inet_ntop(AF, $laddr),
# local_port => $lport,
# max_conn_ip => $MAXCONNIP,
# child_addrs => [values %childstatus],
my $remote = $args{remote_ip};
+ my $rip = new Net::IP($remote);
my $max = $args{max_conn_ip};
my $karma = $self->connection->notes('karma_history');
if ($max) {
my $num_conn = 1; # seed with current value
- my $raddr = inet_aton($remote);
- foreach my $rip (@{$args{child_addrs}}) {
- ++$num_conn if (defined $rip && $rip eq $raddr);
+ foreach my $cip (@{$args{child_addrs}}) {
+ ++$num_conn if (defined $cip && $rip->overlaps($cip)==$IP_IDENTICAL);
}
$max = $self->karma_bump($karma, $max) if defined $karma;
if ($num_conn > $max) {
my $err_mess = "too many connections from $remote";
@@ -83,33 +85,33 @@
return DENYSOFT, "$err_mess, try again later";
}
}
- my @r = $self->in_hosts_allow($remote);
+ my @r = $self->in_hosts_allow($rip);
return @r if scalar @r;
$self->log(LOGDEBUG, "pass");
return DECLINED;
}
sub in_hosts_allow {
my $self = shift;
- my $remote = shift;
+ my $rip = shift;
foreach ($self->qp->config('hosts_allow')) {
s/^\s*//; # trim leading whitespace
- my ($ipmask, $const, $message) = split /\s+/, $_, 3;
+ my ($iprange, $const, $message) = split /\s+/, $_, 3;
next unless defined $const;
- my ($net, $mask) = split /\//, $ipmask, 2;
- $mask = 32 if !defined $mask;
- $mask = pack "B32", "1" x ($mask) . "0" x (32 - $mask);
- if (join('.', unpack('C4', inet_aton($remote) & $mask)) eq $net) {
+ my $overlap = $rip->overlaps(new Net::IP($iprange));
+ next unless defined $overlap;
+
+ if ($overlap == $IP_A_IN_B_OVERLAP || $overlap == $IP_IDENTICAL) {
$const = Qpsmtpd::Constants::return_code($const) || DECLINED;
if ($const =~ /deny/i) {
- $self->log(LOGINFO, "fail, $message");
+ $self->log(LOGINFO, "fail, " . $message || '-');
}
- $self->log(LOGDEBUG, "pass, $const, $message");
+ $self->log(LOGDEBUG, "pass, $const, " . $message || '-');
return $const, $message;
}
}