上周在客户那边做流量分析模块部署,一台 CentOS 7.9 内网服务器,系统盘装完,网络策略只放开了业务端口,外网完全不通。采集程序对 libpcap 的版本有硬性要求,必须用支持pcap_set_immediate_mode()的新版 API,而系统自带的 libpcap 还是 1.5.3,压根没有这个接口。于是只能在内网环境离线编译安装 libpcap,顺手把 flex、bison、m4 这一套构建工具链全部过了一遍。整个过程踩了不少坑,花了一晚上才把问题理清楚,这里把完整过程记录下来,给同样在内网环境做离线安装的朋友一个参考。
这篇内容适合以下几类人:在内网服务器上部署抓包工具、流量分析 Agent、IDS/IPS 探针的运维或开发;需要在离线环境源码编译 C 库的程序员;以及被configure: error: neither flex nor lex found、libpcap.so.1: cannot open shared object file这类报错折磨过的同学。我尽量把每一步的为什么也讲清楚,而不只是甩给你一串命令。
1. 内网部署场景:为什么CentOS 7上非要源码编译libpcap
1.1 系统自带libpcap版本太老,满足不了新程序的接口需求
CentOS 7 的 base 源里 libpcap 的版本是 1.5.3,这个版本本身不旧,但它暴露的 API 比较有限。比如pcap_set_immediate_mode()、pcap_set_tstamp_precision()、pcap_open()这些接口,都是在 1.6 或 1.7 之后才逐步引入的。如果你在内网部署的是比较新的流量采集框架,它编译的时候会直接检查 pcap.h 里有没有某些宏定义,版本不够就编不过去。
有人可能会说,那我在线装一个 libpcap-devel 不就行了?问题在于 CentOS 7 的 yum 源里 libpcap-devel 对应的还是 1.5.3,即使你把 yum 源切到阿里云或者网易,主流镜像里的 EPEL 包通常也是跟随系统版本的,不会提供特别新的 libpcap。所以只要程序对版本有硬性要求,走 RPM 这条路基本死路一条,只能源码编译。
还有一点,离线环境下你没法用常规的yum install libpcap libpcap-devel自动拉依赖,就算提前下载好了 RPM 包,RPM 的依赖树也可能缺这缺那。比如 libpcap-devel 会依赖 glibc-headers、gcc 等一堆包,离线机器上不一定都有。与其在网上一个个找 RPM 依赖,不如直接源码编译,源码包本身是自包含的,依赖的工具链也就那几个,可控性高得多。
1.2 离线环境依赖链条:不是只编libpcap一个包
libpcap 的源码包在 configure 阶段需要三个东西:m4、flex、bison。如果系统里原本就装了,那自然省事,但内网机器经常是精简安装,连 gcc 都不一定有,更别说这些构建辅助工具了。
这里的依赖逻辑是这样的:
- libpcap 的语法解析器由 bison 生成,
scanner.c由 flex 生成; - bison 运行的时候又要调用 m4 这个宏处理器;
- flex 本身在编译的时候也可能用到 m4,虽然有预生成文件兜底,但保险起见还是先装 m4。
所以离线编译 libpcap 的完整构建顺序是:
m4 -> bison -> flex -> libpcap这个顺序不能乱,尤其是如果你打算把这三个工具链都装进/usr/local目录,后面每一步的 configure 都依赖前面的安装结果。
我遇到的坑就是这样:第一台机器上我以为只需要 flex 和 bison,结果 configure 的时候 bison 报错说找不到 m4,当时人直接傻掉。后来才意识到 m4 是 bison 的运行时依赖,不是可选项。
2. 离线物料准备:在有网机器上备齐一套源码包
2.1 版本选择与下载
离线安装的第一步,是在一台有网的机器上把所有需要的源码包下载好,传到内网机器上。这里版本搭配很关键,不是越新越好,要考虑 CentOS 7 默认 GCC 4.8.5 的编译能力。
我最终选择的版本组合如下:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| m4 | 1.4.19 | 1.4.16 也能用,1.4.19 是 GNU 官方长期维护版本,在 CentOS 7 上编译无压力 |
| bison | 3.0.4 | CentOS 7 自带的就是这个版本,兼容性最好;不要尝试 3.8 以上,对 GCC 4.8 不友好 |
| flex | 2.6.4 | 2.6.4 之后的版本对 autotools 版本有更高要求,2.6.4 足够稳 |
| libpcap | 1.9.1 | 对 CentOS 7 的老 GCC 最友好;1.10.x 我也试过,能编译但部分代码对内核头文件版本有要求 |
下载地址方面,libpcap 的官方发布页面在https://www.tcpdump.org/release/,上面能找到libpcap-1.9.1.tar.gz以及 1.10.x 的包。m4、bison、flex 的源码包在 GNU 镜像站都能下到,比如https://ftp.gnu.org/gnu/m4/、https://ftp.gnu.org/gnu/bison/,flex 在 GitHub Releases 页面也可以下载。
这里提醒一句:下载的时候尽量选择官方源,不要随便到一个第三方网站拿包,原因不光是安全问题,第三方打包的源码经常改动过目录结构,解压之后 configure 脚本各种报错,排查起来非常浪费时间。
2.2 校验工具与包完整性
离线传输前,我建议先做一次完整性校验。源码包在传输或下载过程中损坏,会导致 configure 或者 make 阶段报出莫名其妙的问题,而且报错位置往往离真正的坏文件很远,排查成本很高。
校验方法很简单,在下载机器上执行:
sha256sum libpcap-1.9.1.tar.gz m4-1.4.19.tar.gz bison-3.0.4.tar.gz flex-2.6.4.tar.gz然后把输出的哈希值和官网页面公布的值核对一遍。libpcap 官网上每个版本都给出了对应的 SHA256 值,GNU 镜像站的文件旁边也有.sig签名文件可以验证。确认一致后再打包上传到内网机器。
2.3 备选:用 yumdownloader 拉 RPM 依赖树
如果你的场景对 libpcap 版本没有硬性要求,只是为了在内网装一个可用的抓包环境,那其实不必走源码编译,直接在离线机器上安装 RPM 包更快。
具体做法是在有网的 CentOS 7 机器上执行:
yum install -y yum-utils mkdir -p /tmp/rpms yumdownloader --destdir=/tmp/rpms --resolve libpcap libpcap-develyumdownloader --resolve会把 libpcap 和 libpcap-devel 的所有依赖 RPM 包都拉下来,然后你把/tmp/rpms整个目录传到内网机器,执行:
rpm -ivh /tmp/rpms/*.rpm如果提示依赖缺失,一般是某个仓库没有启用或者 RPM 包版本冲突,可以再用rpm -qpR查看具体缺什么,补齐后再装。
不过要记住,CentOS 7 base 源里的 libpcap-devel 最高也就是 1.5.3。如果你需要新 API,RPM 方案就白搭了,老老实实源码编译。
3. 源码编译全流程:从 m4 到 libpcap 一步步落地
3.1 先确认系统基础工具:gcc、make、kernel-devel
在编译任何东西之前,先确认内网机器的基本编译环境是否可用。执行:
rpm -q gcc make kernel-headers kernel-devel如果 gcc 都没有,那离线安装 libpcap 之前还得先离线装 gcc,这就又是另一套 RPM 依赖树了。实操中我见过很多内网机器在交付时已经带了 GCC 和 Make,但 kernel-devel 不一定有,而 libpcap 编译时如果检测到内核头文件版本不匹配,可能在某些 feature 上直接给你 disable 掉,或者报一些奇怪的错误。
我的建议是:至少保证rpm -q gcc make有输出,make -v和gcc --version能正常执行。如果连 gcc 都没有,可以考虑用 CentOS 7 安装光盘作为本地 yum 源,从光盘里装 gcc,这一点不展开,但值得先确认。
3.2 编译 m4 与 bison:解决 configure 阶段的“工具链缺失”
把源码包传到内网机器的/opt/src目录后,开始按顺序编译。
先编 m4:
cd /opt/src tar xzf m4-1.4.19.tar.gz cd m4-1.4.19 ./configure --prefix=/usr/local make -j$(nproc) make install这里--prefix=/usr/local的目的是让整个工具链统一装到/usr/local下,避免和系统自带的/usr/bin下的老版本混在一起。CentOS 7 默认的/usr/bin/m4其实是有的,但版本通常比较老,如果直接系统 m4 能用,你也可以跳过这一步。我之所以自己编,是因为后面 bison 需要 m4,而我不想冒险用老版本 m4 去处理新版本 bison 的语法文件。
编译完 m4 之后,把/usr/local/bin加到 PATH 里,确保后续 configure 能找到新工具:
export PATH=/usr/local/bin:$PATH接下来编译 bison:
cd /opt/src tar xzf bison-3.0.4.tar.gz cd bison-3.0.4 ./configure --prefix=/usr/local make -j$(nproc) make install如果你的系统里已经有 bison 且版本在 3.0 以上,可以跳过这一步。但考虑到内网环境千奇百怪,直接自己编一份最稳。
3.3 编译 flex:避免“neither flex nor lex found”
flex 是 libpcap 编译过程中最容易出问题的点。libpcap 的 configure 脚本会做这样的检查:
checking for flex... no checking for flex... no checking for lex... no configure: error: Your operating system's lex is insufficient to compile libpcap这个报错的意思就是系统里没有 flex,也没有传统的 lex。有些老教程让你去装 lex 兼容包,但在 CentOS 7 上最直接的解决办法就是编译 flex:
cd /opt/src tar xzf flex-2.6.4.tar.gz cd flex-2.6.4 ./configure --prefix=/usr/local make -j$(nproc) make installflex 2.6.4 在 CentOS 7 的 GCC 4.8.5 下编译没有太大问题,偶尔会有几个 warning,不影响生成可执行文件。编译完成后验证一下:
/usr/local/bin/flex --version如果输出的是flex 2.6.4,说明这一步过了。
3.4 编译 libpcap:configure 参数与可选依赖处理
工具链备齐后,终于可以进入正题,编译 libpcap:
cd /opt/src tar xzf libpcap-1.9.1.tar.gz cd libpcap-1.9.1 ./configure --prefix=/usr/local --disable-dbus --without-libnl make -j$(nproc) make install这里的两个参数说一下:
--disable-dbus:libpcap 的蓝牙监控功能依赖 D-Bus,内网服务器上通常没有dbus-devel,不显式禁用的话,configure 可能会自动检测并尝试链接,导致编译环境不一致。--without-libnl:libnl 主要用于 Wi-Fi 接口和某些 netlink 功能,普通以太网抓包用不到。如果系统里没有 libnl 的开发库,不指定这个参数,configure 也会自动禁用,但显式写上能减少config.log里的干扰信息。
configure 结束后建议看一眼输出摘要,里面会列出各个可选组件的启用情况:
libpcap version 1.9.1 ... libnl: no dbus: no只要 grep 到libnl: no、dbus: no,就说明这些可选依赖已经被禁用,不会影响编译。如果某些选项显示yes,说明系统里有对应的库,这也是正常的。
编译完成后默认安装到/usr/local/lib和/usr/local/include。接下来要让动态链接器找到这个库:
echo /usr/local/lib > /etc/ld.so.conf.d/libpcap-local.conf ldconfig这一步非常关键。如果没有这段配置,后面运行依赖 libpcap 的程序时,大概率会报libpcap.so.1: cannot open shared object file。
3.5 安装后的验证:tcpdump 版本与 C 程序测试
装完别急着走,先做一轮验证。
如果系统里已经带了 tcpdump,可以用它来确认链接的是新版 libpcap:
ldd /usr/sbin/tcpdump | grep pcap如果显示/usr/local/lib/libpcap.so.1,说明系统 tcpdump 已经用上了新版库。但更稳妥的做法是写一个简单的 C 程序,直接调用 libpcap 的 API,确认接口可用。下面这个程序会枚举本机所有网卡设备并打印名字:
#include <stdio.h> #include <pcap.h> int main(void) { char errbuf[PCAP_ERRBUF_SIZE]; pcap_if_t *alldevs = NULL; if (pcap_findalldevs(&alldevs, errbuf) == -1) { fprintf(stderr, "pcap_findalldevs error: %s\n", errbuf); return 1; } for (pcap_if_t *d = alldevs; d != NULL; d = d->next) { printf("%s\n", d->name); } pcap_freealldevs(alldevs); return 0; }编译命令:
gcc -o pcap_test pcap_test.c -I/usr/local/include -L/usr/local/lib -lpcap运行之前假设不在/etc/ld.so.conf.d里配置过,你需要指定库路径:
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH ./pcap_test如果能看到eth0或ens192之类的网卡名,说明 libpcap 已经可以正常使用。
4. 编译链接与运行期的典型报错排查
4.1 编译期找不到 pcap.h 和 -lpcap
常见报错有两种:
pcap.h: No such file or directory或者:
/usr/bin/ld: cannot find -lpcap第一种是头文件路径没指定,第二种是库文件路径没指定。原因很简单——libpcap 源码编译默认装到/usr/local/include和/usr/local/lib,而 GCC 默认搜索的路径是/usr/include和/usr/lib64,不会去/usr/local里找。
解决方式就是编译命令里显式加上:
gcc -o my_capture my_capture.c -I/usr/local/include -L/usr/local/lib -lpcap如果你想用得省心一点,可以设置两个环境变量:
export CFLAGS="-I/usr/local/include" export LDFLAGS="-L/usr/local/lib"然后编译时直接gcc -o my_capture my_capture.c -lpcap,GCC 会从环境变量里读取额外的搜索路径。
4.2 运行期 libpcap.so.1 无法加载
编译链接都过了,但运行时报:
./my_capture: error while loading shared libraries: libpcap.so.1: cannot open shared object file: No such file or directory这说明链接阶段没问题,但程序运行时动态链接器找不到libpcap.so.1。Linux 的动态链接器默认搜索的目录基本是/usr/lib64、/usr/lib,再加上/etc/ld.so.conf.d/里配置的目录。源码编译安装的/usr/local/lib并不在默认搜索范围内。
解决办法有两种:
第一种,临时设置环境变量:
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH ./my_capture这种方式只对当前 shell 生效,适合快速验证。
第二种,写入动态链接器配置:
echo /usr/local/lib > /etc/ld.so.conf.d/libpcap-local.conf ldconfig这种是全局生效的,写完后任何用户执行程序都能找到这个库。注意ldconfig是必须要执行的,光写配置文件不重新生成缓存,系统不会立刻生效。
如果程序是通过 systemd 托管的服务,还要在 service 文件里加:
Environment=LD_LIBRARY_PATH=/usr/local/lib或者使用RuntimeDirectory相关的配置,但更简单的还是用 ld.so.conf.d 方案。
4.3 pkg-config 找到旧版 .pc 文件
有些程序的构建系统会用 pkg-config 来查找 libpcap,如果执行:
pkg-config --modversion libpcap输出的不是 1.9.1,那就说明 pkg-config 找到的是系统自带的旧版.pc文件。CentOS 7 的 libpcap-devel 会安装/usr/lib64/pkgconfig/libpcap.pc,里面写的是 1.5.3 的版本信息和路径。
解决方法是指定 PKG_CONFIG_PATH:
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig pkg-config --modversion libpcap看到1.9.1就对了。如果希望以后的路程都指向新版本,可以把这行写进/etc/profile.d/libpcap.sh。
4.4 链接到旧库的隐性坑
最隐蔽的问题不是找不到库,而是同时存在两个libpcap.so.1,程序链接到了新版,但运行加载的却是系统旧版。你编译时明明链接的是/usr/local/lib/libpcap.so,可是程序运行后ldd ./my_capture显示/usr/lib64/libpcap.so.1,用的还是旧库。
原因在于动态链接器加载共享库时,是根据 SONAME 来找的,两个库的 SONAME 都是libpcap.so.1,所以运行时到底加载哪一个,取决于动态链接器的搜索路径顺序。这时候LD_LIBRARY_PATH或ld.so.conf.d里/usr/local/lib的优先级就变得非常关键。我建议在/usr/local/lib存在的情况下,把LD_LIBRARY_PATH设置好,或者直接在编译时写死 rpath:
gcc -o my_capture my_capture.c -I/usr/local/include -L/usr/local/lib -Wl,-rpath,/usr/local/lib -lpcap-Wl,-rpath,/usr/local/lib会把/usr/local/lib写进可执行文件的 RUNPATH 中,运行时动态链接器会优先去这个目录找,不受环境变量影响。这在部署到很多节点时特别有用,不会因为个别节点的环境变量配置遗漏出问题。
5. 和系统 RPM 版 libpcap 的冲突与权限处理
5.1 两个 libpcap.so.1 并存时,动态链接器怎么选
如果你之前用 RPM 装过 libpcap,源码编译又装了一份,两者都是libpcap.so.1,就出现了同名库共存的问题。RPM 版的动态库在/usr/lib64/libpcap.so.1,源码版在/usr/local/lib/libpcap.so.1。
动态链接器的搜索顺序大致是:
LD_LIBRARY_PATH环境变量指定的目录;- 可执行文件里 RUNPATH 指定的目录(如果有
-rpath); /etc/ld.so.conf.d/中配置的目录;- 默认目录
/lib64、/usr/lib64。
所以如果你设置了LD_LIBRARY_PATH=/usr/local/lib,那么运行时会优先用新版;如果没有设置,即使编译时链接的新版,运行也可能会加载旧版,导致某些新接口调用失败。
我的建议是两条路二选一:要么把系统 RPM 版卸载干净,只保留源码版;要么不要卸载,但所有需要新版接口的程序都通过-Wl,-rpath,/usr/local/lib编译,并且用ldd检查最终运行加载路径。生产环境不建议随意卸载系统自带的 libpcap,因为tcpdump、yum等工具可能依赖它,卸载后可能引发连锁问题。
5.2 静态链接 libpcap.a:一劳永逸绕过运行时路径问题
如果你要分发的程序必须在多台内网机器上运行,每台机器去配LD_LIBRARY_PATH或者改 ld.so.conf 实在太麻烦,那可以考虑静态链接 libpcap,把库直接编进可执行文件里。
libpcap 源码编译后会在/usr/local/lib下生成libpcap.a静态库,你可以这样编译:
gcc -o my_capture my_capture.c -I/usr/local/include /usr/local/lib/libpcap.a -lpthread注意要显式传递静态库路径,或者用:
gcc -o my_capture my_capture.c -I/usr/local/include -L/usr/local/lib -Wl,-Bstatic -lpcap -Wl,-Bdynamic这种方式编译出来的程序不再依赖libpcap.so.1,运行时不关心目标机器上有没有装 libpcap,非常适合在大量内网服务器上批量分发。缺点是二进制体积会变大,并且如果 libpcap 有安全更新,你需要重新编译程序才能应用新版本。对于流量采集这种对版本敏感的场景,我认为静态链接利大于弊。
5.3 非 root 用户的抓包权限:setcap 怎么设置
这是另一个高频问题。程序编译好了,用 root 跑没问题,但切到普通用户就开始报权限错误,比如:
tcpdump: raw socket: Operation not permitted原因是 libpcap 抓包时通过AF_PACKET协议族创建套接字,这个操作需要CAP_NET_RAW权限,普通用户默认没有。
解决办法不是给用户 sudo 权限,而是用 setcap 给可执行文件单独授权:
sudo setcap cap_net_raw,cap_net_admin=eip /usr/local/bin/my_capture这里cap_net_raw是抓包必需的能力,cap_net_admin是设置网卡混杂模式需要的(如果程序会调pcap_set_promisc)。=eip分别表示 effective、inheritable、permitted,eip三个标志位可以简写成=ep,但为了保险我一般写eip。
验证一下:
getcap /usr/local/bin/my_capture输出如果是:
/usr/local/bin/my_capture = cap_net_admin,cap_net_raw+ep说明授权成功,普通用户可以直接运行这个程序抓包。
注意一个坑:setcap 之后的文件不能再有写权限,否则内核会丢弃 capabilities。所以不要在编译目录里对my_capture做 setcap,然后又在同一个目录下重新 make,那样 capabilities 会被清掉。正确做法是先把二进制拷贝到安装目录,再执行 setcap。
如果是在容器里部署,还需要在 docker run 时加上:
--cap-add=NET_RAW --cap-add=NET_ADMIN或者在 docker-compose 里配置cap_add,否则容器内的进程同样没有权限创建AF_PACKET套接字。
最后分享一个小经验:我在内网机器的/opt/src目录下,把 m4、bison、flex、libpcap 四个源码包连同安装日志、configure 的 config.log 都保存在那里,下次再需要离线编译别的 C 库时,工具链已经是现成的,直接复用即可。建议你也把这次用到的所有源码包备份到一个单独的目录,别随手删了,内网环境重新下载一次源码包的成本远比你想象的高。