How to write to port 0 with QUdpSocket?

370 views Asked by At

I want to implement the WoL Magic Packet using Qt to be portable cross GNU/Linux and Microsoft Windows. Wikipedia says: "is typically sent as a UDP datagram to port 0 (reserved port number), 7 (Echo Protocol) or 9 (Discard Protocol)", but I couldn't write any data on port 0 with QUdpSocket, Why?

Sample of the problem

QUdpSocket  socket;
auto const  writtenSize = socket.writeDatagram(toSend, magicPacketLength,
                                               QHostAddress(ip), defaultPort);

if (writtenSize != magicPacketLength)
{
  result = { false, "writtenSize(" + QString::number(writtenSize)
             + ") != magicPacketLength(" + QString::number(magicPacketLength) + "): "
             + socket.errorString() };
}

And the output is:

writtenSize(-1) != magicPacketLength(102): Unable to send a message

The other ports (7 and 9) are okay, but why I couldn't write data to port 0?

1

There are 1 answers

1
Zeta On BEST ANSWER

Note: this answer only considers Linux, but the same should hold for any other system that implements UDP according to the IETF RFCs.

TL;DR: Use connectToHost and write

You have to QUdpSocket::connectToHost and then QIODevice::write, e.g.

QUdpSocket socket;
socket.connectToHost(target_address, 0);

socket.write(magic_datagram, magic_datagram_size);

This is due to the Linux kernel implementation of sendmsg. However, given that sendmsg and connect+send (or connectToHost and write) should probably not differ in their behaviour, you shouldn't count connectToHost and `write' working forever. WoL is an ethernet frame, after all.

Why does QUdpSocket::sendTo fail?

Walking along the network stack

The IANA assigns ports to both UDP and TCP. Our destination port 0 is listed in the IANA's registration as reserved. This is only natural, as the source port zero is well-defined in the UDP specification as "not used".

However, a reserved value seldomly stops us from just typing it in, and Qt happely accepts it. So something along the way must stop us from actually sending the datagram.

Our datagram traverses several layers before it finally exits into the wire:

  1. Qt's Network stack
  2. The GNU C library's (glibc) socket (usually just a small layer around the kernel one)
  3. the Linux kernel
  4. the network card (which really shouldn't care at that point)

Qt's error management and C-style errors

Before we delve deeper into the issue, we should first check if the second layer has some more information via errno and perror():

if (writtenSize != magicPacketLength)
{
  if(errno) 
  {
    int err = errno;
    perror("Underlying error in UDP");
    fprintf(stderr "Error number: %d\n", err);
  }
  result = { false, "writtenSize(" + QString::number(writtenSize)
             + ") != magicPacketLength(" + QString::number(magicPacketLength) + "): "
             + socket.errorString() };
}

This will indeed report

Underlying error in UDP: Invalid argument
Error number: 22

Error 22 is -EINVAL, an invalid argument. As Qt usually reports wrong arguments fine (instead of just "Unable to send a message"), we can skip it's implementation and instead look into glibc or even the kernel.

We can also recreate the behaviour without Qt:

int main(int argc, char* argv[]) { 
    int sockfd; 
    int not_ok = 0;
    struct sockaddr_in     servaddr; 
  
    sockfd = socket(AF_INET, SOCK_DGRAM, 0)
  
    memset(&servaddr, 0, sizeof(servaddr)); 
    servaddr.sin_family = AF_INET; 
    inet_aton("192.168.11.31", &servaddr.sin_addr);

    // Use port 0 on no args, port 9 on any arg
    servaddr.sin_port = htons(argc > 1 ? 9 : 0 ); 
      
    sendto(sockfd, "", 0, MSG_CONFIRM,
       (const struct sockaddr *) &servaddr, sizeof(servaddr)); 
  
    if(errno) {
       int err = errno;
       perror("Error during sendto");
       printf("Errno: %d\n", err);
    }
}

We're therefore on the right track. However, if you're interested in Qt's network stack, have a look at

Delving into the abyss

Now let's completely skip glibc and instead head right into the kernel. Since we're dealing with UDP in IPv4, we need to head into /net/ipv4/udp.c. As we already know that we get EINVAL, we can simply search for the error and find:

    // Note: if `usin` is valid than an destination was given to sendto.
    //       This is true for messages sent via QUdpSocket::sendTo. 
    if (usin) {
        if (msg->msg_namelen < sizeof(*usin))
            return -EINVAL;
        if (usin->sin_family != AF_INET) {
            if (usin->sin_family != AF_UNSPEC)
                return -EAFNOSUPPORT;
        }

        daddr = usin->sin_addr.s_addr;
        dport = usin->sin_port;
        if (dport == 0)
            return -EINVAL;
    } 

The Linux kernel recognizes the reserved port and declines it as invalid in udp_sendmsg. While this might seem like the wrong function, the sendto syscall is implemented in terms of socket_sendmsg, which calls udp_sendmsg on UDP sockets.

Therefore, we cannot send any UDP packet via QUdpSocket::sendTo.

An alternative via QUdpSocket::connectToHost

Now, there is an alternative to QUdpSocket::sendTo. If we know that we're going to send all messages to the same port, then we can use connectToHost to keep ourselves from repeating:

QByteArray payload;
QUdpSocket socket;
socket.connectToHost(target_address, 0);
socket.write(payload);

If we try this variant, we get the correct results immediately. Why?

QUdpSocket::connectToHost uses the connect syscall. The connect syscall does not return EINVAL (at least up to 4.15, haven't checked higher ones). Furthermore, it uses ipv4_datagram_connect, which happily accepts any port.

We can also check the behaviour in simple C again:

int main(int argc, char* argv[]) { 
    int sockfd; 
    int not_ok = 0;
    struct sockaddr_in     servaddr; 
  
    sockfd = socket(AF_INET, SOCK_DGRAM, 0)
  
    memset(&servaddr, 0, sizeof(servaddr)); 
    servaddr.sin_family = AF_INET; 
    inet_aton("192.168.11.31", &servaddr.sin_addr);

    // Use port 0 on no args, port 9 on any arg
    servaddr.sin_port = htons(argc > 1 ? 9 : 0 ); 
   
    connect(sockfd, (const struct sockaddr *) &servaddr, sizeof(servaddr));   
    send(sockfd, "", 0, MSG_CONFIRM);
  
    if(errno) {
       int err = errno;
       perror("Error during sendto");
       printf("Errno: %d\n", err);
    }
}

So what about udp_sendmsg, which gets used by QIODevice::write or send? Well, remember the if(usin) in the code above? As the address is stored in the socket's current state, usin == NULL. The destination address check never happens. This might be a bug, or completely intended. One would need to check the git logs for those files.

Given that connect(...) with a zero destination port might be a common use-case for UDP, this behaviour might never get changed as it would break user-space, however, one shouldn't put too much trust into a reserved port that is not meant for use in the given protocol.