透析ICMP协议(四): 牛刀初试之二 应用篇ping(RAW Socket) =============================== 这篇文章出自bugfree/CSDN 平台: VC6 Windows XP 今晚一杯茶水之后, 让我们继续我们的ICMP讨论, 今晚介绍的是用RAW Socket 实现的ping程序. 原理简介: -------- 用RAW Socket实现的ping可能比上一节的应用ICMP.DLL的程序庞大些, 但是这才是我们需要关注的东西, 我的观点真正想做网络开发的程序员应该静下心来读读这篇文章, 相信你会从中获益颇多. 中间我也会讲解一些东西为后一章的路由追踪做一些铺垫. 另一个重要的要讲的东西, 微软宣布随时不支持上节讲的ping用到的开发接口, 但是本节的讲的是更一般的东西. 所以它不会过时, 甚至做很小的改动就可以移植到别的系统上去. 系统移植不是我们的讲的重点. 但是微软的长期支持足以引起我们充分的重视. 如何少作变动来使的这个程序实现追踪路由的功能, 这里只是抛砖引玉. 将ICMP包中IP包的包头该为特定的值就能得到那个路由器的IP(要求到达目的地的跳数大于你设的特定值). 这个程序需要windows2k/WindowsXP/WindowsNT平台和系统管理员的权限. 具体实现: -------- 这段源代码大部分来自: http://tangentsoft.net/wskfaq/examples/rawping.html [bugfree]只做了少量修改,给出了大量的注释, 最后结合经验给出了自己的建议. ---------- /* * 程序名: rawping_driver.cpp * 说明: * 驱动程序,也是主函数 */ #include <winsock2.h> #include <iostream.h> #include "rawping.h" #define DEFAULT_PACKET_SIZE 32 // 默认ICMP包字节数 #define DEFAULT_TTL 30 // 默认TTL值 #define MAX_PING_DATA_SIZE 1024 // 最大数据块 #define MAX_PING_PACKET_SIZE (MAX_PING_DATA_SIZE + sizeof(IPHeader)) //最大ICMP包长度 /* 为 send_buf 和 recv_buf 分配内存 * send_buf大小为 packet_size * recv_buf大小为 MAX_PING_PACKET_SIZE, 保证大于send_buf */ int allocate_buffers(ICMPHeader*& send_buf, IPHeader*& recv_buf, int packet_size); /////////////////////////////////////////////////////////////////////// // Program entry point int main(int argc, char* argv[]) { int seq_no = 0; //用在发送和接受的ICMP包头中 ICMPHeader* send_buf = 0; IPHeader* recv_buf = 0; // 判断命令行是否合法 if (argc < 2) { cerr << "usage: " << argv[0] << " <host> [data_size] [ttl]" << endl; cerr << "\tdata_size can be up to " << MAX_PING_DATA_SIZE << " bytes. Default is " << DEFAULT_PACKET_SIZE << "." << endl; cerr << "\tttl should be 255 or lower. Default is " << DEFAULT_TTL << "." << endl; return 1; } // 处理命令行参数 int packet_size = DEFAULT_PACKET_SIZE; int ttl = DEFAULT_TTL; if (argc > 2) { int temp = atoi(argv[2]); if (temp != 0) { packet_size = temp; } if (argc > 3) { temp = atoi(argv[3]); if ((temp >= 0) && (temp <= 255)) { ttl = temp; } } } packet_size = max(sizeof(ICMPHeader), min(MAX_PING_DATA_SIZE, (unsigned int)packet_size)); // 启动 Winsock WSAData wsaData; if (WSAStartup(MAKEWORD(2, 1), &wsaData) != 0) { cerr << "Failed to find Winsock 2.1 or better." << endl; return 1; } SOCKET sd; // RAW Socket句柄 sockaddr_in dest, source; // 三个任务(创建sd, 设置ttl, 初试dest的值) if (setup_for_ping(argv[1], ttl, sd, dest) < 0) { goto cleanup; //释放资源并退出 } // 为send_buf和recv_buf分配内存 if (allocate_buffers(send_buf, recv_buf, packet_size) < 0) { goto cleanup; } // 初试化IMCP数据包(type=8,code=0) init_ping_packet(send_buf, packet_size, seq_no); // 发送ICMP数据包 if (send_ping(sd, dest, send_buf, packet_size) >= 0) { while (1) { // 接受回应包 if (recv_ping(sd, source, recv_buf, MAX_PING_PACKET_SIZE) < 0) { // Pull the sequence number out of the ICMP header. If // it's bad, we just complain, but otherwise we take // off, because the read failed for some reason. unsigned short header_len = recv_buf->h_len * 4; ICMPHeader* icmphdr = (ICMPHeader*) ((char*)recv_buf + header_len); if (icmphdr->seq != seq_no) { cerr << "bad sequence number!" << endl; continue; } else { break; } } if (decode_reply(recv_buf, packet_size, &source) != -2) { // Success or fatal error (as opposed to a minor error) // so take off. break; } } } cleanup: delete[]send_buf; //释放分配的内存 delete[]recv_buf; WSACleanup(); // 清理winsock return 0; } // 为send_buf 和 recv_buf的内存分配. 太简单, 我略过 int allocate_buffers(ICMPHeader*& send_buf, IPHeader*& recv_buf, int packet_size) { // First the send buffer send_buf = (ICMPHeader*)new char[packet_size]; if (send_buf == 0) { cerr << "Failed to allocate output buffer." << endl; return -1; } // And then the receive buffer recv_buf = (IPHeader*)new char[MAX_PING_PACKET_SIZE]; if (recv_buf == 0) { cerr << "Failed to allocate output buffer." << endl; return -1; } return 0; } /* * 程序名: rawping.h * 说明: * 主要函数库头文件 */ #define WIN32_LEAN_AND_MEAN #include <winsock2.h> // ICMP 包类型, 具体参见本文的第一节 #define ICMP_ECHO_REPLY 0 #define ICMP_DEST_UNREACH 3 #define ICMP_TTL_EXPIRE 11 #define ICMP_ECHO_REQUEST 8 // 最小的ICMP包大小 #define ICMP_MIN 8 // IP 包头 struct IPHeader { BYTE h_len:4; // Length of the header in dwords BYTE version:4; // Version of IP BYTE tos; // Type of service USHORT total_len; // Length of the packet in dwords USHORT ident; // unique identifier USHORT flags; // Flags BYTE ttl; // Time to live, 这个字段我在下一节中用来实现Tracert功能 BYTE proto; // Protocol number (TCP, UDP etc) USHORT checksum; // IP checksum ULONG source_ip; ULONG dest_ip; }; // ICMP 包头(实际的包不包括timestamp字段, // 作者用来计算包的回应时间,其实完全没有必要这样做) struct ICMPHeader { BYTE type; // ICMP packet type BYTE code; // Type sub code USHORT checksum; USHORT id; USHORT seq; ULONG timestamp; // not part of ICMP, but we need it }; extern USHORT ip_checksum(USHORT* buffer, int size); extern int setup_for_ping(char* host, int ttl, SOCKET& sd, sockaddr_in& dest); extern int send_ping(SOCKET sd, const sockaddr_in& dest, ICMPHeader* send_buf, int packet_size); extern int recv_ping(SOCKET sd, sockaddr_in& source, IPHeader* recv_buf, int packet_size); extern int decode_reply(IPHeader* reply, int bytes, sockaddr_in* from); extern void init_ping_packet(ICMPHeader* icmp_hdr, int packet_size, int seq_no); /* * 程序名: rawping.cpp * 说明: * 主要函数库实现部分 */ include <winsock2.h> #include <ws2tcpip.h> #include <iostream.h> #include "rawping.h" // 计算ICMP包的校验和的简单算法, 很多地方都有说明, 这里没有必要详细将 // 只是一点要提, 做校验之前, 务必将ICMP包头的checksum字段置为0 USHORT ip_checksum(USHORT* buffer, int size) { unsigned long cksum = 0; // Sum all the words together, adding the final byte if size is odd while (size > 1) { cksum += *buffer++; size -= sizeof(USHORT); } if (size) { cksum += *(UCHAR*)buffer; } // Do a little shuffling cksum = (cksum >> 16) + (cksum & 0xffff); cksum += (cksum >> 16); // Return the bitwise complement of the resulting mishmash return (USHORT)(~cksum); } //初试化RAW Socket, 设置ttl, 初试化dest // 返回值 <0 表失败 int setup_for_ping(char* host, int ttl, SOCKET& sd, sockaddr_in& dest) { // Create the socket sd = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, 0, 0, 0); if (sd == INVALID_SOCKET) { cerr << "Failed to create raw socket: " << WSAGetLastError() << endl; return -1; } if (setsockopt(sd, IPPROTO_IP, IP_TTL, (const char*)&ttl, sizeof(ttl)) == SOCKET_ERROR) { cerr << "TTL setsockopt failed: " << WSAGetLastError() << endl; return -1; } // Initialize the destination host info block memset(&dest, 0, sizeof(dest)); // Turn first passed parameter into an IP address to ping unsigned int addr = inet_addr(host); if (addr != INADDR_NONE) { // It was a dotted quad number, so save result dest.sin_addr.s_addr = addr; dest.sin_family = AF_INET; } else { // Not in dotted quad form, so try and look it up hostent* hp = gethostbyname(host); if (hp != 0) { // Found an address for that host, so save it memcpy(&(dest.sin_addr), hp->h_addr, hp->h_length); dest.sin_family = hp->h_addrtype; } else { // Not a recognized hostname either! cerr << "Failed to resolve " << host << endl; return -1; } } return 0; } //初试化ICMP的包头, 给data部分填充数据, 最后计算整个包的校验和 void init_ping_packet(ICMPHeader* icmp_hdr, int packet_size, int seq_no) { // Set up the packet's fields icmp_hdr->type = ICMP_ECHO_REQUEST; icmp_hdr->code = 0; icmp_hdr->checksum = 0; icmp_hdr->id = (USHORT)GetCurrentProcessId(); icmp_hdr->seq = seq_no; icmp_hdr->timestamp = GetTickCount(); // "You're dead meat now, packet!" const unsigned long int deadmeat = 0xDEADBEEF; char* datapart = (char*)icmp_hdr + sizeof(ICMPHeader); int bytes_left = packet_size - sizeof(ICMPHeader); while (bytes_left > 0) { memcpy(datapart, &deadmeat, min(int(sizeof(deadmeat)), bytes_left)); bytes_left -= sizeof(deadmeat); datapart += sizeof(deadmeat); } // Calculate a checksum on the result icmp_hdr->checksum = ip_checksum((USHORT*)icmp_hdr, packet_size); } // 发送生成的ICMP包 // 返回值 <0 表失败 int send_ping(SOCKET sd, const sockaddr_in& dest, ICMPHeader* send_buf, int packet_size) { // Send the ping packet in send_buf as-is cout << "Sending " << packet_size << " bytes to " << inet_ntoa(dest.sin_addr) << "..." << flush; int bwrote = sendto(sd, (char*)send_buf, packet_size, 0, (sockaddr*)&dest, sizeof(dest)); if (bwrote == SOCKET_ERROR) { cerr << "send failed: " << WSAGetLastError() << endl; return -1; } else if (bwrote < packet_size) { cout << "sent " << bwrote << " bytes..." << flush; } return 0; } // 接受ICMP包 // 返回值 <0 表失败 int recv_ping(SOCKET sd, sockaddr_in& source, IPHeader* recv_buf, int packet_size) { // Wait for the ping reply int fromlen = sizeof(source); int bread = recvfrom(sd, (char*)recv_buf, packet_size + sizeof(IPHeader), 0, (sockaddr*)&source, &fromlen); if (bread == SOCKET_ERROR) { cerr << "read failed: "; if (WSAGetLastError() == WSAEMSGSIZE) { cerr << "buffer too small" << endl; } else { cerr << "error #" << WSAGetLastError() << endl; } return -1; } return 0; } // 对收到的ICMP解码 // 返回值 -2表忽略, -1 表失败, 0 成功 int decode_reply(IPHeader* reply, int bytes, sockaddr_in* from) { // 跳过IP包头, 找到ICMP的包头 unsigned short header_len = reply->h_len * 4; ICMPHeader* icmphdr = (ICMPHeader*)((char*)reply + header_len); // 包的长度合法, header_len + ICMP_MIN为最小ICMP包的长度 if (bytes < header_len + ICMP_MIN) { cerr << "too few bytes from " << inet_ntoa(from->sin_addr) << endl; return -1; } // 下面的包类型详细参见我的第一部分 "透析ICMP协议(一): 协议原理" else if (icmphdr->type != ICMP_ECHO_REPLY) { //非正常回复 if (icmphdr->type != ICMP_TTL_EXPIRE) { //ttl减为零 if (icmphdr->type == ICMP_DEST_UNREACH) { //主机不可达 cerr << "Destination unreachable" << endl; } else { //非法的ICMP包类型 cerr << "Unknown ICMP packet type " << int(icmphdr->type) << " received" << endl; } return -1; } } else if (icmphdr->id != (USHORT)GetCurrentProcessId()) { //不是本进程发的包, 可能是同机的其它ping进程发的 return -2; } // 指出包传递了多远 // [bugfree]我认为作者这里有问题, 因为有些系统的ttl初值为128如winXP, // 有些为256如我的DNS服务器211.97.168.129, 作者假设为256有点武断, // 可以一起探讨这个问题, 回email:zhangliangsd@hotmail.com int nHops = int(256 - reply->ttl); if (nHops == 192) { // TTL came back 64, so ping was probably to a host on the // LAN -- call it a single hop. nHops = 1; } else if (nHops == 128) { // Probably localhost nHops = 0; } // 所有工作结束,打印信息 cout << endl << bytes << " bytes from " << inet_ntoa(from->sin_addr) << ", icmp_seq " << icmphdr->seq << ", "; if (icmphdr->type == ICMP_TTL_EXPIRE) { cout << "TTL expired." << endl; } else { cout << nHops << " hop" << (nHops == 1 ? "" : "s"); cout << ", time: " << (GetTickCount() - icmphdr->timestamp) << " ms." << endl; } return 0; } 总结和建议: ----------- bugfree建议其中的这些方面需要改进: 1. 头文件iostream.h 改为 iostream, 后者是标准C++的头文件 同时添加对std::cout 和 std::endl;的引用 对于cerr 建议都改为std::cout(因为后者头文件不支持) 2. 程序的发送和接受采用了同步的方式, 这使得如果出现网络问题recv_ping将陷入持续等待. 这是我们不想看到的. 这三种技术可以达到目的: - 使用多线程, 将ping封装进线程, 在主程序中对它的超时进行处理 - 使用select()函数来实现 - 使用windows的 WSAAsyncSelect() 这里对这些方法不作具体讨论, 留给读者自已完成. |