周末的时候写了一篇关于Docker底层支撑技术的文章:
以firejail sandbox解析Docker核心原理依赖的四件套https://blog.csdn.net/dog250/article/details/81025071
获得了一些反响,总结下来就三点:
1. 确实这四件套支撑了很多的容器技术;
2. 唱衰Docker以及OverlayFS,Cgroup,NS这些,以为它们违背了某种原则;
3. 关于firejail网络方面的某些细节。

上述的1和2显得过于形而上,保留个人观点不讨论,或者等哪天闲了再说。关于第三点,我觉得有必要梳理一下,本文先说一个典型的。


有网友问到,firejail的Macvlan配置细节是怎样的,firejail容器内的Macvlan虚拟网卡如何访问容器外宿主机的IP地址呢??(先说答案吧,默认情况下,不能访问)

我并没有深入的了解过firejail,本身接触它也不久,所以只能从其文档代码中去一窥究竟了。firejail的github源在这里:https://github.com/netblue30/firejail

  1. 从以下的链接可以看到一个典型的Demo样例,我和作者描述问题的思路是一致的:
    https://github.com/netblue30/firejail/blob/master/src/firejail/network.txt
  2. 从下面的代码中你可以看到firejail关于macvlan的配置细节:
    https://github.com/netblue30/firejail/blob/master/src/firejail/network_main.c

总而言之,firejail使用网络的方式有两种:

  • Bridge veth的方式:与Docker Bridge模式一致,如果–net参数指定一个Linux Bridge的话。
  • Macvlan bridge模式:如果–net参数指定一块物理网卡,即会产生一个bridge模式的macvlan虚拟网卡。

这些在其manual中也有提到:

–net=bridge_interface
Enable a new network namespace and connect it to this bridge interface. Unless specified with option –ip and –defaultgw, an IP address and a
default gateway will be assigned automatically to the sandbox. The IP address is verified using ARP before assignment. The address configured as
default gateway is the bridge device IP address. Up to four –net bridge devices can be defined. Mixing bridge and macvlan devices is allowed.
.
Example:
sudobrctladdbrbr0 s u d o b r c t l a d d b r b r 0 sudo ifconfig br0 10.10.20.1/24
sudobrctladdbrbr1 s u d o b r c t l a d d b r b r 1 sudo ifconfig br1 10.10.30.1/24
firejailnet=br0net=br1.net=ethernetinterfaceEnableanewnetworknamespaceandconnectittothisethernetinterfaceusingthestandardLinuxmacvlandriver.Unlessspecifiedwithoptionipanddefaultgw,anIPaddressandadefaultgatewaywillbeassignedautomaticallytothesandbox.TheIPaddressisverifiedusingARPbeforeassignment.Theaddressconfiguredasdefaultgatewayisthedefaultgatewayofthehost.Uptofournetdevicescanbedefined.Mixingbridgeandmacvlandevicesisallowed.Note:wlandevicesarenotsupportedforthisoption..Example: f i r e j a i l – n e t = b r 0 – n e t = b r 1 . – n e t = e t h e r n e t i n t e r f a c e E n a b l e a n e w n e t w o r k n a m e s p a c e a n d c o n n e c t i t t o t h i s e t h e r n e t i n t e r f a c e u s i n g t h e s t a n d a r d L i n u x m a c v l a n d r i v e r . U n l e s s s p e c i f i e d w i t h o p t i o n – i p a n d – d e f a u l t g w , a n I P a d d r e s s a n d a d e f a u l t g a t e w a y w i l l b e a s s i g n e d a u t o m a t i c a l l y t o t h e s a n d b o x . T h e I P a d d r e s s i s v e r i f i e d u s i n g A R P b e f o r e a s s i g n m e n t . T h e a d d r e s s c o n f i g u r e d a s d e f a u l t g a t e w a y i s t h e d e f a u l t g a t e w a y o f t h e h o s t . U p t o f o u r – n e t d e v i c e s c a n b e d e f i n e d . M i x i n g b r i d g e a n d m a c v l a n d e v i c e s i s a l l o w e d . N o t e : w l a n d e v i c e s a r e n o t s u p p o r t e d f o r t h i s o p t i o n . . E x a m p l e : firejail –net=eth0 –ip=192.168.1.80 –dns=8.8.8.8 firefox

现在我们关注第二点,即Macvlan的方式。我使用下面的命令启动firejail:

# 我为容器指派192.168.44.55这个IP地址
# 我的宿主机enp0s17这块网卡的IP地址是192.168.44.138
root@debian:/home/zhaoya# firejail --net=enp0s17 --ip=192.168.44.55/24 
Reading profile /etc/firejail/server.profile
Reading profile /etc/firejail/disable-common.inc
Reading profile /etc/firejail/disable-programs.inc
Reading profile /etc/firejail/disable-passwdmgr.inc

** Note: you can use --noprofile to disable server.profile **

Parent pid 870, child pid 871
The new log directory is /proc/871/root/var/log

Interface        MAC                IP               Mask             Status
lo                                  127.0.0.1        255.0.0.0        UP    
eth0-870         1e:01:c7:ff:4b:39  192.168.44.55    255.255.255.0    UP    
Default gateway 192.168.44.2

Child process initialized
root@debian:~# 
root@debian:~# ping 192.168.44.138
PING 192.168.44.138 (192.168.44.138) 56(84) bytes of data.
From 192.168.44.55 icmp_seq=1 Destination Host Unreachable
From 192.168.44.55 icmp_seq=2 Destination Host Unreachable
From 192.168.44.55 icmp_seq=3 Destination Host Unreachable

在容器外的宿主机抓包:

root@debian:/home/zhaoya# tcpdump -i enp0s17 arp or icmp -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp0s17, link-type EN10MB (Ethernet), capture size 262144 bytes
19:31:50.029168 ARP, Request who-has 192.168.44.138 tell 192.168.44.55, length 28
19:31:51.053176 ARP, Request who-has 192.168.44.138 tell 192.168.44.55, length 28
19:31:52.077292 ARP, Request who-has 192.168.44.138 tell 192.168.44.55, length 28
19:31:53.101540 ARP, Request who-has 192.168.44.138 tell 192.168.44.55, length 28
...

显然,ARP请求没有得到回应。

如果此时另外启动一个新的firejail容器,指派另一个同网段IP地址,你会发现,两个容器之间是可以互通的,但是容器和宿主机之间无法互通,这是怎么回事?

这就涉及到Macvlan的几种工作模式的原理了,我简单将其总结成下面的图示:

这里写图片描述

我们专门看看左上角第一幅图,即Bridge模式的Macvlan。显然,没有从虚拟网卡到物理网卡的通路(确实有点像裤衩…)…有了这个图示几乎便不用去看代码了。

值得注意的是,VEPA是一个特殊的模式,它不仅仅关乎Macvlan本身,还涉及到宿主机外部的交换机是否支持Hairpin(发夹弯模式,即交换机端口并不过滤入口的模式,可以实现原路echo),所以说,Bridge模式情况下,Macvlan虚拟网卡不能访问宿主网卡,有关限定条件,即虚拟网卡在主机内部不能访问宿主网卡,虚拟网卡发出的数据包经由物理宿主网卡一旦发到外部,还是有可能通过Hairpin给echo回来的


问题的解释就是这样。

下面是见招拆招了。如果你非要实现Macvlan的虚拟网卡和物理网卡之间的互通,怎么办?其实简单,只需要构造下面的图即可:

这里写图片描述


这下不看代码也得看了!


主要看Macvlan的xmit回调:

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;

        /* send to other bridge ports directly */
        if (is_multicast_ether_addr(eth->h_dest)) {
            macvlan_broadcast(skb, port, dev, MACVLAN_MODE_BRIDGE);
            // 显然在ARP广播注入所有的虚拟网卡接收路径后,便从物理网卡直接发到外部去了。
            goto xmit_world;
        }

        dest = macvlan_hash_lookup(port, eth->h_dest);
        // 如果不是广播,也只有路由到虚拟网卡的数据包从能被注入。
        if (dest && dest->mode == MACVLAN_MODE_BRIDGE) {
            /* send to lowerdev first for its network taps */
            dev_forward_skb(vlan->lowerdev, skb);

            return NET_XMIT_SUCCESS;
        }
    }

xmit_world:
    skb->dev = vlan->lowerdev;
    // 直接经由物理网卡连接的线缆发出。
    return dev_queue_xmit(skb);
}

对付这个非常简单,只需要改成下面的这个就行:


static int macvlan_queue_xmit_v2(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;

                /* send to other bridge ports directly */
                if (is_multicast_ether_addr(eth->h_dest)) {
                        struct sk_buff *nskb;
                        macvlan_broadcast(skb, port, dev, MACVLAN_MODE_BRIDGE);
                        nskb = skb_clone(skb, GFP_ATOMIC);
                        if (likely(nskb)) {
                                nskb->dev = vlan->lowerdev;
                                // 往物理网卡也注入一份。
                                dev_forward_skb(vlan->lowerdev, nskb);
                        }
                        goto xmit_world;
                }

                dest = macvlan_hash_lookup(port, eth->h_dest);
                if (dest && dest->mode == MACVLAN_MODE_BRIDGE) {
                        /* send to lowerdev first for its network taps */
                        dev_forward_skb(vlan->lowerdev, skb);

                        return NET_XMIT_SUCCESS;
                }
                // 如果目标MAC是物理网卡的,则注入到宿主物理网卡
                else /*if (!compareMacs(eth, vlan->lowerdev))*/{
                        struct sk_buff *nskb;
                        nskb = skb_clone(skb, GFP_ATOMIC);

                        if (likely(nskb)) {
                                nskb->dev = vlan->lowerdev;
                                dev_forward_skb(vlan->lowerdev, nskb);
                        }
                }
        }

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

Macvlan虚拟网卡的发送路径就是这么轻松搞定的,那么接下来看一个比较棘手的,即物理网卡的发送路径,因为显然我们需要宿主机也能通过物理网卡和容器通信,比如SSH登录它。

这个比较棘手是因为我们不得不改物理网卡的驱动的hard_start_xmit回调函数,目前也没有在dev_queue_xmit中看到有什么比较好的HOOK点,而修改物理网卡驱动就为了支持Macvlan这么一个并非通用的机制,也有点怪异…

为了尽快跑起来看到效果,不去想那么多,直接使用ptype_all机制。

在数据包将要发送到物理网卡前,会有一个抓包的HOOK点,即dev_queue_xmit_nit函数。我们只需要注册一个ETH_P_ALL类型的packet_type即可将数据包拉到一个func回调中,之后便可以在这个回调中做任何想做的事了:

int xmit_handle(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
        const struct macvlan_port *port = macvlan_port_get_rcu(skb->dev);
        const struct ethhdr *eth = eth_hdr(skb);
        const struct macvlan_dev *vlan;

        if (skb->pkt_type != PACKET_OUTGOING)  {
            // 仅仅在发送路径使能
                goto drop_out;
        }


        if (port) {
                vlan = macvlan_hash_lookup(port, eth->h_dest);
                if (vlan) // 如果找到了虚拟网卡就单播注入。
                        dev_forward_skb(vlan->dev, skb);
                else // 如果没有对应到,就广播注入。
                        macvlan_broadcast(skb, port, dev, MACVLAN_MODE_BRIDGE);
                return 0;
        }

drop_out:
        consume_skb(skb);
        return 0;
}

static struct packet_type macvlan_handle_pack = {
        .type = __constant_htons(ETH_P_ALL),
        .func = xmit_handle,
};

最后别忘了注册:

macvlan_init_module:
dev_add_pack(&macvlan_handle_pack);

macvlan_cleanup_module:
dev_remove_pack(&macvlan_handle_pack);

编译成新的macvlan.ko,然后加载,再执行最开始的流程,宿主机和容器便可以相互通信了。

由于这是一个临时的方案,所以物理网卡发送数据包是旁路给虚拟网卡的,真正的数据包还会在物理链路上冗余一份,这是一个不足。如果将发送逻辑放在物理网卡驱动的发送回调中便可以解决这个问题。另外,其实在调用具体网卡驱动的hard_start_xmit前先check一下公共的标识位也是比较不错的,比如引入一个NETIF_F_MASTER标志,在dev_queue_xmit的发送之前,便可以:

    if ((dev->features & NETIF_F_MASTER) == 0) {
        int ret;
        ret = check_flags(skb, struct net_device *dev, struct packet_type *pt);
        if (ret == E_BYPASS) 
            goto out;
    }
    ...
    dev_hard_start_xmit...
    ...
out:
    rcu_read_unlock_bh();
    return rc;

这样看起来会更加优雅。


这就是整个Macvlan与宿主物理网卡之间通信的整个逻辑了,简单地解决了其不能互通的问题。

其实解决这个问题对于我个人也是很有用的,这样我就可以方便地SSH登录容器了。不过,值得注意的是,这实际上并不一定就是个问题,Macvlan设计上就是如此吧,即把物理网卡仅仅看作是一个容纳虚拟网卡的容器,而不是其中一个节点。这种关系让人不禁想起task_struct这个结构体以及进程/线程之间的关系…


就这么点事,不多说。浙江温州皮鞋湿!

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐