为什么个人站长要在内网自建 DNS
你可能会问:服务器上用运营商的 DNS 解析得好好的,为什么还要自己架一个 dnsmasq?答案藏在"多机环境"和"解析控制权"这两件事里。
先看第一个场景。你手上有五台 VPS:一台主站、一台数据库、一台 Redis、一台监控、一台备份。它们互相之间经常用内网 IP 通信,但内网 IP 是 10.0.0.0/24 这种很难记的地址。每次写配置都要翻备忘录,主站挂了换 IP 又得全改一遍。如果你在内网架一个 dnsmasq,把 db.internal 解析到 10.0.0.5,那么以后改拓扑只改一处 DNS,应用配置一个字都不用动。
第二个场景是解析控制和缓存。运营商的 DNS 会做广告劫持、返回就近节点、偶尔抽风;而 dnsmasq 让你把"我要走哪条解析路径"攥在自己手里,还能顺手给常用域名做本地缓存,把每次查询的 30~80ms 延迟压到 1ms 以内。
第三个场景最实在:内网域名 + 通配符 + 本地覆盖。你可以让 *.internal 全走内网,让 npm.registry 走内网代理镜像,让某个被墙的域名强制走特定上游。这些是公共 DNS 给不了的。
安装与最简可用配置
Debian/Ubuntu 一条命令,注意 dnsmasq 会尝试绑定 53 端口,如果系统里跑着 systemd-resolved,会冲突——先处理掉:
apt-get install -y dnsmasq
# 若 systemd-resolved 占用 53 端口,先禁用它的 stub listener
systemctl disable --now systemd-resolved
# 或者只关掉监听:编辑 /etc/systemd/resolved.conf 设 DNSStubListener=no主配置文件 /etc/dnsmasq.conf 很长,新手别硬啃,直接改成一份清爽的最小可用版:
# 只监听内网网卡,绝不监听 0.0.0.0(重要安全项)
listen-address=127.0.0.1,10.0.0.1
bind-interfaces
# 上游 DNS:不要用运营商,用干净可靠的
server=223.5.5.5
server=119.29.29.29
no-resolv # 忽略 /etc/resolv.conf,只用上面显式指定的
# 本地缓存大小(条目数),个人环境 1000~10000 足够
cache-size=5000
# 不读 /etc/hosts 里那些干扰项?其实读它是好事,保留默认
# 日志(排障时开,稳定后关掉省资源)
log-queries
log-facility=/var/log/dnsmasq.log
# 本地内网域名解析
address=/db.internal/10.0.0.5
address=/redis.internal/10.0.0.6
address=/monitor.internal/10.0.0.7其中 address=/域名/IP 是重点:它会把该域名(含所有子域名)直接解析到指定 IP,不查询上游。改完重载:
dnsmasq --test # 语法检查,务必先跑
systemctl restart dnsmasq
systemctl status dnsmasq --no-pager验证:别只看服务状态,要真解析一次
服务 active 不代表解析正确。用 dig 直接指定 DNS 服务器测试:
dig @127.0.0.1 db.internal +short # 应返回 10.0.0.5
dig @127.0.0.1 www.google.com +short # 测试上游转发是否通
dig @127.0.0.1 example.com +stats | grep -i 'Query time' # 第二次应命中缓存连续查两次同一个域名,第二次 Query time 应该明显更小(比如从 40ms 降到 0ms),这说明缓存生效了。如果 db.internal 解析失败,第一反应是看日志:
tail -f /var/log/dnsmasq.log日志里能看到每条查询的来源、命中还是转发、返回了什么。排障时它是最有用的东西。
进阶招式一:给不同域名指定不同上游
这是 dnsmasq 相对公共 DNS 最大的优势——按域名分流。比如国内域名走国内 DNS,某些境外域名走另一条线路,或者把某个内部镜像域名强制解析到内网:
# 特定域名走特定上游
server=/internal.example.com/10.0.0.5 # 内部域名交给另一台内网 DNS
server=/npm.example.com/10.0.0.8 # npm 镜像强制内网
server=/cn/223.5.5.5 # 所有 .cn 走阿里 DNS
server=/google.com/8.8.8.8 # 特定境外域名走 Google
# 屏蔽广告/统计域名(解析到 0.0.0.0)
address=/doubleclick.net/0.0.0.0
address=/ads.example.com/0.0.0.0注意 server=/cn/223.5.5.5 里的 cn 是顶级域匹配,会命中所有 .cn 结尾的查询。这个语法非常灵活:既可以是完整域名,也可以是顶级域,还可以是 // 表示"默认上游"。
进阶招式二:作为整个私有网络的 DNS 中心
如果你用 WireGuard 或 Tailscale 把几台机器组成了私网,那 dnsmasq 就是天然的私网 DNS 中心。把主机的 /etc/resolv.conf 指向它:
# 在每台客户端上
echo "nameserver 10.0.0.1" > /etc/resolv.conf
# 如果被 systemd-resolved 接管,正确做法是:
# 编辑 /etc/systemd/resolved.conf 设 DNS=10.0.0.1,然后 systemctl restart systemd-resolved这里有个大坑:/etc/resolv.conf 经常是个软链接,指向 /run/systemd/resolve/stub-resolv.conf,你直接写会被覆盖。要么禁掉 systemd-resolved 改成静态文件,要么就通过 resolved.conf 配置。两种方式二选一,别混着来。
进阶招式三:配合 DHCP 或纯解析的分工
dnsmasq 既能当 DNS 也能当 DHCP。如果你只是想要 DNS 服务,绝不要在已有 DHCP 服务器的网段里再开 DHCP——两个 DHCP 会互相打架,客户端拿到哪个网关全凭运气。只做 DNS 时,配置里不要出现 dhcp-range 就行。
反过来,如果是小规模内网没有独立 DHCP,dnsmasq 一条 dhcp-range=10.0.0.100,10.0.0.200,12h 就能兼顾,还能根据 MAC 地址固定分配 IP 并自动生成 DNS 记录,主机的 dhcp-host=aa:bb:cc:dd:ee:ff,web01,10.0.0.50 直接让 web01 这个名字可解析。
安全收口:DNS 是攻击面,别大意
dnsmasq 最常见的严重事故是开放递归解析(Open Resolver)——监听 0.0.0.0,谁都能拿它查询,被利用来做 DNS 放大攻击,你的机器会变成攻击跳板,甚至被上游封禁。防线至少三道:
1. 只监听内网地址。listen-address + bind-interfaces,绝不写 0.0.0.0。这是最根本的一道。
2. 防火墙只放行内网。即使配置写错监听了公网,防火墙也要拦住 53 端口:
# nftables 示例:只允许内网段访问 53,其余丢弃
nft add rule inet filter input udp dport 53 ip saddr != 10.0.0.0/24 drop
nft add rule inet filter input tcp dport 53 ip saddr != 10.0.0.0/24 drop3. 本地回环优先。如果 dnsmasq 只为本机服务,直接 listen-address=127.0.0.1,攻击面缩到最小。
另外,log-queries 开久了日志会很大,记得配 logrotate,否则 /var/log/dnsmasq.log 能涨到几个 G:
# /etc/logrotate.d/dnsmasq
/var/log/dnsmasq.log {
daily
rotate 7
compress
missingok
notifempty
postrotate
systemctl kill -s HUP dnsmasq.service 2>/dev/null || true
endscript
}性能实测:自建缓存到底快多少
说缓存提速不能只靠感觉,简单测一下就有数。用 dig 的 +stats 观察两次查询的 Query time,再用一个更贴近真实场景的方法——批量解析一批常用域名,对比首次和二次的耗时:
# 清掉缓存后测首次(改 cache-size=0 重启,或重启进程)
for d in $(seq 1 200); do dig @127.0.0.1 example$((d%20)).com +short >/dev/null; done
# 再测命中缓存的耗时
time (for d in $(seq 1 200); do dig @127.0.0.1 example$((d%20)).com +short >/dev/null; done)默认配置下,命中缓存的解析通常能压到 0~1ms,而回源上游视网络在 20~80ms 不等。对个人站来说,这个差距在"每天几万次内网调用"的量级下是真实可感的。想进一步看缓存命中率,可以在日志里统计 cached 关键字占比:
grep -c 'cached' /var/log/dnsmasq.log
grep -c 'forwarded' /var/log/dnsmasq.log命中的行里 cached example.com is 1.2.3.4,转发的行里是 forwarded example.com to 223.5.5.5。命中率长期低于七成,说明 cache-size 太小或者 min-cache-ttl 被上游打了个很短的 TTL,可以考虑用 min-cache-ttl=60 强制给短 TTL 的域名兜底(但别设太大,否则上游换 IP 后你会长时间解析到旧地址)。
配合 Docker:容器里的解析为什么老是不对
如果你用 Docker 跑应用,会发现容器里的 DNS 行为跟宿主机不一样——Docker 默认给容器配 127.0.0.11 作为内嵌 DNS,它会把请求转发给宿主机的 /etc/resolv.conf。这意味着:你在 dnsmasq 里配的内网域名,容器默认是解析不到的,因为 Docker 内嵌 DNS 没走你的 dnsmasq。
两种修法。一是全局让 Docker 用你的 dnsmasq,编辑 /etc/docker/daemon.json:
{
"dns": ["127.0.0.1"]
}改完 systemctl restart docker,再新建/重启的容器就会用 dnsmasq。注意这里必须写 127.0.0.1 且 dnsmasq 监听回环——因为 Docker 的 DNAT 会把容器的请求转到宿主回环。二是按需要单独指定:docker run --dns 10.0.0.1 ...,或者在 compose 里写 dns: [10.0.0.1]。生产上我更推荐第一种全局配置,省得每个服务都要单独记。
还有个常见坑:如果容器使用自定义网络,Docker 内嵌 DNS 会优先解析容器名和网络别名,你的 dnsmasq 只负责"剩下的"。所以内网域名和容器名冲突时,容器名会赢——给两条命名空间分开(比如容器名用 svc-*,内网域名用 *.internal)能避免打架。
升级与迁移:让配置可版本化
dnsmasq 的配置全在一个文件(或 /etc/dnsmasq.d/ 下的片段)里,天然适合用 Git 管起来。把 /etc/dnsmasq.d/*.conf 纳入版本控制,每次改动先 dnsmasq --test 校验、再提交、再重启,出问题一条 git checkout 就能回滚。这比"手动改完忘了改了啥"强太多。
迁移到新机器时,直接复制配置文件加同步 /etc/hosts 里那几行静态记录即可,dnsmasq 本身无状态(除缓存外),迁移成本极低。唯一要重新确认的是网卡名和 listen-address——每台机器的内网网卡 IP 可能不同,这是迁移后最容易漏改、导致服务"看起来正常但不监听"的地方。
常见故障速查
服务起不来,dnsmasq --test 报配置错误:逐行看报错行号,常见是 server= 里写了非法字符,或者 address= 少了斜杠。
端口 53 被占用:ss -lunp | grep :53 看到底是 systemd-resolved 还是别的,先停掉占用者。
解析慢或超时:上游不可达,逐个 dig @上游IP 域名 测试,把挂掉的上游从 server= 里删掉。
内网域名不生效:确认客户端确实指向了这台 dnsmasq,且 address= 写在了配置文件而不是没被加载的 /etc/dnsmasq.d/ 里没重启。
自建 dnsmasq 这件事,投入产出比高得离谱:一台最低配的机器,半小时搭好,之后你所有多机内网通信、解析加速、域名分流、广告屏蔽都集中在一个 800 行的配置文件里。它不炫技,但它是那种"搭完就再也不想拆"的基础设施。