
2026/01/10 5:17
**QtNat – Qt UPnPでポートを開く**
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
要約
QtNatはQt 6向けに作られた軽量C++ライブラリで、UPnPを通じてNATポートマッピングを自動化します。シンプルなAPI(
UpnpNat)を公開し、内部で探索・説明取得・ポートマッピング要求を実行時に管理します。
探索
ライブラリはM‑SEARCHリクエストをマルチキャストアドレス 239.255.255.250:1900 に送信し、ルーターからの応答を受け取り、デバイスの説明URLを抽出して
m_descriptionUrl に保存します。
説明解析
QNetworkAccessManager を使用してQtNatはXML説明ファイルをダウンロードし、urn:schemas-upnp-org:device:InternetGatewayDevice、WANDevice、または WANConnectionDevice などのデバイスタイプを検索します。これから関連サービス(WANIPConnection または WANPPPConnection)の制御URLを抽出し m_controlUrl に格納します。
ポートマッピング
SOAP
<AddPortMapping> エンベロープが inja で生成され、外部/内部ポート、プロトコル、内部クライアントIP、説明、およびリース期間を含みます。リクエストは以下のヘッダーを設定します:
Content-Type: text/xml; charset="utf‑8" SOAPAction: "<service type>#AddPortMapping"
成功したレスポンスが返るとステータスは
NAT_ADD に変更され、探索・説明取得・マッピング中に失敗すると NAT_ERROR へ遷移し、説明メッセージが付与されます。
状態フロー
ライブラリの状態機械は次をカバーします:
NAT_IDLENAT_DISCOVERYNAT_GETDESCRIPTIONNAT_DESCRIPTION_FOUNDNAT_FOUNDNAT_READYNAT_ADDNAT_ERROR
使用例
auto nat = UpnpNat::create(); QObject::connect(nat.get(), &UpnpNat::statusChanged, [nat](int status) { switch (static_cast<UpnpNat::Status>(status)) { case UpnpNat::Status::NAT_FOUND: nat->requestDescription(); break; case UpnpNat::Status::NAT_READY: nat->addPortMapping(6664, 6664, "TCP", "QtNat demo"); break; case UpnpNat::Status::NAT_ADD: QCoreApplication::quit(); break; case UpnpNat::Status::NAT_ERROR: qFatal("Error: %s", nat->errorMessage().toUtf8().constData()); } }); nat->discover();
対象読者とメリット
QtNatはピア‑ツー‑ピアアプリ、マルチプレイヤーゲーム、リモートアクセスツール、および手動でルーター設定を行わずに受信NAT接続が必要なソフトウェア向けです。探索とポートマッピングを自動化することでセットアップの摩擦を減らし、デプロイ速度を向上させます。
ソースコードはGitHubでホストされており、貢献者は様々なルーターでライブラリをテストし改善点を提案することが奨励されています。
本文
QtNat – UPnP による NAT ポートマッピングを実現する軽量 C++ ライブラリ(Qt 6)
概要
- UPnP 対応ルーターを自動検出
- ランタイムでポート転送ルールを作成
(必要に応じて既存のマッピングも変更できます) - P2P アプリ、マルチプレイヤーゲーム、リモートアクセスツールなどに最適
クイックスタート
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. 検出
SSDP M-SEARCH をマルチキャストアドレスに送信します。
#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::AnyIPv4, 0); QByteArray datagram(SEARCH_REQUEST_STRING); connect(m_udpSocketV4.get(), &QUdpSocket::readyRead, this, [this](){ QByteArray data; while (m_udpSocketV4->hasPendingDatagrams()){ data.resize(m_udpSocketV4->pendingDatagramSize()); m_udpSocketV4->readDatagram(data.data(), data.size()); } QString result(data); int start = result.indexOf("http://"); if (start < 0){ setError(tr("Unable to read the beginning of server answer")); setStatus(NAT_STAT::NAT_ERROR); return; } int 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.mid(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); }
m_describeUrl にデバイス説明ファイルの URL が格納されます。
2. 説明ファイルの取得
void UpnpNat::requestDescription() { setStatus(NAT_STAT::NAT_GETDESCRIPTION); QNetworkRequest request(QUrl(m_describeUrl)); m_manager.get(request); // 内部で QNetworkAccessManager を使用 }
3. XML 説明ファイルの解析
デバイスは複数の UPnP サービスを公開している可能性があります。
以下のいずれかのサービスを探します。
| デバイスタイプ | 対応するサービス |
|---|---|
| , |
| – |
| – |
void UpnpNat::processXML(QNetworkReply* reply) { QByteArray data = reply->readAll(); if (data.isEmpty()){ setError(tr("Description file is empty")); setStatus(NAT_STAT::NAT_ERROR); return; } // XML を解析し <serviceList> → <serviceType> を取得 m_controlUrl = /* 選択したサービスのコントロール URL */; m_serviceType = /* 例: "urn:schemas-upnp-org:service:WANIPConnection" */; setStatus(NAT_STAT::NAT_READY); }
4. ポートマッピングリクエストの送信
SOAP エンベロープは inja テンプレートで生成します。
<?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>
void UpnpNat::addPortMapping(const QString& description, const QString& destination_ip, unsigned short port_ex, unsigned short 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(); QByteArray body = QByteArray::fromStdString( inja::render(loadFile(key::envelop).toStdString(), subdata)); QNetworkRequest request(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, body); }
5. レスポンスの処理
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); // 成功 }
使い方 / テスト方法
- Qt 6(C++17)でビルド
- UPnP 対応ルーターの後ろにあるマシンで実行
- コンソールに「It worked!」と表示され、正常終了するとプログラムが終了します。
- エラー発生時はメッセージを表示し、
で終了します。exit(1)
ぜひお試しいただき、改善点や機能追加のアイデアを GitHub へ Pull Request してください。