背景:我们的设备上有个链路探测的功能,会定时请求公网的某个IP地址,以探测网络是不是连通的。具体的做法是会使用icmp或dns探测远端服务器,看请求能否正常响应,如果有响应,则认为链路正常,否则则认为不正常,需要采取对应的措施。但是问题的现象是每隔一段时间后,探测包就收不到回复了,导致我们认为线路异常。而实际上网络还是通的,使用系统自带的ping和nslookup工具也是没问题的。
最后抓包分析,怀疑是IP数据包中的identify字段为0导致的,因为不回复的都是为0的id:
因此,我们就打算先把这id改掉试试。本身的实现上,我们使用的是原始套接字来构造icmp和dns请求,没办法控制ip.id。要想修改ip.id,必须让内核放弃自动填充ip头的操作。要想做到这一点,需要用到socket选项中的IP_HDRINCL
选项,它的作用就是告诉内核不要填充头部:
1 2 |
val = 1; setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &val, sizeof(val)); |
注意事项:
- IP报文中,如果校验和设置为0,内核会帮我们自动填充。但在ICMP报文中,内核不会自动填充。
- 如果不填写源地址,内核也会自动帮我们填充。
以下是一个自己修改的PING包示例代码:
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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
#include <arpa/inet.h> #include <netdb.h> #include <netinet/in.h> #include <netinet/in_systm.h> #include <netinet/ip.h> #include <netinet/ip_icmp.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> /* * 计算校验和的函数 * @ptr 待计算校验和的数据 * @nbytes 数据长度 * @return 返回校验和 */ unsigned short check_sum(unsigned short *ptr, int nbytes) { register long sum; unsigned short oddbyte; register short answer; sum = 0; while (nbytes > 1) { sum += *ptr++; nbytes -= 2; } if (nbytes == 1) { oddbyte = 0; *((unsigned char *)&oddbyte) = *(unsigned char *)ptr; sum += oddbyte; } sum = (sum >> 16) + (sum & 0xffff); sum = sum + (sum >> 16); answer = (short)~sum; return (answer); } int main(int argc, char *argv[]) { int sock, val; char buf[1024]; // IP包头 struct iphdr *iph = (struct ip *)buf; // ICMP包头 struct icmphdr *icmph = (struct icmphdr *)(iph + 1); socklen_t addr_len; struct sockaddr_in dst; struct sockaddr_in src_addr, dst_addr; if (argc < 3) { printf("\nUsage: %s <saddress> <dstaddress>\n", argv[0]); return 0; } bzero(buf, sizeof(buf)); // 创建原始套接字 if ((sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0) { perror("socket() error"); /* If something wrong, just exit */ return -1; } val = 1; // 告诉内核我们自己填充IP头部 if (setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &val, sizeof(val)) < 0) { perror("setsockopt() for IP_HDRINCL error"); return -1; } // 填充IP头部 iph->ihl = 5; // ip头部的长度/4 iph->version = 4; // 版本信息 iph->tos = 0; iph->tot_len = sizeof(struct iphdr) + sizeof(struct icmphdr); // 总长度等于ip头部+icmp总长度 iph->id = htons(4321); iph->frag_off = 0; iph->ttl = 128; iph->protocol = IPPROTO_ICMP; iph->check = 0; // 让内核自己去计算校验和 iph->saddr = inet_addr(argv[1]); iph->daddr = inet_addr(argv[2]); // check sum // iph->check = check_sum((unsigned short *)buf, iph->tot_len); dst.sin_addr.s_addr = iph->daddr; dst.sin_family = AF_INET; // 添加ICMP包头 icmph->type = ICMP_ECHO; icmph->code = 0; icmph->checksum = 0; icmph->un.echo.id = htons(9987); icmph->un.echo.sequence = htons(9988); // 首部检验和 icmph->checksum = check_sum((void *)icmph, sizeof(struct icmphdr)); addr_len = sizeof(dst); // 发数据 val = sendto(sock, buf, iph->tot_len, 0, (struct sockaddr *)&dst, addr_len); if (val < 0) { perror("sendto() error\n"); } else { printf("sendto() is OK\n"); } // 收数据 val = recvfrom(sock, buf + 30, sizeof(buf) - 30, 0, NULL, NULL); if (val < 0) { perror("recv from error"); } else { printf("recv %d bytes data\n", val); iph = (void *)(buf + 30); icmph = (struct icmphdr *)(iph + 1); printf("icmp type: %d, icmp code = %d, seq = %u, id = %u\n", icmph->type, icmph->code, ntohs(icmph->un.echo.sequence), ntohs(icmph->un.echo.id)); } // 关闭socket close(sock); return 0; }; |
评论