ICMP协议编程实践:实现ping命令(C语言)
众所周知, ping 命令通过 ICMP 协议探测目标 IP 并计算 往返时间 。本文使用 C 语言开发一个 ping 命令,以演示如何通过 套接字**发送 和 接收**ICMP 协议报文。
注解
程序源码 可在本文末尾复制,或者在 Github 上下载: ping.c 。
报文封装
ICMP 报文承载在 IP 报文之上,头部结构非常简单:
注意到, ICMP 头部只有三个固定字段,其余部分因消息类型而异。固定字段如下:
- type , 消息类型 ;
- code , 代码 ;
- checksum , 校验和 ;
ICMP 报文有很多不同的类型,由 type 和 code 字段区分。而 ping 命令使用其中两种:
如上图,机器 A 通过 回显请求 ( Echo Request ) 询问机器 B ;机器 B 收到报文后通过 回显答复 ( Echo Reply ) 响应机器 A 。这两种报文的典型结构如下:
对应的 type 以及 code 字段值列举如下:
名称 | 类型 | ”代码“ |
---|---|---|
回显请求 | 8 | 0 |
回显答复 | 0 | 0 |
按照惯例,回显报文除了固定字段,其余部分组织成 3 个字段:
- 标识符 ( identifier ),一般填写进程 PID 以区分其他 ping 进程;
- 报文序号 ( sequence number ),用于编号报文序列;
- 数据 ( data ),可以是任意数据;
按 ICMP 规定, 回显答复 报文原封不动回传这些字段。因此,可以将 发送时间 封装在 数据负载 ( payload )中,收到答复后将其取出,用于计算 往返时间 ( round trip time )。
定义一个结构体用以封装报文:
struct icmp_echo {
// header
uint8_t type;
uint8_t code;
uint16_t checksum;
uint16_t ident;
uint16_t seq;
// data
double sending_ts;
char magic[MAGIC_LEN];
};
前 3 个字段为 ICMP 公共头部;中间 2 个字段为 回显请求 、 回显答复 惯例头部;其余字段为 数据负载 ,包括一个双精度 发送时间戳 以及一个固定的魔性字符串。
校验和
ICMP 报文校验和字段需要自行计算,计算步骤如下:
- 以 0 为校验和封装一个用于计算的 伪报文 ;
- 将报文分成两个字节一组,如果总字节数为奇数,则在末尾追加一个零字节;
- 对所有 双字节 进行按位求和;
- 将高于 16 位的进位取出相加,直到没有进位;
- 将校验和按位取反;示例代码如下:
uint16_t calculate_checksum(unsigned char* buffer, int bytes)
{
uint32_t checksum = 0;
unsigned char* end = buffer + bytes;
// odd bytes add last byte and reset end
if (bytes % 2 == 1) {
end = buffer + bytes - 1;
checksum += (*end) << 8;
}
// add words of two bytes, one by one
while (buffer < end) {
checksum += buffer[0] << 8;
checksum += buffer[1];
buffer += 2;
}
// add carry if any
uint32_t carray = checksum >> 16;
while (carray) {
checksum = (checksum & 0xffff) + carray;
carray = checksum >> 16;
}
// negate it
checksum = ~checksum;
return checksum & 0xffff;
}
套接字
编程实现网络通讯,离不开 套接字 ( socket ),收发 ICMP 报文当然也不例外:
#include <arpa/inet.h>
int s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
调用 sendto 系统调用发送 ICMP 报文:
struct icmp_echo icmp;
struct sockaddr_in peer_addr;
sendto(s, &icmp, sizeof(icmp), 0, peer_addr, sizeof(peer_addr));
其中,第一个参数为 套接字 ;第二、三个参数为封装好的 ICMP报文 及 长度 ;第四、五个参数为 目的地址 及地址结构体长度。
调用 recvfrom 系统调用接收 ICMP 报文:
#define MTU 1500
char buffer[MTU];
struct sockaddr_in peer_addr;
int addr_len = sizeof(peer_addr);
recvfrom(s, buffer, MTU, 0, &peer_addr, &addr_len);
struct icmp_echo *icmp = buffer + 20;
参数为接收缓冲区大小,这里用 1500 刚好是一个典型的 MTU 大小。注意到, recvfrom 系统调用返回 IP 报文,去掉前 20 字节的 IP 头部便得到 ICMP 报文。
注解
注意,创建 原始套接字 ( SOCK_RAW )需要超级用户权限。
程序实现
掌握基本原理后,便可着手编写代码了。
首先,实现 send_echo_request 函数,用于发送 ICMP回显请求 报文:
int send_echo_request(int sock, struct sockaddr_in* addr, int ident, int seq)
{
// allocate memory for icmp packet
struct icmp_echo icmp;
bzero(&icmp, sizeof(icmp));
// fill header files
icmp.type = 8;
icmp.code = 0;
icmp.ident = htons(ident);
icmp.seq = htons(seq);
// fill magic string
strncpy(icmp.magic, MAGIC, MAGIC_LEN);
// fill sending timestamp
icmp.sending_ts = get_timestamp();
// calculate and fill checksum
icmp.checksum = htons(
calculate_checksum((unsigned char*)&icmp, sizeof(icmp))
);
// send it
int bytes = sendto(sock, &icmp, sizeof(icmp), 0,
(struct sockaddr*)addr, sizeof(*addr));
if (bytes == -1) {
return -1;
}
return 0;
}
第 3-17 行封装用于计算校验和的 伪报文 ,注意到 类型 字段为 8 ,代码 字段为 0 ,校验和 字段为 0 ,标识符 以及 序号 由参数指定;第 10 行调用 calculate_checksum 函数计算 校验和 ;第 25-26 调 sendto 系统调用将报文发送出去。
对应地,实现 recv_echo_reply 用于接收 ICMP回显答复 报文:
int recv_echo_reply(int sock, int ident)
{
// allocate buffer
char buffer[MTU];
struct sockaddr_in peer_addr;
// receive another packet
int addr_len = sizeof(peer_addr);
int bytes = recvfrom(sock, buffer, sizeof(buffer), 0,
(struct sockaddr*)&peer_addr, &addr_len);
if (bytes == -1) {
// normal return when timeout
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return 0;
}
return -1;
}
// find icmp packet in ip packet
struct icmp_echo* icmp = (struct icmp_echo*)(buffer + 20);
// check type
if (icmp->type != 0 || icmp->code != 0) {
return 0;
}
// match identifier
if (ntohs(icmp->ident) != ident) {
return 0;
}
// print info
printf("%s seq=%d %5.2fms\n",
inet_ntoa(peer_addr.sin_addr),
ntohs(icmp->seq),
(get_timestamp() - icmp->sending_ts) * 1000
);
return 0;
}
第 3-5 行分配用于接收报文的 缓冲区 ;第 9-10 行调用 recvfrom 系统调用 接收 一个 新报文 ;第 13-15 接收报文 超时 ,正常返回;第 21 行从 IP 报文中取出 ICMP 报文;第 24-26 行检查 ICMP报文类型 ;第 29-31 检查 标识符 是否匹配;第 32-38 行计算 往返时间 并打印提示信息。
最后,实现 ping 函数,循环发送并接收报文:
int ping(const char *ip)
{
// for store destination address
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
// fill address, set port to 0
addr.sin_family = AF_INET;
addr.sin_port = 0;
if (inet_aton(ip, (struct in_addr*)&addr.sin_addr.s_addr) == 0) {
return -1;
};
// create raw socket for icmp protocol
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sock == -1) {
return -1;
}
// set socket timeout option
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = RECV_TIMEOUT_USEC;
int ret = setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
if (ret == -1) {
return -1;
}
double next_ts = get_timestamp();
int ident = getpid();
int seq = 1;
for (;;) {
// time to send another packet
if (get_timestamp() >= next_ts) {
// send it
ret = send_echo_request(sock, &addr, ident, seq);
if (ret == -1) {
perror("Send failed");
}
// update next sendint timestamp to one second later
next_ts += 1;
// increase sequence number
seq += 1;
}
// try to receive and print reply
ret = recv_echo_reply(sock, ident);
if (ret == -1) {
perror("Receive failed");
}
}
return 0;
}
第 3-12 行,初始化 目的地址 结构体;第 14-18 行,创建用于发送、接收 ICMP 报文的 套接字 ;第 20-27 行,将套接字 接收超时时间 设置为 0.1 秒,以便 等待答复报文 的同时有机会 发送请求报文 ;第 30-31 行,获取进程 PID 作为 标识符 、同时初始化报文 序号 ;接着,循环发送并接收报文;第 35-46 行,当前时间达到发送时间则调用 send_echo_request 函数 发送请求报文 ,更新下次发送时间并自增序号;第 48-52 行,调用 recv_echo_reply 函数 接收答复报文 。
将以上所有代码片段组装在一起,便得到 ping.c 命令。迫不及待想运行一下:
$ gcc -o ping ping.c
$ sudo ./ping 8.8.8.8
8.8.8.8 seq=1 25.70ms
8.8.8.8 seq=2 25.28ms
8.8.8.8 seq=3 25.26ms
It works!
程序源码
/**
* FileName: ping.c
* Author: Fasion Chan
* @contact: fasionchan@gmail.com
* @version: $Id$
*
* Description:
*
* Changelog:
*
**/
#include <arpa/inet.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <unistd.h>
#define MAGIC "1234567890"
#define MAGIC_LEN 11
#define MTU 1500
#define RECV_TIMEOUT_USEC 100000
struct icmp_echo {
// header
uint8_t type;
uint8_t code;
uint16_t checksum;
uint16_t ident;
uint16_t seq;
// data
double sending_ts;
char magic[MAGIC_LEN];
};
double get_timestamp()
{
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec + ((double)tv.tv_usec) / 1000000;
}
uint16_t calculate_checksum(unsigned char* buffer, int bytes)
{
uint32_t checksum = 0;
unsigned char* end = buffer + bytes;
// odd bytes add last byte and reset end
if (bytes % 2 == 1) {
end = buffer + bytes - 1;
checksum += (*end) << 8;
}
// add words of two bytes, one by one
while (buffer < end) {
checksum += buffer[0] << 8;
checksum += buffer[1];
buffer += 2;
}
// add carry if any
uint32_t carray = checksum >> 16;
while (carray) {
checksum = (checksum & 0xffff) + carray;
carray = checksum >> 16;
}
// negate it
checksum = ~checksum;
return checksum & 0xffff;
}
int send_echo_request(int sock, struct sockaddr_in* addr, int ident, int seq)
{
// allocate memory for icmp packet
struct icmp_echo icmp;
bzero(&icmp, sizeof(icmp));
// fill header files
icmp.type = 8;
icmp.code = 0;
icmp.ident = htons(ident);
icmp.seq = htons(seq);
// fill magic string
strncpy(icmp.magic, MAGIC, MAGIC_LEN);
// fill sending timestamp
icmp.sending_ts = get_timestamp();
// calculate and fill checksum
icmp.checksum = htons(
calculate_checksum((unsigned char*)&icmp, sizeof(icmp))
);
// send it
int bytes = sendto(sock, &icmp, sizeof(icmp), 0,
(struct sockaddr*)addr, sizeof(*addr));
if (bytes == -1) {
return -1;
}
return 0;
}
int recv_echo_reply(int sock, int ident)
{
// allocate buffer
char buffer[MTU];
struct sockaddr_in peer_addr;
// receive another packet
int addr_len = sizeof(peer_addr);
int bytes = recvfrom(sock, buffer, sizeof(buffer), 0,
(struct sockaddr*)&peer_addr, &addr_len);
if (bytes == -1) {
// normal return when timeout
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return 0;
}
return -1;
}
// find icmp packet in ip packet
struct icmp_echo* icmp = (struct icmp_echo*)(buffer + 20);
// check type
if (icmp->type != 0 || icmp->code != 0) {
return 0;
}
// match identifier
if (ntohs(icmp->ident) != ident) {
return 0;
}
// print info
printf("%s seq=%d %5.2fms\n",
inet_ntoa(peer_addr.sin_addr),
ntohs(icmp->seq),
(get_timestamp() - icmp->sending_ts) * 1000
);
return 0;
}
int ping(const char *ip)
{
// for store destination address
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
// fill address, set port to 0
addr.sin_family = AF_INET;
addr.sin_port = 0;
if (inet_aton(ip, (struct in_addr*)&addr.sin_addr.s_addr) == 0) {
return -1;
};
// create raw socket for icmp protocol
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sock == -1) {
return -1;
}
// set socket timeout option
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = RECV_TIMEOUT_USEC;
int ret = setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
if (ret == -1) {
return -1;
}
double next_ts = get_timestamp();
int ident = getpid();
int seq = 1;
for (;;) {
// time to send another packet
if (get_timestamp() >= next_ts) {
// send it
ret = send_echo_request(sock, &addr, ident, seq);
if (ret == -1) {
perror("Send failed");
}
// update next sendint timestamp to one second later
next_ts += 1;
// increase sequence number
seq += 1;
}
// try to receive and print reply
ret = recv_echo_reply(sock, ident);
if (ret == -1) {
perror("Receive failed");
}
}
return 0;
}
int main(int argc, const char* argv[])
{
return ping(argv[1]);
}
下一步
本节以 C
语言为例,演示了 ICMP 编程方法。如果你对其他语言感兴趣,请按需取用:
订阅更新,获取更多学习资料,请关注我们的 微信公众号 :