CS144计算机网络 Lab4

一、简介

这里记录了笔者学习 CS144 计算机网络 Lab4 的一些笔记 - TCP 总实现 TCPConnection

CS144 Lab4 实验指导书 - Lab Checkpoint 4: the summit (TCP in full)

个人 CS144 实验项目地址 - github

二、环境配置

当前我们的实验代码位于 master 分支,而在完成 Lab 之前需要合并一些依赖代码,因此执行以下命令:

1
git merge origin/lab4-startercode

之后重新 make 编译即可。

三、TCPConnection 简述

TCPConnection 需要将 TCPSender 和 TCPReceiver 结合,实现成一个 TCP 终端,同时收发数据。

TCPConnection 有几个规则需要遵守:

对于接收数据段而言:

  • 如果接收到的数据包设置了 RST 标志,则将输入输出字节流全部设置为 错误 状态,并永久关闭 TCP 连接。

  • 如果没有收到 RST 标志,则将该数据包传达给 TCPReceiver 来处理,它将对数据包中的 seqno、SYN、payload、FIN 进行处理。

  • 如果接收到的数据包中设置了 ACK 标志,则向当前 TCPConnection它自己的 TCPSender 告知远程终端的 ackno 和 window_size。

    这一步相当重要,因为数据包在网络中以乱序形式发送,因此远程发送给本地的 ackno 存在滞后性。

    将远程的 ackno 和 window size 附加至发送数据中可以降低这种滞后性,提高 TCP 效率。

  • 如果接收到的 TCP 数据包包含了一个有效 seqno,则 TCPConnection 必须至少返回一个 TCP 包作为回复,以告知远程终端 此时的 ackno 和 window size。

  • 如果接收到的 TCP 数据包包含的 seqno 是无效的,则 TCPConnection 也需要回复一个类似的无效数据包。这是因为远程终端可能会发送无效数据包以确认当前连接是否有效,同时查看此时接收方的 ackno 和 window size。这被称为 TCP 的 keep-alive

    1
    2
    3
    if (_receiver.ackno().has_value() && seg.length_in_sequence_space() == 0 && seg.header().seqno == _receiver.ackno().value() - 1) {
    _sender.send_empty_segment();
    }

对于发送数据段来说:

  • 当 TCPSender 将一个 TCPSegment 数据包添加到待发送队列中时,TCPConnection 需要从中取出并将其发送。
  • 在发送当前数据包之前,TCPConnection 会获取当前它自己的 TCPReceiver 的 ackno 和 window size,将其放置进待发送 TCPSegment 中,并设置其 ACK 标志。

TCPConnection 需要检测时间的流逝。它存在一个 tick 函数,该函数将会被操作系统持续调用。当 TCPConnection 的 tick 函数被调用后,它需要

  • 告知 TCPSender 时间的流逝,这可能会让 TCPSender 重新发送被丢弃的数据包
  • 如果连续重传次数超过 TCPConfig::MAX RETX ATTEMPTS,则发送一个 RST 包。
  • 在条件适合的情况下关闭 TCP 连接(当处于 TCP 的 TIME_WAIT 状态时)。

TCP 连接的关闭稍微麻烦一些,主要有以下几种情况需要考虑:

  • 接收方收到 RST 标志或者发送方发送 RST 标志后,设置当前 TCPConnection 的输入输出字节流的状态为错误状态,并立即停止退出。这种属于暴力退出(unclear shutdown),可能会导致尚未传输完成的数据丢失(例如仍然在网络中运输的数据包在接收方收到RST标志后被丢弃)。

  • 若想让双方都在数据流收发完整后退出(clear shutdonw),则情况略微麻烦一点。先上张四次挥手的图:

    img

    简单讲下挥手的流程:

    • 客户端的数据全部发送完成,则将会发送 FIN 包以告知服务器 客户端数据全部发送完成(发送完成,不等于被接收完成)。但请注意,此时的服务器仍然可以发送数据至客户端。

    • 当服务器对 客户端的 FIN 进行 ack 后,则说明服务器确认接收客户端的全部数据

    • 服务器继续发送数据,直到服务器的数据已经全部发送完成,则向客户端发送 FIN 包以告知服务端数据全部发送完成

    • 当客户端对服务端的 FIN 发送 ack 后,则说明客户端确认接收服务端的全部数据。注意,此时客户端可以确认:

      • 服务端成功接收客户端全部数据
      • 客户端成功接收服务端的全部数据

      此时客户端可以百分百相信,此时断开连接对客户端是没有任何危害的

      但是!当服务器没接收到 客户端的 ACK 时,

      • 服务器可以确认它成功接收客户端全部数据
      • 服务器不知道客户端是否成功接收服务端的全部数据

      也就是说,服务器一定要获得到客户端的 ACK 才能关闭。

      若服务器在超时时间内没获得到客户端的 FIN ACK,则会重发 FIN 包。但假如此时客户端已经断连,那么服务器将永远无法获取到客户端的 FIN ACK。因此即便客户端已经完成了它的所有任务,它仍然需要等待服务器端一小段时间,以便于处理服务端的 FIN 包。

      当服务器获取到了客户端的 FIN_ACK 后,它就直接关闭连接。而客户端也会在超时后静默关闭。此时双方均成功获取对方的全部数据,没有造成任何危害。

      这里有个很重要的点是,TCP 不会对 ACK 包来进行 ACK。例如服务端不会对客户端发来的 FIN_ACK 回复一个 FIN_ACK_ACK。

四、TCP 状态图

这里放两张TCP 双方的状态图,做完这些实验再去看它们就相当轻松了:

这里写图片描述

这里写图片描述

五、调试

测试样例的调试我就不多说了,因为这部分已经在之前说了,直接用 gdb 起一个会话然后单步调试就好,比较简单。这里记录一下 CS144 模拟网卡的调试方式。

首先是启动一个 wireshark 会话抓包,这里有两种方式,一种是终端抓包:

1
sudo tshark -Pw /tmp/debug.raw -i tun144

效果是这样的:

image-20211115175444352

而且抓到的数据包存放于 /tmp/debug.raw 中,也便于后期分析。

不过对我个人而言还是更喜欢图形界面,因此键入以下命令:

注意一定要用 sudo !不然找不到网卡。

1
sudo wireshark

然后在 tun144 和 145 中随意选一个,没区别。这里我选了 tun144。

tun144 和 145 是 CS144 模拟出的两个虚拟网卡。这两张网卡可以互通。

image-20211115175613525

之后分别在两个终端下键入命令以相互连接

1
2
# 在 tun144 网段下启动 server 监听,其地址为 169.254.144.9:9090
./apps/tcp_ipv4 -l 169.254.144.9 9090
1
2
# 在 tun145 网段下启动 client,其地址为 169.254.145.9,向 169.254.144.9:9090 发起连接
./apps/tcp_ipv4 -d tun145 -a 169.254.145.9 169.254.144.9 9090

之后便可以在 wireshark 中捕获其数据包来往:

image-20211115180315678

这是捕获到的错误 TCP 数据包的来往。可以发现在三次握手的时候,Server 貌似没有对 Client 返回的 ACK 进行处理,而是一直重发 SYN+ACK,最后导致重发次数过多被 Server 端挂断连接。

找到了问题便可以通过 gdb 来进行调试。不过在用 gdb 调试时,记得给 ./apps/tcp_ipv4 设置个大一点的 -t 数据包超时时间参数,以避免发送方重复发送数据,扰乱捕获数据包的观察。

这个错误折腾了我一个晚上,最后发现貌似是我本机的 Tun/Tap 机制出现了问题,导致 Client 发给 Server 的数据包的 源 IP 地址不一致(看捕获到的第一行数据包 169.254.144.1 -> 169.254.144.9 和第三行数据包 169.254.145.9 -> 169.254.144.9):

Client 发送的 IP 包头为 169.254.145.9 -> 169.254.144.9

image-20211115175444352

这会导致 Server 接收到 第三行 ACK 数据包时,认为该数据包不来自 Client,因此将其丢弃,一直等待 ACK 包。

一种临时解决方法是,在libsponge/tcp_helpers/tcp_over_ip.cc中的 TCPOverIPv4Adapter::unwrap_tcp_in_ip 函数中,注释掉一个 check:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
optional<TCPSegment> TCPOverIPv4Adapter::unwrap_tcp_in_ip(const InternetDatagram &ip_dgram) {
// is the IPv4 datagram for us?
// Note: it's valid to bind to address "0" (INADDR_ANY) and reply from actual address contacted
if (not listening() and (ip_dgram.header().dst != config().source.ipv4_numeric())) {
return {};
}

//! NOTE: 注释该 check
// is the IPv4 datagram from our peer?
// if (not listening() and (ip_dgram.header().src != config().destination.ipv4_numeric())) {
// return {};
// }

// does the IPv4 datagram claim that its payload is a TCP segment?
if (ip_dgram.header().proto != IPv4Header::PROTO_TCP) {
return {};
}

[...]
}

这真是太折腾了…

六、具体实现

该实验相当相当的费劲,原因时大量的测试样例会涉及到大量的边界检测,以及最后还会有真实网络连接下的数据包交互。TCPSender 和 TCPReceiver 必须足够鲁棒,才能降低 TCPConnection 的实现难度。

TCPConnection 必须实时根据当前的 TCP 状态来处理传入的数据包,过滤无用数据包,其实现必须假设输入的 TCPSegment 不可信任,然后利用大量的 check 来把它慢慢验证为是一个可信的 TCPSegment。

注意:一定要防止整数溢出攻击!

这部分实现的相关解释,我以注释的形式写入代码中,结合代码阅读方便理解。整体实现完成其实也没多少代码,因为大部分的操作都可以推迟到具体的 TCPSender、TCPReceiver 来处理(包括里面的异常处理)。

因此这个实验非常吃前面实验实现的基础代码,非常非常的吃。

而且前面的实验可能也存在一些问题没有被测试样例给检测出来,这次将全部检测出(因为是TCP实验中最后的部分了)。

代码位于:

测试结果:

image-20211116090253846

benchmark:

image-20211116090556892

webget 与真实服务器通信:

image-20211116105134413

七、CS144 模拟网络传输逻辑

CS144 中用来模拟两机网络交互的那部分代码很有意思,这里简单的研究了一下 tcp_ipv4.cc 中的完整逻辑。

首先,项目根路径中的 tun.sh 会使用 ip tuntap 技术创建虚拟 Tun/Tap 网络设备。这类接口仅能工作在内核中。不同于普通的网络接口,没有物理硬件。这样做的目的应该是为了模拟真实网络环境下的网络环境。

这里是 tun/tap 的详细描述 - 虚拟设备之TUN和TAP - 知乎

当 Tun/Tap 网络设备建立好后,tcp_ipv4.cc 中会建立一个 TCPOverIPv4OverTunFdAdapterTunFd指的是连接进 Tun 设备上的 socket,而TCPOverIPv4OverTunFdAdapter是一个 IP 层面的封装接口。当调用 adapter 向其写入 TCP 报文段时,它会自动 wrap 上 IP 段并传输进网络设备中;读取也是亦然,会自动解除 IP 段并返回其内部封装的 TCP报文段。

接下来,无论对于 Server 还是 Client,在三次握手之后,都会建立一个新的线程,来专门执行 LossyTCPOverIPv4SpongeSocket 中的 eventloop。而子线程会另起一个 eventloop 以及另外开辟两个缓冲区,用于存放用户写入的数据与即将输出至屏幕的数据。当用户通过 stdin 输入数据时, eventloop 中所注册的 poll 事件被检测到,则数据将会被写入进本地输入缓冲区中。当 TCPOverIPv4OverTunFdAdapter 可写时,它会将本地输入缓冲区中的数据全部写入至 TCPOverIPv4OverTunFdAdapter ,并最终传输至远程。

而 webget 与真实服务器通信的原理,也是通过将 IP 报文写入 tun 虚拟网络设备,将其注入进 OS 协议栈中,模拟实际的发包情况。

以下是 tun.sh 中创建 tun 网络设备的相关命令:

1
2
3
4
5
6
7
8
# start_tun 144
local TUNNUM=144 TUNDEV=tun144
ip tuntap add mode tun user Kiprey name tun144
ip addr add 169.254.144.1/24 dev tun144
ip link set dev tun144 up
ip route change 169.254.144.0/24 dev tun144 rto_min 10ms
iptables -t nat -A PREROUTING -s 169.254.144.0/24 -j CONNMARK --set-mark 144
iptables -t nat -A POSTROUTING -j MASQUERADE -m connmark --mark 144

这是一个相当有意思的代码,有空可以读读理解理解。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2024 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~