简述
数据包捕获对数据包和网络问题分析很都用,一般情况下我们可以使用
tcpdump或者wireshark工具捕获进行。但是如果我们需要对数据包进行一些
其他的工作,比如数据监控、分析等,就需要自己捕获数据包后自行处理。
依赖
系统环境
$ uname -a
Linux DESKTOP-L4NCHLS 5.10.16.3-microsoft-standard-WSL2 #1 SMP Fri Apr 2 22:23:49 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
捕获数据包使用的是和tcpdump同款的pcap库。
安装pcap库:
$ sudo apt install libpcap-dev
$ dpkg -l libpcap-dev
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name Version Architecture Description
+++-=================-============-============-======================================================
ii libpcap-dev:amd64 1.9.1-3 amd64 development library for libpcap (transitional package)
实现
使用pcap捕获数据包需要根据网卡设备名称打开对应的设备,
获取设备句柄,然后在对应的设备句柄上捕获数据包。
这里我使用一个类来封装以下pcap库对应的操作。类的结构为:
class interface
{
private:
set<string> dev_name;
vector<pcap_t*> dev_handles;
public:
interface();
~interface();
int dispatch(int count, pcap_handler func, u_char* args);
void print_dev();
};
dev_name
用于保存需要捕获数据包的网卡设备名称,dev_handles
保存打开的设备句柄。
在构造函数中进行初始化操作。首先使用`ifaddrs`相关接口遍历可用的网卡设备(即非环回设备,up状态和running状态)保存网卡名称。然后使用pcap库`pcap_open_live`函数根据网卡名称打开对应的设备,获取设备句柄。`pcap_setnonblock`的作用是将捕获模式设为非阻塞。
// The interface is up, not a loopback and running?
bool up_running(int ifa_flags)
{
return !(ifa_flags & IFF_LOOPBACK) && (ifa_flags & IFF_UP) &&
(ifa_flags & IFF_RUNNING);
}
interface::interface()
{
struct ifaddrs *ifaddr_list, *ifa;
if (getifaddrs(&ifaddr_list)) {
std::cout << "failed to get interface addresses\n";
return;
}
for (ifa = ifaddr_list; ifa != NULL; ifa = ifa->ifa_next) {
if (ifa->ifa_addr == NULL || ifa->ifa_netmask == NULL) continue;
if (!up_running(ifa->ifa_flags)) continue;
dev_name.insert(string(ifa->ifa_name));
}
freeifaddrs(ifaddr_list);
for (string i:dev_name) {
pcap_t* dev_hanlde =
pcap_open_live(i.c_str(), 65535, 0, 100, errbuf);
if (!dev_hanlde) {
cout << "open " << i << " failed.\n";
continue;
}
pcap_setnonblock(dev_hanlde, 1, errbuf);
dev_handles.push_back(dev_hanlde);
}
}
析构函数中只需要关闭打开的设备句柄即可。
interface::~interface()
{
for (pcap_t* i:dev_handles){
pcap_close(i);
}
}
添加一个无关紧要的设备名称打印函数。
void interface::print_dev()
{
for (string i:dev_name) {
cout << i << endl;
}
}
最主要的是数据包捕获函数。捕获函数是对`pcap_dispatch`的简单封装。参数func是捕获数据包后的回调处理函数,也是我们自定义处理方式的地方。
typedef void (*pcap_cb)(u_char*, const struct pcap_pkthdr*, const u_char*);
int interface::dispatch(int count, pcap_handler func, u_char* args)
{
int ret;
for (pcap_t* i:dev_handles){
ret = pcap_dispatch(i, count, func, (u_char*)i);
if (ret < 0) {
pcap_perror(i, (char*)"pcap_dispatch failed.");
}
}
return ret;
}
这样之后,主函数就很简单,循环调用数据捕获函数就可以了。
int main()
{
interface in;
in.print_dev();
while (1) in.dispatch(5, process_cb, nullptr);
return 0;
}
后面比较重要的就是回调函数`process_cb`的实现。主要判断数据包的数据链路层类型。 这里只处理以太网类型的包。可以根据需要处理其他的类型。
/**
* 第一个参数是 pcap_dispatch的最后一个参数,当收到足够数量的包后
* 会调用, 第二个参数是收到的数据包的pcap_pkthdr类型指针,
* 第三个参数是收到的数据包数据
*/
void process_cb(u_char* arg, const struct pcap_pkthdr* pkthdr,
const u_char* packet)
{
pcap_t* handle = (pcap_t*)arg;
switch (pcap_datalink(handle)) {
case DLT_EN10MB:
ethernet_cb(handle, pkthdr, packet);
break;
case DLT_RAW:
case DLT_NULL:
default:
printf("Unknown linktype %d", pcap_datalink(handle));
break;
}
}
在以太网数据包的回调函数中,继续判断数据包类型,这里只处理IP数据包。
void ethernet_cb(pcap_t* handle, const struct pcap_pkthdr* pkthdr,
const u_char* packet)
{
const struct ether_header* ethernet = (struct ether_header*)packet;
u_char* payload = (u_char*)packet + sizeof(struct ether_header);
uint16_t protocol = ntohs(ethernet->ether_type);
switch (protocol) {
case ETHERTYPE_IP:
ip_cb(handle, pkthdr, payload);
break;
default:
break;
}
}
在IP数据包处理函数中,输出IP的数据包的源地址和目的地址。然后根据需要处理TCP数据包。
void ip_cb(pcap_t* handle, const struct pcap_pkthdr* pkthdr,
const u_char* packet)
{
const struct ip* ip = (struct ip*)packet;
printf("------ ip connection ------\n");
char* srcIp = strdup(inet_ntoa(ip->ip_src));
char* desIp = strdup(inet_ntoa(ip->ip_dst));
printf("src ip:%s\tdest ip:%s\tid:%d\n", srcIp, desIp, ip->ip_id);
free(srcIp);
free(desIp);
u_char* payload = (u_char*)packet + sizeof(struct ip);
switch (ip->ip_p) {
case IPPROTO_TCP:
tcp_cb(payload);
break;
case IPPROTO_UDP:
// udp_cb(payload);
break;
default:
printf("unsuport protocol %d\n", ip->ip_p);
break;
}
}
在TCP数据处理函数中简单输出数据包的端口号和一些标志位。
void tcp_cb(const u_char* data)
{
struct tcphdr* tcp = (struct tcphdr*)data;
printf("tcp src port=%d\tdist port=%d ack=%d fin=%d syn=%d\n",
ntohs(tcp->source), ntohs(tcp->dest), tcp->ack, tcp->fin, tcp->syn);
if (tcp->syn) printf("tcp connetion create !!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
if (tcp->fin) printf("tcp connection close !!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
}
运行
注意编译的时候需要连接pcap库。运行时需要超级管理员权限(操作网卡)。
$ g++ main.cc -lpcap
$ sudo ./a.out
eth0
------ ip connection ------
src ip:172.17.120.7 dest ip:172.17.112.1 id:5603
tcp src port=43091 dist port=53623 ack=1 fin=0 syn=0
------ ip connection ------
src ip:172.17.120.7 dest ip:172.17.112.1 id:30431
tcp src port=43091 dist port=62669 ack=1 fin=0 syn=0
------ ip connection ------
src ip:172.17.112.1 dest ip:172.17.120.7 id:49514
tcp src port=62669 dist port=43091 ack=1 fin=0 syn=0
... ...
完整代码
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <net/ethernet.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <pcap.h>
#include <signal.h>
#include <sys/socket.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <set>
#include <string>
#include <vector>
using namespace std;
char errbuf[PCAP_ERRBUF_SIZE];
// The interface is up, not a loopback and running?
bool up_running(int ifa_flags)
{
return !(ifa_flags & IFF_LOOPBACK) && (ifa_flags & IFF_UP) &&
(ifa_flags & IFF_RUNNING);
}
bool is_related(string ip)
{
vector<string> ips;
struct ifaddrs *ifaddr_list, *ifa;
if (getifaddrs(&ifaddr_list)) {
std::cout << "failed to get interface addresses" << std::endl;
return 1;
}
char local[INET_ADDRSTRLEN];
void* tmpAddrPtr = NULL;
for (ifa = ifaddr_list; ifa != NULL; ifa = ifa->ifa_next) {
if (ifa->ifa_addr == NULL || ifa->ifa_netmask == NULL) continue;
tmpAddrPtr = &((struct sockaddr_in*)ifa->ifa_addr)->sin_addr;
inet_ntop(AF_INET, tmpAddrPtr, local, INET_ADDRSTRLEN);
ips.push_back(string(local));
}
freeifaddrs(ifaddr_list);
for (vector<string>::iterator it = ips.begin(); it != ips.end(); it++) {
if (*it == ip) return true;
}
return false;
}
typedef void (*pcap_cb)(u_char*, const struct pcap_pkthdr*, const u_char*);
class interface
{
private:
set<string> dev_name;
vector<pcap_t*> dev_handles;
public:
interface();
~interface();
int dispatch(int count, pcap_handler func, u_char* args);
void print_dev();
};
void tcp_cb(const u_char* data)
{
struct tcphdr* tcp = (struct tcphdr*)data;
printf("tcp src port=%d\tdist port=%d ack=%d fin=%d syn=%d\n",
ntohs(tcp->source), ntohs(tcp->dest), tcp->ack, tcp->fin, tcp->syn);
if (tcp->syn) printf("tcp connetion create !!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
if (tcp->fin) printf("tcp connection close !!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
}
void udp_cb(const u_char* data)
{
struct udphdr* udp = (struct udphdr*)data;
printf("udp src port=%d\tdist port=%d\n", ntohs(udp->source),
ntohs(udp->dest));
}
void ip_cb(pcap_t* handle, const struct pcap_pkthdr* pkthdr,
const u_char* packet)
{
const struct ip* ip = (struct ip*)packet;
printf("------ ip connection ------\n");
char* srcIp = strdup(inet_ntoa(ip->ip_src));
char* desIp = strdup(inet_ntoa(ip->ip_dst));
printf("src ip:%s\tdest ip:%s\tid:%d\n", srcIp, desIp, ip->ip_id);
free(srcIp);
free(desIp);
u_char* payload = (u_char*)packet + sizeof(struct ip);
switch (ip->ip_p) {
case IPPROTO_TCP:
tcp_cb(payload);
break;
case IPPROTO_UDP:
// udp_cb(payload);
break;
default:
printf("unsuport protocol %d\n", ip->ip_p);
break;
}
}
void ethernet_cb(pcap_t* handle, const struct pcap_pkthdr* pkthdr,
const u_char* packet)
{
const struct ether_header* ethernet = (struct ether_header*)packet;
u_char* payload = (u_char*)packet + sizeof(struct ether_header);
uint16_t protocol = ntohs(ethernet->ether_type);
switch (protocol) {
case ETHERTYPE_IP:
ip_cb(handle, pkthdr, payload);
break;
default:
break;
}
}
/**
* 第一个参数是 pcap_dispatch的最后一个参数,当收到足够数量的包后
* 会调用, 第二个参数是收到的数据包的pcap_pkthdr类型指针,
* 第三个参数是收到的数据包数据
*/
void process_cb(u_char* arg, const struct pcap_pkthdr* pkthdr,
const u_char* packet)
{
pcap_t* handle = (pcap_t*)arg;
switch (pcap_datalink(handle)) {
case DLT_EN10MB:
ethernet_cb(handle, pkthdr, packet);
break;
case DLT_RAW:
case DLT_NULL:
default:
printf("Unknown linktype %d", pcap_datalink(handle));
break;
}
}
int main()
{
interface in;
in.print_dev();
while (1) in.dispatch(5, process_cb, nullptr);
return 0;
}
interface::interface()
{
struct ifaddrs *ifaddr_list, *ifa;
if (getifaddrs(&ifaddr_list)) {
std::cout << "failed to get interface addresses" << std::endl;
return;
}
for (ifa = ifaddr_list; ifa != NULL; ifa = ifa->ifa_next) {
if (ifa->ifa_addr == NULL || ifa->ifa_netmask == NULL) continue;
if (!up_running(ifa->ifa_flags)) continue;
dev_name.insert(string(ifa->ifa_name));
}
freeifaddrs(ifaddr_list);
for (string i : dev_name) {
pcap_t* dev_hanlde = pcap_open_live(i.c_str(), 65535, 0, 100, errbuf);
if (!dev_hanlde) {
cout << "open " << i << " failed." << endl;
continue;
}
pcap_setnonblock(dev_hanlde, 1, errbuf);
dev_handles.push_back(dev_hanlde);
}
}
interface::~interface()
{
for (pcap_t* i : dev_handles) {
pcap_close(i);
}
}
int interface::dispatch(int count, pcap_handler func, u_char* args)
{
int ret;
for (pcap_t* i : dev_handles) {
ret = pcap_dispatch(i, count, func, (u_char*)i);
if (ret < 0) {
pcap_perror(i, (char*)"pcap_dispatch failed.");
}
}
return ret;
}
void interface::print_dev()
{
for (string i : dev_name) {
cout << i << endl;
}
}