透析ICMP協議(四): 牛刀初試之二 應用篇ping(RAW Socket) (轉)

worldblog發表於2008-01-08
透析ICMP協議(四): 牛刀初試之二 應用篇ping(RAW Socket) (轉)[@more@]

透析ICMP(四): 牛刀初試之二
 應用篇(RAW Socket)
===============================
這篇文章出自free/CSDN
平臺: VC6 XP

今晚一杯茶水之後, 讓我們繼續我們的ICMP討論, 今晚介紹的是用RAW Socket 實現的ping.

原理簡介:
-------- 
 用RAW Socket實現的ping可能比上一節的應用ICMP.DLL的程式龐大些, 但是這才是我們需要關注的東西, 我的觀點真正想做開發的程式設計師應該靜下心來讀讀這篇文章, 相信你會從中獲益頗多. 中間我也會講解一些東西為後一章的追蹤做一些鋪墊.
 另一個重要的要講的東西, 宣佈隨時不支援上節講的ping用到的開發介面, 但是本節的講的是更一般的東西. 所以它不會過時, 甚至做很小的改動就可以移植到別的上去. 系統移植不是我們的講的重點. 但是微軟的長期支援足以引起我們充分的重視.
 如何少作變動來使的這個程式實現追蹤路由的功能, 這裡只是拋磚引玉. 將ICMP包中IP包的包頭該為特定的值就能得到那個路由器的IP(要求到達目的地的跳數大於你設的特定值).
 這個程式需要windows2k/WindowsXP/WindowsNT平臺和員的.

具體實現:
--------
這段大部分來自:
  /wskfaq/examples/rawping.html">
[bugfree]只做了少量修改,給出了大量的註釋, 最後結合給出了自己的建議.

----------

/*
 * 程式名: rawping_.cpp
 * 說明: 
 *  程式,也是主
 */
#include  
#include

#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] << " [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 (Wtartup(MAKE(2, 1), &wsaData) != 0) {
  cerr << "Failed to find Winsock 2.1 or better." << endl;
  return 1;
  }

  SOCKET sd; // RAW Socket控制程式碼
  sockaddr_in dest, ;
 
  // 三個任務(建立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 al 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 _LEAN_AND_MEAN
#include

// 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


// 包頭
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 nt;  // 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
#include
#include
#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 =_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 = 0xDEAEEF;
  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如,
  // 有些為256如我的211.97.168.129, 作者假設為256有點武斷,
  // 可以一起探討這個問題, 回e:zhangliangsd@.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封裝進執行緒, 在主程式中對它的超時進行處理
    - 使用()函式來實現
    - 使用windows的 WSAAsyncSelect()
    這裡對這些方法不作具體討論, 留給讀者自已完成. 

 

連結:
-------
我的其它文章,<>, 和其它文章參見:
http://www.csdn.net/develop/author/netauthor/bugfree/

聯絡方式:
-------
 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-996876/,如需轉載,請註明出處,否則將追究法律責任。

相關文章