在Linux上为指定IP端口模拟网络收发包延迟


  编写网络应用程序时,我们一般都是在网络状况良好的局域网甚至是本机内进行测试调试。有没有办法在网络状况良好的内网环境中,在不改动程序自身代码的前提下,为应用程序模拟复杂的外网环境——尤其是网络延迟呢?这是我在学校写网络程序时就有过的想法,只是一直没认真研究,直到最近在公司编写跨服代码。

 

  跨服涉及多台服务器之间,还有服务器与客户端之间的通讯,流程很复杂,其中每一步都要正确处理网络异常延迟与断开的情况。测试人员通过改代码或下断点的方式来测试网络延迟是极麻烦的,而且能模拟的延迟用例也很有限。因此如果有一个第三方工具为应用程序使用的某个socket(IP端口)模拟网络延迟,那测试人员应该会非常喜欢的。

 

  最初找到的工具有Linux自带的tc命令(需要配合tc自带的模块netem)和一个第三方工具dummynet。前者概念很复杂,命令行参数也很复杂。后者跨平台,在Windows上也可用;但在Linux上安装非常麻烦,为了编译dummynet提供的内核模块,需要编译正确版本的Linux内核源代码——我在这一步卡了很久,一直没搞定。最终还是决定用tc。计划的方案是用tc为服务端端口分别设置收包和发包的网络延迟,这样可解决tc只能工作在Linux中的问题。tc手册和网上很多文章都提到tc只能设置发包延迟,而无法设置收包延迟。但只要配合Linux自带的ifb(Intermediate Functional Block device)内核模块和一点小技巧,tc就可以设置收包延迟。

 

  其实tc可以很简单地为一块网卡设置网络延迟:

 

# tc qdisc add dev wlan0 root netem delay 1s

 

  这条命令给无线网卡wlan0发送的包设置了1秒网络延迟。

 

  可以通过ping局域网中的其它机器来验证:

 

# ping -c 4 192.168.1.5

PING 192.168.1.5 (192.168.1.5) 56(84) bytes of data.

64 bytes from 192.168.1.5: icmp_req=1 ttl=64 time=1002 ms

64 bytes from 192.168.1.5: icmp_req=2 ttl=64 time=1001 ms

64 bytes from 192.168.1.5: icmp_req=3 ttl=64 time=1002 ms

64 bytes from 192.168.1.5: icmp_req=4 ttl=64 time=1004 ms

 

---192.168.1.5 ping statistics ---

4 packets transmitted, 4 received, 0% packet loss, time 3006 ms

rtt min/avg/max/mdev = 1001.446/1002.830/1004.967/1.642 ms


  但是这样会影响所有通过该网卡发送的包。这不是我想要的。我只想给服务器上指定的端口设置网络延迟,不想影响其它端口,所以还是把这条延迟规则去掉吧:

 

# tc qdisc del dev wlan0 root

 

  为了只给指定的IP端口设置延迟,我们需要使用tc中的三个有点复杂的概念:qdisc(排队规则)、class(类)和filter(过滤器)。我花了很多天才基本理解它们是如何组合在一起工作的。这里我不打算详细解释这些概念(想详细了解的可查看文末列出的参考资料),只写下我是怎么做的。


  假设现在本机上有两个相互通讯的应用程序在运行,程序A在端口14100监听,程序B和A的14100端口之间建立了TCP连接。我想在B到A的通讯方向上设置延迟,方法是在本地环回网卡的发送端设置qdisc与filter,过滤所有发给本地14100端口的包,并给这些包设置延迟。

 

  首先在本地环回网卡lo添加一条root qdisc:

 

# tc qdisc add dev lo root handle 1: prio bands 4

 

  这条qdisc下设4个class,handle id为1:。在没有filter的情况下,tc从IP协议层收到的包会根据IP包头的TOS(Type of Service)字段进入第1~第3个class(与pfifo_fast规则相同),第4个class是没用的。现在给第4个class添加一个5秒延迟的qdisc:


# tc qdisc add dev lo parent 1:4 handle 40: netem delay 5s

 

  给root qdisc添加一个filter,将发给14100端口的包都送到第4个class:

 

# tc filter add dev lo protocol ip parent 1:0 prio 4 u32 \

match ip dport 14100 0xffff flowid 1:4

 

  这样就可以了。

 

  如果要撤销网络延迟,可以把filter删掉。先列出filter的信息:

 

# tc -s filter show dev lo

filter parent 1: protocol ip pref 4 u32

filter parent 1: protocol ip pref 4 u32 fh 800: ht divisor 1

filter parent 1: protocol ip pref 4 u32 fh 800::800 order 2048 key ht 800bkt 0 flowid 1:4 (rule hit 672 success 76)

match 00003714/0000ffff at 20 (success 76 )

 

  上面的信息显示有76个包被filter过滤了出来,这些包都是由本地环回网卡发给14100端口的。现在删除filter:

 

# tc filter del dev lo pref 4

 

  不过,上面的情景是两个应用程序都在本地,因此可以通过设置环回网卡的发送端来变相控制14100端口(在环回网卡上)的收包速度。如果程序B在另一台机器上,那就需要ifb的配合了。ifb会在系统中开辟出一块虚拟网卡。如果我们将wlan0(实际网卡)收到的包重定向到ifb,ifb就会将收到的包又发回给wlan0,最后仍然通过wlan0送给IP层,上层协议毫不知情。因此通过设置ifb的发包延迟就可以实现wlan0的收包延迟。


  为了使用ifb,首先需要载入ifb内核模块,这个模块在Debian 7中是自带的:

 

# modprobe ifb

 

  通过ip命令可看到系统中多出了ifb0和ifb1两块网卡:

 

# ip link list

1:lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc prio state UNKNOWN mode DEFAULT

link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

2:eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT qlen 1000

link/ether 60:eb:69:99:66:54 brd ff:ff:ff:ff:ff:ff

3:wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mqstate UP mode DORMANT qlen 1000

link/ether 1c:65:9d:a9:db:01 brd ff:ff:ff:ff:ff:ff

10:ifb0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 32

link/ether 7a:2a:33:6b:e7:f7 brd ff:ff:ff:ff:ff:ff

11:ifb1: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 32

link/ether ce:ba:f4:38:df:6c brd ff:ff:ff:ff:ff:ff


  启动ifb0网卡:

 

# ip link set ifb0 up

 

  确认ifb0网卡已启动:


# ip link list

1:lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc prio state UNKNOWN mode DEFAULT

link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

2:eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT qlen 1000

link/ether 60:eb:69:99:66:54 brd ff:ff:ff:ff:ff:ff

3:wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DORMANT qlen 1000

link/ether 1c:65:9d:a9:db:01 brd ff:ff:ff:ff:ff:ff

10:ifb0: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN mode DEFAULT qlen 32

link/ether 7a:2a:33:6b:e7:f7 brd ff:ff:ff:ff:ff:ff

11:ifb1: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 32

link/ether ce:ba:f4:38:df:6c brd ff:ff:ff:ff:ff:ff

 

  在wlan0添加ingress qdisc,即收包的排队规则:

 

# tc qdisc add dev wlan0 ingress

 

  将wlan0收到的包重定向到ifb0:

# tc filter add dev wlan0 parent ffff: \

protocol ip u32 match u32 0 0 flowid 1:1 action mirred egress redirect dev ifb0

  接下来像之前一样,在ifb0的发送端设置qdisc和filter,为发送到14100端口的包设置5秒延迟:

# tc qdisc add dev ifb0 root handle 1: prio bands 4

# tc qdisc add dev ifb0 parent 1:4 handle 40: netem delay 5s

# tc filter add dev ifb0 protocol ip parent 1:0 prio 4 u32 \

match ip dport 14100 0xffff flowid 1:4

 

  大功告成!从头到尾整个过程都没有对应用程序本身做任何修改,也没有改变网络协议的行为,也没有影响机器上其它正在运行的程序。

 

  不过这些tc命令对于测试人员来说仍然太复杂了,毕竟tc的目标用户似乎是专业网管和系统管理员。本来只想简单地模拟网络延迟,却没想到最后发现这涉及一个很大的课题——流量控制Orz。届时我还要把它们封装成简单的命令才行。

相关内容