※一年半前の話なので記憶がうろ覚えです。
リポジトリはこれです
あらすじ
Minecraftのマルチプレイがしたいわけですが、みんなが使ってるHamachiってのを使うのが正攻法じゃない感じがして気分良くなかったので、ポート開放したいなーって方法を探してたら開放くんを見つけました。
でも開放くんでポート開放出来る人と出来ない人が出てきて、しかもエラーメッセージの内容がかなり薄い。「ポート開放に失敗しました。」とか「get_StaticPortMappingCollectionに失敗しました。」とか言われても何をどう直せばいいのかさっぱり分からない。
自分も初め「ポート開放に失敗しました。」を食らって、試行錯誤して「Hamachiをインストールしていたことが悪い」という結論にたどり着き、こっち側でサーバーを建てることは可能になりました。でも友達側で開こうとしてもでは未だに「get_StaticPortMappingCollectionに失敗しました。」を食らい続けてどうにも直せない。
開放くんを解説しているサイトはIPv6を無効にしたり「ルーターを再起動すれば直ります」とか言ってる場合じゃあないんですよ。
じゃあ開放くんをリバースエンジニアリングして書き直せば仕組み分かるしエラー出たら自分で手入れできるし初めて実践的なソフトウェア書ける良い機会になるじゃん!ということで
リバースエンジニアリング
- 「初めてのマルウェア解析」に紹介されているIDA使って頑張るかー(^-^)/
- うーんsub_401100がポート開放する処理でsub_401440がポートを閉じる方の処理かー
- 401100、401440内部:
call eax
←????? 処理追っても分からん… - IDA断念
- Ghidra:C言語デコンパイル助かる!
-
call eax
→(**(code **)(*local_318 + 0x1c))(local_318,&stack0xfffffce4,uVar2)
- ↑構造体のメンバの関数ポインタだったのか
- COM学習するか…
COM
- MSDNの構造どうなってんの 実践的なコードどこ
- https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/example--the-open-dialog-box 見つけた…
- 開放くんのCoCreateInstanceのCLSID何…?
- UPnPNAT←やりたいことそのまんまの名前だし完全にこれじゃん
- get_StaticPortMappingCollectionあった
windows-rs
解析と並行してwindows-rsをどうやって使えばいいのか試行錯誤してました。
作り始めた時のwindows-rsのバージョンが0.36.1なのに対しMicrosoft公式のガイドがバージョン0.9.0のチュートリアルをしていた(今は0.43.0)ので使い方が全く異なっていて困惑しました。
ドキュメントに頼ろうとしてもドキュメンテーションコメントはほぼ何も無くただのAPI列挙です。
それもそのはずで、windows-rsはwin32metadataからコードを自動生成しているだけです。もしドキュメントコメントがすべてに付いていたら労力とサイズが桁違いになるのでさすがにない。
最近(0.52.0)riddleというメタデータからRustコードを生成するツール自体が実行バイナリとしてリリースされたみたいですね。あまり調べていませんが。
今は日本語記事がQiitaにもZennにも色々あるのでありがたいです。
0.51.0までリクエストが無いとバージョン上げないらしい記述があったので、Issue出す勇気はないけど最新のを使いたい場合はDependenciesにgit指定する感じなのかなーとか思いましたが、0.52.0ではそのような記述がありません。どっちなんだ
それでもwindows-rsにはめちゃくちゃ助けられましたし、とても使いやすいと感じています。Kenny Kerrさん、windows-rsのコントリビューターの皆さん、win32metadataの皆さん本当にありがとうございます。
作成
Ghidraで出てきたコードをRustに書き写すだけなので、windows-rsの使い方に慣れればそれほど困難が出てくることはないです。
コードを書いている間に特に感じたことを列挙していきます
-
型が引数や
match
の他のマッチアームに合わない時がある
なんかたまーに型が引数とかmatch
の他のやつとかに合わない時があるため、.0
で取り外したり逆に型で包んだりしなきゃいけなくてどうしたものかねぇ…となったことが何度かありました。
MESSAGEBOX_RESULT
、HWND
、LPARAM
とかでそういうのが起きています。型で厳しく(といっても緩い)するために多少は仕方がないことかなと思います。無理やり型を合わせるために
as
を使ったときは結構不安になります。
例えば以下main.rs - dlg_proc抜粋・簡略unsafe extern "system" fn dlg_proc( window_handle: HWND, message: u32, wparam: WPARAM, _: LPARAM, ) -> isize { match message { WM_COMMAND => match wparam.0 & 0xffff { x if MESSAGEBOX_RESULT(x as i32) == IDCANCEL => { let _ = EndDialog(window_handle, 2); 0 } _ => 0, }, _ => 0, } }
WPARAMの下位ワードがIDCANCELと一致していることを確認したいわけですが、WPARAMはusizeを包んでいて、下位ワードを出すためにはまず
.0
で取り出さなきゃいません。さらにMESSAGEBOX_RESULTはi32を包んでいるため、asで型変換までしてようやく比較できます。マッチパターンで式は使えないのでscrutinee(match
キーワードの後の式)上で変換するかマッチガードで変換するしかありません。もしくはIDCANCELを使わず直にリテラル2
でマッチするしかありません。 -
大体unsafe
このクレートは関数の使用がWin32ならunsafeであり(この関数の中のquote!
部分)、利用するためにunsafeブロックを多用するか全体まるっとunsafeで囲んじゃうかしちゃいます。あとFFIなので関数の引数や返り値はポインタだったりするため、勢い余って何も考えずポインタ外しとかしちゃいます。
このコードを書き始めた頃はnull可能なポインタが引数となっているものはnull()
とかnull_mut()
とかでnullポインタを渡さなきゃいけませんでしたが、この記事を書いている頃にはその大体がOptionで渡すように改善されています。ありがとう -
COMの手厚いサポート
前述のC++によるコード例では構造体メンバの関数ポインタを利用する形だったので、これをRustで書くならメソッド記法がいいな…と思っていたら実際にそうなっていて感動しました。それだけでなくHRESULTとインターフェースがResult<T>
でセットになって返ってきます。しかも型を指定する形で欲しいインターフェースを指定できるので、IIDが省略され引数が5つ必要なCoCreateInstance
関数もRustでは3つで済みます:port_mapping.rs - 75-95行抜粋・簡略let upnp_nat = CoCreateInstance::<_, IUPnPNAT>( &UPnPNAT, None, CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER, )?; let static_port_mapping_collection = upnp_nat.StaticPortMappingCollection()?;
ちなみに↑のCOMに指定するコンテキストはどうすればいいのか全く分かっていません…リバースエンジニアリング時に出てきた
23
をそのまま流用しているだけです… -
PCWSTRの正しい生成方法が分からない
結構様々なところでPCWSTR型が必要になるのですが、生成してくれる関数はPCWSTR::from_raw
だけ、しかも内容はPCWSTR(pointer)
とやってることは同じです。w!
マクロもありますが、これが受け付けるのは文字列リテラルのみです。式を渡したい。自分が知っている方法はlet pcwstr = PCWSTR::from_raw("abc\0".encode_utf16().collect::<Vec<u16>>().as_ptr()); let pcwstr = PCWSTR::from_raw(HSTRING::from("abc").as_ptr());
の2通りのみです。他にいい方法があったら教えてください…
オリジナル開放くんからの改良点
-
Hamachiが原因で失敗しないように
Hamachiが原因でポート開放に失敗するのはgethostbyname
が返すアドレスのリストにおいてHamachiの仮想アダプタのアドレスが優先されるからです。Hamachiのアドレスは25.0.0.0/8の範囲なので、IPアドレスの初めの2文字が"25"の場合はスキップすることで対応しています。 -
エラーメッセージの改良
エラーコードとエラーコードによるエラーメッセージ、失敗した関数についての公式ドキュメントへのリンクを追加しました。それが解決の糸口になることはかなり少ないとは思いますが… -
成功時外部IPアドレスを表示するように
これで逐一自分のグローバルIPアドレスが何なのか確認しにWebサイトにアクセスする必要が無くなりました。
IStaticPortMappingCollection::Add
の成功時の返り値IStaticPortMapping
に外部IPアドレスを出してくれるExternalIPAddress
メソッドがあったので利用しただけです。
「get_StaticPortMappingCollectionに失敗しました」の原因究明はできたのか
出来ていません!!!!!!!!!!!!!!
まずエラーメッセージを改良したところで「get_StaticPortMappingCollectionに失敗しました」のエラーが出る時のエラーコードは十中八九0x00000000
(S_OK)です。エラーコードの意味がない(ちなみにこのエラーが来るのはStaticPortMappingCollection
のHRESULTに対してis_ok()
でtrueが返ってきた上でインターフェース(今回はIStaticPortMappingCollection
)へのポインタがnullのままの場合に起こる)。
とりあえずルーターのUPnPが無効になっている場合はこのエラーが返ってくることだけ言えます。
ファイアウォールの設定に関してはいじっても基本はエラーが出ず、一度エラーが出たらファイアウォールの設定を元に戻してもエラーが出続け、ネットワークアダプターをリセットしたら直ったり…とごちゃごちゃしているので本当によく分かりません。ごめん
とはいえ
開放くんのやることはUPnPを使ってポート転送をすることであり、そのポート転送が必要になるのはグローバルIPアドレスとプライベートIPアドレスの概念が存在するIPv4のみです。
今のMinecraftサーバーはIPv6のソケットもバインドするようになったので、サーバー側とクライアント側双方がIPv6アドレスを持っていればサーバー側のIPv6アドレスを指定することで開放くん無しで直で接続できます。もっとIPv6普及してくれ
おまけ
ついでに現在登録されているポート転送リストを確認するやつも作りました。