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();
- We create the object (l:0)
- We connect to statusChanged signal to get notified (l:2)
- When status is NAT_FOUND, we request the description (l:11)
- When status is NAT_READY, we request the port mapping (l:14)
- When status is NAT_ADD, It means the port mapping request has been added, It worked! The application quits.(l:17)
- When status is NAT_ERROR, Error occured and display the error text. The application exits on error. (l:21)
- We connect to error changed in order to detect errors. (l:14)
- 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 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.