当前位置: 首页 > news >正文

【网络编程】原始套接字编程

七、原始套接字编程

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网络编程实战》

相关文章:

  • 【UI自动化框架第五张】AndroidUiAutomation 类功能简介
  • deepseek R1提供的3d迷宫设计方案
  • freeswitch(多台服务器级联)
  • 文件和异常
  • 串口通信ASCII码转16进制及C#串口编程完整源码下载
  • Dify平台部署全记录
  • Redis--Set类型
  • Zabbix 7.2 + Grafana 中文全自动安装ISO镜像
  • mysql的binlog,redolog,undolog作用
  • 架构师面试(十四):注册中心设计
  • ICLESCTF-web-misc-wp
  • 小程序配置webview
  • 用栈实现队列 用队列实现栈
  • 《Windows 文件命名规则与 Python 日志文件生成技巧》
  • . 从理论到实践:小红书、京东如何玩转大模型
  • Go Ebiten小游戏开发:俄罗斯方块
  • 【Linux网络(一)】初识网络
  • 使用外挂工具,在教师资格面试抽题系统中自动填入身份证号
  • git文件过大导致gitea仓库镜像推送失败问题解决(push failed: context deadline exceeded)
  • ragflow-组件可视化工具 es默认用户名elastic
  • 导航网站cms/公众号引流推广平台
  • 网站建设公司推广方案/网页设计html代码大全
  • 深圳旅游路线设计方案/seo网站优化师
  • 做鞋子批发的网站有哪些/东营seo网站推广
  • 动态网站开发 PHP/杭州网站建设方案优化
  • 网站开发怎么做才有利于seo/免费智能seo收录工具