ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于libpcap和线程池的C语言网络入侵检测系统设计

基于libpcap和线程池的C语言网络入侵检测系统设计 简介这是一份基于PCAP的网络入侵检测系统C语言实现项目面向计算机网络与信息安全方向的本科生和研究生可作为课程设计、毕业设计或期末大作业直接使用。项目在导师指导下完成并获97分高分评价源码完整、构建配置齐备确保下载后无需修改即可运行实现涵盖数据包捕获、分发调度、检测分析以及线程池管理等核心模块对学习网络流量处理与入侵检测机制很有参考价值。压缩包共17个文件以C源码6个与头文件4个为主配合Makefile构建脚本、Shell测试脚本、Python辅助工具含ARP欺骗模拟脚本、PDF报告和README说明文档。报告详细介绍了系统设计、检测流程与实验结论使用说明可快速完成编译与测试这些内容覆盖了从抓包、分发到检测的完整链路便于按模块研读包体仅891KB轻量便携。已有142人学习下载适合需要快速产出高质量网络课程项目的同学借鉴。1. 用 C 语言实现网络入侵检测系统这不是调库是把 pcap 和线程池串成一条流水线基于 PCAP 做网络入侵检测最常见的翻车方式是开头就去找 Snort 规则集做完连 libpcap 的函数手册都没翻过。这份 CS241 课程设计的源码走的是另一条路线sniff.c 负责抓包analysis.c 做协议异常检测dispatch.c 加上 thread_pool 承担并发分发三个模块各管一段再用一份 97 分的课程报告把设计过程完整串起来。学期末要找 C 语言课程设计参考、或者想搞懂抓包到告警中间到底经过哪些环节的人这个项目可以直接当脚手架用。它不追求工业级检测能力但把核心链路讲透了改起来也不费劲。2. 先看懂骨架文件职责、流水线与线程池选型2.1 文件结构与模块边界打开 intrusion-detection-sysem-master 目录src 下九个文件把职责切得很干净。main.c 是入口负责解析命令行参数、安装信号处理函数、拉起和停止整个系统sniff.c / sniff.h 是 libpcap 抓包层负责打开网卡、注册回调、把原始帧从内核缓冲区搬进用户态analysis.c / analysis.h 是检测层拿到一个完整的以太网帧之后解析 IP/TCP 头再对照规则给出结论dispatch.c / dispatch.h 是连接层维护任务队列把 sniff 层收到的包交给 analysis 层的线程池处理。每个文件只做一件事编译顺序由 Makefile 固定下来。这种分层方式在课程设计里是明显加分项。很多 C 语言期末大作业把抓包、解析、打印全塞进一个 main.c两三百行之后自己都找不着北。这份源码把边界划在数据如何流动上抓包回调里只负责拷贝和入队解析和判定全部放在 worker 线程里因此单看某一个文件就能复述它的职责。我收到课设源码时会先看 main.c 里信号处理写了什么再看 sniff.c 的回调是否只做轻量操作这两处能直接判断代码能不能扛住真实流量。源码目录里还附带 test.sh 和 arp-poison.py。test.sh 做自动化验证先 make clean 再 make然后用 sudo 拉起 sniffer 跑一段固定时间的抓包结束后打印统计结果。arp-poison.py 是配合测试用的 ARP 欺骗模拟脚本用来快速制造异常流量让检测规则有东西可抓。这两份脚本一静一动把如何证明系统在工作这个问题解决了演示时也方便。2.2 抓包-分发-分析流水线整个系统的数据流只有一条主链网卡收到帧后libpcap 在内核缓冲区攒一批pcap_loop 每取出一帧就回调一次 packet_handler回调里把帧拷贝到 task 结构体然后 dispatch_enqueue 丢进环形队列线程池里的 worker 在队列另一头取出 task调用 analysis_process 做协议解析和规则匹配最后把结果写到日志或标准输出。抓包线程和 worker 线程之间只通过队列传递数据没有共享的全局状态这是整个设计里最值得抄的部分。这条流水线上藏着一个关键约束pcap_loop 是阻塞调用回调返回之前libpcap 不会去取下一帧。如果回调里做了 printf 或者复杂的规则匹配抓包缓冲区很快就会溢出内核丢包会直接反映在 pcap_stats 的 ps_drop 上。所以 packet_handler 里只做两件事拷贝帧数据、入队。真正的分析被挪到 worker 线程里从源头避免了抓包线程被拖死。这个思路在多线程网络程序里很通用不只适用于 IDS。2.3 为什么用线程池而不是每包建线程处理每个包有两条路可选收到一帧就 pthread_create 一个线程去处理或者启动固定数量的 worker 线程轮流消费队列。前者的优点是实现简单但 pthread_create 涉及系统调用、栈分配和调度器唤醒每包一次的开销在高频流量下非常可观可能比解析本身还贵。后者把线程生命周期固定在启动阶段运行时只做任务投递和消费系统开销小得多。这份源码选线程池是合理的工程取舍。线程池规模一般按 CPU 核数乘 2 起步核多的机器可以放到 8 到 16。我一般会看 analysis.c 里的检测规则复杂度来调如果规则只是几个字段比对4 到 8 个 worker 就够如果后面加了会话跟踪或重组再把线程数往上提。队列溢出时可以阻塞生产者或丢包这份源码用的是丢包策略因为网络抓包场景下丢一帧比阻塞整个抓包线程划算得多。2.4 Makefile 与 test.sh 里值得抄的点Makefile 里最需要注意的是两个链接参数-lpcap 和 -lpthread前者链接 libpcap 抓包库后者链接 POSIX 线程库。编译选项里通常还有 -Wall -g把警告全开、保留调试信息性能调优时可以加 -O2。test.sh 的价值不在命令本身而在它规定了验证顺序先确认编译产物存在再检查当前用户能否打开网卡最后才真正跑抓包。这个顺序把代码没编译过和没权限抓包两类问题拆开报错时一眼就能定位。#!/bin/bash # 先编译编译不过直接退出 make clean make if [ $? -ne 0 ]; then echo build failed exit 1 fi # 再检查网卡是否可读防止后面 pcap_open_live 静默失败 if ! ping -c 1 -W 1 127.0.0.1 /dev/null 21; then echo loopback not ready exit 1 fi # 最后用 sudo 拉起抓包跑 30 秒自动退出 sudo ./sniffer -i lo -t 30这个脚本体现了一个好习惯凡是涉及抓包的工具都要先确认环境和权限再跑主体逻辑。后面会专门说权限这个坑这里先记住 test.sh 的结尾一定有一段清理逻辑把临时文件和网卡配置复位避免上一次测试的残留影响下一次结果。3. 抓包回调与协议解析从网卡到原始字节3.1 打开网卡的那几个参数sniff.c 里必然会有一句 pcap_open_live它的参数值得逐个看。第一个参数是网卡名可以用 pcap_findalldevs 枚举得到也可以直接写 eth0 这类固定名字。第二个参数 snaplen 表示每帧最多抓多少字节这里用 65535 是为了保证整个以太网帧不被截断如果只设 1500遇到带 VLAN tag 或巨型帧就会丢尾部解析时取不到完整负载。第三个参数 promisc 设为 1 表示混杂模式让网卡把目的 MAC 不是自己的帧也收进来这是入侵检测的前提。第四个参数 to_ms 是读超时设 1000 毫秒时抓包线程会周期性返回便于处理信号。char errbuf[PCAP_ERRBUF_SIZE]; pcap_t *handle pcap_open_live(dev, 65535, 1, 1000, errbuf); if (handle NULL) { fprintf(stderr, pcap_open_live: %s\n, errbuf); return -1; } // 设置 BPF 过滤只收 IPv4 和 ARP避免无关流量灌爆队列 struct bpf_program fp; pcap_compile(handle, fp, ip or arp, 0, PCAP_NETMASK_UNKNOWN); pcap_setfilter(handle, fp);权限问题在打开网卡时就会暴露。pcap_open_live 返回 NULL 且 errbuf 写着 socket: Operation not permitted 时基本就是没提权。常见做法是 sudo 运行或者对二进制做 setcap cap_net_raw,cap_net_admineip这样不用每次都用 root。只开混杂模式而没有 cap_net_admin 同样会失败因为设置混杂模式需要网卡管理权限这两个能力都得给。BPF 过滤表达式 ip or arp 把非 IP 非 ARP 的帧挡在外面队列压力会小很多。3.2 回调里为什么不干重活packet_handler 是整个系统里最容易被写坏的地方。有人会把协议解析、规则判定、日志输出全塞进回调跑几万包之后丢包率直线上升。原因很简单pcap_loop 一次只取一帧回调不返回内核的 socket 接收缓冲区就会被新到的帧塞满后面的包直接丢掉。这份源码的处理方式是把回调当搬运工只做拷贝和入队。void packet_handler(u_char *args, const struct pcap_pkthdr *header, const u_char *packet) { task_t *t task_new(header-len); if (t NULL) { return; // 队列满丢弃这一帧 } memcpy(t-data, packet, header-len); t-len header-len; dispatch_enqueue(t); // 只做投递不做分析 }task_new 会分配一块和帧长一致的内存然后 memcpy 把整帧拷贝过去。这个拷贝不是可有可无libpcap 在回调返回后可能复用 packet 指针指向的缓冲区不拷贝就拿着悬垂指针交给 worker 线程结果是典型的 use-after-free表现成随机段错误和乱码日志。拷贝的成本在千字节级别完全值得。参数 args 是 pcap_loop 传入的上下文指针这里用来传递队列句柄实现回调与分发模块的解耦。3.3 手工解析 IP/TCP 头以太网帧前 14 字节是链路层头0 到 5 字节是目的 MAC6 到 11 字节是源 MAC12 到 13 字节是以太网类型0x0800 表示 IPv4。从第 14 字节开始才是 IP 头。IP 头里版本号占高 4 位IHL头部长度占低 4 位单位是 4 字节所以 IP 头的实际长度是 ihl * 4。协议号在 IP 头的第 9 个字节6 是 TCP17 是 UDP1 是 ICMP。TCP 头从 IP 头结束的位置开始源端口在 TCP 头前 2 字节目的端口在接下来的 2 字节。#define ETH_HEADER_LEN 14 #define IP_PROTO_TCP 6 const uint8_t *eth packet; uint16_t ethertype (eth[12] 8) | eth[13]; if (ethertype ! 0x0800) { return; // 非 IPv4直接忽略 } const uint8_t *ip packet ETH_HEADER_LEN; uint8_t ip_ihl (ip[0] 0x0F) * 4; uint8_t ip_proto ip[9]; uint32_t src_ip (ip[12] 24) | (ip[13] 16) | (ip[14] 8) | ip[15]; uint32_t dst_ip (ip[16] 24) | (ip[17] 16) | (ip[18] 8) | ip[19]; if (ip_proto IP_PROTO_TCP) { const uint8_t *tcp ip ip_ihl; uint16_t src_port (tcp[0] 8) | tcp[1]; uint16_t dst_port (tcp[2] 8) | tcp[3]; // 之后把 src_ip、dst_ip、src_port、dst_port 交给规则引擎 }这里有一个很多新手会踩的坑直接写struct iphdr *ip_hdr (struct iphdr *)ip;然后把结构体字段拿出来用。结构体有内存对齐和字节序问题x86 小端机器上读出来的端口号和网络字节序相反而且结构体定义在不同平台有差异代码换个编译环境就跑不对。手工按偏移取字节是最稳的还能顺便做字节序转换。src_ip 和 dst_ip 拼成 uint32_t 是为了方便比较判断 Land 攻击时直接对比两个值。3.4 检测规则与日志输出analysis.c 里维护一张规则表每条规则包含一个判定函数指针和一个告警等级。判定函数收到解析好的元组协议、源 IP、目的 IP、源端口、目的端口、TCP 标志后返回 1 表示命中。规则是顺序匹配的命中的第一条决定告警类型。这份源码里几条典型规则如下它们都针对不需要维持会话状态的单包检测实现成本低且适合 C 语言课设。| 检测项 | 判定字段 | 触发条件 | | Land 攻击 | IP 源/目的地址 | src_ip dst_ip | | 端口扫描 | TCP 标志与端口 | 同一源 IP 在 10 秒内 SYN 包访问目的端口数超过 20 | | 畸形 TCP 标志 | TCP flags | SYN 与 FIN 同位置 1 | | ARP 欺骗 | ARP 载荷 | 同一 IP 短时间出现多个不同 MAC 的 ARP 应答 |告警输出统一走一个日志函数格式是[告警等级] 时间戳 攻击类型 源IP:源端口 - 目的IP:目的端口这样后面用 grep 或 awk 都能快速统计。日志默认打到 stdout作业要求里如果要求落盘把输出重定向到文件即可。对于端口扫描这类需要时间窗口的检测analysis.c 里维护一张小哈希表记录源 IP 和首次 SYN 的时间窗口超时后自动清理旧条目避免内存膨胀。4. 并发分发与线程池dispatch.c 的核心逻辑4.1 环形任务队列的设计dispatch.c 的核心是一个无锁的单生产者单消费者队列但在 C 语言课设这个规模下直接用 pthread 互斥锁加条件变量更稳妥。队列用环形缓冲实现head 指向下一个出队位置tail 指向下一个入队位置capacity 是固定容量count 是当前元素个数。队列满时入队直接返回 -1由调用方决定丢包策略队列空时出队会阻塞在条件变量上等抓包线程投递新任务。typedef struct task_queue { task_t **slots; int capacity; int head, tail, count; pthread_mutex_t lock; pthread_cond_t not_empty; pthread_cond_t not_full; } task_queue_t; int queue_push(task_queue_t *q, task_t *t) { pthread_mutex_lock(q-lock); while (q-count q-capacity) { pthread_cond_wait(q-not_full, q-lock); // 队列满等待 worker 消费 } q-slots[q-tail] t; q-tail (q-tail 1) % q-capacity; q-count; pthread_cond_signal(q-not_empty); // 通知 worker 有新任务 pthread_mutex_unlock(q-lock); return 0; }生产者和消费者用两个条件变量分别表示不为空和不为满比只用一个条件变量更高效避免生产者频繁被无效唤醒。入队阻塞的代价只在队列持续满载时出现正常情况下每次操作都是微秒级。capacity 分配多大取决于内存预算一般每个 task 带上完整帧数据65535 字节一帧队列长度如果设 1024最坏情况要占 64MB 内存课设里可以降到 512。4.2 worker 线程的等待与唤醒worker 线程的核心是一个死循环加锁、检查队列是否为空、条件变量等待、取任务、解锁、执行分析。这里有一个必须用 while 而不能用 if 的原因pthread_cond_wait 存在虚假唤醒spurious wakeup即使没有线程调用 signal它也可能返回。如果只用 if 判断队列状态虚假唤醒时会取到一个空槽然后拿到野指针。写成 while 循环可以在唤醒后重新检查条件这才是条件变量配互斥锁的标准姿势。void *worker_main(void *arg) { thread_pool_t *pool (thread_pool_t *)arg; while (1) { pthread_mutex_lock(pool-queue.lock); while (pool-queue.count 0 !pool-shutdown) { pthread_cond_wait(pool-queue.not_empty, pool-queue.lock); } if (pool-shutdown pool-queue.count 0) { pthread_mutex_unlock(pool-queue.lock); break; // 停止且队列已空退出 } task_t *t pool-queue.slots[pool-queue.head]; pool-queue.head (pool-queue.head 1) % pool-queue.capacity; pool-queue.count--; pthread_cond_signal(pool-queue.not_full); pthread_mutex_unlock(pool-queue.lock); analysis_process(t); // 离开锁之后再做解析减少临界区 task_free(t); } return NULL; }重点看退出条件的顺序先判断 count 为 0 再判断 shutdown两个条件都满足才退出。如果反着写shutdown 置位后 worker 会立刻退出队列里残留的任务没人处理日志就会少最后一批告警。task_free 在 worker 线程里执行也没问题因为任务数据已经不再被其他线程引用。4.3 优雅关停信号处理与资源回收主程序收到 SIGINT 或 SIGTERM 时不能直接 exit否则线程池里的线程还在跑抓包的 pcap_handle 还开着端口和内存都没释放。标准做法是信号处理器里只置一个全局标志主线程循环检测到标志后先 pcap_breakloop 让抓包循环退出再置 shutdown 并 broadcast 所有条件变量最后 join 每个 worker 线程关闭文件描述符清理队列内存。static volatile sig_atomic_t stop_flag 0; void handle_signal(int sig) { stop_flag 1; } int main(int argc, char **argv) { signal(SIGINT, handle_signal); signal(SIGTERM, handle_signal); pcap_loop(handle, -1, packet_handler, NULL); // 阻塞抓包 // pcap_loop 因 breakloop 返回后开始关停线程池 pool_shutdown(pool); pcap_close(handle); return 0; }pcap_breakloop 要在信号处理器里调用吗我一般不在信号处理器里做任何库函数调用因为信号处理器不是异步信号安全函数的安乐窝。稳妥流程是把 pcap_breakloop 放到主线程检测到 stop_flag 之后调用让抓包循环尽快返回。pcap_loop 的 timeout 设为 1000 毫秒还有一个好处即使没有 breakloop循环也最多阻塞 1 秒就会检查一次 stop_flag关停延迟完全可接受。5. 避坑指南五个最容易翻车的现场5.1 打开网卡报 Operation not permitted现象pcap_open_live 返回 NULLerrbuf 内容是socket: Operation not permitted程序直接退出。原因libpcap 抓包需要 CAP_NET_RAW 能力普通用户默认没有。混杂模式还需要 CAP_NET_ADMIN这个权限通常只有 root 或经过 setcap 的二进制才具备。解决先试 sudo 运行能跑再考虑长期方案。给编译好的二进制加能力sudo setcap cap_net_raw,cap_net_admineip ./sniffer加完不用 root 也能抓包。注意 setcap 对脚本无效必须是 ELF 可执行文件。5.2 丢包率居高不下现象抓包结束后统计 pcap_statsps_drop 字段数值很高甚至接近 ps_recv 的一半。原因回调函数里做了解析、打印或磁盘写入阻塞时间过长内核缓冲区被后续到达的包填满后开始丢弃新包。解决把回调函数瘦身成拷贝加入队解析和规则判定全部移到 worker 线程。另外检查 BPF 过滤表达式能提前过滤的协议就提前过滤掉比如只留 ip or arp广播帧和 LLDP 这类无关包就不会涌进来。5.3 端口号显示成五位数乱码现象解析 TCP 头之后源端口显示成 51200 之类的大数和 tcpdump 里看到的 80、443 对不上。原因网络字节序是大端x86 主机是小端。直接用tcp[0] 8 | tcp[1]是正确做法但如果把结构体强转后直接读字段字段会被解释成小端整数数值就错了。端口值大于 255 时尤其明显。解决手工按偏移取字节并做移位合并。(tcp[0] 8) | tcp[1]得到的就是主机字节序的正确端口值。IP 地址的四字节拼接同理全部手工完成不依赖结构体对齐。做完之后和 tcpdump 输出对一次数值一致才算过。5.4 TCP 校验和永远不对现象规则里加了 TCP 校验和验证之后所有经过网卡的包都是bad checksum告警刷屏。原因现代网卡普遍开启 TSO/GRO 硬件 offload数据包在网卡层面才补全校验和libpcap 抓到的帧可能是校验和字段为零或未计算的状态。这在本地回环接口上尤其常见因为回环接口根本不经过物理网卡。解决检测规则不要把校验和当作唯一判定依据改成校验和错误且端口匹配组合条件。测试环境里可以关掉 offloadsudo ethtool -K eth0 tx off rx off但这不是所有机型都支持代码层面做好容忍更通用。5.5 线程池退出时卡死现象程序运行正常CtrlC 之后停在某个 join 调用上进程不退出过几秒才被系统杀掉。原因worker 线程在条件变量上等待而关停流程只置了 shutdown 标志没有 broadcast 唤醒条件变量。或者 worker 退出的判断条件写反shutdown 置位后线程立刻退出队列里剩下的任务没人处理主线程 join 等待的资源被早早释放。解决关停顺序改成三步置 shutdown、pthread_cond_broadcast 唤醒所有 worker、再逐个 join。worker 内部退出条件写成while (count 0 !shutdown)退出前再检查一次队列是否真空。这两处改完CtrlC 之后进程应该在一秒内干净退出。6. 用 ARP 欺骗脚本验证 IDS如何证明它真的能报警验证一个入侵检测系统能不能用靠嘴上说抓到了没用得有可复现的对照。我把这套源码跑通的流程整理成四步全程不需要外网环境两台虚拟机同一个网段就行。第一步编译并启动 sniffer。编译走 test.sh 的逻辑make 成功之后用 sudo 拉起抓包网卡选虚拟机的内网口时间设长一点比如 60 秒保证后面有充足时间制造流量。第二步在另一台虚拟机上运行 arp-poison.py。脚本会持续向目标机器发送伪造的 ARP 应答报文把一个 IP 映射到多个不同的 MAC 地址这正是 ARP 欺骗规则的触发条件。脚本跑 20 秒左右就可以停期间 sniffer 应该持续产生告警。第三步回到 sniffer 所在机器等抓包窗口结束后统计日志里告警行的数量和 arp-poison.py 发出的报文数量做对照。如果告警数接近发送数说明从抓包到解析到规则匹配整条链路是通的如果告警数为零先查网卡是否混杂模式再查 BPF 过滤表达式是否把 ARP 包滤掉了。为了验证得更干净我习惯在跑完一轮之后做一次差分先用 tcpdump 抓同一段流量保存成 pcap再用这套 IDS 解析那个 pcap 文件两边输出的告警数量和类型做对比。tcpdump 能抓到而 IDS 没报警就是检测规则漏了IDS 报了大量 tcpdump 里不存在的告警就要怀疑字节序解析或规则阈值写错。从那以后我每次改完检测规则都会强制自己跑一遍同样的制造流量 → 抓包 → 对告警数流程不看到匹配数对上号就不算完成。这套源码从 Makefile 到攻击模拟脚本把验证闭环都准备好了照着跑一遍就能定位问题在哪一层希望帮到你。本文还有配套的精品资源点击获取
返回列表