共计 5231 个字符,预计需要花费 14 分钟才能阅读完成。
导读 | 本文通过介绍原始套接字实现经典的网络命令,即 Ping 命令。通过完成一个 Ping 命令来初步了解和掌握原始套接字的使用。 |
使用 TCP 或 UDP 时,需要在调用 socket() 函数时为它的第 2 个参数指定相应的类型,比如 SOCK_STREAM 是代表要使用 TCP,而 SOCK_DGRAM 表示要使用 UDP 协议。除了可以指定这两种类型以外,还可以指定为原始套接字类型,即 SOCK_RAW。当 socket() 函数的第 2 个参数指定为 SOCK_STREAM 或 SOCK_DGRAM 时,第 3 个参数可以缺省。而当 socket() 函数的第 2 个参数指定为 SOCK_RAW 时,第 3 个参数就必须明确指定需要使用的协议。
当套接字类型指定为 SOCK_RAW 时,协议类型的常用取值有 IPPROTO_IP、IPPROTO_ICMP、IPPROTO_TCP、IPPROTO_UDP 和 IPPROTO_RAW。使用前四种类型,当发送数据时,系统会自动为数据加上 IP 首部并设置 IP 首部中的上层协议字段(如果有 IP_HDRINCL 选项,则系统不会自动添加 IP 首部);当接收数据时,系统不会将 IP 首部移除,需要程序自行处理。如果使用 IPPROTO_RAW,那么系统将数据包直接送到网络层发送数据,并且需要程序自己构造 IP 首部中的字段。
本文通过介绍原始套接字实现经典的网络命令,即 Ping 命令。通过完成一个 Ping 命令来初步了解和掌握原始套接字的使用。
Ping 命令的目的是为了测试另一台主机是否可达,Ping 命令发送一份 ICMP 回显请求报文给主机,并等待返回 ICMP 回显应答。一般来说,如果不能 Ping 到某台主机,那么就不能与该主机进行通信(例外的情况是对方主机的防火墙将进入主机的回显请求报文屏蔽掉了,这种情况虽然 Ping 不通,但是仍然可以正常进行通信)。
Ping 命令有很多参数,打开命令行直接输入 Ping 后按下回车键,这样就可以看到 Ping 命令的参数列表,如下图所示。
通常情况下,用户都只是简单 Ping 一下某个主机的地址。Ping 命令的参数可以是主机名称、域名和 IP 地址,后两者是较为常用的。下面简单演示一个 Ping 的例子,具体如下:
C:\>ping 8.8.4.4
Pinging 8.8.4.4 with 32 bytes of data:
Reply from 8.8.4.4: bytes=32 time=57ms TTL=47
Reply from 8.8.4.4: bytes=32 time=54ms TTL=47
Reply from 8.8.4.4: bytes=32 time=54ms TTL=47
Reply from 8.8.4.4: bytes=32 time=51ms TTL=47
Ping statistics for 8.8.4.4:
Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 51ms, Maximum = 57ms, Average = 54ms
上面就是使用 Ping 命令对 8.8.4.4 这个 IP 进行回显请求后的输出信息。这里来解释一下请求后的回显信息的含义。
Pinging 8.8.4.4 with 32 bytes of data:
正在将 32 字节数据发送到远程主机 8.8.4.4,如果 Ping 的是一个域名或主机名的话,这里会将域名(主机名)转换为 IP 地址显示出来。
Reply from 8.8.4.4: bytes=32 time=57ms TTL=47
本地主机已经收到回显应答信息,bytes=32 表示有 32 字节,time=57ms 表示公用了 57 毫秒,TTL 表示的是生存时间值,该值可以进行设置,该值最大为 255。每个处理数据包的路由器都需要把 TTL 的值减 1 或减去数据包在路由器中停留的秒数。由于大多数路由器转发数据包的延时都小于 1 秒,因此 TTL 最终成为一个跳站的计数器,所经过的每个路由器都将其值减 1,当该值被减到 0 值时,该包将被丢弃。
Ping statistics for 8.8.4.4:
Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 51ms, Maximum = 57ms, Average = 54ms
Ping 8.8.4.4 的统计信息为:Sent= 4 表示发送了 4 个数据包,Received= 4 表示接收了 4 个数据包,Lost=0(0% loss) 表示丢失的数据包是 0 个,丢包率为 0%。
发送时间的大概情况:Mininum=51ms,最快是 51ms,Maximum=57ms,最慢是 57ms,Average=54ms,平均为 54ms。
Ping 命令依赖的不是 TCP,也不是 UDP,它依赖的是 ICMP。ICMP 是 IP 层的协议之一,它传递差错报文以及其他需要注意的信息。ICMP 报文通常被 IP 层或高层协议使用。ICMP 封装在 IP 数据报内部,如下图所示。
ICMP 报文的格式如下图所示。
ICMP 协议的类型码与代码根据不同的情况,各自取不同的值。Ping 命令类型码用到了 2 个值,分别是 0 和 8。而代码的取值都是 0。当类型码取值为 0 时,代码的 0 值表示回显应答;当类型码取值为 8 时,代码的 0 值表示请求回显。Ping 命令发送一个 ICMP 数据报时,类型码为 8,代码为 0,表示向对方主机进行请求回显;当收到对方的 ICMP 数据报时,类型码为 0,代码为 0,表示收到了对方主机的回显应答。简单来说,Ping 命令发出的数据中,类型是 8,代码是 0,如果对方有回应,那么对方回应的数据中,类型是 0,代码是 0。
在自己实现 Ping 命令时,就是去自己构造一个请求回显的 ICMP 数据报,然后进行发送。ICMP 的数据结构定义如下:
// ICMP 协议结构体定义
struct icmp_header
{
unsigned char icmp_type; // 消息类型
unsigned char icmp_code; // 代码
unsigned short icmp_checksum; // 校验和
unsigned short icmp_id; // 用来唯一标识此请求的 ID 号,通常设置为进程 ID
unsigned short icmp_sequence; // 序列号
unsigned long icmp_timestamp; // 时间戳
};
ICMP 的数据结构在网络开发中会经常用到,可以将其保存以备后用。
明白了 ICMP 协议的数据结构,现在用抓包工具(也可以称为协议分析工具)Wireshark 来分析一下 ICMP 结构真实的情况,如下图所示。
在上图中,标识 1 的部分是对协议进行过滤设置的,在该部分输入“ICMP”可以让 Wireshark 只显示 ICMP 的数据记录。相应地,可以输入“TCP”、“UDP”、“HTTP”等协议进行筛选过滤。标识 2 的部分用于显示筛选后的 ICMP 记录,从这里可以明显看出源 IP 地址、目的 IP 地址和协议的类型。标识 3 的部分用于显示 ICMP 数据结构的值和附加的数据内容。最下面的部分显示了数据的原始的二进制数据,在熟练掌握协议后,查看原始的二进制数据也并不是不可能的。
有了前面的基础,就可以构造自己的 ICMP 数据报来构造自己的 Ping 命令了。首先,定义两个常量,还有计算校验和的函数,具体如下:
struct icmp_header
{
unsigned char icmp_type; // 消息类型
unsigned char icmp_code; // 代码
unsigned short icmp_checksum; // 校验和
unsigned short icmp_id; // 用来唯一标识此请求的 ID 号,通常设置为进程 ID
unsigned short icmp_sequence; // 序列号
unsigned long icmp_timestamp; // 时间戳
};
#define ICMP_HEADER_SIZE sizeof(icmp_header)
#define ICMP_ECHO_REQUEST 0x08
#define ICMP_ECHO_REPLY 0x00
// 计算校验和
unsigned short chsum(struct icmp_header *picmp, int len)
{
long sum = 0;
unsigned short *pusicmp = (unsigned short *)picmp;
while (len > 1)
{sum += *(pusicmp++);
if (sum & 0x80000000)
{sum = (sum & 0xffff) + (sum >> 16);
}
len -= 2;
}
if (len)
{sum += (unsigned short)*(unsigned char *)pusicmp;
}
while (sum >> 16)
{sum = (sum & 0xffff) + (sum >> 16);
}
return (unsigned short)~sum;
}
ICMP 的校验值是一个 16 位的无符号整型,它会将 ICMP 协议头不的数据进行累加,当累加有溢出的话,会将溢出的部分也进行累加。具体计算校验和的算法就不过多介绍了,如果对校验和计算的代码不了解,可以进行单步调试来进行分析。再来看一下对于 ICMP 结构体的填充,具体代码如下:
BOOL MyPing(char *szDestIp)
{
BOOL bRet = TRUE;
WSADATA wsaData;
int nTimeOut = 1000;
char szBuff[ICMP_HEADER_SIZE + 32] = {0};
icmp_header *pIcmp = (icmp_header *)szBuff;
char icmp_data[32] = {0};
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 创建原始套接字
SOCKET s = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP)
// 设置接收超时
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char const*)&nTimeOut, sizeof(nTimeOut));
// 设置目的地址
sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_addr.S_un.S_addr = inet_addr(szDestIp);
dest_addr.sin_port = htons(0);
// 构造 ICMP 封包
pIcmp->icmp_type = ICMP_ECHO_REQUEST;
pIcmp->icmp_code = 0;
pIcmp->icmp_id = (USHORT)::GetCurrentProcessId();
pIcmp->icmp_sequence = 0;
pIcmp->icmp_timestamp = 0;
pIcmp->icmp_checksum = 0;
// 拷贝数据
// 这里的数据可以是任意的
// 这里使用 abc 是为了和系统提供的看起来一样
memcpy((szBuff + ICMP_HEADER_SIZE), "abcdefghijklmnopqrstuvwabcdefghi", 32);
// 计算校验和
pIcmp->icmp_checksum = chsum((struct icmp_header *)szBuff, sizeof(szBuff));
sockaddr_in from_addr;
char szRecvBuff[1024];
int nLen = sizeof(from_addr);
sendto(s, szBuff, sizeof(szBuff), 0, (SOCKADDR *)&dest_addr, sizeof(SOCKADDR));
recvfrom(s, szRecvBuff, MAXBYTE, 0, (SOCKADDR *)&from_addr, &nLen);
// 判断接收到的是否是自己请求的地址
if (lstrcmp(inet_ntoa(from_addr.sin_addr), szDestIp) )
{bRet = FALSE;}
else
{struct icmp_header *pIcmp1 = (icmp_header *)(szRecvBuff + 20);
printf("%s\r\n", inet_ntoa(from_addr.sin_addr));
}
return bRet;
}
这就是 Ping 命令的全部代码了。自己写一个函数调用它进行测试。
在 Windows XP 以上的操作系统中运行时,比如 Windows 8 系统,程序可能会无法正常的运行,这是因为操作系统权限所导致的。在被编译好的程序上单击右键,在弹出的菜单上选择“以管理员身份运行”,这样程序就可以正常的执行了。