背景:我们的设备上有个链路探测的功能,会定时请求公网的某个 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; }; |
评论