Linux環境のDockerでMacvlanとIPVlanを設定:LAN IPを直接割り当ててNATをバイパスする

Network tutorial - IT technology blog
Network tutorial - IT technology blog

実際の背景:Docker Bridgeがボトルネックになるとき

半年以上前、社内サーバー環境(従業員約50名、データセンター12ノード)で奇妙なパケットロスが発生し始めました。通常、APIやWebアプリを運用する程度であれば、iptablesのNAT(マスカレード)を用いたデフォルトのDocker bridgeで非常にスムーズに動作します。-p 8080:80と指定するだけで完了するからです。

しかし、Asteriskによる音声通話システム(SIP/RTP)、30台のRTSPカメラ、そしてインフラ監視用のSNMPエージェント群をコンテナへ移行した途端、問題が一気に噴出しました。NAT層における3つの致命的な弱点が露呈したのです。

  • conntrackテーブルの枯渇: SIP/RTPの各通話は、ランダムに数千個のUDPポートをオープンします。カーネルのnf_conntrackテーブルは上限である262,144セッションに頻繁に達し、ホストのCPU使用率はiptablesルールの探索処理だけで15%から75%へと跳ね上がりました。
  • 送信元IPの消失: 外部へのリクエストがすべてDockerホストのIPに変換されてしまいます。コンテナごとの個別IPに基づいてpfSenseファイアウォールのアクセス制御を設定することは不可能でした。
  • ブロードキャスト/マルチキャストの不通: Docker bridgeはブロードキャストドメインを分離します。その結果、mDNSやBACnet、ONVIF対応カメラといった自動検出(Service Discovery)機能がコンテナを認識できなくなりました。

これらを根本的に解決するため、Linuxカーネルの2つのドライバであるMacvlanとIPVlanを活用し、コンテナを物理LANへ直接接続する構成へと移行しました。

MacvlanとIPVlanのどちらを選ぶべきか?

本番環境で数か月間併用運用した結果、以下のような違いが明確になりました。

  • Macvlan: カーネルがコンテナごとに固有の仮想MACアドレスを生成します。スイッチ側からは、コンテナがスイッチポートに直接接続された物理マシンのように見えます。注意点: CiscoやAruba製スイッチのポートセキュリティ機能(デフォルトで1ポートあたり1〜2個のMACのみ学習可能)に抵触しやすくなります。また、VMware ESXiやAWS/GCPなどのクラウド環境では、MACスプーフィング防止機能によってパケットが即座に破棄されます。
  • IPVlan(L2モード): すべてのコンテナが物理NICのMACアドレスを共有しながら、個別のIPアドレスを保持します。スイッチ側にはホストの単一MACアドレスのみが見えるため、ポートセキュリティの制限を容易にクリアできます。仮想マシン環境やプライベートクラウドに最適な選択肢です。

ホストネットワーク環境の準備

まず、プライマリNIC(ここではeth0を使用)と現在のサブネット範囲を確認します。

ip -br addr show eth0

パケットが正常にルーティングされるよう、LinuxホストでIPフォワーディングを有効化します。

sudo sysctl -w net.ipv4.ip_forward=1
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.d/99-docker-network.conf

インフラに関する注意:VMware ESXi環境でMacvlanを使用する場合は、対象のvSwitchまたはポートグループの設定で無差別モード(Promiscuous Mode)とMACアドレス変更(MAC Address Changes)の両方を承諾(Accept)に設定する必要があります。

詳細な設定手順

1. Macvlanの構築

オフィスLANが192.168.1.0/24、ゲートウェイが192.168.1.1であると仮定します。DHCPプールとの重複を避けるため、Docker専用の小さなサブネット範囲(例:30個のIPを含む192.168.1.192/27)を割り当てます。

docker network create -d macvlan \
  --subnet=192.168.1.0/24 \
  --ip-range=192.168.1.192/27 \
  --gateway=192.168.1.1 \
  -o parent=eth0 \
  macvlan_lan

指定した固定IPでコンテナを起動します。

docker run -d --name legacy-app \
  --net=macvlan_lan \
  --ip=192.168.1.200 \
  --restart=always \
  nginx:alpine

よくある問題の対処:ホストからコンテナへpingが通らない

Linuxカーネルのセキュリティ仕様により、物理NICはホスト自身と子Macvlanインターフェース間の直接ルーティングトラフィックをすべて破棄します。そのため、同一LAN内の他のPCからは問題なくWebアクセスできるにもかかわらず、ホストから192.168.1.200へpingを実行すると100%タイムアウトします。

この解決策として、ホスト側にもMacvlanサブインターフェースを追加作成します。

# 1. ホスト上に仮想macvlanインターフェースを作成
sudo ip link add mac0 link eth0 type macvlan mode bridge

# 2. 専用IPを割り当てて有効化(サブネット内の未使用IPを1つ指定)
sudo ip addr add 192.168.1.220/32 dev mac0
sudo ip link set mac0 up

# 3. コンテナのIPレンジへのルートを作成したインターフェース経由に設定
sudo ip route add 192.168.1.192/27 dev mac0

これでホストとコンテナ間でもシームレスに通信できるようになります。

2. IPVlan(L2モード)の構築

Proxmoxなどの仮想マシン環境や、MACスプーフィングが禁止されているクラウド環境では、IPVlan L2がベストな選択肢です。作成コマンドの構文はMacvlanとほぼ同様です。

docker network create -d ipvlan \
  --subnet=192.168.1.0/24 \
  --ip-range=192.168.1.224/28 \
  --gateway=192.168.1.1 \
  -o ipvlan_mode=l2 \
  -o parent=eth0 \
  ipvlan_lan

IPVlanネットワークを指定してコンテナを起動します。

docker run -d --name voip-service \
  --net=ipvlan_lan \
  --ip=192.168.1.225 \
  alpine sleep 3600

動作確認と検証

環境構築後は、ARPテーブルやパケットフローを確認して正常に動作しているか検証します。

コンテナ内のIPおよびゲートウェイ確認

# コンテナが実際に取得したIPアドレスを確認
docker exec -it legacy-app ip -br addr show

# ゲートウェイへpingを送信しL2接続を確認
docker exec -it legacy-app ping -c 3 192.168.1.1

パケットキャプチャによるMACアドレスの検証

同一LAN内の別端末からコンテナ(192.168.1.200)へ継続してpingを送信します。同時にホストのターミナルでtcpdumpを実行します。

sudo tcpdump -i eth0 icmp and host 192.168.1.200 -n -e

Macvlanの場合、tcpdumpログに表示される宛先MACはコンテナ固有の仮想MACアドレスになります。一方、IPVlanの場合はホストの物理NIC eth0 のMACアドレスと完全に一致します。

まとめ

一般的なWeb APIやマイクロサービスを運用するだけであれば、取り回しの良さからbridgeネットワークが最も手軽です。しかし、VoIP、RTSPカメラ、L2サービス検出を扱う場合は、MacvlanやIPVlanを利用してコンテナを直接LANに参加させることで、大幅なCPU負荷軽減、NATボトルネックの解消、そして整然としたファイアウォール設計を実現できます。

Share: