DockerコンテナとブリッジとDHCP

## やること

- DockerコンテナをDockerホストと同じネットワークに繋ぎます
- DockerコンテナにDockerホストと同じDHCPサーバーからIPアドレスを割り当てます
- Dockerコンテナとその他ホストで、ポートバインディングなしで双方向に通信します
- Dockerコンテナ内でsystemdを動かします

つまり仮想マシンをブリッジ接続したときと同じようなことをDockerコンテナで行います。
これができると、特権ポートである80や443を使うhttpサーバーを、仮想マシンよりも軽量にいくらでも増やせるようになります。


## 環境

- Dockerホスト：Fedora37
    - 物理PC上で動作している
    - firewalldとselinuxは無効
    - ブローバンドルーターからDHCPでIPアドレスをアサインされている
- Dockerエンジン：Moby-Engine 20.10.21

Moby-Engineは、DockerのOSS版です。chromeに対するchromiumのような位置づけです。


## 仮想ブリッジ環境構築

まずDockerホスト側で仮想ブリッジを作ります。特にDockerに依存した話ではなく、ここで作成した仮想ブリッジをKVM仮想マシンでも使うことができます。

```
既存の接続情報を削除
# nmcli connection del 有線接続
ブリッジを追加
# nmcli connection add type bridge autoconnect yes con-name br0 ifname br0
IPv4はDHCP、IPv6は無効
# nmcli connection modify br0 ipv4.method auto
# nmcli connection modify br0 ipv6.method disabled

ブリッジbr0にネットワークインタフェースenp0s3を追加する。enp0s3の実際のイーサーネットデバイスに置き換える
# nmcli connection add type bridge-slave autoconnect yes con-name enp0s3 ifname enp0s3 master br0
```

Linuxカーネルのbridgeモジュールは、br_netfilterというモジュールを使ってパケットをフィルタしているので、フィルタを無効化する。

```text:/etc/modules-load.d/br_netfilter.conf
br_netfilter
```

```text:/etc/sysctl.d/bridge.conf
net.bridge.bridge-nf-call-ip6tables=0
net.bridge.bridge-nf-call-iptables=0
net.bridge.bridge-nf-call-arptables=0
```

設定が終わったら再起動する

```
# shutdown -r now
```

再起動後、うまく行っていれば、br0にブロードバンドルーターのDHCPからIPアドレスがアサインされています

### よくわからなかったもの

- スパニングツリープロトコル
    - イーサーネットフレームがネットワークを循環しないようにするプロトコル
    - 仮想ブリッジについて調べると、これを無効にしている記事をよく見かける。今回のような仮想ブリッジの構成では、循環が発生しないので、不要なので無効にしているということか？
- プロミスキャス・モード
    - Ethernetデバイスが自身宛のパケット以外も受け取るモード
    - 物理デバイスの方で設定が必要そうにも思えるが、今の所設定しなくても動いている。


## docker-net-dhcp

ここからDockerコンテナを立ち上げていきます。

dockerエンジンは以下のようにインストールします。

```
$ sudo dnf install -y moby-engine
$ sudo systemctl enable docker --now
$ sudo usermod -aG docker $USER
$ shutdown -r now
```

Dockerコンテナのプラグインであるdocker-net-dhcpをインストールします。

https://github.com/devplayer0/docker-net-dhcp

```
# docker-net-dhcpのインストール
$ docker plugin install ghcr.io/devplayer0/docker-net-dhcp:release-linux-amd64
# docker-net-dhcpを使ったDockerネットワークを作成
$ docker network create -d ghcr.io/devplayer0/docker-net-dhcp:release-linux-amd64 --ipam-driver null -o bridge=br0 my-dhcp-net
```

上記でbridge=br0を指定しています。これでdocker-net-dhcpが、br0を経由してDHCPサーバーにアクセスするようになります。

ネットワークにmy-dhcp-netを指定してnginxコンテナを立ち上げて、そのIPアドレスを取得します。

```
$ docker run --rm -d --network my-dhcp-net --name nginx nginx
$ docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nginx
192.168.1.109
```
ブラウザで、 http://192.168.1.109 を開くとnginxのページが表示されます。
Dockerホスト以外のマシンからでもアクセス可能です。

## 追記 2024/4/7

最近のdockerでは、docker-net-dhcpはバージョンミスマッチで動かないようです。

https://github.com/devplayer0/docker-net-dhcp/issues/42

修正版があるのでこちらを使えば動きます。

https://github.com/Celerway/docker-net-dhcp/tree/master

古いdocker-net-dhcpをインストールしている場合は、事前にネットワークとプラグイン削除しておきます（ブリッジは残します）。

```
$ docker network rm my-dhcp-net
$ docker plugin disable ghcr.io/devplayer0/docker-net-dhcp:release-linux-amd64
$ docker plugin rm ghcr.io/devplayer0/docker-net-dhcp:release-linux-amd64
```

修正版を使用します

```
$ git clone git@github.com:Celerway/docker-net-dhcp.git
$ cd docker-net-dhcp
$ make
$ docker network create -d ghcr.io/devplayer0/docker-net-dhcp:golang --ipam-driver null -o bridge=br0 my-dhcp-net
```

これでネットワークができたので、nginxの動作確認方法は同じです。

```
$ docker run --rm -d --network my-dhcp-net --name nginx nginx
$ docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nginx
192.168.1.109
```
## コンテナ内でsystemdを動かす

最後にコンテナ内でsystemdとsshdを動かして、そこにsshログインします。
ここまでくるとDockerコンテナを仮想マシンのように扱えます。
ただし、セキュリティ的によろしくないと言われる、priviledgeモードを使うので、あくまで実験ということで。パラメータによっては、Dockerホスト側にも影響するのでご注意ください。

まずsystemdを含んだコンテナイメージを作ります。
以下のようなDockerfileを作ります。

```Dockerfile:Dockerfile
FROM rockylinux/rockylinux:8.4
ENV container docker
RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == \
systemd-tmpfiles-setup.service ] || rm -f $i; done); \
rm -f /lib/systemd/system/multi-user.target.wants/*;\
rm -f /etc/systemd/system/*.wants/*;\
rm -f /lib/systemd/system/local-fs.target.wants/*; \
rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
rm -f /lib/systemd/system/basic.target.wants/*;\
rm -f /lib/systemd/system/anaconda.target.wants/*;
RUN dnf install -y openssh-server openssh-clients passwd procps-ng iproute
RUN echo "hogehoge" | passwd --stdin root
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
VOLUME [ "/sys/fs/cgroup" ]
CMD ["/usr/sbin/init"]
```

上記ではrootのパスワードをhogehogeにして、sshログインできるようにしています。
ビルドして実行します。

```
$ docker build -t myrocky .
$ docker run --rm -d -v /sys/fs/cgroup:/sys/fs/cgroup:rw --privileged --cgroupns=host --network my-dhcp-net  --name myrocky myrocky
$ docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' myrocky
192.168.1.111
$ ssh root@192.168.1.111
```

参考
- https://hub.docker.com/r/rockylinux/rockylinux
- https://graff-it-i.com/2021/11/16/rocky-linux-container/