关于c:单机容器网络

3次阅读

共计 5153 个字符,预计需要花费 13 分钟才能阅读完成。

基于 linux 内核 5.4.54
昨天分享了 veth 的原理:
veth 原理 —- 两个容器通过 veth 通信时数据包的收发门路
个别状况容器间接不会通过 veth 间接通信,会通过 docker0 网桥通信
明天剖析容器通过 veth 和 docker0 网桥的通信门路

单机容器网络结构

在宿主机上通过 docker 创立两个容器时会主动生成如图所示的网络结构

  • 在宿主机上会生成一个 docker0 网桥
  • 容器 1 和 docker0 网桥之间通过 veth 相连,容器 2 一样

简略看一下 Namespace

网络设备的 Namespace:

网络设备注册时,会通过 net_device->nd_net(网络设备构造体字段)设置 Net Namespace。

剖析结构图设施的 Namespace:
  • veth0 属于 Namespace1;veth1 属于 Namespace2;
  • eth0,docker0,docker0 上的两个 veth 设施属于 Host Namespace

数据包的 Namespace:

数据包的 Namespace 由 skb_buff->dev->nd_net(数据包目标设施的 Namespace)决定

过程的 Namespace:

通过 clone()创立过程时会通过 task_struct->nsproxy(过程构造体字段)为过程设置 Namespace,nsproxy->net_ns 决定过程的 Net Namespace

/* nsproxy 构造体 其中蕴含了各种命名空间隔离和 Cgroup,当前有工夫会多理解 */
struct nsproxy {
    atomic_t count;
    struct uts_namespace *uts_ns;
    struct ipc_namespace *ipc_ns;
    struct mnt_namespace *mnt_ns;
    struct pid_namespace *pid_ns_for_children;
    struct net       *net_ns;
    struct cgroup_namespace *cgroup_ns;
};

Socket 套接字的 Namespace

过程创立 Socket 时, 会设置 sock->sk_net 为 current->nsproxy->net_ns,行将以后过程的 Net Namespace 传递给 sock 套接字。

剖析两种状况

1. 容器 1 通过网桥向容器 2 发送数据包

收发门路:


过程(容器 1)
|-- 通过 socket 零碎调用进入内核,通过的是 Namespace1 的网络协议栈
kernel 层: 创立 skb 构造体,从用户空间拷贝数据到内核空间
TCP/UDP 封包
IP 封包, 跑 Namespace1 的路由和 netfilter
|-- 出协定栈进入网络设备
调用网络设备驱动的传输数据包函数
|
veth_xmit: veth 驱动注册的传输函数
    |
    veth_forward_skb
        |
        __dev_forward_skb: 革除 skb 中可能影响命名空间隔离的所有信息
        |          并且会更新数据包要到的网络设备(skb->dev), 由 veth0 改为 docker-veth0
        |          数据包要跑的协定栈 (network namespace) 由 skb->dev 的 nd_net 字段决定
        |
        XDP 钩子点
        |
        netif_rx
            |
            netif_rx_internal: cpu 软中断负载平衡
                |
                enqueue_to_backlog: 将 skb 包退出指定 cpu 的 input_pkt_queue 队尾
                                    queue 为空时激活网络软中断,
                                    queue 不为空不须要激活软中断,cpu 没清空队列之前
                                    会主动触发软中断
    每个 cpu 都有本人的 input_pkt_queue(接管队列, 默认大小 1000, 可批改), 和 process_queue(解决队列), 软中断处理函数解决实现 process_queue 中的所有 skb 包之后, 会将将 input_pkt_queue 拼接到 process_queue
    input_pkt_queue 和 process_queue 是 cpu 为非 NAPI 设施筹备的队列,NAPI 设施有本人的队列

    始终到这里,数据包门路和 veth 文档中的两个 veth 通信的发送阶段是完全一致的,docker0 网桥解决数据包次要在__netif_receive_skb_core 中

cpu 解决网络数据包过程:

do_softirq()
|
net_rx_action: 网络软中断处理函数
    |
    napi_poll
        |
        n->poll: 调用目标网络设备驱动的 poll 函数
            |    veth 设施没有定义 poll, 调用默认 poll 函数 -process_backlog
            |
            process_backlog: cpu 循环从 process_queue 中取出 skb 解决, 最多解决 300 个 skb,
                |            解决队列清空后, 拼接 input_pkt_queue 到 process_queue 队尾
                |
                __netif_receive_skb
                    |
                    ...
                    |
                    __netif_receive_skb_core

数据包解决代码剖析:


/*
* __netif_receive_skb_core 代码剖析
* 代码做了很多删减,剩下了网桥的解决和数据包传递给下层解决的局部
* 其余很多局部例如 vlan,xdp,tcpdump 等代码删去了
*/
static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
                    struct packet_type **ppt_prev)
{
    struct packet_type *ptype, *pt_prev;
    rx_handler_func_t *rx_handler;
    struct sk_buff *skb = *pskb;
    struct net_device *orig_dev;
    bool deliver_exact = false;
    int ret = NET_RX_DROP;
    __be16 type;

    /* 记录 skb 包目标设施 */
    orig_dev = skb->dev;

    /* 设置 skb 包的协定头指针 */
    skb_reset_network_header(skb);
    if (!skb_transport_header_was_set(skb))
        skb_reset_transport_header(skb);
    skb_reset_mac_len(skb);

    pt_prev = NULL;

another_round:
...
    /**
    * skb 包的目标设施是 docker-veth0,veth 作为了 bridge 的一个接口
    * docker-veth0 在注册时会设置 rx_handler 为网桥的收包函数 br_handle_frame
    * 黄色处代码为调用 bridge 的 br_handle_frame
    */
    rx_handler = rcu_dereference(skb->dev->rx_handler);
    if (rx_handler) {
        ...
        switch (rx_handler(&skb)) {
        case RX_HANDLER_CONSUMED: /* 已解决,无需进一步解决 */
            ret = NET_RX_SUCCESS;
            goto out;
        case RX_HANDLER_ANOTHER: /* 再解决一次 */
            goto another_round;
        case RX_HANDLER_EXACT: /* 准确传递到 ptype->dev == skb->dev */
            deliver_exact = true;
        case RX_HANDLER_PASS:
            break;
        default:
            BUG();}
    }
...
    /* 获取三层协定 */
    type = skb->protocol;

    /* 
    * 调用指定协定的协定处理函数(例如 ip_rcv 函数) 把数据包传递给下层协定层解决
    * ip_rcv 函数是网络协议栈的入口函数
    * 数据包达到这里会通过 netfilter,路由,最初被转发或者发给下层协定栈
    */
    deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
                   &orig_dev->ptype_specific);
...
    if (pt_prev) {if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC)))
            goto drop;
        *ppt_prev = pt_prev;
    } else {
drop:
        if (!deliver_exact)
            atomic_long_inc(&skb->dev->rx_dropped);
        else
            atomic_long_inc(&skb->dev->rx_nohandler);
        kfree_skb(skb);
        ret = NET_RX_DROP;
    }

out:
    *pskb = skb;
    return ret;
}

网桥解决代码剖析:


/* br_handle_frame, 已删减 */
rx_handler_result_t br_handle_frame(struct sk_buff **pskb)
{
    struct net_bridge_port *p;
    struct sk_buff *skb = *pskb;
    const unsigned char *dest = eth_hdr(skb)->h_dest;
...
forward:
    switch (p->state) {
    case BR_STATE_FORWARDING:
    case BR_STATE_LEARNING:
        /* 目标地址是否是设施链路层地址 */
        if (ether_addr_equal(p->br->dev->dev_addr, dest))
            skb->pkt_type = PACKET_HOST;

        return nf_hook_bridge_pre(skb, pskb);
    default:
drop:
        kfree_skb(skb);
    }
    return RX_HANDLER_CONSUMED;
}

nf_hook_bridge_pre
    |
    br_handle_frame_finish

/* br_handle_frame_finish, 已删减 */
int br_handle_frame_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{struct net_bridge_port *p = br_port_get_rcu(skb->dev);
    enum br_pkt_type pkt_type = BR_PKT_UNICAST;
    struct net_bridge_fdb_entry *dst = NULL;
    struct net_bridge_mdb_entry *mdst;
    bool local_rcv, mcast_hit = false;
    struct net_bridge *br;
    u16 vid = 0;
...
    if (dst) {
        unsigned long now = jiffies;

        /* 如果目的地是宿主机 */
        if (dst->is_local)
            /*
            * 这个函数最终会回到__netif_receive_skb_core
            * 把 skb 送上 Host Net Namespace 的三层协定栈解决
            */
            return br_pass_frame_up(skb);

        if (now != dst->used)
            dst->used = now;
        /*
        * 目的地不是宿主机把数据包转发到指定端口
        * 代码实现是调用目标端口设施驱动的数据包接管函数
        * 这次门路是调用 docker-veth1 的 veth_xmit
        * 上文剖析了 veth_xmit, 会批改数据包目标设施
        * 从 docker-veth1 批改为 veth1,而后送到 cpu 队列期待解决
        * cpu 解决数据包时,跑 veth1(也就是 Namespace2)的网络协议栈
        * 最初容器 2 过程收包
        */
        br_forward(dst->dst, skb, local_rcv, false);
    }
...

out:
    return 0;
drop:
    kfree_skb(skb);
    goto out;
}

总结门路:

容器 1 过程生成数据包
|
通过 Namespace1 协定栈送到 veth0
|
veth0 驱动改 skb 目标设施为 docker-veth0,送 skb 到 cpu 队列
|
cpu 解决数据包,因为 docker-veth0 是网桥的一个端口,调用网桥收包函数
|
网桥批改 skb 目标设施为 docker-veth1,调用 docker-veth1 驱动
|
docker-veth1 驱动改 skb 目标设施为 veth1,送 skb 到 cpu 队列
|
cpu 解决数据包,送上 veth1(Namespace2) 的网络协议栈
|
容器 2 过程收包

2. 容器 1 通过网桥向宿主机发送数据包

代码后面都剖析过了,间接总结

总结门路:

容器 1 过程生成数据包
|
通过 Namespace1 协定栈送到 veth0
|
veth0 驱动改 skb 目标设施为 docker-veth0,送 skb 到 cpu 队列
|
cpu 解决数据包,因为 docker-veth0 是网桥的一个端口,调用网桥收包函数
|
网桥判断目的地为宿主机,间接跑宿主机 (Host Namespace) 协定栈

正文完
 0