Nuro光のルーター(HG8045Q)でDMZからglobal側のIPアドレスを取得する(UPnP)

Pocket

最近、自宅の回線をNuro光にしました。今までLinuxマシンにPPPoEルーターをやらせていたのですが、それと同等の環境を得るためにDMZへLinuxマシンを置いて処理をやらせるようにしました。

ここで一つ問題なのが、いかにして外部サービスに頼らすglobal IPのアドレスを調べるかです。ルーターはISPから貸与されたHG8045Qであり、管理画面は今どきのJavaScript必須なwebインターフェースです。httpで管理画面をたたいてアドレスを取得するのは面倒そうでした。できれば外部サービスには頼りたくありません。

UPnPのNAT越えについて調べてみた – いろいろな何か :こちらの記事を参考に、UPnPでアドレスを取得する方法を行いました。

まずは記事に習ってM-SEARCHを投げます。Python3用コードに書き直しました。

#!/usr/bin/python3

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'
M_SEARCH = bytes(M_SEARCH, 'ascii')
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.decode('ascii'))
print('=' * 40)
except socket.timeout as e: # タイムアウトしたときの処理
print(e)
break
s.close()

これを実行するとDLNAサーバになれるWindows10マシンたちも反応するのですが、必要なルータ(手で固定IPとして設定した)のレスポンスのみに着目します。

HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Thu, 14 May 2020 22:12:07 GMT
EXT:
LOCATION: http://192.168.xx.254:49652/49652gatedesc.xml
OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01
01-NLS: 0405f658-xxxx-xxxx-xxxx-e6e487df1fbe
SERVER: Linux/3.10.53-HULK2, UPnP/1.0, Portable SDK for UPnP devices/1.6.18
X-User-Agent: UPnP/1.0 DLNADOC/1.50
ST: urn:schemas-upnp-org:service:Layer3Forwarding:1
USN: uuid:00e0fc37-xxxx-xxxx-xxxx-084F0AC2AED2::urn:schemas-upnp-org:service:Layer3Forwarding:1


LOCATIONに書かれているURLにアクセスしてみます。

 <?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device>
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
<friendlyName>Huawei IGD</friendlyName>
<manufacturer>Huawei</manufacturer>
<manufacturerURL>www.huawei.com</manufacturerURL>
<modelDescription>Huawei EchoLife Series</modelDescription>
<modelName>Huawei EchoLife Series</modelName>
<modelNumber>Huawei ONT</modelNumber>
<modelURL></modelURL>
(略)
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
<controlURL>/upnp/control/WANIPConn1</controlURL>
<eventSubURL>/upnp/control/WANIPConn1</eventSubURL>
<SCPDURL>/gateconnSCPD.xml</SCPDURL>
</service>
(以下略)

参考にした記事ではWANPPPConnection:1を見るようになっていましたが、Nuro光はIPoEでアドレスをもらうためWANIPConnection:1を見るようにすれば良いようです。以下のようなコードでSOAPリクエストを投げ、結果をparseしました。

#!/usr/bin/python3
#-*- coding:utf-8 -*-

import urllib.request, urllib.error, urllib.parse
from bs4 import BeautifulSoup

HOST = '192.168.xx.254'
PORT = 49652
CONTROL = '/upnp/control/WANIPConn1'
URL = 'http://' + HOST + ':' + str(PORT) + CONTROL

SOAP = '''<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
     s:encodingStyle="http://schemas.xmlsoap.org/encoding/">
  <s:Body>
    <m:GetExternalIPAddress xmlns:m="urn:schemas-upnp-org:service:WANIPConnection:1">
    </m:GetExternalIPAddress>
  </s:Body>
</s:Envelope>
'''
SOAP = bytes(SOAP, 'ascii')

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

res = urllib.request.urlopen(req)

x = res.read()
x = x.decode('ascii')

soup = BeautifulSoup(x, 'xml')
ex = soup.find_all('NewExternalIPAddress')[0]
ipaddr = ex.text
print(ipaddr)

これで無事グローバスIPアドレスを外部に頼らず取得できるようになりました。これでddnsの更新が行えます。