見出し画像

Pythonでポート開放してみた ~UPnPでプログラムからポート操作 GUIのおまけつき~

割引あり

※本記事は最後の項目の一部以外無料です

こんにちはRcatです。
今更ですが、ポート開放の自動化について書こうと思います。今更というのはだいぶ前にプログラムだけ書いていて表に出していなかったという意味です。
というわけで、今回はUPnPを使ってポート開放を行っていきます。



はじめに

利用規約

情報や作品の活用時は事前に利用規約をご確認ください。

コメントについて

利用規約のガイドラインを確認の上コメントしてください


概要

皆さんupnpって聞いたことありますか?
調べてみるとユニバーサルプラグアンドプレイの略みたいです。
んで何ができるのと言うとポート開放ができます
これを使うメリットとしては、以下のようなことが挙げられます。

  • ルーター設定画面に入らなくてもポート開放ができる

  • 自動なので必要な時に開けて不要な時は閉めると言ったことがにできる

  • プログラムに組み込んで、サーバーの立ち上げとボートの開放を自動化できる

実は大昔こんなの作ってた。ルーターに風穴開けたアイコンです。

ポートの設定は、相手先が起動していないと意味がないので、基本的に開けっぱなしでも実質空いてないので問題はないですが、知らないうちにそのポートで待ち受けていたら開いてしまいます。
例えば、この後SSL証明書の自動更新プログラムを書くために、ポート80番を自動的に開け閉めしたいと考えています。というか、そのために復習も兼ねてこの記事を書いています。
しかし、さすがに内容が古すぎたので、今回プログラムを書き直すことにしました。


配布ツールの使い方

さて、今回プログラムの方を作り直しましたので、使い方を先に説明しておきます。
使い方が2通りあり、guiで誰でも簡単に使える方法とモジュールとしてインポートし、他のスクリプトから参照する方法があります。

動作環境

プログラム言語:Python

Windows/Linuxで動作確認済み
GUIはWindowsのみ確認

共通設定環境構築

まず最初に環境構築をします。とは言ってもライブラリを2つインストールするだけです。
Pythonって何?ライブラリって何?という初めての方は、まずは以下の記事を参考にPythonの導入をしてください。

本作は自動環境構築対応です
guiで使う場合は歯車マークのStart.batを起動するだけでOKです。
初回のみ起動に数十秒時間がかかります。
ライブラリがあるならpywの方をダブルクリックでもいいです。

Linuxでは他のスクリプトから参照するだろうということで、環境構築系は入れていません。
必要なライブラリはrequirementsを確認してください。requestsだけで動くとは思いますが

ポート開放アプリの使い方(GUIでサクッと使う)

このセクションではGUIを起動して、誰でも簡単に使える方法を紹介します。
上の方で話しましたが、すでに環境構築の時点でこの画面が立ち上がっているのではないでしょうか。

各項目の設定

  • ホスト
    このプログラムを立ち上げているコンピューターのIPアドレスです。
    自動的に入力されるので変更しなくて大丈夫です。

  • 有効時間
    0と入力すれば無限に開きっぱなしです。自分で閉じる必要があります。
    分で数字を入力した場合はその時間で勝手に閉じます。

  • ポート番号
    開けたいポート番号を入力してください。
    例えばマイクラなら25565ですね。

  • モード
    開放か閉鎖かで選べます。
    有効時間を設定していない場合は、終わったら閉鎖をしてください。

設定が終わったら実行ボタンを押すことで操作できます。
なお、ネットワーク環境によりうまく動かない場合があります。個人の環境の問題なのでサポートはできませんのでご了承ください。


ポート開放プログラムの使い方(モジュールとして使う)

次のセクションではモジュールとしてインポートすることでインタープリターから使ったり、他のプログラムに組み込む方法を紹介します。

使い方

  • 環境を構築する
    2つのライブラリが必要です。
    Windowsではrequestsのみ。Linuxではipgetが追加で必要です。

  • 本スクリプトをモジュールとしてインポートする
    使いたいフォルダの中に入れて、ファイル名でインポートしてください。

  • クラスをインスタンスする
    クラフトの名前は"UPnPPortOpen"です。好きな変数に対してインスタンスしてください。

  • ポート開放を実行する
    "PortControl"メソッドを使ってコントロールを行います。引数は以下の通りです。
    ポート番号だけが必須で、それ以外は任意です。

    • Portnum
      開けたいポート番号。必須です。

    • Host
      クライアントのIPアドレス。自動で設定されます。

    • Protocol
      デフォルトは"TCP"です。
      udpを開ける場合は明示的に指定してください。

    • Mode
      ポートを開放するか閉鎖するか動作を選択します。
      デフォルトは"open"です。
      それ以外の文字列を入力すると閉じるように動作します。

    • Time
      開放の継続時間を設定します。デフォルトは0で無限です。
      短時間だけ開けておくなら、これを設定しておけば閉じる手間がありません。

(venv) R:\Python\Network\UPNP\MSearch2>py
Python 3.11.6 (tags/v3.11.6:8b6ee5b, Oct  2 2023, 14:57:12) [MSC v.1935 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import M_search2 as ms
>>> upnp = ms.UPnPPortOpen()
>>> upnp.PortControl(80,Time=5)
操作に成功しました
True
>>> upnp.PortControl(80,Mode="close")
コントロールURLアクセスエラー。コード:500
False
>>> upnp.PortControl(80,Time=30)
操作に成功しました
True
>>> upnp.PortControl(80,Mode="close")
操作に成功しました

実際に使うとこんな感じです。
上からインポート、インスタンス、開放という順番で行っています。
最初の開放は5秒間なので、5秒以上経ってから閉じようとしたのでエラーになっていますね。つまり自動で閉じたということです。
次に少し長くしてから30秒以内にクローズを行います。操作に成功したので手動で閉じたということです。

Linuxで失敗する場合

エラーメッセージが以下の場合、ファイアウォールにブロックされていることがあります。
作った後、やっとメインのLinuxで試した時に全然動かなくて焦りました。

  • ルーターが見つかりませんでした

  • ルーターのロケーションを取得できませんでした

原因ですが、ufwに思いっきりブロックされていました。
このコマンドでログを見てブロックがかかっていればufwのせいです。

tail /var/log/ufw.log
Sep  7 02:22:25 rcat-Green-G5 kernel: [1220117.272885] [UFW BLOCK] IN=enp1s0 OUT= MAC=000000 SRC=192.168.0.1 DST=224.0.0.1 LEN=32 TOS=0x00 PREC=0x00 TTL=1 ID=0 DF PROTO=2

解決方法ですが、1900番を通るようにすればいけると思ったのですが、なぜか通らない…。
最終的にIPアドレスで許可にしました。
ちなみに1900番を開けた時点で他の機器は呼びかけすると応答があるので、デフォルトゲートウェイだけ変な扱いなのかな?
まあ、ローカルIPアドレスなので深く気にしないで設定することにします。

sudo ufw allow from 192.168.0.1

Linuxでデバイス名が違う場合

このツールを使うには自分のIPアドレスが必要です。Windowsは簡単に取得できるのですが、Linuxはそうもいかないのが実情です。
内部で自動的に取得するようにはしていますが、うまく入手できない場合、まず最初にクラスのインスタンス時に直接入力してしまうという方法があります。

ご自身のプログラムの中で最適なIPアドレスの取得方法を検討し、以下のような形で渡してください。
こうすることでIPアドレスが渡したものになっていることがわかります。

>>> import M_search2 as ms
>>> upnp = ms.UPnPPortOpen("192.168.0.4")
>>> upnp.MyIP
'192.168.0.4'

ポートを開放するソースコード解説

ここから先はプログラム的な話が気になる方向けです。
まぁ解説と言ってもだいぶ前のことなのであんまり覚えてないんですが…。
ソースコードを見ると4年以上前ですね。
というわけでリフファクタリングしながら思い出しつつ解説していきます。

M-Searchでルーターを探す

まず最初に、ポート開放はルーターに対する設定なので、まずはルーターを見つける必要があります。それにはM-Searchを使います。
あれ?この名前どっかでもみましたね。

実は以下のESP32を検索するの記事で書いてます。
これの元ネタが実は今回のポート開放だったりします。
詳しくは以下の記事で書いていますが、ネットワーク全体に呼びかけて特定のデバイスに応答してもらうというものです。
今回はルーターに出てこいやぁと声をかけます。

さて、さすがに4年前に書いたソースコードをそのまま晒すのはちょっとやなので、書き直したもので解説します。
さすがにこの時期になると、なんでそういう書き方をしたのかっていうツッコミどころが多すぎてたまりません。

まずはルーターを検索するコードです。
この関数に対して検索するサービスを渡すことで応答を得ることができます。
この辺に関しては上の記事で紹介しているので、細かいことは省きます。

ちなみにこの関数は失敗すると数回呼び出されます。
というのも上で行っているサービスというのがメーカーによって違うことがあるからです。
ネットで調べたところ、今のところ2種類あることがわかっているので、以下が渡されます。

RouterServiceList = ['urn:schemas-upnp-org:service:WANIPConnection:1','urn:schemas-upnp-org:service:WANPPPConnection:1']

ロケーションを取得する

もしもルーターから応答があれば先に進みます。次に必要なのはロケーションです。ここにアクセスすることで、ルーターが提供しているコントロールURLを取得することができます。

ルーターの検索からロケーションの取得までは以下の関数にまとめられています。4行目でサーチ関数が呼び出されていますね。

ちなみにサーチ関数だけ動かすと以下のようになります。
これがルーターからの応答になります。今回に関してはどっちのサービス名でも反応することが分かりますね。
ロケーションの部分はLOCATIONに書いてあります。
上の関数では正規表現でこれを引っ掛けています。

>>> r = m.Search("urn:schemas-upnp-org:service:WANPPPConnection:1")
>>> r
'HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=120\r\nST: urn:schemas-upnp-org:service:WANPPPConnection:1\r\nUSN: uuid:8cd::urn:schemas-upnp-org:service:WANPPPConnection:1\r\nEXT:\r\nSERVER: TP-Link/TP-LINK UPnP/1.1 MiniUPnPd/1.8\r\nLOCATION: http://192.168.0.1:1900/rootDesc.xml\r\nOPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01\r\n01-NLS: 1\r\nBOOTID.UPNP.ORG: 1\r\nCONFIGID.UPNP.ORG: 1337\r\n\r\n'
>>> print(r)
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=120
ST: urn:schemas-upnp-org:service:WANPPPConnection:1
USN: uuid:8cd::urn:schemas-upnp-org:service:WANPPPConnection:1
EXT:
SERVER: TP-Link/TP-LINK UPnP/1.1 MiniUPnPd/1.8
LOCATION: http://192.168.0.1:1900/rootDesc.xml
OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01
01-NLS: 1
BOOTID.UPNP.ORG: 1
CONFIGID.UPNP.ORG: 1337


>>> r = m.Search("urn:schemas-upnp-org:service:WANIPConnection:1")
>>> print(r)
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=120
ST: urn:schemas-upnp-org:service:WANIPConnection:1
USN: uuid:8cd::urn:schemas-upnp-org:service:WANIPConnection:1
EXT:
SERVER: TP-Link/TP-LINK UPnP/1.1 MiniUPnPd/1.8
LOCATION: http://192.168.0.1:1900/rootDesc.xml
OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01
01-NLS: 1
BOOTID.UPNP.ORG: 1
CONFIGID.UPNP.ORG: 1337

見てわかる通りロケーションは"http://192.168.0.1:1900/rootDesc.xml"となります。

コントロールURLを取得する

次にポート開放の指示を出すためのcgiのURLを取得します。
先ほど取得したロケーションにアクセスしてみます。
もう見てもわかんないですね。

調べたところによると、サービスタイプが一致しているところのコントロールURLにアクセスするとのことでした。
確かにM-Searchで使ったサービス名がありますね。

というわけでこのxmlからコントロールURLを抜き出す関数がこちら。
xmlを解析するためのライブラリがあるみたいなので、それを使ってサービスタイプが一致するところのコントロールURLを抜き出しています。

最終的にコントロールURLが"192.168.0.1:1900/ctl/Conn"であることがわかります。

ここまでで情報収集は終わりです。やっとですね。
後はこの情報をキャッシュとして保存しておくなどを工夫しておけば、次回以降はこの手順はスキップできそうですね。

ポート開放指示を出す

さて、当時のポート開放スクリプトがひどすぎたので、もう完全に書き直してます。
ちょっと長いのではみ出てる部分はDLでお願いします。
上の方の数行は先ほどやってた情報収集ですね。

調べたところによると、コントロールURLに対してSOAPという形式のリクエストボディを送り付けることでポート開放を行えるみたいです。
xml形式のボディでその中に開けたいポート、プロトコル、クライアントのIPアドレス、開放時間などを入れます。
改行コードが厳密に決められているみたいなので、改行の部分はリストにして後でジョインしてます。
この中身が開放と閉鎖で若干異なるので、リストを少しずつ作ることで分岐を入れています。
リクエストヘッダーにも専用のアクションがあるみたいです。詳しくはダウンロード。

出来上がったらリクエストライブラリでPOSTすればオッケーです。
うまくいけばステータスコード200が返ってくるので、レスポンスのOK属性がTRUEになるはずです。

確認する

正常に操作できたことを確認して、直後にルーター管理画面から確認するとこうなってました。
設定画面のupnpクライアントの部分で、きちんと今回リクエストしたポート開放が行われています。
SOAPの中で書いた説明の部分がきちんと反映されているので、今回の実験のリクエストが正常に処理されていることがわかります。
ちなみに30秒に設定したので放っておいても30秒でポートが閉じます。
地味にこれ便利なんですよね。

適当に安くてアンテナがたくさんついているルーター買ったんですが、設定画面がすごいんですよねこれ…。
ちなみにルーター管理画面はメーカーによって思いっきり違うので、私に聞かれてもわかりません。それ系のコメントは受け付けませんので、予めご了承ください。

プログラムの配布

今回製作したMSearch&ポート開放ツールは以下のURLから配布しています。

https://script.google.com/macros/s/AKfycbxdcr8pnazR7RbjaSICTtaNWfN7h_rjQrKlZ3h9CZpPRFzRILk1OGc8mZqKbF-NXNO9/exec?name=ポート開放ツール

ダウンロードするには利用規約同意パスワードのほかに、有料区間で開示されるキーワードは必要です。

ここから先は

68字

情報が役に立ったと思えば、僅かでも投げ銭していただけるとありがたいです。