在Linux下数据链路层对的访问通常是通过编写内核驱动程序来实现的,在应用层使用SOCKE_PACKET类型的协议族可以实现部分功能。
SOCK_PACKET类型建立套接字的时候选择SOCK_PACKET类型,内核将不对网络数据进行处理而直接交给用户,数据直接从网卡的协议栈交给用户。建立一个SOCK_PACKET类型的套接字使用方式如下:
socket(AF_INET, SOCK_PACKET, htons(0x0003));
其中AF_INET表示因特网协议族,SOCK_PACKET表示截取数据帧的层次在物理层,网络协议栈对数据不做处理。值0x0003表示截取的数据帧的类型为不确定,处理所有的包。
使用SOCK_PACKET进行程序设计的时候,需要注意的主要方面包括协议族选择、获取原始包、定位IP包、定位TCP包、定位UDP包、定位应用层数据几个部分,下面进行详细介绍。
设置套接口以捕获链路帧的编程方法
在Linux 下编写网络监听程序,比较简单的方法是在超级用户模式下,利用类型为SOCK_PACKET的套接口(用socket()函数创建)来捕获链路帧数据。Linux 程序中需引用如下头文件件:
#include <sys/socket.h>
#include <sys/ioctl.h> /*ioctl 命令*/
#include <netinet/if_ether.h> /*ethhdr 结构*/
#include <net/if.h> /*ifreq 结构*/
#include <netinet/in.h> /*in_addr 结构*/
#include <netinet/ip.h> /*iphdr 结构*/
#include <netinet/udp.h> /*udphdr 结构*/
#include <netinet/tcp.h> /*tcphdr 结构*/
建立SOCK_PACKET 类型套接字,监听所有类型的包,采用如下代码:
int fd;
fd = socket(AF_INET, SOCK_PACKET, htons(0x0003));
侦听其他主机网络的数据在局域网诊断中经常使用。如果监听其他网卡的数据,需要将本地的网卡设置为“混杂”模式,还需要一个都连接于同一HUB 的局域网或者具有“镜像”功能的交换机才可以,否则,只能接收到其他主机的广播包。
char *ethname = “eth0”; /*对网卡eth0进行混杂设置*/
struct ifreq ifr; /*网络接口结构*/
strcpy(ifr.ifr_name, ethname); /*eth0 写入ifr结构的一个字段中*/
i = ioctl(fd, SIOCGIFFLAGS, &ifr);
if(i<0){
close(fd);
perror(“can’t get flags \n”);
return -1;
}
ifr.ifr_flags |= IFF_PROMISC; /*保留原来的设置的情况下,在标志位中加入“混杂”方式*/
i = ioctl(fd, SIOCSIFFLAGS, &ifr);
if(i<0){
perror(“promiscuous set error\n”);
return -2;
}
上面的代码使用了ioctl()的SIOCGIFFLAGS 和 SIOCSIFFLAGS命令,用来取出和写入网络接口的标志设置。注意,在修改网络接口标志的时候,务必要先将之前的标志取出,与想要设置的位进行“位或”计算后再写入,不要直接将设置的位值写入,因为直接写入覆盖之前的设置,造成网络接口混乱。
从套接口读取链路帧的编程方法
以太网的数据结构如下图,总长度为1518字节,最小为64字节,其中目标地址的MAC为6字节,源地址的MAC为6字节,协议类型为2字节,含有46-1500字节的数据,尾部为4字节的CRC校验和,以太网的CRC校验和一般由硬件自动设置或剥离,应用层不用考虑。
目标地址 6字节 |
源地址 6字节 |
类型 2字节 |
帧数据 46-1500 |
校验和 4字节 |
在头文件<netinet/if_ether.h>中定义了如下常量:
#define ETH_ALEN 6
#define ETH_HLEN 14
#define ETH_ZLEN 60
#define ETH_DATA_LEN 1500
#define ETH_PARME_LEN 1514
以太网头部结构定义如下:
struct ethhdr{
unsigned char h_dest[ETH_ALEN]; /*目的以太网地址*/
unsigned char h_source[ETH_ALEN]; /*源以太网地址*/
__be16 h_proto; /*包类型*/
};
套接字文件描述符建立后,就可以从描述符中读取数据,数据的格式为上述的以太网数据,即以太网帧。套接口建立以后,就可以从中循环捕获的链路层以太网帧。要建立一个大小为ETH_FRAME_LEN 的缓冲区,并将以太网的头部指向此缓冲区,例如:
char ef[ETH_FRAME_LEN]; /*以太网缓存区*/
struct ethhdr *p_ethhdr; /*以太网头部*/
int n;
p_ethhdr = (struct ethhdr*)ef; /*使p_ethhdr指向以太网的帧头*/
n = read(fd, ef, ETH_FRAME_LEN); /*读取以太网数据,n为返回的实际捕获的以太网帧长*
接收到数据以后,缓冲区ef和以太网头部的对应关系如下:
h_dest | h_source | h_proto | |
6字节 | 6字节 | 2字节 | |
ef |
因此,获得以太网的目的MAC地址,源MAC地址和协议的类型,可通过p_ethhdr->h_dest, p_ethhdr->h_source, p_ethhdr->h_proto 得到。
定位IP包头的编程方法
获得以太网帧后,当协议为0x0800,其负载部分为IP协议。IP协议的数据结构如下:
struct iphdr{
#if defined(__LITTLE_ENDIAN_BITFIELD) /*小端*/
_u8 ihl:4,
version:4;
#endif defined(__BIG_ENDIAN_BITFIELD) /*大端*/
#else
#error “please fix <asm/byteorder.h>”
#endif
__u8 tos; /*服务类型*/
__be16 tot_len; /*总长度*/
__be16 id; /*标志*/
__be16 frag_off; /*片偏移*/
__u8 ttl; /*生存时间*/
__u8 protocol; /*协议类型*/
__u16 check; /头部校验和*/
__be32 saddr; /*源IP*/
__be32 daddr; /*目的IP*/
};
若捕获的以太网帧中h_proto的值为0x0800,将类型为iphdr的结构指针指向帧头后面载荷数据的起始位置,则可以得到IP数据包的报头部分,通过saddr 和 daddr可以得到IP报文的源IP地址和目的IP地址。下面的代码打印IP报文的源IP地址和目的IP地址。
if(ntohs(p_ethhdr->h_proto) == 0x0800){
struct iphdr *p_iphdr = (struct iphdr*)(ef + ETH_HLEN);
printf(“src ip:%s\n”,inet_ntoa(p_iphdr->sadrr));
printf(“dest ip:%s\n”,inet_ntoa(p_iphdr->daddr));
}