1. 项目缘起与整体设计思路
很多中小型网络环境里,DNS往往是最容易被忽视的一环。大家习惯性地把域名解析交给上游服务商,或者随便找一台机器装个DNS软件就完事。直到某天内网服务大面积解析异常、外网访问时快时慢、运维排查发现解析记录被人误改,才意识到DNS这块地基没打牢。我这次要聊的,就是一套在真实内网环境中反复验证过的方案:DNS主从服务器配合分离解析,用一台主服务器、一台从服务器,加上视图(view)机制,把内网用户和外网用户的解析请求彻底分开处理。
先说清楚这套方案到底解决什么问题。第一,单点故障。只有一台DNS服务器时,它挂了整个内网就瘫了,主从架构让从服务器自动同步主服务器的区域数据,主服务器宕机时从服务器顶上,解析不中断。第二,内外网解析混乱。同一个域名,内网用户应该解析到内网地址(走内网高速通道),外网用户应该解析到公网地址,如果混在一起,要么内网流量绕到公网出口再回来,要么外网用户拿到内网地址根本访问不了。第三,管理效率。所有解析记录只在主服务器上维护,从服务器自动同步,不用两头改,减少人为出错。
这套方案适合谁?我总结下来是三类人:一是中小企业或园区的网络运维,内网有一定规模但预算有限,不想买商业DNS设备;二是自己搭实验环境学习DNS原理的技术爱好者,主从加分离解析是DNS进阶的必经之路;三是需要给多个业务系统做域名隔离的开发者,比如测试环境和生产环境用不同解析结果。不管你属于哪一类,只要跟着把配置走一遍,基本能掌握DNS服务里最核心的几块内容。
方案选型上,我用的软件是BIND(Berkeley Internet Name Domain),这是目前最成熟、文档最全的开源DNS服务实现,几乎所有Linux发行版都能直接装。为什么不用dnsmasq或者CoreDNS?dnsmasq更适合小型转发场景,对主从同步和视图分离的支持比较弱;CoreDNS插件化设计很灵活,但配置视图分离需要额外插件,学习成本反而更高。BIND原生支持主从复制(zone transfer)和视图(view),配置直观,排错资料多,是这类需求的首选。
整体架构是这样:主服务器(假设内网地址为192.168.10.11)负责维护所有区域文件,包括内网视图和外网视图两套解析数据;从服务器(192.168.10.12)通过区域传送从主服务器同步数据,同样配置两套视图。客户端根据来源IP被划分到不同视图,内网网段走内网视图,其他来源走外网视图。主从之间通过TSIG密钥做传输认证,防止未授权的区域传送。下面我按实际搭建顺序,把每一步的原理、参数和踩坑点都拆开讲。
2. 核心细节解析与实操要点
2.1 主从复制的工作机制与关键参数
主从复制的核心是区域传送(Zone Transfer)。主服务器上的区域文件发生变化时,会通过NOTIFY消息通知从服务器,从服务器收到后发起传送请求,把最新的区域数据拉过来。传送方式有两种:AXFR(全量传送)和IXFR(增量传送)。AXFR每次把整个区域文件传一遍,数据量大时开销明显;IXFR只传变化的部分,效率高,但需要主服务器保留变化日志。实际配置中,我一般让BIND自动选择,主服务器开启IXFR支持,从服务器优先请求增量,失败时回退全量。
关键参数在named.conf里,主服务器的区域定义中要写type master,从服务器写type slave,并用masters指定主服务器地址。还有一个容易被忽略的参数是allow-transfer,必须限制只允许从服务器IP发起传送,否则任何人都能把你的区域文件拉走,等于把内网拓扑暴露了。我见过不少配置直接写allow-transfer { any; };,这是典型的危险操作。
序列号(Serial)是主从同步的触发依据。每次修改区域文件后,必须手动递增Serial值,从服务器才会认为数据有更新。Serial通常用日期加序号的形式,比如2025010101表示2025年1月1日第1次修改。这里有个坑:如果你改了记录但忘了改Serial,从服务器不会同步,排查时容易误以为是网络问题。我的习惯是每次编辑区域文件前先改Serial,形成肌肉记忆。
2.2 分离解析的视图机制与匹配逻辑
分离解析靠的是BIND的**视图(view)**功能。一个视图就是一套独立的解析策略,包含自己的区域文件和选项。客户端请求到达时,BIND按named.conf中视图定义的顺序逐个匹配match-clients,第一个匹配上的视图生效。所以视图的顺序很重要,通常把范围小的内网视图放前面,范围大的外网视图放后面。
match-clients支持IP网段、ACL(访问控制列表)和密钥。我一般先定义一个ACL,把内网网段写进去,视图里引用这个ACL,这样管理起来清晰。比如:
acl "internal-net" { 192.168.10.0/24; 192.168.20.0/24; 127.0.0.1; };然后内网视图写match-clients { internal-net; };,外网视图写match-clients { any; };。注意any必须放在最后,否则它会匹配所有请求,后面的视图永远轮不到。
每个视图里都要完整定义根区域(hint)、本地回环解析和需要解析的域名区域。这里有个常见错误:只在内网视图里定义域名区域,外网视图忘了定义,结果外网用户解析该域名时得到REFUSED。视图之间是隔离的,不会互相继承,每个视图都得写全。
2.3 区域文件编写规范与记录类型
区域文件(zone file)的语法看着简单,但细节很多。开头必须是$TTL定义默认生存时间,然后SOA记录,接着NS记录,最后才是各种A、CNAME、MX等记录。SOA记录里的参数直接影响从服务器同步行为:
- Serial:序列号,前面说过,每次修改必须递增。
- Refresh:从服务器多久检查一次主服务器是否有更新,我设的是3600秒(1小时)。
- Retry:同步失败后多久重试,设600秒(10分钟)。
- Expire:从服务器联系不上主服务器时,多久后停止对外提供该区域解析,设604800秒(7天)。
- Minimum:否定缓存时间,设86400秒(1天)。
这些值不是随便填的。Refresh太短会增加主服务器负担,太长会导致更新延迟;Expire太短,主从网络短暂中断就会导致从服务器停止解析,太长又可能提供过期数据。我实测下来,中小企业环境用上面这组值比较均衡。
记录类型里,A记录是IPv4地址,AAAA是IPv6,CNAME是别名,MX是邮件交换,PTR是反向解析。分离解析场景下,同一个主机名在内网视图和外网视图的A记录值不同,这是核心。比如app.example.com在内网视图解析到192.168.10.100,在外网视图解析到203.0.113.50。注意CNAME记录不能和MX记录共存于同一名称,这是DNS协议限制,踩过一次坑,排查了半天。
2.4 TSIG密钥与传输安全
主从之间的区域传送如果不加密不认证,任何知道主服务器IP的人都能请求传送。TSIG(Transaction Signature)用共享密钥对传送消息做签名,主从双方配置相同密钥,验证通过才允许传送。生成密钥用tsig-keygen命令:
tsig-keygen -a HMAC-SHA256 transfer-key输出会包含密钥名称和密钥值,把这段配置分别放到主从服务器的named.conf里。主服务器的allow-transfer写{ key "transfer-key"; };,从服务器的masters写{ 192.168.10.11 key "transfer-key"; };。这样即使有人伪造从服务器IP,没有密钥也传不走数据。
密钥算法选HMAC-SHA256,比HMAC-MD5更安全,现代BIND版本都支持。密钥值要保密,配置文件权限设成640,属主是named用户。我见过把密钥写在公开文档里的,等于没设防。
3. 实操过程与核心环节实现
3.1 环境准备与软件安装
我用的系统是常见的Linux发行版,两台机器:主服务器192.168.10.11,从服务器192.168.10.12。系统最小化安装后,先更新软件源,然后安装BIND:
# 主从服务器都执行 sudo apt update sudo apt install bind9 bind9utils bind9-doc -y安装完成后,BIND服务默认可能已经启动,先停掉,等配置改完再启动:
sudo systemctl stop named sudo systemctl disable named配置文件在/etc/bind/目录下,主要文件有named.conf(主配置)、named.conf.options(全局选项)、named.conf.local(本地区域定义)、named.conf.default-zones(默认区域)。我习惯把自定义配置写在named.conf.local里,全局选项改named.conf.options,这样升级时不容易被覆盖。
防火墙方面,DNS主要用53端口,TCP和UDP都要放行。主从之间还需要TCP 53做区域传送。如果开了防火墙,记得加规则:
sudo ufw allow 53/tcp sudo ufw allow 53/udp sudo ufw allow from 192.168.10.12 to any port 533.2 主服务器全局选项配置
先改named.conf.options,重点是监听地址、允许查询范围和转发器。监听地址写主服务器自己的IP和回环:
options { directory "/var/cache/bind"; listen-on port 53 { 127.0.0.1; 192.168.10.11; }; listen-on-v6 { none; }; allow-query { any; }; recursion yes; forwarders { 8.8.8.8; 114.114.114.114; }; dnssec-validation auto; auth-nxdomain no; };这里allow-query设any是因为要同时服务内外网用户,具体哪些请求走哪个视图由视图的match-clients控制。forwarders是转发器,本地解析不了的域名转发给上游。recursion yes允许递归查询,内网客户端需要这个。dnssec-validation auto开启DNSSEC验证,增强安全性。
注意:如果服务器只做权威解析不做递归,
recursion要设no,allow-query也要收紧。我这里因为要服务内网客户端,所以开了递归。
3.3 定义ACL与TSIG密钥
在named.conf开头(或单独的named.conf.local里)定义ACL和密钥:
acl "internal-net" { 192.168.10.0/24; 192.168.20.0/24; 127.0.0.1; }; key "transfer-key" { algorithm hmac-sha256; secret "你生成的密钥值"; };密钥值用tsig-keygen生成后粘贴进来。ACL里把内网网段和回环都写上,回环地址用于本机测试。
3.4 主服务器视图与区域配置
接下来是核心部分,在named.conf.local里定义两个视图。先写内网视图:
view "internal" { match-clients { internal-net; }; recursion yes; zone "." { type hint; file "/usr/share/dns/root.hints"; }; zone "localhost" { type master; file "/etc/bind/db.local"; }; zone "10.168.192.in-addr.arpa" { type master; file "/etc/bind/db.192.168.10"; }; zone "example.com" { type master; file "/etc/bind/db.example.com.internal"; allow-transfer { key "transfer-key"; }; also-notify { 192.168.10.12; }; }; };再写外网视图:
view "external" { match-clients { any; }; recursion no; zone "example.com" { type master; file "/etc/bind/db.example.com.external"; allow-transfer { key "transfer-key"; }; also-notify { 192.168.10.12; }; }; };几个关键点解释一下。内网视图开了递归,因为内网客户端需要解析外部域名;外网视图关了递归,只做权威解析,防止被利用做DNS放大攻击。also-notify让主服务器在区域变化时主动通知从服务器,加快同步速度。反向解析区域10.168.192.in-addr.arpa用于内网IP反查主机名,外网视图里不需要。
3.5 区域文件编写实例
内网视图的区域文件db.example.com.internal:
$TTL 86400 @ IN SOA ns1.example.com. admin.example.com. ( 2025010101 ; Serial 3600 ; Refresh 600 ; Retry 604800 ; Expire 86400 ; Minimum TTL ) @ IN NS ns1.example.com. @ IN NS ns2.example.com. ns1 IN A 192.168.10.11 ns2 IN A 192.168.10.12 app IN A 192.168.10.100 db IN A 192.168.10.101 www IN CNAME app外网视图的区域文件db.example.com.external:
$TTL 86400 @ IN SOA ns1.example.com. admin.example.com. ( 2025010101 ; Serial 3600 ; Refresh 600 ; Retry 604800 ; Expire 86400 ; Minimum TTL ) @ IN NS ns1.example.com. @ IN NS ns2.example.com. ns1 IN A 203.0.113.10 ns2 IN A 203.0.113.11 app IN A 203.0.113.50 www IN CNAME app注意两个文件的Serial要一致,从服务器同步时按Serial判断是否需要更新。内网app解析到192.168.10.100,外网解析到203.0.113.50,这就是分离解析的效果。
3.6 从服务器配置与同步验证
从服务器的named.conf.options和主服务器类似,监听地址改成自己的IP。视图配置里,区域类型改成type slave,masters指向主服务器并带上密钥:
view "internal" { match-clients { internal-net; }; recursion yes; zone "example.com" { type slave; file "/var/cache/bind/db.example.com.internal"; masters { 192.168.10.11 key "transfer-key"; }; }; };外网视图同理。从服务器的区域文件不用手动创建,BIND同步后会自动生成到file指定的路径。配置完成后,先启动主服务器,再启动从服务器:
sudo systemctl start named sudo systemctl enable named验证同步是否成功,在从服务器上查日志:
sudo tail -f /var/log/syslog | grep named看到transfer of 'example.com/IN/internal' from 192.168.10.11#53: Transfer completed就说明同步成功。也可以用dig命令测试:
dig @192.168.10.12 app.example.com如果返回的地址和主服务器一致,说明从服务器工作正常。
3.7 分离解析效果验证
验证分离解析,需要从不同来源IP发起查询。内网机器上执行:
dig @192.168.10.11 app.example.com应该返回192.168.10.100。外网机器(或模拟外网来源)执行同样命令,应该返回203.0.113.50。如果结果不对,检查视图的match-clients顺序和ACL定义。我踩过一次坑:ACL里写了192.168.10.0/24,但测试机IP是192.168.10.200,按理说应该匹配,结果却走了外网视图。排查发现是named.conf里视图顺序写反了,外网视图的any放在了前面,所有请求都被它截胡了。调整顺序后正常。
4. 常见问题与排查技巧实录
4.1 主从同步失败排查速查表
主从同步出问题是这类配置里最高频的故障。我把常见现象、原因和解决方法整理成表,方便对照排查:
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 从服务器日志无传送记录 | 主服务器未通知或网络不通 | 主服务器also-notify是否配从IP;telnet 主IP 53测试连通性 | 补配also-notify;放行防火墙 |
| 传送被拒绝 | TSIG密钥不匹配或allow-transfer未放行 | 主服务器日志看transfer of ... denied | 核对两端密钥名称和值;检查allow-transfer |
| 同步后记录仍旧 | Serial未递增 | 对比主从区域文件Serial值 | 修改主服务器区域文件后递增Serial |
| 从服务器区域文件为空 | 文件路径权限问题 | 检查/var/cache/bind目录属主和权限 | chown bind:bind并确保可写 |
| 同步延迟大 | Refresh值过大或NOTIFY未送达 | 查看主服务器also-notify和从服务器Refresh值 | 调小Refresh;确认NOTIFY端口53可达 |
这张表覆盖了我遇到过的八成同步问题。特别提醒:修改主服务器区域文件后,一定要rndc reload或重启named让配置生效,光改文件不重载等于没改。
4.2 分离解析不生效的典型场景
分离解析不生效,表现是内外网用户拿到相同解析结果。除了前面说的视图顺序问题,还有几个隐蔽原因。一是match-clients里用了localhost而不是127.0.0.1,某些BIND版本对localhost的解析依赖系统hosts文件,可能匹配不上。二是ACL定义在视图之后,BIND解析配置时ACL必须先定义后引用,顺序错了会报语法错误或静默失效。三是客户端走了缓存,之前查询的结果被本地或中间DNS缓存了,改配置后要清缓存再测。我一般用dig +norecurse直接问权威服务器,绕过缓存干扰。
还有一个容易忽略的点:如果内网客户端配置的DNS服务器地址是主服务器的外网IP,那它的请求来源会被识别为外网,走外网视图。所以内网客户端的DNS设置必须指向内网IP。这个在跨网段环境里尤其要注意。
4.3 性能与安全加固经验
DNS服务跑起来容易,跑得稳、跑得安全需要额外功夫。性能方面,BIND默认的缓存大小可能不够,可以在named.conf.options里加max-cache-size 256M;,根据内存调整。查询日志默认不开,排查问题时可以临时开querylog,但生产环境长期开着会吃磁盘,记得关。
安全方面,除了TSIG和allow-transfer限制,还要注意:关闭版本信息泄露,在options里加version "not currently available";,防止有人通过dig chaos txt version.bind探测BIND版本;限制递归查询来源,外网视图已经关了递归,内网视图的allow-recursion可以进一步收紧到内网ACL;定期检查区域文件权限,确保只有named用户可读。
提示:
rndc是BIND的管理工具,可以不用重启就重载配置、查看状态。配置rndc.key后,用rndc reload重载区域,rndc status看运行状态,比systemctl restart温和得多,生产环境推荐用这个。
4.4 日常维护与记录管理心得
主从加分离解析的环境,日常维护主要是改记录和看同步。改记录的标准流程是:先改主服务器区域文件,递增Serial,rndc reload,然后等从服务器自动同步,最后在从服务器上dig验证。不要直接改从服务器的区域文件,因为下次同步会被覆盖。
记录管理上,我建议按业务或部门分区域文件,不要所有记录堆在一个文件里。比如db.example.com.internal里再按$INCLUDE拆分子文件,或者用子域名委派。这样改一处不影响其他部分,也方便多人协作时减少冲突。Serial的递增我习惯用日期+两位序号,比如2025010101、2025010102,一天内改多次就递增后两位,跨天重置为01。这样一眼能看出最后修改时间。
还有个小技巧:主从服务器的系统时间要同步,用NTP服务对齐。时间差太大可能导致TSIG签名验证失败,因为签名带时间戳。我遇到过从服务器时间慢了半小时,传送一直失败,查了半天才发现是时间问题。现在新环境部署,第一步就是配NTP。
这套配置我在几个不同规模的环境里跑过,从十几台机器的小内网到上百台的中型网络,稳定性没问题。关键是把视图顺序、Serial递增、TSIG密钥这三件事做对,剩下的就是日常维护的细活了。后面如果业务扩展,还可以在主从基础上加第二台从服务器做负载分担,或者用allow-update配合密钥做动态更新,那是另一个话题了。