动手写一个探测网络质量(丢包率/RTT/队形等)的工具

还像往常一样,本文的内容没有收敛,依然是随笔式的备忘,而不是文档。人在外地,本不该来的,也挺沮丧,不过每周总结总是必不可少。
说到网络技术,我个人比较关注IP,其次是链路设备,然后才是TCP,这可能跟我第一次接触网络技术时所遇到的公司有关,它们是华为3Com以及Cisco,而不是Google,Yahoo或者BAT。
        然而能接触到的大多数的人可能更关注的是TCP,因为这是他们唯一能接触到的网络技术,虽然在侠义上,TCP并不属于网络,它是典型的一个端到端系统,网络只是它经由的一个路径。
        以公共交通系统为例,TCP比较类似票务和调度系统,它关注是否可以卖票,可以卖多少票,始发站是否可以发车,间隔多久发一班等等这种事情。至于车在途径道路上发生了什么,甚至途径哪些道路,票务系统并不关心,偶尔,小概率的,坐在公交总站的阿姨会接到电话,路上司机打来的,汇报一些突发情况,车坏了,车翻了,自己被捅了一刀...然后阿姨唯一能做的就是再派一辆车过去,她既无力修车,也无力修路,更无力(事实上也无权)惩罚歹徒...
...
        令人悲哀的痛点在于,一个承诺质量的端到端系统跑在一个根本无法保证质量的统计复用的分组交换网络里面!请注意,公路也是统计复用的分组交换网路。
         整个网络是拥塞的,而且随时都可能拥塞,如果我们想设计一个好的TCP,我们就必须能动态适应网络的当前状况,我们必须能够探知网络的当前状况,因此我们需要一个工具。
        在给出这个工具之前,我们先从Wireshark说起。

1.如何看Wireshark里TCP trace图

用Wireshark打开一个保存TCP流的pcap文件,点击“统计”-“TCP流图形”-“时间序列(tcptrace)”,我们可以看到一张图表,通过该图表可以探知关于网络状况的大部分细节:




然而,促使我写自己工具的动机在于,TCP trace这个图受到TCP本身的拥塞控制算法的控制,因此它并不客观!诚然,如果发生了拥塞,拥塞窗口要下降,但是具体下降到多少合适,这个就是拥塞控制算法说了算了,而它是个黑盒子!另外,如果线路有噪声产生了误码,按照比较垃圾的拥塞控制算法,也会降低窗口,从图表上看,你会误以为发生了拥塞...
        因此,我需要一个比较客观的工具,也就是说它对网络状态是无知的,也不奢望获知网络状态,它只是按照自己的参数固定速率发送数据,然后我们通过回应来探测网络到底发生了什么。

2.基于数据包守恒的ping -f

在写工具之前,首先看看现成的ping -f是否好用。
        这个工具自己试一下便知,不多谈。事实上,它是基于数据包守恒的,即收到了n个Reply,发出去n个Request,因此它会根据网络的状态自动调速,但是它计算的是一种保守状态的丢包率,
比如下面的序列:
1).发出100个Request,收到100个Reply。
2).发出100个Request,收到80个Reply,假设此时真的发生了拥塞丢包。
3).发出80个Request,收到80个Reply,假设拥塞马上缓解了。
4).请问怎么可以快速获知拥塞缓解了??

因此我更希望的是,固定数量发出数据包,然后看回应:
1).发出100个Request,收到100个Reply。
2).发出100个Request,收到80个Reply,假设此时真的发生了拥塞丢包。
3).发出100个Request,收到100个Reply,拥塞缓解马上被探知。

3.我的Python工具概述

在上面的篇幅,我给出了不用Wireshark和ping -f的理由:

不用Wireshark的理由:

它完全是TCP实现的行为勾画,在实现的很垃圾的TCP中,根本无法勾勒网络的状态,即便是在完美的TCP实现中,它表示的也是TCP如何反应网络状况变化的,这个信息丝毫没有指导意义,我们也很难知道网络到底发生了什么。总之,不同的TCP实现针对相同的网络质量会给出不同的数据,画出不同的图,而且,它太复杂了!

不用ping -f的理由:

很直接的说,ping -f跟tcptrace没有什么根本的不同,只是它的“拥塞控制”机制更加简单,完全就是根据数据包守恒来的。不多说了。
        我写这个工具完全是为了自用,而且确实也还可以,这个工具可以完成以下的功能:

1).探测丢包率

这种丢包率是客观的丢包率,包括噪声和排队造成的丢包。我基于ping -f修改了,把根据Reply的调速反馈机制去掉了,因为这样可以探测出更加详细的队形情况以及拥塞对丢包的影响。不然的话,如果使用ping -f,你不得不通过斜率来观测这种影响,而且当拥塞恢复,探测端无法及时感知。
        我在这里就不贴图了,基本就跟ping -f是一样的。

2).探测RTT波动

该工具最终会生成一个文件,该文件的截图如下:




基于这个图画出一个图,类似tcptrace那样的:




注意,我是用gnuplot画的,而不是Python,因为我在Windows上装Python画图库失败了,在Linux上没有X环境...所以本着UNIX组合小玩意的原则,我用Python生产并解析数据,然后用gnuplot来展现,我的画图脚本很简单:



3).探测队列的队形

和ping -f不同,我的工具可以生成点星文件("."表示收到了回应,"*"表示发送了数据),从点和星的分布,我们可以更加深入的探测队列细节。如下只是一个例子:



动态的图跟ping -f 几乎一样:




如果使用ping -f,你不得不时刻盯着点号的打印和消除...然后有个突增,随后有个突减,这意味着发生了排队且队列被突发清空...然而却没有留下数据事后分析。我的工具可以输出三种数据,首先,详细数据是可以无条件输出的,其次,你可以选择是看动态数据还是看静态的点星数据,事实上,通过详细的输出,完全可以生成后面两类数据,之所以在程序中直接支持,完全是为了方便。
        以RED队列为例,丢包往往是缓速随机的,然后如果拥塞不缓解,丢包就会趋于连续,表现为星号越来越连续且增多,点号的连续性则相反,趋于离散化...我的工具不重传,只观测,所以完全可以通过点星的分布来解析队列的细节,并且很有可能你能把路径上是否有UDP流氓找出来!即便你无法控制你的路径从而绕开,不也是可以反制一下么?你可以通过点星分布的变化情况得知拥塞是否由于发送端主动降速而缓解。

4.如何使用这个工具

首先,你不能盲目的去探测,你首先要有一个大概的拓扑。比如,还是baidu,我们想知道到达baidu的路径中的情况,利用上古神奇traceroute,我们可以得到很多信息:




为什么用Windows的tracert?因为我的虚拟机是NAT模式,特殊原因不能用Bridge,所以我用Windows...

得到了路径细节,我们可以逐步探测各个节点了,如下这样:
./ic.py 183.56.64.62 1 1000 1
./ic.py 14.29.121.206 1 1000 1
...

由于我程序的时间精度不够,很难探知更详细的信息,但是这个问题是可以10秒内解决的...为什么不解决,是因为我觉得这已经够了。

5.说到最后,代码呢?

前面扯了那么多,代码呢?代码在 github
        但是这里也贴一份吧:
#!/usr/local/bin/python

import sys
import time
from time import sleep,ctime

import signal
import threading
from scapy.all import *

# 指定目标IP地址
target = sys.argv[1]
# 指定执行次数
tot = int(sys.argv[2])
# 指定每次发送的包量
tot_per = int(sys.argv[3])
# 指定是否回显
vl = int(sys.argv[4])
flt = "host " + target + " and icmp"

handle = open("/dev/null", 'w')

out_list = []
in_list = []

def output():
	all = out_list + in_list
	all.sort(lambda x,y:cmp(x[3],y[3]))
	for item in all:
		print item[0], item[1], item[2], item[3]*10
	sys.stdout.flush()
        os._exit(0)

def signal_handler(signal, frame):
	output()

class ThreadWraper(threading.Thread):
	def __init__(self,func,args,name=''):
		threading.Thread.__init__(self)
		self.name=name
		self.func=func
		self.args=args

	def run(self):
		apply(self.func,self.args)
		
# 将结果输出到list
def printrecv(pktdata):
	if ICMP in pktdata and pktdata[ICMP]:
		seq = str(pktdata[ICMP].seq)
		if seq == tot_per + 2:
			return
		if str(pktdata[IP].dst) == target:
    			handle.write('*')
    			handle.flush()
			out_list.append(('+', 1, seq, time.clock()))
		else:
			if vl == 2:
    				handle.write('.')
			else:
    				handle.write('\b \b')
    			handle.flush()
			in_list.append(('-', 0, seq, time.clock()))

# 收到seq+2的包就停止抓包并终止程序
def checkstop(pktdata):
	if ICMP in pktdata and pktdata[ICMP]:
		seq = str(pktdata[ICMP].seq)
		if int(seq) == tot_per + 2 and str(pktdata[IP].src) == target:
    			handle.write("\nExit:" + ctime() + '\n')
			output()
			return True
	return False

# 发送线程
def send_packet():
	times = 0
	while times < tot:
		times += 1
    		send(IP(dst = target)/ICMP(seq = (0, tot_per))/"test", verbose = 0, loop = 1, count = 1)
    	send(IP(dst = target)/ICMP(seq = tot_per+2)/"bye", verbose = 0)

# 接收线程
def recv_packet():
	sniff(prn = printrecv, store = 1, filter = flt, stop_filter = checkstop)

def startup():
    	handle.write("Start:" + ctime() + '\n')

	send_thread = ThreadWraper(send_packet,(),send_packet.__name__)
	send_thread.setDaemon(True)  
	send_thread.start()

	recv_thread = ThreadWraper(recv_packet,(),recv_packet.__name__)
	recv_thread.setDaemon(True)  
	recv_thread.start()

	signal.pause()

if __name__ == '__main__':
	if vl != 0:
		handle.close()
		handle = sys.stderr
	signal.signal(signal.SIGINT, signal_handler)
	startup()

这个代码写的不好,因为我真的不怎么会编程。
        这个代码使用了令人陶醉的scapy!这个我5年前就接触过的东西直到今天才用起来。关于scapy的文档,我觉得比较好的是 这个

本文最后,再举一个例子来聊一下TCP

蝙蝠是个瞎子,TCP也是个瞎子。蝙蝠虽瞎但不会撞墙,依靠的是声波定位,而TCP虽瞎也能保持平滑发送,依靠的是ACK时钟流。而这个引发了另一个思路,其效果就是,我们可以采取一些措施撞死蝙蝠。
        TCP和蝙蝠不同的是,蝙蝠发出的声波无法被缓存,它永远直来直去,而TCP发出的数据包却可以被中间的网络设备缓存很久,因此,TCP测出的RTT除以2并不一定是数据包到达接收端的时间!如果你不理解我的意思,请考虑以下场景:
0).发送端到接收端的路径,路径传输用时为10秒,因此不考虑缓存,来回为20秒;
1).时间点A,数据包P发出;
2).到达接收端前,经过了5秒,数据包P在路径正中间被一个设备缓存了10秒;
3).在时间点A的15秒后数据包P继续前行,又过了5秒,到达接收端;
4).接收端对数据包P的ACK未缓存经过了10秒返回到发送端。

经过计算得到时间点A发送的数据包P的RTT为5+10+5+10=30秒,除以2就是15秒,请问15秒是发送端到达接收端的时间吗?显然不是!如果TCP不是瞎子,那么它肯定知道上述的过程,然而TCP是个瞎子,所以它不知道上面的具体过程。如果数据包P没有被缓存,显然只需要10秒就能到达接收端,然而现在却算出来是15秒,这多出来的5秒发生了什么?TCP不知道,因此传统上,TCP会认为发生了拥塞排队。
        但是对于拥塞排队的细节,TCP真的很容易判断清楚吗?
        TCP会把RTT的陡增视为发生了拥塞,但是:
A.如果排队发生在数据包P到达接收端的过程中,数据包到达接收端的总时间为20秒,RTT/2=15秒,此时TCP少算了5秒;
B.如果排队发生在ACK返回到发送端的过程中,数据包到达接收端的总时间为10秒,RTT/2=15秒,此时TCP多算了5秒。
B-1.如果ACK发生了拥塞排队或者丢弃,后面的ACK会连带着代替该ACK去确认已经发送的数据,TCP对待数据包P和ACK是不同的!

因此,RTT陡增(并且持续保持高值)可以比较容易定性拥塞的发生,却很难从RTT抖动情况去辨析拥塞发生的细节,就更别说去区分偶然的噪声丢包还是拥塞丢包了...
于是回声定位技术给了我一些思路,这个思路其实也想了蛮久了。我们可以从如何发现中间网络设备的流量整形说起...

写在最后

虽然我是一个典型的IP粉,但我丝毫没有贬低TCP的意思,事实上我也不常接触到中间网络的设备,我之所以总是对TCP表现的不屑一顾是因为我总是发现很多所谓的“精通网络编程”的人事实上只是“精通编程”而根本不懂网络,对此的另一个极端,是我在2013到2014年的时候碰到的一些CCIE,他们真的十分精通网络,但是却丝毫不懂socket编程,也不懂协议栈的实现,而我在多年的时间内试图调和两者。说实话,我鄙视过那些玩弄网络设备而不懂编程的人,也鄙视过那些在端主机编程但是不懂网络的人,但其实我更鄙视的是我自己。

        tcpdump/Wireshark/tshark抓包工具是利器,但是绝对不是唯一的,碰到问题仅仅依赖分析数据包的人事实上很大的可能是他曾经从事过很长一段时间协议分析和逆向的工作,在这段时间形成了自己的工作方式,如果是一个从事路由器交换机工作的人,他可能就会依赖另外一种工具了。这就是网络的复杂性之体现。

        如果我碰到那些CCIE曾经看不惯他们天天的HSRP,BGP,PolicyRouting,SpanningTree,Teaming等等之来之去的,同时又对程序员天天socket,TCP,通告窗口,抓包等等之来之去的有意见,那只能说明一个问题,我是贱人,我是傻逼。
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页