Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 52 additions & 5 deletions lib/Net/Daemon.pm
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ use Net::Daemon::Log ();
use POSIX ();
use File::Spec ();

our $INET_CLASS = eval { require IO::Socket::IP; 'IO::Socket::IP' } || 'IO::Socket::INET';

our $VERSION = '0.52';
our @ISA = qw(Net::Daemon::Log);

Expand Down Expand Up @@ -377,13 +379,37 @@ sub Accept ($) {
);
}
else {
( $name, $aliases, $addrtype, $length, @addrs ) =
gethostbyaddr( $socket->peeraddr(), Socket::AF_INET() );
my $peerhost = $socket->peerhost();
my $ipv4_addr;

# IO::Socket::IP dual-stack sockets present IPv4 clients as
# IPv4-mapped IPv6 addresses (::ffff:x.x.x.x). Extract the
# embedded IPv4 address so gethostbyaddr() and ACL masks work.
if ( defined $peerhost && $peerhost =~ /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i ) {
$ipv4_addr = Socket::inet_aton($1);
}
elsif ( defined $peerhost && $peerhost =~ /^\d+\.\d+\.\d+\.\d+$/ ) {
$ipv4_addr = $socket->peeraddr();
}

if ($ipv4_addr) {
( $name, $aliases, $addrtype, $length, @addrs ) =
gethostbyaddr( $ipv4_addr, Socket::AF_INET() );
}
}
my @patterns =
@addrs
? map { Socket::inet_ntoa($_) } @addrs
: $socket->peerhost();
: ();
if ( $self->{'proto'} ne 'unix' ) {
# Always include peerhost() so ACL masks can match the raw address
my $peerhost = $socket->peerhost();
push( @patterns, $peerhost ) if defined $peerhost;
# For IPv4-mapped IPv6, also add the bare IPv4 address
if ( defined $peerhost && $peerhost =~ /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i ) {
push( @patterns, $1 );
}
}
push( @patterns, $name ) if ($name);
push( @patterns, split( / /, $aliases ) ) if $aliases;

Expand Down Expand Up @@ -598,7 +624,7 @@ sub Bind ($) {
umask $old_umask;
}
else {
$self->{'socket'} = IO::Socket::INET->new(
$self->{'socket'} = $INET_CLASS->new(
'LocalAddr' => $self->{'localaddr'},
'LocalPort' => $self->{'localport'},
'Proto' => $self->{'proto'} || 'tcp',
Expand Down Expand Up @@ -827,6 +853,26 @@ override those methods that aren't appropriate for you, but typically
inheriting will safe you a lot of work anyways.


=head2 IPv6 Support

When L<IO::Socket::IP> is installed (it is core since Perl 5.20, and
available on CPAN for earlier versions), the server will automatically
use it instead of L<IO::Socket::INET>. This provides transparent
dual-stack IPv4/IPv6 support: clients can connect over either protocol
without any configuration change.

To bind to a specific IPv6 address, use the B<--localaddr> option:

--localaddr ::1 # IPv6 loopback only
--localaddr :: # all IPv6 (and IPv4 on most systems)

If IO::Socket::IP is not available, the server falls back to
IO::Socket::INET (IPv4 only), preserving backward compatibility.

Existing IPv4 access control masks (the C<clients> configuration)
continue to work unchanged: IPv4-mapped IPv6 addresses
(C<::ffff:x.x.x.x>) are automatically normalized for matching.

=head2 Constructors

$server = Net::Daemon->new($attr, $options);
Expand Down Expand Up @@ -920,7 +966,8 @@ GID's can be passed as group names or numeric values.

By default a daemon is listening to any IP number that a machine
has. This attribute allows to restrict the server to the given
IP number.
IP number. Both IPv4 and IPv6 addresses are accepted when
L<IO::Socket::IP> is available (core since Perl 5.20).

=item I<localpath> (B<--localpath=path>)

Expand Down
13 changes: 10 additions & 3 deletions lib/Net/Daemon/Test.pm
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ use Net::Daemon ();
use Symbol ();
use File::Basename ();

# Use the same IPv4/IPv6 class as Net::Daemon
my $INET_CLASS = $Net::Daemon::INET_CLASS || 'IO::Socket::INET';

our $VERSION = '0.52';
our @ISA = qw(Net::Daemon);

Expand Down Expand Up @@ -167,22 +170,26 @@ sub Bind ($) {
}
}
else {
# Default to 127.0.0.1 for test servers. IO::Socket::IP with no
# LocalAddr may bind to :: (dual-stack IPv6), which test clients
# using IO::Socket::INET to connect to 127.0.0.1 can't reach
# on some platforms (notably Windows).
my @socket_args = (
'LocalAddr' => $self->{'localaddr'},
'LocalAddr' => $self->{'localaddr'} || '127.0.0.1',
'LocalPort' => $self->{'localport'},
'Proto' => $self->{'proto'} || 'tcp',
'Listen' => $self->{'listen'} || 10,
'Reuse' => 1
);
$socket = eval { IO::Socket::INET->new(@socket_args) };
$socket = eval { $INET_CLASS->new(@socket_args) };
if ($socket) {
$port = $socket->sockport();
}
else {
$port = 30049;
while ( !$socket && $port++ < 30060 ) {
$socket = eval {
IO::Socket::INET->new(
$INET_CLASS->new(
@socket_args,
'LocalPort' => $port
);
Expand Down
70 changes: 70 additions & 0 deletions t/ipv6.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# -*- perl -*-
#
# Test IPv6 support: IO::Socket::IP preference and IPv4-mapped address handling
#
use strict;
use warnings;
use Test::More;

use Net::Daemon;

# Test 1: $INET_CLASS is set
ok( defined $Net::Daemon::INET_CLASS, 'INET_CLASS is defined' );
like( $Net::Daemon::INET_CLASS, qr/^IO::Socket::I(?:NET|P)$/,
"INET_CLASS is IO::Socket::INET or IO::Socket::IP" );

# Test 2: IO::Socket::IP is preferred when available
SKIP: {
skip "IO::Socket::IP not available", 1
unless eval { require IO::Socket::IP; 1 };
is( $Net::Daemon::INET_CLASS, 'IO::Socket::IP',
'IO::Socket::IP is preferred when available' );
}

# Test 3-6: Accept() with IPv4-mapped IPv6 addresses in ACL
# Create a mock socket that presents an IPv4-mapped IPv6 address
{
package MockSocket;
sub new { bless { host => $_[1], port => $_[2], addr => $_[3] }, $_[0] }
sub peerhost { $_[0]->{host} }
sub peerport { $_[0]->{port} }
sub peeraddr { $_[0]->{addr} }
}

# Server with IPv4 ACL mask that should match mapped addresses
my $server = Net::Daemon->new(
{
'mode' => 'single',
'proto' => 'tcp',
'clients' => [
{ 'mask' => '^127\.0\.0\.1$', 'accept' => 1 },
{ 'mask' => '.*', 'accept' => 0 },
],
}
);

# Test with plain IPv4 address
$server->{'socket'} = MockSocket->new(
'127.0.0.1', 12345, Socket::inet_aton('127.0.0.1')
);
ok( $server->Accept(), 'Accept() allows plain IPv4 127.0.0.1' );

# Test with IPv4-mapped IPv6 address
$server->{'socket'} = MockSocket->new(
'::ffff:127.0.0.1', 12345, Socket::inet_aton('127.0.0.1')
);
ok( $server->Accept(), 'Accept() allows IPv4-mapped ::ffff:127.0.0.1' );

# Test that denied address still rejected
$server->{'socket'} = MockSocket->new(
'10.0.0.1', 12345, Socket::inet_aton('10.0.0.1')
);
ok( !$server->Accept(), 'Accept() denies non-matching 10.0.0.1' );

# Test denied via mapped address
$server->{'socket'} = MockSocket->new(
'::ffff:10.0.0.1', 12345, Socket::inet_aton('10.0.0.1')
);
ok( !$server->Accept(), 'Accept() denies non-matching ::ffff:10.0.0.1' );

done_testing();
Loading