コンテナネットワークとvethペアの物理(仮想イーサネットケーブル)

2026-11-06 / シリーズ「コンテナとLinuxカーネルの物理」第4話 / Docker, Linux, ネットワーク, iptables, 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)のみ
ホストOSのネットワーク空間 Root NetNS 🌐 物理NIC: eth0 IP: 192.168.1.50 🧭 ホストのルーティングテーブル 宛先ごとに出口を管理している コンテナのネットワーク空間 Child NetNS 🔁 ループバック: lo 127.0.0.1(自分自身だけ) 🧭 コンテナのルーティングテーブル 空っぽ(外への出口なし) ✕ 🚧 壁(NET名前空間)によって完全に遮断!

このままでは、コンテナは外の世界と1ビットも通信できません。 この無人島とホストマシンの大陸を繋ぐために、Linuxカーネルは**「ある架空のハードウェア」**をメモリ上に創り出します。


2. 両端がつながったLANケーブル:「vethペア」の力学

現実世界で2台のPCを直接繋ぎたいとき、私たちは「LANケーブル」を両方のLANポートにカチャリと挿します。 Linuxカーネルの中で、これと寸分違わぬ働きをする仮想デバイスが**「veth(Virtual Ethernet)ペア」**です。

スイッチのポートに何本も挿さった色とりどりのLANケーブル
現実の世界でPC同士をつなぐのは、ポートにカチャリと挿すLANケーブルそのもの。vethペアは、この「両端がつながったケーブル」をメモリ上に再現した仮想デバイスである。写真:Raysonho(CC0/Wikimedia Commons)

vethは、必ず**「2個1組のペア」**として誕生します。

vethペア(仮想LANケーブル)の物理 端点 A: vethXXXX (ホストOS側に残す) 端点 B: eth0 (コンテナの中に押し込む!) = 仮想イーサネットケーブル(メモリ上の直結パイプ) = 片方にパケットを放り込むと、もう片方からそのまま飛び出す

片方にパケットを放り込むと、もう片方から飛び出す

vethペアの物理動作は極めてシンプルです。 端点A(vethXXXX)に入ったネットワークパケット(sk_buff 構造体)は、カーネルのネットワークスタックを通り抜け、そのまま寸分の遅延もなく端点B(eth0)の受信バッファへとワープして出現します。

コンテナを起動するとき、Dockerエンジン(dockerd)は裏で以下の土木工事を瞬時に実行しています。

  1. vethペアの作成:ホストOS上で vethA と vethB のペアを作る。
  2. 端点の押し込み:vethB を、コンテナのNET名前空間の中へと物理的にテレポートさせる(名前空間を跨ぐ移動)。
  3. 名前の改名:コンテナの中に入った vethB の名前を、わかりやすく「eth0」に改名し、IPアドレス(例: 172.17.0.2/16)を割り当てる。
  4. 回線開通:ホスト側の vethA とコンテナ側の eth0 を両方とも UP(有効化)にする。

これで、コンテナという無人島から、ホストの大陸へと1本のケーブルが引き込まれました。


3. 仮想のハブ:「docker0(Linux Bridge)」による集約

しかし、コンテナが10個、100個と増えたらどうなるでしょうか? ホスト側に伸びてきた何十本ものvethケーブルを、物理NIC(eth0)に直接挿すことはできません。物理ポートは1個しかないからです。

そこで登場するのが、オフィスのデスクの下に置かれているような「L2スイッチングハブ」をソフトウェアで再現した**「Linux Bridge(ブリッジ)」です。 Dockerをインストールすると、ホストOSに自動的に作られるdocker0**というインターフェースがそれです。

ラックマウントされたスイッチングハブとパッチパネルに束ねられた多数のLANケーブル
現実のスイッチングハブは、何十本ものケーブルをポートに束ねてパケットの交通整理を行う。Linux Bridge(docker0)は、このハブの役割をソフトウェアで担う仮想スイッチである。写真:Dsimic(CC BY-SA 4.0/Wikimedia Commons)
🌐 インターネット ホストOS(Root NetNS) 🌉 仮想ブリッジ: docker0 IP: 172.17.0.1(デフォルトゲートウェイ) 物理NIC: eth0 IP: 192.168.1.50 iptables NAT 物理ケーブル veth1a veth2a vethペア 1 vethペア 2 📦 コンテナ 1 eth0(veth1b) IP: 172.17.0.2 📦 コンテナ 2 eth0(veth2b) IP: 172.17.0.3

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)」**です。

📦 コンテナ送信: [Src: 172.17.0.2 ➔ Dst: 8.8.8.8] 🌉 docker0: パケットをカーネルのルーティングへ渡す ★ IPマスカレード(SNAT)発動! 送信元IPをホストの物理IPへ書き換える 書換後: [Src: 192.168.1.50:49152 ➔ Dst: 8.8.8.8] 🌐 物理NIC(192.168.1.50)からインターネット(8.8.8.8)へ送出 ↩️ 返信到着: [Src: 8.8.8.8 ➔ Dst: 192.168.1.50:49152] 📖 conntrack参照: 「これはコンテナ 172.17.0.2 宛ての返事だ!」 🔁 逆変換: [Src: 8.8.8.8 ➔ Dst: 172.17.0.2] ✅ コンテナへ返信パケットを届ける!

パケットの「差出人」をすり替える

コンテナが外に出ようとするとき、ホスト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
💻 外部クライアントがアクセス: 宛先 192.168.1.50:8080 📡 ホスト物理NICがパケットを受信 🔍 iptables PREROUTING: 「ポート8080宛か?」→ YES ★ DNAT発動! 宛先を 172.17.0.2:80 に書き換える 🌉 docker0ブリッジ → vethペアを通ってコンテナへ ✅ コンテナ内 Nginx(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行で頭に入る『試験・実務・投資の要点まとめ』

  1. 【vethペアの物理】 コンテナネットワークは、片方に入ったパケットがもう片方へワープする「vethペア」の一端をコンテナ(NET名前空間)へ押し込むことで成立する。
  2. 【docker0ブリッジ】 ホストOS上の仮想L2スイッチ docker0 が全コンテナのvethを集約し、同一ホスト内コンテナ同士の高速な相互通信とデフォルトゲートウェイを担う。
  3. 【iptablesのNAT変換】 コンテナから外部への通信は送信元IPをホスト物理IPに化かす「IPマスカレード(SNAT)」、外部からコンテナへのポート公開は宛先を書き換える「DNAT」で実現される。

確認クイズ

📝 確認クイズ(3問)