实验5 运输层和应用层协议解析
一、 实验目的
本实验通过运用Wireshark对网络活动进行分析,观察TCP协议报文,分析通信时序,理解TCP的工作过程,掌握TCP工作原理与实现;学会运用Wireshark分析TCP连接管理、流量控制和拥塞控制的过程,发现TCP的性能问题。
二、 实验内容
任务1:TCP正常连接观察
实验准备:
用PC1 ping 一下PC2,看是否能ping通,观察到两主机能联通:
- 利用 python 自带的 SimpleHTTPServer 模块,在 PC2 上启动一个简易的 web 服务器。终端上运行 echo "TCP lab test" > index.html 创建 index.html 文件为测试站首页,运行 sudo python -m SimpleHTTPServer 80 启动一个简易 web 服务器;打开新终端,键入 ss -tln查看当前主机打开的 TCP 连接,确认 80 端口处于监听状态。
- 在 PC1 上打开一个终端,键入 sudo wireshark 启动抓包软件;再打开一个新终端,键入curl <PC2 的 IP> ;停止抓包,在 wireshark 过滤出 TCP 类型报文。观察首个 TCP 报文头,并分析各段值代表的意义。如果想要关闭相对序号/确认号,可以选择 Wireshark 菜单栏中的Edit→Preference→protocols→TCP,去掉 Relative sequence number 勾选项。使用 Wireshark 内置的绘制流功能,选择菜单栏中的 Statistics→Flow Graph, Flow Type 选择 TCP flows 可以直观地显示 TCP 序号和确认号是如何工作的。
实验要求:
- 利用Wireshark,抓包分析并截图,分析该报文TCP首部各字段的定义、值及其含义
(1) 利用Wireshark抓包并截图(保存在tcp1.pcapng中):
在Wireshark软件中设置display filter如下,进行报文过滤:
抓取到整个TCP报文流:
使用Statistics->Flow Graph工具,可以清楚地看到三次握手过程:
四次挥手过程:
可是,这里我观察到了一个“奇怪”的现象。不是说好了四次挥手吗?怎么只看到两次挥手。为了解决这个问题,我上网查找了很多资料。首先连接释放的时候不一定需要四次挥手,事实上,很多情况下都只有三次挥手。而满足三次挥手需要两个条件,当被动关闭方(上图的客户端)在TCP挥手过程中,[没有数据要发送]并且[开启了TCP延迟确认机制],那么第二次和第三次挥手就会合并传输,这样就出现了三次挥手。这里的合并传输也就是这个FIN包和ACK包合并传输了。
那么,分析完了四次挥手变成三次挥手的原因,可是我这里观察到的只有两次挥手啊,这是为什么呢?
这个问题困惑了我一整天,查阅资料发现,并不一定是客户端会主动断开连接,服务器端也有这个功能。于是我就去看了[FIN,ACK]之前的那个由服务器发给客户端的HTTP包,果然找到了答案:
我发现这个报文里的FIN标志位竟然是1,说明这是服务器向客户端发送的第一个FIN包,也就是第一次挥手。于是,四次挥手便再次显现出来。
(2) 分析报文TCP首部各字段的定义、值及其含义
这里,我选择了三次握手的第一个报文:SYN报文进行分析
下面依据TCP报文的首部格式分析该TCP报文首部各字段:
前两个字节是16位源端口号,该报文的源端口号为34318
接下来两个字节是16位目标端口号,该报文的目标端口号为80
接下来四个字节是32位客户端随机初始化的序列号,该报文的序列号为1551970934
接下来四个字节是32位的确认应答号,该报文是三次握手的第一个SYN报文,没有设置确认应答号,故该报文确认应答号为0
接下来4位bit是首部长度,该报文的首部长度为40Bytes
接下来6位标志位保留,报文中都为0
接下来6位标志位,该报文中只有SYN被置1
接下来两个字节是窗口大小,该报文的窗口大小为64240
接下来两个字节是校验和
接下来两个字节是紧急指针字段,该报文URG标志位为0,故这里的紧急指针字段也为0
最后的20字节是选项字段,这里不多分析
至此,该报文的首部各字段分析完毕。
- 画出该TCP流的流图
任务2:TCP异常传输观察分析
- 尝试连接未存活的主机或未监听端口
(1) 用 curl 访问一个不存在的主机 IP,抓包观察共发送了几次 SYN 报文。根据每次时间间隔变化,估算 RTO(重传超时)。
抓包观察到共发送了两次SYN报文:
估算RTO:
由上图可以大致估算出,RTO ≈ 9.514 – 6.443 = 3.071s
(2) 查看 Linux 主机的系统的 TCP 参数 SYN 重传设定:
`cat /proc/sys/net/ipv4/tcp_syn_retries`
(3) 更改 SYN 重传次数为 3:
`echo "3" > /proc/sys/net/ipv4/tcp_syn_retries`
注意,这里要切换到root用户下才能更改,否则会“Permission Denied”
(4) 再次 curl 访问,观察抓包内容。
(5) 关闭服务器端的 SimpleHTTPServer(ctrl+C 中断,或关闭所在终端),客户端 curl 访问服务器 80 端口,观察应答报文。
(6) 运行 nmap -sS <PC2 的 IP> 扫描服务器,并抓包。
(7) 在报告中总结以上观察结果,解释 SYN 扫描原理。
- 观察客户端发送了第一个SYN连接请求,服务器无响应的情景
(1) 服务器开启 telnet 或 ssh 服务,客户端先尝试连接服务器,连接成功后,在双方键入 ss -tan 查看所有 TCP 连接状态。我们看到的 TCP 连接建立过程同 1 中的 HTTP 访问类似。在客户端,利用 iptables 拦截服务器回应的 SYN ACK 包,命令如下:
` sudo iptables -I INPUT -s 192.168.13.128 -p tcp -m tcp --tcp-flags ALL SYN,ACK -j DROP `
为了让服务器能够开启ssh服务,需要先`sudo apt install openssh-server`
输入`sudo service ssh start`启动ssh服务
输入`sudo ps -e |grep ssh`查看ssh服务是否启动,观察到服务器已启动:
接下来尝试使用客户端连接服务器,服务器的ip为192.168.13.128
在客户机上使用`sudo ssh [email protected]`远程连接服务器(注意这里需要root权限,否则会Permission Denied):
看到如上界面说明成功远程连接到了服务器。
连接成功后,在双方键入`ss -tan`查看所有TCP连接状态:
在客户端输入命令` sudo iptables -I INPUT -s 192.168.13.128 -p tcp -m tcp --tcp-flags ALL SYN,ACK -j DROP `利用iptables拦截服务器回应的SYN ACK包:
(2) 再次尝试连接并启动 wireshark 抓包,并在双方多次用 ss -tan 观察 TCP 状态。
服务器:
客户机:
(3) 观察 TCP 的状态变化,分析 wireshark 捕获的 TCP 异常报文。
会产生这样的异常报文的原因是,由于我们设置了防火墙,阻塞了从服务器端发过来的SYN&ACK包,这样客户机发第一个SYN包后,就始终不会收到服务器端的SYN&ACK包,它就会以为是传输过程中丢包了,于是重新传输SYN包;而服务器那边也就不会收到客户机发来的三次挥手中的最后一个ACK包,超时后就会重传SYN&ACK包。这就是产生图中异常报文的原因。
(4) 服务端的 SYN-RECV 状态何时释放?
当终端显示”Connection timed out”时释放
(5) SYN ACK 重传了几次,时间间隔有何变化?
可以观察到SYN ACK重传了11次
可以观察到时间间隔一开始呈指数级别增大,最后稳定在16s左右
(6) 参考 1 中的操作,在服务端修改 SYN ACK 重传次数 (tcp_synack_retries),再次观察,此任务结束后清空防火墙规则 (iptables -F)。
在服务端改重传次数为3次:
再次观察(注意这里我连续尝试连接了两次):
清空防火墙规则:
之后就可以顺利连接了:
任务3:拥塞控制
- 配置虚拟机设置:
- 使用ftp传输大文件:
首先,我自己编写了一个程序gen_ran.c,使用命令`./gen_ran <filename> [size(MB)]`
该程序可生成一个大小为size的文件,名称为filename,size可选,默认为100MB
使用`./gen_ran scpfile`生成一个大小为50MB的大文件scpfile:
使用ftp传输该文件,同时使用wireshark抓包:
首先在终端输入命令`ftp 192.168.13.128`与服务端连接:
连接成功后显示如上。
然后使用命令`put ftpfile`传输大文件,同时wireshark抓包:
- 传输完毕,进行结果分析(该结果存放在ftp.pcapng中):
刚开始时,执行慢开始算法,说是慢开始,其实它并不慢,因为它是呈指数增长的。
超时,网络发生拥塞,这时可能连续收到三个重复确认,执行快恢复算法,拥塞窗口变为原来的一半。
然后拥塞避免算法,加法增大拥塞窗口。
任务4:HTTP协议分析
- 搭建HTTP1.0服务器
任务1搭建的即为HTTP1.0服务器。使用命令` sudo python -m SimpleHTTPServer 80`在虚拟机UbuntuV2上搭建服务器,在虚拟机Ubuntu 64bit上使用命令`curl 192.168.13.128`向服务器请求数据,
即可抓到HTTP1.0报文:
data的内容即为网页中的内容。
- 搭建HTTP1.1服务器
在Ubuntu 64bit中编写python脚本(源代码文本见附件),将协议的版本类型修改为HTTP/1.1,即可搭建HTTP1.1服务:
终端输入命令运行服务器,并打开wireshark进行抓包:
输入后,终端会自动打开一个网页,该网页是我在该目录下写的一个html文件(源码见附件):
可以使用wireshark捕捉到HTTP1.1的报文:
- 搭建HTTP2.0服务器
在搭建HTTP1.1的基础上,将协议的类型修改为HTTP2.0即可:
与上一步类似,使用wireshark抓包,可以抓到TLS1.2的报文,该报文是HTTP2.0报文加密后的结果:
若想要分析HTTP2.0报文,需要对wireshark进行解密,步骤如下:
(1) 配置系统环境变量:
在~/.profile文件下加入`export SSLKEYLOFFILE=/home/rongrong/Desktop/sslkey.log`
终端输入命令:`source ~/.profile`
重启Ubuntu。
然后打开一个浏览器,会看到密钥已经写入到sslkey.log文件中了:
(2) 配置Wireshark:
Wireshark->Edit->Protocols->TLS下,将(Pre)-Master-Secret log filename设置为刚刚设置的环境变量值:
然后就可以看到HTTP2.0报文
点开该报文可以看到,内容与index.html中的一致:
- 接下来对三种HTTP版本的报文进行分析
(1) HTTP1.0
右键点击该报文,选择Follow->HTTP Stream可以追踪HTTP流:
其中,红色的部分是请求格式,蓝色的部分是响应格式,可以很清楚地看到该HTTP流的请求响应流程。
可以分析一下HTTP1.0的报文结构:
起始行是状态行:HTTP/1.0 200 OK
可以看到版本为HTTP1.0;状态码为200,表示成功;短语为OK。
(2) HTTP1.1
与上一步的(1)相同,可以肯定看到HTTP stream:
可以对HTTP1.1的请求报文结构和响应报文结构进行分析:
起始行除了版本,其他与HTTP1.0相同
可以看到Server与HTTP/1.0不同,HTTP/1.1的Server为nginx;
HTTP/1.1报文还会将使用的是什么浏览器显示出来:Via: 1.1 google
HTTP/1.1相比于HTTP/1.0做了如下几点优化:
- 使用长连接的方式改善了HTTP/1.0短连接造成的性能开销
- 支持管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。
(3) HTTP2.0
与前面两步相同,我们同样可以在这里看到HTTP2.0的HTTP stream:
HTTP/2.0协议与HTTP/1.1协议有很大的不同,前者把后者存在的性能问题全部一一攻破了。
HTTP/2.0相比于HTTP/1.1在性能上的改进有如下几点:
l 头部压缩
HTTP/2 会压缩头(Header)如果你同时发出多个请求,他们的头是一样的或是相似的,那么,协议会帮你消除重复的部分。
这就是所谓的 HPACK 算法:在客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。
l 二进制格式
HTTP/2 不再像 HTTP/1.1 里的纯文本形式的报文,而是全面采用了二进制格式,头信息和数据体都是二进制,并且统称为帧(frame):头信息帧(Headers Frame)和数据帧(Data Frame)。
这一点观察报文也能稍微看出一些区别。
这样虽然对人不友好,但是对计算机非常友好,因为计算机只懂二进制,那么收到报文后,无需再将明文的报文转成二进制,而是直接解析二进制报文,这增加了数据传输的效率
比如状态码 200 ,在 HTTP/1.1 是用 '2''0''0' 三个字符来表示(二进制:00110010 00110000 00110000),共用了 3 个字节,如下图:
在 HTTP/2 对于状态码 200 的二进制编码是 10001000,只用了 1 字节就能表示,相比于 HTTP/1.1 节省了 2 个字节,如下图:
l 并发传输
l 服务器主动推送资源
三、 实验结果分析
本次实验遇到了很多奇怪的现象,比如对于任务1,始终无法观察到连接释放时候的四次挥手,只能观察到两次挥手:由客户机向服务器发送一个FIN包,然后服务器再向客户机发送一个ACK包,然后就结束了,并没有观察到服务器向客户机发送的FIN包,如下图:
可以看到只有两次。
多次尝试好不容易抓到了一个四次挥手的报文,可是奇怪的事情又发生了。按理来说第一次的FIN包应该由客户机发给服务器才对,但是我观察到的却是服务器先发送非客户机:
发生这个现象的原因可能是HTTP/1.0是短连接的,每发起一个请求都要新建一次TCP连接,这样服务端就会先发FIN包;若是使用HTTP/1.1则不会是这样的现象。
经过我的不懈努力,搜索了大量的有关TCP抓包的资料,终于解决了上述两个问题。
以及在做观察快恢复现象的时候无法观察到与教材上相同的现象,只能根据包的数量大致分析这个阶段是在执行哪个算法。
除此以外,本次实验都完成地比较顺利。
四、 实验小结与感想
据老师所说,本次实验是最后一个需要写实验报告的实验。太好啦!终于不用写实验报告啦!!!
不过,和wireshark打了快一个学期的交道了,我深深感受到了wireshark这个抓包工具的强大。它除了可以抓包外,还提供了可视化分析网络包的图形界面,还内置了一系列的汇总分析工具。就拿本次实验来说,我就用到了许多除了抓包以外的工具,比如Flow Graph以及IO Graph里面的HTTP Stream等工具。通过这些工具分析流,可比光看报文方便多了。
五、 思考题
- 在 TCP 状态机中,有些状态停留时间较长,易观察到,有些状态很短暂不易观察到。试列出不易观察到的状态,并考虑观察到它们的可能方法。
不易观察到的状态有: FIN_WAIT_1、FIN_WAIT_2、CLOSE_WAIT、LAST_ACK。可以阻断中间的某个报文,以观察到接下来预计达到的状态。
- TCP 在不可靠的 IP 层上建立了可靠的端对端连接,如果要在不可靠的 UDP 上建立可靠的端对端传输系统,需要考虑哪些方面?
现在市面上已经有基于 UDP 协议实现的可靠传输协议的成熟方案了,那就是 QUIC 协议,已经应用在了 HTTP/3。
要基于 UDP 实现的可靠传输协议,那么就要在应用层下功夫,也就是要设计好协议的头部字段。
需要把TCP 可靠传输的特性(序列号、确认应答、超时重传、流量控制、拥塞控制)在应用层全部实现一遍。
六、 附件
gen_ran.c
#include<stdio.h> #include<stdlib.h> #include<time.h> #include<stdint.h>
#define ONE_MB_SIZE 262144 int32_tout[ONE_MB_SIZE];
intmain(intargc, char* argv[]) { if(argc < 2 || argc > 3) { fprintf(stderr, "Usage: ./gen_ran <filename> [size(MB)]\n"); exit(1); } int size; if(argc == 2) size = 100; // 如果只有两个参数即只有一个文件名称默认生成100MB的文件 else size = atoi(argv[2]); FILE* outfile; if((outfile = fopen(argv[1], "wb")) == NULL) { fprintf(stderr, "open error\n"); exit(1); } srand((unsignedint)time(0)); int i, j; for(i = 1; i <= size; i++) { for(j = 0; j < ONE_MB_SIZE; j++) { out[j] = rand(); } fwrite(out, sizeof(int32_t), ONE_MB_SIZE, outfile); } fprintf(stdout, "random input file %s was generated successfully\n", argv[1]); exit(0); } |
HTTPserver.py
import json from http.server import HTTPServer, SimpleHTTPRequestHandler import webbrowser
ip = "localhost" # 监听IP,配置项 port = 8800 # 监听端口,配置项 index_url = "http://%s:%d/index.html" % (ip, port) # 监听主页url,配置项
# 创建http server classGetHttpServer(SimpleHTTPRequestHandler): protocol_version = "HTTP/1.0" server_version = "PSHS/0.1" sys_version = "Python/3.9.x" target = "./" # 监听目录,配置项
defdo_get(self): ifself.path.find("/json/") > 0: print(self.path) self.send_response(200) self.send_header("Content-type", "json") self.end_headers() req = {"success": "ok"} self.wfile.write(req.encode("utf-8")) else: SimpleHTTPRequestHandler.do_GET(self)
defdo_post(self): ifself.path == "/signin": print("postmsg recv, path right") else: print("postmsg recv, path error") data = "" data = json.loads(data) self.send_response(200) self.send_header("Content-type", "text/html") self.end_headers() rspstr = "recv ok, data = " rspstr += json.dumps(data, ensure_ascii=False) self.wfile.write(rspstr.encode("utf-8"))
defhttp_server(): server = HTTPServer((ip, port), GetHttpServer) try: # 弹出窗口 webbrowser.open(index_url) # 输出信息 print("服务器监听地址: ", index_url) server.serve_forever() exceptKeyboardInterrupt: server.socket.close()
# 执行服务器脚本 http_server() |
index.html
<!DOCTYPEhtml> <htmllang="en"> <head> <metacharset="UTF-8"> <metahttp-equiv="X-UA-Compatible"content="IE=edge"> <metaname="viewport"content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> hello world </body> </html> |