ICMP is used as a means of messaging between hosts. Most ICMP messages relate to errors that occur in communication between hosts; the remaining ICMP messages are used to query hosts. The ICMP protocol uses IP addressing because it is a protocol encapsulated within an IP datagram. Figure 13-1 illustrates the fields of an ICMP message. The ICMP message is wrapped in an IP header.
The first field is the ICMP message type, which can be classified as either a query or an error. The code field further defines the type of query or message. The checksum field is the 16-bit one's complement sum of the ICMP header. Finally, the ICMP contents depend on the ICMP type and code. Table 13-1 lists the various types and codes.
When an ICMP error message is generated, the message always contains the IP header and the first 8 bytes of the IP datagram that caused the ICMP error to occur. This allows the host receiving the ICMP error to associate the message with one particular protocol and process associated with that error. In our case, Ping relies on the echo request and echo reply ICMP queries rather than on error messages. Hosts generate ICMP messages in response to problems with TCP or UDP; ICMP doesn't have many applications beyond that. In the next section, we will discuss how to use the ICMP protocol with a raw socket to generate a Ping request by using the echo request and echo reply messages. If you require additional information about ICMP errors or the other types of ICMP queries, consult more in-depth sources, such as Stevens's TCP/IP Illustrated Vol. 1.
Figure 13-1. ICMP header
Table 13-1. ICMP message types
Type | Query/Error (Error Type) | Code | Description |
---|---|---|---|
0 | Query | 0 | Echo reply |
3 | Error: Destination unreachable | 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Network unreachable Host unreachable Protocol unreachable Port unreachable Fragmentation needed, but the Don't Fragment bit has been set Source route failed Destination network unknown Destination host unknown Source host isolated (obsolete) Destination network administratively prohibited Destination host administratively prohibited Network unreachable for TOS Host unreachable for TOS Communication administratively prohibited by filtering Host precedence violation Precedence cutoff in effect |
4 | Error | 0 | Source quench |
5 | Error: Redirect | 0 1 2 3 | Redirect for network Redirect for host Redirect for TOS and network Redirect for TOS and host |
8 | Query | 0 | Echo request |
9 | Query | 0 | Router advertisement |
10 | Query | 0 | Router solicitation |
11 | Error: Time exceeded | 0 1 | TTL equals 0 during transit TTL equals 0 during reassembly |
12 | Error: Parameter problem | 0 | IP header bad Required option missing |
13 | Query | 0 | Time stamp request |
14 | Query | 0 | Time stamp reply |
15 | Query | 0 | Information request |
16 | Query | 0 | Information reply |
17 | Query | 0 | Address mask request |
18 | Query | 0 | Address mask reply |
A ping is often used to determine whether a particular host is alive and reachable through the network. By generating an ICMP echo request and directing it to the host you are interested in, you can determine whether you can successfully reach that machine. Of course, this does not guarantee that a socket client will be able to connect to a process on that host (a process on the remote server might not be listening, for example); it just means that the network layer of the remote host is responding to network events. Essentially, the Ping example performs the following steps:
When you send the ICMP echo request, the remote machine intercepts the ICMP query and generates an echo reply message back to you. If for some reason the host is not reachable, the appropriate ICMP error message—such as destination host unreachable—will be returned by a router somewhere along the path to the intended recipient. If the physical network connection to the host is good but the remote host is either down or not responding to network events, you need to perform your own timeout to determine this. The Ping.c example in Figure 13-2 illustrates how to create a socket capable of sending and receiving ICMP packets, as well as how to use the IP_OPTIONS socket option to implement the record route option.
Figure 13-2. Ping.c
// Module Name: Ping.c // // Command Line Options/Parameters: // Ping [host] [packet-size] // // host String name of host to ping // packet-size Integer size of packet to send // (smaller than 1024 bytes) // //#pragma pack(1) #define WIN32_LEAN_AND_MEAN #include <winsock2.h> #include <ws2tcpip.h> #include <stdio.h> #include <stdlib.h> #define IP_RECORD_ROUTE 0x7 // // IP header structure // typedef struct _iphdr { unsigned int h_len:4; // Length of the header unsigned int version:4; // Version of IP unsigned char tos; // Type of service unsigned short total_len; // Total length of the packet unsigned short ident; // Unique identifier unsigned short frag_and_flags; // Flags unsigned char ttl; // Time to live unsigned char proto; // Protocol (TCP, UDP, etc.) unsigned short checksum; // IP checksum unsigned int sourceIP; unsigned int destIP; } IpHeader; #define ICMP_ECHO 8 #define ICMP_ECHOREPLY 0 #define ICMP_MIN 8 // Minimum 8-byte ICMP packet (header) // // ICMP header structure // typedef struct _icmphdr { BYTE i_type; BYTE i_code; // Type sub code USHORT i_cksum; USHORT i_id; USHORT i_seq; // This is not the standard header, but we reserve space for time ULONG timestamp; } IcmpHeader; // // IP option header--use with socket option IP_OPTIONS // typedef struct _ipoptionhdr { unsigned char code; // Option type unsigned char len; // Length of option hdr unsigned char ptr; // Offset into options unsigned long addr[9]; // List of IP addrs } IpOptionHeader; #define DEF_PACKET_SIZE 32 // Default packet size #define MAX_PACKET 1024 // Max ICMP packet size #define MAX_IP_HDR_SIZE 60 // Max IP header size w/options BOOL bRecordRoute; int datasize; char *lpdest; // // Function: usage // // Description: // Print usage information // void usage(char *progname) { printf("usage: ping -r <host> [data size]\n"); printf(" -r record route\n"); printf(" host remote machine to Ping\n"); printf(" datasize can be up to 1 KB\n"); ExitProcess(-1); } // // Function: FillICMPData // // Description: // Helper function to fill in various fields for our ICMP request // void FillICMPData(char *icmp_data, int datasize) { IcmpHeader *icmp_hdr = NULL; char *datapart = NULL; icmp_hdr = (IcmpHeader*)icmp_data; icmp_hdr->i_type = ICMP_ECHO; // Request an ICMP echo icmp_hdr->i_code = 0; icmp_hdr->i_id = (USHORT)GetCurrentProcessId(); icmp_hdr->i_cksum = 0; icmp_hdr->i_seq = 0; datapart = icmp_data + sizeof(IcmpHeader); // // Place some junk in the buffer // memset(datapart,'E', datasize - sizeof(IcmpHeader)); } // // Function: checksum // // Description: // This function calculates the 16-bit one's complement sum // of the supplied buffer (ICMP) header // USHORT checksum(USHORT *buffer, int size) { unsigned long cksum=0; while (size > 1) { cksum += *buffer++; size -= sizeof(USHORT); } if (size) { cksum += *(UCHAR*)buffer; } cksum = (cksum >> 16) + (cksum & 0xffff); cksum += (cksum >>16); return (USHORT)(~cksum); } // // Function: DecodeIPOptions // // Description: // If the IP option header is present, find the IP options // within the IP header and print the record route option // values // void DecodeIPOptions(char *buf, int bytes) { IpOptionHeader *ipopt = NULL; IN_ADDR inaddr; int i; HOSTENT *host = NULL; ipopt = (IpOptionHeader *)(buf + 20); printf("RR: "); for(i = 0; i < (ipopt->ptr / 4) - 1; i++) { inaddr.S_un.S_addr = ipopt->addr[i]; if (i != 0) printf(" "); host = gethostbyaddr((char *)&inaddr.S_un.S_addr, sizeof(inaddr.S_un.S_addr), AF_INET); if (host) printf("(%-15s) %s\n", inet_ntoa(inaddr), host->h_name); else printf("(%-15s)\n", inet_ntoa(inaddr)); } return; } // // Function: DecodeICMPHeader // // Description: // The response is an IP packet. We must decode the IP header to // locate the ICMP data. // void DecodeICMPHeader(char *buf, int bytes, struct sockaddr_in *from) { IpHeader *iphdr = NULL; IcmpHeader *icmphdr = NULL; unsigned short iphdrlen; DWORD tick; static int icmpcount = 0; iphdr = (IpHeader *)buf; // Number of 32-bit words * 4 = bytes iphdrlen = iphdr->h_len * 4; tick = GetTickCount(); if ((iphdrlen == MAX_IP_HDR_SIZE) && (!icmpcount)) DecodeIPOptions(buf, bytes); if (bytes < iphdrlen + ICMP_MIN) { printf("Too few bytes from %s\n", inet_ntoa(from->sin_addr)); } icmphdr = (IcmpHeader*)(buf + iphdrlen); if (icmphdr->i_type != ICMP_ECHOREPLY) { printf("nonecho type %d recvd\n", icmphdr->i_type); return; } // Make sure this is an ICMP reply to something we sent! // if (icmphdr->i_id != (USHORT)GetCurrentProcessId()) { printf("someone else's packet!\n"); return ; } printf("%d bytes from %s:", bytes, inet_ntoa(from->sin_addr)); printf(" icmp_seq = %d. ", icmphdr->i_seq); printf(" time: %d ms", tick - icmphdr->timestamp); printf("\n"); icmpcount++; return; } void ValidateArgs(int argc, char **argv) { int i; bRecordRoute = FALSE; lpdest = NULL; datasize = DEF_PACKET_SIZE; for(i = 1; i < argc; i++) { if ((argv[i][0] == '-') || (argv[i][0] == '/')) { switch (tolower(argv[i][1])) { case 'r': // Record route option bRecordRoute = TRUE; break; default: usage(argv[0]); break; } } else if (isdigit(argv[i][0])) datasize = atoi(argv[i]); else lpdest = argv[i]; } } // // Function: main // // Description: // Set up the ICMP raw socket, and create the ICMP header. Add // the appropriate IP option header, and start sending ICMP // echo requests to the endpoint. For each send and receive, // we set a timeout value so that we don't wait forever for a // response in case the endpoint is not responding. When we // receive a packet, decode it. // int main(int argc, char **argv) { WSADATA wsaData; SOCKET sockRaw = INVALID_SOCKET; struct sockaddr_in dest, from; int bread, fromlen = sizeof(from), timeout = 1000, ret; char *icmp_data = NULL, *recvbuf = NULL; unsigned int addr = 0; USHORT seq_no = 0; struct hostent *hp = NULL; IpOptionHeader ipopt; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup() failed: %d\n", GetLastError()); return -1; } ValidateArgs(argc, argv); // // WSA_FLAG_OVERLAPPED flag is required for SO_RCVTIMEO, // SO_SNDTIMEO option. If NULL is used as last param for // WSASocket, all I/O on the socket is synchronous, the // internal user mode wait code never gets a chance to // execute, and therefore kernel-mode I/O blocks forever. // A socket created via the socket function has the over- // lapped I/O attribute set internally. But here we need // to use WSASocket to specify a raw socket. // // If you want to use timeout with a synchronous // nonoverlapped socket created by WSASocket with last // param set to NULL, you can set the timeout by using // the select function, or you can use WSAEventSelect and // set the timeout in the WSAWaitForMultipleEvents // function. // sockRaw = WSASocket (AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0, WSA_FLAG_OVERLAPPED); if (sockRaw == INVALID_SOCKET) { printf("WSASocket() failed: %d\n", WSAGetLastError()); return -1; } if (bRecordRoute) { // Setup the IP option header to go out on every ICMP packet // ZeroMemory(&ipopt, sizeof(ipopt)); ipopt.code = IP_RECORD_ROUTE; // Record route option ipopt.ptr = 4; // Point to the first addr offset ipopt.len = 39; // Length of option header ret = setsockopt(sockRaw, IPPROTO_IP, IP_OPTIONS, (char *)&ipopt, sizeof(ipopt)); if (ret == SOCKET_ERROR) { printf("setsockopt(IP_OPTIONS) failed: %d\n", WSAGetLastError()); } } // Set the send/recv timeout values // bread = setsockopt(sockRaw, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout)); if(bread == SOCKET_ERROR) { printf("setsockopt(SO_RCVTIMEO) failed: %d\n", WSAGetLastError()); return -1; } timeout = 1000; bread = setsockopt(sockRaw, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(timeout)); if (bread == SOCKET_ERROR) { printf("setsockopt(SO_SNDTIMEO) failed: %d\n", WSAGetLastError()); return -1; } memset(&dest, 0, sizeof(dest)); // // Resolve the endpoint's name if necessary // dest.sin_family = AF_INET; if ((dest.sin_addr.s_addr = inet_addr(lpdest)) == INADDR_NONE) { if ((hp = gethostbyname(lpdest)) != NULL) { memcpy(&(dest.sin_addr), hp->h_addr, hp->h_length); dest.sin_family = hp->h_addrtype; printf("dest.sin_addr = %s\n", inet_ntoa(dest.sin_addr)); } else { printf("gethostbyname() failed: %d\n", WSAGetLastError()); return -1; } } // // Create the ICMP packet // datasize += sizeof(IcmpHeader); icmp_data = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, MAX_PACKET); recvbuf = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, MAX_PACKET); if (!icmp_data) { printf("HeapAlloc() failed: %d\n", GetLastError()); return -1; } memset(icmp_data,0,MAX_PACKET); FillICMPData(icmp_data,datasize); // // Start sending/receiving ICMP packets // while(1) { static int nCount = 0; int bwrote; if (nCount++ == 4) break; ((IcmpHeader*)icmp_data)->i_cksum = 0; ((IcmpHeader*)icmp_data)->timestamp = GetTickCount(); ((IcmpHeader*)icmp_data)->i_seq = seq_no++; ((IcmpHeader*)icmp_data)->i_cksum = checksum((USHORT*)icmp_data, datasize); bwrote = sendto(sockRaw, icmp_data, datasize, 0, (struct sockaddr*)&dest, sizeof(dest)); if (bwrote == SOCKET_ERROR) { if (WSAGetLastError() == WSAETIMEDOUT) { printf("timed out\n"); continue; } printf("sendto() failed: %d\n", WSAGetLastError()); return -1; } if (bwrote < datasize) { printf("Wrote %d bytes\n", bwrote); } bread = recvfrom(sockRaw, recvbuf, MAX_PACKET, 0, (struct sockaddr*)&from, &fromlen); if (bread == SOCKET_ERROR) { if (WSAGetLastError() == WSAETIMEDOUT) { printf("timed out\n"); continue; } printf("recvfrom() failed: %d\n", WSAGetLastError()); return -1; } DecodeICMPHeader(recvbuf, bread, &from); Sleep(1000); } // Cleanup // if (sockRaw != INVALID_SOCKET) closesocket(sockRaw); HeapFree(GetProcessHeap(), 0, recvbuf); HeapFree(GetProcessHeap(), 0, icmp_data); WSACleanup(); return 0; } |
One noticeable feature of the Ping example is its use of the IP_OPTIONS socket option. We use the record route IP option so that when our ICMP packet hits a router, its IP address is added into the IP option header at the location indicated by the offset field in the IP option header. This offset is also incremented by 4 each time a router adds its address. The increment value is based on the fact that an IP version 4 address is 4 bytes in length. This book does not address any IP version 6 concerns, as no current Windows platforms support this yet. Once you receive the echo reply, decode the option header and print the IP addresses and host names of the routers visited. See Chapter 9 for more information on the other types of IP options available.
Another valuable IP networking tool is the Traceroute utility. This allows you to determine the IP addresses of the routers that are traversed in order to reach a certain host on the network. With Ping, using the record route option in the IP option header also allows you to determine the IP addresses of intermediary routers, but Ping is limited to only 9 hops—the maximum space allocated for addresses in the option header. A hop occurs whenever an IP datagram must pass through a router in order to traverse multiple physical networks. For routes with more than 9 hops, use Traceroute.
The idea behind Traceroute is to send a UDP packet to the destination and incrementally change the IP time-to-live (TTL) value. Initially, the TTL value is 1, which means the UDP packet will reach the first router, where the TTL will expire. The expiration will cause the router to generate an ICMP time-exceeded packet. Then the initial TTL value increases by 1, so this time the UDP packet gets one router farther and an ICMP time-exceeded packet is sent from that router. Collecting each of the ICMP messages gives you a clear path of the IP addresses traversed in order to reach the endpoint. Once the TTL is incremented enough so that packets actually reach the endpoint in question, an ICMP port-unreachable message is most likely returned, as no process on the recipient is waiting for this message.
Traceroute is a useful utility because it gives you a lot of information about the route to a particular host, which is often a concern when you use multicasting or when you experience routing problems. Fewer applications need to perform a Traceroute programmatically than a ping, but certain tasks might require Traceroute-like capabilities.
Two methods can be used to implement the Traceroute program. First you can use UDP packets and send datagrams, incrementally changing the TTL. Each time the TTL expires, an ICMP message will be returned to you. This method requires one socket of the UDP protocol to send the messages and another socket of the ICMP protocol to read the returned messages. The UDP socket is a normal UDP socket, as you saw in Chapter 7. The ICMP socket is of type SOCK_RAW and protocol IPPROTO_ICMP. The TTL of the UDP socket needs to be manipulated via the IP_TTL socket option. Alternatively, you can create a UDP socket and use the IP_HDRINCL option (discussed later in this chapter) to set the TTL manually within the IP header, but this is quite a lot of work.
The other method is simply to send ICMP packets to the destination, also incrementally changing the TTL. This also results in ICMP error messages being returned when the TTL expires. This method resembles the Ping example in that it requires only one socket (of the ICMP protocol). Under the sample code folder on the companion CD, you will find a Traceroute example using ICMP packets named Traceroute.c. We won't include the whole example in this chapter, as it is similar in design to the Ping example.