docker コンテナで DHCP のリースを埋める

Pocket

とある DHCP サーバー機能を持つルーターに対して、大量のクライアントからリース要求が来た場合の動作を確認したい状況があった。

物理マシンや VM を何十何百と用意するのは現実的ではないため、 DHCP クライアントとして機能するコンテナを大量に用意する方法を検証してみる。

docker のデフォルト動作では、コンテナが NAT の下に入りブロードキャストドメインが区切られてしまうため、(IPv4 の場合) UDP のブロードキャストに依存する DHCP は機能しない。

この問題を解決するため、 macvlan ネットワークのブリッジモード を利用して、各コンテナに独立した MAC アドレスを割り当てる方法を採用する。

macvlan は Linux ホストでのみ動作するため、 Windows PC を使用する場合は、 VM 上で Linux と docker を実行し、それをホストマシンの NIC とブリッジする構成を取る。


構成

以下のような構成とする。

graph LR
  routerA[ルーターA]
  subgraph PC["Windows ホスト PC"]
    NIC[NIC]
    subgraph HyperV["Hyper-V"]
      VSwitch["外部ネットワーク仮想スイッチ(プロミスキャスモード状態)"]
      subgraph LinuxVM["Linux VM (Ubuntu 24.04)"]
        eth["eth0<br>(promisc on)"]
        subgraph Docker["docker"]
          Macvlan["macvlanネットワーク"]
          DC1[container]
          DC2[container]
          DC3["..."]
        end
      end
    end
  end
  routerA --> NIC
  NIC --> VSwitch
  VSwitch --> eth
  eth --> Macvlan
  Macvlan --> DC1
  Macvlan --> DC2
  Macvlan --> DC3

ホストPCの NIC、 Linux VM のネットワークインターフェース、コンテナは、それぞれ独立した MAC アドレスを持つ。

ルーターとコンテナの間にある NIC と Linux VM は、自分自身以外の MAC アドレス宛ての通信を受信した場合、スイッチングハブのようにブリッジ先に転送する必要がある。

各 NIC は、デフォルトでは自身以外の MAC アドレス宛ての通信を無視するため、コンテナまでイーサネットフレームが到達しない。これを解決するには、全ての通信を受け入れる「プロミスキャスモード」相当の設定が必要となる。

順を追って設定していこう。

ホスト PC の NIC とブリッジする VM を作成

任意のハイパーバイザーで VM を作成する。なお、 Linux 物理マシンを利用可能な場合は、この章をスキップして次に進んで構わない。

Windows 上の Hyper-V を例に説明しよう。

まず、ホストPC のNIC と繋がる「外部ネットワーク」仮想スイッチを作成する。

ホスト OS が使用している NIC をコンテナから直接利用する場合は、「管理オペレーティング システムにこのネットワーク アダプターの共有を許可する」にチェックを入れる。別の USB LAN アダプタなどを使用する場合は、チェックを外してもよい(ただしホスト OS からその NIC が見えなくなる)。

次に VM を作成し、そのネットワークアダプターに前項の仮想スイッチを接続する。さらに、「高度な設定」で「MAC アドレスのスプーフィングを有効にする」にチェックを入れる。

これにより、ハイパーバイザー側で前述のプロミスキャスモード相当の設定が有効になる。

Linux で docker を立ち上げる

VM に Linux と docker をインストールする。

以下の例では、 Ubuntu 24.04 に snap で docker をインストールする手順を示す。

$ sudo addgroup --system docker
$ sudo adduser $USER docker
$ newgrp docker
$ sudo snap install docker
$ sudo chown root:docker /var/run/docker.sock

そして、 Linux 側のネットワークインターフェースにもプロミスキャスモードの設定を適用する。

$ sudo ip link set dev eth0 promisc on
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host noprefixroute
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.26/24 metric 100 brd 192.168.1.255 scope global dynamic eth0
...

外部の NIC と接続するインターフェース(例: eth0)で PROMISC と表示されていることと、 DHCP から IP アドレスがリースされていることを確認する。

なお、このプロミスキャスモードの設定は、ゲスト OS を再起動すると毎回リセットされるため、再設定が必要となることに注意。

コンテナを立ち上げる

この状態で、 $HOME 以下のディレクトリのいずれかに、以下のような compose.yaml を作成する。

networks:
  macvlan_net:
    name: macvlan_net
    driver: macvlan
    driver_opts:
      parent: eth0
    ipam:
      config:
        - subnet: 169.254.0.0/16
          gateway: 169.254.0.1
x-svc: &dockersvc
  build:
    context: .
    dockerfile_inline: |
      FROM ubuntu:24.04
      RUN apt-get update && apt-get install -y isc-dhcp-client iputils-ping net-tools && apt-get clean && rm -rf /var/lib/apt/lists/*
  command: bash -c 'ip -4 a | awk '\''/169\.254\./ {print $$2, "dev", $$NF}'\'' | xargs -r ip a del && sleep $$((RANDOM % 8))s && dhclient eth0 -v && sleep 2s && ip a && sleep 1m'
  cap_add:
    - NET_ADMIN
  networks:
    - macvlan_net
services:
  svc000: *dockersvc
  svc001: *dockersvc
  svc002: *dockersvc
  svc003: *dockersvc
  svc004: *dockersvc
  svc005: *dockersvc
  svc006: *dockersvc
  svc007: *dockersvc
  svc008: *dockersvc
  svc009: *dockersvc
  # ...

compose.yaml が存在するフォルダをカレントディレクトリとして docker compose up を実行すると、 services で定義したコンテナの数だけ DHCP クライアントが起動し、それぞれが IP アドレスのリースを受ける。

docker イメージはキャッシュが効くため、イメージのビルドは1回で済み、大量の DHCP クライアントを高速に起動できる。

より多くのリースを行う場合は、以下のいずれかの方法を取ることができる:

  • services のコンテナ数を増やして実行
  • docker compose down の後に再度 up を実行し、異なる MAC アドレスでリースを要求

なお、エスケープ処理のため command 部分が複雑になっているが、以下の3つの処理を行っている:

  1. ip -4 a | awk '/169\.254\./ {print $2, "dev", $NF}' | xargs -r ip a del で、 macvlan ネットワーク作成時に仮で登録したリンクローカルアドレスを(あれば)削除
  2. sleep $((RANDOM % 8))s で、コンテナごとに 0~7 秒のランダムスリープ
  3. dhclient eth0 -v で DHCP から IP アドレスをリース

おわりに

本記事では、 DHCP サーバーの負荷テストを行う場合を念頭に、 docker コンテナに MAC アドレスを割り当て、 DHCP クライアントを大量に作成する方法を説明した。

プロミスキャスモードの設定など、利用に際して注意する点はいくつかあるが、手順自体は単純である。この方法を活用することで、物理マシンや VM を大量に用意することなく、効率的に物理的な DHCP サーバーの動作検証を行うことができる。

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください