IPVlan 源码探秘

1,965 阅读12分钟

ipvlan是Linux提供的一个内核网络虚拟化的机制,最近在支持集团内某些业务的时候直接使用了ipvlan的网络方案,在解决问题和方案设计的时候也深入看了一下这块源码,看了一下并不复杂,索性写下来记录一下,以飨读者。

本文会省略一些具体的模拟环境的测试结论,因此更适合有ipvlan使用和调戏经验的同学阅读。

总论

ipvlan的使用体验实际上跟bridge是比较接近的,在配置好子网卡和IP地址后,就可以借助underlay网络互通了,在Linux Howto上有配置ipvlan的详细指引:

  +=============================================================+
  |  Host: host1                                                |
  |                                                             |
  |   +----------------------+      +----------------------+    |
  |   |   NS:ns0             |      |  NS:ns1              |    |
  |   |                      |      |                      |    |
  |   |                      |      |                      |    |
  |   |        ipvl0         |      |         ipvl1        |    |
  |   +----------#-----------+      +-----------#----------+    |
  |              #                              #               |
  |              ################################               |
  |                              # eth0                         |
  +==============================#==============================+


	(a) Create two network namespaces - ns0, ns1
		ip netns add ns0
		ip netns add ns1

	(b) Create two ipvlan slaves on eth0 (master device)
		ip link add link eth0 ipvl0 type ipvlan mode l2
		ip link add link eth0 ipvl1 type ipvlan mode l2

	(c) Assign slaves to the respective network namespaces
		ip link set dev ipvl0 netns ns0
		ip link set dev ipvl1 netns ns1

	(d) Now switch to the namespace (ns0 or ns1) to configure the slave devices
		- For ns0
			(1) ip netns exec ns0 bash
			(2) ip link set dev ipvl0 up
			(3) ip link set dev lo up
			(4) ip -4 addr add 127.0.0.1 dev lo
			(5) ip -4 addr add $IPADDR dev ipvl0
			(6) ip -4 route add default via $ROUTER dev ipvl0
		- For ns1
			(1) ip netns exec ns1 bash
			(2) ip link set dev ipvl1 up
			(3) ip link set dev lo up
			(4) ip -4 addr add 127.0.0.1 dev lo
			(5) ip -4 addr add $IPADDR dev ipvl1
			(6) ip -4 route add default via $ROUTER dev ipvl1

具体说来,每一个ipvlan网卡都会挂在一个父网卡上面,同一个父网卡下面挂的ipvlan默认是三层互通的(除非配置了private模式)并且使用相同的mac地址,而不同父网卡之间的子网卡则在ipvlan这一层相互隔离,从而形成了一种类似于vlan的隔离机制,因此取名叫做ipvlan。

但是这种隔离的关系仅仅是在Linux系统内部生效的,报文在出ipvlan子网卡之后会因ipvlan的各种配置和外部网络因素形成各种复杂的关系,本文就先从源码角度入手看一下ipvlan的内部实现,再配合外部网络常见的各种环境,对ipvlan可能的业务场景和技术方案做一些分析。

ipvlan基本的转发思路以每个父网卡作为一个隔离域配置一个hash表,每个子网卡在更新IP的时候把IP地址注册到这个hash表中,ipvlan再转发流程里面以这个ip-网卡之间的hash作为转发依据。

L2模式

对于L2模式而言,其与L3模式的主要区别在于这种模式下,还是保留了一些二层相关的流程到达ipvlan子网卡这里的,主要包括ARP流程和广播报文。

话不多说,我们可以先看下对于L2模式而言的关键转发逻辑:

static int ipvlan_xmit_mode_l2(struct sk_buff *skb, struct net_device *dev)
{
	const struct ipvl_dev *ipvlan = netdev_priv(dev);
	struct ethhdr *eth = eth_hdr(skb);
	struct ipvl_addr *addr;
	void *lyr3h;
	int addr_type;

	if (!ipvlan_is_vepa(ipvlan->port) &&
	    ether_addr_equal(eth->h_dest, eth->h_source)) {
		lyr3h = ipvlan_get_L3_hdr(ipvlan->port, skb, &addr_type);
		if (lyr3h) {
			addr = ipvlan_addr_lookup(ipvlan->port, lyr3h, addr_type, true);
			if (addr) {
				if (ipvlan_is_private(ipvlan->port)) {
					consume_skb(skb);
					return NET_XMIT_DROP;
				}
				return ipvlan_rcv_frame(addr, &skb, true);
			}
		}
		skb = skb_share_check(skb, GFP_ATOMIC);
		if (!skb)
			return NET_XMIT_DROP;

		/* Packet definitely does not belong to any of the
		 * virtual devices, but the dest is local. So forward
		 * the skb for the main-dev. At the RX side we just return
		 * RX_PASS for it to be processed further on the stack.
		 */
		return dev_forward_skb(ipvlan->phy_dev, skb);

	} else if (is_multicast_ether_addr(eth->h_dest)) {
		ipvlan_skb_crossing_ns(skb, NULL);
		ipvlan_multicast_enqueue(ipvlan->port, skb, true);
		return NET_XMIT_SUCCESS;
	}

	skb->dev = ipvlan->phy_dev;
	return dev_queue_xmit(skb);
}

兄弟网卡间的转发逻辑

这段代码比较清晰的描述了对于L2模式的转发逻辑,在不考虑vepa模式的情况下(这个模式会在4.1这个部分简单讲下),会首先判断一下报文的源mac和目的mac是否一致,如果一致则认为是ipvlan内部转发转流程,判断是否有三层头,有三层头自然就是带有IP层的报文,然后就会寻找挂在这个父网卡下的目的IP所在的子网卡,直接调用该网卡的ipvlan_rcv_frame函数,送到对应的子网卡下。

跨主机的转发逻辑

实际上对于ipvlan而言,还有一种重要场景就是跨linux之间的转发,对于这种情况,l2模式采用的方法是简单粗暴的:直接调用skb->dev = ipvlan->phy_dev; dev_queue_xmit(skb);通过父网卡直接送出去。

\

对于多播报文的处理

多播报文的处理其实也不需要太多关注,对于多播报文而言,只要ipvlan发现目的mac是一个多播地址,就会直接把他塞到backlog队列里去做转发。

一些衍生的问题

这里面有几个比较值得玩味的细节:

  • 为什么源mac和目的mac地址一致就认为是ipvlan内部转发流程呢?
    • 因为ipvlan这个神奇的模式,ipvlan子网卡会继承父网卡的mac地址,这也是他和macvlan模式的明显区别之一,所以兄弟网卡之间的mac地址必然是相同的。
  • l2模式在跨主机转发的场景下是直接使用父网卡link的dev_queue_xmit发送的,bypass了所有的三四层的协议栈,这也就意味着ipvlan子网卡和父网卡在二层上是隔离的,和宿主机上的其他网卡更加是隔离的,这可能会给某些需要父子网卡互通的场景带来一些奇妙的麻烦,当然,你也不能指望L3模式帮你避开这个问题。

L3模式

对于L3模式而言,会给网卡做一些比较奇妙的配置,首先所有的多播和广播报文会在L3模式的网卡里面直接丢弃,等于多播完全在这种模式里面废除了;另外就是网卡会配置成noarp模式,不会响应和发出任何arp请求。那么就由小朋友要问了:那mac地址可怎么填呀?答案是对于子网卡而言,其发出的三层报文,源mac和目的mac都会设置成自己的mac地址。

简单过一下L3模式的转发代码:

static int ipvlan_xmit_mode_l3(struct sk_buff *skb, struct net_device *dev)
{
	const struct ipvl_dev *ipvlan = netdev_priv(dev);
	void *lyr3h;
	struct ipvl_addr *addr;
	int addr_type;

	lyr3h = ipvlan_get_L3_hdr(ipvlan->port, skb, &addr_type);
	if (!lyr3h)
		goto out;

	if (!ipvlan_is_vepa(ipvlan->port)) {
		addr = ipvlan_addr_lookup(ipvlan->port, lyr3h, addr_type, true);
		if (addr) {
			if (ipvlan_is_private(ipvlan->port)) {
				consume_skb(skb);
				return NET_XMIT_DROP;
			}
			return ipvlan_rcv_frame(addr, &skb, true);
		}
	}
out:
	ipvlan_skb_crossing_ns(skb, ipvlan->phy_dev);
	return ipvlan_process_outbound(skb);
}


// 哎呀,他还调用了ipvlan_process_outbound,那ipvlan_process_outbound在下面了:
static int ipvlan_process_outbound(struct sk_buff *skb)
{
	struct ethhdr *ethh = eth_hdr(skb);
	int ret = NET_XMIT_DROP;

	/* The ipvlan is a pseudo-L2 device, so the packets that we receive
	 * will have L2; which need to discarded and processed further
	 * in the net-ns of the main-device.
	 */
	if (skb_mac_header_was_set(skb)) {
		/* In this mode we dont care about
		 * multicast and broadcast traffic */
		if (is_multicast_ether_addr(ethh->h_dest)) {
			pr_debug_ratelimited(
				"Dropped {multi|broad}cast of type=[%x]\n",
				ntohs(skb->protocol));
			kfree_skb(skb);
			goto out;
		}

		skb_pull(skb, sizeof(*ethh));
		skb->mac_header = (typeof(skb->mac_header))~0U;
		skb_reset_network_header(skb);
	}

	if (skb->protocol == htons(ETH_P_IPV6))
		ret = ipvlan_process_v6_outbound(skb);
	else if (skb->protocol == htons(ETH_P_IP))
		ret = ipvlan_process_v4_outbound(skb);
	else {
		pr_warn_ratelimited("Dropped outbound packet type=%x\n",
				    ntohs(skb->protocol));
		kfree_skb(skb);
	}
out:
	return ret;
}

// 还有一层?没关系,我们继续贴代码
static int ipvlan_process_v4_outbound(struct sk_buff *skb)
{
	const struct iphdr *ip4h = ip_hdr(skb);
	struct net_device *dev = skb->dev;
	struct net *net = dev_net(dev);
	struct rtable *rt;
	int err, ret = NET_XMIT_DROP;
	struct flowi4 fl4 = {
		.flowi4_oif = dev->ifindex,
		.flowi4_tos = RT_TOS(ip4h->tos),
		.flowi4_flags = FLOWI_FLAG_ANYSRC,
		.flowi4_mark = skb->mark,
		.daddr = ip4h->daddr,
		.saddr = ip4h->saddr,
	};

	rt = ip_route_output_flow(net, &fl4, NULL);
	if (IS_ERR(rt))
		goto err;

	if (rt->rt_type != RTN_UNICAST && rt->rt_type != RTN_LOCAL) {
		ip_rt_put(rt);
		goto err;
	}
	skb_dst_set(skb, &rt->dst);
	err = ip_local_out(net, skb->sk, skb);
	if (unlikely(net_xmit_eval(err)))
		dev->stats.tx_errors++;
	else
		ret = NET_XMIT_SUCCESS;
	goto out;
err:
	dev->stats.tx_errors++;
	kfree_skb(skb);
out:
	return ret;
}

兄弟网卡之间的转发逻辑

这个转发代码是不是有点过于简单?可以看出,兄弟网卡的判断逻辑跟L2是有所区别的,不能通过目的mac来判断了,因为不管是不是给兄弟发,目的mac都是一样的,那只能直接根据目的IP查表了,能查到就是兄弟的,直接调用兄弟的ipvlan_rcv_frame给他送过去,不用谢。

跨主机的转发逻辑

对于跨主机的转发, 和L2模式有显著的不同,不同点在于:

  1. 在outbound函数中增加了对mac_header的处理,对于目的MAC为多播的报文直接丢弃,然后把skbuf的mac_header直接清空,这一步应该是为了防止mac地址的配置影响路由子系统对发包的判断。
  2. 欸……等等,你刚才说了“路由子系统”对吧?嗯是的,可以看出,对于ipv4而言,发送的核心代码是ip_route_output_flow 和 ip_local_out ,这也就意味着,对于L3模式而言,报文的发送是经过了父网卡的路由子系统的,也就是最终网卡的发包是由父网卡路由子系统决定的下一跳……这是和L2模式的核心区别。

多播报文的处理

无论是收还是发,都是直接丢弃了事,就是这么残忍。

一些衍生的思考

  • 在2.2中说到,在L2模式下父子网卡之间二层隔离,那这个问题在L3模式下是不是就不存在了?
    • 是的,如果你直接在ipvlan子网卡ping父网卡,那父网卡是会在协议栈处理这个报文,并且能够回包的……但是你需要把子网卡的IP段设置在Host里面配置Link路由,报文才能正确地回到子网卡,这对于绝大多数容器网络应用来说并不是一个很明智的做法——不管你的host和容器是不是处于同一个网段。

关于那些flag

4.1 vepa这玩意

vepa这玩意是802.1Qbg提出的一种虚拟网络方案,其有独特的背景还是值得说一说的。

在古老的计算机网络体系里面,“交换”一般是指的基于MAC地址的二层转发,其对于未知主机的触达主要是依赖于“泛洪”,即对于未知目的MAC的报文,就很暴力地转发到全部物理端口,但是这个泛洪和转发,通常都是不包含这个报文的源物理端口的,原因嘛,也很简单,一个报文从这个口子发出来了,我再给他转发回去,万一下面也是个交换机,也给我原样转回来,这流量岂不是原地爆炸?不行不行。STP原理上也是不支持这样的转发模式。

但是随着云网络这玩意的兴起,交换机下面挂着的物理机也会有很多VM、容器之类的,有时候同一台物理机的VM之间互相通信,报文也直接怼给交换机了,这就是vepa模式。

那交换机转吗?转的话,那就开启一个hairpin模式,老老实实的转回去。

所以可以看出,vepa模式就是不管是不是同一物理机,我协议栈也不管,就是往外发,就是莽,交换机来给我处理好。所以这种模式通常是需要交换机配置来配合的,一般场景下,还真的用不到。

看ipvlan源代码也看出来,对于vepa模式,不管l2还是l3,都直接发出去了。

4.2 关于L3S和L3模式之间的差异

对于L3和L3s之间的差别,主要体现在接收流程上,2和3两节基本上没有讲接受逻辑,因为基本上乏善可陈,但是对于L3和L3s之间的差异,主要就体现在这个接收流程了,还是值得看一下的,关注一下rx_handle的相关逻辑:

rx_handler_result_t ipvlan_handle_frame(struct sk_buff **pskb)
{
	struct sk_buff *skb = *pskb;
	struct ipvl_port *port = ipvlan_port_get_rcu(skb->dev);

	if (!port)
		return RX_HANDLER_PASS;

	switch (port->mode) {
	case IPVLAN_MODE_L2:
		return ipvlan_handle_mode_l2(pskb, port);
	case IPVLAN_MODE_L3:
		return ipvlan_handle_mode_l3(pskb, port);
	case IPVLAN_MODE_L3S:
		return RX_HANDLER_PASS;
	}

	/* Should not reach here */
	WARN_ONCE(true, "ipvlan_handle_frame() called for mode = [%hx]\n",
			  port->mode);
	kfree_skb(skb);
	return RX_HANDLER_CONSUMED;
}

static rx_handler_result_t ipvlan_handle_mode_l3(struct sk_buff **pskb,
						 struct ipvl_port *port)
{
	void *lyr3h;
	int addr_type;
	struct ipvl_addr *addr;
	struct sk_buff *skb = *pskb;
	rx_handler_result_t ret = RX_HANDLER_PASS;

	lyr3h = ipvlan_get_L3_hdr(port, skb, &addr_type);
	if (!lyr3h)
		goto out;

	addr = ipvlan_addr_lookup(port, lyr3h, addr_type, true);
	if (addr)
		ret = ipvlan_rcv_frame(addr, pskb, false);

out:
	return ret;
}

可以看出来,l3模式的收包基本上走的还是查IP->确定子网卡->转发的老套路,而L3S却在ipvlan_handle_frame的流程里直接返回了RX_HANDLER_PASS,走了Host的协议栈,欸,这是几个意思? 不过可以看出,在L3S的网卡初始化过程中,ipvlan悄悄配置了几个回调进去:

static const struct l3mdev_ops ipvl_l3mdev_ops = {
	.l3mdev_l3_rcv = ipvlan_l3_rcv,
};



static int ipvlan_set_port_mode(struct ipvl_port *port, u16 nval)
{
	struct ipvl_dev *ipvlan;
	struct net_device *mdev = port->dev;
	unsigned int flags;
	int err;

	ASSERT_RTNL();
// .... 省略一些不太关心的代码
    if (nval == IPVLAN_MODE_L3S) {
			/* New mode is L3S */
			err = ipvlan_register_nf_hook(read_pnet(&port->pnet));
			if (!err) {
				mdev->l3mdev_ops = &ipvl_l3mdev_ops;
				mdev->priv_flags |= IFF_L3MDEV_RX_HANDLER;
			} else
				goto fail;
		} else if (port->mode == IPVLAN_MODE_L3S) {
			/* Old mode was L3S */
			mdev->priv_flags &= ~IFF_L3MDEV_RX_HANDLER;
			ipvlan_unregister_nf_hook(read_pnet(&port->pnet));
			mdev->l3mdev_ops = NULL;
		}

 // ....
    
}

oh,他在netfilter的l3mdev_l3_rcv注册了一个ipvlan_l3_rcv的回调,结合上面的分析,l3s的收包流程会一路走到PREROUTING hook点,找到要转发的子网卡后,再通过子网卡的LOCAL_IN hook点到达。

这也就意味着,在ipvlan子网卡的PreRouting里面常做的一些配置是可能被L3S模式绕过的,比如nat。目前我还没有遇到过需要使用L3S模式的应用场景。

4.3 关于private

private主要用于隔离兄弟网卡之间的报文转发。对于一些只提供南北向流量服务,又对隔离性有需求的应用可以使用private模式。

5. 总结一下

通过上面的代码分析,可以看出ipvlan这玩意,提供了最基本的转发和隔离能力,并且代码这块短小精悍,省略了很多麻烦的处理流程,我们实际测试下来性能也相当可观。其基于网卡粒度的隔离思路,配合vlan子网卡这个模式可以在IDC配合交换机配置内轻松地实现vlan隔离。其与三层转发强关联、子网卡共享mac地址的方式也更适合带有三层交换网关的业务隔离系统,与使用去堆叠架构或者云上网络的纯三层转发系统相性非常好,是云上容器网络的一个绝佳方案。

但是可以看出ipvlan是一个underlay的解决方案,因此也需要上联的云网络或者物理网络的配置配合,考虑到物理网络更新的复杂性和风险,其实更适合做云上容器网络的解决方案。