QtNat – 使用Qt UPnP打开你的端口
QtNat – Open you port with Qt UPnP

原始链接: http://renaudguezennec.eu/index.php/2026/01/09/qtnat-open-you-port-with-qt/

## QtNat:简化的UPnP端口映射 QtNat是一个轻量级的C++库,基于Qt 6构建,旨在简化使用UPnP的NAT端口映射。它自动处理将本地服务暴露到外部网络的复杂性,无需手动配置路由器。 该库简化了发现UPnP设备和创建端口转发规则的过程。它利用信号/槽机制通知开发者状态变化——从发现到成功的端口映射。一个关键函数`addPortMapping`允许开发者轻松地为诸如点对点软件、游戏和远程访问工具等应用程序转发端口。 发现过程包括向UPnP服务器发送M-SEARCH请求,然后请求和解析它们的描述文件以识别兼容设备。端口映射请求使用Inja模板构建,并通过HTTP POST请求发送。 QtNat目前正在开发中,可在Github上进行测试和贡献。作者鼓励用户在各种设备上验证其功能并提供改进反馈。

## Hacker News 上关于 UPnP 与网络安全的讨论 最近 Hacker News 上进行了一场关于 QtNat 的讨论,QtNat 是一个 Qt 库,用于利用 UPnP(通用即插即用)打开网络端口。 讨论很快演变成对 UPnP 安全影响的争论。 虽然 UPnP 为网络应用程序提供了便捷的自动配置,但许多用户表达了对其可能创建意外防火墙漏洞的担忧。 一些人提倡路由器控制,允许用户*授权* UPnP 请求,而不仅仅是启用或禁用该功能。 另一些人指出,禁用 UPnP 常常会导致降级到较慢、不太可靠的方法,例如 STUN。 讨论还涉及了路由器实现的质量,Fritz!Box 路由器收到了褒贬不一的评价——因其功能和稳定性而受到赞扬,但其安全性也受到质疑。 几位评论员建议使用 OpenWRT 自行托管路由器,以获得更大的控制权。 除了 UPnP,用户还探讨了替代的、安全的远程访问解决方案,例如 Tailscale、ZeroTier 和 WireGuard,强调了在动态 IP 地址下维持稳定连接的挑战。 最后,对 Qt 的复杂性和下载要求进行了批评,尽管它具有强大的功能。
相关文章

原文

QtNat is a lightweight C++ library built with Qt 6 that simplifies NAT port mapping using UPnP (Universal Plug and Play). It is designed to help developers easily expose local services to external networks without requiring manual router configuration for users.

By leveraging UPnP, QtNat automatically communicates with compatible routers to create port forwarding rules at runtime. This makes it particularly useful for peer-to-peer applications, multiplayer games, remote access tools, and any software that needs reliable inbound connectivity behind a NAT.

QtNat provides a simplified API to do all steps automatically: discovery and mapping. This has been tested on my local device. Feel free to test it and improve it.

Use it

    UpnpNat nat;

    QObject::connect(&nat, &UpnpNat::statusChanged, [&nat, &app]() {
        switch(nat.status())
        {
        case UpnpNat::NAT_STAT::NAT_IDLE:
        case UpnpNat::NAT_STAT::NAT_DISCOVERY:
        case UpnpNat::NAT_STAT::NAT_GETDESCRIPTION:
        case UpnpNat::NAT_STAT::NAT_DESCRIPTION_FOUND:
            break;
        case UpnpNat::NAT_STAT::NAT_FOUND:
            nat.requestDescription();
            break;
        case UpnpNat::NAT_STAT::NAT_READY:
            nat.addPortMapping("UpnpTest", nat.localIp(), 6664, 6664, "TCP");
            break;
        case UpnpNat::NAT_STAT::NAT_ADD:
            qDebug() << "It worked!";
            app.quit();
            break;
        case UpnpNat::NAT_STAT::NAT_ERROR:
            qDebug() <<"Error:" <<nat.error();
            app.exit(1);
            break;
        }
    });

    nat.discovery();

  1. We create the object (l:0)
  2. We connect to statusChanged signal to get notified (l:2)
  3. When status is NAT_FOUND, we request the description (l:11)
  4. When status is NAT_READY, we request the port mapping (l:14)
  5. When status is NAT_ADD, It means the port mapping request has been added, It worked! The application quits.(l:17)
  6. When status is NAT_ERROR, Error occured and display the error text. The application exits on error. (l:21)
  7. We connect to error changed in order to detect errors. (l:14)
  8. We start the discovery. (l:28)

Technical explainations

The discovery

Basically, we need to know if there is a upnp server around. To do so, we send an M-SEARCH request on the multicast address.

Here is the code:

#define HTTPMU_HOST_ADDRESS "239.255.255.250"
#define HTTPMU_HOST_PORT 1900
#define SEARCH_REQUEST_STRING "M-SEARCH * HTTP/1.1\n"            \
                              "ST:UPnP:rootdevice\n"             \
                              "MX: 3\n"                          \
                              "Man:\"ssdp:discover\"\n"          \
                              "HOST: 239.255.255.250:1900\n"     \
                                                            "\n"
void UpnpNat::discovery()
{
    setStatus(NAT_STAT::NAT_DISCOVERY);
    m_udpSocketV4.reset(new QUdpSocket(this));

    QHostAddress broadcastIpV4(HTTPMU_HOST_ADDRESS);

    m_udpSocketV4->bind(QHostAddress(QHostAddress::AnyIPv4), 0);
    QByteArray datagram(SEARCH_REQUEST_STRING);

    connect(m_udpSocketV4.get(), &QTcpSocket::readyRead, this, [this]() {
        QByteArray datagram;
        while(m_udpSocketV4->hasPendingDatagrams())
        {
            datagram.resize(int(m_udpSocketV4->pendingDatagramSize()));
            m_udpSocketV4->readDatagram(datagram.data(), datagram.size());
        }

        QString result(datagram);
        auto start= result.indexOf("http://");

        if(start < 0)
        {
            setError(tr("Unable to read the beginning of server answer"));
            setStatus(NAT_STAT::NAT_ERROR);
            return;
        }

        auto end= result.indexOf("\r", start);
        if(end < 0)
        {
            setError(tr("Unable to read the end of server answer"));
            setStatus(NAT_STAT::NAT_ERROR);
            return;
        }

        m_describeUrl= result.sliced(start, end - start);

        setStatus(NAT_STAT::NAT_FOUND);
        m_udpSocketV4->close();
    });

    connect(m_udpSocketV4.get(), &QUdpSocket::errorOccurred, this, [this](QAbstractSocket::SocketError) {
        setError(m_udpSocketV4->errorString());
        setStatus(NAT_STAT::NAT_ERROR);
    });

    m_udpSocketV4->writeDatagram(datagram, broadcastIpV4, HTTPMU_HOST_PORT);
}

The whole goal of the discovery is to get the description file from the server with all available devices and services. The result is stored in m_describeUrl.

Request Description file

Simple request using QNetworkAccessManager.

void UpnpNat::requestDescription()
{
    setStatus(NAT_STAT::NAT_GETDESCRIPTION);
    QNetworkRequest request;
    request.setUrl(QUrl(m_describeUrl));
    m_manager.get(request);
}

Parsing Description file

Your physical network device may act as several Upnp devices. You are looking for one of these device type:

  • urn:schemas-upnp-org:device:InternetGatewayDevice
  • urn:schemas-upnp-org:device:WANDevice
  • urn:schemas-upnp-org:device:WANConnectionDevice

Those type are followed with a number (1 or 2), It is the Upnp protocol version supported by the device.

void UpnpNat::processXML(QNetworkReply* reply)
{
    auto data= reply->readAll();

    if(data.isEmpty()) {
        setError(tr("Description file is empty"));
        setStatus(NAT_STAT::NAT_ERROR);
        return;
    }

    setStatus(NAT_STAT::NAT_DESCRIPTION_FOUND);

    /*
     Boring XML&nbsp;parsing in order to find devices and services.
     Devices:
        constexpr auto deviceType1{"urn:schemas-upnp-org:device:InternetGatewayDevice"};
        constexpr auto deviceType2{"urn:schemas-upnp-org:device:WANDevice"};
        constexpr auto deviceType3{"urn:schemas-upnp-org:device:WANConnectionDevice"};

     Services:
        constexpr auto serviceTypeWanIP{"urn:schemas-upnp-org:service:WANIPConnection"};
        constexpr auto serviceTypeWANPPP{"urn:schemas-upnp-org:service:WANPPPConnection"};  
     */

     m_controlUrl = /* Most important thing to find the controlUrl of the proper service.*/

    setStatus(NAT_STAT::NAT_READY);
}

Send mapping Request

Sending a request is just sending HTTP request with the proper data.

I use inja to generate the http data properly.

This is the inja template.

<?xml version="1.0" encoding="utf-8"?>
<s:Envelope
  xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
  s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:AddPortMapping
      xmlns:u="{{ service }}">
      <NewRemoteHost></NewRemoteHost>
      <NewExternalPort>{{ port }}</NewExternalPort>
      <NewProtocol>{{ protocol }}</NewProtocol>
      <NewInternalPort>{{ port }}</NewInternalPort>
      <NewInternalClient>{{ ip }}</NewInternalClient>
      <NewEnabled>1</NewEnabled>
      <NewPortMappingDescription>{{ description }}</NewPortMappingDescription>
      <NewLeaseDuration>0</NewLeaseDuration>
    </u:AddPortMapping>
  </s:Body>
</s:Envelope>

Then, let’s create a json object with all data. As final step, we need to create a request, set its data, and then post it.

void UpnpNat::addPortMapping(const QString& description, const QString& destination_ip, unsigned short int port_ex,
                             unsigned short int port_in, const QString& protocol)
{
    inja::json subdata;
    subdata["description"]= description.toStdString();
    subdata["protocol"]= protocol.toStdString();
    subdata["service"]= m_serviceType.toStdString();
    subdata["port"]= port_in;
    subdata["ip"]= destination_ip.toStdString();

    auto text= QByteArray::fromStdString(inja::render(loadFile(key::envelop).toStdString(), subdata));

    QNetworkRequest request;
    request.setUrl(QUrl(m_controlUrl));
    QHttpHeaders headers;
    headers.append(QHttpHeaders::WellKnownHeader::ContentType, "text/xml;  charset=\"utf-8\"");
    headers.append("SOAPAction", QString("\"%1#AddPortMapping\"").arg(m_serviceType));
    request.setHeaders(headers);
    m_manager.post(request, text);
}

Finally, just check the answer

The reply has no error, it worked, the status changes to NAT_ADD. Otherwise, the status changes to error.

void UpnpNat::processAnswer(QNetworkReply* reply)
{
    if(reply->error() != QNetworkReply::NoError)
    {
        setError(tr("Something went wrong: %1").arg(reply->errorString()));
        setStatus(NAT_STAT::NAT_ERROR);
        return;
    }
    setStatus(NAT_STAT::NAT_ADD);
}

Don’t hesitate to test it on your own device. Just to validate, it works everywhere. Any comment or change request, please use Github for that.

Source code

联系我们 contact @ memedata.com