{"id":5105,"date":"2026-01-09T17:38:35","date_gmt":"2026-01-09T17:38:35","guid":{"rendered":"http:\/\/renaudguezennec.eu\/?p=5105"},"modified":"2026-01-11T01:05:54","modified_gmt":"2026-01-11T01:05:54","slug":"qtnat-open-you-port-with-qt","status":"publish","type":"post","link":"http:\/\/renaudguezennec.eu\/index.php\/2026\/01\/09\/qtnat-open-you-port-with-qt\/","title":{"rendered":"QtNat &#8211; Open your port with Qt"},"content":{"rendered":"\n<p>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.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"use-it\">Use it<\/h2>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; title: ; quick-code: false; notranslate\" title=\"\">\n    UpnpNat nat;\n\n    QObject::connect(&amp;nat, &amp;UpnpNat::statusChanged, &#x5B;&amp;nat, &amp;app]() {\n        switch(nat.status())\n        {\n        case UpnpNat::NAT_STAT::NAT_IDLE:\n        case UpnpNat::NAT_STAT::NAT_DISCOVERY:\n        case UpnpNat::NAT_STAT::NAT_GETDESCRIPTION:\n        case UpnpNat::NAT_STAT::NAT_DESCRIPTION_FOUND:\n            break;\n        case UpnpNat::NAT_STAT::NAT_FOUND:\n            nat.requestDescription();\n            break;\n        case UpnpNat::NAT_STAT::NAT_READY:\n            nat.addPortMapping(&quot;UpnpTest&quot;, nat.localIp(), 6664, 6664, &quot;TCP&quot;);\n            break;\n        case UpnpNat::NAT_STAT::NAT_ADD:\n            qDebug() &lt;&lt; &quot;It worked!&quot;;\n            app.quit();\n            break;\n        case UpnpNat::NAT_STAT::NAT_ERROR:\n            qDebug() &lt;&lt;&quot;Error:&quot; &lt;&lt;nat.error();\n            app.exit(1);\n            break;\n        }\n    });\n\n    nat.discovery();\n\n<\/pre><\/div>\n\n\n<ol class=\"wp-block-list\">\n<li>We create the object (l:0)<\/li>\n\n\n\n<li>We connect to statusChanged signal to get notified (l:2)<\/li>\n\n\n\n<li>When status is <strong>NAT_FOUND<\/strong>, we request the description (l:11)<\/li>\n\n\n\n<li>When status is <strong>NAT_READY<\/strong>, we request the port mapping (l:14)<\/li>\n\n\n\n<li>When status is <strong>NAT_ADD<\/strong>, It means the port mapping request has been added, It worked! The application quits.(l:17)<\/li>\n\n\n\n<li>When status is <strong>NAT_ERROR<\/strong>, Error occured and display the error text. The application exits on error. (l:21)<\/li>\n\n\n\n<li>We connect to error changed in order to detect errors. (l:14)<\/li>\n\n\n\n<li>We start the discovery. (l:28)<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"technical-explainations\">Technical explainations<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"the-discovery\">The discovery<\/h3>\n\n\n\n<p>Basically, we need to know if there is a upnp server around. \nTo do so, we send an <strong>M-SEARCH<\/strong> request on the multicast address.<\/p>\n\n\n\n<p>Here is the code: <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; title: ; quick-code: false; notranslate\" title=\"\">\n#define HTTPMU_HOST_ADDRESS &quot;239.255.255.250&quot;\n#define HTTPMU_HOST_PORT 1900\n#define SEARCH_REQUEST_STRING &quot;M-SEARCH * HTTP\/1.1\\n&quot;            \\\n                              &quot;ST:UPnP:rootdevice\\n&quot;             \\\n                              &quot;MX: 3\\n&quot;                          \\\n                              &quot;Man:\\&quot;ssdp:discover\\&quot;\\n&quot;          \\\n                              &quot;HOST: 239.255.255.250:1900\\n&quot;     \\\n                                                            &quot;\\n&quot;\nvoid UpnpNat::discovery()\n{\n    setStatus(NAT_STAT::NAT_DISCOVERY);\n    m_udpSocketV4.reset(new QUdpSocket(this));\n\n    QHostAddress broadcastIpV4(HTTPMU_HOST_ADDRESS);\n\n    m_udpSocketV4-&gt;bind(QHostAddress(QHostAddress::AnyIPv4), 0);\n    QByteArray datagram(SEARCH_REQUEST_STRING);\n\n    connect(m_udpSocketV4.get(), &amp;QTcpSocket::readyRead, this, &#x5B;this]() {\n        QByteArray datagram;\n        while(m_udpSocketV4-&gt;hasPendingDatagrams())\n        {\n            datagram.resize(int(m_udpSocketV4-&gt;pendingDatagramSize()));\n            m_udpSocketV4-&gt;readDatagram(datagram.data(), datagram.size());\n        }\n\n        QString result(datagram);\n        auto start= result.indexOf(&quot;http:\/\/&quot;);\n\n        if(start &lt; 0)\n        {\n            setError(tr(&quot;Unable to read the beginning of server answer&quot;));\n            setStatus(NAT_STAT::NAT_ERROR);\n            return;\n        }\n\n        auto end= result.indexOf(&quot;\\r&quot;, start);\n        if(end &lt; 0)\n        {\n            setError(tr(&quot;Unable to read the end of server answer&quot;));\n            setStatus(NAT_STAT::NAT_ERROR);\n            return;\n        }\n\n        m_describeUrl= result.sliced(start, end - start);\n\n        setStatus(NAT_STAT::NAT_FOUND);\n        m_udpSocketV4-&gt;close();\n    });\n\n    connect(m_udpSocketV4.get(), &amp;QUdpSocket::errorOccurred, this, &#x5B;this](QAbstractSocket::SocketError) {\n        setError(m_udpSocketV4-&gt;errorString());\n        setStatus(NAT_STAT::NAT_ERROR);\n    });\n\n    m_udpSocketV4-&gt;writeDatagram(datagram, broadcastIpV4, HTTPMU_HOST_PORT);\n}\n\n<\/pre><\/div>\n\n\n<p>The whole goal of the discovery is to get the <strong>description<\/strong> file from the server with all available devices and services.\nThe result is stored in <code>m_describeUrl<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"request-description-file\">Request Description file<\/h3>\n\n\n\n<p>Simple request using <a href=\"https:\/\/doc.qt.io\/qt-6\/qnetworkaccessmanager.html\">QNetworkAccessManager<\/a>.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; title: ; quick-code: false; notranslate\" title=\"\">\nvoid UpnpNat::requestDescription()\n{\n    setStatus(NAT_STAT::NAT_GETDESCRIPTION);\n    QNetworkRequest request;\n    request.setUrl(QUrl(m_describeUrl));\n    m_manager.get(request);\n}\n\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"parsing-description-file\">Parsing Description file<\/h3>\n\n\n\n<p>Your physical network device may act as several Upnp devices.\nYou are looking for one of these device type:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>urn:schemas-upnp-org:device:InternetGatewayDevice<\/li>\n\n\n\n<li>urn:schemas-upnp-org:device:WANDevice<\/li>\n\n\n\n<li>urn:schemas-upnp-org:device:WANConnectionDevice<\/li>\n<\/ul>\n\n\n\n<p>Those type are followed with a number (1 or 2), It is the Upnp protocol version supported by the device.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; title: ; quick-code: false; notranslate\" title=\"\">\nvoid UpnpNat::processXML(QNetworkReply* reply)\n{\n    auto data= reply-&gt;readAll();\n\n    if(data.isEmpty()) {\n        setError(tr(&quot;Description file is empty&quot;));\n        setStatus(NAT_STAT::NAT_ERROR);\n        return;\n    }\n\n    setStatus(NAT_STAT::NAT_DESCRIPTION_FOUND);\n\n    \/*\n     Boring XML&amp;nbsp;parsing in order to find devices and services.\n     Devices:\n        constexpr auto deviceType1{&quot;urn:schemas-upnp-org:device:InternetGatewayDevice&quot;};\n        constexpr auto deviceType2{&quot;urn:schemas-upnp-org:device:WANDevice&quot;};\n        constexpr auto deviceType3{&quot;urn:schemas-upnp-org:device:WANConnectionDevice&quot;};\n\n     Services:\n        constexpr auto serviceTypeWanIP{&quot;urn:schemas-upnp-org:service:WANIPConnection&quot;};\n        constexpr auto serviceTypeWANPPP{&quot;urn:schemas-upnp-org:service:WANPPPConnection&quot;};  \n     *\/\n\n     m_controlUrl = \/* Most important thing to find the controlUrl of the proper service.*\/\n\n    setStatus(NAT_STAT::NAT_READY);\n}\n\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"send-mapping-request\">Send mapping Request<\/h3>\n\n\n\n<p>Sending a request is just sending HTTP&nbsp;request with the proper data. <\/p>\n\n\n\n<p>I use <a href=\"https:\/\/github.com\/pantor\/inja\">inja<\/a> to generate the http data properly.<\/p>\n\n\n\n<p>This is the inja template.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: xml; title: ; quick-code: false; notranslate\" title=\"\">\n&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;\n&lt;s:Envelope\n  xmlns:s=&quot;http:\/\/schemas.xmlsoap.org\/soap\/envelope\/&quot;\n  s:encodingStyle=&quot;http:\/\/schemas.xmlsoap.org\/soap\/encoding\/&quot;&gt;\n  &lt;s:Body&gt;\n    &lt;u:AddPortMapping\n      xmlns:u=&quot;{{ service }}&quot;&gt;\n      &lt;NewRemoteHost&gt;&lt;\/NewRemoteHost&gt;\n      &lt;NewExternalPort&gt;{{ port }}&lt;\/NewExternalPort&gt;\n      &lt;NewProtocol&gt;{{ protocol }}&lt;\/NewProtocol&gt;\n      &lt;NewInternalPort&gt;{{ port }}&lt;\/NewInternalPort&gt;\n      &lt;NewInternalClient&gt;{{ ip }}&lt;\/NewInternalClient&gt;\n      &lt;NewEnabled&gt;1&lt;\/NewEnabled&gt;\n      &lt;NewPortMappingDescription&gt;{{ description }}&lt;\/NewPortMappingDescription&gt;\n      &lt;NewLeaseDuration&gt;0&lt;\/NewLeaseDuration&gt;\n    &lt;\/u:AddPortMapping&gt;\n  &lt;\/s:Body&gt;\n&lt;\/s:Envelope&gt;\n\n<\/pre><\/div>\n\n\n<p>Then, let&#8217;s create a json object with all data. \nAs final step, we need to create a request, set its data, and then post it. <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; title: ; quick-code: false; notranslate\" title=\"\">\nvoid UpnpNat::addPortMapping(const QString&amp; description, const QString&amp; destination_ip, unsigned short int port_ex,\n                             unsigned short int port_in, const QString&amp; protocol)\n{\n    inja::json subdata;\n    subdata&#x5B;&quot;description&quot;]= description.toStdString();\n    subdata&#x5B;&quot;protocol&quot;]= protocol.toStdString();\n    subdata&#x5B;&quot;service&quot;]= m_serviceType.toStdString();\n    subdata&#x5B;&quot;port&quot;]= port_in;\n    subdata&#x5B;&quot;ip&quot;]= destination_ip.toStdString();\n\n    auto text= QByteArray::fromStdString(inja::render(loadFile(key::envelop).toStdString(), subdata));\n\n    QNetworkRequest request;\n    request.setUrl(QUrl(m_controlUrl));\n    QHttpHeaders headers;\n    headers.append(QHttpHeaders::WellKnownHeader::ContentType, &quot;text\/xml;  charset=\\&quot;utf-8\\&quot;&quot;);\n    headers.append(&quot;SOAPAction&quot;, QString(&quot;\\&quot;%1#AddPortMapping\\&quot;&quot;).arg(m_serviceType));\n    request.setHeaders(headers);\n    m_manager.post(request, text);\n}\n\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"finally-just-check-the-answer\">Finally, just check the answer<\/h3>\n\n\n\n<p>The reply has no error, it worked, the status changes to <strong>NAT_ADD<\/strong>. Otherwise, the status changes to error.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; title: ; quick-code: false; notranslate\" title=\"\">\nvoid UpnpNat::processAnswer(QNetworkReply* reply)\n{\n    if(reply-&gt;error() != QNetworkReply::NoError)\n    {\n        setError(tr(&quot;Something went wrong: %1&quot;).arg(reply-&gt;errorString()));\n        setStatus(NAT_STAT::NAT_ERROR);\n        return;\n    }\n    setStatus(NAT_STAT::NAT_ADD);\n}\n\n<\/pre><\/div>\n\n\n<p>Don&#8217;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.<\/p>\n\n\n\n<p><a href=\"https:\/\/github.com\/obiwankennedy\/QtUpnpNat\">Source code<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":5107,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_import_markdown_pro_load_document_selector":0,"_import_markdown_pro_submit_text_textarea":"","footnotes":""},"categories":[81],"tags":[9,88,37,86,10,87,85],"class_list":["post-5105","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-en","tag-c","tag-cpp","tag-dev","tag-network","tag-qt","tag-qt6","tag-upnp"],"_links":{"self":[{"href":"http:\/\/renaudguezennec.eu\/index.php\/wp-json\/wp\/v2\/posts\/5105","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/renaudguezennec.eu\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/renaudguezennec.eu\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/renaudguezennec.eu\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/renaudguezennec.eu\/index.php\/wp-json\/wp\/v2\/comments?post=5105"}],"version-history":[{"count":4,"href":"http:\/\/renaudguezennec.eu\/index.php\/wp-json\/wp\/v2\/posts\/5105\/revisions"}],"predecessor-version":[{"id":5112,"href":"http:\/\/renaudguezennec.eu\/index.php\/wp-json\/wp\/v2\/posts\/5105\/revisions\/5112"}],"wp:featuredmedia":[{"embeddable":true,"href":"http:\/\/renaudguezennec.eu\/index.php\/wp-json\/wp\/v2\/media\/5107"}],"wp:attachment":[{"href":"http:\/\/renaudguezennec.eu\/index.php\/wp-json\/wp\/v2\/media?parent=5105"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/renaudguezennec.eu\/index.php\/wp-json\/wp\/v2\/categories?post=5105"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/renaudguezennec.eu\/index.php\/wp-json\/wp\/v2\/tags?post=5105"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}