skaiuijing

引言

ping命令我们经常使用,也是最常见的icmp协议。

ping命令的主要作用是诊断网络连接性,所以并没有什么特性,它要做的仅仅是回应与报告。

相信大家都已经知道什么是icmp协议了,那么怎么实现ping呢?

RFC文档

参考文档为RFC792,RFC 792: Internet Control Message Protocol

报文参考格式,由于使用ip报文,因此我们需要实现ip结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Message Formats

ICMP messages are sent using the basic IP header. The first octet of
the data portion of the datagram is a ICMP type field; the value of
this field determines the format of the remaining data. Any field
labeled "unused" is reserved for later extensions and must be zero
when sent, but receivers should not use these fields (except to
include them in the checksum). Unless otherwise noted under the
individual format descriptions, the values of the internet header
fields are as follows:

Version

4

IHL

Internet header length in 32-bit words.

Type of Service

0

Total Length

Length of internet header and data in octets.

Identification, Flags, Fragment Offset

Used in fragmentation, see [1].

Time to Live

Time to live in seconds; as this field is decremented at each
machine in which the datagram is processed, the value in this
field should be at least as great as the number of gateways which
this datagram will traverse.

Protocol

ICMP = 1

Header Checksum

The 16 bit one's complement of the one's complement sum of all 16
bit words in the header. For computing the checksum, the checksum
field should be zero. This checksum may be replaced in the
future.

Source Address

The address of the gateway or host that composes the ICMP message.
Unless otherwise noted, this can be any of a gateway's addresses.

Destination Address

The address of the gateway or host to which the message should be
sent.

目标不可达报文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

Destination Unreachable Message

0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unused |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Internet Header + 64 bits of Original Data Datagram |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

IP Fields:

Destination Address

The source network and address from the original datagram's data.

ICMP Fields:

Type

3

Code

0 = net unreachable;

1 = host unreachable;

2 = protocol unreachable;

3 = port unreachable;

4 = fragmentation needed and DF set;

5 = source route failed.

Checksum

The checksum is the 16-bit ones's complement of the one's
complement sum of the ICMP message starting with the ICMP Type.
For computing the checksum , the checksum field should be zero.
This checksum may be replaced in the future.

Internet Header + 64 bits of Data Datagram

The internet header plus the first 64 bits of the original


datagram's data. This data is used by the host to match the
message to the appropriate process. If a higher level protocol
uses port numbers, they are assumed to be in the first 64 data
bits of the original datagram's data.

Description

If, according to the information in the gateway's routing tables,
the network specified in the internet destination field of a
datagram is unreachable, e.g., the distance to the network is
infinity, the gateway may send a destination unreachable message
to the internet source host of the datagram. In addition, in some
networks, the gateway may be able to determine if the internet
destination host is unreachable. Gateways in these networks may
send destination unreachable messages to the source host when the
destination host is unreachable.

If, in the destination host, the IP module cannot deliver the
datagram because the indicated protocol module or process port is
not active, the destination host may send a destination
unreachable message to the source host.

Another case is when a datagram must be fragmented to be forwarded
by a gateway yet the Don't Fragment flag is on. In this case the
gateway must discard the datagram and may return a destination
unreachable message.

Codes 0, 1, 4, and 5 may be received from a gateway. Codes 2 and
3 may be received from a host.

这里我们只实现一个简单的ping,所以其他的部分留给感兴趣读者继续阅读RFC文档了

实现

看了上面的信息,我们开始尝试实现结构体:

这是ip的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

struct ip_struct {
/*LITTLE_ENDIAN!!!*/
unsigned char ip_hl:4;
unsigned char ip_v:4;
unsigned char ip_tos;
short ip_len;
unsigned short ip_id;
short ip_off;
unsigned char ip_ttl;
unsigned char ip_p;
unsigned short ip_sum;
struct _in_addr ip_src;
struct _in_addr ip_dst;

}__attribute__((packed));

让我们进一步实现icmp的结构体,由于icmp有不同的类型,我们需要使用union字段兼容不同类型的报文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

struct icmp {
unsigned char icmp_type;
unsigned char icmp_code;
unsigned short icmp_cksum;

union {
unsigned char ih_pptr;
struct _in_addr addr;
struct ih_idseq {
unsigned short icmp_id;
unsigned short icmp_seq;
}idseq;

struct ih_pmtu {
short ipm_void;
short ipm_nextmtu;
}pmtu;

}icmp_hun;

}__attribute__((packed));

icmp处理

当我们在ip首部判断出来是icmp协议时,协议栈调用icmp_input:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

void icmp_input(struct buf *sk, int hlen)
{
struct icmp *icp;
icp = (struct icmp *)sk->data;

switch (icp->icmp_type)
{
case ICMP_ECHO:
printf("icmp echo!\r\n");

icmp_reflect(sk);

break;

default:
break;
}

}

我们要做的事情非常简单,甚至连申请buf都不用,我们仅仅只需要将数据包反射回去即可:

1
2
3
4
5
6
7
8
9
10
11
12
void icmp_reflect(struct buf *sk)
{
struct buf *opts;
unsigned short len;
len = sk->data_len + sizeof(struct ip_struct) + sizeof(struct eth_hdr);
struct icmp *send_icp = (struct icmp *)sk->data;
send_icp->icmp_type = ICMP_ECHOREPLY;
send_icp->icmp_cksum = 0;
send_icp->icmp_cksum = in_checksum((void *)send_icp, len);

icmp_send(sk, opts);
}

在修改icmp报文类型并指定目的ip后,我们调用ip输出处理,数据包将会从网卡发送出去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21


void icmp_send(struct buf *sk, struct buf *opts)
{
struct ip_struct *ip;
struct _sockaddr_in *sa;
struct route rt;

sk->data -= sizeof(struct ip_struct);
sk->data_len += sizeof(struct ip_struct);

ip = (struct ip_struct *)sk->data;
sk->type = ip->ip_p;

sa = (struct _sockaddr_in *)&rt.ro_dst;
sa->sin_family = AF_INET;
sa->sin_addr = ip->ip_src;
sa->sin_len = sizeof(*sa);

ip_output(sk, &rt);
}

测试

现在让我们试一试ping协议:

在终端使用ping命令,其中的192.168.1.200是我们虚拟网卡的ip:

1
ping 192.168.1.200

我们的协议栈成功回应了ping请求:

1
2
3
4
5
PING 192.168.1.200 (192.168.1.200) 56(84) bytes of data.
64 bytes from 192.168.1.200: icmp_seq=1 ttl=64 time=0.314 ms
64 bytes from 192.168.1.200: icmp_seq=2 ttl=64 time=0.461 ms
64 bytes from 192.168.1.200: icmp_seq=3 ttl=64 time=0.391 ms
64 bytes from 192.168.1.200: icmp_seq=4 ttl=64 time=0.264 ms

结语

综上,我们实现了一个ping协议。

网络协议的框架其实就是TCP与IP。

TCP与IP协议毕竟是核心,这部分内容需要结合操作系统源码进行详细讲解。

考虑到TCP协议的重要性,下面还是直接来到TCP协议吧,在这一部分,将会详细讲解RFC文档,主要使用4.4FreeBSD源码讲解,结合Linux内核的部分代码设计。

其实应该先详细讲ip协议的,但是IP协议的分片、重组、路由等各种特性都值得仔细讲一讲。如果先讲ip,就不可避免从头讲到尾,操作系统与Internet校验和算法的优化、Internet本身与抽象代数的性质、数据包分片、网络地址分配等等,特别是路由部分,不可避免要涉及Linux内核的多重哈希或者FreeBSD的radix树设计。

TCP协议的内容估计要讲十几二十章,光是RFC文档,理解其内容和设计哲学就要花不少功夫。

TCP的设计哲学其实就一句话:对自己做的事情要严格,对别人做的事情要宽容。