ICMP(Internet Control Message Protocol,Internet控制报文协议)是一种面向无连接的协议,是TCP/IP协议族的一个子协议,属于网络层协议,用于在IP主机、路由器之间传递控制消息(网络通不通、主机是否可达、路由是否可用等)。当遇到IP数据无法访问目标、IP路由器无法按当前的传输速率转发数据包等情况时,会自动发送ICMP消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。
ICMP类型的部分列表如下:
TYPE | CODE | Description | Query | Error |
---|---|---|---|---|
0 | 0 | Echo Reply——回显应答(Ping应答) | x | |
3 | 0 | Network Unreachable——网络不可达 | x | |
3 | 1 | Host Unreachable——主机不可达 | x | |
3 | 2 | Protocol Unreachable——协议不可达 | x | |
3 | 3 | Port Unreachable——端口不可达 | x | |
3 | 6 | Destination network unknown——目的网络未知 | x | |
3 | 7 | Destination host unknown——目的主机未知 | x | |
8 | 0 | Echo request——回显请求(Ping请求) | x |
原始套接字提供普通TCP和UDP套接字所不提供的能力。具体如下:
进程可以使用原始套接字读与写ICMPv4、ICMPv6和IGMPv4等分组。
ping程序使用原始套接字发送ICMP回射请求并接收ICMP回射应答。
#include
int sockfd; // 使用原始套接字创建ICMPv4套接字
sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
大多数内核仅仅处理ICMP、IGMP、TCP和UDP的数据报,有了原始套接字,进程可以读写内核不处理其协议字段的IPv4数据报。
使用原始套接字时,进程可使用IP_HDRINCL
套接字选项自行构造IPv4首部。
const int on = 1;
if (setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0)
printf("出错");
原始套接字不存在端口号的概念。因此,在原始套接字上调用bind仅仅设置从这个原始套接字发送的所有数据报的源IP地址(未开启IP_HDRINCL
选项的前提下)。在原始套接字上调用connect仅仅指定目的IP地址,调用connect之后可把sendto调用改为write或send调用。
IP_HDRINCL
选项时,内核将根据socket函数第三个参数构造IPv4首部并把它置于来自进程的数据之前。IP_HDRINCL
选项后,整个IPv4首部由进程构造,不过IPv4标识字段可置为0以让内核设置,而且IPv4首部校验和字段总是由内核计算并存储。另外,IPv4选项字段是可选的。内核将IP数据报传递到原始套接字时,因没有端口号的概念,内核会检查所有进程上的所有原始套接字,进行如下3个测试以寻找所匹配的原始套接字。
注意,如果创建原始套接字socket函数的第三个参数为0值,且未调用过bind和connect,那么该原始套接字接收所有内核传递到原始套接字的数据报的一个副本。另外,内核传递到原始套接字的数据报一定是包括IPv4首部的完整数据报。
此ping程序仅支持一个-v
命令行选项,以打印详尽输出。为了追求最简化,也仅支持IPv4。ping程序的操作非常简单,往某个IP地址发送一个ICMP回射请求,该节点则以一个ICMP回射应答响应。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* C99中规定宏可以像函数一样带有可变参数 */
#define error_exit(format, ...) \
do { fprintf(stderr, format"\n", ##__VA_ARGS__); exit(EXIT_FAILURE); } while (0)
#define BUFSIZE 1500
char sendbuf[BUFSIZE];
int datalen = 56; // “可选数据”的长度
char *host; // 主机名或IP地址数串
int nsent; // 发送出去的分组“序列号”
pid_t pid; // 进程ID
int sockfd; // 原始套接字描述符
int verbose; // "-v"命令行选项是否存在标记
struct proto {
void (*fproc)(char *, ssize_t, struct msghdr *, struct timeval *);
void (*fsend)(void);
struct sockaddr *sasend; // sockaddr{} for send, from getaddrinfo
struct sockaddr *sarecv; // sockaddr{} for receiving
socklen_t salen; // length of sockaddr{}s
int icmpproto; // IPPROTO_xxx value for ICMP
} *pr;
void* Signal(int signo, void (*func)(int)) {
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
#ifdef SA_INTERRUPT
if (signo == SIGALRM) act.sa_flags |= SA_INTERRUPT;
#endif
#ifdef SA_RESTART
if (signo != SIGALRM) act.sa_flags |= SA_RESTART;
#endif
if (sigaction(signo, &act, &oact) < 0)
return SIG_ERR;
return oact.sa_handler; // 返回信号的旧行为
}
struct addrinfo* host_serv(const char *host, const char *serv, int family, int socktype) {
int n;
struct addrinfo hints, *res;
bzero(&hints, sizeof(struct addrinfo));
hints.ai_flags = AI_CANONNAME; // always return canonical name
hints.ai_family = family; // 0, AF_INET, AF_INET6, etc
hints.ai_socktype = socktype; // 0, SOCK_STREAM, SOCK_DGRAM, etc
if ((n = getaddrinfo(host, serv, &hints, &res)) != 0) {
error_exit("host_serv error for %s, %s: %s",
(host == NULL) ? "(no hostname)" : host,
(serv == NULL) ? "(no service name)" : serv,
gai_strerror(n));
}
return(res); // return pointer to first on linked list
}
char* sock_ntop_host(const struct sockaddr *sa, socklen_t salen) {
static char str[128]; // Unix domain is largest
switch (sa->sa_family) {
case AF_INET: {
struct sockaddr_in *sin = (struct sockaddr_in *)sa;
if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
return(NULL);
return(str);
}
default:
snprintf(str, sizeof(str), "sock_ntop_host: unknown AF_xxx: %d, len %d", sa->sa_family, salen);
return(str);
}
return (NULL);
}
/* 相减两个timeval时间,结果放到out中 */
static void tv_sub(struct timeval *out, struct timeval *in) {
if ((out->tv_usec -= in->tv_usec) < 0) { // out -= in
--out->tv_sec;
out->tv_usec += 1000000;
}
out->tv_sec -= in->tv_sec;
}
/* 处理所有接收到的ICMPv4消息。
* (当一个ICMPv4消息由进程在原始套接字上收取时,内核已经证实它的IPv4首部和ICMPv4首部中的基本字段的有效性)
*
* |--------------------len-------------------|
* |--------hlen1--------|-------icmplen------|
* ++++++++++++++++++++++++++++++++++++++++++++
* + IPv4 + IPv4 + ICMPv4 + ICMP +
* + 首部 + 选项 + 首部 + 数据 +
* ++++++++++++++++++++++++++++++++++++++++++++
* |--20B--|----0~40B----|---8B---|-----------|
* [ip] [icmp]
*
* (图:处理ICMPv4应答涉及的首部、指针和长度)
************************************************/
void proc_v4(char *ptr, ssize_t len, struct msghdr *msg, struct timeval *tvrecv) {
int hlen1, icmplen;
double rtt;
struct ip *ip;
struct icmp *icmp;
struct timeval *tvsend;
ip = (struct ip *)ptr; // start of IP header
hlen1 = ip->ip_hl << 2; // length of IP header
if (ip->ip_p != IPPROTO_ICMP) // 检查ICMP标识符字段
return; // not ICMP
icmp = (struct icmp *)(ptr + hlen1); // start of ICMP header
if ((icmplen = len - hlen1) < 8) // 是否为完整ICMP数据
return; // malformed packet
if (icmp->icmp_type == ICMP_ECHOREPLY) { // ICMP消息的“类型”
if (icmp->icmp_id != pid) // ICMP消息的“标识符”
return; // not a response to our ECHO_REQUEST
if (icmplen < 16)
return; // not enough data to use
tvsend = (struct timeval *)icmp->icmp_data; // ICMP消息的“可选数据”
tv_sub(tvrecv, tvsend);
rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0; // 计算RTT时间
printf("%d bytes from %s: seq=%u, ttl=%d, rtt=%.3f ms\n",
icmplen, sock_ntop_host(pr->sarecv, pr->salen),
icmp->icmp_seq, ip->ip_ttl, rtt);
}
/* 若指定-v(详尽输出)则显示所有接收ICMP消息 */
if (verbose) {
printf(" %d bytes from %s: type = %d, code = %d\n",
icmplen, sock_ntop_host(pr->sarecv, pr->salen),
icmp->icmp_type, icmp->icmp_code);
}
}
/* 计算网际网校验和(具体是指被校验的各个16位值的二进制反码和) */
static uint16_t in_cksum(uint16_t *addr, int len) {
int nleft = len;
uint32_t sum = 0;
uint16_t *w = addr;
uint16_t answer = 0;
/* Our algorithm is simple, using a 32 bit accumulator (sum), we add
* sequential 16 bit words to it, and at the end, fold back all the
* carry bits from the top 16 bits into the lower 16 bits. */
while (nleft > 1) {
sum += *w++;
nleft -= 2;
}
if (nleft == 1) {
*(unsigned char *)(&answer) = *(unsigned char *)w ;
sum += answer;
}
/* add back carry outs from top 16 bits to low 16 bits */
sum = (sum >> 16) + (sum & 0xffff); // sum的高16位与低16位第一次相加
sum += (sum >> 16); // 将上一步可能产生的高16位进位再次与低16位累加
answer = ~sum; // truncate to 16 bits
return(answer);
}
/* 发送ICMPv4回射请求消息。
*
* 0_______7.8_____15.16_____________31
* |__类型__|__代码__|_____校验和_____|
* |______标识符_____|_____序列号_____|
* | |
* . 可选数据 .
* |__________________________________|
*
* (图:ICMPv4消息的格式)
************************************************/
void send_v4(void) {
int len;
struct icmp *icmp;
/* 构造ICMPv4消息 */
icmp = (struct icmp *)sendbuf;
icmp->icmp_type = ICMP_ECHO; // 消息的“类型”
icmp->icmp_code = 0; // 消息的“代码”值为0
icmp->icmp_id = pid; // 消息的“标识符”使用ping进程的PID
icmp->icmp_seq = nsent++; // 消息的“序列号”递增
memset(icmp->icmp_data, 0xa5, datalen); // 消息的“可选数据”填充0xa5
gettimeofday((struct timeval *)icmp->icmp_data, NULL); // 消息的“可选数据”开始处存放发送时刻的8字节时间戳
/* 计算ICMP校验和 */
len = 8 + datalen; // ICMP“首部”和“可选数据”的长度和
icmp->icmp_cksum = 0; // 计算校验和之前,要将校验和字段置为0
icmp->icmp_cksum = in_cksum((u_short *)icmp, len);
/* 发送数据报(由于没有开启IP_HDRINCL选项,内核将构造IPv4首部并安置在上述ICMPv4消息缓冲区之前) */
sendto(sockfd, sendbuf, len, 0, pr->sasend, pr->salen);
}
/* 每隔1s发送一个ICMP回射请求 */
static void sig_alrm(int signo) {
(*pr->fsend)();
alarm(1);
return;
}
void readloop(void) {
int size;
char recvbuf[BUFSIZE];
char controlbuf[BUFSIZE];
struct msghdr msg;
struct iovec iov;
ssize_t n;
struct timeval tval;
/* 创建原始套接字(需超级用户特权) */
sockfd = socket(pr->sasend->sa_family, SOCK_RAW, pr->icmpproto); // 进程必须拥有超级用户特权才能创建原始套接字
setuid(getuid()); // 把进程的有效用户ID设置为实际用户ID,使得进程放弃对超级用户特权的拥有(防攻击)
/* 设置套接字接收缓冲区的大小,防止接收缓冲区溢出 */
size = 60 * 1024;
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
/* 启动回射请求,由SIGALRM信号每秒钟驱动一次 */
sig_alrm(SIGALRM);
/* 在一个原始套接字上读入收到的每个分组,显示ICMP回射应答 */
iov.iov_base = recvbuf;
iov.iov_len = sizeof(recvbuf);
msg.msg_name = pr->sarecv;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = controlbuf;
for ( ; ; ) {
msg.msg_namelen = pr->salen;
msg.msg_controllen = sizeof(controlbuf);
n = recvmsg(sockfd, &msg, 0); // 读入回射到原始ICMP套接字的每个分组
if (n < 0) {
if (errno == EINTR)
continue;
else
error_exit("recvmsg(%d)=%d", sockfd, n);
}
gettimeofday(&tval, NULL); // 记录分组收取时刻,用于计算RTT
(*pr->fproc)(recvbuf, n, &msg, &tval);
}
}
int main(int argc, char **argv) {
struct proto proto_v4 = { proc_v4, send_v4, NULL, NULL, 0, IPPROTO_ICMP }; // IPv4的proto结构
int c;
struct addrinfo *ai;
char *h;
/* getopt被用来解析命令行选项参数。调用一次,返回一个选项。
* extern int opterr; //当opterr=0时,getopt不向stderr输出错误信息
* extern int optopt; //未知选项存储在optopt中,并且getopt返回'?'
* extern int optind; //getopt要处理的argv中的下一个字符选项的索引
* extern char *optarg; //选项的参数指针
* 当再也检查不到包含的选项时,getopt返回-1,同时optind储存第一个不包含选项的命令行参数的索引。*/
opterr = 0;
while ((c = getopt(argc, argv, "v")) != -1) { // 此程序只有-v选项
switch (c) {
case 'v': // -v选项
verbose++;
break;
case '?': // 未知选项
error_exit("unrecognized option: %c", optopt);
}
}
if (optind != argc-1)
error_exit("usage: ping [ -v ] " );
host = argv[optind]; // 主机名或IP地址数串
pid = getpid() & 0xffff; // ICMP ID field is 16 bits
Signal(SIGALRM, sig_alrm);
ai = host_serv(host, NULL, 0, 0); // 处理主机名或IP地址数串,返回addrinfo结构中含有协议族AF_INET等信息
h = sock_ntop_host(ai->ai_addr, ai->ai_addrlen); // 从套接字地址结构中得到主机IP信息
printf("PING %s (%s): %d data bytes\n", ai->ai_canonname ? ai->ai_canonname : h, h, datalen);
if (ai->ai_family == AF_INET)
pr = &proto_v4;
else
error_exit("unknown address family %d", ai->ai_family);
pr->sasend = ai->ai_addr;
pr->sarecv = calloc(1, ai->ai_addrlen);
pr->salen = ai->ai_addrlen;
readloop();
free(pr->sarecv);
freeaddrinfo(ai);
exit(0);
}
测试:
:~$ sudo ./ping www.baidu.com
PING www.baidu.com (123.125.114.144): 56 data bytes
64 bytes from 123.125.114.144: seq=0, ttl=52, rtt=37.399 ms
64 bytes from 123.125.114.144: seq=1, ttl=52, rtt=36.838 ms
64 bytes from 123.125.114.144: seq=2, ttl=52, rtt=62.450 ms
:~$ sudo ./ping -v www.baidu.com
PING www.baidu.com (123.125.114.144): 56 data bytes
64 bytes from 123.125.114.144: seq=0, ttl=52, rtt=52.391 ms
64 bytes from 123.125.114.144: type = 0, code = 0
64 bytes from 123.125.114.144: seq=1, ttl=52, rtt=37.155 ms
64 bytes from 123.125.114.144: type = 0, code = 0
64 bytes from 123.125.114.144: seq=2, ttl=52, rtt=36.360 ms
64 bytes from 123.125.114.144: type = 0, code = 0
:~$ sudo ./ping -v 123.3.4.5
PING 123.3.4.5 (123.3.4.5): 56 data bytes
36 bytes from 203.134.25.118: type = 3, code = 1
36 bytes from 203.134.25.118: type = 3, code = 1