Switch-Router

理解 macvlan

Published at 2019-11-02 | Last Update 2019-11-02

macvlan 是什么 ?

macvlan 是 Linux 内核 3.0 版本引入的一种虚拟网卡,它让用户可以在一块物理网卡上,再创建若干个依附于它的虚拟网卡,这些网卡拥有不同的 MAC 地址,如此就完成了网卡的虚拟化。

macvlan 应用在哪里呢? 答案是:macvlan 可以轻松地解决虚拟机之间或者容器之间的网络通信问题。

我们可以假设这样一种场景:一台宿主机上存在两个容器,它们属于不同的 namespace ,容器间有通信需求(互相通信和与宿主机外部通信)。可以怎么做呢? veth + bridge 是一种解决方案,我们可以创建两对 veth pair 和一个 bridge,然后将每对 veth pair 的一端扔进容器的 namespace,另一端挂到 bridge 上,像 Figure-1 这样:

Figure.1 Communication between containers with veth and bridge

bridge 在图中起到了中转报文的作用,所有<容器-容器>,<容器-外部>的流量都会经过 bridge。

而使用 macvlan 的话,这种场景可以简化为 Figure-2, 图中 mac1 和 mac2 是依附于 eth 创建的 macvlan 虚拟网卡

Figure.2 Communication between containers with macvlan

macvlan 的模式

macvlan 根据网卡间的互通性分为以下几种模式:

Figure.3 Four mode macvlan(from dog250)

bridge 模式

beidge 模式下,物理网卡可以作为供 macvlan 虚拟网卡通信。但需要注意,macvlan 虚拟网卡与物理网卡不能通信( macvlan 所有模式均不支持),不能通信意味着网卡上方的协议栈不能互通,即你不能从容器 ping 通宿主机,反之也一样。

VEPA 模式

VEPA 模式与 bridge 模式相比,macvlan 虚拟网卡之间也不能在宿主机内部互通了!但它们还可以借助外部工作在 Hairpin 模式(发夹弯模式)的交换机完成通信, 工作在发夹弯模式的交换机在收到广播报文或者未知单播报文时,会向所有端口(不会排除报文入接口)发送报文。不仅是 macvlan 虚拟网卡之间, macvlan 虚拟网卡与物理网卡之间也能通过外部 Hairpin 交换机通信。

private 模式

private 模式正如其名字,意味着该模式是”私密”的,在这种模式下,即使外部有工作在 Hairpin 模式的交换机,macvlan 虚拟网卡之间也不能通信。此时,从macvlan 虚拟网卡发出的广播报文经过外部 Hairpin 模式的交换机 echo 回来,只是会送回自己。

passthrou 模式

passthrou, 即直通,此模式下,某一个 macvlan 虚拟网卡会接管物理网卡,它将独占物理网卡。

macvlan 的实现

作为一种虚拟网卡实现, macvlan 以驱动模块的形式存在于内核源码中,具体请看 /driver/net/macvlan.c

创建 macvlan 虚拟网卡

用户通过输入 ip link add link eth0 name macv1 type macvlan mode bridge 添加 macvlan 虚拟网卡。iproute 将名字、模式等信息打包成 Netlink 消息下发到内核。内核,调用 macvlan_common_newlink() 创建

static int rtnl_newlink(struct sk_buff *skb, struct nlmsghdr *nlh)
{
    ......
    ops = rtnl_link_ops_get(kind); // 会得到 macvlan_link_ops
    
    // 创建 macvlan 虚拟网卡的 net_device, 私有部分为 struct macvlan_dev
    dev = rtnl_create_link(link_net ? : dest_net, ifname, name_assign_type, ops, tb);
     |
     |- dev = alloc_netdev_mqs(ops->priv_size, ifname, name_assign_type,
			       ops->setup, num_tx_queues, num_rx_queues);
                   
    ops->newlink(link_net ? : net, dev, tb, data);
      |
      |- macvlan_common_newlink(src_net, dev, tb, data);
         |
         |  // lowerdev 是依附的物理网卡 eth0
         |- lowerdev = __dev_get_by_index(src_net, nla_get_u32(tb[IFLA_LINK]));
         |
         |  // 随机分配 macvlan 网卡的 MAC 地址
         |- if (...) eth_hw_addr_random(dev);
         |
         |  // 如果 eth 还没有创建过 macvlan 网卡, 就创建
         |- if (...) macvlan_port_create(lowerdev)
            |
            | // 初始化保存 macvlan 网卡MAC地址的 HASH 表
            |- INIT_HLIST_HEAD(&port->vlan_hash[i]);
            |
            |  // 设置物理网卡 eth0 的 rx_handler 为 macvlan_handle_frame
            |- netdev_rx_handler_register(dev, macvlan_handle_frame, port);
         |
         |   // 将 macvlan 设备注册到系统
         |-  register_netdevice(dev);
}

上面的代码很好懂,最重要的就是在用户第一次为 eth0 创建 macvlan 虚拟网卡时,内核会设置 rx_handler 为 macvlan 特定的接收处理函数。

macvlan 虚拟网卡接收报文

从宿主机外发给 macvlan 虚拟网卡的网卡的报文总是先经过物理网卡 eth0,在 eth0 的接收过程中,会调用到创建 macvlan 虚拟网卡时注册的 macvlan_handle_frame,这里面将报文分为了多播(广播)和单播报文分别处理,先来看多播报文的处理

static rx_handler_result_t macvlan_handle_frame(struct sk_buff **pskb)
{
    port = macvlan_port_get_rcu(skb->dev);
	if (is_multicast_ether_addr(eth->h_dest)) {		
		*pskb = skb;
		eth = eth_hdr(skb);

        // 根据报文的 源MAC 查找是否是某个虚拟网卡的 MAC 地址
		src = macvlan_hash_lookup(port, eth->h_source);
		if (src && src->mode != MACVLAN_MODE_VEPA &&
		    src->mode != MACVLAN_MODE_BRIDGE) {
			
            // 如果找到且它是 private 或者 passthrou 模式,则将其送给该端口 
			vlan = src;
			ret = macvlan_broadcast_one(skb, vlan, eth, 0) ?: netif_rx(skb);
            
            // 返回 CONSUMED 是告诉外面该报文已经"处理过"了
			handle_res = RX_HANDLER_CONSUMED;
			goto out;
		}

        // 如果没有找到对应的虚拟网卡 或者 其模式为 bridge 或者 vepa,则将该报文广播给所有虚拟网卡
		MACVLAN_SKB_CB(skb)->src = src;
		macvlan_broadcast_enqueue(port, skb);

        // 返回 PASS 是告诉外面 这个包还可以继续处理,也就是还可以给 eth0 上方的协议栈
		return RX_HANDLER_PASS;
	}  
    ......
}

再看是单播报文的情况

static rx_handler_result_t macvlan_handle_frame(struct sk_buff **pskb)
{
    ...
    // 单播报文的处理
    if (port->passthru)
		vlan = list_first_or_null_rcu(&port->vlans,
					      struct macvlan_dev, list);
	else
        // 根据报文的 目的MAC 看是否是应该某个 macvlan 虚拟网卡接收
		vlan = macvlan_hash_lookup(port, eth->h_dest);
	if (vlan == NULL)
        // 不是的话 返回 PASS,让 eth0 的协议栈处理
		return RX_HANDLER_PASS;
    
    // 如果是,则修改报文的入设备为 macvlan 虚拟网卡
    dev = vlan->dev;
    skb->dev = dev;
    
    // 返回 ANOTHER 让调用者重新走一次接收匹配过程
    handle_res = RX_HANDLER_ANOTHER;
    return handle_res;
}

macvlan 虚拟网卡发送报文

macvlan 虚拟网卡的发送比较简单,只有 bridge 模式有一个特别处理:广播报文需要发送给其他“兄弟”虚拟网卡,当然它们也必须是 bridge 模式,单播报文如果能找到确定的目标,就直接回路到接收过程。

只要不是单播并且找到了目标的情况,该报文最终都会修改报文出设备为物理网卡 eth0 后,由物理网卡发送出去。

static int macvlan_queue_xmit(struct sk_buff *skb, struct net_device *dev)
{
	const struct macvlan_dev *vlan = netdev_priv(dev);
	const struct macvlan_port *port = vlan->port;
	const struct macvlan_dev *dest;

	if (vlan->mode == MACVLAN_MODE_BRIDGE) {
		const struct ethhdr *eth = (void *)skb->data;

		// 广播报文: 广播给其他同样依附于 eth0 且是 bridge 模式的 macvlan 虚拟网卡 
		if (is_multicast_ether_addr(eth->h_dest)) {
			macvlan_broadcast(skb, port, dev, MACVLAN_MODE_BRIDGE);
			goto xmit_world;
		}
        
        // 单播报文如果能找到确切的 macvlan 虚拟网卡 就直接走接收过程
		dest = macvlan_hash_lookup(port, eth->h_dest);
		if (dest && dest->mode == MACVLAN_MODE_BRIDGE) {
			dev_forward_skb(vlan->lowerdev, skb);
			return NET_XMIT_SUCCESS;
		}
	}

xmit_world:
	skb->dev = vlan->lowerdev;
	return dev_queue_xmit(skb);
}

REF