From 084a6ed6d79cbe85381f3ae099bc333735d443fc Mon Sep 17 00:00:00 2001 From: Toddr Bot Date: Tue, 14 Apr 2026 12:56:47 +0000 Subject: [PATCH 1/2] feat: add IPv6 support via IO::Socket::IP Prefer IO::Socket::IP (core since Perl 5.20) over IO::Socket::INET for transparent dual-stack IPv4/IPv6 support. Falls back to IO::Socket::INET on older Perls, preserving backward compatibility. Key changes: - Daemon.pm/Test.pm: use $INET_CLASS resolved at load time - Accept(): handle IPv4-mapped IPv6 addresses (::ffff:x.x.x.x) so existing IPv4 ACL masks continue to work on dual-stack sockets - New t/ipv6.t: 7 tests covering class selection and ACL normalization - POD: document IPv6 support and --localaddr IPv6 usage Co-Authored-By: Claude Opus 4.6 --- lib/Net/Daemon.pm | 57 +++++++++++++++++++++++++++++++--- lib/Net/Daemon/Test.pm | 7 +++-- t/ipv6.t | 70 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 t/ipv6.t diff --git a/lib/Net/Daemon.pm b/lib/Net/Daemon.pm index c5f76d0..9433a6f 100644 --- a/lib/Net/Daemon.pm +++ b/lib/Net/Daemon.pm @@ -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); @@ -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; @@ -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', @@ -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 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. 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 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); @@ -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 is available (core since Perl 5.20). =item I (B<--localpath=path>) diff --git a/lib/Net/Daemon/Test.pm b/lib/Net/Daemon/Test.pm index e678d5e..56c2949 100644 --- a/lib/Net/Daemon/Test.pm +++ b/lib/Net/Daemon/Test.pm @@ -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); @@ -174,7 +177,7 @@ sub Bind ($) { '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(); } @@ -182,7 +185,7 @@ sub Bind ($) { $port = 30049; while ( !$socket && $port++ < 30060 ) { $socket = eval { - IO::Socket::INET->new( + $INET_CLASS->new( @socket_args, 'LocalPort' => $port ); diff --git a/t/ipv6.t b/t/ipv6.t new file mode 100644 index 0000000..d8f796d --- /dev/null +++ b/t/ipv6.t @@ -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(); From ac2680220d51828d497484ec5d93ae86bfad3c14 Mon Sep 17 00:00:00 2001 From: Toddr Bot Date: Thu, 23 Apr 2026 05:53:47 +0000 Subject: [PATCH 2/2] fix: default Test.pm server to 127.0.0.1 for IO::Socket::IP compat IO::Socket::IP with no LocalAddr may bind to :: (dual-stack IPv6) on some platforms. Test clients use IO::Socket::INET to connect to 127.0.0.1, which can't reach an IPv6-only listener on Windows. Default the test server's LocalAddr to 127.0.0.1. Co-Authored-By: Claude Opus 4.6 --- lib/Net/Daemon/Test.pm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/Net/Daemon/Test.pm b/lib/Net/Daemon/Test.pm index 56c2949..4de282f 100644 --- a/lib/Net/Daemon/Test.pm +++ b/lib/Net/Daemon/Test.pm @@ -170,8 +170,12 @@ 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,