コンテナネットワークとvethペアの物理(仮想イーサネットケーブル)
Dockerコンテナの中で ip addr や ifconfig を叩くと、そこには見慣れないネットワーク機器の姿があります。
ホストマシンの物理NIC(eth0 や enp3s0)は影も形も見当たらず、代わりに鎮座しているのは**「eth0@if...」という奇妙なインターフェースと、「172.17.0.2」といった独立したプライベートIPアドレス**です。
さらに、コンテナを起動するときに -p 8080:80 と指定すれば、外部のブラウザからホストの8080番ポートを叩いた瞬間、コンテナ内のWebサーバー(80番ポート)へとパケットが吸い込まれていきます。
しかし、物理的なLANケーブルはホストマシンに1本しか挿さっていません。 第1話で明かした通り、コンテナとは独立した仮想マシンではなく、ただのプロセスです。
「物理的な差込口が1つしかないのに、なぜコンテナは自分専用のIPアドレスを持てるのか?」
「コンテナから放たれたパケットは、どうやってホストの物理NICをすり抜けてインターネットへと旅立つのか?」
「-p 8080:80 の裏で、Linuxカーネルは何を書き換えているのか?」
この通信手品を成立させている正体は、Linuxカーネルがメモリ上に敷設する仮想のLANケーブル「vethペア」、仮想のL2スイッチ「Linux Bridge」、そしてパケットの宛先を強引に改ざんする**「iptables(Netfilter)」の物理**です。
1. 隔離された島:ネットワーク名前空間(Network Namespace)の孤立
第1話で学んだ通り、コンテナは「NET名前空間(CLONE_NEWNET)」によって、ホストマシンのネットワークから完全に隔離されています。
NET名前空間を切り離された直後のコンテナは、いわば**「LANケーブルが1本も繋がっていない、無人島のPC」**です。
- 独自のルーティングテーブル(空っぽ)
- 独自のiptablesルール(空っぽ)
- 存在するインターフェースは、自分自身を指すループバック(
lo:127.0.0.1)のみ
このままでは、コンテナは外の世界と1ビットも通信できません。 この無人島とホストマシンの大陸を繋ぐために、Linuxカーネルは**「ある架空のハードウェア」**をメモリ上に創り出します。
2. 両端がつながったLANケーブル:「vethペア」の力学
現実世界で2台のPCを直接繋ぎたいとき、私たちは「LANケーブル」を両方のLANポートにカチャリと挿します。 Linuxカーネルの中で、これと寸分違わぬ働きをする仮想デバイスが**「veth(Virtual Ethernet)ペア」**です。
vethは、必ず**「2個1組のペア」**として誕生します。
片方にパケットを放り込むと、もう片方から飛び出す
vethペアの物理動作は極めてシンプルです。
端点A(vethXXXX)に入ったネットワークパケット(sk_buff 構造体)は、カーネルのネットワークスタックを通り抜け、そのまま寸分の遅延もなく端点B(eth0)の受信バッファへとワープして出現します。
コンテナを起動するとき、Dockerエンジン(dockerd)は裏で以下の土木工事を瞬時に実行しています。
- vethペアの作成:ホストOS上で
vethAとvethBのペアを作る。 - 端点の押し込み:
vethBを、コンテナのNET名前空間の中へと物理的にテレポートさせる(名前空間を跨ぐ移動)。 - 名前の改名:コンテナの中に入った
vethBの名前を、わかりやすく「eth0」に改名し、IPアドレス(例:172.17.0.2/16)を割り当てる。 - 回線開通:ホスト側の
vethAとコンテナ側のeth0を両方ともUP(有効化)にする。
これで、コンテナという無人島から、ホストの大陸へと1本のケーブルが引き込まれました。
3. 仮想のハブ:「docker0(Linux Bridge)」による集約
しかし、コンテナが10個、100個と増えたらどうなるでしょうか?
ホスト側に伸びてきた何十本ものvethケーブルを、物理NIC(eth0)に直接挿すことはできません。物理ポートは1個しかないからです。
そこで登場するのが、オフィスのデスクの下に置かれているような「L2スイッチングハブ」をソフトウェアで再現した**「Linux Bridge(ブリッジ)」です。
Dockerをインストールすると、ホストOSに自動的に作られるdocker0**というインターフェースがそれです。
MACアドレス学習とパケットの交通整理
docker0 は、通常の物理スイッチと全く同じ振る舞いをします。
各コンテナから伸びてきたvethの端点を、自分のポートに束ねて収容します。
- コンテナ1からコンテナ2への通信:
コンテナ1(172.17.0.2)がコンテナ2(172.17.0.3)にパケットを送ると、パケットはvethを通って
docker0に届きます。docker0はMACアドレステーブルを参照し、コンテナ2が繋がっているveth2aへとパケットをスイッチング(転送)します。ホストの外部に出ることなく、メモリ内のスイッチングだけで通信が完結します。
そして、すべてのコンテナのルーティングテーブルには、以下の「デフォルトゲートウェイ」が自動設定されます。
default via 172.17.0.1 dev eth0
「知らない宛先(インターネット等)への通信は、すべて docker0(172.17.0.1)へ投げろ」。
これで、コンテナ内のパケットがホストOSの玄関口まで届くようになりました。
4. 外の世界へ出る物理:iptablesによる「IPマスカレード(NAT)」
しかし、ここで重大なネットワークの壁に衝突します。
コンテナのIPアドレス(172.17.0.2)は、Dockerが勝手に作った**「プライベートIPアドレス」**です。
もし、このパケットがそのままの送信元IPでインターネットに飛び出したらどうなるでしょうか?
Googleのサーバー(8.8.8.8)に届くことすらできませんし、仮に届いたとしても、Googleは「172.17.0.2なんて知らない(私設アドレス)」として、返信パケットを破棄してしまいます。
これを解決するのが、Linuxカーネルのパケットフィルタリング機構(Netfilter)を操る**「iptables」の「IPマスカレード(NAPT / 送信元NAT)」**です。
パケットの「差出人」をすり替える
コンテナが外に出ようとするとき、ホストOSのカーネルは、パケットのIPヘッダーを物理NICから出る直前に書き換えます(POSTROUTING チェイン)。
- 書き換え前:送信元
172.17.0.2:5000 - 書き換え後:送信元
192.168.1.50:49152(ホストの物理IPと空きポート)
ホストマシンが、あたかも「自分がインターネットにアクセスした」かのように差出人を偽装して代理送信するのです。
返信パケットが返ってきたときは、カーネル内の通信記録簿(conntrack:コネクショントラッキング)を参照し、宛先を再び 172.17.0.2 へと書き戻してコンテナへ渡します。
家庭用のWi-Fiルーターが、何台ものスマホを1本の光回線でネットに繋いでいるのと全く同じ物理現象が、Linuxカーネルの中でソフトウェアとして実行されています。
触って試す:パケットの旅ステップ実行
🧳 パケットの旅(コンテナ → インターネット)
「次へ」ボタンを押すたびに、パケットが1区間ずつ進みます。途中のカーネルで送信元IPが書き換わる瞬間に注目してください。
「次へ」を押して旅を始めましょう。
5. 外から中へ入れる物理:ポートフォワーディング(DNAT)
では、逆方向はどうでしょうか?
Webサーバーをコンテナで動かしているとき、私たちは -p 8080:80 と指定して外部からのアクセスを受け付けます。
このとき、裏側では**「DNAT(Destination NAT:宛先NAT)」**というルールがiptablesに刻み込まれています。
# Dockerが自動でiptablesのnatテーブル(PREROUTING)に追加するルールの正体
-A PREROUTING -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
外部からホストの8080番ポートへ届いたTCP SYNパケットは、ルーティングの判定を受ける前(PREROUTING)にiptablesによって首根っこを掴まれます。
そして、パケットの宛先IPとポート番号が、物理NICのアドレスから**「コンテナの内部IP(172.17.0.2:80)」へと無理やり書き換え**られます。
宛先を書き換えられたパケットは、カーネルの通常のルーティング処理に従って docker0 ブリッジへと流し込まれ、vethペアを通ってコンテナ内のNginxへと吸い込まれていくのです。
6. Linux標準コマンドだけで「コンテナネットワーク」を自作する
この一連のネットワーク物理は、Dockerを完全に停止させた状態でも、Linuxの標準コマンド ip と iptables だけでゼロから手作りできます。
実験:2つのコマンドで独立したネットワーク島を作る
Linuxターミナル(root権限)で、以下の手順を実行してみてください。
# 1. 新しいネットワーク名前空間「net-test」を作成
ip netns add net-test
# 2. vethペア「veth-host」と「veth-guest」を作成
ip link add veth-host type veth peer name veth-guest
# 3. 片方の端点「veth-guest」を名前空間「net-test」の中にテレポート!
ip link set veth-guest netns net-test
# 4. ホスト側端点にIPを設定してUP
ip addr add 10.0.0.1/24 dev veth-host
ip link set veth-host up
# 5. 名前空間(コンテナ側)の端点にIPを設定してUP
ip netns exec net-test ip addr add 10.0.0.2/24 dev veth-guest
ip netns exec net-test ip link set veth-guest up
ip netns exec net-test ip link set lo up
準備は整いました。
ホストOSから、隔絶された名前空間の中へ向けて ping を撃ってみます。
# ホストからコンテナ側IPへping
ping -c 2 10.0.0.2
# ➔ 通る!
今度は、隔離された名前空間の中から、ホスト側へ向けて ping を撃ち返してみます。
# 名前空間の内部からホスト側IPへping
ip netns exec net-test ping -c 2 10.0.0.1
# ➔ 通る!
さらに、名前空間の中で ip addr を叩いてみます。
ip netns exec net-test ip addr
画面には、ホストマシンの物理NICは一切表示されず、lo と veth-guest(10.0.0.2)だけが存在する、完璧に隔離されたネットワークの島が出現しています。
Dockerがやっていることの99%は、この ip コマンドと iptables の設定を、コンテナの起動・停止に合わせて自動で美しく後始末(スクラップ&ビルド)しているだけなのです。
結論:コンテナネットワークとは「カーネル内のパケット配管工学」である
クラウドの世界で「コンテナのSDN(Software Defined Network)」や「KubernetesのCNI(Container Network Interface)」と呼ばれる巨大な技術体系。 その基礎にある物理は、驚くほど古典的で堅牢なLinuxの道具の組み合わせでした。
- NET Namespaces でプロセスを無人島に隔離する。
- vethペア という両端がつながった土管で島と大陸を繋ぐ。
- Linux Bridge(docker0) で何十本もの土管を束ねる。
- iptables(NAT) でパケットの差出人と宛先を書き換えて外の世界と橋渡しする。
物理的なケーブルやスイッチを1台も買い足すことなく、Linuxカーネルのメモリ空間の中に巨大なデータセンターのネットワーク網を丸ごと構築してしまう。 このパケット配管の柔軟性こそが、コンテナが物理サーバーの束縛を完全に脱ぎ去った最大の理由なのです。
しかし、ネットワークが繋がり、ファイルが見え、リソースが制限されたとしても、「セキュリティ」における最後の難問が残っています。
コンテナの中で root(管理者権限)になったプロセスは、なぜホストマシンのカーネルを破壊できないのか?
次回第5話、特権を分子レベルで解体する**「SeccompとLinux Capabilities(root権限の無力化工学)」**を暴きます。
📝 3行で頭に入る『試験・実務・投資の要点まとめ』
- 【vethペアの物理】 コンテナネットワークは、片方に入ったパケットがもう片方へワープする「vethペア」の一端をコンテナ(NET名前空間)へ押し込むことで成立する。
- 【docker0ブリッジ】 ホストOS上の仮想L2スイッチ
docker0が全コンテナのvethを集約し、同一ホスト内コンテナ同士の高速な相互通信とデフォルトゲートウェイを担う。 - 【iptablesのNAT変換】 コンテナから外部への通信は送信元IPをホスト物理IPに化かす「IPマスカレード(SNAT)」、外部からコンテナへのポート公開は宛先を書き換える「DNAT」で実現される。