写点什么

网络抓包实战 03——TCP/IP 协议栈:数据包如何穿越各层协议

发布于: 2 小时前
网络抓包实战03——TCP/IP协议栈:数据包如何穿越各层协议

所有互联网服务,均依赖于 TCP/IP 协议栈。懂得数据是如何在协议栈传输的,将会帮助你提升互联网程序的性能和解决 TCP 相关问题的能力。

我们讲述在 Linux 场景下数据包是如何在协议层传输的。

1. 发送数据

应用层发送数据的过程大致如下

我们把上述处理过程的区域大致分为:

  1. User 区域

  2. Kernel 区域

  3. Device 区域

在 user 和 kernel 区域的任务都是由本机 cpu 执行,这两个区域合并称为 host 区域,以区分 device 区域(网络接口卡上有单独的 cpu)。device 是接收和发送数据包的网络接口卡(Network Interface Card),一般也称为 LAN card。

当应用程序调用 write(fd, buf, len)来发送数据时,用户态区域会进入内核态区域,建立这个关系的纽带是 socket fd 和系统调用 write。

在内核态的 socket 有两个 buffer:

  1. send socket buffer,用于发送数据

  2. receive socket buffer,用于接收数据

当 write 系统调用被执行,用户态的数据(buf,长度)会被拷贝到内核区域的内存,并被放入到 send socket buffer 的末尾(见下图,发送是按照顺序发送的),然后 TCP 就会被调用。

TCP 中的数据结构是 TCB(TCP Control Block)。TCB 包含了执行 TCP 会话所需要的信息,包括 TCP 连接状态,接收窗口,拥塞窗口,序号,重传 timer 等。

TCP 会创建 TCP 数据分段,而 TCP 数据分段包括 TCP header 和 payload,如下图:

Payload 是待发送的 socket buffer 中的数据,而 TCP header 是为了 TCP 可靠发送数据而加的辅助信息。

这些数据分段会进入到 IP 层,IP 层会加上 IP 头部信息到数据分段,如下图:

IP 在执行路由之前会去检查 Netfilter LOCAL_OUT 钩子,看是否需要执行 iptables 相关配置。之后执行 IP 路由。IP 路由主要功能是寻找下一跳(例如网关或路由器)的 IP 地址,而路由的目的是到达目的地 IP 地址所在的机器。

IP 执行路由之后,检查 Netfilter POST_ROUTING 钩子,如果有 iptables 在这方面的配置,就会去执行相关操作。委托给数据链路层之前,IP 层还会执行 ARP(网络地址转换),通过下一跳 IP 地址来查找目的 MAC 地址,并把 Ethernet 头部添加到 IP 数据包,如下图。

IP 层同时还给用户提供了 raw socket 接口,即发送数据包的接口。raw socket 发送的数据包与正常流程的数据包不一样,在执行 Netfilter 的时候,会跳过这些钩子。

IP 层做完工作以后,会把数据包(上图中的数据包,一般称 frame)委托给数据链路层。

由于 ARP 已经把目的 MAC 地址写入到数据包头部,这样就减轻了驱动 driver 的工作。进入数据链路层后,内核会去检测是否有抓包工具在监听抓包(例如 tcpdump),如果有,内核会拷贝数据包信息到抓包工具的内存地址空间。

之后,根据一定的协议规则,驱动 driver 会要求 NIC 传递这个数据包。当 NIC 收到这个请求后,NIC 复制数据包到自己的内存里,并且发送给网络。当 NIC 发送完一个数据包,会产生一个中断, 主机 cpu 去执行中断处理程序,完成后续工作。

2. 接收数据

应用程序接收数据的过程大致如下:

首先 NIC 把数据包写入自己的内存,并校验数据包是不是有效的,如果是有效的,把数据包写入主机的内存空间,然后 NIC 给主机操作系统发送一个中断信号,这时就进入到 kernel 区域。

在数据链路层,内核首先会做数据包检测,然后 Driver 驱动把数据包进行改装,以便后续 TCP/IP 能够理解这个数据包。改装完以后,根据 Ethernet 头部信息中的 Ethertype 分发给上层,假设为 IPv4,去除 Ethernet 头部,并发送给 IP 层。值得注意的是,委托给 IP 层之前,如果有抓包工具在监听抓包,那么内核就会拷贝数据包信息到抓包工具的内存地址空间。

IP 层通过计算 checksum 来校验 IP 头部的 checksum 是否有效,如果有效,接着检查 PRE_ROUTING 钩子(比如查看是否有 iptables 的相应配置需要执行),然后执行 IP 路由,IP 路由会判断这个数据包是本地处理还是转发当前数据包到其它主机。如果是转发数据包,执行 FORWARD 和 POST_ROUTING 钩子,并转发给数据链路层;如果是本地处理,IP 还会检查 LOCAL_IN 钩子,执行完以后,根据 IP 头部信息的 proto 值,假设为 TCP,去除 IP 头部,并把数据包传递给上层 TCP。值得注意的是,委托给 TCP 层之前,如果有 raw socket 在监听抓包,那么内核会拷贝数据包信息到 raw socket 的内存地址空间(默认 tcpcopy 利用 raw socket 来监听 IP 层的数据包)。

TCP 层会根据 TCP checksum 来检测数据包是否有效(如果采用了 checksum offload,NIC 会去做相关计算),然后就给这个数据包查找相应的 TCB(TCP control block),查找的方法是通过如下组合信息来查找:

<source IP, source port, target IP, target port>

如果没有查到,一般会发送 reset 数据包;如果查到了,进入 TCP 数据包处理环节。

如果是接收到新数据,TCP 就会把它放入到 socket 接收缓冲区,然后根据 TCP 状态,必要时发送 ack 确认数据包。Socket 接收缓冲区的大小就是 TCP 接收窗口大小。在某种程度上,如果接收窗口很大,TCP 吞吐量就会很大。目前较新的内核都能动态调整窗口的大小,无需用户去修改系统参数。

用户应用程序根据读事件去执行读操作,用户态空间进入到内核空间。内核把 socket buffer 里面的内容复制到用户指定的内存区域,然后把 socket buffer 读取过的内容释放,TCP 增加接收窗口大小,如果有必要,会传递一个更新窗口的数据包给对端 TCP。例如下图,TCP 发送了一个 ack 数据包,用于通知对端 TCP,本方 TCP 接收窗口更新了。

​ 编辑删除

3. 抓包工具工作原理

知道了数据如何发送和接收以后,我们分析一下 tcpdump 抓包原理。

在数据链路层和 IP 层交界的地方(属于数据链路层,如下图),是数据包被 tcpdump 捕获的场所。


执行到这个交界处时,内核会去查看 tcpdump 是否在监听,一旦有监听,就把数据包内容放入到 tcpdump 设置的缓冲区。理论上只要 tcpdump 及时去提取数据,在线上压力不大的情况下,抓包不会丢包。

tcpdump 所抓到的数据包,仅仅是代表数据包经过了链路层和网络层之间的交界处。从网卡进来的数据包未来的命运,可能是继续一路往前走到 TCP,也有可能在 IP 层被干掉,还有可能被路由转发出去;从本机发送出去的数据包,一旦被 tcpdump 捕获到,说明已经到了数据链路层,没有被 IP 层过滤掉,因为如果数据包被 IP 层过滤掉,这些数据包就不会到达 tcpdump 捕获点,也不会出现在抓包文件里。

下面我们通过一些实验来验证上述结论。

实验之前,我们先介绍一下 iptables 工具。iptables 是被广泛使用的防火墙工具,它主要跟内核 netfilter 数据包过滤框架进行交互。

3.1. 实验 LOCAL_IN 过滤

我们在服务器上面配置如下的 iptables 命令:

iptables -I INPUT -p tcp --dport 3306 -s 172.17.0.2 -j QUEUE

上述 iptables 命令设置了“-I INPUT”参数,意味着在 netfilter LOCAL_IN 钩子处执行上述 iptables 规则,即通往服务器端 TCP 之前,如果匹配到上述 iptables 规则,则会被放入目标 QUEUE(默认情况下是直接丢弃数据包),不再继续前行。

具体命令执行见下图:

设置上述 iptables 后,当 172.17.0.2 访问 172.17.0.3 3306 服务时,IP 数据包(如下图绿色箭头)会在服务器端 IP 层被丢弃掉,而红色箭头所指方向是 tcpdump 抓包的地方。


我们开启 tcpdump 抓包:

tcpdump -i any tcp and port 3306 and host 172.17.0.2 -n -v

在 172.17.0.2 上利用 MySQL 客户端命令访问 172.17.0.3 上面的 3306 服务,如下图:

结果经过长时间等待,最终显示连接不上。

服务器端抓包结果如下:

我们看到第一次握手数据包反复重传。

利用 netstat 命令,查看有没有相应的 TCP 状态,结果发现没有,如下图:

正常情况下,没有 TCP 状态,说明数据包没有进入服务器端 TCP,第一次握手数据包在服务器端 IP 层被干掉了。

利用 netstat -s 命令,在服务器端 TCP/IP 统计参数里找线索:

上图服务器端 IP 层接收到 20079 个数据包,下图接收到 20086 个数据包,MySQL 客户端登入过程累计增加了 7 个数据包,正好符合抓包文件显示的 7 个第一次握手数据包。

在服务器端 TCP 层,对比上面两张图,数据没有任何变化,说明了服务器端 TCP 没有收到任何数据包。

实验说明了在服务器端 IP 层进来的方向干掉数据包,服务器端 TCP 层不会有任何变化。

3.2. 实验 LOCAL_OUT 过滤

我们这次实验的目的是查看 IP 层 netfilter LOCAL_OUT 情况下的抓包情况。

如下图:


我们设置如下 iptables 命令:

iptables -I OUTPUT -p tcp --sport 3306 -d 172.17.0.2 -j QUEUE

具体操作如下图:

上述 iptables 命令设置了 OUTPUT 参数,意味着在 netfilter LOCAL_OUT 钩子处会执行上述 iptables 规则,即 IP 数据包在 IP 路由之前,如果匹配上述 iptables 规则,则会被放入目标 QUEUE(默认情况下直接丢弃数据包),不会继续往下走。

在 172.17.0.2 上利用 MySQL 客户端命令访问 172.17.0.3 上面的 3306 服务,如下图:

结果经过长时间等待,最终显示连接不上。

服务器端抓包结果如下:

我们看到第一次握手数据包反复重传,跟上一个抓包结果几乎一模一样

利用 netstat 命令,查看有没有相应的 TCP 状态,结果发现有 SYN_RECV 状态,如下图:

有 TCP 状态,说明数据包进入服务器端 TCP,并进入 SYN_RECV 状态,服务器端 TCP 会发送第二次握手数据包,但抓包显示并没有第二次握手数据包,说明被 iptables 配置干掉了。

查看 netstat -s 结果:

上图显示了实验之前的值,下图显示了实验之后的值。

从 TCP 层面信息来看,发送了 17 个数据分段,说明服务器端 TCP 发送了第二次握手数据包,而且发送了很多次,但因为设置了 iptables,这些数据包被拦截掉了,所以到不了数据链路层,也就没法被 tcpdump 捕获到。

从这两个实验来看,tcpdump 抓的数据包是一样的,都是在努力重传第一次握手数据包,但 iptables 设置的位置不一样,一个在入口,在 TCP 层无状态,一个在出口,在 TCP 层有状态。

进一步的分析可以尝试下面两个方向:

  1. 通过分析 TCP 状态来区分这两种情况

  2. 利用 netstat -s 给出的 TCP/IP 统计参数变化

通过上面实验,我们看出 tcpdump 抓包只是从一个点来观察世界,并不能看到全貌,这个时候就需要通过推理来辅助解决问题。

4. 潜在协议层的干扰

4.1. 接收数据

下图展示了数据包从 NIC 到协议栈,再到应用程序的过程。

TCP offload 由 NIC 完成,目的是减轻 TCP 的工作量,但存在潜在坑;在数据链路层,存在抓包接口,供 tcpdump 等抓包工具抓包,同时也存在着 raw socket 原始抓包方式接口;在网络层,存在 raw socket 抓包接口,IP Forward 转发功能,还有一整套 Netfilter 框架(存在大量坑的地方);在 TCP 层则相对比较清静,干扰少;用户程序通过 socket 接口从 TCP 取出数据或者获取新建连接。

4.2. 发送数据

下图展示了数据包从应用发送数据到 NIC 的过程。

用户程序通过 socket 接口来委托 TCP 发送数据或者建立连接;在网络层,存在 raw socket 发包接口,还有一整套 Netfilter 框架(存在大量坑的地方);在数据链路层,存在 pcap 发包接口,同时也存在着 raw socket 原始发包接口;TCP offload 是 NIC 做的,目的为了提升减轻 TCP 的工作量(比如分段,checksum),我们也遇到过由于 TCP offload 不当导致的丢包问题。

4.3. 案例

下面是一个从 NIC 接收数据包,并一路到应用,再发送响应出去的案例:

我们的应用程序是 Nginx(Web 服务器软件),其中 Nginx 配置监听端口为 8080,且开启 access log。

上图设置了 nginx keepalive_timeout = 0,即保持客户端空闲连接(方便实验)。

启动 nginx,通过 netstat 查看,nginx 已经在监听 8080 端口的连接请求。

刚开始 nginx 没有任何访问,access log 都为空,iptables 也没有设置。

在 172.17.0.2 机器,利用 telnet 访问 172.17.0.3 上面的 8080 端口服务,如下图:

这样 telnet 跟 nginx 建立连接,下图可以看出服务器端相应连接已经进入 ESTABLISHED 状态。

建立连接后,我们设置 iptables 命令,如下图,对返回 172.17.0.2 的 nginx 响应进行拦截并丢弃。

我们在客户端(172.17.0.2)上面继续执行 telnet 命令,键入“GET hello.html”,然后回车执行。

从 nginx 日志来看,这个请求已经被处理了,虽然是非法请求,但请求已经确认到达 nginx 了。

大概过了 2 分钟,查看客户端抓包情况,累计捕获了 16 个数据包,客户端还显示连接处于 ESTABLISHED 状态。

我们查看服务器端情况,利用 netstat 已经查不到服务器端的相应连接了,说明连接在服务器端的 TCP 层已经不存在了。

我们分析抓包情况(服务器抓包和客户端抓包效果一样):

自从发送了请求数据包,客户端由于没有看到任何服务器端的数据包回来,一直在重传请求数据包。客户端以为服务器还没有收到请求,但其实请求已经被 nginx 处理完毕。

在服务器端查看 netstat -st 的统计情况。

上图是执行 telnet 请求之前的状况,下图是执行 telnet 请求之后的状况。

从上图我们可以看出 connection aborted due to timeout 增加了一个,说明在服务器端 TCP 看来,请求的响应数据包(同时带有关闭 fin 标志)由于发送不出去,连接被 aborted,这个时候在服务器端看不到连接相应状态的存在。

在上层 nginx 看来,遇到了非法请求,回复了响应并关闭了连接。在 TCP 层看来,由于带有关闭 fin 的数据包到不了 tcpdump 抓包接口,服务器端的 TCP 状态会处于 FIN_WAIT_1 状态(“遇到大量 FIN_WAIT1,怎么破?”会有详细介绍),会维持一段时间并不断努力重传。由于重传一直得不到响应,TCP 就把 FIN_WAIT_1 状态变为 CLOSED 状态,在服务器端查不到该连接了。

这里案例中,我们事先知道我们设置了 iptables,但如果不知道呢,我们如何判断出问题出在哪一个环节呢?

仅仅靠 tcpdump 抓包,明显不够,因为通过抓包分析,我们只能得出服务器端没有接收到请求,我们还需要利用服务器端的信息,才能继续进一步判断。通过 nginx 日志,判断出请求已经被应用层处理了,说明请求数据包已经到达应用层,nginx 已经处理请求,并作了响应处理,接着委托服务器端 TCP 去发送这些响应数据包,但显然服务器端 TCP 发送的响应都没有到达抓包接口,说明在 IP 层干掉了,于是可以根据这些信息去找数据包出去方向(outgoing)的 netfilter 相关配置,看看有没有这样针对这些响应进行过滤。

从上面案例,可以看出仅仅利用 tcpdump 是不够的,还需要综合利用各种信息,并加以推理,最终得出问题出在哪一个环节,才能解决问题。如果不会利用这些知识,客户端就就会得出服务器端没有收到请求的错误判断。

5. 跨机器判断


在跨机器访问过程中,存在着如下潜在干涉(坑):

  1. 本机器自身 IP 层安全过滤

  2. 链路层发送 QUEUE 丢包

  3. 链路层 TCP offload 潜在问题(这里把 NIC 归入数据链路层)

  4. 中途设备各种问题(设备包括路由器/交换机/防火墙/网关/负载均衡器等)

  5. 对端机器链路层接收 QUEUE 丢包

  6. 对端链路层 TCP offload(NIC)潜在问题

  7. 对端 IP 层安全过滤

  8. 对端 TCP 异常状态干扰

这些问题将在 TCPCopy 和其它章节会有所介绍,这里不再详细描述。

6. 常用工具工作层次分析

上图展示了部分流行性工具的工作层次,比如 tcpcopy 默认工作在 4 层,调用 IP 层提供的 raw socket 接口来抓包和发包;netstat 或者 ss 工具可以去获取 TCP/IP 各种统计值;LVS 工作在 4 层,利用 Netfilter 来强行改变路由;tcpdump 工作在数据链路层;HTTP 应用工作在应用层。

懂得了这些工作原理,可以更加深刻的理解问题,并解决各种 TCP 相关问题。

用户头像

你如今的气质藏着走过的路读过的书爱过的人 2019.05.09 加入

自由搏击爱好者/撸铁狂魔/指弹爱好者/滴滴D8/DBA/考研人/户外青年/摄影初学者/老任主机游戏粉/猫奴/宅

评论

发布
暂无评论
网络抓包实战03——TCP/IP协议栈:数据包如何穿越各层协议