在linux下实现的一个数据包监控程序(C语言),此程序主要包含3个主要模块:底层模块、中层模块和上层统计模块。其中底层模块是数据包的捕获过程; 中层模块包括MAC 层处理模块、IP 层处理模块、TCP 处理模块、UCP 处理模块和ICMP 处理模块; 上层统计处理模块包括数据包的统计模块、数据包协议统计模块、网络网元发现模、数据包构造模块和数据包过滤模块。
代码参考自《TCP/IP网络实验程序篇》,并在其基础上进行了一定改进。
对数据包进行拆解:
void print_ethernet(struct ether_header *eth) //显示ethernet报头
{
int type = ntohs(eth->ether_type); //将一个16位数由网络字节顺序转换为主机字节顺序
if (type <= 1500) //<1500为802.3帧
printf("IEEE 802.3 Ethernet Frame:\n");
else //>1500为以太网帧
printf("Ethernet Frame:\n");
printf("+-------------------------+-------------------------"
"+-------------------------+\n");
printf("| Destination MAC Address: "
" %17s|\n", mac_ntoa(eth->ether_dhost)); //占17位
printf("+-------------------------+-------------------------"
"+-------------------------+\n");
printf("| Source MAC Address: "
" %17s|\n", mac_ntoa(eth->ether_shost));
printf("+-------------------------+-------------------------"
"+-------------------------+\n");
if (type < 1500)
printf("| Length: %5u|\n", type); //十进制无符号整数
else
printf("| Ethernet Type: 0x%04x|\n", type); //以十六进制形式输出,占宽4位右对齐,不足4位的前面用0补齐
printf("+-------------------------+\n");
}
htonl() htons() ntohl() ntohs()比较:
#include <arpa/inet.h>(头文件)uint32_t htonl(uint32_t hostlong);表示将16位的主机字节顺序转化为16位的网络字节顺序。
uint16_t htons(uint16_t hostshort);将主机的无符号短整形数转换成网络字节顺序,返回一个网络字节顺序的值。
uint32_t ntohl(uint32_t netlong);表示将32位的主机字节顺序转化为32位的网络字节顺序。
uint16_t ntohs(uint16_t netshort);将一个无符号短整形数从网络字节顺序转换为主机字节顺序,返回一个以主机字节顺序表达的数。
区分帧根据源地址段后的前两个字节的类型不同。 如果值大于 1500(0x05DC),说明是以太网类型字段,EthernetII 帧格式。值小于等于 1500,说明是长 度字段,IEEE802.3 帧格式,该类型字段值最小的是 0x0600。而长度最大为 1500。以太网的帧的格式与标准和802.3标准的格式分别如下:
以太头定义在#include <netinet/if_ether.h>:
- typedef struct ether_header
- {
- u_char ether_dhost[ETHER_ADDR_LEN]; // Destination MAC address
- u_char ether_shost[ETHER_ADDR_LEN]; // Source MAC address
- u_short ether_type; // Protocol type
- }
/*****显示一个arp包*****/
void print_arp(struct ether_arp *arp)
{
static char *arp_op_name[] = {
"Undefine",
"(ARP Request)",
"(ARP Reply)",
"(RARP Request)",
"(RARP Reply)"
}; //显示可选域种类的字符串
#define ARP_OP_MAX (sizeof arp_op_name / sizeof arp_op_name[0])
int op = ntohs(arp->ea_hdr.ar_op); //ARP操作码
if (op < 0 || ARP_OP_MAX < op) //如果操作码为负或大于选择域,赋值为0(未定义)
op = 0;
printf("Protocol: ARP\n");
printf("+-------------------------+-------------------------+\n");
printf("| Hard Type: %2u%-11s| Protocol:0x%04x%-9s|\n",
ntohs(arp->ea_hdr.ar_hrd), 硬件类型
(ntohs(arp->ea_hdr.ar_hrd)==ARPHRD_ETHER)?"(Ethernet)":"(Not Ether)",
ntohs(arp->ea_hdr.ar_pro), //协议类型
(ntohs(arp->ea_hdr.ar_pro)==ETHERTYPE_IP)?"(IP)":"(Not IP)");
printf("+------------+------------+-------------------------+\n");
printf("| HardLen:%3u| Addr Len:%2u| OP: %4d%16s|\n", //硬件地址长度,协议地址长度,ARP操作码
arp->ea_hdr.ar_hln, arp->ea_hdr.ar_pln, ntohs(arp->ea_hdr.ar_op),
arp_op_name[op]);
printf("+------------+------------+-------------------------"
"+-------------------------+\n");
printf("| Source MAC Address: "
" %17s|\n", mac_ntoa(arp->arp_sha)); //源MAC地址
printf("+---------------------------------------------------"
"+-------------------------+\n");
printf("| Source IP Address: %15s|\n",
inet_ntoa(*(struct in_addr *) &arp->arp_spa)); //源IP地址
printf("+---------------------------------------------------"
"+-------------------------+\n");
printf("| Destination MAC Address: "
" %17s|\n", mac_ntoa(arp->arp_tha)); //目的MAC地址
printf("+---------------------------------------------------"
"+-------------------------+\n");
printf("| Destination IP Address: %15s|\n",
inet_ntoa(*(struct in_addr *) &arp->arp_tpa)); //目的IP地址
printf("+---------------------------------------------------+\n");
}
ether_arp定义在#include <netinet/if_ether.h>:
- typedef struct ether_arp
- {
- struct arphdr ea_hdr; //Fixed-size header
- u_char arp_sha[6]; //Source hardware address
- u_char arp_spa; //Source protocol address
- u_char arp_tha[6]; //Target hardware address
- u_char arp_tpa; //Target protocol address
- }ETH_ARP;
- typedef struct arphdr
- {
- u_short ar_hrd; //Format of hardware address
- u_short ar_pro; //Format of protocol address
- u_char ar_hln; //Length of hardware address
- u_char ar_pln; //Length of protocol address
- u_short ar_op; //ARP operation.
- }
/*****显示IP报头*****/
void print_ip(struct ip *ip)
{
char protocol[5];
switch(ip->ip_p) //ip协议,比较常用的协议号
{
case 17:strncpy (protocol,"UDP",5);break;
case 6:strncpy (protocol,"TCP",5);break;
case 1:strncpy (protocol,"ICMP",5);break;
case 2:strncpy (protocol,"IGMP",5);break;
case 88:strncpy (protocol,"IGRP",5);break;
case 89:strncpy (protocol,"OSPF",5);break;
default:sprintf(protocol,"%d",ip->ip_p);break;//itoa(ip->ip_p,protocol,5)
}
printf("Protocol: IP\n");
printf("+--------+------+------------+----------------------+\n");
printf("| IV:IPv%1u| HL:%2u| T: %8s| Total Length: %7u|\n",
ip->ip_v, ip->ip_hl, ip_ttoa(ip->ip_tos), ntohs(ip->ip_len)); //版本,包头长度,服务类型,总长度
printf("+-----+------+------------+-------+-----------------+\n");
printf("| Identifier: %5u| FF:%3s| FO: %5u|\n",
ntohs(ip->ip_id), ip_ftoa(ntohs(ip->ip_off)), //标识符,偏移量
ntohs(ip->ip_off) &IP_OFFMASK); //===============================??????
printf("+------------+------------+-------+-----------------+\n");
printf("| TTL: %3u| Pro: %3s| Header Checksum: %5u|\n",
ip->ip_ttl, protocol, ntohs(ip->ip_sum)); //生存时间,协议,头部校验
printf("+------------+------------+-------------------------+\n");
printf("| Source IP Address: %15s|\n",
inet_ntoa(*(struct in_addr *) &(ip->ip_src))); //源ip地址
printf("+---------------------------------------------------+\n");
printf("| Destination IP Address: %15s|\n",
inet_ntoa(*(struct in_addr *) &(ip->ip_dst))); //目的ip地址
printf("+---------------------------------------------------+\n");
}
ip报头定义在#include <ip.h>:
- typedef struct ip
- {
- u_int ip_v:4; //version(版本)
- u_int ip_hl:4; //header length(包头长度)
- u_char ip_tos; //服务类型
- u_short ip_len; //总长度
- u_short ip_id; //标识符
- u_short ip_off; //偏移量
- u_char ip_ttl; //生存时间
- u_char ip_p; //协议
- u_short ip_sum; //首部校验和
- struct in_addr ip_src; //源ip地址
- struct in_addr ip_dst; //目的ip地址
- }
PPP 定义包的优先级:000 普通 (Routine);001 优先的 (Priority);010 立即的发送 (Immediate);011 闪电式的 (Flash);100 比闪电还闪电式的 (Flash Override);
D 时延: 0:普通 1:尽量小
T 吞吐量: 0:普通 1:尽量大
R 可靠性: 0:普通 1:尽量大
C 传输成本: 0:普通 1:尽量小
0 最后一位被保留,恒定为0
/*****将ip头的服务类型(tos)域变为字符串*****/ char *ip_ttoa(int flag) { static int f[] = {'1', '1', '1', 'D', 'T', 'R', 'C', 'X'}; #define TOS_MAX (sizeof f / sizeof f[0]) static char str[TOS_MAX + 1]; unsigned int mask = 0x80; int i; for (i = 0; i < TOS_MAX; i++) { if (((flag << i) & mask) != 0) str[i] = f[i]; else str[i] = '0'; } str[i] = '\0'; return str; }
标识符(Identifier):长度16比特。该字段和Flags和Fragment Offest字段联合使用,对大的上层数据包进行分段(fragment)操作。路由器将一个包拆分后,所有拆分开的小包被标记相同的值,以便目的端设备能够区分哪个包属于被拆分开的包的一部分。
标记(Flags):长度3比特。该字段第一位不使用。第二位是DF(Don't Fragment)位,DF位设为1时表明路由器不能对数据包分片。如果一个数据包无法在不分片的情况下进行转发,则路由器会丢弃该数据包并返回一个错误信息。第三位是MF(More Fragments)位,当路由器对一个数据包分片,则路由器会在除了最后一个分段的IP包的包头中将MF位设为1来表明后面还有分片。上述代码对标识符的转换如下:
/*****将ip标识符变为字符串*****/ char *ip_ftoa(int flag) { static int f[] = {'R', 'D', 'M'}; //显示段标志的字符 #define IP_FLG_MAX (sizeof f / sizeof f[0]) static char str[IP_FLG_MAX + 1]; //存储返回值的缓冲区 unsigned int mask = 0x8000; //掩码 int i; //循环变量 for (i = 0; i < IP_FLG_MAX; i++) { if (((flag << i) & mask) != 0) str[i] = f[i]; else str[i] = '0'; } str[i] = '\0'; return str; }
头部校验(Header Checksum):长度16位。用来做IP头部的正确性检测,但不包含数据部分。 因为每个路由器要改变TTL的值,所以路由器会为每个通过的数据包重新计算这个值。
struct icmp { u_char icmp_type; /* type of message*/ u_char icmp_code; /* type sub code(报文类型子码) */ u_short icmp_cksum; /* 校验和 */ }各类型的icmp报文及其代码如图:
struct tcphdr
{
u_int16_t th_sport; /* 源端口 */
u_int16_t th_dport; /* 目的端口 */
tcp_seq th_seq; /* 序列号 */
tcp_seq th_ack; /* ack确认号 */
u_int8_t th_off:4; /* 数据偏移量 */
u_int8_t th_flags; /*标志位*/
u_int16_t th_win; /* 窗口大小 */
u_int16_t th_sum; /* 校验和 */
u_int16_t th_urp; /* 紧急数据偏移量 */
};
ACK:确认标志 大多数情况下该标志位是置位的。TCP报头内的确认编号栏内包含的确认编号为下一个预期的序列编号,同时提示远端系统已经成功接收所有数据。
PSH:推标志 该标志置位时,数据包到达接收端以后,立即传送给应用程序,而不是在缓冲区中排队。
RST:复位标志 复位标志有效。用来复位那些产生错误的连接,也被用来拒绝错误和非法的数据包。
SYN:同步标志 该标志仅在三次握手建立TCP连接时有效,用来建立连接。
FIN:结束标志 表示发送端已经达到数据末尾,也就是说双方的数据传送完成,没有数据可以传送了,发送FIN标志位的TCP数据包后,连接将被断开。
代码中对标志位的显示代码如下:
udp类似,其头部结构如下(代码类似就不赘述):char *tcp_ftoa(int flag) { static int f[] = {'U', 'A', 'P', 'R', 'S', 'F'}; #define TCP_FLG_MAX (sizeof f / sizeof f[0]) static char str[TCP_FLG_MAX + 1]; unsigned int mask = 1 << (TCP_FLG_MAX - 1); int i; for (i = 0; i < TCP_FLG_MAX; i++) { if (((flag << i) & mask) != 0) str[i] = f[i]; else str[i] = '0'; } str[i] = '\0'; return str; }
typedef struct udphdr {
uint16_t uh_sport; //源端口
uint16_t uh_dport; //目的端口
uint16_t uh_ulen; //数据包长度
uint16_t uh_sum; //检验和
}
#ifndef __linux
/*打开bpf*/
int open_bpf(char *ifname, int *bufsize)
{
char buf[CMAX];
int bpfd;
struct ifreq ifr;
int i;
for (i = 0; i < 4; i++) {
snprintf(buf, CMAX, "/dev/bpf%d", i);
if ((bpfd = open(buf, O_RDWR, 0)) > 0)
break;
}
if (i >= 5) {
fprintf(stderr, "cannot open BPF\n");
return -1;
}
*bufsize = MAXSIZE;
if (ioctl(bpfd, BIOCSBLEN, bufsize) < 0) { //定义缓冲区长度
snprintf(buf, CMAX, "ioctl(BIOCSBLEN, %d)", *bufsize);
perror(buf);
return -1;
}
snprintf(ifr.ifr_name, CMAX, "%s", ifname);
if (ioctl(bpfd, BIOCSETIF, &ifr) < 0) { //定义和文件关联的硬件接口
snprintf(buf, CMAX, "ioctl(BIOCSETIF, '%s')", ifname);
perror(buf);
return -1;
}
fprintf(stderr, "BPF read from '%s' (%s)\n", ifr.ifr_name, buf);
if (ioctl(bpfd, BIOCPROMISC, NULL) < 0) { //设置混杂模式
perror("ioctl(BIOCPROMISC)");
return -1;
}
i = 1;
if (ioctl(bpfd, BIOCIMMEDIATE, &i) < 0) { //启用"immediate mode"
perror("ioctl(BIOCIMMEDIATE)");
return -1;
}
return bpfd;
}
#endif
bpf是 伯克利封包过滤器(Berkeley Packet Filter,缩写 BPF),是类Unix系统上数据链路层的一种原始接口,提供原始链路层封包的收发,除此之外,如果网卡驱动支持洪泛模式,那么它可以让网卡处于此种模式,这样可以收到网络上的所有包,不管他们的目的地是不是所在主机。