查看“︁编写一个TCP/IP栈2-IPv4和ICMPv4”︁的源代码
←
编写一个TCP/IP栈2-IPv4和ICMPv4
跳转到导航
跳转到搜索
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
您可以查看和复制此页面的源代码。
此次在我们的用户空间TCP/IP堆栈中,我们将实现一个最小可行的IP层,''并使用ICMP的回声请求(''也称为pings)进行测试。 我们将查看IPv4和ICMPv4的格式,并介绍如何检查其完整性。一些功能,例如IP分片,作为练习。 对于我们的网络栈, 选择IPv4 优先于 IPv6,因为它仍然是互联网的默认网络协议。然而,未来我们的网络栈可以通过IPv6进行扩展。 = 互联网协议版本4 = 在实现以太网帧之后,下一层(L3)负责将数据传输到目标。''即互联网协议''(IP) 是为TCP和UDP等传输协议提供基础而发明的。它是无连接的,这意味着与TCP不同,所有数据报在网络堆栈中都是相互独立处理的。这也意味着IP数据报可能会丢失或者过期<sup>。</sup> 此外, IP 不保证成功交付。这是协议设计者有意识地选择的,因为IP旨在为同样无法保证交付的协议提供基础。UDP就是这样一个协议。 如果需要通信方之间的可靠性,则在IP之上使用TCP等协议。在这种情况下,更高级别的协议负责检测缺失的数据, 并确保所有数据均已送达。 == 头部格式 == IPv4 头通常长度为 20 个 八位。头部可以包含尾随选项,但在我们的实现中会省略这些选项。字段的含义相对简单,可以描述为一个C结构体: 4位<code>version</code>字段表示互联网头的格式。在我们的情况下,IPv4 的值为 4。<syntaxhighlight lang="c" line="1"> 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)); </syntaxhighlight>''I''互联网头长字段<code>ihl</code>长度同样为4位,''words''表示IP头中32位字数。由于字段大小为 4 位,因此最大值只能为 15。因此,IP头的最大长度为60个八位。 ''服务字段的类型''<code>tos</code>源自第一个IP规范。在后续规格中,它已被划分为更小的字段,但为了简单起见,我们将按照原始规范中定义的字段进行处理。该字段传达了用于IP数据报的服务质量。 全长场<code>len</code>传达整个IP数据报的长度。由于是16位字段,因此最大长度为65535字节。大型IP数据报存在碎片化现象,在这些数据报中,它们被分割成更小的数据报,以满足不同通信接口的最大传输单元(MTU)。 <code>id</code>字段用于索引数据报,最终用于重新组装碎片化的IP数据报。字段的值只是一个由发送方递增的计数器。接收方通过该字段知道如何对传入的分片数据排序。 <code>flags</code>字段定义了数据报的各种控制标志。具体而言,发送方可以指定数据报是否允许分片,是最后分片数据还是更多分片数据进入。 ''分片偏移字段''<code>frag_offset</code>在数据报中表示片段的位置。当然,第一个数据报将该索引设置为0。 <code>ttl</code>一个常用的属性,用来计算数据报的生命周期。通常由原始发送方设置为64,每个接收方都会逐一声明此计数器。当数据报降至零时,数据报将被丢弃, 并可能回复ICMP消息以表示错误。 <code>proto</code>字段为数据图提供了在其有效载荷中携带其他协议的固有能力。该字段通常包含16(UDP)或6(TCP)等值,仅用于将实际数据类型传达给接收方。 <code>csum</code>用于验证IP头的完整性。其算法相对简单,将在本教程中进一步说明。 最后,<code>saddr</code><code>和daddr</code>字段分别表示数据报的源地址和目标地址。尽管这些字段长度为32位,因此提供了约45亿个地址,但地址范围将在不久的将来耗尽。IPv6协议将这一长度延长至128位,因此未来能够支持更大范围的互联网协议的地址范围。 == 校验和 == 使用校验和字段来检查IP数据报的完整性。 算法的实际代码如下:<syntaxhighlight lang="c" line="1"> 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; } </syntaxhighlight>以IP头为例<code>45 00 00 54 41 e0 40 00 40 01 00 00 0a 00 00 04 0a 00 00 05</code>: # 将字段加在一起,使两者的互补之数产生<code>01 1b 3e</code>。 # 然后,为了将其转换为其互补:<code>1b 3e</code>+<code>01</code>=<code>1b 3f</code>。 # 最后,取一个互补的值,从而得出了校对值<code>e4c0</code>。 IP头<code>45 00 00 54 41 e0 40 00 40 01 e4 c0 0a 00 00 04 0a 00 00 05</code>。 可以通过再次应用算法来验证校验,如果结果为0,数据很可能就好了。 = 互联网控制消息协议版本(ICMP协议) = 由于互联网协议缺乏可靠性机制,因此需要通过某种方式向各方通报可能的错误情况。因此,''互联网控制消息协议''(ICMP) 被用于网络中的诊断措施。一个例子是网关无法访问的情况——识别此功能的网络堆栈会将ICMP“Gateway Unreachable”消息发送回源。 == 头部格式 == ICMP 头位于相应 IP 数据包的有效载荷中。ICMPv4 头标的结构如下:<syntaxhighlight lang="c"> struct icmp_v4 { uint8_t type; uint8_t code; uint16_t csum; uint8_t data[]; } __attribute__((packed)); </syntaxhighlight>在这里,<code>type</code>字段发送消息的类型。该字段预留了42个值,仅使用约8个常用值。在这里的实现中,使用了类型0(回声回复)、3(目的地不可访问)和8型(回声请求)。 <code>code</code>字段进一步描述了消息的含义。例如,当类型为3(目的地不可访问)时,代码字段意味着原因。常见错误是数据包无法路由到网络时:源主机很可能接收到带有第3类代码和代码0(Net Unreachable)的ICMP消息。 <code>csum</code>字段与 IPv4 头字段相同,并且可以使用相同的算法来计算它。然而,在ICMPv4中,''校验和是端到端的'',这意味着在计算校验和时也包含有效载荷。 == 消息及其处理 == 实际的ICMP有效载荷包括查询/信息消息和错误消息。首先,我们将查看 Echo 请求/回复消息,通常称为网络中的“ping”:<syntaxhighlight lang="c"> struct icmp_v4_echo { uint16_t id; uint16_t seq; uint8_t data[]; } __attribute__((packed)); </syntaxhighlight>消息格式很紧凑。字段<code>id</code>由发送主机设置,以确定回声回复的处理过程。 <code>字段seq</code>是响应的序列号,它只是一个从零开始的数值,每当出现新的响应请求时,它就会逐一递增。用于检测在传输过程中是否出现响应消息消失或被重新排序。 <code>data</code>字段是可选的,但通常包含像响应的时间戳这样的信息。然后可用于估算主机之间的往返时间。 ''Destination Unreachable''最常见的ICMPv4错误消息“目的地无法访问”具有以下格式:<syntaxhighlight lang="c"> struct icmp_v4_dst_unreachable { uint8_t unused; uint8_t len; uint16_t var; uint8_t data[]; } __attribute__((packed)); </syntaxhighlight>第一个octet 未使用。然后<code>len</code>字段表示原始数据报的长度,在 IPv4 的 4 个octet 中。2-octet 字段的值<code>var</code>取决于ICMP代码。 最后,尽可能将导致“目的地无法到达状态”的原始IP数据包放入<code>data</code><code>字段</code>。 = 测试实现 = 从 shell 中,我们可以验证我们的用户空间网络堆栈是否响应 ICMP 的回波请求: = 关键代码实现 = <syntaxhighlight lang="c" line="1"> 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); } </syntaxhighlight>IP报文的接收<syntaxhighlight lang="c" line="1"> 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; } </syntaxhighlight>IP报文发送:<syntaxhighlight lang="c" line="1"> 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); } </syntaxhighlight>这篇文章的[https://github.com/saminiir/level-ip 源代码]可在 GitHub 上找到。
返回
编写一个TCP/IP栈2-IPv4和ICMPv4
。
导航菜单
个人工具
登录
命名空间
页面
讨论
大陆简体
查看
阅读
查看源代码
查看历史
更多
搜索
导航
首页
最近更改
随机页面
特殊页面
工具
链入页面
相关更改
页面信息