代码文件为qemu-bridge-helper.c,负责完成QEMU TAP设备的创建和网桥相关配置。
QEMU程序在安装之后,网桥帮助程序默认安装到位于/usr/local/libexec目录下,可执行文件为qemu-bridge-helper。其默认创建名称为br0(见宏定义DEFAULT_BRIDGE_INTERFACE)的网桥设备。
#define CONFIG_QEMU_HELPERDIR "/usr/local/libexec"
#define DEFAULT_BRIDGE_HELPER CONFIG_QEMU_HELPERDIR "/qemu-bridge-helper"
#define DEFAULT_BRIDGE_INTERFACE "br0"
#define CONFIG_QEMU_CONFDIR "/usr/local/etc/qemu"
#define DEFAULT_ACL_FILE CONFIG_QEMU_CONFDIR "/bridge.conf"
由其帮助可知,两个必须的参数为网桥的名称和一个UNIX类型套接口的描述符。
$ qemu-bridge-helper --help
Usage: qemu-bridge-helper [--use-vnet] --br=bridge --fd=unixfd
QEMU调用部分
在QEMU代码中,函数net_init_bridge初始化与帮助程序交互的参数,并且调用net_bridge_run_helper执行帮助函数。
int net_init_bridge(const Netdev *netdev, const char *name, NetClientState *peer, Error **errp)
{
const NetdevBridgeOptions *bridge;
const char *helper, *br;
assert(netdev->type == NET_CLIENT_DRIVER_BRIDGE);
bridge = &netdev->u.bridge;
helper = bridge->has_helper ? bridge->helper : DEFAULT_BRIDGE_HELPER;
br = bridge->has_br ? bridge->br : DEFAULT_BRIDGE_INTERFACE;
fd = net_bridge_run_helper(helper, br, errp);
fcntl(fd, F_SETFL, O_NONBLOCK);
vnet_hdr = tap_probe_vnet_hdr(fd);
s = net_tap_fd_init(peer, "bridge", name, fd, vnet_hdr);
}
函数net_bridge_run_helper执行具体的helper调用,其将使用fork创建一个子进程执行helper。 首先创建两个UNIX类型的套接口(socketpair)用于qemu与helper两个进程之间的通信,父进程QEMU使用sv[0]套接口,sv[1]由子进程helper使用。
int sv[2];
if (socketpair(PF_UNIX, SOCK_STREAM, 0, sv) == -1) {
error_setg_errno(errp, errno, "socketpair() failed");
return -1;
}
/* try to launch bridge helper */
pid = fork();
对于子进程来说,其关闭了所有从父进程继承来的描述符,仅保留UNIX套接口描述符sv[1],并且将sv[1]作为helper程序的--fd=的参数值传递。另外一个参数--br=使用net_bridge_run_helper的第二个参数赋值,默认为br0。由以下代码可见,QEMU默认有--use-vnet选项。
if (pid == 0) {
int open_max = sysconf(_SC_OPEN_MAX), i;
for (i = 3; i < open_max; i++) {
if (i != sv[1])
close(i);
}
snprintf(fd_buf, sizeof(fd_buf), "%s%d", "--fd=", sv[1]);
if (strrchr(helper, ' ') || strrchr(helper, '\t')) {
} else {
snprintf(br_buf, sizeof(br_buf), "%s%s", "--br=", bridge);
parg = args;
*parg++ = (char *)helper;
*parg++ = (char *)"--use-vnet";
*parg++ = fd_buf;
*parg++ = br_buf;
*parg++ = NULL;
execv(helper, args);
}
}
对于父进程来说,其关闭子进程使用的UNIX套接口sv[1],之后轮询UNIX套接口sv[0],等待helper子进程返回执行结果,随后关闭UNIX套接口,并且使用waitpid等待子进程结束。
else {
int fd;
int saved_errno;
close(sv[1]);
do {
fd = recv_fd(sv[0]);
} while (fd == -1 && errno == EINTR);
saved_errno = errno;
close(sv[0]);
while (waitpid(pid, &status, 0) != pid) {
/* loop */
}
return fd;
}
网桥帮助程序
再来看一下qemu-bridge-helper帮助程序的主函数。其在执行任何操作之前,先检查访问控制文件DEFAULT_ACL_FILE是否允许操作此网桥,位于目录/usr/local/etc/qemu/,文件名bridge.conf。只有在ACL_ALLOW_ALL后者明确允许此网桥ACL_ALLOW的条件下,并且没有拒绝的ACL控制条件下,才允许继续操作。
if (parse_acl_file(DEFAULT_ACL_FILE, &acl_list) == -1) {
fprintf(stderr, "failed to parse default acl file `%s'\n",
DEFAULT_ACL_FILE);
ret = EXIT_FAILURE;
goto cleanup;
}
QSIMPLEQ_FOREACH(acl_rule, &acl_list, entry) {
switch (acl_rule->type) {
case ACL_ALLOW_ALL:
access_allowed = 1;
break;
case ACL_ALLOW:
if (strcmp(bridge, acl_rule->iface) == 0) {
access_allowed = 1;
}
break;
}
接下来,创建TAP设备,名称指定为tap%d,由内核决定其索引值,例如第一个TAP设备为tap0。随后指定其标志IFF_TAP确定为TAP设备,IFF_NO_PI标志标明不需要传输数据包信息(Packet Info),由于在之前的父进程中固定了--use-vnet选项,此处,如果支持vnet头部的话,指定IFF_VNET_HDR标明需要传输virtio头部信息。
fd = open("/dev/net/tun", O_RDWR);
prep_ifreq(&ifr, "tap%d");
ifr.ifr_flags = IFF_TAP|IFF_NO_PI;
if (use_vnet && has_vnet_hdr(fd)) {
ifr.ifr_flags |= IFF_VNET_HDR;
}
ioctl(fd, TUNSETIFF, &ifr);
接下来,获取到网桥的MTU数值,并将其设置给创建的TAP设备。
/* get the mtu of the bridge */
prep_ifreq(&ifr, bridge);
ioctl(ctlfd, SIOCGIFMTU, &ifr);
/* save mtu */
mtu = ifr.ifr_mtu;
/* set the mtu of the interface based on the bridge */
prep_ifreq(&ifr, iface);
ifr.ifr_mtu = mtu;
ioctl(ctlfd, SIOCSIFMTU, &ifr);
由于网桥的MAC地址取自其子接口中MAC地址最小的接口, 为了不影响网桥目前的MAC地址,将目前TAP设备的MAC地址的第一个字节赋值为0xFE。
ioctl(ctlfd, SIOCGIFHWADDR, &ifr);
ifr.ifr_hwaddr.sa_data[0] = 0xFE;
ioctl(ctlfd, SIOCSIFHWADDR, &ifr);
将TAP设备设置为网桥的一个子接口,由ioctl系统调用SIOCBRADDIF实现。
/* add the interface to the bridge */
prep_ifreq(&ifr, bridge);
ifindex = if_nametoindex(iface);
ifr.ifr_ifindex = ifindex;
ret = ioctl(ctlfd, SIOCBRADDIF, &ifr);
将TAP设备设置为UP状态。
/* bring the interface up */
prep_ifreq(&ifr, iface);
ioctl(ctlfd, SIOCGIFFLAGS, &ifr);
ifr.ifr_flags |= IFF_UP;
ioctl(ctlfd, SIOCSIFFLAGS, &ifr);
最后,将创建的TAP设备的文件描述符fd通过以参数传递进来的UNIX套接口发送给父进程。
/* write fd to the domain socket */
send_fd(unixfd, fd);
后记
在父进程QEMU接收到新创建的TAP设备的文件描述符之后,net_init_bridge函数调用net_tap_fd_init进行此fd在QEMU中的初始化。qemu_new_net_client函数创建网络客户端结构,及其相关参数。
static TAPState *net_tap_fd_init(NetClientState *peer, const char *model, const char *name, int fd, int vnet_hdr)
{
NetClientState *nc;
TAPState *s;
nc = qemu_new_net_client(&net_tap_info, peer, model, name);
s = DO_UPCAST(TAPState, nc, nc);
s->fd = fd;
tap_read_poll(s, true);
return s;
}
函数tap_read_poll使能轮询读操作,注册tap_send函数接收TAP设备的数据包。tap_send函数随后将读取到的数据包挂载到之前创建的网络客户端结构(NetClientState)的成员incoming_queue所定义的队列上。
static void tap_update_fd_handler(TAPState *s)
{
qemu_set_fd_handler(s->fd,
s->read_poll && s->enabled ? tap_send : NULL,
s->write_poll && s->enabled ? tap_writable : NULL,
s);
}
static void tap_read_poll(TAPState *s, bool enable)
{
s->read_poll = enable;
tap_update_fd_handler(s);
}
QEMU版本:qemu-3.1.0