UPnPのNAT越えについて調べてみた

はじめに

P2Pアプリケーションを作ろう!と思い立ったのでP2Pについて調べていました。すると、NAT(またはNAPT)越えをしないとP2P通信が出来ないそうじゃありませんか!!
ということで、今度はNAT越えについて調べてみるとUPnP、STUN、UDP Hole Punchingという方法があるそうですね。なので、今回はUPnPのNAT越えについて調べてみました。UPnPについては以下のページを参考にしました。
UPnPとポートマッピング(GARAさんのページ)
作業メモ -upnpデバイスの取得-
UPnPを利用してグローバルIPを取得する - 2 | ::Hikaru's blog

ネットワーク環境(自宅)

NTTからレンタルしているモデムUPnPに対応していたので、外部から自宅のPCにアクセスできる(NAT越えができる)ようにしてみました。自宅のネットワーク環境を図で表すと、以下のような感じです。

UPnP対応機器を探し出す

まずは、自宅LAN内のUPnP対応機器(上の図のモデム)を探し出します。以下のM-SEARCHメッセージをマルチキャストしてUPnP対応機器からのレスポンスを調べます。

M-SEARCH * HTTP/1.1
MX: 3
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
ST: upnp:rootdevice

このM-SEARCHメッセージをマルチキャストするPythonスクリプトを書きました。以下のコードをsearchRootDevice.pyという名前で保存します。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import socket

M_SEARCH  = 'M-SEARCH * HTTP/1.1\r\n'
M_SEARCH += 'MX: 3\r\n'
M_SEARCH += 'HOST: 239.255.255.250:1900\r\n'
M_SEARCH += 'MAN: "ssdp:discover"\r\n'
M_SEARCH += 'ST: upnp:rootdevice\r\n'
M_SEARCH += '\r\n'

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.settimeout(5)   # 5秒でタイムアウト
s.bind(('', 1900))

# M-SEARCHをマルチキャストする
s.sendto(M_SEARCH, ('239.255.255.250', 1900))

while True:
    try:
        response, address = s.recvfrom(8192)
        print 'from', address
        print response
        print '=' * 40
    except socket.timeout, e: # タイムアウトしたときの処理
        print e
        break

s.close()

このスクリプトを実行すると以下のような結果になりました。

 % ./searcheRootDevice.py
from ('192.168.1.1', 1900)
HTTP/1.1 200 OK
Ext: 
Date: WED, 22 JAN 2070 04:40:41 GMT
ST: upnp:rootdevice
USN: uuid:53563300-0101-0000-000ba233dcd7::upnp:rootdevice
Location: http://192.168.1.1:2869/DeviceDescription.xml        ← ここ大事!
Cache-Control: max-age=600
Server: VxWorks/5.4.2 UPnP/1.0 UPnP-Device-Host/1.0
Content-Length: 0


========================================
timed out

同じメッセージが繰り返し表示されるときもありますが、この結果で注目するのはLocationの値です。ここにはデバイス情報を取得するためにアクセスするURLが書かれています。なので、実際にこのURLにアクセスしてみます。

 % wget http://192.168.1.1:2869/DeviceDescription.xml
--23:28:17--  http://192.168.1.1:2869/DeviceDescription.xml
           => `DeviceDescription.xml'
Connecting to 192.168.1.1:2869... connected.
HTTP request sent, awaiting response... 200 OK
Length: 5,056 (4.9K) [text/xml]

100%[==================================================>] 5,056         --.--K/s             

23:28:17 (161.25 KB/s) - `DeviceDescription.xml' saved [5056/5056]

このファイル(DeviceDescription.xml)の内容は以下のとおりです。

<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<URLBase>http://192.168.1.1:2869/</URLBase>
<device>
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
<friendlyName>ADSL Modem-SV3</friendlyName>
<manufacturer>NTTEAST/NTTWEST</manufacturer>
<manufacturerURL></manufacturerURL>
<modelDescription>ADSL Broadband Router with VoIP function</modelDescription>
<modelName>ADSL Modem-SV3</modelName>
<modelNumber></modelNumber>
<modelURL></modelURL>
<serialNumber></serialNumber>
<UDN>uuid:53563300-0101-0000-000ba233dcd7</UDN>
<UPC></UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType>
<serviceId>urn:upnp-org:serviceId:Layer3Forwarding1</serviceId>
<SCPDURL>/Layer3Forwarding.xml</SCPDURL>
<controlURL>/UD/?0</controlURL>
<eventSubURL>/?0</eventSubURL>
</service>
<service>
<serviceType>urn:schemas-microsoft-com:service:OSInfo:1</serviceType>
<serviceId>urn:microsoft-com:serviceId:OSInfo1</serviceId>
<SCPDURL>/OSInfo.xml</SCPDURL>
<controlURL>/UD/?1</controlURL>
<eventSubURL>/?1</eventSubURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
<friendlyName>WANDevice1</friendlyName>
<manufacturer>NTTEAST/NTTWEST</manufacturer>
<manufacturerURL></manufacturerURL>
<modelDescription>ADSL Broadband Router with VoIP function</modelDescription>
<modelName>ADSL Modem-SV3</modelName>
<modelNumber></modelNumber>
<modelURL></modelURL>
<serialNumber></serialNumber>
<UDN>uuid:53563300-0201-0000-000ba233dcd7</UDN>
<UPC></UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
<SCPDURL>/WANCommonInterfaceConfig.xml</SCPDURL>
<controlURL>/UD/?2</controlURL>
<eventSubURL>/?2</eventSubURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
<friendlyName>WANConnectionDevice1</friendlyName>
<manufacturer>NTTEAST/NTTWEST</manufacturer>
<manufacturerURL></manufacturerURL>
<modelDescription>ADSL Broadband Router with VoIP function</modelDescription>
<modelName>ADSL Modem-SV3</modelName>
<modelNumber></modelNumber>
<modelURL></modelURL>
<serialNumber></serialNumber>
<UDN>uuid:53563300-0301-0000-000ba233dcd7</UDN>
<UPC></UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANDSLLinkConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANDSLLinkC1</serviceId>
<SCPDURL>/WANDSLLinkConfig.xml</SCPDURL>
<controlURL>/UD/?4</controlURL>
<eventSubURL>/?4</eventSubURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:WANPPPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANPPPConn1</serviceId>
<SCPDURL>/WANPPPConnection.xml</SCPDURL>
<controlURL>/UD/?6</controlURL>
<eventSubURL>/?6</eventSubURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:WANPPPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANPPPConn2</serviceId>
<SCPDURL>/WANPPPConnection.xml</SCPDURL>
<controlURL>/UD/?7</controlURL>
<eventSubURL>/?7</eventSubURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:WANPPPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANPPPConn3</serviceId>
<SCPDURL>/WANPPPConnection.xml</SCPDURL>
<controlURL>/UD/?8</controlURL>
<eventSubURL>/?8</eventSubURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:WANPPPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANPPPConn4</serviceId>
<SCPDURL>/WANPPPConnection.xml</SCPDURL>
<controlURL>/UD/?9</controlURL>
<eventSubURL>/?9</eventSubURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:WANPPPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANPPPConn5</serviceId>
<SCPDURL>/WANPPPConnection.xml</SCPDURL>
<controlURL>/UD/?10</controlURL>
<eventSubURL>/?10</eventSubURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:WANPPPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANPPPConn6</serviceId>
<SCPDURL>/WANPPPConnection.xml</SCPDURL>
<controlURL>/UD/?11</controlURL>
<eventSubURL>/?11</eventSubURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:WANPPPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANPPPConn7</serviceId>
<SCPDURL>/WANPPPConnection.xml</SCPDURL>
<controlURL>/UD/?12</controlURL>
<eventSubURL>/?12</eventSubURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:WANPPPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANPPPConn8</serviceId>
<SCPDURL>/WANPPPConnection.xml</SCPDURL>
<controlURL>/UD/?13</controlURL>
<eventSubURL>/?13</eventSubURL>
</service>
</serviceList>
</device>
</deviceList>
</device>
</deviceList>
<presentationURL>http://192.168.1.1/</presentationURL>
</device>
</root>

このファイルの中で大事なのは以下の部分です。

<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<URLBase>http://192.168.1.1:2869/</URLBase>    ← ここが大事!
<device>
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>

     省略

<service>
<serviceType>urn:schemas-upnp-org:service:WANPPPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANPPPConn1</serviceId>
<SCPDURL>/WANPPPConnection.xml</SCPDURL>
<controlURL>/UD/?6</controlURL>   ← ここが大事!
<eventSubURL>/?6</eventSubURL>
</service>
<service>

上記のURLBaseとcontrolURLの値を用いてWAN側のIPアドレスの取得、ポートマッピングの設定・削除などを行っていきます。

WAN側のIPアドレスの取得

上記の方法でcontrolURLが分かれば、以下のようなSOAPアクションをUPnP対応機器に送信することでWAN側のIPアドレス(グローバル)が取得できます。

POST /UD/?6 HTTP/1.1
HOST: 192.168.1.1:2869
CONTENT-LENGTH: 294
CONTENT-TYPE: text/xml; charset="utf-8"
SOAPACTION: "urn:schemas-upnp-org:service:WANPPPConnection:1#GetExternalIPAddress"

<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetExternalIPAddress xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">
</u:GetExternalIPAddress>
</s:Body>
</s:Envelope>

SOAPアクションはtelnetコマンドで192.168.1.1:2869に送信できますが、とりあえずPythonスクリプトを書いてみました。以下のコードをgetExternalIPAddress.pyという名前で保存します。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import urllib2

HOST = '192.168.1.1'
PORT = 2869
CONTROL = '/UD/?6'
URL = 'http://' + HOST + ':' + str(PORT) + CONTROL

SOAP  = '<?xml version="1.0"?>\r\n'
SOAP += '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">\r\n'
SOAP += '<s:Body>\r\n'
SOAP += '<u:GetExternalIPAddress xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">\r\n'
SOAP += '</u:GetExternalIPAddress>\r\n'
SOAP += '</s:Body>\r\n'
SOAP += '</s:Envelope>\r\n'

req = urllib2.Request(URL)
req.add_header('Content-Type', 'text/xml; charset="utf-8"')
req.add_header('SOAPACTION', '"urn:schemas-upnp-org:service:WANPPPConnection:1#GetExternalIPAddress"')
req.add_data(SOAP)

res = urllib2.urlopen(req)

print res.read()

このスクリプトを実行すると以下のような結果になりました。

 % ./getExternalIPAddress.py
<s:Envelope
 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetExternalIPAddressResponse xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">
 <NewExternalIPAddress>XXX.XXX.XXX.XXX</NewExternalIPAddress>    ← ここ大事!
</u:GetExternalIPAddressResponse>
</s:Body>
</s:Envelope>

WAN側のIPアドレスが記述されている部分は隠していますが、ちゃんと取得することができました。念のため、レンタルしているモデムにWebブラウザーからアクセスしてWAN側のIPアドレス(モデムではADSLアドレスという表現をしてた)を見てみると、SOAPアクションで取得したアドレスと一致していました。

現在のポートマッピング情報の取得

WAN側のIPアドレス取得と同様に、以下のSOAPアクションを送信することで現在のポートマッピング情報を取得できます。

POST /UD/?6 HTTP/1.1
HOST: 192.168.1.1:2869
CONTENT-LENGTH: 353
CONTENT-TYPE: text/xml; charset="utf-8"
SOAPACTION: "urn:schemas-upnp-org:service:WANPPPConnection:1#GetGenericPortMappingEntry"

<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<m:GetGenericPortMappingEntry xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1">
<NewPortMappingIndex>0</NewPortMappingIndex>    ← ここが大事!
</m:GetGenericPortMappingEntry>
</s:Body>
</s:Envelope>

このSOAPアクションのNewPortMappingIndexの値を0から順に変化させることでポートマッピングの情報を取得できます。もし、NewPortMappingIndexの値が登録されているポートマッピングの数より大きいとHTTPステータスコード 500を返すそうです。以下のコードをgetGenericPortMappingEntry.pyという名前で保存します。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import urllib2

HOST = '192.168.1.1'
PORT = 2869
CONTROL = '/UD/?6'
URL = 'http://' + HOST + ':' + str(PORT) + CONTROL

ID = 0
while True:
    SOAP  = '<?xml version="1.0"?>\r\n'
    SOAP += '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">\r\n'
    SOAP += '<s:Body>\r\n'
    SOAP += '<m:GetGenericPortMappingEntry xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1">\r\n'
    SOAP += '<NewPortMappingIndex>' + str(ID) + '</NewPortMappingIndex>\r\n'
    SOAP += '</m:GetGenericPortMappingEntry>\r\n'
    SOAP += '</s:Body>\r\n'
    SOAP += '</s:Envelope>\r\n'

    req = urllib2.Request(URL)
    req.add_header('Content-Type', 'text/xml; charset="utf-8"')
    req.add_header('SOAPACTION', '"urn:schemas-upnp-org:service:WANPPPConnection:1#GetGenericPortMappingEntry"')
    req.add_data(SOAP)

    try:
        res = urllib2.urlopen(req)
        print res.read()
    except urllib2.HTTPError, e:
        if e.code != 500:
            print e.code
            print e.msg
        break
    print '=' * 40
    ID += 1

このスクリプトを実行すると以下のような結果になりました。

 % ./getGenericPortMappingEntry.py
<s:Envelope
 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetGenericPortMappingEntryResponse xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">
 <NewRemoteHost></NewRemoteHost>
 <NewExternalPort>80</NewExternalPort>
 <NewProtocol>TCP</NewProtocol>
 <NewInternalPort>80</NewInternalPort>
 <NewInternalClient></NewInternalClient>
 <NewEnabled>0</NewEnabled>
 <NewPortMappingDescription>Webサーバ (HTTP)</NewPortMappingDescription>
 <NewLeaseDuration>0</NewLeaseDuration>
</u:GetGenericPortMappingEntryResponse>
</s:Body>
</s:Envelope>

========================================

私は何も設定していませんでしたが、始めから80番ポートに対して何らかの転送設定がされていました。ここで、先ほどのgetExternalIPAddress.pyで取得したWAN側のIPアドレスWebブラウザーからアクセスすると、http://192.168.1.1と同じ画面(モデムの設定画面)が表示されることがわかりました。なので、既に登録された転送設定はWAN側のIPアドレスからでもモデムの設定が行えるようにするためだと思います。

ポートマッピングの設定を行う

以下のSOAPアクションを送信して、モデムの9090番ポートにきたアクセスを自宅PC(192.168.1.3)の9090番ポートに転送するように設定します。

POST /UD/?6 HTTP/1.1
Host: 192.168.1.1:2869
Content-Length: 671
Content-Type: text/xml; charset="utf-8"
Connection: Close
SOAPACTION: "urn:schemas-upnp-org:service:WANPPPConnection:1#AddPortMapping"

<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<m:AddPortMapping xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>9090</NewExternalPort>
<NewProtocol>TCP</NewProtocol>
<NewInternalPort>9090</NewInternalPort>
<NewInternalClient>192.168.1.3</NewInternalClient>
<NewEnabled>1</NewEnabled>
<NewPortMappingDescription>Test</NewPortMappingDescription>
<NewLeaseDuration>0</NewLeaseDuration>
</m:AddPortMapping>
</s:Body>
</s:Envelope>

これをPythonスクリプトで行うために、以下のコードをaddPortMapping.pyという名前で保存します。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import urllib2

HOST = '192.168.1.1'
PORT = 2869
CONTROL = '/UD/?6'
URL = 'http://' + HOST + ':' + str(PORT) + CONTROL

NEW_EXTERNAL_PORT = 9090   # WAN側のポート番号
NEW_INTERNAL_PORT = 9090   # 転送先ホストのポート番号
NEW_INTERNAL_CLIENT = '192.168.1.3'   # 転送先ホストのIPアドレス
NEW_PROTOCOL = 'TCP'
LEASE_DURATION = '0'    # 設定の有効期間(秒)。0のときは無期限
DESCRIPTION = 'test'

SOAP  = '<?xml version="1.0"?>\r\n'
SOAP += '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">\r\n'
SOAP += '<s:Body>\r\n'
SOAP += '<m:AddPortMapping xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1">\r\n'
SOAP += '<NewRemoteHost></NewRemoteHost>\r\n'
SOAP += '<NewExternalPort>' + str(NEW_EXTERNAL_PORT) + '</NewExternalPort>\r\n'
SOAP += '<NewProtocol>' + NEW_PROTOCOL + '</NewProtocol>\r\n'
SOAP += '<NewInternalPort>' + str(NEW_INTERNAL_PORT) + '</NewInternalPort>\r\n'
SOAP += '<NewInternalClient>' + NEW_INTERNAL_CLIENT + '</NewInternalClient>\r\n'
SOAP += '<NewEnabled>1</NewEnabled>\r\n'
SOAP += '<NewPortMappingDescription>' + DESCRIPTION + '</NewPortMappingDescription>\r\n'
SOAP += '<NewLeaseDuration>' + LEASE_DURATION + '</NewLeaseDuration>\r\n'
SOAP += '</m:AddPortMapping>\r\n'
SOAP += '</s:Body>\r\n'
SOAP += '</s:Envelope>\r\n'

req = urllib2.Request(URL)
req.add_header('Content-Type', 'text/xml; charset="utf-8"')
req.add_header('SOAPACTION', '"urn:schemas-upnp-org:service:WANPPPConnection:1#AddPortMapping"')
req.add_data(SOAP)

try:
    res = urllib2.urlopen(req)
    print res.read()
except urllib2.HTTPError, e:
    print e.code
    print e.msg

このスクリプトを実行すると、以下のような結果になりました。

 % ./addPortMapping.py
<s:Envelope
 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:AddPortMappingResponse xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">
</u:AddPortMappingResponse>
</s:Body>
</s:Envelope>

特にエラーらしきものが出力されていないので、9090番ポートに対する設定が行われたと思います。本当に設定できたのかをgetGenericPortMappingEntry.pyを実行して確認してみます。

 % ./getGenericPortMappingEntry.py
<s:Envelope
 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetGenericPortMappingEntryResponse xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">
 <NewRemoteHost></NewRemoteHost>
 <NewExternalPort>9090</NewExternalPort>
 <NewProtocol>TCP</NewProtocol>
 <NewInternalPort>9090</NewInternalPort>
 <NewInternalClient>192.168.1.3</NewInternalClient>
 <NewEnabled>1</NewEnabled>
 <NewPortMappingDescription>test</NewPortMappingDescription>
 <NewLeaseDuration>0</NewLeaseDuration>
</u:GetGenericPortMappingEntryResponse>
</s:Body>
</s:Envelope>

========================================
<s:Envelope
 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetGenericPortMappingEntryResponse xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">
 <NewRemoteHost></NewRemoteHost>
 <NewExternalPort>80</NewExternalPort>
 <NewProtocol>TCP</NewProtocol>
 <NewInternalPort>80</NewInternalPort>
 <NewInternalClient></NewInternalClient>
 <NewEnabled>0</NewEnabled>
 <NewPortMappingDescription>Webサーバ (HTTP)</NewPortMappingDescription>
 <NewLeaseDuration>0</NewLeaseDuration>
</u:GetGenericPortMappingEntryResponse>
</s:Body>
</s:Envelope>

========================================

この実行結果より、新たに9090番ポートへの転送設定が追加されていることが分かります。また、実際に自宅PC上でechoサーバーを立ち上げ(192.168.1.3:9090)、外部のネットワークからWAN側のIPアドレス:9090にtelnetでアクセスすると、NAT越えをしてechoサーバーと通信することができました!

ポートマッピングの設定を削除する

以下のSOAPアクションを送信して、先ほど追加した9090番ポートへの転送設定を削除することができます。

POST /UD/?6 HTTP/1.1
Host: 192.168.1.1:2869
Content-Length: 424
Content-Type: text/xml; charset="utf-8"
Connection: Close
SOAPACTION: "urn:schemas-upnp-org:service:WANPPPConnection:1#DeletePortMapping"

<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<m:DeletePortMapping xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>9090</NewExternalPort>
<NewProtocol>TCP</NewProtocol>
</m:DeletePortMapping>
</s:Body>
</s:Envelope>

これをPythonスクリプトで行うために、以下のコードをdeletePortMapping.pyという名前で保存しました。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import urllib2

HOST = '192.168.1.1'
PORT = 2869
CONTROL = '/UD/?6'
URL = 'http://' + HOST + ':' + str(PORT) + CONTROL

NEW_EXTERNAL_PORT = 9090
NEW_PROTOCOL = 'TCP'

SOAP  = '<?xml version="1.0"?>\r\n'
SOAP += '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">\r\n'
SOAP += '<s:Body>\r\n'
SOAP += '<m:DeletePortMapping xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1">\r\n'
SOAP += '<NewRemoteHost></NewRemoteHost>\r\n'
SOAP += '<NewExternalPort>' + str(NEW_EXTERNAL_PORT) + '</NewExternalPort>\r\n'
SOAP += '<NewProtocol>' + NEW_PROTOCOL + '</NewProtocol>\r\n'
SOAP += '</m:DeletePortMapping>\r\n'
SOAP += '</s:Body>\r\n'
SOAP += '</s:Envelope>\r\n'

req = urllib2.Request(URL)
req.add_header('Content-Type', 'text/xml; charset="utf-8"')
req.add_header('SOAPACTION', '"urn:schemas-upnp-org:service:WANPPPConnection:1#DeletePortMapping"')
req.add_data(SOAP)

try:
    res = urllib2.urlopen(req)
    print res.read()
except urllib2.HTTPError, e:
    print e.code
    print e.msg

このスクリプトを実行すると、以下のような結果になりました。

 % ./deletePortMapping.py
<s:Envelope
 xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:DeletePortMappingResponse xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">
</u:DeletePortMappingResponse>
</s:Body>
</s:Envelope>

特にエラーらしきものが出力がされていないので、9090番ポートへの転送設定は削除されたはずです。この後のgetGenericPortMappingEntry.pyの実行結果は割愛しますが、実際に外部からWAN側のIPアドレス:9090への通信はできなくなっていました。

WAN側のIPアドレスがプライベートだった場合

多分、2重ルーティングになってると思います。私の自宅も、始めは2重ルーティングになっていたのでgetExternalIPAddress.pyを実行するとWAN側のIPアドレスが192.168.1.2というプライベートアドレスになっていました。原因は、モデムとPCの間にある無線LANルーターがルーティングモードに設定されていたためでした。なので、Webブラウザーから無線LANルーターの設定をブリッジモードに変換することで解決できました。

おわりに

とりあえず、私の自宅においてはUPnPでNAT越えを行うことが出来ました。他の環境では試していないので、上のスクリプトがちゃんと動くかどうかわかりません。あしからず。
この調子で、次はSTUNかUDP Hole Punchingについて調べてみようと思います。