0
votes

I am trying to write a small controller binary for a sonos speaker. To do that I want to perform a SSDP discovery.

   QHostAddress groupAddress = QHostAddress("239.255.255.250");
    m_socket = new QUdpSocket(this);
    auto ok = m_socket->bind(QHostAddress::AnyIPv4, 56123, QUdpSocket::ShareAddress);

    if (!ok)
    {
        return;
    }


    ok = m_socket->joinMulticastGroup(groupAddress);
    if (!ok)
    {
        return;
    }
    QByteArray message("M-SEARCH * HTTP/1.1\r\n"        \
                       "HOST: 239.255.255.250:1900\r\n" \
                       "MAN: \"ssdp:discover\"\r\n" \
                       "MX: 5\r\n" \
                       "ST: urn:smartspeaker-audio:service:SpeakerGroup:1\r\n" \
                       "linux/4.13 UPnP/1.1 myos/0.11.1\r\n" \
                       "\r\n");

    for(int i=0; i<4; i++){
        auto writeOk = m_socket->writeDatagram(message.data(), groupAddress, 1900);

        if (writeOk == -1)
        {
            qDebug() << "Writing Datagram failed";
        }

    }

    while (m_socket->hasPendingDatagrams())
    {
        QByteArray reply;
        reply.resize(m_socket->pendingDatagramSize());
        m_socket->readDatagram(reply.data(), reply.size());
        qDebug() << reply.data();
    }
    qDebug() << "No more pending datagrams";

However when using wireshark or any other network monitor I do see any packets being send. I ran strace on the binary and got this output:

socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 5
setsockopt(5, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0
setsockopt(5, SOL_IP, IP_PKTINFO, [1], 4) = 0
setsockopt(5, SOL_IP, IP_RECVTTL, [1], 4) = 0
setsockopt(5, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(5, {sa_family=AF_INET, sin_port=htons(56123), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
getsockname(5, {sa_family=AF_INET, sin_port=htons(56123), sin_addr=inet_addr("0.0.0.0")}, [28->16]) = 0
getpeername(5, 0x7ed3aae8, [16])        = -1 ENOTCONN (Transport endpoint is not connected)
getsockopt(5, SOL_SOCKET, SO_TYPE, [2], [4]) = 0
setsockopt(5, SOL_IP, IP_ADD_MEMBERSHIP, {imr_multiaddr=inet_addr("239.255.255.250"), imr_interface=inet_addr("0.0.0.0")}, 8) = 0
sendmsg(5, {msg_name={sa_family=AF_INET, sin_port=htons(1900), sin_addr=inet_addr("239.255.255.250")}, msg_namelen=16, msg_iov=[{iov_base="M-SEARCH * HTTP/1.1\r\nHOST: 239.2"..., iov_len=167}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, MSG_NOSIGNAL) = 167
sendmsg(5, {msg_name={sa_family=AF_INET, sin_port=htons(1900), sin_addr=inet_addr("239.255.255.250")}, msg_namelen=16, msg_iov=[{iov_base="M-SEARCH * HTTP/1.1\r\nHOST: 239.2"..., iov_len=167}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, MSG_NOSIGNAL) = 167
sendmsg(5, {msg_name={sa_family=AF_INET, sin_port=htons(1900), sin_addr=inet_addr("239.255.255.250")}, msg_namelen=16, msg_iov=[{iov_base="M-SEARCH * HTTP/1.1\r\nHOST: 239.2"..., iov_len=167}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, MSG_NOSIGNAL) = 167
sendmsg(5, {msg_name={sa_family=AF_INET, sin_port=htons(1900), sin_addr=inet_addr("239.255.255.250")}, msg_namelen=16, msg_iov=[{iov_base="M-SEARCH * HTTP/1.1\r\nHOST: 239.2"..., iov_len=167}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, MSG_NOSIGNAL) = 167
recv(5, 0x7ed3ab2b, 1, MSG_PEEK)        = -1 EAGAIN (Resource temporarily unavailable)

What sticks out here for me is the syscall recv() failing and also getpeername(). To verify its not a network permission issue or something similar I wrote a python script:

class SSDPClient:

    def __init__(self):
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    def discover(self, timeout):
        request_content = ("M-SEARCH * HTTP/1.1\n"
                          "HOST: 239.255.255.250:1900\n"
                          "MAN: \"ssdp:discover\"\n"
                          "MX: {duration}\n"
                          "ST: urn:smartspeaker-audio:service:SpeakerGroup:1\n"
                          "USER-AGENT: {osstring} UPnP/1.1 {productstring}\n").format(duration=timeout, osstring="linux/4.13", productstring="myos/0.11.1").encode('utf-8')
        [self._socket.sendto(request_content, ("239.255.255.250", 1900)) for i in range(0,3)]
        self._socket.settimeout(timeout)
        response, address = self._socket.recvfrom(1024)
        print(response)

This script works. Running strace on that:

socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC, IPPROTO_IP) = 3
sendto(3, "M-SEARCH * HTTP/1.1\nHOST: 239.25"..., 171, 0, {sa_family=AF_INET, sin_port=htons(1900), sin_addr=inet_addr("239.255.255.250")}, 16) = 171
sendto(3, "M-SEARCH * HTTP/1.1\nHOST: 239.25"..., 171, 0, {sa_family=AF_INET, sin_port=htons(1900), sin_addr=inet_addr("239.255.255.250")}, 16) = 171
sendto(3, "M-SEARCH * HTTP/1.1\nHOST: 239.25"..., 171, 0, {sa_family=AF_INET, sin_port=htons(1900), sin_addr=inet_addr("239.255.255.250")}, 16) = 171
ioctl(3, FIONBIO, [1])                  = 0
clock_gettime(CLOCK_MONOTONIC, {tv_sec=10531, tv_nsec=209135513}) = 0
poll([{fd=3, events=POLLIN}], 1, 3000)  = 1 ([{fd=3, revents=POLLIN}])

What strikes me is that the python code does not seem to do all of the setsockopt syscalls. Can anyone help me figuring out whats the issue with using the QUdpSocket? I know that if I need to that I can implement using raw sockets but I'd rather use the built in stuff, if I can get it working

1
You are blasting three UDP messages out and expect a response back the same millisecond. That will not work network-wise. Furthermore, Qt UDP sockets are non-blocking by default. As a quick hack you can call m_socket->waitForReadyRead(), but a proper solution would be to attach a handler to the readyRead signal and drop back into the event loop.Botje
Thanks that was the issue, the socket was closed immediately after sending the payload. Stupid mistake <.<Curunir

1 Answers

0
votes

Qt UDP sockets are non-blocking by default. The hasPendingDatagrams method expects that the event loop has had time to receive and process incoming messages first.

As a quick hack you can call m_socket->waitForReadyRead(). This runs the event loop for a limited time (30s) and hopefully receives some packets and sets the flag. A proper solution would be to attach a handler to the readyRead signal and drop back into the event loop.