Как писать в порт 0 с помощью QUdpSocket?

Я хочу реализовать WoL Magic Packet, используя Qt, чтобы быть переносимым кросс GNU / Linux и Microsoft Windows. Википедия сообщает: обычно отправляется как дейтаграмма UDP на порт 0 (зарезервированный номер порта), 7 (протокол эхо) или 9 (протокол отмены), но я не мог записать данные на порт 0 с помощью QUdpSocket, почему?

Образец проблемы

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() };
}

И вывод:

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

Остальные порты (7 и 9) в порядке, но почему я не могу записать данные в порт 0?


person Ghasem Ramezani    schedule 03.02.2021    source источник
comment
Чертовски хороший вопрос. К сожалению, я только что заметил, что вас интересуют обе Windows и Linux. Если _1 _ + _ 2_ не работает в Windows, добавьте комментарий.   -  person Zeta    schedule 03.02.2021


Ответы (1)


Примечание: этот ответ касается только Linux, но то же самое должно относиться к любой другой системе, которая реализует UDP в соответствии с IETF RFC.

TL; DR: используйте connectToHost и write

Вы должны QUdpSocket::connectToHost, а затем QIODevice::write, например

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

socket.write(magic_datagram, magic_datagram_size);

Это связано с реализацией sendmsg в ядре Linux. Однако, учитывая, что sendmsg и _8 _ + _ 9_ (или connectToHost и write), вероятно, не должны отличаться по своему поведению, вам не следует считать connectToHost и `писать 'работающими вечно. В конце концов, WoL - это Ethernet-фрейм.

Почему QUdpSocket::sendTo терпит неудачу?

Прогулка по сетевому стеку

IANA назначает порты как для UDP, так и для TCP. Наш порт назначения 0 указан в регистрации IANA как зарезервированный. Это естественно, поскольку нулевой порт источника четко определен в спецификации UDP как не использовал.

Однако зарезервированное значение редко мешает нам просто ввести его, и Qt случайно его принимает. Так что что-то в процессе должно помешать нам отправить дейтаграмму.

Наша дейтаграмма проходит несколько уровней, прежде чем наконец выйдет в сеть:

  1. Сетевой стек Qt
  2. Сокет библиотеки GNU C (glibc) (обычно небольшой слой вокруг ядра)
  3. ядро Linux
  4. сетевая карта (которая действительно не должна заботиться в этот момент)

Управление ошибками Qt и ошибки в стиле C

Прежде чем мы углубимся в проблему, мы должны сначала проверить, есть ли на втором уровне дополнительная информация через errno и 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() };
}

Это действительно будет сообщать

Underlying error in UDP: Invalid argument
Error number: 22

Ошибка 22 - -EINVAL, недопустимый аргумент. Поскольку Qt обычно нормально сообщает о неверных аргументах (а не просто о невозможности отправить сообщение), мы можем пропустить его реализацию и вместо этого заглянуть в glibc или даже в ядро.

Мы также можем воссоздать поведение без 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);
    }
}

Таким образом, мы на правильном пути. Однако, если вас интересует сетевой стек Qt, взгляните на

Погружаясь в бездну

Теперь давайте полностью пропустим glibc и вместо этого перейдем прямо к ядру. Поскольку мы имеем дело с UDP в IPv4, нам нужно перейти на _ 24_. Поскольку мы уже знаем, что получаем EINVAL, мы можем просто найдите ошибку и найдите:

    // 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;
    } 

Ядро Linux распознает зарезервированный порт и отклоняет его как недопустимый в udp_sendmsg. Хотя это может показаться неправильной функцией, системный вызов sendto реализован в терминах socket_sendmsg, который вызывает udp_sendmsg на сокетах UDP.

Следовательно, мы не можем отправлять какие-либо пакеты UDP через QUdpSocket::sendTo.

Альтернатива через QUdpSocket::connectToHost

Теперь есть альтернатива QUdpSocket::sendTo. Если мы знаем, что собираемся отправлять все сообщения на один и тот же порт, тогда мы можем использовать connectToHost, чтобы не повторяться:

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

Если мы попробуем этот вариант, то сразу получим правильный результат. Почему?

QUdpSocket::connectToHost использует системный вызов connect. Системный вызов connect не возвращает EINVAL (по крайней мере, до 4.15, более высокие не проверял). Кроме того, он использует ipv4_datagram_connect, который с радостью принимает любой порт.

Мы также можем снова проверить поведение на простом C:

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);
    }
}

Так что насчет udp_sendmsg, который используется QIODevice::write или send? Ну, помните if(usin) в приведенном выше коде? Поскольку адрес хранится в текущем состоянии сокета, usin == NULL. Проверка адреса назначения никогда не происходит. Это может быть ошибкой или полностью задумано. Для этих файлов нужно будет проверить git logs.

Учитывая, что connect(...) с нулевым портом назначения может быть обычным вариантом использования для UDP, это поведение может никогда не измениться, поскольку оно нарушит пространство пользователя, однако не следует слишком доверять зарезервированный порт, который не предназначен для использования в данном протоколе.

person Zeta    schedule 03.02.2021
comment
На самом деле спасибо: D. В настоящее время я не могу тестировать в Windows. В кратчайшие сроки дам отзыв. - person Ghasem Ramezani; 03.02.2021