ubuntu中qemu启动多个guest虚拟机,guest如何访问外网?guest之间如何实现网络通信?我常用qemu调试自己编译的kernel,需要在guest和host之间共享一些调试文件,如何达到这个目的?
开发人员经常遇到环境差异(ubuntu宿主机、自己编译的kernel config、rootfs、qemu等等因素)导致"别人成功的经验”不能复制到自己机器上,所以有必要深入了解一下qemu的网络是怎么实现的,遇到问题才能快速解决。
参考ubuntu20.04 搭建kernel调试环境第六篇(上)--网络配置_geshifei的博客-CSDN博客_qemu 网络配置
libvirt是一套管理虚拟化平台的工具集(官网https://libvirt.org/),支持qemu等多种虚拟化平台。这是本文的主角,我们用它的网络模块实现网络通讯。
安装命令:
1)apt install bridge-utils
2)apt install uml-utilities
3)apt install libvirt-daemon-system
开机后,libvirt启动deamon进程/usr/sbin/libvirtd在ubuntu在宿主机中创建一个虚拟网桥virbr0,并在网桥中创建一个虚拟网卡virbr0-nic。
root@linux:/home/gsf# brctl show
bridge name bridge id STP enabled interfaces
virbr0 8000.5254005ec9bb yes virbr0-nic
virbr0用于桥接多个虚拟机,配合libvirt生成的NAT规则,可实现guest访问外网、其他机器。
virbr0-nic上并没有网络流量,引入它只是为了让virbr0有个固定的mac,virbr0的mac是从virbr0-nic复制过来的。这么做是因为内核的一个行为导致的,内核会拷贝网桥下的第一块网卡的mac做为网桥的mac,所以如果virbr0网卡有变化时,则会导致virbr0的mac地址也随着变化,导致一段时间内网络异常,所以增加一个virbr0-nic来固定virbr0的mac。
ubuntu中启动两个虚拟机后的信息:
root@linux:/home/gsf# brctl show
bridge name bridge id STP enabled interfaces
virbr0 8000.5254005ec9bb yes tap0
tap1
virbr0-nic
网桥名称、mac、ip定义在/var/lib/libvirt/dnsmasq/default.conf。文件内容如下。
注意,不要直接修改该文件(重启后会丢失),而应用virsh net-edit default命令来修改。
root@linux:/home/gsf# cat /var/lib/libvirt/dnsmasq/default.conf
##WARNING: THIS IS AN AUTO-GENERATED FILE. CHANGES TO IT ARE LIKELY TO BE
##OVERWRITTEN AND LOST. Changes to this configuration should be made using:
## virsh net-edit default
## or other application using the libvirt API.
##
## dnsmasq conf file created by libvirt
strict-order
user=libvirt-dnsmasq
pid-file=/run/libvirt/network/default.pid
except-interface=lo
bind-dynamic
interface=virbr0
dhcp-range=192.168.122.2,192.168.122.254,255.255.255.0
dhcp-no-override
dhcp-authoritative
dhcp-lease-max=253
dhcp-hostsfile=/var/lib/libvirt/dnsmasq/default.hostsfile
addn-hosts=/var/lib/libvirt/dnsmasq/default.addnhosts
root@linux:/home/gsf#
用buildroot编译rootfs时,可以指定eth0通过DHCP获取ip地址(参考ubuntu20.04 搭建kernel调试环境第六篇(上)--网络配置)。qemu启动后,可以查看/etc/network/interfaces文件:
# cat /etc/network/interfaces
# interface file auto-generated by buildroot
auto lo
iface lo inet loopback
# 指定DHCP方式
auto eth0
iface eth0 inet dhcp
pre-up /etc/network/nfs_check
wait-delay 15
hostname $(hostname)
#
guset申请ip地址时,宿主机libvird通过dnsmasq给guest分配ip地址。
root@linux:/home/gsf# ps -aux |grep libvirt
root …… /usr/sbin/libvirtd
libvirt+ …… /usr/sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/lib/libvirt/libvirt_leaseshelper
文件default.conf(见上)字段dhcp-range=192.168.122.2,192.168.122.254,255.255.255.0指定了DHCP地址池范围,所以如果我在ubuntu host机器中启动两个guest虚拟机,dnsmasq将给他们分配类似于192.168.122.76、192.168.122.78这样的地址。
guest与host机器之间是怎么进行DHCP交互的,后文说明。
如果buildroot编译rootfs时没有指定eth0为DHCP,就需要通过静态方式获取ip了。qemu启动后:
# cat /etc/network/interfaces
# interface file auto-generated by buildroot
auto lo
iface lo inet loopback
因为interfaces文件中只有lo接口,所以ifconfig是看不到eth0的:
# ifconfig
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
我们需要配置静态ip、dns:
在虚拟机中执行命令
# vi /etc/network/interfaces
# interface file auto-generated by buildroot
auto lo
iface lo inet loopback
# 增加eth0配置
iface eth0 inet static
address 192.168.122.16
netmask 255.255.255.0
# 设置dns
dns-nameservers 192.168.122.1
在/etc/resolv.conf中增加一行:
nameserver 192.168.122.1 # eth0
在我的系统中/etc/resolv.conf -> /tmp/resolv.conf,tmp是tmpfs,所以重启会丢失。
执行ifup eth0,ifconfig就能看到eth0接口了,并且ping其他的guest机器、外网都是正常的。
# ifconfig
eth0 Link encap:Ethernet HWaddr 52:54:00:12:34:59
inet addr:192.168.122.16 Bcast:0.0.0.0 Mask:255.255.255.0
inet6 addr: fe80::5054:ff:fe12:3459/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:3 errors:0 dropped:0 overruns:0 frame:0
TX packets:21 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:252 (252.0 B) TX bytes:1665 (1.6 KiB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:12 errors:0 dropped:0 overruns:0 frame:0
TX packets:12 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:876 (876.0 B) TX bytes:876 (876.0 B)
# ping www.baidu.com
PING www.baidu.com (180.101.49.13): 56 data bytes
64 bytes from 180.101.49.13: seq=0 ttl=51 time=13.801 ms
64 bytes from 180.101.49.13: seq=1 ttl=51 time=10.078 ms
# ping 192.168.122.76
PING 192.168.122.76 (192.168.122.76): 56 data bytes
64 bytes from 192.168.122.76: seq=0 ttl=64 time=9.660 ms
64 bytes from 192.168.122.76: seq=1 ttl=64 time=2.889 ms
guest ip有了,那guest的mac又是怎么来的?答案是,qemu启动时,人为指定的。
qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -drive file=rootfs.f2fs,if=ide,format=raw,id=myid0 --nographic -append "root=/dev/sda console=ttyS0" -hdb ext4.img -net nic,macaddr=32:54:00:11:34:51,model=e1000 -net bridge,id=net0,helper=/usr/lib/qemu/qemu-bridge-helper,br=virbr0
注意macaddr第一个字节不能是基数(基数是组播地址),比如示例中32字段不能改成31。
有了ip、mac,就具备了网络通信的条件,下文描述通讯细节。
以ubuntu宿主机中启动2个qemu虚拟机为例(qemu参数差别在于指定的nic mac不一样),执行ping 114.114.114.114来说明。
启动虚拟机:
虚拟机1:
qemu-system-x86_64 -kernel bzImage -drive file=rootfs.f2fs,if=ide,format=raw,id=myid0 --nographic -append "root=/dev/sda console=ttyS0" -hdb ext4.img -net nic,macaddr=32:24:01:11:34:55,model=e1000 -net bridge,id=net0,helper=/usr/lib/qemu/qemu-bridge-helper,br=virbr0
虚拟机2:
qemu-system-x86_64 -kernel bzImage -drive file=rootfs.f2fs,if=ide,format=raw,id=myid0 --nographic -append "root=/dev/sda console=ttyS0" -hdb ext4.img -net nic,macaddr=32:24:01:11:34:56,model=e1000 -net bridge,id=net0,helper=/usr/lib/qemu/qemu-bridge-helper,br=virbr0
参数 -net nic,创建一个虚拟网络接口eth0,qemu本质上就是一个用户态程序,所以eth0并不是真正的网络接口,而是模拟出来的nic。eth0流量需要借助于linux的tun/tap设备驱动来实现(tun/tap设备可以理解成虚拟网卡,功能类似物理网卡)。
qemu启动时会在ubuntu宿主机创建tap0设备,并将将tap设备加入到虚拟网桥virbr0,同时还会将eth0与tap0关联起来,用户态eth0的网络访问通过read/write tap设备来做,所以多个guest之间可以经virbr0进行网络通讯。
libvirt基于iptables在宿主机中做了NAT(net address translation)规则,将souce ip是192.168.122.0/24的数据包(即guest机器发出的数据包,具体ip地址取决于网桥ip),用MASQUERADE这个target将source ip替换成宿主机物理网卡ip地址。这样数据包看起来就像是宿主机发出的数据包一样,宿主机收到响应数据包时,根据连接跟踪表,决定响应的数据包发给哪个guest。具体说来,一个访问外网的数据包处理流程如下:
1)nat表postrouting链(iptables的四表五链,见最后附图),MASQUERADE将数据包的源ip(192.168.122.17)替换成宿主机物理网卡ip(10.233.11.5)
root@linux:/home/gsf# iptables -nvL -t nat
Chain PREROUTING (policy ACCEPT 1340 packets, 354K bytes)
pkts bytes target prot opt in out source destination
Chain POSTROUTING (policy ACCEPT 159K packets, 11M bytes)
pkts bytes target prot opt in out source destination
159K 11M LIBVIRT_PRT all -- * * 0.0.0.0/0 0.0.0.0/0
Chain LIBVIRT_PRT (1 references)
pkts bytes target prot opt in out source destination
273 19121 RETURN all -- * * 192.168.122.0/24 224.0.0.0/24
0 0 RETURN all -- * * 192.168.122.0/24 255.255.255.255
0 0 MASQUERADE tcp -- * * 192.168.122.0/24 !192.168.122.0/24 masq ports: 1024-65535
0 0 MASQUERADE udp -- * * 192.168.122.0/24 !192.168.122.0/24 masq ports: 1024-65535
28 2352 MASQUERADE all -- * * 192.168.122.0/24 !192.168.122.0/24
root@linux:/home/gsf#
在guest虚拟机中执行ping 114.114.114.114,分别在tap0口和物理网卡口抓包,可以看到从tap0口出来的数据包,源ip被替换成了物理网卡ip:
tap0口数据包(源ip是192.168.122.17):
物理网卡口数据包(源ip是宿主机物理网卡的ip 10.223.11.5):
响应数据包达到宿主机时,宿主机根据连接跟踪表,将响应包转给对应的请求者,即转给字段src=192.168.122.17指定的接收者:
root@linux:/home/gsf# conntrack -L --any-nat
icmp 1 29 src=192.168.122.17 dst=114.114.114.114 type=8 code=0 id=39169 src=114.114.114.114 dst=10.223.11.5 type=0 code=0 id=39169 mark=0 use=1
conntrack v1.4.5 (conntrack-tools): 1 flow entries have been shown.
root@linux:/home/gsf#
guest机器运行的是linux系统,所以它的网络初始化流程与我们常见的linux系统大同小异。
开机执行/etc/init.d/rcS脚本,这个脚本按数字大小执行S开头的脚本,本例中执行S40network
# cat S40network
#!/bin/sh
#
# Start the network....
#
# Debian ifupdown needs the /run/network lock directory
mkdir -p /run/network
case "$1" in
start)
printf "Starting network: "
/sbin/ifup -a
[ $? = 0 ] && echo "OK" || echo "FAIL"
;;
stop)
printf "Stopping network: "
/sbin/ifdown -a
[ $? = 0 ] && echo "OK" || echo "FAIL"
;;
restart|reload)
"$0" stop
"$0" start
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
esac
exit $?
开机时执行/sbin/ifup -a,ifup是buildroot编译busybox生成的可执行程序,用来激活指定的网络接口。ifup命令读取/etc/network/interfaces文件,激活文件配置的网络接口。
(图片来源自网络)
1,为了让guest访问外网,需要guest、host在同一个网段中吗?
答:不需要。
libvirt在宿主机插入了一下iptables规则,通过NAT将来自guest的数据包源ip替换成了物理网卡ip,这个数据包就可以发到外网了。
2,guset可以访问samba服务器吗?
答:可以。
参考ubuntu20.04 搭建kernel调试环境第六篇(上)--网络配置_geshifei的博客-CSDN博客_qemu 网络配置
3,buildroot的默认网络配置脚本(/etc/network/interfaces)是怎么生成的?
答:buildroot-2020.02.8/package/ifupdown-script/fupdown-scripts.mk生成
4,为什么修改了buildroot-2020.02.8/package/ifupdown-script,重新编译buildroot,ifupdown-script改动不生效?
答:删除buildroot-2020.02.8/output/build/ifupdown-scripts目录,再重新编译即可。
buildroot没有根据文件修改时间来判断是否重新编译,只要buildroot-2020.02.8/output/build目录中有对应的package,就不会重新编译。
5,为什么conntrack -L --any-nat看的是空的?
答:guest无网络请求。执行ping后试试。