Search code examples
perlsocketssendto

Why does sendto() via Perl Socket->send() ignore the peer address?


I have a program (too complex to present here) that uses an UDP socket created by IO::Socket::INET->new() with both, Local... and Peer... address and port. So expectation is that a plain $sock->send($data, $flags) will send $data to the peer address specified when creating the socket. That seems to work.

However when I try to send an individual packet to a different destination by using $sock->send($data, $flags, $dest), $dest seems to be ignored (and the packet is being sdent to the socket's peer address). I added lots of debug messages, and the parameter $dest is passed correctly to send, but strace shows that sendto() is called with NULL for sockaddr and 0 for socklen.

perldoc -f send doesn't help me. So why is the destination address ignored?

As requested, here's the (somewhat extra verbose) send_packet code:

$RE_saddr = qr/^(.+):(\d+)$/;

sub send_packet($$;$)
{
    my ($sock, $packet, $dest) = @_;
    my @params = ($packet, 0);

    if (defined($dest)) {
        if (my ($addr, $port) = $dest =~ $RE_saddr) {
            if (my $addr_bin = inet_aton($addr)) {
                if (defined($dest = sockaddr_in($port, $addr_bin))) {
                    push(@params, $dest);
                } else {
                    warn "bad address or socket for $addr:$port";
                }
            } else {
                warn "bad address $addr";
            }
        } else {
            warn "bad destination address $dest";
        }
    }
    return 1
        if ($sock->send(@params));
    return undef;
}

So obviously the destination is passed as "host : port" (one string without blanks (blanks are due to markup deficits here)).


Solution

  • If you use Peer* when creating an IO::Socket::INET then connect() will be called, which sets the peername of the socket. On such a socket you'll never have to specify the remote socket address, because it has a default one.

    IO::Socket::send() has a "feature": it ignores the TO parameter when the socket has a valid peername:

     my $r = defined(getpeername($sock))
         ? send($sock, $_[1], $flags)
         : send($sock, $_[1], $flags, $peer);
    

    That change was introduced in IO 1.12, which unfortunately predates the history of the current git repository:

    Modified IO::Socket::send so not to pass 4 arguments to send if the socket is connected

    If you don't like this behaviour, you'll have to use raw CORE::send() instead, i.e.:

    #!/usr/bin/perl
    use warnings;
    use strict;
    
    use IO::Socket::INET;
    use Socket qw(pack_sockaddr_in);
    
    my $client = IO::Socket::INET->new(
        PeerAddr  => '127.0.0.1',
        PeerPort  => 2000,
        Proto      => 'udp',
    ) or die "client socket: $!\n";
    
    my $addr = pack_sockaddr_in(2000, inet_aton('127.0.0.1'));
    
    $client->send('ABCD', 0)
        or die "IO::Socket::send() no addr: $!\n";
    $client->send('ABCD', 0, $addr)
        or die "IO::Socket::send() with addr: $!\n";
    send($client, 'ABCD', 0, $addr)
        or die "send() with addr: $!\n";
    
    exit 0;
    

    Test run:

    $ strace -e sendto,connect,socket perl dummy.pl
    socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC, IPPROTO_UDP) = 4
    connect(4, {sa_family=AF_INET, sin_port=htons(2000), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
    sendto(4, "ABCD", 4, 0, NULL, 0)        = 4
    sendto(4, "ABCD", 4, 0, NULL, 0)        = 4
    sendto(4, "ABCD", 4, 0, {sa_family=AF_INET, sin_port=htons(2000), sin_addr=inet_addr("127.0.0.1")}, 16) = 4
    

    The strace from my Linux machine indicates that send() is mapped to sendto(), because the Perl code does call send() and sendto().


    UPDATE: I've created the upstream ticket #133936.