flannel を使って Linux で実現する vxlan のさわりを知る

Linux Advent Calendar 21日目を震えながら書きます。 @udzura です。普段は昨日紹介した [Haconiwa](https://qiita.com/udzura/items/5a77ed273370f2dc0f83) などのようにLinuxコンテナランタイムやRubyを書いたりしています。正確には、雰囲気で書いています。

今日は、LinuxでOverlay networkを実現するミドルウェア、flannelの簡単な使い方と、コンテナとの連携の仕方、そしてvxlanのパケットの様子などのメモを書いていこうと思います。

## flannelとは

CoreOS社の開発している [`simple and easy` な](https://github.com/coreos/flannel#readme) Overlay networkのためのミドルウェアです。

もともとはCoreOS（現Container Linux）自身で使うために開発されていた記憶があるのですが、現在はKubernetes と連携するためというところが第一のゴールのようで、普通はkubectlコマンド経由でセットアップするようです。

今回は、挙動などを把握して色々実験するためにも、手動でコマンドを叩きつつ組んでみます。

## 環境

Vagrantを使って漠然と3台を立てます。

```ruby:Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/zesty64"
  config.vm.provider "virtualbox" do |vbox|
    vbox.cpus = 2
  end

  config.vm.define 'flannel-1' do |c|
    c.vm.hostname = 'flannel-1'
    c.vm.network "private_network", ip: "192.168.254.10"
  end

  config.vm.define 'flannel-2' do |c|
    c.vm.hostname = 'flannel-2'
    c.vm.network "private_network", ip: "192.168.254.11"
  end

  config.vm.define 'flannel-3' do |c|
    c.vm.hostname = 'flannel-3'
    c.vm.network "private_network", ip: "192.168.254.12"
  end
end
```

```console
$ vagrant up
```

ディストロは Ubuntu Zesty 64bit, カーネルは `linux-image-generic 4.10.0.37.37` だそうです。

## 動作に必要な etcd のセットアップ

flannelはサブネットなどの情報を、etcdをバックエンドとして保持するので、先にインストールしてクラスタを組んでおく必要があります。etcd自体はパッケージのもので十分です。

```console
$ sudo apt install etcd
```

ホスト `flannel-[1,2,3]` について、 `/etc/default/etcd` ファイルを以下のようにします。

```
ETCD_NAME="flannel-1"               
ETCD_INITIAL_CLUSTER="flannel-1=http://192.168.254.10:2380,flannel-2=http://192.168.254.11:2380,flannel-3=http://192.168.254.12:2380"             
ETCD_INITIAL_CLUSTER_STATE="new"    
ETCD_LISTEN_PEER_URLS="http://192.168.254.10:2380,http://192.168.254.10:7001"                                                                     
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"                            
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.254.10:2379"                  
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.254.10:2380"            
```

```
ETCD_NAME="flannel-2"               
ETCD_INITIAL_CLUSTER="flannel-1=http://192.168.254.10:2380,flannel-2=http://192.168.254.11:2380,flannel-3=http://192.168.254.12:2380"             
ETCD_INITIAL_CLUSTER_STATE="new"    
ETCD_LISTEN_PEER_URLS="http://192.168.254.11:2380,http://192.168.254.11:7001"                                                                     
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"                            
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.254.11:2379"                  
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.254.11:2380"            
```

```
ETCD_NAME="flannel-3"               
ETCD_INITIAL_CLUSTER="flannel-1=http://192.168.254.10:2380,flannel-2=http://192.168.254.11:2380,flannel-3=http://192.168.254.12:2380"             
ETCD_INITIAL_CLUSTER_STATE="new"    
ETCD_LISTEN_PEER_URLS="http://192.168.254.12:2380,http://192.168.254.12:7001"                                                                     
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"                            
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.254.12:2379"                  
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.254.12:2380"            
```

この状態で、可能なら3つターミナルを立ててそれぞれsystemctl経由で再起動すればOKです。うまくいかない場合は `/var/lib/etcd/default/*` にあるデータを消していきましょう。

```console
$ sudo systemctl restart etcd
```

正直etcdのクラスタ組むのが結構大変ですが、本筋ではないのでさらっと作りました...。 [こちらのQiita記事は参考になりました。](https://qiita.com/hana_shin/items/602f98bd9b153d22e50c)

## flannel のインストールと起動

etcdが入れば、どこのホストからでもいいので、先にflanneldが利用する設定を入れます。

```console
$ etcdctl set /coreos.com/network/config \
    '{ "Network": "10.254.0.0/16", "Backend": {"Type": "vxlan"}}'
```

その後、それぞれのホストで以下のようにざっくりフランネルさんたちをを立ち上げます。

```console
$ wget https://github.com/coreos/flannel/releases/download/v0.9.1/flanneld-amd64 && \
    chmod a+x ./flanneld-amd64 && \
    sudo nohup sh -c './flanneld-amd64 -iface enp0s8 1>/var/log/flanneld.log 2>&1' &
```

するとそれぞれのホストに `flannel.1` なるデバイスが生えます。それぞれのIPは、/24の範囲かつ`10.254.0.0/16`のサブネットワークのはず。

```
6: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default 
    link/ether 5e:19:70:99:41:64 brd ff:ff:ff:ff:ff:ff
    inet 10.254.1.0/32 scope global flannel.1
       valid_lft forever preferred_lft forever
    inet6 fe80::5c19:70ff:fe99:4164/64 scope link 
       valid_lft forever preferred_lft forever
```

割り当たっている様子は以下のようにも確認できます。

```console
ubuntu@flannel-3:~$ etcdctl ls /coreos.com/network/subnets/
/coreos.com/network/subnets/10.254.67.0-24
/coreos.com/network/subnets/10.254.1.0-24
/coreos.com/network/subnets/10.254.69.0-24
```

なんかIPがかぶってる、などの場合 `/var/run/flannel/subnet.env` というファイルを消すといいかもしれない。

このvxlanデバイスは、各ホストでお互いに疎通出来ます。pingを打ったりして満足しましょう。

```console
ubuntu@flannel-1:~$ ip route
default via 10.0.2.2 dev enp0s3 
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15 
10.254.1.0/24 via 10.254.1.0 dev flannel.1 onlink <- !!
10.254.69.0/24 via 10.254.69.0 dev flannel.1 onlink <- !!
192.168.254.0/24 dev enp0s8 proto kernel scope link src 192.168.254.10 
ubuntu@flannel-1:~$ ping 10.254.69.0
PING 10.254.69.0 (10.254.69.0) 56(84) bytes of data.
64 bytes from 10.254.69.0: icmp_seq=1 ttl=64 time=0.399 ms
64 bytes from 10.254.69.0: icmp_seq=2 ttl=64 time=0.469 ms
```

## コンテナにくっつける

コンテナランタイムにDockerを使う場合、ひとまずこちらもパッケージで入れてしまい、以下のように設定を変えます。

```console
$ sudo apt install docker.io

# ExecStart を置き換えるのでDrop-Inはうまく動かず、Unitファイルを直接上書き作成...
$ sudo vim /etc/systemd/system/docker.service
[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network.target docker.socket firewalld.service
Requires=docker.socket

[Service]
Type=notify
EnvironmentFile=/run/flannel/subnet.env
ExecStart=/usr/bin/dockerd --bip $FLANNEL_SUBNET --mtu $FLANNEL_MTU -H fd:// $DOCKER_OPTS
ExecReload=/bin/kill -s HUP $MAINPID
LimitNOFILE=1048576
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
TimeoutStartSec=0
Delegate=yes
KillMode=process

[Install]
WantedBy=multi-user.target

$ sudo systemctl daemon-reload
$ sudo systemctl restart docker.service
```

dockerdがこんな様子のオプションで立ち上がっていれば成功で、ブリッジもできています。

```
/usr/bin/dockerd --bip 10.254.69.1/24 --mtu 1450 -H fd://
 \_ containerd -l unix:///var/run/docker/libcontainerd/docker-containerd.sock
```

```console
ubuntu@flannel-3:~$ ip a s docker0
7: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:51:f1:38:f5 brd ff:ff:ff:ff:ff:ff
    inet 10.254.1.1/24 scope global docker0
       valid_lft forever preferred_lft forever
```

適当な軽めのイメージに `sh -l` で入って、相互にpingしたりして遊べます。

```console
$ sudo docker run -ti debian:latest /bin/sh -l
## flannel-1
# ip a s eth0
8: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default 
    link/ether 02:42:0a:fe:43:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.254.67.2/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:aff:fefe:4302/64 scope link 
       valid_lft forever preferred_lft forever

## flannel-3
# ip a s eth0                       
8: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default 
    link/ether 02:42:0a:fe:01:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.254.1.2/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:aff:fefe:102/64 scope link 
       valid_lft forever preferred_lft forever
# ping 10.254.67.2
PING 10.254.67.2 (10.254.67.2): 56 data bytes
64 bytes from 10.254.67.2: icmp_seq=0 ttl=62 time=0.651 ms
64 bytes from 10.254.67.2: icmp_seq=1 ttl=62 time=0.537 ms
64 bytes from 10.254.67.2: icmp_seq=2 ttl=62 time=0.411 ms
```

もちろん自分でbridgeを作り、vethを引っ張って自作Linux Namespaceで相互pingも出来ます。楽しいですね。

## パケットキャプチャをしてみる

だいぶ長くなったので、最後に実際にこのように作ったOverlay networkについてもう少しだけ（本当に少しだけ）詳しく眺めてみようと思います。

ネットワーク...眺める... ということで安易ですがtcpdumpを使います。

「ホストをまたいで」、あるコンテナから別のコンテナにpingを送り続けておきます。

```console
# ping 10.254.1.2
```

この状態で、対向側のホストにtcpdumpを仕掛けます。この時、vxlanデバイスやブリッジを監視すると、普通に流れているのが見えます。

```console
ubuntu@flannel-3:~$ sudo tcpdump -i flannel.1 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on flannel.1, link-type EN10MB (Ethernet), capture size 262144 bytes
11:42:34.197889 IP flannel-1 > 10.254.1.2: ICMP echo request, id 13, seq 41, length 64
11:42:34.197942 IP 10.254.1.2 > flannel-1: ICMP echo reply, id 13, seq 41, length 64
11:42:35.199681 IP flannel-1 > 10.254.1.2: ICMP echo request, id 13, seq 42, length 64
11:42:35.199733 IP 10.254.1.2 > flannel-1: ICMP echo reply, id 13, seq 42, length 64
11:42:36.200875 IP flannel-1 > 10.254.1.2: ICMP echo request, id 13, seq 43, length 64
11:42:36.200922 IP 10.254.1.2 > flannel-1: ICMP echo reply, id 13, seq 43, length 64
11:42:37.202760 IP flannel-1 > 10.254.1.2: ICMP echo request, id 13, seq 44, length 64
11:42:37.202823 IP 10.254.1.2 > flannel-1: ICMP echo reply, id 13, seq 44, length 64
11:42:38.208945 IP flannel-1 > 10.254.1.2: ICMP echo request, id 13, seq 45, length 64
11:42:38.209012 IP 10.254.1.2 > flannel-1: ICMP echo reply, id 13, seq 45, length 64
```

ですが、その上におそらく通るであろうデバイス、Vagrantのプライベートネットワークに割り振られた `enp0s8` を見てみても、icmpのパケットが流れません。

```console
ubuntu@flannel-3:~$ sudo tcpdump -i enp0s8 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp0s8, link-type EN10MB (Ethernet), capture size 262144 bytes
^C
0 packets captured
0 packets received by filter
0 packets dropped by kernel
```

どうすれば見えるかというと、udpの8472番を覗いてみると何かが流れています。

```console
ubuntu@flannel-3:~$ sudo tcpdump -i enp0s8 udp port 8472
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp0s8, link-type EN10MB (Ethernet), capture size 262144 bytes
11:46:35.783729 IP flannel-1.48158 > flannel-3.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP flannel-1 > 10.254.1.2: ICMP echo request, id 13, seq 282, length 64
11:46:35.783881 IP flannel-3.49529 > flannel-1.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.254.1.2 > flannel-1: ICMP echo reply, id 13, seq 282, length 64
11:46:36.784927 IP flannel-1.48158 > flannel-3.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP flannel-1 > 10.254.1.2: ICMP echo request, id 13, seq 283, length 64
11:46:36.785069 IP flannel-3.49529 > flannel-1.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.254.1.2 > flannel-1: ICMP echo reply, id 13, seq 283, length 64
11:46:37.786588 IP flannel-1.48158 > flannel-3.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP flannel-1 > 10.254.1.2: ICMP echo request, id 13, seq 284, length 64
11:46:37.786677 IP flannel-3.49529 > flannel-1.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.254.1.2 > flannel-1: ICMP echo reply, id 13, seq 284, length 64
11:46:38.788530 IP flannel-1.48158 > flannel-3.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP flannel-1 > 10.254.1.2: ICMP echo request, id 13, seq 285, length 64
```

udpなのに `ICMP echo request` と出ていて、vxlanなのでホストをまたぐ時にもともとのICMPイーサネットフレームをUDP/IPでカプセル化してくれているわけですが、tcpdumpでその中身を見せているというわけですね。

----

正直、 flannel がどのようにvxlanバックエンドをやっていっているかは [中井さんの記事](http://enakai00.hatenablog.com/entry/2015/04/02/173739) の方が詳細でわかりやすく、パケットのヘッダがどんな感じに追加されてカプセル化されているかは [こちらの記事](https://blog.bitmeister.jp/?p=4061) が詳しいので、この記事はflannelのセットアップ手順でしかなかった...。

そんな感じなのですが、KubernetesやDocker SwarmでOverlay network - つまり、 L2 を L3 までで実現するネットワークを使っている方もいらっしゃると思い、その技術は具体的にはどういう感じで動いているのか、少し親しみが持てたのなら幸いに思います。僕は今度RFC読みます。

Enjoy networking!!!