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?
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
andwrite
You have to
QUdpSocket::connectToHost
and thenQIODevice::write
, e.g.This is due to the Linux kernel implementation of
sendmsg
. However, given thatsendmsg
andconnect
+send
(orconnectToHost
andwrite
) should probably not differ in their behaviour, you shouldn't countconnectToHost
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:
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
andperror()
:This will indeed report
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:
We're therefore on the right track. However, if you're interested in Qt's network stack, have a look at
QUdpSocket::writeDatagram
QNativeSocketEngine::writeDatagram
QNativeSocketEnginePrivate::sendDatagram
(which sets the error)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 getEINVAL
, we can simply search for the error and find:The Linux kernel recognizes the reserved port and declines it as invalid in
udp_sendmsg
. While this might seem like the wrong function, thesendto
syscall is implemented in terms ofsocket_sendmsg
, which callsudp_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 useconnectToHost
to keep ourselves from repeating:If we try this variant, we get the correct results immediately. Why?
QUdpSocket::connectToHost
uses theconnect
syscall. Theconnect
syscall does not return EINVAL (at least up to 4.15, haven't checked higher ones). Furthermore, it usesipv4_datagram_connect
, which happily accepts any port.We can also check the behaviour in simple C again:
So what about
udp_sendmsg
, which gets used byQIODevice::write
orsend
? Well, remember theif(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 thegit log
s 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.