手を動かして学ぶ QEMU とネットワーク

このポストは [Linux Advent Calendar 2021](https://qiita.com/advent-calendar/2021/linux) の 19日目の記事です。

Linux 成分があんまり入れられなかったですが、強い気持ちで見てもらうことにします。

QEMU で起動させた VM がどのような設定で通信するのか、思いを馳せたことはないでしょうか。

今回は、オーソドックスな設定である Linux Bridge + TAP デバイスによる接続をすべて自分の手で構築し、VM 同士が通信できるようになるまでやってみます。

## QEMU プロセスは設定が多機能すぎる

QEMU は、動作させるために必要な設定が多岐にわたるため、運用する上で直接 qemu コマンドを実行して VM を起動することはありません。

OpenStack, Vagrant for kvm, kubevirt など、ほとんどのプラットフォームは libvirt という仮想化に関する機能を包括した api を提供してくれるソフトウェアを使用します。

例えば、uvtool という、ubuntu 上で簡単に VM を起動できるツールがあり、そちらも libvirt を経由して VM を起動します。

VM を起動した際に実際に起動するプロセスは以下のようになります。

```
libvirt+  291694  100  0.6 1257616 98960 ?       Sl   17:03   0:11 /usr/bin/qemu-system-x86_64 -name guest=test,debug-threads=on -S -object secret,id=masterKey0,format=raw,file=/var/lib/libvirt/qemu/domain-1-test/master-key.aes -machine pc-q35-focal,accel=kvm,usb=off,dump-guest-core=off -cpu qemu64 -m 512 -overcommit mem-lock=off -smp 1,sockets=1,cores=1,threads=1 -uuid 6ecbc830-38d4-479a-a319-65e1d71e854e -no-user-config -nodefaults -chardev socket,id=charmonitor,fd=32,server,nowait -mon chardev=charmonitor,id=monitor,mode=control -rtc base=utc -no-shutdown -boot strict=on -device pcie-root-port,port=0x10,chassis=1,id=pci.1,bus=pcie.0,multifunction=on,addr=0x2 -device pcie-root-port,port=0x11,chassis=2,id=pci.2,bus=pcie.0,addr=0x2.0x1 -device pcie-root-port,port=0x12,chassis=3,id=pci.3,bus=pcie.0,addr=0x2.0x2 -device pcie-root-port,port=0x13,chassis=4,id=pci.4,bus=pcie.0,addr=0x2.0x3 -device pcie-root-port,port=0x14,chassis=5,id=pci.5,bus=pcie.0,addr=0x2.0x4 -device pcie-root-port,port=0x15,chassis=6,id=pci.6,bus=pcie.0,addr=0x2.0x5 -device pcie-root-port,port=0x16,chassis=7,id=pci.7,bus=pcie.0,addr=0x2.0x6 -device qemu-xhci,id=usb,bus=pci.2,addr=0x0 -device virtio-serial-pci,id=virtio-serial0,bus=pci.3,addr=0x0 -blockdev {"driver":"file","filename":"/var/lib/uvtool/libvirt/images/x-uvt-b64-Y29tLnVidW50dS5jbG91ZDpzZXJ2ZXI6MTguMDQ6YW1kNjQgMjAyMTExMjk=","node-name":"libvirt-3-storage","auto-read-only":true,"discard":"unmap"} -blockdev {"node-name":"libvirt-3-format","read-only":true,"driver":"qcow2","file":"libvirt-3-storage","backing":null} -blockdev {"driver":"file","filename":"/var/lib/uvtool/libvirt/images/test.qcow","node-name":"libvirt-2-storage","auto-read-only":true,"discard":"unmap"} -blockdev {"node-name":"libvirt-2-format","read-only":false,"driver":"qcow2","file":"libvirt-2-storage","backing":"libvirt-3-format"} -device virtio-blk-pci,scsi=off,bus=pci.4,addr=0x0,drive=libvirt-2-format,id=virtio-disk0,bootindex=1 -blockdev {"driver":"file","filename":"/var/lib/uvtool/libvirt/images/test-ds.qcow","node-name":"libvirt-1-storage","auto-read-only":true,"discard":"unmap"} -blockdev {"node-name":"libvirt-1-format","read-only":false,"driver":"qcow2","file":"libvirt-1-storage","backing":null} -device virtio-blk-pci,scsi=off,bus=pci.5,addr=0x0,drive=libvirt-1-format,id=virtio-disk1 -netdev tap,fd=34,id=hostnet0,vhost=on,vhostfd=35 -device virtio-net-pci,netdev=hostnet0,id=net0,mac=52:54:00:b6:4f:14,bus=pci.1,addr=0x0 -chardev pty,id=charserial0 -device isa-serial,chardev=charserial0,id=serial0 -chardev socket,id=charchannel0,fd=37,server,nowait -device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=org.qemu.guest_agent.0 -vnc 127.0.0.1:0 -spice port=5901,addr=127.0.0.1,disable-ticketing,seamless-migration=on -device qxl-vga,id=video0,ram_size=67108864,vram_size=67108864,vram64_size_mb=0,vgamem_mb=16,max_outputs=1,bus=pcie.0,addr=0x1 -device virtio-balloon-pci,id=balloon0,bus=pci.6,addr=0x0 -sandbox on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny -msg timestamp=on

```

便利機能をいろいろデフォルトで載せてくれているのですが、少々分かりづらいですね。

今回は手動で qemu-system-x86_64 バイナリを叩いて VM を起動することにします。

## QEMU を通信させるためには

QEMU のネットワークにはどんなものが使えるのか、以下のドキュメントに載っています。

https://wiki.qemu.org/Documentation/Networking

この中で、今回は Linux Advent Calendar ということで、Linux Bridge + Tap デバイスで通信してみようと思います。

## 手を動かす

### 作成する構成

今回は以下のような構成を作成します。

![Untitled presentation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387167/198a18b5-39f4-8aa7-56c6-4a2381a4ffcf.png)

Linux bridge に対して2本の veth が刺さっており、2台の VM が TAP デバイスでつながっている構成です。IP が２つほど飛んでいるのは趣味です。

この VM に IP をつけて、お互いに疎通できるかどうか試してみます。

### Bridge を作成する

Linux Bridge を作成します。みんな大好き brctl を使うなり、 iproute2 を使うなり、お好きにどうぞ。僕は iproute2 を使います。

```
$ sudo ip link add br-test type bridge
```

できました

```
11: br-test: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 42:91:b9:ae:3d:41 brd ff:ff:ff:ff:ff:ff
```

### TAP デバイスを作成する

作成します。ip tuntap コマンドで作成できます。

```
[owner@mobile]  ~
% sudo ip tuntap add dev tap-test0 mode tap
[owner@mobile]  ~
% sudo ip tuntap add dev tap-test1 mode tap
[owner@mobile]  ~
% ip a
12: tap-test0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether a2:e8:69:11:27:4e brd ff:ff:ff:ff:ff:ff
13: tap-test1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether f6:d8:f2:f1:be:71 brd ff:ff:ff:ff:ff:ff
```
できました。

### TAP デバイスを Bridge に接続する

```
ip link set tap-test0 master br-test
```


### TAP デバイスを指定して VM を起動する

このくらいオプションをつければ良さそうでした。 -machine に与えられる引数は環境によって異なると思います。

なお、TAP デバイスを操作するため管理者権限が必要です。

```
/usr/bin/qemu-system-x86_64 -machine pc-q35-focal,accel=kvm -netdev tap,id=mynet0,ifname=tap-test0,script=no,downscript=no -device e1000,netdev=mynet0
```

### イメージを作る

起動できそうということがわかったので、ブートイメージを作成します。

今回は cirros をバッキングファイルにした qcow2 イメージを作成して利用します。

```
qemu-img create -f qcow2 -b cirros-0.5.2-x86_64-disk.img test0.qcow2 2G
```

以下の引数をつけると、イメージを VM にアタッチできます。

```
-drive file=./test0.qcow2,index=0,format=qcow2
```

完成したコマンドは以下となります。

```
/usr/bin/qemu-system-x86_64 -machine pc-q35-focal,accel=kvm -netdev tap,id=mynet0,ifname=tap-test0,script=no,downscript=no -device e1000,netdev=mynet0 -drive file=./test0.qcow2,index=0,format=qcow2
```

### test0 に IP をつける

cirros でブートした VM にログインして、IP をつけます。

```
ip a add 10.50.0.3/24 dev eth0
```

### ping を飛ばしてみる

ping を飛ばして、tap の様子を tcpdump で見てみます。

```
% sudo tcpdump -i tap-test0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tap-test0, link-type EN10MB (Ethernet), capture size 262144 bytes
18:50:59.744654 ARP, Request who-has 10.50.0.4 tell 10.50.0.3, length 46
18:51:00.768584 ARP, Request who-has 10.50.0.4 tell 10.50.0.3, length 46
18:51:02.728406 ARP, Request who-has 10.50.0.4 tell 10.50.0.3, length 46
```

arp を飛ばしていることがわかります。

test1 に IP をつけて、L2 でつながっていたら通信できそうということがわかります。

### test1 に IP をつける

さあどうなるでしょうか？

```
ip a add 10.50.0.4/24 dev eth0
```

疎通しました！ :tada:

![Screenshot from 2021-12-18 19-05-44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387167/93693f9f-9bca-f777-8fa0-1ad396cfc38c.png)

## まとめ

API がよしなにやってくれる作業を手動で実行することにより、システムへの理解がより深まりますね。

NAT の仕組みを自分で作って、VM を外部疎通させるのも面白そうと思いました。

## 参考資料

https://i-beam.org/2020/03/01/go-modern-bootserver-02/
https://gist.github.com/extremecoders-re/e8fd8a67a515fee0c873dcafc81d811c


## おまけ

### kvm が使われているのか確認した

strace したところ、きちんと KVM を使っていそうということが確認できました。

kvm は大体の命令を ioctl システムコールで実行するため、これだけ見れば大丈夫です。

`KVM_CREATE_VM` なんていいですね。

```
% sudo strace -e ioctl /usr/bin/qemu-system-x86_64 -machine pc-q35-focal,accel=kvm -netdev tap,id=tap-test0
ioctl(12, KVM_GET_API_VERSION, 0)       = 12
ioctl(12, KVM_CHECK_EXTENSION, KVM_CAP_IMMEDIATE_EXIT) = 1
ioctl(12, KVM_CHECK_EXTENSION, KVM_CAP_NR_MEMSLOTS) = 509
ioctl(12, KVM_CHECK_EXTENSION, KVM_CAP_MULTI_ADDRESS_SPACE) = 2
ioctl(12, KVM_CREATE_VM, 0)             = 13
ioctl(13, KVM_CHECK_EXTENSION, KVM_CAP_NR_VCPUS) = 240
ioctl(12, KVM_CHECK_EXTENSION, KVM_CAP_MAX_VCPUS) = 288
ioctl(12, KVM_CHECK_EXTENSION, KVM_CAP_USER_MEMORY) = 1
ioctl(12, KVM_CHECK_EXTENSION, KVM_CAP_DESTROY_MEMORY_REGION_WORKS) = 1
ioctl(12, KVM_CHECK_EXTENSION, KVM_CAP_JOIN_MEMORY_REGIONS_WORKS) = 1
...
```

### ハマったこと

#### デフォルトの mac address がかぶる

tap-test1 では arp reply が返っているのに、tap-test0 では受け取っていない。。。

```
% sudo tcpdump -i tap-test1
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tap-test1, link-type EN10MB (Ethernet), capture size 262144 bytes
18:58:40.322845 ARP, Request who-has 10.50.0.4 tell 10.50.0.3, length 46
18:58:40.323759 ARP, Reply 10.50.0.4 is-at 52:54:00:12:34:56 (oui Unknown), length 46
18:58:41.347097 ARP, Request who-has 10.50.0.4 tell 10.50.0.3, length 46
18:58:41.347928 ARP, Reply 10.50.0.4 is-at 52:54:00:12:34:56 (oui Unknown), length 46
18:58:43.297178 ARP, Request who-has 10.50.0.4 tell 10.50.0.3, length 46
18:58:43.298373 ARP, Reply 10.50.0.4 is-at 52:54:00:12:34:56 (oui Unknown), length 46
```

デフォルトの mac address が設定されており、両方の VM が共に `52:54:00:12:34:56` だったので、変更したら疎通が確認できました。