编写一个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:
- 将字段加在一起,使两者的互补之数产生
01 1b 3e。 - 然后,为了将其转换为其互补:
1b 3e+01=1b 3f。 - 最后,取一个互补的值,从而得出了校对值
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 上找到。