bridge在内核的实现

这里来讲下bridge。本文可以看成是对《Understanding Linux Network Internals》的学习笔记。

在之前的文章中可以知道,当一个数据包进来后,poll函数会通过netif_receive_skb进行下一步的处理,在提交给上层协议栈前,会先看是不是要走bridge的逻辑,如果要走的话就会由bridge的逻辑进行处理,而不是往上层继续调用。

bridge的STP概念就不多说了,大家可以往上搜下。下文看到的BPDUs是和STP有关的一个概念。

在linux中,如果一个eth口属于了一个bridge,但是其自己又配了一个地址,那么对于发包没有影响,但是对于收包就要决定这个包是交给bridge处理还是交给ip层处理。这个需要用到ebtables的概念。

当bridge被加载的时候,入口文件为net/bridge/br.c,代码为:

module_init(br_init)
module_exit(br_deinit)

br_init的实现主要做了下面的事情:
1.初始化转发表
2.设置ioctl对应的br_ioctl_hook
3.初始化BPDUs相关的函数指针
4.在netdev_chain中注册个回调

ioctl对应的实现为:

int br_ioctl_deviceless_stub(struct net *net, unsigned int cmd, void __user *uarg)
{
    switch (cmd) {
    case SIOCGIFBR:
    case SIOCSIFBR:
        return old_deviceless(net, uarg);

    case SIOCBRADDBR:
    case SIOCBRDELBR:
    {   
        char buf[IFNAMSIZ];

        if (!ns_capable(net->user_ns, CAP_NET_ADMIN))
            return -EPERM;

        if (copy_from_user(buf, uarg, IFNAMSIZ))
            return -EFAULT;

        buf[IFNAMSIZ-1] = 0;
        if (cmd == SIOCBRADDBR)
            return br_add_bridge(net, buf);

        return br_del_bridge(net, buf);
    }
    }
    return -EOPNOTSUPP;
}

这里会看到br_add_bridge和br_del_bridge,我们下面会讲到其实现。

继续看br_init,重要代码为:

    err = stp_proto_register(&br_stp_proto);
    if (err < 0) {
        pr_err("bridge: can't register sap for STP\n");
        return err;
    }

    err = br_fdb_init();
    if (err)
        goto err_out;

    err = register_pernet_subsys(&br_net_ops);
    if (err)
        goto err_out1;

    err = br_nf_core_init();
    if (err)
        goto err_out2;

    err = register_netdevice_notifier(&br_device_notifier);
    if (err)
        goto err_out3;

    err = register_netdev_switch_notifier(&br_netdev_switch_notifier);
    if (err)
        goto err_out4;

    err = br_netlink_init();
    if (err)
        goto err_out5;

    brioctl_set(br_ioctl_deviceless_stub);

br_netlink_init中会在netlink里注册这:

int __init br_netlink_init(void)
{
    int err;

    br_mdb_init();
    rtnl_af_register(&br_af_ops);

    err = rtnl_link_register(&br_link_ops);
    if (err)
        goto out_af;

    return 0;

out_af:
    rtnl_af_unregister(&br_af_ops);
    br_mdb_uninit();
    return err;
}

现在来看下一个bridge的建立流程,在内核中一个bridge的体现也是一个net_device结构体,由于是虚拟设备,所以private的字段会比较重要。根据上面看的,我们建立一个bridge的时候入口从br_add_bridge开始看:

int br_add_bridge(struct net *net, const char *name)
{
    struct net_device *dev;
    int res;

    dev = alloc_netdev(sizeof(struct net_bridge), name, NET_NAME_UNKNOWN,
               br_dev_setup);

    if (!dev)
        return -ENOMEM;

    dev_net_set(dev, net);
    dev->rtnl_link_ops = &br_link_ops;

    res = register_netdev(dev);
    if (res)
        free_netdev(dev);
    return res;
}

这里代码一眼就能看出我们要关注的东西:br_dev_setup、dev_net_set以及br_link_ops。

br_dev_setup这个根据之前文章讲的对于alloc_netdev分析,我们知道其主要会初始化net_device的私有属性。代码如下:

void br_dev_setup(struct net_device *dev)
{
    struct net_bridge *br = netdev_priv(dev);

    eth_hw_addr_random(dev);
    ether_setup(dev);

    dev->netdev_ops = &br_netdev_ops;
    dev->destructor = br_dev_free;
    dev->ethtool_ops = &br_ethtool_ops;
    SET_NETDEV_DEVTYPE(dev, &br_type);
    dev->tx_queue_len = 0;
    dev->priv_flags = IFF_EBRIDGE;
        
    dev->features = COMMON_FEATURES | NETIF_F_LLTX | NETIF_F_NETNS_LOCAL |
            NETIF_F_HW_VLAN_CTAG_TX | NETIF_F_HW_VLAN_STAG_TX;
    dev->hw_features = COMMON_FEATURES | NETIF_F_HW_VLAN_CTAG_TX |
               NETIF_F_HW_VLAN_STAG_TX;
    dev->vlan_features = COMMON_FEATURES;

    br->dev = dev;
    spin_lock_init(&br->lock);
    INIT_LIST_HEAD(&br->port_list);
    spin_lock_init(&br->hash_lock);

    br->bridge_id.prio[0] = 0x80;
    br->bridge_id.prio[1] = 0x00;

    ether_addr_copy(br->group_addr, eth_reserved_addr_base);

    br->stp_enabled = BR_NO_STP;
    br->group_fwd_mask = BR_GROUPFWD_DEFAULT;
    br->group_fwd_mask_required = BR_GROUPFWD_DEFAULT;

    br->designated_root = br->bridge_id;
    br->bridge_max_age = br->max_age = 20 * HZ;
    br->bridge_hello_time = br->hello_time = 2 * HZ;
    br->bridge_forward_delay = br->forward_delay = 15 * HZ;
    br->ageing_time = 300 * HZ;

    br_netfilter_rtable_init(br);
    br_stp_timer_init(br);
    br_multicast_init(br);
}

首先可以看到一个bridge其实是个以太网设备(ether_setup)。然后的代码是一些函数指针的初始化,这个和一般的net_device没有啥区别。然后的代码是对br这个指向私有内存段的指针做相关的赋值。

在函数指针中我们先熟悉下bridge的几个赋值内容,混个眼熟:

static const struct net_device_ops br_netdev_ops = {
    .ndo_open        = br_dev_open,
    .ndo_stop        = br_dev_stop,
    .ndo_init        = br_dev_init,
    .ndo_start_xmit      = br_dev_xmit,
    .ndo_get_stats64     = br_get_stats64,
    .ndo_set_mac_address     = br_set_mac_address,
    .ndo_set_rx_mode     = br_dev_set_multicast_list,
    .ndo_change_rx_flags     = br_dev_change_rx_flags,
    .ndo_change_mtu      = br_change_mtu,
    .ndo_do_ioctl        = br_dev_ioctl,
#ifdef CONFIG_NET_POLL_CONTROLLER
    .ndo_netpoll_setup   = br_netpoll_setup,
    .ndo_netpoll_cleanup     = br_netpoll_cleanup,
    .ndo_poll_controller     = br_poll_controller,
#endif
    .ndo_add_slave       = br_add_slave,
    .ndo_del_slave       = br_del_slave,
    .ndo_fix_features        = br_fix_features,
    .ndo_fdb_add         = br_fdb_add,
    .ndo_fdb_del         = br_fdb_delete,
    .ndo_fdb_dump        = br_fdb_dump,
    .ndo_bridge_getlink  = br_getlink,
    .ndo_bridge_setlink  = br_setlink,
    .ndo_bridge_dellink  = br_dellink,
};

dev_net_set其实没有做什么事情,而br_link_ops初始化了下面的一些函数指针,目前还不清楚是干嘛的:

struct rtnl_link_ops br_link_ops __read_mostly = {
    .kind           = "bridge",
    .priv_size      = sizeof(struct net_bridge),
    .setup          = br_dev_setup,
    .maxtype        = IFLA_BRPORT_MAX,
    .policy         = br_policy,
    .validate       = br_validate,
    .newlink        = br_dev_newlink,
    .changelink     = br_changelink,
    .dellink        = br_dev_delete,
    .get_size       = br_get_size,
    .fill_info      = br_fill_info,
    
    .slave_maxtype      = IFLA_BRPORT_MAX,
    .slave_policy       = br_port_policy,
    .slave_changelink   = br_port_slave_changelink,
    .get_slave_size     = br_port_get_slave_size,
    .fill_slave_info    = br_port_fill_slave_info,
};

再来看看一个port是怎么被加入到bridge中的,加port的代码为br_add_if(bridge的net_device中的ioctl指针,即br_dev_ioctl会负责调用)。主要做了下面几件事情:
1.判断这个port是不是一个以太网设备,如果不是则失败
2.判断这个port是不是一个bridge,如果是则失败
3.判断这个port是否已经属于某个bridge,如果是则失败
4.分配一个ID号
5.分配默认的priority
6.生成PORT ID
7.根据port所对应的device的速率生成默认的cost
8.设置状态
9.将这个port对应的net_device结构和bridge的net_device关联

对于最后一步中的关联,在linux中所有的bridge上的port都是以链表形式串在一起的,链表的头在bridge的net_device的私有数据中。同时在port被加入的时候其MAC地址就被加入了转发表中,同时会打开混杂模式。

下面来看下数据包的接收(发送逻辑下面在看转发的时候会看到),内核中这里的代码和netfilter的hook是紧密关联的,我们熟悉的iptables就是基于netfilter实现的。

来看收包,一个包到达后会走netif_receive_skb,后者会判断是否走bridge的逻辑。《Understanding Linux Network Internals》中的几个函数目前的内核里找不到了,取而代之的是netif_receive_skb中的下面代码:

    rx_handler = rcu_dereference(skb->dev->rx_handler);
    if (rx_handler) {
        if (pt_prev) {
            ret = deliver_skb(skb, pt_prev, orig_dev);
            pt_prev = NULL;
        }    
        switch (rx_handler(&skb)) {
        case RX_HANDLER_CONSUMED:
            ret = NET_RX_SUCCESS;
            goto unlock;
        case RX_HANDLER_ANOTHER:
            goto another_round;
        case RX_HANDLER_EXACT:
            deliver_exact = true;
        case RX_HANDLER_PASS:
            break;
        default:
            BUG();
        }    
    }  

skb->dev->rx_handler是在br_add_if这个添加port端口的时候给对应的dev赋值的,也就是说如果有个port是一个bridge的port了,那么通过其的sk_buff对象的dev->rx_handler就是被赋值的了,于是收包的时候就会走bridge的逻辑。赋值的内容为:

    err = netdev_rx_handler_register(dev, br_handle_frame, p); 

所以我们下面会分析下br_handle_frame的实现。从这里开始就又和《Understanding Linux Network Internals》同步了。br_handle_frame的基本逻辑录下:
1.判断port的状态,如果disable了则drop数据包。如果是FORWARDING/LEARNING则学习相关信息更新转发表。
2.判断STP是否enable,我们这边看简单的没有启动STP的
3.如果port状态不是FORWARDING则丢弃
4.走ebtables的逻辑,判断是routing还是bridging。如果是routing则直接返回
5.判断目的地址是不是接收设备,如果是的话设置pkt type为PACKET_HOST,然后走netfile的NF_BR_PRE_ROUTING这个hook
6.NF_BR_PRE_ROUTING返回可以继续处理这个包,则调用br_handle_frame_finish(如果我们走STP的路则此时会调用br_stp_handle_bpdu):

forward:
    switch (p->state) {
    case BR_STATE_FORWARDING:
        rhook = rcu_dereference(br_should_route_hook);
        if (rhook) {
            if ((*rhook)(skb)) {
                *pskb = skb;
                return RX_HANDLER_PASS;
            }
            dest = eth_hdr(skb)->h_dest;
        }
        /* fall through */
    case BR_STATE_LEARNING:
        if (ether_addr_equal(p->br->dev->dev_addr, dest))
            skb->pkt_type = PACKET_HOST;

        NF_HOOK(NFPROTO_BRIDGE, NF_BR_PRE_ROUTING, NULL, skb,
            skb->dev, NULL,
            br_handle_frame_finish);
        break;
    default:
drop:
        kfree_skb(skb);
    }
    return RX_HANDLER_CONSUMED;

我们来看br_handle_frame_finish,其会做如下的事情:
1.通过br_fdb_update更新转发表
2.根据目的mac查找转发表找对应的port,如果找到则调用br_forward,否则调用br_flood_frame广播。
3.如果设备处于混杂模式,或者目的mac地址就是这个port或应该走这个port,则调用br_pass_frame_up

以上三步就能说明一个入包的大致逻辑了。当网卡收到一个数据包的时候,由于这个卡的net_device是bridge的一个port,所以这个数据包的sk_buff-dev-rx_handler不为NULL,所以会走bridge的逻辑,然后通过查表发现要发给另一个此bridge下的port,于是调用br_forward发给这个port。然后另一个port的netif_receive_skb会的被调用,此时会走第三步,于是调用br_pass_frame_up,而此时就会走上层协议栈了。

另外如果一个数据包直接发给了bridge,则会调用其net_device绑定的发包函数,也就是br_dev_xmit。后者的逻辑和普通的port一样,首先查转发表,查到的话发个这个port,否则广播。

来看下代码:

int br_handle_frame_finish(struct sock *sk, struct sk_buff *skb)
{
    const unsigned char *dest = eth_hdr(skb)->h_dest;
    struct net_bridge_port *p = br_port_get_rcu(skb->dev);
    struct net_bridge *br;
    struct net_bridge_fdb_entry *dst;
    struct net_bridge_mdb_entry *mdst;
    struct sk_buff *skb2;
    bool unicast = true;
    u16 vid = 0;

    if (!p || p->state == BR_STATE_DISABLED)
        goto drop;

    if (!br_allowed_ingress(p->br, nbp_get_vlan_info(p), skb, &vid))
        goto out;

    /* insert into forwarding database after filtering to avoid spoofing */
    br = p->br;
    if (p->flags & BR_LEARNING)
        br_fdb_update(br, p, eth_hdr(skb)->h_source, vid, false);

    if (!is_broadcast_ether_addr(dest) && is_multicast_ether_addr(dest) &&
        br_multicast_rcv(br, p, skb, vid))
        goto drop;

    if (p->state == BR_STATE_LEARNING)
        goto drop;

    BR_INPUT_SKB_CB(skb)->brdev = br->dev;

首先是状态的判断以及转发表的操作。

    /* The packet skb2 goes to the local host (NULL to skip). */
    skb2 = NULL;

    if (br->dev->flags & IFF_PROMISC)
        skb2 = skb;

    dst = NULL;

    if (IS_ENABLED(CONFIG_INET) && skb->protocol == htons(ETH_P_ARP))
        br_do_proxy_arp(skb, br, vid, p);

    if (is_broadcast_ether_addr(dest)) {
        skb2 = skb;
        unicast = false;
    } else if (is_multicast_ether_addr(dest)) {
        mdst = br_mdb_get(br, skb, vid);
        if ((mdst || BR_INPUT_SKB_CB_MROUTERS_ONLY(skb)) &&
            br_multicast_querier_exists(br, eth_hdr(skb))) {
            if ((mdst && mdst->mglist) ||
                br_multicast_is_router(br))
                skb2 = skb;
            br_multicast_forward(mdst, skb, skb2);
            skb = NULL;
            if (!skb2)
                goto out;
        } else
            skb2 = skb;

        unicast = false;
        br->dev->stats.multicast++;
    } else if ((dst = __br_fdb_get(br, dest, vid)) &&
            dst->is_local) {
        skb2 = skb;
        /* Do not forward the packet since it's local. */
        skb = NULL;
    }

这里会有一个arp代理,这个在小秦之前的DVR的文章中有提到过。其余的代码会判断是不是广播或多播报文,是的话就多播。

    if (skb) {
        if (dst) {
            dst->used = jiffies;
            br_forward(dst->dst, skb, skb2);
        } else
            br_flood_forward(br, skb, skb2, unicast);
    }

    if (skb2)
        return br_pass_frame_up(skb2);

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

这里的逻辑是,如果dst = __br_fdb_get(br, dest, vid)) && dst->is_local,也就是如果这个包是本地的则走br_pass_frame_up,否则走br_forward/br_flood_forward。

来看下br_forward,按照我们的分析,这个应该调用目的口的发送方法:

/* called with rcu_read_lock */
void br_forward(const struct net_bridge_port *to, struct sk_buff *skb, struct sk_buff *skb0)
{           
    if (should_deliver(to, skb)) {
        if (skb0)
            deliver_clone(to, skb, __br_forward);
        else
            __br_forward(to, skb);
        return;
    }

    if (!skb0)
        kfree_skb(skb);
}

__br_forward的实现为:

static void __br_forward(const struct net_bridge_port *to, struct sk_buff *skb)
{           
    struct net_device *indev;
        
    if (skb_warn_if_lro(skb)) {
        kfree_skb(skb);
        return;
    }   
    
    skb = br_handle_vlan(to->br, nbp_get_vlan_info(to), skb);
    if (!skb)
        return;

    indev = skb->dev;
    skb->dev = to->dev;
    skb_forward_csum(skb);
             
    NF_HOOK(NFPROTO_BRIDGE, NF_BR_FORWARD, NULL, skb,
        indev, skb->dev,
        br_forward_finish);
}
......
int br_forward_finish(struct sock *sk, struct sk_buff *skb)
{   
    return NF_HOOK(NFPROTO_BRIDGE, NF_BR_POST_ROUTING, sk, skb,
               NULL, skb->dev,
               br_dev_queue_push_xmit);
        
}
......
int br_dev_queue_push_xmit(struct sock *sk, struct sk_buff *skb)
{   
    if (!is_skb_forwardable(skb->dev, skb)) {
        kfree_skb(skb);
    } else {
        skb_push(skb, ETH_HLEN);
        br_drop_fake_rtable(skb);
        dev_queue_xmit(skb);
    }

    return 0;
} 

可以看到,这里最终调用了dev_queue_xmit将包发送出去。注意:这一连串操作都是在同一个核上做的,这个对以后的性能评估会有影响。

再来看下br_pass_frame_up,按照我们的分析,这个应该调用目的口的接收方法:

static int br_pass_frame_up(struct sk_buff *skb)
{   
    struct net_device *indev, *brdev = BR_INPUT_SKB_CB(skb)->brdev;
    struct net_bridge *br = netdev_priv(brdev);
    struct pcpu_sw_netstats *brstats = this_cpu_ptr(br->stats);
    struct net_port_vlans *pv;

    u64_stats_update_begin(&brstats->syncp);
    brstats->rx_packets++;
    brstats->rx_bytes += skb->len;
    u64_stats_update_end(&brstats->syncp);

    /* Bridge is just like any other port.  Make sure the
     * packet is allowed except in promisc modue when someone
     * may be running packet capture.
     */
    pv = br_get_vlan_info(br);
    if (!(brdev->flags & IFF_PROMISC) &&
        !br_allowed_egress(br, pv, skb)) {
        kfree_skb(skb);
        return NET_RX_DROP;
    }

    indev = skb->dev;
    skb->dev = brdev;
    skb = br_handle_vlan(br, pv, skb);
    if (!skb)
        return NET_RX_DROP;

    return NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN, NULL, skb,
               indev, NULL,
               netif_receive_skb_sk);
}

最后的netif_receive_skb_sk就是我们的收包逻辑了,那么这里怎么保证这个数据包不会在走bridging呢?根据我们上面的分析,主要就是看sk_buff->dev->rx_handler是不是空。这里我们看到skb->dev = brdev,也就是我们的bridge。bridge的rx_handler是空的,所以接下来就走ip等上层协议了。注意,这里的一连串事情也都是发生在同一个核上的。另外netif_receive_skb_sk是poll调用软中断的软中断内部的实现,所以这个方法会等待其执行完成后才会返回,而不是走软中断这一套。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*