数据链路层访问

  • Post author:
  • Post category:其他


在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));

}



版权声明:本文为qq_39448776原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。