Lever's Castle

Let's code a TCP/IP stack, 1 - Ethernet & ARP

December 23, 2018

原文链接 http://www.saminiir.com/lets-code-tcp-ip-stack-1-ethernet-arp/

TCP 中最重要的部分:

  1. TCP header parsing - TCP 报头压缩
  2. state machine - 状态机
  3. congestion control - 拥塞控制
  4. retransmission timeout computation - 超时重试

    TUN/TAP 设备

    TUN 用来操作 L3 的流量 TAP 用来操作 L2 的流量

TUN 和 TAP 是操作系统内核中的虚拟网络设备

TAP 相当于一个以太网设备,他用来操作 L2 的数据包如以太网数据帧 TUN 模拟了网络层设备,操作 L3 的数据包如 IP 数据封包

操作系统使用 TAP 和 TUN 设备向绑定该设备的用户空间程序发送数据,同样的用户空间的程序也会向这两个设备发送数据。感觉类似于真实物理设备与用户程序之间的一层代理。

/*
 * Taken from Kernel Documentation/networking/tuntap.txt
 */
int tun_alloc(char *dev)
{
    struct ifreq ifr;
    int fd, err;

    if( (fd = open("/dev/net/tap", O_RDWR)) < 0 ) {
        print_error("Cannot open TUN/TAP dev");
        exit(1);
    }

    CLEAR(ifr);

    /* Flags: IFF_TUN   - TUN device (no Ethernet headers)
     *        IFF_TAP   - TAP device
     *
     *        IFF_NO_PI - Do not provide packet information
     */
    ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
    if( *dev ) {
        strncpy(ifr.ifr_name, dev, IFNAMSIZ);
    }

    if( (err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0 ){
        print_error("ERR: Could not ioctl tun: %s\n", strerror(errno));
        close(fd);
        return err;
    }

    strcpy(dev, ifr.ifr_name);
    return fd;
}

用 open 方法拿到 “/dev/net/tap” 的文件描述符放在 fd 里,之后就可以用 fd 读取和写入以太网虚拟设备的缓冲区中的数据。

使用上面的代码就可以启动一个 TAP 设备,IFFNOPI 表示不需要额外的包的信息。

如果想要启动一个 TUN 设备,只需要把第 21 行的 IFFTAP 改为 IFFTUN 即可。

以太帧

https://zh.wikipedia.org/wiki/%E4%BB%A5%E5%A4%AA%E7%BD%91%E5%B8%A7%E6%A0%BC%E5%BC%8F

以太网的第一个版本比现在的速度慢很多,大概在 10MB/s ,并且是半双工的,即要么在读,要么在写,不能同时读和写。

100 BASE - T 以太网标准中,使用双绞线来实现全双工通信以及更高的吞吐速度。此外,以太网交换机的普及加速了 CSMA/CD 的过时。

CSMA/CD —— 带冲突检测的载波监听多路访问技术

它的工作原理是: 发送数据前 先侦听信道是否空闲 ,若空闲,则立即发送数据。若信道忙碌,则等待一段时间至信道中的信息传输结束后再发送数据;若在上一段信息发送结束后,同时有两个或两个以上的节点都提出发送请求,则判定为冲突。若侦听到冲突,则立即停止发送数据,等待一段随机时间,再重新尝试。

简单总结为:先听后发,边发边听,冲突停发,随机延迟后重发

有人将CSMA/CD的工作过程形象的比喻成很多人在一间黑屋子中举行讨论会,参加会议的人都是只能听到其他人的声音。每个人在说话前必须先倾听,只有等会场安静下来后,他才能够发言。人们将发言前监听以确定是否已有人在发言的动作称为”载波监听”;将在会场安静的情况下每人都有平等机会讲话成为“多路访问”;如果有两人或两人以上同时说话,大家就无法听清其中任何一人的发言,这种情况称为发生“冲突”。发言人在发言过程中要及时发现是否发生冲突,这个动作称为“冲突检测”。如果发言人发现冲突已经发生,这时他需要停止讲话,然后随机后退延迟,再次重复上述过程,直至讲话成功。如果失败次数太多,他也许就放弃这次发言的想法。通常尝试16次后放弃。

了解完这些,我们来看看以太帧的数据结构:

#include <linux/if_ether.h>

struct eth_hdr
{
    unsigned char dmac[6]; // 目标 MAC 地址
    unsigned char smac[6]; // 源 MAC 地址
    uint16_t ethertype; // 有效负载的长度或者类型
    unsigned char payload[];
} __attribute__((packed));

ethertype 的值大于或等于 1536 ,其中就包含有效负载的类型信息(IPv4, ARP 等),小于该值,则其只包含有效负载的长度信息。在开头引入了 if_ether.h 文件,用来提供 ethertype 及其 16 进制之间的映射。

image

以太帧中可能会包含 标签 信息,这些标签可以用来描述他属于哪个 VLAN 或者优先级(QOS)

payload 字段包含以太帧负载的指针,这里会包含 IPv4 或者 ARP 的数据包。

在上面的声明中,还缺少了对冗余校验的定义,在真实的以太帧中,需要包含一个帧校验码,用来循环冗余校验帧的完整性。

以太帧解析

在上面声明的 struct 最后使用了 packed,这是用来告知 GNU 的 C 编译器不要用 padding bytes 的方式优化 struct 的内存结构以使其数据结构对齐

数据结构对齐是代码编译后在内存中的布局和使用方式。包括:数据对齐、数据结构填充(padding)、包入(packing)

现代计算机一般是32比特或64比特地址对齐,如果要访问的变量没有对齐,可能会触发总线错误

当数据小于计算机的字(word)尺寸,可能把几个数据元素放在一个字中,称为包入(packing)。

许多编程语言自动处理数据结构对齐。

更多相关讨论可以看看这里 —— https://stackoverflow.com/questions/4306186/structure-padding-and-packing

if (tun_read(buf, BUFLEN) < 0) {
    print_error("ERR: Read from tun_fd: %s\n", strerror(errno));
}

struct eth_hdr *hdr = init_eth_hdr(buf);

// 查看 ethertype 字段值,并根据其值判断下一步逻辑
handle_frame(&netdev, hdr);

ARP - Address Resolution Protocol

ARP 协议用来将 MAC 地址动态映射到协议地址(例如:IPv4 地址)

ARP 数据包的格式相对简单:

struct arp_hdr
{
    uint16_t hwtype; // 硬件类型,eg: 以太网(0x0001)
    uint16_t protype; // 协议类型
    unsigned char hwsize;  // 硬件地址长度
    unsigned char prosize; // 协议地址长度
    uint16_t opcode; // 操作码
    unsigned char data[]; // 实际的负载信息
} __attribute__((packed));

// data 中还会包含 IPv4 信息
struct arp_ipv4
{
    unsigned char smac[6]; // 源 MAC
    uint32_t sip; // 源 IP
    unsigned char dmac[6]; // 目标 MAC
    uint32_t dip; // 目标 IP
} __attribute__((packed));

image

地址解析算法

规范中描述的实现方式:

?Do I have the hardware type in ar$hrd?
Yes: (almost definitely)
  [optionally check the hardware length ar$hln]
  ?Do I speak the protocol in ar$pro?
  Yes:
    [optionally check the protocol length ar$pln]
    Merge_flag := false
    If the pair <protocol type, sender protocol address> is
        already in my translation table, update the sender
        hardware address field of the entry with the new
        information in the packet and set Merge_flag to true.
    ?Am I the target protocol address?
    Yes:
      If Merge_flag is false, add the triplet <protocol type,
          sender protocol address, sender hardware address> to
          the translation table.
      ?Is the opcode ares_op$REQUEST?  (NOW look at the opcode!!)
      Yes:
        Swap hardware and protocol fields, putting the local
            hardware and protocol addresses in the sender fields.
        Set the ar$op field to ares_op$REPLY
        Send the packet to the (new) target hardware address on
            the same hardware on which the request was received

看上去也不是很复杂,就是一些操作检查,然后针对不同的行为做相应的处理。

ARP 的实现中,会把 IP 和 MAC 地址的关系维护在一张缓存表里,以减少 ARP 广播请求。

作者对 ARP 的实现 —— https://github.com/saminiir/level-ip/blob/e9ceb08f01a5499b85f03e2d615309c655b97e8f/src/arp.c#L53


Lever

痕迹
没有过去,就没法认定现在的自己