Ingress traffic control

相信很多人都知道 Linux 内核很难做 ingress 流量控制的,毕竟发送方理论上可以无限制地发送包到达网络接口。即使你控制了从网络接口到达协议栈的流量,你也很难从根源上控制这个流量。

利用 Linux 内核你可以轻松地完成前者,即控制从网络接口到达协议栈的流量。这个是通过一个叫做 ifb 的设备完成的,这个设备如此不起眼以至于很多人不知道它的存在。

它的使用很简单,加载 ifb 模块后,激活 ifbX 设备:

modprobe ifb numifbs=1
ip link set dev ifb0 up # ifb1, ifb2, ... 操作相同
`</pre>

然后就要通过 ingress tc filter 把流入某个网络接口的流量 redirect 到 ifbX 上:
<pre>`tc qdisc add dev eth0 handle ffff: ingress
tc filter add dev eth0 parent ffff: protocol ip u32 match u32 0 0 action mirred egress redirect dev ifb0

ingress 这个 qdisc 是不能加 class,但是我们这里使用了 ifb0,所以就可以在 ifb0 设备上添加各种 tc class,这样就和 egress 没有多少区别了。

ifb 的实现其实很简单,因为流入网络接口的流量是无法直接控制的,那么我们必须要把流入的包导入(通过 tc action)到一个中间的队列,该队列在 ifb 设备上,然后让这些包重走 tc 层,最后流入的包再重新入栈,流出的包重新出栈。看代码一目了然:

[c]

    while ((skb = __skb_dequeue(&amp;dp-&gt;tq)) != NULL) {
            u32 from = G_TC_FROM(skb-&gt;tc_verd);

            skb-&gt;tc_verd = 0;
            skb-&gt;tc_verd = SET_TC_NCLS(skb-&gt;tc_verd);

            u64_stats_update_begin(&amp;dp-&gt;tsync);
            dp-&gt;tx_packets++;
            dp-&gt;tx_bytes += skb-&gt;len;
            u64_stats_update_end(&amp;dp-&gt;tsync);

            rcu_read_lock();
            skb-&gt;dev = dev_get_by_index_rcu(dev_net(_dev), skb-&gt;skb_iif);
            if (!skb-&gt;dev) {
                    rcu_read_unlock();
                    dev_kfree_skb(skb);
                    _dev-&gt;stats.tx_dropped++;
                    if (skb_queue_len(&amp;dp-&gt;tq) != 0)
                            goto resched;
                    break;
            }
            rcu_read_unlock();
            skb-&gt;skb_iif = _dev-&gt;ifindex;

            if (from &amp; AT_EGRESS) {
                    dev_queue_xmit(skb);
            } else if (from &amp; AT_INGRESS) {
                    skb_pull(skb, skb-&gt;dev-&gt;hard_header_len);
                    netif_receive_skb(skb);
            } else
                    BUG();
    }

[/c]

这里有两个不太明显的地方值得注意:

1) 不论流入还是流出,tc 层的代码都是工作在 L2 的,也就是说代码是可以重入的,尤其是 skb 里的一些 header pointer;

2) 网络设备上的 ingress 队列只有一个,这也是为啥 ingress qdisc 只能做 filter 的原因,可以看下面的代码:

[c]
static int ing_filter(struct sk_buff skb, struct netdev_queue rxq)
{
struct net_device dev = skb->dev;
u32 ttl = G_TC_RTTL(skb->tc_verd);
int result = TC_ACT_OK;
struct Qdisc
q;

    if (unlikely(MAX_RED_LOOP %d)n",
                                 skb-&gt;skb_iif, dev-&gt;ifindex);
            return TC_ACT_SHOT;
    }

    skb-&gt;tc_verd = SET_TC_RTTL(skb-&gt;tc_verd, ttl);
    skb-&gt;tc_verd = SET_TC_AT(skb-&gt;tc_verd, AT_INGRESS);

    q = rxq-&gt;qdisc;
    if (q != &amp;noop_qdisc) {
            spin_lock(qdisc_lock(q));
            if (likely(!test_bit(__QDISC_STATE_DEACTIVATED, &amp;q-&gt;state)))
                    result = qdisc_enqueue_root(skb, q);
            spin_unlock(qdisc_lock(q));
    }

    return result;

}

static inline struct sk_buff handle_ing(struct sk_buff skb,
struct packet_type *pt_prev,
int
ret, struct net_device orig_dev)
{
struct netdev_queue
rxq = rcu_dereference(skb->dev->ingress_queue);

    if (!rxq || rxq-&gt;qdisc == &amp;noop_qdisc)
            goto out;

    if (*pt_prev) {
            *ret = deliver_skb(skb, *pt_prev, orig_dev);
            *pt_prev = NULL;
    }

    switch (ing_filter(skb, rxq)) {
    case TC_ACT_SHOT:
    case TC_ACT_STOLEN:
            kfree_skb(skb);
            return NULL;
    }

out:
skb->tc_verd = 0;
return skb;
}
[/c]

而经过 tc action 后是导入到了 ifb 设备的发送队列,见 net/sched/act_mirred.c 的 tcf_mirred() 函数:

[c]
//…
skb2->skb_iif = skb->dev->ifindex;
skb2->dev = dev;
err = dev_queue_xmit(skb2);
[/c]

也就是说这样就会有多个队列了,可以添加 class 了。:) 这大体也是 ingress traffic control 如何工作的了,可见 ifb 的存在既简单又巧妙地粘合了 egress 上的流量控制。

正如本文一开始讲到的,即使用 ifb 我们所做的流量控制仍然有限,很难从根本上控制发送方的速率。如果双方是通过交换机连接的话,或许通过 L2 上的一些协议可以控制速率,而如果中间经过路由器,那么所能做的就更加有限,只能期待传输层的协议进行控制,而不是靠传输层自身感受到丢包的多少去自动调整速率,因为这可能会花很长时间。

参考资料:

1. http://serverfault.com/questions/350023/tc-ingress-policing-and-ifb-mirroring
2. http://stackoverflow.com/questions/15881921/why-tc-cannot-do-ingress-shaping-does-ingress-shaping-make-sense