学生姓名 | 张泽凡 | 班级学号 | 1303050326 | |
专 业 | 计算机科学与技术 | 课程设计题目 | 解析IP数据包 | |
评 语 | 组长签字: | |||
成绩 | ||||
日期 | 2014 年12 月29 日 |
学 院 | 信息科学与工程学院 | 专 业 | 计算机科学与技术 | ||
学生姓名 | 张泽凡 | 班级学号 | 1303050326 | ||
课程设计题目 | 解析IP数据包 | ||||
实践教学要求与任务: 任务:解析IP数据包。 要求:1.设计一个解析IP数据包的程序。 2.根据这个程序,说明IP数据包的结构及IP协议的相关问题。 工作计划与进度安排: 计算机网络课程设计时间安排在教学第18周、第19周内完成,按上述题目要求完成查阅资料、程序设计、调试、运行等工作。 时间安排如下: 1、查阅资料、与初步设计;1-2天 2、整体功能设计、代码编码与初步调试。1-3天 3、调试和软件功能的测试和验收。1-3天 4、答辩与论文撰写。1-2天 | |||||
指导教师: 2014年12月 日 | 专业负责人: 2014年12月 日 | 学院教学副院长: 2014年12月 日 |
IP数据包是网络成传输的基本数据单元,熟悉IP数据包结构对于理解网络工作原理具有重要意义。本课程设计的主要目的是通过接受与解析IP数据包,了解IP数据包的基本结构与IP协议的基本功能。
2 课程设计要求
根据后面介绍的IP数据包结构,编写程序接收并解析IP数据包。
1)以命令行形式运行;
ParsePacket log_file
其中,ParsePacket为程序名,log_file为日志文件名。
2)输出内容:IP数据包的各字段值,包括版本、头度、服务类型、总长度、标识、分段标志、分段偏移值、生存时间、上层协议类型、头校验和、源IP地址和目的IP地址等。
3)当程序接收到键盘输入Ctrl+C时退出。
3相关知识
互联网络层是TCP/IP协议参考模型中的关键部分.IP协议把传输层送来的消息组装成IP数据包,并把IP数据包传送给数据链层.IP协议在TCP/IP协议族中处于核心地位,IP协议制定了统一的IP数据包格式,以消除个通信子网中的差异,从而为信息发送方和接收方提供了透明的传输通道.编制本程序前,首先要对IP包的格式有一定了解,图1给出了IP协议的数据包格式.
IP数据包的第一个字段是版本字段,其度是4位,表示所使用的IP协议的版本.目前的版本是IPV4,版本字段的值是4,下一代版本是IPV6,版本字段值是6.本程序主要针对版本是IPV4的数据包的解析.
报头标长字段为4位,它定义了以4B为一个单位的IP包的报文长度.报头中除了选项字段和填充域字段外,其他各字段是定长的.因此,IP数据包的头长度在20—40B之间,是可变的.
0 4 8 16 19 24 31(位)
版本 | 报头标长 | 服务类型 | 总长度 | ||
标识 | 标志 | 片偏移 | |||
生存时间 | 协议 | 头校验和 | |||
源IP地址 | |||||
目的IP地址 | |||||
选项 | 填充域 | ||||
数据部分 |
服务类型字段共8位,用于指示路由器如何处理该数据包.该字段长度由4位服务类型(TOS)子域和3位优先级子域组成,1位为保留位,该字段结构如图2所示.
b7 b6 b5 b4 b3 b2 b1 b0
优先级 | D | T | R | C | 0 |
优先级共有8种,优先级越高表明数据包越重要.表1中列出了各种优先级所代表的意义.
表3.2 优先子域的说明
位数(b7b6b5) 意义 |
111 网络控制 110 网络间控制 101 重要 100 即时,优先 011 即时 010 理科 001 优先 000 普通 |
表3.3列出了服务器类型自域的构成.
位数( b4b3b2b1) | 意义 |
1111 安全级最高 1000 延迟最小 0100 吞吐量最大 0001 金钱成本最小 0000 普通服务 |
标识字段的长度为16位,用于识别IP数据包的编号.每批数据都要有一个标识值,用于让目的主机判断新来的数据属于哪个分组.
报头中的标志字段如图7-3所示.标志字段共3位,最高位是0.禁止分片标志DF(do not fragment)字段的值若为1,表示不能对数据包分片;若DF值为0,则表明可以分片.分片标志MF( more fragment)的值为1,表示接收到的不是最后一个分片;若MF值为0,表示接收到的是最后一个分片.
片偏移字段共13位,说明分片在整个数据包中的相对位置.片偏移值是以8B为单位来记数的,因此选择的分片长度应该是8B的整数倍.
生存时间(TTL)字段为8位,用来设置数据包在互联网络的传输过程的寿命,通常是用一个数据包可以经过的最多的路由器跳步数来限定的.
协议字段为8位,表示使用此IP数据包的高层协议类型,常用的协议号如表7-3所示.
序号 | 协议名称 | 序号 | 协议名称 |
1 2 4 6 8 | ICMP IGMP IP inIP TCP EGP | 17 41 46 | UDP IPV6 RSVP OSPFA |
头校验和字段为16位,用于存放检查报头错误的校验码。检验的范围是整个IP包的报头。校验和按如下方法计算:
1)将头校验和的字段置为0。
2)将报头部分的所有数据以16位为单位进行累加,累加方式是求异或。
3)将累加的结果取反码,就是头校验和。
当收到一个IP包时,要检查报头是否出错,就把报头中的所有数据以16位为单位进行累加,若累加的结果为0,则报文没有出错。
地址字段包括源地址和目的地址。源地址和目的地址的长度都是32位,分别表示发送数据包的源主机和目的主机的IP地址。
选项字段的长度范围为0~40B,主要用于控制和测试。在使用选项字段的过程中,有可能出现报头部分的长度不是32位的整数倍的情况。如果出现这种情况,就需要通过填充位来凑齐。
4课程设计分析
为了获取网络中的IP数据包,必须对网卡进行编程,在这里我们使用套接字(socket)进行编程。但是,在通常情况下,网络通信的套接字程序只能响应与自己硬件地址相匹配的数据包或是以广播形式出发的数据包。对于其他形式的数据包,如已到达网络接口但却不是发送到此地址的数据包,网络接口在验证投递地址并非自身地址之后将不引起响应,也就是说应用程序无法收取与自己无关的数据包。我们要想获取流经网络设备的所有数据包,就需要将网卡设置为混杂模式。
本程序主要由三部分构成:初始化原始套接字,反复监听捕获数据包和解析数据包。下面就结合核心代码对程序的具体实现进行讲解,同时使程序流程更加清晰,去掉了错误检查等保护性代码。
4.1使用原始套接字
套接字分为三种,即流套接字(Stream Socket)、数据报套接字(Datagram Socket)和原始套接字(Raw Socket)。要进行IP数据包的接受与发送,应使用原始套接字。创建原始套接字的代码如下:
SOCKET sock
Sock=WSASoccet(AF_INET,SOCK_RAW,IPPROTO_IP,NULL,0,WSA_FLAG_OVERRLAPPED);
在WSASoccet函数中,第一个参数指定通信发生的区字段,AF_INET是针对Internet的,允许在远程主机之间通信。第二个参数是套接字的类型,AF_INET地址族下,有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW三种套接字类型。在这里,我们设置为SOCK_RAW,表示我们声明的是一个原始套接字类型。第三个参数依赖于第二个参数,用于指定套接字所用的特定协议,这里使用IP协议。第四个参数为WSAPROTOCOL_INFO位,该位可以置空,永远置0。第六个参数是标志位,WSA_FLAG_OVERRLAPPED表明可以使用发送接收超时设置,本课程设计也可以把这个标志位设置为NULL,因为本设计不用考虑超时情况。
创建原始套接字后,IP头就会包含在接收的数据中。然后,我们可以设置IP头操作选项,调用sotscockpot函数。其中flag设置为TRUE,并设定IP_HDRINCL选项,表明用户可以亲自对IP头进行处理。
BOOL flag=true;
setsockopt (sock,IPPROTO_IP,IP_HDRINCL,(char*)&flag,sizeof(flag));
之后,使用如下代码完成对socket的初始化工作
/*获取主机名*/
char hostname[128];
gethostname(hostname, 100);
/*获取IP地址*/
hostent *pHostIP;
pHostIP=gethostbyname(hostname);
/* 填充SOCKADDR_IN的结构内容*/
sockaddr_in addr_in;
addr_in.sin_addr= *(in_addr*)pHostIP->h_addr_list[0];
addr_in.sin_family=AF_TNET;
addr-in.sin_port=htons(6000);
/* 绑定socket */
bind(sock, (POSCKADDR)&addr_in,sizeof(addr_in));
填写sockaddr_in的内容时,其地址值应填写为本机IP地址可以通过gethostbyname()函数获取;端口号可以随便填写,但不能与系统冲突;协议族应填写为AF_INET。注意,sockaddr_in 结构的值必须是以网络字节顺序表示的值,而不能直接使用本机字节顺序的值,使用htoms()函数可以将无符号短整型的主机数据转换为网络字节的顺序的数据。最后使用bind()函数将socket绑定到本地网卡上。
绑定网卡后,需要WSAIoctl()函数把网卡设置为混杂模式,使网卡能够接收所有网络数据,其关键代码如下:
#define SIO_RCVALL_WSAIOW(IOC_VENDOR,1)
DWORD dwBufferLen[10];
DWORD dwBufferInLen=1;
DWORD dwBytesReturned=0;
WSAIoctl(SnifferSocket,IO-RCVALL,&dwBufferInLen,sizeof(dwBufferInLen),&dwBufferLen,Sizeof(dwBufferLen),&dwByteReturned,NULL,NULL);
如果接收的数据包中的协议类型和定义的原始套接字匹配,那么接收到的数据就拷贝到套接字中。因此,网卡就可以接收所有经过的IP包。
4.2接收数据包
在程序中可使用RECV()函数接收经过的IP包。该函数有四个参数,第一个参数接收操作所用的套接字描述符;第二个参数接收到缓冲区的地址;第二个参数接收缓冲区的地址;第三个参数接收缓冲区的大小,也就是所要接收的字节数;第四个参数是一个附加标志,如果对所发送的数据没特殊要求,直接设为0。因为IP数据包的最大长度是65536B,因此缓冲区的大小不能小于65535B。设置缓冲区后,可利用循环来反复监听接收IP包,用recv()函数接收功能的代码如下:
#dedine BUFFER_SIZE 65535
Char buffer[BUFFER_SIZE]; //设置缓冲区
While(true)
{recv(sock,buffer,BUFFER_SIZE,0); //j接收数据包
4.3定义IP头部的数据结构
程序需要定义一个数据结构表示IP头部。这个数据结构应该和图7-1吻合,其代码如下:
typedef struct _IP_HEADER //定义IP头
{
union
{
BYTE Version; //版本前4位
BYTE HdrLen; //报头标长(后四位),IP头长度
};
BYTE ServiceType;//服务类型
WORD TotalLen; //总长度
WORD ID; //标识
union
{
WORD Flags; //标志
Word FragOff; //分段偏移
};
BYTE TimeToLive; //生命期
BYTE Protiocol; //协议
WORD HdrChksum; //头校验和
DWORD SrcAddr; //源地址
DWORD DstAddr: //目的地址
BYTE Options; //选项
}IP_HEADER;
这是我们只考虑IP头部结构,不考虑数据部分。在捕获IP数据包后,可以通过指针把缓冲区的内容强制转化为IP_HEADER数据结构。
IP_HEADER ip = *( IP_HEADER *)buffer;
4.4 IP包的解析
解析IP包的字段有两种策略。针对长度为8位、16位和32位的字段 (或子字段)时,可以利用IP_HEADER的成员指教获取。要解析长度不是9位倍数的字段(或子字段)时,可以利用C语言中的位移以及与、或操作完成。下面给出了通过IP_HEADER解析IP头各个字段的代码。
/*获取版本字段*/
ip.Version>>4;
/*获取头度字段*/
ip.HdrLen & 0x0f;
/*获取服务类型字段中的优先级子域*/
ip.ServiceType>>5;
/*获取服务类型字段中的TOS子域*/
(IP.sERVICEtYPE>>1)&0X0F;
/*获取总长度字段*/
ip.TotalLEN;
/*获取标识字段*/
ip.ID;
/*解析标识字段*/
DF=(ip.Flags>>14) &0x01;
MF=(ip.Flags>>13) &0X01;
/*获取分段偏移字段*/
ip.FragOff &0x1fff;
/*获取生存时间字段*/
ip.TimeToLive;
/*获取协议字段*/
ip.Protocol;
/*获取头校验和字段*/
ip.HdrChksum;
/*解析源IP地址字段*/
inet_ntoa(*(in_addr*)&ip.SrcAddr;
/*解析目的的IP地址字段*/
inet_ntoa(*(in_addr*)&ip.DstAddr);
5程序代码
#include "stdafx.h"
#include "winsock2.h"
#include "ws2tcpip.h"
#include "stdio.h"
typedef struct _IP_HEADER
{
union
{
BYTE Version;
BYTE HdrLen;
};
BYTE ServiceType;
WORD TotalLen;
WORD ID;
union
{
WORD Flags;
WORD Fragoff;
};
BYTE TimeToLive;
BYTE Protocol;
WORD HdrChksum;
DWORD SrcAddr;
DWORD DstAddr;
BYTE Options;
}IP_HEADER;
void getVersion(BYTE b,BYTE & version)
{
version =b>>4;
}
void getIHL(BYTE b,BYTE & result)
{
result = (b & 0x0f) *4;
}
char * parseServiceType_getProcedence(BYTE b)
{
switch(b>>5)
{
case 7:
return "Network Control";
break;
case 6:
return "Internet work Control";
break;
case 5:
return "CRITIC/ECP";
break;
case 4:
return "Flash Override";
break;
case 3:
return "Falsh";
break;
case 2:
return "Immediate";
break;
case 1:
return "Priority";
break;
case 0:
return "Routine";
break;
default :
return "Unknown"
}
}
char * parseServiceType_getTOS(BYTE b)
{
b=(b>>1)&0x0f;
switch(b)
{
case 0:
return "Normal service";
break;
case 1:
return "Minimize monetary cost";
break;
case 2:
return "Maximize reliability";
break;
case 4:
return "Maximize throughput";
break;
case 8:
return "Minimize delay";
break;
case 15:
return "Maximize security";
break;
default:
return "Unknown";
}
}
void getFlags(WORD w,BYTE & DF, BYTE & MF)
{
DF=(w>>14)&0x01;
MF=(w>>13)&0x01;
}
void getFragoff(WORD w,WORD & fragoff)
{
fragoff=w&0x1ffff;
}
char * getProtocol(BYTE Protocol)
{
switch (Protocol)
{
case 1:
return "ICMP";
case 2:
return "IGMP";
case 4:
return "IP in IP ";
case 6:
return "TCP";
case 8:
return "EGP";
case 17:
return "UPD";
case 41:
return "IPv6";
case 46:
return "OSPF";
default:
return "UNKNOWN";
}
}
void ipparse(FILE * file,char *buffer)
{
IP_HEADER ip=*(IP_HEADER *)buffer;
fseek(file,0,SEEK_END);
BYTE version;
getVersion(ip.Version,version);
fprintf(file,"版本=%d\\r\\n",version);
BYTE headerLen;
getIHL(ip.HdrLen,headerLen);
fprintf(file,"头长度=%d(BYTE)\\r\\n",headerLen);
fprintf(file,"服务类型=%s,%s\\r\\n");
parseServiceType_getProcedence(ip.ServiceType);
parseServiceType_getTOS(ip.ServiceType);
fprintf(file,"数据报长度=%d(BYTE)\\r\\n",ip.TotalLen);
fprintf(file,"数据报ID=%d\\r\\n",ip.ID);
BYTE DF,MF;
getFlags(ip.Flags,DF,MF);
fprintf(file,"分段标志 DF=%d,MF=%d\\r\\n",DF,MF);
WORD fragOff;
getFragOff(ip.FragOff,fragOff);
fprintf(file,"分段偏移值=%d\\r\\n",fragOff);
fprintf(file," 生存期=%d(hops)\\r\\n",ip.TimeToLive);
fprintf(file,"协议=%s\\r\\n",getProtocol(ip.Protocol));
fprintf(file,"头校验和=0x%0x\\r\\n",ip.HdrChksum);
fprintf(file,"源IP地址=%s\\r\\n",inet-ntoa(*(in-addr*)&ip.SrcAddr));
fprintf(file,"目的IP地址=%s\\r\\n",inet_ntoa(*(in-addr*)&ip.DstAddr));
fprintf(file,"__________________________________\\r\\n");
}
int main(int argc,char *argv[])
{
if(argc!=2)
{
printf("usage error!\\n");
return -1;
}
FILE * file;
if((file=foopen(argv[1],"wb+"))==NULL)
{
printf("fail to open file %s",argv{1});
return -1;
}
WSADATA wsData;
if(WSAStartup(MAKEWORD(2,2),$WSdATA)!=0)
{
PRINTF("WSAStartup FAILED!\\n");
return -1;
}
SOCKET sock;
if((sock=socket(AF_INET,SOCK_RAW,ippROTO_IP))==INVALID_SOCKET)
{
PRINTF("CREATE socket failed!\\n");
return -1;
}
BOOL flag=TRUE;
IF(setsockopt(sock,IPPROTO_IP,IP_HDRINCL,(CHAR*)&FLAG,sizeof(flag))==SOCKET_ERROR)
{
printf("setsockopt failed!\\n");
return -1;
}
char hostName[128];
if(gethostname(hostName,100)==SOCKET_ERROR)
{
printf("gethostname failed!\\n");
return -1;
}
hostent * pHostIP;
if(pHostIP=gethostbyname(hostName))==NULL)
{
printf("gethostbyname failed!\\n");
return -1;
}
sockaddr_in addr_in;
addr_in.sin_addr=*(in_addr*)pHostIP->h_addr_list[0];
addr_in.sin_family=AF_INET;
addr_in.sin_port=htone(6000);
if(bind(sock,(PSOCKADDR)&addr_in,sizeof(addr_in))==SOCKET_ERROR)
{
printf("bind failed");
return -1;
}
DWORD dwValue=1;
#define IO_RCVALL_WSAIOW(IOC_VENDOR,1)
DWORD dwBufferLen[10];
DWORD dwBufferInLen=1;
DWORD dwBytesReturned=0;
if(WSAIoctl(sock,IO_RCVALL,&dwBufferInLen,sizeof(dwBufferInLen),
&dwBufferLen,sizeof(dwBufferLen),&dwBytesReturned,NULL,NULL)==
NULL,NULL)==SOCKET_ERROR)
{
printf("icotlsocket failed\\n");
return -1;
}
#define BUFFER_SIZE 65535
char buffer[BUFFER_SIZE];
printf("开始解析\\n\\n");
while(true)
{
int size=recv(sock,buffer,BUFFER_SIZE,0);
if(size>0)
{
ipparse(stdout,buffer);
ipparse(filebuffer);
}
}
fclose(file);
return 0:
}
6运行结果与分析
本程序设计也可以利用Winpcap完成,部分核心代码的简略提示如下。
获取所有存在的网络设备的链表。
Pcap_if_t *alldevs; //网络设备结构链表
Char errbuf[PCAP_ERRBUF_SIZE]; //错误信息
/*所有网络设备的信息以链表形式存在alldevs中*/
pcap_findalldevs(&alldevs,errbuf);
从链表中选择物理Ethernet卡后,用混杂模式打开,调用的函数为pcap_open_live(const char*device,int snaplen,int promisc,int to_ms,char*errbuf).在这个函数中,第一个参数为要打开的设备名称,这里是Ethernet卡,可以从设备链表alldevs中选出。第二个参数应为捕获的数据包长度,填入65535以保证在链路层的整个数据包都被捕获。第三个参数为打开模式,填入1表明用混杂模式打开网卡。最后两个参数本别为读入超时的时间和保存错误信息。
编译、设置过滤器。
char packet_filter[]=”ip”;
pcap_compile(adhandle,&fcode,packet_filter,1,netmask);//编译过滤器
pcap_setfilter(adhandle,&fcode); //设置过滤器
adhandle参数为网卡描述符,fcode参数是一个BPF伪汇编程序,packet_filter参数用于设置的过滤规则,在这里我们只需要捕获IP包。
可利用pcap_loop函数捕获数据包。对于捕获的数据包,去掉数据链路层的14B的头部后才是真正的IP包信息。利用winpcap编程和利用socket编程在处理IP包上并没有太大区别,我们依然可以使用7。4节中采用的数据结构IP——HEADER来保存、解析IP头部信息。
7参考文献
[1]谢希仁 编著. 计算机网络 (第6版) .北京:电子工业出版社,2013.6
[2]吴宜功 吴英 编著. 计算机网络课程设计 (第2版). 北京: 机械工业出版社,2012