Linux bridgeを使って仮想ネットワークを作る

## はじめに
https://qiita.com/tsuzuki_takaaki/items/2a3b85e74c9f79da2098

上記の記事では、2つの異なるNetwork Namespace間を直接繋ぐような仮想ネットワークを作成しました。
次は3つ以上のNetwork Namespace間を繋ぐような仮想ネットワークを作成します。

2つの異なるNetwork Namespace間の通信は、それぞれにvethをアタッチして直接リンクする形で実現することができましたが、3つ以上のNetwork Namespace間の通信はこの方法では実現できません。
そこでLinux bridgeを使います。

## Linux bridgeとは
仮想的なL2スイッチです。
なので、このLinux bridgeを使うことによってどのポートにどのMACアドレスの機器がつながっているのかを管理することができます。

DockerのネットワークでもこのLinux bridgeが使われています。

https://docs.docker.com/engine/network/drivers/bridge/

## Linux bridgeを使って複数Network Namespace間で疎通する
3つのNetwork Namespace間での通信をLinux bridgeを使って実現します。
構成は図に表すと、以下のようになります。

![スクリーンショット 2024-12-22 11.39.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2849988/0b1fed67-aa9b-5926-1a90-6e1018247f0b.png)

まずは、3つのNetwork Namespaceを作成します。

```sh
$ sudo ip netns add ns1
$ sudo ip netns add ns2
$ sudo ip netns add ns3
```

次に、bridgeを作成します。
https://man7.org/linux/man-pages/man8/ip-link.8.html

```sh
$ sudo ip link add br0 type bridge
```

両端がNetwork Interfaceになっている仮想的なケーブルを作成し、一方はNetwork Namespaceにアタッチし、もう一方はbridgeにアタッチします。
(マシンのポートと、L2スイッチのポートにイーサーネットケーブルを挿し込む操作を仮想的に再現していることになります。)

```sh
# 両端がNetwork Interfaceになっている仮想的なケーブルの作成
$ sudo ip link add ns1-veth0 type veth peer ns1_br0-veth0
$ sudo ip link add ns2-veth0 type veth peer ns2_br0-veth0
$ sudo ip link add ns3-veth0 type veth peer ns3_br0-veth0

# Network Namespaceとbridgeにアタッチ
$ sudo ip link set ns1-veth0 netns ns1
$ sudo ip link set ns1_br0-veth0 master br0

$ sudo ip link set ns2-veth0 netns ns2
$ sudo ip link set ns2_br0-veth0 master br0

$ sudo ip link set ns3-veth0 netns ns3
$ sudo ip link set ns3_br0-veth0 master br0
```
加えて、初期状態ではNetwork InterfaceはDOWNになっていて通信ができない状態にあるため、UPに変更しておきます。
```sh
# Network InterfaceをDOWNからUPに
$ sudo ip link set dev br0 up
$ sudo ip netns exec ns1 ip link set ns1-veth0 up
$ sudo ip link set ns1_br0-veth0 up
$ sudo ip netns exec ns2 ip link set ns2-veth0 up
$ sudo ip link set ns2_br0-veth0 up
$ sudo ip netns exec ns3 ip link set ns3-veth0 up
$ sudo ip link set ns3_br0-veth0 up
```
ping(L3)で疎通確認をするために各Network NamespaceのNetwork Interfaceに対してIPアドレスを割り振ります。

```sh
$ sudo ip netns exec ns1 ip address add 192.168.0.1/24 dev ns1-veth0
$ sudo ip netns exec ns2 ip address add 192.168.0.2/24 dev ns2-veth0
$ sudo ip netns exec ns3 ip address add 192.168.0.3/24 dev ns3-veth0
```

これで3つのNetwork Namespace間で、Linux bridgeを使って通信をする準備ができたのでpingしてみます。
```sh
# ns1 <-> ns2
$ sudo ip netns exec ns1 bash
$ ping -c 3 192.168.0.2
PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.
64 bytes from 192.168.0.2: icmp_seq=1 ttl=64 time=0.127 ms
64 bytes from 192.168.0.2: icmp_seq=2 ttl=64 time=0.090 ms
64 bytes from 192.168.0.2: icmp_seq=3 ttl=64 time=0.089 ms

# ns1 <-> ns3
$ ping -c 3 192.168.0.3
PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data.
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=0.144 ms
64 bytes from 192.168.0.3: icmp_seq=2 ttl=64 time=0.085 ms
64 bytes from 192.168.0.3: icmp_seq=3 ttl=64 time=0.075 ms

# ns2 <-> ns3
$ sudo ip netns exec ns1 bash
$ ping -c 3 192.168.0.3
PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data.
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=0.126 ms
64 bytes from 192.168.0.3: icmp_seq=2 ttl=64 time=0.093 ms
64 bytes from 192.168.0.3: icmp_seq=3 ttl=64 time=0.081 ms
```
3つ以上のNetwork Namespace間で疎通を確認できました。

## L2スイッチとしてのLinux bridgeの挙動
L2スイッチは、自身のどのポートにどのMACアドレス機器がつながっているのかをテーブルとして管理しています。(forwardingテーブル)
これは今回のLinux bridgeも同様なので、そちらの確認もします。

確認しやすくするために、各Network Namespaceに割り当てたNetwork InterfaceのMACアドレスを変更しておきます。
```sh
$ sudo ip netns exec ns1 ip link set dev ns1-veth0 address 00:00:00:00:00:01
$ sudo ip netns exec ns2 ip link set dev ns2-veth0 address 00:00:00:00:00:02
$ sudo ip netns exec ns3 ip link set dev ns3-veth0 address 00:00:00:00:00:03
```

初期状態だと、forwardingテーブルにこれらのMACアドレスは登録されていません。

```sh
$ brctl showmacs br0
port no mac addr                is local?       ageing timer
```

L2スイッチは、forwaringテーブルにすでにエントリが存在する場合は、それに従ってパケットをforwardingし、存在しなかった場合は送信元以外のすべてのインターフェースにforwardingすることでforwaringテーブルを更新します。
この宛先が不明な場合にすべてのインターフェースにfowardingすることをフラッディングと言います。

初回の疎通確認時にはフラッディングが発生しており、`ns1 <-> ns3`の通信パケットが`ns2`にも到達しています。
ここで初めてforwardingテーブルの更新が発生し、エントリが追加されます。

```sh
$ sudo ip netns exec ns1 ping -c 3 192.168.0.3
PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data.
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=0.167 ms
64 bytes from 192.168.0.3: icmp_seq=2 ttl=64 time=0.076 ms
64 bytes from 192.168.0.3: icmp_seq=3 ttl=64 time=0.071 ms

$ brctl showmacs br0
port no mac addr                is local?       ageing timer
  1     00:00:00:00:00:01       no                15.14
  3     00:00:00:00:00:03       no                15.14
```
bridgeのforwardingテーブルが更新されていることも確認できました。

## Dockerとbridge
Dockerのネットワーキングは、このLinux bridgeを使って実現されています。

> Docker をインストールした全ての環境には、 docker0 と表示されるブリッジ（ bridge ）ネットワークが現れます。

https://docs.docker.jp/engine/userguide/networking/dockernetworks.html

```sh
$ ip addr show docker0
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:e4:91:b1:8b brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:e4ff:fe92:b14b/64 scope link 
       valid_lft forever preferred_lft forever

# macOSだとhostからdocker0を見ることはできません
```

Dockerでコンテナを立ち上げると、各コンテナはデフォルトの`docker0`ブリッジに接続されます。
これは、先ほど検証したNetwork Namespaceとbridgeの構造と同じです。

```sh
$ docker run -d --name nginx nginx:latest
$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "a8c55753238929f05deac5b15c3dae68c5474027d514752c6f3579072fe2c90e",
        "Created": "2024-12-21T15:07:09.274191183+09:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "ac64a77d8bb3ebea67f2f5e8a99111430602491986ec34478d499656d88200d2": {
                "Name": "nginx",
                "EndpointID": "ca1f41043ad45c0c234486b7ca91c4e1e90ac0cf5940b9802496345555c450a3",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]
```

### docker composeとbridge
`docker compose`は特に何も指定しなかった場合、docker0とは別にbridgeが新規で作成され、コンテナがそのbridgeを通して通信をすることができます。

hogeというディレクトリで、以下のようなcomposeファイルの設定でコンテナを立ち上げます。
```compose.yaml
services:
  host-1:
    image: ubuntu:latest
    container_name: host-1
    tty: true
    cap_add:
      - NET_ADMIN
  host-2:
    image: ubuntu:latest
    container_name: host-2
    tty: true
    cap_add:
      - NET_ADMIN
```
新規で`hoge_default`という名前のbridge networkが作成され、対象のコンテナがそのbridge越しにつながっていることを確認できます。
```sh
$ docker network ls
NETWORK ID     NAME           DRIVER    SCOPE
a8c557532389   bridge         bridge    local
9478816f6d2a   hoge_default   bridge    local
cab48c3a05f6   host           host      local
5e62a3cdff2a   none           null      local
```
```sh
$ docker network inspect hoge_default
[
    {
        "Name": "hoge_default",
        "Id": "9478816f6d2a318d5f13b2e6f687fb622c423f821722f164bbd671ba77e01435",
        "Created": "2024-12-22T19:13:40.442523611+09:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "15a7e657e980f50dfd359368d609304cf831582bc42d01bd8216c99f02d638aa": {
                "Name": "host-2",
                "EndpointID": "7f9a7b2653ca3cd9c86105e766255d2d8b265beccf850407ef1e0aac86b4cb69",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            },
            "ba33992848379546d38f11fd123ae2a6f754fd38dc7d03d026600a4f04647bfe": {
                "Name": "host-1",
                "EndpointID": "e2abc2fc0abcb6b7bf5157f7eb896f1a0bb7e6bd8f230d3e4f2ab6b2c50e148d",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {
            "com.docker.compose.network": "default",
            "com.docker.compose.project": "hoge",
            "com.docker.compose.version": "2.29.7"
        }
    }
]
```

## 参考
https://github.com/momijiame/linux-tcpip-book

https://tatsu-zine.com/books/superuser-linux-system

https://www.sbcr.jp/product/4815618599/

https://man7.org/linux/man-pages/man8/bridge.8.html

https://www.ohmsha.co.jp/book/9784274224478/

https://bookplus.nikkei.com/atcl/catalog/07/P83110/