基于Linux平台的libpcap源代码分析b

static int
live_open_new(pcap_t *handle, const char *device, int promisc,
  int to_ms, char *ebuf)
{
/* 
如果设备给定 , 则打开一个  RAW  类型的套接字 , 否则 , 打开  DGRAM  类型的套接字  */
sock_fd = device ?
                 socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))
                : socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL));

/* 
取得回路设备接口的索引  */
handle->md.lo_ifindex = iface_get_id(sock_fd, "lo", ebuf);

/* 
如果设备给定,但接口类型未知或是某些必须工作在加工模式下的特定类型,则使用加工模式  */
if (device) {
/* 
取得接口的硬件类型  */
arptype = iface_get_arptype(sock_fd, device, ebuf); 

/* linux 
使用  ARPHRD_xxx  标识接口的硬件类型,而  libpcap  使用 DLT_xxx
来标识。本函数是对上述二者的做映射变换,设置句柄的链路层类型为
DLT_xxx
,并设置句柄的偏移量为合适的值,使其与链路层头部之和为  4  的倍数,目的是边界对齐  */
map_arphrd_to_dlt(handle, arptype, 1);

/* 
如果接口是前面谈到的不支持链路层头部的类型,则退而求其次,使用  SOCK_DGRAM  模式  */
if (handle->linktype == xxx) 
{
close(sock_fd)

sock_fd = socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL));
}

/* 
获得给定的设备名的索引  */
device_id = iface_get_id(sock_fd, device, ebuf);
                 
/* 
把套接字和给定的设备绑定,意味着只从给定的设备上捕获数据包  */
iface_bind(sock_fd, device_id, ebuf)


} else { /* 
现在是加工模式  */
handle->md.cooked = 1;
/* 
数据包链路层头部为结构  sockaddr_ll  SLL  大概是结构名称的简写形式  */
handle->linktype = DLT_LINUX_SLL;
                 device_id = -1;
           }
           
/* 
设置给定设备为混杂模式  */
if (device && promisc) 
{
memset(&mr, 0, sizeof(mr));
mr.mr_ifindex = device_id;
mr.mr_type = PACKET_MR_PROMISC;
setsockopt(sock_fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, 
&mr, sizeof(mr))

}

/* 
最后把创建的  socket  保存在句柄  pcap_t   */
handle->fd = sock_fd;
     }

/* 2.0 
内核下函数要简单的多,因为只有唯一的一种  socket  方式  */
static int
live_open_old(pcap_t *handle, const char *device, int promisc,
          int to_ms, char *ebuf)
{
/* 
首先创建一个 SOCK_PACKET 类型的  socket */
handle->fd = socket(PF_INET, SOCK_PACKET, htons(ETH_P_ALL));
           
/* 2.0 
内核下,不支持捕获所有接口,设备必须给定  */
if (!device) {
strncpy(ebuf, "pcap_open_live: The /"any/" device isn't supported on 2.0[.x]-kernel systems", PCAP_ERRBUF_SIZE);
break;
}
           
/* 
 socket  和给定的设备绑定  */
iface_bind_old(handle->fd, device, ebuf)

           
/*
以下的处理和  2.2  版本下的相似,有所区别的是如果接口链路层类型未知,则  libpcap  直接退出  */
            
arptype = iface_get_arptype(handle->fd, device, ebuf);
map_arphrd_to_dlt(handle, arptype, 0);
if (handle->linktype == -1) {
snprintf(ebuf, PCAP_ERRBUF_SIZE, "unknown arptype %d", arptype);
break;
}

/* 
设置给定设备为混杂模式  */
if (promisc) {
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, device, sizeof(ifr.ifr_name));
ioctl(handle->fd, SIOCGIFFLAGS, &ifr)

ifr.ifr_flags |= IFF_PROMISC;
ioctl(handle->fd, SIOCSIFFLAGS, &ifr)

}
}



比较上面两个函数的代码,还有两个细节上的区别。首先是  socket  与接口绑定所使用的结构:老式的绑定使用了结构  sockaddr ,而新式的则使用了  2.2  内核中定义的通用链路头部层结构  sockaddr_ll



iface_bind_old(int fd, const char *device, char *ebuf)
{
struct sockaddr      saddr;
memset(&saddr, 0, sizeof(saddr));
strncpy(saddr.sa_data, device, sizeof(saddr.sa_data));
bind(fd, &saddr, sizeof(saddr))

}

iface_bind(int fd, int ifindex, char *ebuf)
{
struct sockaddr_ll      sll;
memset(&sll, 0, sizeof(sll));
sll.sll_family = AF_PACKET;
sll.sll_ifindex = ifindex;
sll.sll_protocol      = htons(ETH_P_ALL);
bind(fd, (struct sockaddr *) &sll, sizeof(sll)

}



第二个是在  2.2  版本中设置设备为混杂模式时,使用了函数  setsockopt() ,以及新的标志  PACKET_ADD_MEMBERSHIP  和结构  packet_mreq 。我估计这种方式主要是希望提供一个统一的调用接口,以代替传统的(混乱的) ioctl  调用。



struct packet_mreq
{
int         mr_ifindex;   /* 
接口索引号  */
unsigned short mr_type;     /* 
要执行的操作 ( ) */
unsigned short mr_alen;     /* 
地址长度  */
unsigned char   mr_address[8]; /* 
物理层地址  */ 
};



用户应用程序接口
Libpcap 
提供的用户程序接口比较简单,通过反复调用函数 pcap_next()[pcap.c]  则可获得捕获到的数据包。下面是一些使用到的数据结构:



/* 
单个数据包结构,包含数据包元信息和数据信息  */
struct singleton [pcap.c]
{
struct pcap_pkthdr hdr; /* libpcap 
自定义数据包头部  */
const u_char * pkt; /* 
指向捕获到的网络数据  */
};

/* 
自定义头部在把数据包保存到文件中也被使用  */
struct pcap_pkthdr 
{
           struct timeval ts; /* 
捕获时间戳  */ 
           bpf_u_int32 caplen; /* 
捕获到数据包的长度  */
           bpf_u_int32 len; /* 
数据包的真正长度  */
}

/* 
函数  pcap_next()  实际上是对函数  pcap_dispatch()[pcap.c]  的一个包装  */
const u_char * pcap_next(pcap_t *p, struct pcap_pkthdr *h)
{
struct singleton s;
s.hdr = h;

/*
入参 "1" 代表收到 1 个数据包就返回;回调函数  pcap_oneshot()  是对结构  singleton  的属性赋值  */
if (pcap_dispatch(p, 1, pcap_oneshot, (u_char*)&s) <= 0)
return (0);
return (s.pkt); /* 
返回数据包缓冲区的指针  */
}



pcap_dispatch() 
简单的调用捕获句柄  pcap_t  中定义的特定操作系统的读数据函数: return p->read_op(p, cnt, callback, user) 。在  linux  系统下,对应的读函数为  pcap_read_linux() (在创建捕获句柄时已定义  [pcap-linux.c] ),而 pcap_read_linux()  则是直接调用  pcap_read_packet()([pcap-linux.c])

pcap_read_packet() 
的中心任务是利用了  recvfrom()  从已创建的  socket  上读数据包数据,但是考虑到  socket  可能为前面讨论到的三种方式中的某一种,因此对数据缓冲区的结构有相应的处理,主要表现在加工模式下对伪链路层头部的合成。具体代码分析如下:



static int
pcap_read_packet(pcap_t *handle, pcap_handler callback, u_char *userdata)
{
/* 
数据包缓冲区指针  */
u_char * bp;

/* bp 
与捕获句柄  pcap_t   handle->buffer
之间的偏移量,其目的是为在加工模式捕获情况下,为合成的伪数据链路层头部留出空间  */
int offset;

/* PACKET_SOCKET 
方式下, recvfrom()  返回  scokaddr_ll  类型,而在 SOCK_PACKET  方式下,
返回  sockaddr  类型  */
#ifdef HAVE_PF_PACKET_SOCKETS 
                 struct sockaddr_ll      from;
                 struct sll_header      * hdrp;
#else
                 struct sockaddr            from;
#endif

socklen_t            fromlen;
int                  packet_len, caplen;

/* libpcap 
自定义的头部  */
struct pcap_pkthdr      pcap_header;

#ifdef HAVE_PF_PACKET_SOCKETS
/* 
如果是加工模式,则为合成的链路层头部留出空间  */
if (handle->md.cooked)
offset = SLL_HDR_LEN;

/* 
其它两中方式下,链路层头部不做修改的被返回,不需要留空间  */
else
offset = 0;
#else
offset = 0;
#endif

bp = handle->buffer + handle->offset;
     
/* 
从内核中接收一个数据包,注意函数入参中对  bp  的位置进行修正  */
packet_len = recvfrom( handle->fd, bp + offset,
handle->bufsize - offset, MSG_TRUNC,
(struct sockaddr *) &from, &fromlen);
     
#ifdef HAVE_PF_PACKET_SOCKETS
     
/* 
如果是回路设备 , 则只捕获接收的数据包,而拒绝发送的数据包。显然,我们只能在  PF_PACKET
方式下这样做 , 因为  SOCK_PACKET  方式下返回的链路层地址类型为
sockaddr_pkt
,缺少了判断数据包类型的信息。 */
if (!handle->md.sock_packet &&
from.sll_ifindex == handle->md.lo_ifindex &&
from.sll_pkttype == PACKET_OUTGOING)
return 0;
#endif

#ifdef HAVE_PF_PACKET_SOCKETS
/* 
如果是加工模式,则合成伪链路层头部  */
if (handle->md.cooked) {
/* 
首先修正捕包数据的长度,加上链路层头部的长度  */
packet_len += SLL_HDR_LEN;
           hdrp = (struct sll_header *)bp;
           
/* 
以下的代码分别对伪链路层头部的数据赋值  */
hdrp->sll_pkttype = xxx;
hdrp->sll_hatype = htons(from.sll_hatype);
hdrp->sll_halen = htons(from.sll_halen);
memcpy(hdrp->sll_addr, from.sll_addr, 
(from.sll_halen > SLL_ADDRLEN) ? 
SLL_ADDRLEN : from.sll_halen);
hdrp->sll_protocol = from.sll_protocol;
}
#endif
     
/* 
修正捕获的数据包的长度,根据前面的讨论, SOCK_PACKET  方式下长度可能是不准确的  */
caplen = packet_len;
if (caplen > handle->snapshot)
caplen = handle->snapshot;

/* 
如果没有使用内核级的包过滤 , 则在用户空间进行过滤 */
if (!handle->md.use_bpf && handle->fcode.bf_insns) {
if (bpf_filter(handle->fcode.bf_insns, bp,
packet_len, caplen) == 0)
{
/* 
没有通过过滤,数据包被丢弃  */
return 0;
}
}

/* 
填充  libpcap  自定义数据包头部数据:捕获时间 , 捕获的长度 , 真实的长度  */
ioctl(handle->fd, SIOCGSTAMP, &pcap_header.ts)

pcap_header.caplen      = caplen;
pcap_header.len            = packet_len;
     
/* 
累加捕获数据包数目,注意到在不同内核 / 捕获方式情况下数目可能不准确  */
handle->md.stat.ps_recv++;

/* 
调用用户定义的回调函数  */
callback(userdata, &pcap_header, bp);
}



数据包过滤机制
大量的网络监控程序目的不同,期望的数据包类型也不同,但绝大多数情况都都只需要所有数据包的一(小)部分。例如:对邮件系统进行监控可能只需要端口号为  25 smtp )和  110 pop3)   TCP  数据包,对  DNS  系统进行监控就只需要端口号为  53   UDP  数 据包。包过滤机制的引入就是为了解决上述问题,用户程序只需简单的设置一系列过滤条件,最终便能获得满足条件的数据包。包过滤操作可以在用户空间执行,也 可以在内核空间执行,但必须注意到数据包从内核空间拷贝到用户空间的开销很大,所以如果能在内核空间进行过滤,会极大的提高捕获的效率。内核过滤的优势在 低速网络下表现不明显,但在高速网络下是非常突出的。在理论研究和实际应用中,包捕获和包过滤从语意上并没有严格的区分,关键在于认识到捕获数据包必然有 过滤操作。基本上可以认为,包过滤机制在包捕获机制中占中心地位。

包过滤机制实际上是针对数据包的布尔值操作函数,如果函数最终返回  true ,则通过过滤,反之则被丢弃。形式上包过滤由一个或多个谓词判断的并操作( AND )和或操作( OR )构成,每一个谓词判断基本上对应了数据包的协议类型或某个特定值 , 例如:只需要  TCP  类型且端口为  110  的数据包或  ARP  类型的数据包。包过滤机制在具体的实现上与数据包的协议类型并无多少关系,它只是把数据包简单的看成一个字节数组,而谓词判断会根据具体的协议映射到数组特定位置的值。如判断 ARP 类型数据包,只需要判断数组中第  13 14  个字节(以太头中的数据包类型)是否为  0X0806 。从理论研究的意思上看,包过滤机制是一个数学问题,或者说是一个算法问题,其中心任务是如何使用最少的判断操作、最少的时间完成过滤处理,提高过滤效率。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值