【网络编程】原始套接字编程
七、原始套接字编程
7.1 原始套接字概述
原始套接字是指在传输层下面使用的套接字。前面介绍了流式套接字和数据报套接字的编 程方法,这两种套接字工作在传输层,主要为应用层的应用程序提供服务,并且在接收和发送 时只能操作数据部分,而不能对IP 首部或TCP 和 UDP 首部进行操作,通常把流式套接字和 数据报套接字称为标准套接字,开发应用层的程序用这两类套接字就够了。但是,如果我们开 发更底层的应用比如发送一个自定义的 IP 包 、UDP 包 、TCP 包 或ICMP 包、捕获所有经过 本机网卡的数据包、伪装本机IP 地址、想要操作IP 首部或传输层协议首部等。这些功能对于 这两种套接字就无能为力了。这些功能需要另外一种套接字来实现,这种套接字叫作原始套接 字 (Raw Socket),该套接字的功能更强大、更底层。原始套接字可以在链路层收发数据帧。 在Windows 下,在链路层上收发数据帧的通用做法是使用WinPcap 开源库来实现。
7.2 原始套接字的强大功能
相对于标准套接字,原始套接字功能更强大,能让开发者实现更底层的功能。使用了标准 套接字的应用程序,只能控制数据包的数据部分,即传输层和网络层头部以外的数据部分,传 输层和网络层头部的数据由协议栈根据套接字创建时候的参数决定,开发者是接触不到这两个 头部数据的。而使用原始套接字的程序允许开发者自行组装数据包,也就是说,开发者不仅可 以控制传输层的头部,还能控制网络层的头部 (IP 包的头部),并且可以接收流经本机网卡 的所有数据帧,这就大大增加了程序开发的灵活性,但也对程序可靠性提出了更高的要求,毕 竟原来是系统组包,现在好多字段都要自己来填充。值得注意的是,必须在管理员权限下才能 使用原始套接字。
通常情况下所接触到的标准套接字为两类:
-
(1)流式套接字(SOCK STREAM):一种面向连接的Socket,针对面向连接的TCP 服 务应用。
-
(2)数据报式套接字 (SOCK_DGRAM): 一种无连接的Socket, 对应于无连接的UDP 服务应用。
原始套接字(SOCK RAW) 与标准套接字 (SOCK_STREAM 、SOCK_DGRAM) 的区别 在于原始套接字直接置“根”于操作系统网络核心 (Network_Core),而 SOCK_STREAM 、 SOCK_DGRAM 则“悬浮”于TCP 和 UDP 协议的外围,如图7-1所示。
流式套接字只能收发 TCP 协议的数据,数据报套接字只能收发 UDP 协议的数据,即标 准套接字只能收发传输层及以上的数据包,因为当IP 层把数据传递给传输层时,下层的数据 包头已经被丢掉了。而原始套接字功能大得多,既可以对上至应用层的数据进行操作,也可以 对下至链路层的数据进行操作。
总的来说,原始套接字主要有以下几大常用功能:
-
(1)原始套接字可以收发ICMPv4 、ICMPv6 和IGMP 数据包,只要在IP 头部中预定义 好网络层上的协议号即可,比如IPPROTO_ICMP 、IPPROTO_ICMPV6和 IPPROTO_IGMP( 这 些都是系统定义的宏,在ws2def.h 中可以看到)等。
-
(2)可以对IP 包头某些字段进行设置。不过这个功能需要设置套接字选项IP HDRINCL。
-
(3)原始套接字可以收发内核不处理(或不认识)的IPv4 数据包,原因可能是IP 包头 的协议号是我们自定义的,或是一个当前主机没有安装的网络协议,比如OSPF 路由协议,该 协议既不使用TCP 也不使用UDP, 其 IP 包头的协议号为89,如果当前主机没有安装该路由 协议,那么内核就不认识也不处理了,此时我们可以通过原始套接字来收发该协议包。我们知 道 ,IPv4 包头中有一个8位长的协议字段,通常用系统预定义的协议号来赋值,并且内核仅 处理这几个系统预定义的协议号(见ws2def.h 中 的IPPROTO, 也可见下面一节)的数据包, 比如协议号为1(IPPROTO_ICMP) 的 ICMP 数据报文、协议号为2(IPPROTO_IGMP) 的 IGMP 报文、协议号为6(IPPROTO_TCP) 的 TCP 报文、协议号为17(IPPROTO_UDP) 的 UDP报文等。除了预定义的协议号外,我们可以自己定义协议号,并赋值给IPv4 包头的协议字段, 这样我们的程序就可以处理不经内核处理的IPv4 数据包了。
-
(4)通过原始套接字可以让网卡处于混杂模式,从而能捕获流经网卡的所有数据包。这 个功能对于制作网络嗅探器很有用。
7.3 原始套接字的基本编程步骤
原始套接字编程方式和前面的UDP 编程方式类似,不需要预先建立连接。
发送的基本编程步骤如下:
- (1)初始化winsock 库。
- (2)创建一个原始套接字。
- (3)设置对端的IP 地址,注意原始套接字通常不涉及端口号(端口号是传输层才有的概 念 ) 。
- (4)组织IP数据包,即填充首部和数据部分。
- (5)使用发送函数发送数据包。
- (6)关闭释放套接字。
- (7)释放套接字库。
原始套接字接收的一般编程过程如下:
- (1)初始化winsock 库。
- (2)创建一个原始套接字。
- (3)把原始套接字绑定到本地的一个协议地址上。
- (4)使用接收函数接收数据包。
- (5)过滤数据包,即判断收到的数据包是否为所需要的数据包。
- (6)对数据包进行处理。
- (7)关闭释放套接字。
- (8)释放套接字库。
是不是感觉和UDP 编程类似。对于常用的IPv4 而言,协议地址就是32位的IPv4 地址和 16位的端口号组合。需要再次强调的是,使用原始套接字的函数通常需要用户有管理员权限。 请检查一下当前Windows 登录用户是否具有管理员权限。
7.3.1 创建原始套接字函数socket
创建原始套接字的函数socket或 WSASocket (该函数是扩展版本,用得不多),这两个 函数在流套接字编程那一章我们介绍过了,只要传入特定的参数就能创建出原始套接字。我们再来看一 下它们的声明:
SOCKET socket(int af, int type, int protocol);
-
其中,参数af 用于指定套接字所使用的协议簇,通常取AF_INET或 AF_INET6;
-
type 表 示套接字的类型,因为我们要创建原始套接字,所以 type 总是取值为 SOCK RAW;
-
参数 protocol用于指定原始套接字所使用的协议,由于原始套接字能使用的协议较多,因此该参数 通常不为0,为0通常表示取该协议簇af所默认的协议,对于AF INET 来说,默认的协议是 TCP。该参数值会被填充到IP 包头协议字段中,这个参数既可以使用系统预定义的协议号也 可以使用自定义的协议号,在ws2def.h 中预定义常见网络协议的协议号:
我们需要原始套接字访问什么协议,就让参数protocol 取上面的协议号,比如我们创建一 个用于访问ICMP 协议报文的原始套接字,可以这样:
SOCKET s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
如果要创建一个用于访问IGMP 协议报文的原始套接字,可以这样:
SOCKET s = socket(AF INET, SOCK RAW, IPPROTO IGMP);
如果要创建一个用于访问IPv4 协议报文的原始套接字,可以这样:
SOCKET s = socket(AF INET, SOCK RAW, IPPROTO IP);
以此类推,值得注意的是,对于原始套接字,参数protocol一般不能为0,这是因为取了 0后,所创建的原始套接字可以接收内核传递给原始套接字的任何类型的IP 数据报,需要大 家再去区分。
-
另外有一点,参数protocol 不仅仅取上面预定义的协议号,上面的枚举IPPROTO 中,范围达到了0-255,因此protocol 可取值的范围是0-255,而且系统没有全部用完,所以 我们完全可以在0~255范围内定义自己的协议号,即利用原始套接字来实现自定义的上层协议。
-
顺便科普一下,IANA 组织负责管理协议号。另外,如果想完全构造包括IP 头部在内的 数据包,可以使用协议号 IPPROTO_RAW。 如果函数成功就返回新建的套接字描述符,失败则返回INVALID_SOCKET, 此时可以用函数WSAGetLastError 来查看错误码。
-
另外,也可以通过扩展版本函数WSASocket 来创建原始套接字。
7.3.2 接收函数recvfrom
实际上原始套接字被认为是无连接套接字,因此原始套接字的数据接收函数同UDP的接收数据函数,都是recvfrom 。 该函数声明如下:
int recvfrom(sOCKET s, char *buf, int len, int flags, struct sockaddr *from,int *fromlen);
- 其中,参数s 是将要从其接收数据的原始套接字描述符;
- buf 为存放消息接收后的缓冲区;
- len为 buf 所指缓冲区的字节大小;from[out]是一个输出参数(记住这一点,不是用来指定接 收来源,如果要指定接收来源,要用bind 函数进行套接字和物理层地址绑定),用来获取对 端地址,所以from 指向一个已经开辟好的缓冲区,如果不需要获得对端地址,就设为NULL, 即不返回对端socket地址;
- fromlen[in,out]是一个输入/输出参数,作为输入参数时指向存放表 示 from 所指缓冲区的最大长度,作为输出参数时指向存放表示 from 所指缓冲区的实际长度,如果from 取 NULL, 那么 fromlen 也要设为0。如果函数成功执行时,返回收到数据的字节数; 如果另 一端已优雅地关闭就返回0;函数执行失败则返回 SOCKET_ERROR, 可以用 WSAGetLastError。
- 当操作系统收到一个数据包后,系统对所有由进程创建的原始套接字进行匹配,所有匹配 成功的原始套接字都会收到数据包的一份备份。
- 值得注意的是,对于IPv4,recvfrom 总是能接收到包括IP 头在内的完整数据包,不管原 始套接字是否指定了IP HDRINCL 选项。对于IPv6,recvfrom 只能接收除了IPv6 头部及扩展 头部以外的数据,即无法通过原始套接字接收IPv6 的头部数据。
- 该函数使用时和UDP 基本相同,只不过套接字用的是原始套接字。值得注意的是,对于 IPv4,创建原始套接字后,接收到的数据就会包含IP 包头。
光了解接收函数本身是不够的,我们还需要了解用这个函数接收时什么类型的数据会接 收,接收到的数据内容是什么样的。
-
值得注意的是,对于IPv4,原始套接字接收到的数据总是包含IP 首部在内的完整数据包; 对于IPv6, 收到的数据则是去掉了IPv6 首部和扩展首部的。
-
首先我们来看接收类型,协议栈把从网络接口(比如网卡)处收到的数据传递到应用程序 的缓冲区中(recvfrom 的第二个参数)经历了3次传递,先把数据复制到原始套接字层,然后 把数据复制到原始套接字的接收缓冲区,最后把数据从接收缓冲区复制到应用程序的缓冲区。 在前两次复制的过程中,不是所有网卡的数据都会复制过去,而是有条件、有选择的,第三次 复制通常是无条件复制。对于第一次复制,协议栈通常会对下列IP 数据包进行复制:
-
(1)UDP 分组或TCP 分组。
-
(2)部分ICMP 分组。注意是“部分”,大家待会会看到这个效果。默认情况下,原始 套接字抓不到ping 包。
-
(3)所有IGMP 分组。
-
(4)IP 首部的协议字段不被协议栈认识的所有IP 包。
-
(5)重组后的IP 分片。
第二次复制也是有条件的复制,协议栈会检查每个进程,并查看进程中所有已创建的套接字,看其是否符合条件,如果符合就把数据复制到原始套接字的接收缓冲区。具体条件如下:
- (1)协议号是否匹配:还记得原始套接字创建函数 socket 的第三个参数吗?协议栈检查 收到的IP 包的首部协议字段是否和socket 的第三个参数相等,如果相等就会把数据包复制到 原始套接字的接收缓冲区。后面会在接收UDP 分组的例子中体会到这一点。
- (2)目的IP 地址是否匹配:如果接收端用bind函数把原始套接字绑定接收端的某个IP, 协议栈会检查数据包中的目的IP 地址是否和该套接字所绑的IP 地址相符,如果相符就把数据 包复制到该套接字的接收缓冲区,如果不相符就不复制。如果接收端原始套接字绑定的是任意 IP地址,即使用了INADDR_ANY,也会复制数据。大家会在后面的例子中体会到这一点。
7.3.3 发送函数sendto
在原始套接字上发送数据包都被认为是无连接套接字上的数据包,因此发送函数同 UDP 的发送函数,都是用sendto 或 WSASendTo(sendto 的扩展版本,用得不多)。sendto 声明如 下:
int sendto(SOCKET s, const char *buf, int len, int flags, const struct sockaddr *to, int tolen);
- 其中,参数s 为原始套接字描述符;
- buf 为要发送的数据内容;len 为 buf 的字节数;
- 参数 flags 一般设为零;参数to 用来指定欲传送数据的对端网络地址;tolen 为 to 的字节数。如果 函数成功就返回实际发送出去的数据字节数,否则返回SOCKET_ERROR。
下面进入实战,看一个简单的原始套接字小例子——原始套接字和标准套接字联合作战。 这个小例子是笔者精心设计的,一般书上都没有。在这个例子中,我们的解决方案分为发送工 程和接收工程。发送工程生成的程序是用标准套接字的一种——数据报套接字来发送一个 UDP 包,而接收工程生成的程序是一个原始套接字程序,用来接收发送程序发来的UDP 包, 并打印出源和目的的IP 地址和端口号。
7.4 常规编程示例
在介绍了原始套接字的基本编程步骤和编程函数后,我们就可以进入实战环节来加深理解 原始套接字的使用了。几个小例子都非常典型,希望大家多加练习。
【例7.1】原始套接字接收UDP 分组
客户端:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include "winsock2.h"
#pragma comment(lib, "ws2_32.lib")
#include <stdio.h>
char wbuf[50];
int main()
{
int sockfd;
int size;
char on = 1;
struct sockaddr_in saddr;
int ret;
size = sizeof(struct sockaddr_in);
memset(&saddr, 0, size);
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号
err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
if (err != 0) return 0;
//设置地址信息,ip信息
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = inet_addr("192.168.48.1");
sockfd = socket(AF_INET, SOCK_DGRAM, 0); //创建udp 的套接字
if (sockfd < 0)
{
perror("failed socket");
return -1;
}
//设置端口复用
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
//发送信息给服务端
puts("please enter data:");
scanf_s("%s", wbuf, sizeof(wbuf));
ret = sendto(sockfd, wbuf, sizeof(wbuf), 0, (struct sockaddr*)&saddr,
sizeof(struct sockaddr));
if (ret < 0)
{
perror("sendto failed");
}
closesocket(sockfd);
WSACleanup(); //释放套接字库
return 0;
}
在上述代码中,首先设置服务器端(接收端)的地址信息(IP 和端口),端口其实不设 置也没关系,因为我们的接收端是原始套接字,是在网络层上抓包的,端口信息对原始套接字 来说没啥用,这里设置了端口信息(9999),目的是为了在接收端下能把这个端口信息打印出 来,让大家更深刻地理解UDP 协议的一些字段,即端口信息是在传输层的字段。
服务端:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include "winsock2.h"
#pragma comment(lib, "ws2_32.lib")
#include <stdio.h>
char rbuf[500];
typedef struct _IP_HEADER //IP头定义,共20个字节
{
char m_cVersionAndHeaderLen; //版本信息(前4位),头长度(后4位)
char m_cTypeOfService; // 服务类型8位
short m_sTotalLenOfPacket; //数据包长度
short m_sPacketID; //数据包标识
short m_sSliceinfo; //分片使用
char m_cTTL; //存活时间
char m_cTypeOfProtocol; //协议类型
short m_sCheckSum; //校验和
unsigned int m_uiSourIp; //源IP地址
unsigned int m_uiDestIp; //目的IP地址
}IP_HEADER, * PIP_HEADER;
typedef struct _UDP_HEADER // UDP头定义,共8个字节
{
unsigned short m_usSourPort; // 源端口号16bit
unsigned short m_usDestPort; // 目的端口号16bit
unsigned short m_usLength; // 数据包长度16bit
unsigned short m_usCheckSum; // 校验和16bit
}UDP_HEADER, * PUDP_HEADER;
int main()
{
int sockfd;
int size;
int ret;
char on = 1;
struct sockaddr_in saddr;
struct sockaddr_in raddr;
IP_HEADER iph;
UDP_HEADER udph;
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号
err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
if (err != 0) return 0;
//设置地址信息,ip信息
size = sizeof(struct sockaddr_in);
memset(&saddr, 0, size);
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
saddr.sin_addr.s_addr = inet_addr("192.168.48.1");
//创建udp 的套接字
sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);//该原始套接字使用UDP协议
if (sockfd < 0)
{
perror("socket failed");
return -1;
}
//设置端口复用
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
//绑定地址信息,ip信息
ret = bind(sockfd, (struct sockaddr*)&saddr, sizeof(struct sockaddr));
if (ret < 0)
{
perror("sbind failed");
return -1;
}
int val = sizeof(struct sockaddr);
//接收客户端发来的消息
while (1)
{
puts("waiting data");
ret = recvfrom(sockfd, rbuf, 500, 0, (struct sockaddr*)&raddr, &val);
if (ret < 0)
{
printf("recvfrom failed:%d", WSAGetLastError());
return -1;
}
memcpy(&iph, rbuf, 20);
memcpy(&udph, rbuf + 20, 8);
int srcp = ntohs(udph.m_usSourPort);
struct in_addr ias, iad;
ias.s_addr = iph.m_uiSourIp;
iad.s_addr = iph.m_uiDestIp;
char s[100];
strcpy_s(s, inet_ntoa(iad));
printf("(sIp=%s,sPort=%d), \n(dIp=%s,dPort=%d)\n", inet_ntoa(ias), ntohs(udph.m_usSourPort), s, ntohs(udph.m_usDestPort));
printf("recv data :%s\n", rbuf + 28);
}
//关闭原始套接字
closesocket(sockfd);
WSACleanup(); //释放套接字库
return 0;
}
- 在上述代码中,首先为结构体 saddr 设置本地地址信息。然后创建一个原始套接字sockfd, 并设置第三个参数为IPPROTO_UDP, 表明这个原始套接字使用的是UDP 协议,能收到UDP 数据包。接着把 sockfd 绑定到地址saddr 上。
- 再接着开启一个循环阻塞接收数据, 一旦收到数 据就把缓冲区前20个字节复制到iph 中,因为数据包的IP 包头占20字节,20字节后面的8 字节是UDP 头部,因此再把20字节后的8字节复制到udph 中。获取IP 首部字段后,就可以 打印出源和目的IP 地址了。获取 UDP 首部字段后,就可以打印出源和目的端口了。最后打印 出UDP 包头后的文本信息,即发送端用户输入的文本。
- 另外有一点要注意,接收端绑定的IP 地址使用了INADDR_ANY 。这种情况下,协议栈会把数据包复制给原始套接字,如果绑定的IP 地址用了数据包的目的IP 地址(120.4.6.200), 即:
saddr.sin addr.s addr = inet addr("120.4.6.200");
-
接收端也是可以收到数据包的,有兴趣的人可以试试,这里不再赘述。如果接收端绑定了一个 本机的IP 地址,但不是数据包中的目的IP 地址,会如何?答案是收不到,我们可以在下一个 例中体会这 一 点。
-
另外,我们的原始套接字使用的是UDP 协议,所以只收到UDP 报文,其他报文不会接 收。大家在其他主机上ping 120.4.6.200,可以发现rcver 程序没有任何反映。
再次强调一下,对于IPv4, 接收到的数据总是完整的数据包,而且是包含IP 首部的。
下面这个案例读者我没有动手实操过,有兴趣可以自己试验一下:
【例7.2】接收端绑定一个和数据包目的地址不同的IP 后,收不到数据包
默认情况下,原始套接字是抓不到ping包的,大家可以看下面这个例子。
- (1)在这个例子中,我们要在接收端主机上设置两个 IP 地址,比如120.4.6.200和 192.168.1.2。
- (2)把例7. 1中的test 解决方案复制一份作为例7.2的解决方案。
- (3)打开test 解决方案,发送工程test 不需要修改任何代码,在接收端工程rcver 中修改 一行代码,即将
saddr.sin_addr.s_addr=htonl(INADDR_ANY);
改为
saddr.sin_addr.s_addr=inet addr(" 192.168.1.2");
- (4)编译运行rcver, 然后运行test 并输入一行文本,可以发现rcver 没有任何反应,如 图7-5所示。
默认情况下,原始套接字是抓不到ping包的,大家可以看下面这个例子。
【例7.3】原始套接字收不到ping包(默认情况)
这个例子读者也没有自己实验过,感兴趣可以自己实验一下,这里就分享作者自己的实验过程
- (1)新建 一个控制台工程rcver。
- (2) 在rcver.cpp 中输入如下代码:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include "winsock2.h"
#pragma comment(lib, "ws2_32.lib")
#include <stdio.h>
// IP 头定义,共 20 字节
typedef struct IP_HEADER {
char m_cVersionAndHeaderLen; // 版本信息(前 4 位),头长度(后 4 位)
char m_cTypeOfService; // 服务类型 8 位
short m_sTotalLenOfPacket; // 数据包长度
short m_sPacketID; // 数据包标识
short m_sSliceinfo; // 分片使用
char m_cTTL; // 存活时间
char m_cTypeOfProtocol; // 协议类型
short m_sCheckSum; // 校验和
unsigned int m_uiSourIp; // 源 IP 地址
unsigned int m_uiDestIp; // 目的 IP 地址
} IP_HEADER, *PIP_HEADER;
// UDP 头定义,共 8 字节
typedef struct UDP_HEADER {
unsigned short m_usSourPort; // 源端口号 16bit
unsigned short m_usDestPort; // 目的端口号 16bit
unsigned short m_usLength; // 数据包长度 16bit
unsigned short m_usCheckSum; // 校验和 16bit
} UDP_HEADER, *PUDP_HEADER;
int main() {
int sockfd;
int size;
int ret;
char on = 1;
struct sockaddr_in saddr;
struct sockaddr_in raddr;
IP_HEADER iph;
UDP_HEADER udph;
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2); // 制作 Winsock 库的版本号
err = WSAStartup(wVersionRequested, &wsaData); // 初始化 Winsock 库
if (err != 0) {
printf("WSAStartup failed: %d\n", err);
return 0;
}
// 设置地址信息,IP 信息
size = sizeof(struct sockaddr_in);
memset(&saddr, 0, size);
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888); // 端口号
// 一个本机的 IP 地址,但和发送端设定的目的 IP 地址不同
saddr.sin_addr.s_addr = inet_addr("120.4.6.200");
// 创建原始套接字,使用 ICMP 协议
sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sockfd < 0) {
perror("socket failed");
WSACleanup();
return -1;
}
// 设置端口复用
ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if (ret < 0) {
perror("setsockopt failed");
closesocket(sockfd);
WSACleanup();
return -1;
}
// 绑定地址信息,IP 信息
ret = bind(sockfd, (struct sockaddr*)&saddr, sizeof(struct sockaddr_in));
if (ret < 0) {
perror("bind failed");
closesocket(sockfd);
WSACleanup();
return -1;
}
int val = sizeof(struct sockaddr_in);
char rbuf[500];
// 接收客户端发来的消息
while (1) {
ret = recvfrom(sockfd, rbuf, 500, 0, (struct sockaddr*)&raddr, &val);
if (ret < 0) {
printf("recvfrom failed: %d\n", WSAGetLastError());
continue;
}
memcpy(&iph, rbuf, 20);
memcpy(&udph, rbuf + 20, 8);
int srcp = ntohs(udph.m_usSourPort);
struct in_addr ias, iad;
ias.s_addr = iph.m_uiSourIp;
iad.s_addr = iph.m_uiDestIp;
printf("(sIp=%s,sPort=%d),\n(dIp=%s,dPort=%d)\n", inet_ntoa(ias),
ntohs(udph.m_usSourPort), inet_ntoa(iad), ntohs(udph.m_usDestPort));
printf("recv data : %s\n", rbuf + 28);
}
// 关闭原始套接字
closesocket(sockfd);
WSACleanup(); // 释放套接字库
return 0;
}
- (3)假设rcver 程序所在主机的IP 为120.4.6.200,而虚拟机XP 的 IP 为120.4.6.100,现 在我们先编译运行 rcver, 此时它将处于等待接收数据状态。然后在虚拟机 XP 下 ping 120.4.6.200,接着重新查看rcver 程序,发现没有收到任何包,如图7-6所示。
在上述代码中,我们新建了一个使用ICMP 协议的原始套接字,是不是应该会抓到ping 命令过来的数据包呢?答案是否定的。我们在同一网段下的虚拟机XP 中使用ping 命令来测 试。
- 这就说明,默认情况下,即使使用ICMP 协议的原始套接字,也是收不到Windows 自带 的ping 命令发来的数据包的。是不是很扫兴?
- 别急,前面提到协议栈是会把部分ICMP 包传 给原始套接字的,既然自带的ping 命令包收不到,就自己写一个ICMP 包的发送程序,能否 收到呢?答案是肯定的。这样也验证了原始套接字是可以收到部分ICMP 分组的。
- 因为涉及未学的网络编程知识,所以暂且不表。如何能抓到ping 命令发来的包呢?且看下节分解。
7.5 抓取所有IP 数据包
从上一个例子中可看出,默认情况下,协议栈是不会把网卡收到的数据全部复制到原始套 接字上的,如果需要抓包分析网络数据包怎么办?有一些应用场合下,希望抓到网卡收到的数 据包,甚至是流经本机网卡但不是发往本机的数据包。Winsock 早已为我们提供了办法,那就 是设置套接字控制台命令 SIO_RCVALL(允许原始套接字能接收所有经过本机的网络数据 包)。设置的方法是利用API 函 数WSAloctl来发送I/O 控制命令(源自Winsock2 版本,函数声明见第5章)。
使用原始套接字抓取所有IP 包,注意以下几点:
- (1)SIO_RCVALL 系统并没有暴露给我们使用,我们需要在程序中自己定义:
#define SIO_RCVALL WSAIOW(IOC_VENDOR, 1)
- (2)SIO_RCVALL 目前只能用于 IPv4, 因此在创建原始套接字的时候协议簇必须是AF_INET。
- (3)使 用socket函数创建原始套接字的时候,协议类型参数要为IPPROTO_IP,比如
sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_IP);
- (4)必须将原始套接字绑定到本地的某个网络接口。
【例7.4】抓取所有IP 数据包并分析(包括ping包 )
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include "winsock2.h"
#pragma comment(lib, "ws2_32.lib")
#include <stdio.h>
char rbuf[500];
#define SIO_RCVALL _WSAIOW(IOC_VENDOR,1)
typedef struct _IP_HEADER //IP头定义,共20个字节
{
char m_cVersionAndHeaderLen; //版本信息(前4位),头长度(后4位)
char m_cTypeOfService; // 服务类型8位
short m_sTotalLenOfPacket; //数据包长度
short m_sPacketID; //数据包标识
short m_sSliceinfo; //分片使用
char m_cTTL; //存活时间
char m_cTypeOfProtocol; //协议类型
short m_sCheckSum; //校验和
unsigned int m_uiSourIp; //源IP地址
unsigned int m_uiDestIp; //目的IP地址
}IP_HEADER, * PIP_HEADER;
typedef struct _UDP_HEADER // UDP头定义,共8个字节
{
unsigned short m_usSourPort; // 源端口号16bit
unsigned short m_usDestPort; // 目的端口号16bit
unsigned short m_usLength; // 数据包长度16bit
unsigned short m_usCheckSum; // 校验和16bit
}UDP_HEADER, * PUDP_HEADER;
int main()
{
int sockfd;
int size;
int ret;
char on = 1;
struct sockaddr_in saddr;
struct sockaddr_in raddr;
IP_HEADER iph;
UDP_HEADER udph;
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号
err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
if (err != 0) return 0;
//设置地址信息,ip信息
size = sizeof(struct sockaddr_in);
memset(&saddr, 0, size);
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
//创建udp 的套接字
sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_IP);
if (sockfd < 0)
{
perror("socket failed");
return -1;
}
//绑定地址信息,ip信息
ret = bind(sockfd, (struct sockaddr*)&saddr, sizeof(struct sockaddr));
if (ret < 0)
{
perror("sbind failed");
return -1;
}
DWORD dwlen[10], dwlenRtned = 0, Optval = 1;
WSAIoctl(sockfd, SIO_RCVALL, &Optval, sizeof(Optval), &dwlen, sizeof(dwlen), &dwlenRtned, NULL, NULL);
int val = sizeof(struct sockaddr);
//接收客户端发来的消息
while (1)
{
puts("waiting data");
ret = recvfrom(sockfd, rbuf, 500, 0, (struct sockaddr*)&raddr, &val);
if (ret < 0)
{
printf("recvfrom failed:%d", WSAGetLastError());
return -1;
}
printf("----------rcv-------------\n");
memcpy(&iph, rbuf, 20);
struct in_addr ias, iad;
ias.s_addr = iph.m_uiSourIp;
iad.s_addr = iph.m_uiDestIp;
char dip[100];
strcpy_s(dip, inet_ntoa(iad));
printf("m_cTypeOfProtocol=%d", iph.m_cTypeOfProtocol);
switch (iph.m_cTypeOfProtocol)
{
case IPPROTO_ICMP:
printf("收到ICMP包");
break;
case IPPROTO_UDP:
memcpy(&udph, rbuf + 20, 8);
printf("收到UDP包,内容为:%s\n", rbuf + 28);
break;
}
printf("\nsIp=%s, dIp=%s, \n", inet_ntoa(ias), dip);
}
//关闭原始套接字
closesocket(sockfd);
WSACleanup(); //释放套接字库ss
return 0;
}
-
在上述代码中,首先创建一个协议类型为 IPPROTO_IP 的原始套接字,然后绑定到本机 地址,接着使用函数WSAloctl 发送套接字命令SIO_RCVALL,设置成功后就可以收到所有发 往本机的IP 包了。
-
收到包后,我们对IP 头部的协议类型进行判断,这里就简单地区分了ICMP 包和UDP 包。大家可以见代码中的switch 语句。最后我们打印了源目的IP 地址。
-
值得注意 的是,不要在打印IP 地 址 的printf 函数中用两次inet_ntoa, 这样无法正确打印出全部IP 地址, 估计是微软的一个bug。所以,在上面的代码中,把目的IP 地址单独放到了一个数组dip 中 。
-
(4)rcver 除了能抓ICMP 包外,也对UDP 进行了捕获,所以我们可以另外编写一个发 送UDP 包的程序,然后放到另外一台主机(120.4.2.100)上运行,看一下rcver能否捕获到其 发来的UDP 包。
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include "winsock2.h"
#pragma comment(lib, "ws2_32.lib")
#include <stdio.h>
char wbuf[50];
int main()
{
int sockfd;
int size;
char on = 1;
struct sockaddr_in saddr;
int ret;
size = sizeof(struct sockaddr_in);
memset(&saddr, 0, size);
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0) return 0;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("failed socket");
return -1;
}
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
puts("please enter data:");
scanf_s("%s", wbuf, sizeof(wbuf));
ret = sendto(sockfd, wbuf, sizeof(wbuf), 0, (struct sockaddr*)&saddr,
sizeof(struct sockaddr));
if (ret < 0)
{
perror("sendto failed");
}
closesocket(sockfd);
WSACleanup();
return 0;
}
效果如下:
参考书籍:《Visual C++ 2017网络编程实战》