素槿
Published on 2025-08-22 / 3 Visits
0

使用pcap库捕获数据包

简述

数据包捕获对数据包和网络问题分析很都用,一般情况下我们可以使用
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;
    }
}