1. 为什么放着现成的bind不装,非要从源码编译
先说结论:如果你只是想在CentOS 8上搭一个能用、够用的DNS服务器,yum install bind一行命令就搞定了,这篇文章的很多内容你都用不上。但如果你遇到的是下面这些情况,才真的需要走源码编译这条路。
首先是版本太旧。CentOS 8发行版自带的bind版本停留在9.11.x,这个版本在2022年就已经结束常规维护了,只留了安全修复通道。但DNS领域的变化从来没停过,比如新的攻击手法、EDNS Client Subnet的解析优化、对DoT(DNS over TLS)和DoH(DNS over HTTPS)的支持,这些新特性如果你不去升级bind版本,根本拿不到。企业内网做递归解析的,对性能和安全的敏感度极高,停留在一个EOL版本上,说实话心里是不踏实的。
其次是系统自带的bind想改编译参数很痛苦。rpm包在打包时固定了一组编译选项,比如是否启用DNSSEC验证、是否带GeoIP支持、Listen端口绑定方式,这些在编译阶段就定死了,运行阶段你改不了。我遇到过需要把bind的max-cache-size调大、需要启用new-zones特性,结果用系统自带包怎么调都调不到想要的最终效果,最后只能回归源码编译。
最后还有一个现实问题:新版本的bind对OpenSSL、libuv这些基础库版本有明确要求,CentOS 8系统库相对保守,直接装新版bind的二进制包会出现so库版本不匹配的情况。而源码编译时绑定到系统已有的库上去构建,反而能绕开一部分兼容性问题。
所以我的判断标准很直接:能用旧版凑合就凑合,一旦业务对DNS解析的性能、安全特性或者特殊配置有硬性要求,就该上源码编译这条路。这篇文章就是从零开始,在CentOS 8上从源码编译安装新版bind9的完整记录。
2. 编译前的地基:依赖环境准备,这一步偷懒后面全是坑
2.1 CentOS 8 EOL之后,先把源切对
有一个很容易被忽略的前提:CentOS 8在2024年5月已经正式EOL了。默认的mirrorlist.centos.org源会报404或者连不上,如果你不处理这个问题,后面连yum install都会失败,更别提编译依赖了。这一步不说清楚,很多新手会在装依赖时直接卡死。
我当时用的是vault.centos.org的源。先把你系统的repo文件里的mirrorlist注释掉,把baseurl改到vault源。具体操作是用sed批量替换。
cd /etc/yum.repos.d/ sed -i 's/mirrorlist=/#mirrorlist=/g' *.repo sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' *.repo sed -i 's|baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' *.repo替换完以后先把缓存清掉,再重新建立:
yum clean all yum makecache这里有个细节:如果你用的CentOS版本是8.5.2111,你的repo文件里写的是$releasever变量,它会被替换成8,而vault源还需要精确到小版本号。稳妥的做法是手动把repo文件里的$releasever改成8.5.2111,或者直接用--releasever=8.5.2111参数执行yum命令。
2.2 编译工具的完整清单
先装基础编译工具链:
yum install -y gcc gcc-c++ make cmake这就是新版bind和旧版在构建系统上的一个关键差异:bind 9.16及更早的版本用的是Autotools体系,执行./configure就行;但是bind 9.18开始切换到CMake构建系统。所以如果你准备编译的是9.18或者更新版本,cmake必须装上,这和以前装bind源码的体验完全不一样。
然后是bind核心依赖,每一个都有明确的用途:
libuv-devel:bind 9.16以后用它替代了自研的事件处理库,网络I/O、定时器、信号处理全走libuv。没有这个包,你编译100%会报错。openssl-devel:提供TLS和加密算法库。bind 9.18的DNS over TLS、DNSSEC验证都依赖它。不装的话,configure阶段会警告禁用TLS。libcap-devel:提供Linux Capabilities支持,让bind在非root用户下也能绑定53端口。这个对生产环境非常重要。libxml2-devel和json-c-devel:分别提供XML和JSON输出支持,给rndc status、named-checkconf这些命令提供统计信息的序列化能力。pkg-config:CMake在检测依赖时主要依赖pkg-config来查找库文件路径,不装的话很多依赖检测不到。
我建议一次性装全:
yum install -y libuv-devel openssl-devel libcap-devel libxml2-devel json-c-devel pkg-config顺便把文档生成时会用到的工具也装了,比如python3-docutils和perl,避免编译到一半因为docbook格式问题中断。
2.3 小坑提醒:依赖版本与链接方式
CentOS 8自带的libuv版本是1.40.0左右,而bind 9.18要求libuv >= 1.40.0,刚好卡线,没问题。但如果你用的是CentOS 8 Stream或者自己更新过部分库,版本跨度可能会带来意想不到的问题。
另外注意一点,CentOS 8的OpenSSL默认是1.1.1,而bind 9.20系列要求OpenSSL >= 1.1.1,也能满足。如果哪天CentOS 8装的是OpenSSL 3.0以上的版本,编译时很可能遇到OPENSSL_API_COMPAT相关的警告甚至报错,碰到这种情况不要去改bind源码,应该考虑在configure阶段关闭部分TLS特性,或者降低OpenSSL版本。我在实际编译中没有遇到这个问题,但是提前了解这些边界,可以省掉很多查错时间。
3. 源码获取与CMake配置阶段:参数的坑,全在这层
3.1 从哪下载源码,怎么确认校验值
新版bind的源码可以从ISC官网下载,也可以去GitHub的isc-projects/bind9仓库拉Release tag。推荐走官网下载页面,因为会同时给出SHA256校验值,安全上更稳。
我编译时用的是bind 9.18.29这个版本,属于9.18维护分支的后期版本,修了一堆已知CVE,稳定性也好。
cd /usr/local/src wget https://downloads.isc.org/isc/bind9/9.18.29/bind-9.18.29.tar.xz wget https://downloads.isc.org/isc/bind9/9.18.29/bind-9.18.29.tar.xz.sha256 sha256sum -c bind-9.18.29.tar.xz.sha256校验输出出现OK字样后再解压:
tar -xf bind-9.18.29.tar.xz cd bind-9.18.293.2 CMake配置命令:理解参数比抄命令更重要
在bind 9.18及之后的新版中,传统的./configure --prefix=xxx变成了cmake -B build这种形式。它的配置过程可以通过cmake -L查看所有可选项,也可以在源码根目录下的README.md里找到官方推荐配置。
以下是我在实际编译时使用的配置方案:
cmake -B build \ -DCMAKE_INSTALL_PREFIX=/usr/local/bind9 \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_TESTING=off \ -DENABLE_THREADS=on \ -DENABLE_DNSTAP=off \ -DENABLE_AFL=off \ -DENABLE_LINUX_CAPABILITIES=on \ -DENABLE_DEVELOPER=off \ -DENABLE_FUZZING=off \ -DENABLE_LIBLZ4=off \ -DENABLE_LIBZSTD=off \ -DENABLE_LIBUV=on \ -DENABLE_OPENSSL=on \ -DENABLE_LIBXML2=on \ -DENABLE_JSON_C=on这里为什么这样选?我给你逐个拆解。
CMAKE_INSTALL_PREFIX很好理解,就是安装路径。我特意没用默认的/usr/local,而是单独放到/usr/local/bind9下。这样做的核心原因是不和系统自带的bind rpm包冲突。如果你之前用yum装过bind,rpm包里的二进制都在/usr/sbin/named、/etc/named.conf这些标准路径,而你源码编译的版本单独放一个目录,用哪个版本由你自己控制,启停服务时不会互相干扰。
DCMAKE_BUILD_TYPE=Release会让编译器开优化选项,线程模型默认启用,这对bind的性能影响非常直接。
ENABLE_LINUX_CAPABILITIES=on是关键。bind的日常运行不应该用root直接跑,必须降权到专用用户。但是这个CAP_NET_BIND_SERVICE权限要保留,否则无法监听53端口。Linux capabilities机制允许进程降权后仍保留“绑定小于1024端口”的能力,这就是这项配置的用处。生产环境务必开启。
ENABLE_DNSTAP默认关闭,因为启用后还需要额外装protobuf-c这样的依赖,而且大部分场景用不上。你在排查DNS解析延迟问题时可以后续再编译一个带dnstap的版本,没必要一开始就加上。
ENABLE_LIBUV和ENABLE_OPENSSL、ENABLE_LIBXML2、ENABLE_JSON_C这四个是显式指定编译依赖,避免CMake自动检测时漏掉或者找错库路径。在定制过库安装路径的系统上,这些显式选项能减少很多配置阶段的不确定性。
3.3 Make阶段可能出现的编译错误与处理
CMake配置成功后,执行:
cmake --build build -j$(nproc)这一步会编译一段时间,9.18系列的代码量不小,-j参数用来指定并行编译进程数,按CPU核心数来。如果你的机器内存不大,建议限制一下,比如-j2,否则编译过程中内存吃满可能直接被oom-killer干掉。
编译过程中有可能会遇到下面几个典型报错,我列出真实的处理经验。
如果你看到Could NOT find LibUV或者Could NOT find OpenSSL这种错误,先去yum list installed | grep libuv确认是否装了devel包。CentOS 8区分libuv和libuv-devel,运行库和解压头的开发包是两回事。只装了运行库,CMake是找不到头文件的。
如果你看到与json-c相关的错误,比如json_object_new_int64未定义引用,那一般是因为系统安装的是旧版json-c。虽然CMake检测通过了,但是链接阶段发现函数表对不上。这个问题的解决办法不是去换json-c版本,而是在CMake配置里增加-DENABLE_JSON_C=off,bind没有json-c也能正常编译运行,损失的只是rndc输出JSON格式统计数据的能力。
如果编译过程中报ISC_LANG_BEGIN_DECLS找不到等奇怪的宏错误,通常不是bind源码的问题,而是你下载的源码解压不完整,或者系统时间不对导致make依赖判断错乱。重新校验tar包后再解压一次,基本能解决。
编译完成后,当前路径下会生成build/bin/named/named这个二进制文件,这个就是我们需要的核心程序。在正式安装之前,建议先看一下它的动态链接依赖,确认编译产物引用的so库都在系统里存在。
ldd build/bin/named/named如果出现not found,说明某个动态库没装或者路径不对。这种检查一小步,能帮你省掉安装完以后服务起不来的大麻烦。
4. 安装与基础配置:编译装完,工作才完成一半
4.1 安装到目标路径
编译完成后执行:
cmake --install build这一步会把named、rndc、dig、nslookup、delv、named-checkconf、named-checkzone等一系列二进制文件拷贝到/usr/local/bind9/bin和/usr/local/bind9/sbin下面。
这里我建议把sbin目录做一个软链到/usr/local/sbin,或者把它加进PATH环境变量,不然每次执行named-checkconf都要写全路径,时间久了很难受。
ln -sf /usr/local/bind9/sbin/named /usr/local/sbin/named ln -sf /usr/local/bind9/sbin/rndc /usr/local/sbin/rndc ln -sf /usr/local/bind9/sbin/dig /usr/local/sbin/dig ln -sf /usr/local/bind9/sbin/named-checkconf /usr/local/sbin/named-checkconf ln -sf /usr/local/bind9/sbin/named-checkzone /usr/local/sbin/named-checkzone这里有个非常重要的点,系统里如果之前用yum装过bind,/usr/sbin/named可能已经存在。你软链到/usr/local/sbin之后,要注意/usr/local/sbin在PATH中的位置是否在/usr/sbin之前。可以用which named确认一下实际指向。
4.2 必须创建的运行用户与目录结构
新编译的bind不会自动创建运行用户,这一步需要手动做。安全上必须用非root用户运行named,这是bind文档里强调过的要求。
useradd -r -d /var/named -s /sbin/nologin named-r表示创建系统用户,-d指定它的home目录,-s /sbin/nologin防止有人用这个账户登录。然后创建bind运行所需的目录:
mkdir -p /var/named/chroot/var/named mkdir -p /var/named/chroot/var/run/named mkdir -p /var/named/chroot/etc mkdir -p /run/named chown -R named:named /var/named/chroot chown -R named:named /run/named如果你不打算启用chroot,那么直接把工作目录放到/var/named下:
mkdir -p /var/named/zones mkdir -p /run/named chown -R named:named /var/named chown -R named:named /run/namedchroot是bind的老话题了,把进程锁在固定目录里,即使被攻击也只影响chroot目录内的文件。但是chroot在故障排查时会给你带来额外负担,日志和文件路径都是双层结构。如果你的DNS服务器暴露在公网上,建议启用chroot;如果只是内网解析服务,普通目录就够了。
4.3 生成rndc密钥与最小可用配置
bind通过rndc命令来管理服务,它依赖一个预共享密钥。创建方式如下:
rndc-confgen -a -c /etc/rndc.key chown root:named /etc/rndc.key chmod 640 /etc/rndc.key如果不想用rndc-confgen -a生成单独文件,也可以在named.conf里直接写入key块、controls块。两种方式在效果上一样,但单独文件隔离性好,升级重启时不容易弄丢密钥,我推荐前者。
然后写一个最简的named.conf:
options { listen-on port 53 { any; }; listen-on-v6 port 53 { any; }; directory "/var/named"; dump-file "/var/named/data/cache_dump.db"; statistics-file "/var/named/data/named_stats.txt"; memstatistics-file "/var/named/data/named_mem_stats.txt"; recursion yes; allow-query { any; }; allow-recursion { 127.0.0.1; 10.0.0.0/8; }; dnssec-validation auto; }; zone "." IN { type hint; file "/var/named/named.ca"; }; include "/etc/rndc.key";这里把allow-query设为any,允许所有客户端查询;allow-recursion限制为本地回环和内部网段,避免被开放递归放大攻击利用。dnssec-validation auto用系统默认的root信任锚做DNSSEC校验,需要系统root key文件存在。
然后需要下载root zone hint文件:
cd /var/named curl -o named.ca https://www.internic.net/domain/named.root chown named:named named.ca4.4 把bind交给systemd管理
源码编译安装的一个好处是,你可以完全控制服务的启动参数。但随之而来的问题是没有现成的systemd unit文件。我写一个最小可用的服务文件,放到/etc/systemd/system/named.service:
[Unit] Description=BIND 9.18 DNS Server After=network.target [Service] Type=forking PIDFile=/run/named/named.pid ExecStart=/usr/local/bind9/sbin/named -c /etc/named.conf -u named ExecReload=/usr/local/bind9/sbin/rndc reload ExecStop=/usr/local/bind9/sbin/rndc stop Restart=on-failure [Install] WantedBy=multi-user.target注意Type=forking表示named启动后会fork到后台运行,PID文件路径要和named.conf里的pid-file设置一致,否则systemd无法准确判断服务是否存活。在options里加一行pid-file "/run/named/named.pid";,确保路径匹配。
写完以后加载并启动:
systemctl daemon-reload systemctl enable named systemctl start named启动以后用systemctl status named和ss -lunp | grep :53确认进程是否在监听。
5. 排错实录:编译和启动阶段最值得记录的坑
5.1 53端口被系统自带dnsmasq或NetworkManager抢占
CentOS 8桌面版或某些最小化安装上,dnsmasq经常默认占用53端口,NetworkManager的DNS代理也可能占用。启动named后如果发现Address already in use,可以用:
ss -lunp | grep :53看输出里哪条进程占用了53端口。如果是dnsmasq,直接systemctl stop dnsmasq并禁止开机启动;如果是NetworkManager的dns=dnsmasq配置,去/etc/NetworkManager/NetworkManager.conf里把dns=行注释掉再重启NetworkManager。
5.2 权限不足导致启动即失败
如果启动时日志里出现permission denied或者using default key,多半是目录权限或者capabilities没设置到位。
我遇到过的真实情况是named报错无法创建/var/named/data/cache_dump.db,最后发现是/var/named/data目录属主是root而不是named。调试这种问题最快的办法不是反复重启服务,而是执行:
journalctl -u named -f看named标准输出和错误输出,再配合named-checkconf检查配置。
需要注意的是,如果配置了linux-caps功能,建议用setcap cap_net_bind_service=+ep /usr/local/bind9/sbin/named给named加上绑定低端口的权限,不然即便用named用户启动,53端口还是无法绑定。这是我踩过一次的坑:named用户下启动时bind端口绑定不了,日志提示没有权限,而我用root启动就正常,浪费了两个小时才想到是capabilities的问题。
5.3 chroot模式下找不到zone文件
如果你按照网上教程配置了chroot,需要清楚bind在chroot下看到的/etc/named.conf实际是宿主机上的/var/named/chroot/etc/named.conf,而zone文件的路径则在/var/named/chroot/var/named/里。
一个经典错误是:明明把zone文件放进了/var/named/zones目录,named 却报文件不存在。原因就是配置文件里写的是file "/var/named/zones/example.zone";,但chroot后它真实查找的路径是/var/named/chroot/var/named/zones/example.zone。你需要确认目录是否存在,并且把zone文件拷贝到chroot目录里。
如果你是新手,我建议第一版先不要开chroot,直接把DNS服务跑起来,验证通了以后再做加固。否则chroot的路径问题会和配置问题混在一起,排查难度直接翻倍。
5.4 DNSSEC验证失败导致的解析异常
还有一个比较隐蔽的问题:如果配置了dnssec-validation auto,但系统里没有root信任锚文件,或者缓存了旧的DS记录,你可能会发现某些域名可以解析,某些域名迟迟不返回结果。
排查方法是临时把配置改成dnssec-validation no;,如果问题消失,说明就是DNSSEC相关。此时可以执行:
dig . DNSKEY | grep -E "DNSKEY|RRSIG"确认root信任锚能正常获取,或者用rndc flush清空缓存后重试。绝大多数情况是根信任锚获取失败,而不是bind本身出问题。
6. 生产环境下的进阶考量
6.1 安全加固:从裸奔到像样的配置
跑通之后,如果这台DNS要长期服役,还需要做三件事。
第一是allow-transfer。如果你的服务器不是权威DNS,尽量设置allow-transfer { none; };,避免别人恶意拉取区域数据。如果是主从结构,这里应该限死为从服务器的IP。
第二是配置ACL。在named.conf里定义:
acl internal { 10.0.0.0/8; 172.16.0.0/12; 192.168.0.0/16; 127.0.0.1; }; options { allow-query { internal; }; allow-recursion { internal; }; };这样能限制外部用户通过你的服务器做递归解析,防止被用来做DDoS放大攻击。
第三是日志配置。默认情况下bind把日志写到syslog,排查问题非常难用。建议单独配置日志:
logging { channel default_log { file "/var/named/log/named.log" versions 3 size 5m; severity info; print-time yes; print-category yes; }; category default { default_log; }; category queries { default_log; }; };记住,日志文件目录需要提前创建并chown给named用户,否则bind会因为无法写入日志文件而拒绝启动。
6.2 验证安装成功以及性能基准建议
安装完成后,做一轮功能验证:
dig @127.0.0.1 www.baidu.com如果拿到正确的A记录,说明递归解析功能正常。然后测试正向反向解析,可以用delv来验证DNSSEC,用rndc status检查服务器运行状态。
生产环境正式上线前,建议用dnsperf这类工具做一次性能基准测试。我在内网环境里测过,源码编译的9.18版本在并发解析场景下,QPS(每秒查询数)明显比yum自带的9.11高出不少,延迟也更稳定。这背后的原因,一方面是libuv事件库的高并发处理能力,另一方面是新版本对缓存和递归算法的优化,这两项特性在编译后是天然生效的,不需要额外配置。如果你所在环境的DNS请求量比较大,这个收益值得关注。
6.3 升级与卸载的注意事项
源码编译安装的升级路径,和yum包完全不同。以后从9.18升级到9.20系列时,需要重新走一遍下载源码、CMake配置、编译、安装的流程。安装后检查一下/usr/local/bind9/etc下面的是否有新旧配置冲突,尤其是named.conf里的语法、zone文件格式有没有变化。
如果以后不想要这个源码编译版本了,卸载也很简单,直接删掉/usr/local/bind9整个目录、移除软链接、删除/etc/systemd/system/named.service和/etc/rndc.key,最后systemctl daemon-reload即可。因为没有rpm包,所以没有任何包管理器的残留记录,这也是源码编译的一个实际优点。
我在实际使用中还有一个习惯:每次升级前用tar备份当前的/etc/named.conf和所有zone文件,放到/root/bind_backup/下。源码编译的升级很简单,但配置文件的兼容性问题才是真正容易出幺蛾子的地方。备份做了,升级出错就能随时回滚。