引言
原始套接字是允许访问底层传输协议的一种套接字类型,提供了普通套接字所不具备的功能,能够对网络数据包进行某种程度的控制操作。因此原始套接字通常用开发简单网络性能监视程序以及网络探测、网络攻击等工具。今天我们来探索一下,从实现原始套接字到捕获数据包的整个过程。
原始套接字与TCP套接字和UDP套接字的区别
Berkeley套接字将流式套接字和数据报套接字定义为标准套接字,用于在主机之间通过TCP和UDP来传输数据。为了保证Internet的使用效率,除了传输数据之外,操作系统的协议栈还处理了大量的非数据流量,如果程序员在创建应用时也需要对这些非数据流量进行控制的话,那么此时就需要另一种套接字,即原始套接字。这种套接字越过了TCP/IP协议栈的部分层次,为程序员提供了完全且直接的的数据包级别的Internet访问能力,如下图所示。
从图中我们可以清晰看出,对于普通流式套接字和数据包套接字的应用程序,他们只能控制数据包的数据部分,也就是除了传输层首部和网络层首部以外的,需要通过网络传输的数据部分。而传输层首部和网络层首部则由协议栈根据创建套接字时候指定的参数负责填充,显而易见的是,这两部分,开发者是无法实现管理的,此时就有了原始套接字的用武之地,它可以控制传输层首部,也可以控制网络层层首部,给程序员带来了很大的灵活性。
原始套接字编程使用的场合
尽管原始套接字的功能强大,可以构造TCP和UDP的协议数据完成数据传输,但是该套接字类型也有其局限性。在网络层上,原始套接字基于不可靠的IP分组传输服务,与数据报套接字类似,这种服务的特点是无连接、不可靠。无连接的特点决定了原始套接字的传输非常灵活,具有资源消耗小,处理速度快的巨大优点,不可靠也意味着在网络质量不好的情况下,数据包的丢失情况可能会非常严重。结合原始套接字的开发层次和能力,适用于以下场合。
原始套接字的通信过程
(1)基于原始套接字的数据发送过程
在通信过程中,数据发送方根据协议要求,将要发送的数据填充进发送缓冲区,同时给发送数据附加上必要的协议首部,全部填写好后,将数据发送出去。
基本通信过程如下:
1)Windows Sockets DLL 初始化,协商版本号
2)创建套接字,指定使用原始套接字进行通信,根据需要设置IP控制选项
3)指定目的地址和通信端口
4)填充首部和数据
5)发送数据
6)关闭套接字
7)结束对Windows Sockets DLL 的使用,释放资源
(2)基于原始套接字的数据接收过程
在通信过程中,数据接收方设定好接受条件后,从网络中接收到与预设条件相匹配的网络数据后,,如果出现了噪声,对数据进行过滤,然后协商版本号。
1)Windows Sockets DLL 初始化,协商版本号
2)创建套接字,指定使用原始套接字进行通信,并声明特定的协议类型
3)根据需要设定接受选项
4)接收数据
5)过滤数据
6)关闭套接字
7)结束对Windows Sockets DLL 的使用,释放资源
创建原始套接字
创建原始套接字,程序首先要求操作系统创建套接字抽象层的实例,在WinSock2中,完成这个人的函数是socket()和WSASocket()。
socket()
SOCKET WSAAPT socket(
_in int af, //缺顶套接字的通信地址族
_in int type, //指定套接字类型
_in int protocol //指定要使用的特定传输协议
);
WSASocket()
SOCKET WSASocket(
_in int af, //缺顶套接字的通信地址族
_in int type, //指定套接字类型
_in int protocol //指定要使用的特定传输协议
_in LPWSAPROTOCOL_INFO lpProtocolInfo, //与前三个参数互斥使用,是一个指向LPWSAPROTOCOL_INFO结构的指针,该结构定义所创建套接字的特性
_in GROUP g, //标识一个已存在的套接字组ID或指明创建一个新的套接字组
_in DWORD dwFlags //声明一组套接字属性的描述
);
bind()
int bind(
_in SOCKET s, //调用socket()返回的描述符
_in const struct sockaddr *name, //地址参数,被声明为一个指向sockaddr结构的指针
_in int namelen
);
常用协议定义列表
使用原始套接字接收数据
通常使用原始套接字接受数据可以调用recvfrom()或WSARecvFrom()函数实现。在这里我们需要关心两个问题。接收数据的内容和接受数据的类型。
第一步:由步骤①来看,在接收到一个数据包之后,协议栈把满足以下条件的IP数据包传递到套接字实现的原始套接字部分:
1.非UDP分组或TCP分组
2.部分ICMP
3.所有的IGMP分组
4.协议栈不认识其协议字段所有IP数据包
5.重组后的分片数据
第二步:由步骤②来看,当协议栈有一个需传递到原始套接字的IP数据包时,它将检查所有进程的所有打开的原始套接字,寻找满足条件的套接字,如果满足条以下条件,每个匹配的套接字的接收缓冲区中都将受到数据包的一份拷贝:
使用原始套接字接收数据包
在使用原始套接字发送数据是以五连接的方式完成的,创建好原始套接字后可以直接将构造好的数据发送发送出去,但是由于原始套接字工作的层次比数据报套接字更低,在发送内容上有一定的区别。
发送数据的目标
从发送数据的目标来看,原始套接字不存在端口号的概念,对目的地址描述时,端口号是被忽略的,但是任然可以再连接模式和非连接模式两种方式下为套接字管理远端地址。
2.发送数据的内容
从发送数据的内容来看,原始套接字发送内容涉及多种协议首部的构造,对于IPv4或IPv6数据的发送,IP首部控制选项为协议首部填充了两个层次的选择:如果是IPV4,选项为IP_HDRINCL,选项级别为IPPRPOTO_IP;如果是IPv6,选项为IPv6_HDRINCL,选项级别为IPPROTO_IPv6。
源代码如下
#include "winsock2.h"
#include "mstcpip.h"
#include "iostream"
#include<stdlib.h>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
#define DEFAULT_BUFLEN 65535
#define DEFAULT_NAMELEN 512
int main()
{
WSADATA wsaData;
SOCKET SnifferSocket = INVALID_SOCKET;
char recvbuf[DEFAULT_BUFLEN];
int iResult;
int recvbuflen = DEFAULT_BUFLEN;
HOSTENT* local;
char HostName[DEFAULT_NAMELEN];
IN_ADDR addr;
SOCKADDR_IN LocalAddr, RemoteAddr;
int addrlen = sizeof(SOCKADDR_IN);
int in = 0, i = 0;
DWORD dwBufferLen[10];
DWORD Optval = 1;
DWORD dwBytesReturned = 0;
//初始化套接字
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0)
{
cout << "初始化失败:" << iResult << endl;
return 1;
}
//创建套接字
SnifferSocket = socket(AF_INET, SOCK_RAW, IPPROTO_IP);
if (INVALID_SOCKET == SnifferSocket)
{
cout << "创建套接字失败:" << WSAGetLastError() << endl;
WSACleanup();
return 1;
}
//获取本机名称
memset(HostName, 0, DEFAULT_NAMELEN);
iResult = gethostname(HostName, sizeof(HostName));
if (SOCKET_ERROR == iResult)
{
cout << "获取本机名称失败:" << WSAGetLastError() << endl;
WSACleanup();
return 1;
}
//获取本机IP
local = gethostbyname(HostName);
cout << "本机可用的IP地址有:" << endl;
if (NULL == local)
{
cout << "获取IP失败:" << WSAGetLastError() << endl;
WSACleanup();
return 1;
}
while (local->h_addr_list[i] != 0)
{
addr.s_addr = *(u_long*)local->h_addr_list[i++];
cout << "\t" << i << ":\t" << inet_ntoa(addr) << endl;
}
cout << "请选择捕获数据包待使用的接口号:";
cin >> in;
memset(&LocalAddr, 0, sizeof(LocalAddr));
memcpy(&LocalAddr.sin_addr.S_un.S_addr, local->h_addr_list[in - 1], sizeof(LocalAddr.sin_addr.S_un.S_addr));
LocalAddr.sin_family = AF_INET;
LocalAddr.sin_port = 0;
//绑定
iResult = bind(SnifferSocket, (SOCKADDR*)&LocalAddr, sizeof(LocalAddr));
if (SOCKET_ERROR == iResult)
{
cout << "绑定失败:" << WSAGetLastError() << endl;
closesocket(SnifferSocket);
WSACleanup();
return 1;
}
cout << "成功绑定套接字和" << in << "号借口地址";
//设置套接字接受命令
iResult = WSAIoctl(SnifferSocket, SIO_RCVALL, &Optval, sizeof(Optval), &dwBufferLen, sizeof(dwBufferLen), &dwBytesReturned, NULL, NULL);
if (SOCKET_ERROR == iResult)
{
cout << "套接字设置失败:" << WSAGetLastError() << endl;
closesocket(SnifferSocket);
WSACleanup();
return 1;
}
//开始接受数据
cout << "开始接受数据" << endl;
do
{
//接受数据
iResult = recvfrom(SnifferSocket, recvbuf, DEFAULT_BUFLEN, 0, (SOCKADDR*)&RemoteAddr, &addrlen);
if (iResult > 0)
{
cout << "接受来自" << inet_ntoa(RemoteAddr.sin_addr) << "的数据包," << "长度为" << iResult << endl;
}
else
cout << "接受失败:" << WSAGetLastError() << endl;
} while (iResult > 0);
{
}
system("pause");
return 0;
}
8.运行截图如下: