一部のポートが潰されちゃう件と正しい回避方法

前回の記事で紹介した一部のポートが利用できなくなる件、 その理由と正しい対策が判明したので紹介します。

本記事は前回の DockerNATが一部のポートを潰しちゃう話 を読んでることを前提に書かれています。 まだの方は先にそちらを読んでからこちらをどうぞ。

TL;DR

  • Windows には「ポート除外範囲」(excludedportrange)という指定した範囲のポートの利用を禁ずる機能がある
  • 実は「ポート除外範囲」には2種類ある
    • エフェメラルポートの動的割当から除外する
    • 特定プログラムが予約し、そのプログラム以外からの利用を禁ずる
  • 2種類とも同じコマンドで確認できるのでややこしい
  • Hyper-Vをインストールする前に「ポート除外範囲」を追加するのが良い

    コマンド例:

    > netsh int ipv4 add excludedportrange protocol=tcp startport=2222 numberofports=1
    

経緯

前回の記事 を公開したところ、 ある方から以下のような情報をいただきました。

まず excludedportrange を手がかりに調べたところ TL;DR に書いた 「ポート除外範囲」なるものに行き着きました。 実験してみると DockerNAT のオン・オフに伴い、 問題にしていた 2222 番を含む範囲がポート除外範囲に追加されたり削除されたのです。 確認のためのコマンドは次の通りです。

netsh int ipv4 show excludedportrange protocol=tcp

excludedportrange という名前から エフェメラルポートの動的割当から除外するものだろう という予想は容易だったのですが、 それが LISTEN すらも妨げるのはどういうことだろうと それでもまだ疑問は残ります。

そこで次に PersistentTcpPortReservation というものを 主にAPI方面から調べ、判明したことは以下の通りです。

  1. CreatePersistentTcpPortReservation でポートの範囲を指定して予約すると、トークン (64ビットの非負整数値) が得られる
  2. この範囲のポートを普通に使おうとするとエラーになる
  3. ソケットに対して WSAIoctl() を用いて 1 で得たトークンを与えると使えるようになる
  4. 指定した範囲は netsh int ipv4 show excludedportrange で表示される

これは PersistentTcpPortReservation の単純な和訳である 「永続化TCPポート予約」に違わない動作であり、 エフェメラルポートから除外されるのも一定の理解ができます。 しかしそれがどちらも同じコマンドで確認できるのはやや面食らいました。 twitter である方から同じ意見をいただいてます。

なお netsh int ipv4 show excludedportrange protocol=tcp の出力上、この2つは微妙に差別化されています。

> netsh int ipv4 show excludedportrange protocol=tcp

プロトコル tcp ポート除外範囲

開始ポート    終了ポート
----------    --------
        80          80
      1628        1727
      1802        1901
      1902        2001
      2222        2222     *
      2223        2322
      4688        4787
      5357        5357
     50000       50059     *

* - 管理されている除外ポート。

最後に * - 管理されている除外ポート。 とある通り * が付いている方が「ポート除外範囲」で それ以外は「永続化TCPポート予約」で確保されたものです。

完璧な理解まで、最後の一歩

こうして DockerNAT のオン・オフにより Docker Desktop for Windows と VirtualBox の排他的な共存(?) を自宅PCにて実現し、 さらにその背景にある動作原理を理解したわけですが 後日十分ではなかったことが判明します。 というのはオフィスのPCで同様の環境を構築しようとしたところ DockerNAT をオフにしても 2222 番を含む範囲が解放されず VirtualBox から利用できないという事態に陥りました。

そこで思い出されたのが先程のツイートに追いツイートされてたこの情報です。

Docker Desktop for Windows だけではなく Hyper-V も「永続化TCPポート予約」してたんですね。 しかも予約する範囲はそれぞれ固定ではない、と推測されます。

つまり自宅PCとオフィスPCではHyper-VとDockerをインストールする際に 微妙に手順が違ったと推測されます。 おそらく自宅環境は以下の手順を踏んだと推測されます。

  1. VirtualBoxのVMを起動する
  2. Hyper-Vを有効化する
  3. 再起動する
  4. Docker Desktop for Windowsをインストールする

ステップ2でHyper-VはVirtualBoxのVMが利用している分を避けて 永続化TCPポート予約しました。 そのためVirtualBox VMが使ってる2222を含むポートは影響を受けずに済みます。 つぎにステップ4でDocker Desktop for Windows=DockerNAT がポートを予約する際はステップ3で再起動を挟んでおり、 かつHyper-Vが有効であるためVMが起動できず 2222を含んだ範囲が予約されてしまいました。

これによりDockerNATだけをオフにすれば2222が利用できる環境が 偶然できあがってしまったのです。

一方でオフィス環境においては以下の通り VMを起動せずにインストールしたため、 2222を含むVMで必要なポートをHyper-Vが予約した状態になっていました。

  1. Hyper-Vを有効化する
  2. 再起動する
  3. Docker Desktop for Windowsをインストールする

ここまでわかれば正しいHyper-V及び Docker Desktop for Windowsのインストール方法が 以下の通りであるとわかります。

  1. VM含め自分が使う可能性のあるポートを予め「ポート除外範囲」に追加する
  2. Hyper-Vを有効化する
  3. 再起動する
  4. Docker Desktop for Windowsをインストールする

ポート除外範囲に追加する方法は 「管理者として実行」したコンソールで以下のコマンドを実行します。

netsh int ipv4 add excludedportrange protocol=tcp startport=2222 numberofports=1

startportnumberofports の値は自身の用途に合わせて調整してください。

以上、これでHyper-VとVirtualBoxを 自信を持って切り替えながら利用できる環境が作れるようになりました。 機会があればぜひお試しを。