Network Stack

来自osdev
(重定向自Networking
跳到导航 跳到搜索

这个页面正在建设中! 此页面或部分内容仍在改进中,因此可能还不完整。 其内容可能会在不久的将来更改。

本文是关于编写TCP/IP协议栈的,即使用链路层(如以太网卡)来处理IP、ARP、TCP、UDP等协议的数据包的子系统。

扫描PCI设备

首先要做的是扫描安装在机器上的PCI设备,以便通过查看特定的供应商ID和设备ID来检测以太网卡。 有关更多详细信息,请参见PCI页面。

为NIC编写驱动程序

找到以太网卡后,需要实现一个驱动程序,使其能够发送和接收数据。 如果你使用的是仿真器,Intel E1000是一款很好的驱动程序编写卡,因为它可以在各种仿真器上使用,比如VirtualBox - osdev.org上的这里对进行了更多介绍(请参阅Intel Ethernet i217)。 如果在实现E1000驱动程序时遇到问题,可以从RTL8139开始,这是一种更简单的旧以太网卡。

从以太网卡出来的第一件事是机器的MAC地址。 在本地网络上交换数据需要这个6字节的地址。

你能做的最简单的测试就是在网络上发送ARP广播。 你可以用Wireshark既可以捕获有效ARP请求的示例,也可以验证目标主机是否已接收到你自己的请求。 就接收数据而言,你的网卡应该捕获通过本地网络发送的数据,即使这些数据没有发送到你的机器。

网络协议

一旦你可以通过NIC发送和接收数据,并获得机器的MAC地址,你就必须实施(至少部分实施)几种相互共存的网络协议:

  • Ethernet以太网协议:这是使用MAC地址将数据发送到本地网络上另一台机器的基本协议。 这是所有其他功能的基础,因为如果你想与外界通信,你需要向路由器发送数据。
    • ARP(Address Resolution Protocol 地址解析协议):允许将IPv4地址转换为MAC地址
    • IP(Internet Protocol)协议:它位于以太网之上,需要在给定IP地址的互联网上发送数据。 最常见的版本是使用32位IP地址的IPv4,但IPv6(使用128位IP地址)也正在获得越来越多的注意。 请注意,IP提供了发送数据包的“最大努力(best effort)”发送,但不能保证数据包将成功到达目的地,也不能保证数据包将按发送顺序接收
      • ICMP(Internet Control Message Protocol互联网控制消息协议):由ping或traceroute等工具使用
      • UDP(User Datagram Protocol 用户数据报协议):一种无连接传输协议,将源端口和目标端口的概念添加到IP中。 应用程序服务可以订阅一个或多个端口,以便在UDP消息发送到该端口时收到通知
        • DHCP(Dynamic Host Configuration Protocol动态主机配置协议):允许请求机器网络配置信息,如其IP地址、本地路由器的IP地址、DNS等。
        • DNS(Domain Name System 域名系统):获取给定域名的IP地址
      • TCP(Transmission Control Protocol 传输控制协议):与UDP一样,它添加了源端口和目标端口的概念。 然而,TCP更为复杂,因为它创建了自己的会话机制,并确保使用它的应用程序将按顺序接收数据包,如果需要,还会重新发送数据包。
        • SSL/TLS(可选):在建立安全连接时使用
          • HTTP(HyperText Transfer Protocol 超文本传输协议):定义一种请求和响应机制,用于传输网页、图像和其他资源。
          • Telnet:使用命令行Shell远程访问机器的协议。

Wireshark是可以帮助你的首选工具,它是一个免费的网络嗅探器和分析器。 因为它非常详细地解释了数据包的每个字节对应于什么,可以用来理解各种网络协议是如何编码的,。 请注意,在Windows上,Wireshark不会捕获环回(loopback)流量(即从本地主机到本地主机的流量),因此可能不会捕获模拟器和主机之间的网络流量。 但是,你可以使用Rawcap将网络流量捕获到文件中,并使用Wireshark对其进行检查。

网络栈

网络协议被组织成一个栈,每一层调用下一层。 通过网络发送的数据包将由多个报头组成,每一层对应一个报头。

考虑DHCP请求的例子。 这是你可能希望尽早实施的协议之一,因为它允许你的机器查找其IP地址,获取本地路由器IP地址,DNS IP地址- 这样就能够获得通过网络正确通信的基本信息。

实现这些的一种方法如下:

  • 操作系统决定发送DHCP请求,因此调用DHCP层
    • DHCP层要求UDP层创建一个数据包,其目标是IP地址255,255,255,255(广播到整个本地网络)、端口53,有效负载大小为300字节(长度可能不同)
      • UDP层要求IP层创建一个UDP类型的数据包,地址为255,255,255,255,大小为308字节
        • IP层要求以太网层创建一个长度为328字节的IPv4类型的数据包,其目标是IP地址255,255,255,255
          • 以太网层创建一个大小为342字节的数据包,并在前14个字节中写入以太网报头,包括源地址(机器的MAC地址)、目标MAC地址FF:FF:FF:FF:FF:FF(从IP地址255,255,255,255翻译过来)并将其发送回IP层
        • IP层将IP报头写入以太网报头后的20字节中,并将其发送到UDP层
      • UDP层在IP报头之后的8个字节中写入报头,并将其发送到DHCP层
    • DHCP层将其请求写入剩下的300字节,并将其发送回UDP层
      • UDP层通过写入校验和(包含DHCP消息)来完成报头,并将其发送到IP层
        • IP层将其发送到以太网层
          • 以太网层将数据包发送到以太网卡,以太网卡通过网络发送消息

通过网络实际发送的数据包如下所示:

以太网报头(14字节)
IPv4报头(20字节)
UDP报头(8字节)
DHCP请求(300字节有效负载)

DHCP响应的格式与请求的格式相同,应按如下方式处理:

  • 以太网卡驱动程序将验证目标MAC是当前机器的,如果是,则将数据包发送到以太网层
  • 以太网层将查看以太网报头,检查服务类型(应该是IP),并将数据包(去掉以太网报头)发送到IP层
  • IP层将检查IP报头,验证校验和,因为其类型是UDP,所以将数据包(不带IP报头)转发到UDP层。
  • UDP层将检查UDP报头,验证校验和,并根据目标端口将有效负载发送到正确的服务——在本例中是DHCP层(再次剥离UDP报头)
  • DHCP层将读取DHCP消息,验证消息类型是否为响应(即,它是来自路由器的响应),并将检索其IP地址、路由器IP地址和其他网络配置信息。

请注意,根据定义,网络协议是异步的,即在网络上发送请求,需要等待其响应。 特别是,如果有回应,也你无法预测什么时候会有回应。 由于传入的数据包由中断处理程序处理,因此它可以随时中断你的代码。

小端和大端

按照惯例,任何在互联网上编码的消息都使用big-endian(最高有效字节排在第一位)。 对于在英特尔和AMD处理器上开发的人来说,这是一件需要时刻牢记的事情,因为x86处理器使用little endian编码数字。 因此,你必须经常转换数字。 以下两个函数用于将endian转换为16位和32位整数:

   uint16_t switch_endian16(uint16_t nb) {
       return (nb>>8) | (nb<<8);
   }
   
   uint_t switch_endian32(uint_t nb) {
       return ((nb>>24)&0xff)      |
              ((nb<<8)&0xff0000)   |
              ((nb>>8)&0xff00)     |
              ((nb<<24)&0xff000000);
   }

校验和(Checksums)

一些网络协议使用校验和来验证消息在传输过程中是否被意外更改。 如果没有有效的校验和,数据包可能会被忽略。 校验和是一个16位数字,可以使用的计算方式如下:

  • 将消息拆分为16位的校验和块
  • 累加那些块
  • 如果消息的字节数为奇数,则最后一个字节应计为较高的字节,以得到一个完整16位的数字(例如,如果最后一个字节为0x42,则添加0x4200)
  • 如果总和不适合16位数字(即大于0xFFFF),则去掉前16位并将其添加到低16位。 重复最后一步,直到得到16位和
  • 返回该和的二进制反转

IP校验和只覆盖它自己的报头。 UDP和TCP校验和比较复杂,因为它们包括UDP/TCP报头、有效负载(即UDP/TCP报头之后的任何内容)以及由源和目标IP地址、IP类型(0x11代表UDP,0x06代表TCP)和UDP/TCP消息长度(从UDP/TCP报头开始)组成的“伪报头”。

正确计算校验和可能很棘手,Wireshark可以帮助你。 为此,通过进入编辑/首选项/协议,确保它正在验证校验和(默认情况下未启用该选项),选择所需的协议(例如UDP、TCP、IPv4),并确保选中“如果可能,验证校验和”。 这样,Wireshark将告诉你校验和是否有效,如果无效,其值应该是多少。

ARP

ARP协议将是你需要实现的首批协议之一。 没有它,你将无法在本地网络上进行通信,更不用说在互联网上了。 幸运的是,这是一个简单的协议,只需要实现几个功能:

  • 发送请求和处理回复:你的操作系统需要执行一个请求,将IP地址转换成MAC地址,这是与你的本地路由器通信所必需的。 这意味着不仅要发送一个请求包,还要在收到回复时进行处理,以便操作系统可以更新其ARP表
*接收请求并发送回复:你的操作系统还需要能应答按其方式发送的请求(例如,当有人询问其MAC地址时)。 特别是,本地路由器会定期向你的机器发送ARP请求。 如果没有响应,路由器会认为你的机器坏了,不会再转发更多的流量了。

TCP

TCP是最复杂的网络协议之一。

首先,它在客户端和服务器之间创建了一个虚拟连接。 为了实现这一点,TCP报头包含多个标志,双方将使用这些标志来通信该连接的状态:SYN(同步 synchronize)、ACK(确认 acknowledge)、PSH(推送 Push)、FIN(完成 finish)和其他标志。

除此之外,TCP还试图解决这样一个困境:IP不能保证数据包按发送顺序被接收,更不用说完整接收了。 这就是为什么它跟踪实际发送的数据量,要求各方定期确认他们收到的数据,并在需要时重新发送数据包的原因。 为此,TCP报头包含一个序列号和一个确认号。

在TCP连接过程中,双方都可以互相发送一些数据,这些数据分为多个数据包。 考量当前通信到什么阶段的一种方法是发送该通信中的位置(按字节数)。 TCP数据包中的序列号(sequence number)是当前数据包所在的位置。 同样,确认号(acknowledgement number)表示一方希望另一方发送的位置(仍以字节为单位)。

当任何一方接收到序列号为S、确认号为A、负载大小为N的TCP数据包时,其发送的下一个数据包应具有序列号A(即发送另一方期望的数据)和确认号S+N(如果N为空,则为S+1)。

建立连接

通过以下三次握手建立TCP连接:

  • 客户端向服务器发送SYN请求(即设置了SYN标志的消息)
  • 服务器以SYN+ACK请求进行响应(该标准还允许它分别发送ACK和SYN,尽管这种情况很少发生)。
  • 客户端发送ACK响应。

SYN包中使用的序列号是初始序列号; 所有进一步的数据包应使用初始序列号的增量序列号。 序列号可以通过发送带有新序列号的新SYN数据包来重置。

初始SYN和SYN+ACK数据包“可能”还包含要发送到应用程序的数据,但很少使用。 TCP规范规定,在建立连接之前(即,在接收到最终ACK响应数据包之后),不得将该数据发送到应用程序。

传输数据

要发送数据,任何一方都可以发送PSH、ACK消息,实际数据在TCP报头之后。 另一方需要发送ACK消息以确认其已收到数据包。 如果没有,发送方将再次发送数据包。 这就是一些TCP/IP实现的不同之处 - 有些可能会在发送ACK之前等待或多或少的时间。

关闭连接

连接的终止:

  • 想要关闭连接的一方发送一个带有FIN标志的数据包
  • 另一方发送FIN,ACK消息
  • 一开始主动关闭发送确认消息

与用于建立连接的数据包一样,这些数据包不包含任何有效负载,只包含TCP报头。

一个例子

让我们来看一个HTTP GET请求的TCP通信示例:

源->目的地 目的地->源 说明
Flag: SYN

seq_nb=0, ack_nb=0

TCP握手的开始。 它正在发送字节 #0(没有有效负载的数据包将被视为至少有一个字节的通信),并且尚未从服务器接收任何数据
Flags: SYN, ACK

seq_nb=0, ack_nb=1

Flag: ACK

seq_nb=1, ack_nb=1

TCP握手完成后,可以开始通信
Flags: PSH, ACK

seq_nb=1, ack_nb=1, len=77

这是客户端发送的HTTP GET请求。这是第一个具有实际有效载荷的数据包
Flag: ACK

seq_nb=1, ack_nb=78

服务器确认HTTP请求:它已成功读取字节#77,因此希望下一次通信从字节#78开始
Flags: PSH, ACK

seq_nb=1, ack_nb=78, len=1009

这是主体的HTML
Flag: ACK

seq_nb=78, ack_nb=1010

客户端确认服务器发送的消息:它正在发送字节#78,并且已接收到字节#1009,因此希望下一次通信从字节#1010开始
Flags: FIN, ACK

seq_nb=78, ack_nb=1010

客户端终止TCP连接
Flags: FIN, ACK

seq_nb=1010, ack_nb=79

Flag: ACK

seq_nb=79, ack_nb=1011

TCP通信的结束

需要关注什么

协议栈的形式会因设计决策而有所不同。这些决策点可能包括

  • 数据包是否在一个缓冲区中的处理层之间传递,或者在通过层边界时是否复制到新的缓冲区;
  • 针对无论是入站帧还是出站帧,使用专用线程与链路层通信,该线程是都完全包含在中断处理程序中,还是单线程环境中的循环中;
  • 帧(如以太网帧)是立即处理还是排队处理;
  • 无论你想要TCP支持还是UDP支持,或者仅仅是IP支持;TCP是协议栈中最复杂的部分,在lwip实现中,有一半代码是特定于TCP的。

例如,一个协议栈可能

  • 让NIC的API提供三个功能:设置NIC、轮询帧和发送帧;
  • 在一个线程中与NIC进行入帧和出帧通信;
  • 在另一个线程中从接收队列解编入站帧。

一般考虑事项

  • 在以太网上编写协议栈时,你可能希望提供对ARP协议和解析功能的支持。
  • 为了模块化,工作站的IP最好存储在nic_info结构中,而不是作为全局变量。
  • 你也许会希望使用Wireshark或其他数据包嗅探器来检查通信和netcat,一旦你获得UDP或TCP支持,netcat将转储从操作系统发送的调试数据。 此外,在调试arp代码时,工具命令arping也很有用。 你也许需要编写一个触发器,例如,在收到ARP表示已为所选IP设置了地址时,重新启动你的网络系统。
  • 你可以在一台计算机上使用专用以太网卡,通过交叉电缆连接到另一台计算机(哪里运行着你自制的操作系统),并使用静态IP。 其他可选的办法包括在为其提供的网络设备实现驱动程序后,在bochs或qemu下进行测试。

另见

文章

论坛主题

外部链接

许多tcp/ip协议栈都附带了它们的实现文档;读起来不错。