编写一个TCP/IP栈2-IPv4和ICMPv4

来自程序员技术小站
跳转到导航 跳转到搜索

此次在我们的用户空间TCP/IP堆栈中,我们将实现一个最小可行的IP层,并使用ICMP的回声请求(也称为pings)进行测试。

我们将查看IPv4和ICMPv4的格式,并介绍如何检查其完整性。一些功能,例如IP分片,作为练习。

对于我们的网络栈, 选择IPv4 优先于 IPv6,因为它仍然是互联网的默认网络协议。然而,未来我们的网络栈可以通过IPv6进行扩展。

互联网协议版本4

在实现以太网帧之后,下一层(L3)负责将数据传输到目标。即互联网协议(IP) 是为TCP和UDP等传输协议提供基础而发明的。它是无连接的,这意味着与TCP不同,所有数据报在网络堆栈中都是相互独立处理的。这也意味着IP数据报可能会丢失或者过期

此外, IP 不保证成功交付。这是协议设计者有意识地选择的,因为IP旨在为同样无法保证交付的协议提供基础。UDP就是这样一个协议。

如果需要通信方之间的可靠性,则在IP之上使用TCP等协议。在这种情况下,更高级别的协议负责检测缺失的数据, 并确保所有数据均已送达。

头部格式

IPv4 头通常长度为 20 个 八位。头部可以包含尾随选项,但在我们的实现中会省略这些选项。字段的含义相对简单,可以描述为一个C结构体:

4位version字段表示互联网头的格式。在我们的情况下,IPv4 的值为 4。

struct iphdr {
    uint8_t version : 4;
    uint8_t ihl : 4;
    uint8_t tos;
    uint16_t len;
    uint16_t id;
    uint16_t flags : 3;
    uint16_t frag_offset : 13;
    uint8_t ttl;
    uint8_t proto;
    uint16_t csum;
    uint32_t saddr;
    uint32_t daddr;
} __attribute__((packed));

I互联网头长字段ihl长度同样为4位,words表示IP头中32位字数。由于字段大小为 4 位,因此最大值只能为 15。因此,IP头的最大长度为60个八位。

服务字段的类型tos源自第一个IP规范。在后续规格中,它已被划分为更小的字段,但为了简单起见,我们将按照原始规范中定义的字段进行处理。该字段传达了用于IP数据报的服务质量。

全长场len传达整个IP数据报的长度。由于是16位字段,因此最大长度为65535字节。大型IP数据报存在碎片化现象,在这些数据报中,它们被分割成更小的数据报,以满足不同通信接口的最大传输单元(MTU)。

id字段用于索引数据报,最终用于重新组装碎片化的IP数据报。字段的值只是一个由发送方递增的计数器。接收方通过该字段知道如何对传入的分片数据排序。

flags字段定义了数据报的各种控制标志。具体而言,发送方可以指定数据报是否允许分片,是最后分片数据还是更多分片数据进入。

分片偏移字段frag_offset在数据报中表示片段的位置。当然,第一个数据报将该索引设置为0。

ttl一个常用的属性,用来计算数据报的生命周期。通常由原始发送方设置为64,每个接收方都会逐一声明此计数器。当数据报降至零时,数据报将被丢弃, 并可能回复ICMP消息以表示错误。

proto字段为数据图提供了在其有效载荷中携带其他协议的固有能力。该字段通常包含16(UDP)或6(TCP)等值,仅用于将实际数据类型传达给接收方。

csum用于验证IP头的完整性。其算法相对简单,将在本教程中进一步说明。

最后,saddr和daddr字段分别表示数据报的源地址和目标地址。尽管这些字段长度为32位,因此提供了约45亿个地址,但地址范围将在不久的将来耗尽。IPv6协议将这一长度延长至128位,因此未来能够支持更大范围的互联网协议的地址范围。

校验和

使用校验和字段来检查IP数据报的完整性。

算法的实际代码如下:

uint16_t checksum(void *addr, int count)
{
    /* Compute Internet Checksum for "count" bytes
     *         beginning at location "addr".
     * Taken from https://tools.ietf.org/html/rfc1071
     */

    register uint32_t sum = 0;
    uint16_t * ptr = addr;

    while( count > 1 )  {
        /*  This is the inner loop */
        sum += * ptr++;
        count -= 2;
    }

    /*  Add left-over byte, if any */
    if( count > 0 )
        sum += * (uint8_t *) ptr;

    /*  Fold 32-bit sum to 16 bits */
    while (sum>>16)
        sum = (sum & 0xffff) + (sum >> 16);

    return ~sum;
}

以IP头为例45 00 00 54 41 e0 40 00 40 01 00 00 0a 00 00 04 0a 00 00 05:

  1. 将字段加在一起,使两者的互补之数产生01 1b 3e
  2. 然后,为了将其转换为其互补:1b 3e+01=1b 3f
  3. 最后,取一个互补的值,从而得出了校对值e4c0

IP头45 00 00 54 41 e0 40 00 40 01 e4 c0 0a 00 00 04 0a 00 00 05

可以通过再次应用算法来验证校验,如果结果为0,数据很可能就好了。

互联网控制消息协议版本(ICMP协议)

由于互联网协议缺乏可靠性机制,因此需要通过某种方式向各方通报可能的错误情况。因此,互联网控制消息协议(ICMP) 被用于网络中的诊断措施。一个例子是网关无法访问的情况——识别此功能的网络堆栈会将ICMP“Gateway Unreachable”消息发送回源。

头部格式

ICMP 头位于相应 IP 数据包的有效载荷中。ICMPv4 头标的结构如下:

struct icmp_v4 {
    uint8_t type;
    uint8_t code;
    uint16_t csum;
    uint8_t data[];
} __attribute__((packed));

在这里,type字段发送消息的类型。该字段预留了42个值,仅使用约8个常用值。在这里的实现中,使用了类型0(回声回复)、3(目的地不可访问)和8型(回声请求)。

code字段进一步描述了消息的含义。例如,当类型为3(目的地不可访问)时,代码字段意味着原因。常见错误是数据包无法路由到网络时:源主机很可能接收到带有第3类代码和代码0(Net Unreachable)的ICMP消息。

csum字段与 IPv4 头字段相同,并且可以使用相同的算法来计算它。然而,在ICMPv4中,校验和是端到端的,这意味着在计算校验和时也包含有效载荷。

消息及其处理

实际的ICMP有效载荷包括查询/信息消息和错误消息。首先,我们将查看 Echo 请求/回复消息,通常称为网络中的“ping”:

struct icmp_v4_echo {
    uint16_t id;
    uint16_t seq;
    uint8_t data[];
} __attribute__((packed));

消息格式很紧凑。字段id由发送主机设置,以确定回声回复的处理过程。

字段seq是响应的序列号,它只是一个从零开始的数值,每当出现新的响应请求时,它就会逐一递增。用于检测在传输过程中是否出现响应消息消失或被重新排序。

data字段是可选的,但通常包含像响应的时间戳这样的信息。然后可用于估算主机之间的往返时间。

Destination Unreachable最常见的ICMPv4错误消息“目的地无法访问”具有以下格式:

struct icmp_v4_dst_unreachable {
    uint8_t unused;
    uint8_t len;
    uint16_t var;
    uint8_t data[];
} __attribute__((packed));

第一个octet 未使用。然后len字段表示原始数据报的长度,在 IPv4 的 4 个octet 中。2-octet 字段的值var取决于ICMP代码。

最后,尽可能将导致“目的地无法到达状态”的原始IP数据包放入data字段

测试实现

从 shell 中,我们可以验证我们的用户空间网络堆栈是否响应 ICMP 的回波请求:

关键代码实现

void icmpv4_incoming(struct sk_buff *skb) 
{
    struct iphdr *iphdr = ip_hdr(skb);
    struct icmp_v4 *icmp = (struct icmp_v4 *) iphdr->data;

    //TODO: Check csum

    switch (icmp->type) {
    case ICMP_V4_ECHO:
        icmpv4_reply(skb);
        return;
    case ICMP_V4_DST_UNREACHABLE:
        print_err("ICMPv4 received 'dst unreachable' code %d, "
                  "check your routes and firewall rules\n", icmp->code);
        goto drop_pkt;
    default:
        print_err("ICMPv4 did not match supported types\n");
        goto drop_pkt;
    }

drop_pkt:
    free_skb(skb);
    return;
}

void icmpv4_reply(struct sk_buff *skb)
{
    struct iphdr *iphdr = ip_hdr(skb);
    struct icmp_v4 *icmp;
    struct sock sk;
    memset(&sk, 0, sizeof(struct sock));
    
    uint16_t icmp_len = iphdr->len - (iphdr->ihl * 4);

    skb_reserve(skb, ETH_HDR_LEN + IP_HDR_LEN + icmp_len);
    skb_push(skb, icmp_len);
    
    icmp = (struct icmp_v4 *)skb->data;
        
    icmp->type = ICMP_V4_REPLY;
    icmp->csum = 0;
    icmp->csum = checksum(icmp, icmp_len, 0);

    skb->protocol = ICMPV4;
    sk.daddr = iphdr->saddr;

    ip_output(&sk, skb);
    free_skb(skb);
}

IP报文的接收

int ip_rcv(struct sk_buff *skb)
{
    struct iphdr *ih = ip_hdr(skb);
    uint16_t csum = -1;

    if (ih->version != IPV4) {
        print_err("Datagram version was not IPv4\n");
        goto drop_pkt;
    }

    if (ih->ihl < 5) {
        print_err("IPv4 header length must be at least 5\n");
        goto drop_pkt;
    }

    if (ih->ttl == 0) {
        //TODO: Send ICMP error
        print_err("Time to live of datagram reached 0\n");
        goto drop_pkt;
    }

    csum = checksum(ih, ih->ihl * 4, 0);

    if (csum != 0) {
        // Invalid checksum, drop packet handling
        goto drop_pkt;
    }

    // TODO: Check fragmentation, possibly reassemble

    ip_init_pkt(ih);

    ip_dbg("in", ih);

    switch (ih->proto) {
    case ICMPV4:
        icmpv4_incoming(skb);
        return 0;
    case IP_TCP:
        tcp_in(skb);
        return 0;
    default:
        print_err("Unknown IP header proto\n");
        goto drop_pkt;
    }

drop_pkt:
    free_skb(skb);
    return 0;
}

IP报文发送:

void ip_send_check(struct iphdr *ihdr)
{
    uint32_t csum = checksum(ihdr, ihdr->ihl * 4, 0);
    ihdr->csum = csum;
}

int ip_output(struct sock *sk, struct sk_buff *skb)
{
    struct rtentry *rt;
    struct iphdr *ihdr = ip_hdr(skb);

    rt = route_lookup(sk->daddr);

    if (!rt) {
        // TODO: dest_unreachable
        print_err("IP output route lookup fail\n");
        return -1;
    }

    skb->dev = rt->dev;
    skb->rt = rt;

    skb_push(skb, IP_HDR_LEN);

    ihdr->version = IPV4;
    ihdr->ihl = 0x05;
    ihdr->tos = 0;
    ihdr->len = skb->len;
    ihdr->id = ihdr->id;
    ihdr->frag_offset = 0x4000;
    ihdr->ttl = 64;
    ihdr->proto = skb->protocol;
    ihdr->saddr = skb->dev->addr;
    ihdr->daddr = sk->daddr;
    ihdr->csum = 0;

    ip_dbg("out", ihdr);

    ihdr->len = htons(ihdr->len);
    ihdr->id = htons(ihdr->id);
    ihdr->daddr = htonl(ihdr->daddr);
    ihdr->saddr = htonl(ihdr->saddr);
    ihdr->csum = htons(ihdr->csum);
    ihdr->frag_offset = htons(ihdr->frag_offset);

    ip_send_check(ihdr);

    return dst_neigh_output(skb);
}

这篇文章的源代码可在 GitHub 上找到。