Windowsユーザーのための WSL2で始める Linux環境構築術 22

外部から「WSL1/2」のサーバーにネットワークアクセスをしてみよう

第22回目の今回は、WSL1/2それぞれの環境において、外部からサーバーへネットワークアクセスする方法を紹介します。

水野 源

12月2日 6:30

はじめに

本連載を読んでいる方であれば、WSLはWindows上における開発環境として使っているケースが多いのではないでしょうか。Webアプリであれば、コードを書いてテストサーバーを起動したら、別のPCのブラウザからアクセスしてテストしたいこともあるでしょう。

今回は、WSL1/2それぞれの環境において、外部からサーバーへネットワークアクセスする方法を紹介します。

WSL1/2における
ネットワークアーキテクチャの違い

以前にも解説した通り、WSL2はHyper-Vの軽量仮想マシン上で本物のLinuxカーネルを動かしています。これにより、ほぼ完全なシステムコール互換性を実現しているわけです。ところが実体が仮想マシンであるため、ホストのWindowsとは「別マシン」になってしまうという問題があります。

これはネットワークも例外ではなく、WSL2はHyper-Vの仮想スイッチによる独立した仮想ネットワークを持っており、外部ネットワークにはNATを経由して出て行きます。家庭内LANに所属するPCがルーターを経由してインターネットに出ていくのと、構成的には同じです。そのためインターネットから家庭内のPCに直接アクセスできないのと同様に、LAN内の別のPCからWSL2内で立ち上げたテスト環境にも直接アクセスはできません。

対してWSL1はシステムコールの変換レイヤーであるため、LinuxのプロセスをWindows上で直接動作させています。ネットワークもWindowsのTCP/IPスタックを直接利用しており、WindowsとIPアドレスも共有しているのです。そのためWSL1を使えば、Windows Defenderファイアウォールの設定のみで、外部からWSL環境へのアクセスを実現できます。

WSL1環境の用意

まずは、シンプルなWSL1を使った方法を紹介しましょう。第21回を参考に、WSL1のUbuntu 24.04環境を用意してください。新規にインストールする場合は、PowerShellで以下のコマンドを実行します。

$ wsl --install -d Ubuntu-24.04 --version 1

既存のWSL2環境をWSL1に変換するには、以下のコマンドを実行します。

$ wsl --set-version ディストリビューション名(例:Ubuntu-24.04) 1

Streamlitのインストール

今回は、Webアプリ開発の例としてPythonベースのフレームワークStreamlitを使って解説します。WSL1のUbuntu 24.04に、以下のコマンドでStreamlitをインストールしてください。なお、ここでは後述のスクリプトで利用する「psutil」と「plotly」というライブラリも同時にインストールしています。

$ sudo apt install -U -y python3-venv
$ mkdir ~/work && cd ~/work
$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install streamlit psutil plotly

StreamlitでWebアプリを立ち上げる

WSLの「~/work」ディレクトリ内に、以下のPythonスクリプトを「dashboard.py」という名前で作成してください。

import streamlit as st
import psutil
import pandas as pd
import plotly.graph_objects as go
from datetime import datetime
import time

st.set_page_config(page_title="システムモニター", layout="wide")

st.title("WSLシステムモニタリング")
st.markdown("---")

auto_refresh = st.sidebar.checkbox("自動更新", value=True)
refresh_interval = st.sidebar.slider("更新間隔(秒)", 1, 10, 2)

col1, col2, col3, col4 = st.columns(4)

cpu_percent = psutil.cpu_percent(interval=1)
col1.metric(
    label="CPU使用率",
    value=f"{cpu_percent}%",
    delta=f"{cpu_percent - 50}%" if cpu_percent > 50 else None
)

memory = psutil.virtual_memory()
col2.metric(
    label="メモリ使用率",
    value=f"{memory.percent}%",
    delta=f"{memory.used / (1024**3):.1f}GB 使用中"
)

disk = psutil.disk_usage('/')
col3.metric(
    label="ディスク使用率",
    value=f"{disk.percent}%",
    delta=f"{disk.used / (1024**3):.1f}GB 使用中"
)

col4.metric(label="プロセス数", value=len(psutil.pids()))

st.markdown("---")

col_left, col_right = st.columns(2)

with col_left:
    st.subheader("CPUコア別使用率")
    cpu_per_core = psutil.cpu_percent(percpu=True)

    fig_cpu = go.Figure(data=[
        go.Bar(
            x=[f"Core {i}" for i in range(len(cpu_per_core))],
            y=cpu_per_core,
            marker_color='lightblue'
        )
    ])
    fig_cpu.update_layout(
        yaxis_title="使用率 (%)",
        height=300,
        showlegend=False
    )
    st.plotly_chart(fig_cpu, use_container_width=True)

with col_right:
    st.subheader("メモリ使用状況")

    fig_mem = go.Figure(data=[
        go.Pie(
            labels=['使用中', '利用可能'],
            values=[memory.used, memory.available],
            hole=0.4,
            marker_colors=['#ff6b6b', '#51cf66']
        )
    ])
    fig_mem.update_layout(height=300)
    st.plotly_chart(fig_mem, use_container_width=True)

st.subheader("ネットワーク統計")
net_io = psutil.net_io_counters()

net_col1, net_col2, net_col3, net_col4 = st.columns(4)
net_col1.metric("送信", f"{net_io.bytes_sent / (1024**2):.2f} MB")
net_col2.metric("受信", f"{net_io.bytes_recv / (1024**2):.2f} MB")
net_col3.metric("送信パケット", f"{net_io.packets_sent:,}")
net_col4.metric("受信パケット", f"{net_io.packets_recv:,}")

st.subheader("CPU使用率上位プロセス")
processes = []
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']):
    try:
        processes.append(proc.info)
    except (psutil.NoSuchProcess, psutil.AccessDenied):
        pass

df = pd.DataFrame(processes)
df = df.sort_values('cpu_percent', ascending=False).head(10)
df['memory_percent'] = df['memory_percent'].round(2)
df['cpu_percent'] = df['cpu_percent'].round(2)

st.dataframe(
    df,
    column_config={
        "pid": "PID",
        "name": "プロセス名",
        "cpu_percent": "CPU (%)",
        "memory_percent": "メモリ (%)"
    },
    hide_index=True,
    use_container_width=True
)

if auto_refresh:
    time.sleep(refresh_interval)
    st.rerun()

以下のコマンドでアプリを起動します。

$ streamlit run dashboard.py

初回起動時、アプリへのネットワークアクセスを許可して良いかを尋ねてきます。ここで「許可」をクリックすると、Pythonが待ち受けているポートに対してアクセスを許可するルールがWindows Defenderファイアウォールに追加され、外部からのアクセスが可能になります。

Pythonへのネットワークアクセスを許可する

起動が完了すると、ターミナル内にアプリへのアクセスに使用するIPアドレスとポートが表示されます。2番目に表示されているのがWindowsのプライベートIPアドレスです。Webブラウザから、このアドレスとポートへアクセスしてみましょう。

ここでは192.168.1.99がWindowsのIPアドレスになる。ここへ別PCのブラウザからアクセスする

macOSのSafariからアクセスした例。Streamlitで作成したダッシュボードが表示された

初回起動時に
アクセスを許可しなかった場合の対処

初回起動時のダイアログで「キャンセル」を選択すると、Windows Defenderファイアウォールに明示的にアクセスをブロックするルールが追加されてしまいます。うっかりこの状態になってしまったら、一度ルールを削除しない限りアクセスを許可できなくなります。そのような場合は、次のように操作してください。

「コントロールパネル」→「システムとセキュリティ」→「Windows Defenderファイアウォール」を開き、左ペインにある「詳細設定」をクリックします。

Windows Defenderファイアウォールのルールを確認する

「受信の規則」をクリックすると、ファイアウォールのルール一覧が表示されます。その中に「Python」をブロックするルールが2つ登録されているので、これを削除してください。

python3.12がブロックされている状態

この状態でもう一度Streamlitを起動すれば、再度許可を求めるダイアログが表示されます。

WSL2に外部からアクセスする方法

ポート転送でアクセスする

ここまで説明したように、WSL1はWindowsとネットワークを共有しているため、Windows側でファイアウォールにルールを追加するだけでWSL上のプロセスにアクセスできました。しかし、WSL2のネットワークはホストのWindowsとは異なる仮想ネットワークです。そのため、WSL2で同様に外部からのアクセスを可能にするには別途ポート転送の設定が必要となります。

PowerShellを管理者権限で起動したら以下のコマンドを実行し、WSL2に割り当てられているIPアドレスを調べてください。この例では172.22.112.136が該当のアドレスになります。

$ wsl -d (WSL2のディストリビューション名) hostname -I
172.22.112.136

続いて、PowerShellで以下のコマンドを実行してください(connectaddressに指定するIPアドレスは、上記のコマンドで調べたものに読み替えてください)。これで、Windowsのポート8501番へのアクセスがWSL2に転送されるようになります。

$ netsh interface portproxy add v4tov4 listenport=8501 listenaddress=0.0.0.0 connectport=8501 connectaddress=172.22.112.136

ただし、この方法には問題もあります。WSL2のIPアドレスは再起動などで動的に変化するという点です。netshによるポート転送は、ターゲットのIPアドレスが変化すると機能しなくなります。そのため、都度ポート転送の設定をやり直す必要があるのです。恒久的に利用するには向きません。

ミラーモードでアクセスする

これを解決する手段が、ホストであるWindowsのNICをWSL2にミラーリングする「ミラーモード」です。この機能を有効にするとWSL2とWindowsで同じIPアドレスが使えます。もし前述のポート転送を設定済みの場合は管理者権限のPowerShellで以下のコマンドを実行し、ルールをすべて削除しておいてください。

$ netsh interface portproxy reset

ミラーモードを有効にするには、スタートメニューから「WSL Settings」を起動してください。「ネットワーク」→「ネットワークモード」を既定の「Nat」から「Mirrored」に変更します。

ネットワークモードをMirroredに変更する

設定が完了したら、以下のコマンドで一度WSL2をシャットダウンしてから再起動します。

$ wsl --shutdown

もう一度WSL2のIPアドレスを確認してみると、ホストのWindowsと同一のIPアドレスが割り当てられていることが分かります。また、IPv6も有効になっています。

$ wsl -d Ubuntu-2 hostname -I
192.168.1.99 2405:6583:d820:800:4c6:55f4:f2e:4575 2405:6583:d820:800:55e3:e5c9:91b5:384b

ミラーモードは、WSL2のネットワークがWSL1のようにWindowsと統合されます。そのため、サービスに外部からアクセスさせるには別途Windows Defenderファイアウォールの設定が必要となる点に注意してください。Streamlitにアクセスさせるには管理者権限のPowerShellで以下のコマンドを実行し、8501番ポートへのアクセスを許可する必要があります。

$ New-NetFirewallRule -DisplayName "Streamlit" -Direction Inbound -LocalPort 8501 -Protocol TCP -Action Allow

以後は、WSL2でもWindowsのIPアドレスでStreamlitにアクセスできるようになります。それだけならWSL1と同じに過ぎませんが、WSL2はsystemdが使えます。つまり、複数のデーモンを動かしっぱなしにするような用途ではWSL2+ミラーモードの方が便利でしょう。例えば、WSL2内でSSHサーバーを起動し、以下のコマンドで22番ポートへのアクセスを許可すれば別のPCからSSH経由でWSLを操作できます。

$ New-NetFirewallRule -DisplayName "SSH" -Direction Inbound -LocalPort 22 -Protocol TCP -Action Allow

おわりに

WSL1でも2でも、外部からアクセス可能なサービスを立てられることが理解できたのではないでしょうか。ただし、WSL2はミラーモードを使わない場合は少々面倒です。また、ミラーモードはすべてのWSL2ディストリビューションに設定が反映されてしまうという問題もあります。

最初からWindowsとネットワークが統合されているWSL1と、systemdで複数サービスを動かしやすいWSL2。ネットワークの用途によっても、WSLのバージョンの使い分けを検討してみてください。

この記事のキーワード

この記事をシェアしてください

人気記事トップ10

人気記事ランキングをもっと見る