为什么不满足于域名商自带的 DNS
绝大多数个人站长的域名,DNS 都是域名注册商自带的——买域名的时候顺手填几条 A 记录,就再也没管过。这在刚起步时完全够用。但当你开始在意下面这些事,注册商的免费 DNS 就会开始不够看:
- 你的用户分布在国内和海外,想让国内用户解析到国内 CDN 节点、海外用户解析到海外节点,做到「就近访问」——这就是 GEO 分流(GeoDNS)。
- 你想做故障转移:主服务器挂了自动切到备用机。很多免费 DNS 不支持健康检查。
- 你嫌注册商的解析生效太慢、TTL 被强制拉得很长,改一条记录要等半小时。
- 你想自己掌控解析日志,看清楚到底谁在查你的域名,而不是两眼一抹黑。
这些需求,一个自建的 PowerDNS Authoritative Server 就能全部满足。PowerDNS 是业界最主流的开源权威 DNS 服务器之一(另一个是 BIND),它的特点是:配置清晰、后端可插拔(可以用 MySQL、SQLite、LDAP 甚至纯文件做数据存储)、支持 LUA 记录实现动态解析——而 LUA 记录正是做 GeoDNS 的关键。
这篇文章带你从零搭一个 PowerDNS 权威服务器,并实现基于国家的 GEO 分流。
先分清:权威 DNS 和递归 DNS 不是一回事
这是新手最容易混淆的概念,必须先厘清。
- 权威 DNS(Authoritative):存放你域名的真实记录,回答「example.com 的 A 记录是什么」这类问题,并且声称「我就是这个域名的权威」。你自己搭的就是它。默认监听
53端口。 - 递归 DNS(Recursive / Resolver):帮你从根域名开始一层层查询、缓存结果、最后把答案交给你的客户端。比如你电脑里配的
8.8.8.8、运营商的 DNS,都是递归服务器。
你要做的是把域名的 NS 记录指向你自建的权威服务器,让别人来查你。这就要求你有公网 IP,并且在域名注册商那里把 NS 换成你的服务器(注册商通常还要求你提供「胶水记录」——即你的 NS 服务器主机名的 A 记录,防止循环依赖)。
个人站如果没有独立公网 IP(比如家宽、或者服务器都走 Cloudflare 代理),严格意义上没法直接做权威解析。折中方案是:在注册商那里把 NS 指向两台有公网 IP 的 VPS,VPS 上跑 PowerDNS。两台是底线——DNS 是单点故障的重灾区,一台挂了全站就解析不出来了。
安装 PowerDNS 与 MySQL 后端
我们选 MySQL 后端(gmysql),因为要动态写 LUA 记录,数据库操作最灵活。先装:
apt update apt install -y pdns-server pdns-backend-mysql mariadb-server
建库建表。PowerDNS 官方提供了 schema,装完就在 /usr/share/doc/pdns-backend-mysql/schema.mysql.sql:
mysql -uroot -p -e "CREATE DATABASE powerdns CHARACTER SET utf8mb4;" mysql -uroot -p -e "CREATE USER 'pdns'@'localhost' IDENTIFIED BY '强密码';" mysql -uroot -p -e "GRANT ALL ON powerdns.* TO 'pdns'@'localhost';" mysql -uroot -p powerdns < /usr/share/doc/pdns-backend-mysql/schema.mysql.sql
配置后端。PowerDNS 的主配置是 /etc/powerdns/pdns.conf,但推荐把后端相关配置拆到 /etc/powerdns/pdns.d/ 下的独立文件里,便于管理:
# /etc/powerdns/pdns.d/gmysql.conf launch=gmysql gmysql-host=127.0.0.1 gmysql-user=pdns gmysql-password=强密码 gmysql-dbname=powerdns
这里有个非常关键、但几乎每篇教程都会写错的地方:配置多后端时,用逗号分隔,不是加号。 比如你同时要用 gmysql 和 geoip 后端做 GEO 分流,写的是:
launch=gmysql,geoip
写成 gmysql+geoip 会启动失败。这个符号的坑坑了无数人。
建 zone、加记录
PowerDNS 提供了 pdnsutil 这个命令行工具管理 zone。先创建一个 zone:
pdnsutil create-zone example.com pdnsutil add-record example.com www A 300 1.2.3.4 pdnsutil add-record example.com @ A 300 1.2.3.4 pdnsutil add-record example.com @ MX "300 10 mail.example.com"
300 是 TTL(秒),@ 表示根域名。pdnsutil 会自动处理 SOA 和 NS 记录的生成。加完可以用 pdnsutil list-zone example.com 查看整个 zone。
配好 NS 记录后,本地测试:
dig @127.0.0.1 www.example.com A +short # 应该返回 1.2.3.4
如果返回正确,说明权威服务器本身工作正常。接下来就是把它暴露出去,接受外部查询。
开放 53 端口与安全底线
DNS 同时用 UDP 和 TCP 的 53 端口(UDP 用于普通查询,TCP 用于大响应和区域传送)。防火墙两个都要开:
ufw allow 53/udp ufw allow 53/tcp ufw allow from 8.8.8.8 to any port 53 # 可选:限制特定解析器
这里有一条必须守住的安全线:绝对不能让你的权威服务器变成「开放递归解析器」。权威服务器只应该回答自己托管的域名,对于不属于自己的查询应该返回 REFUSED。如果它变成了对全世界开放、有问必答的递归解析器,会被攻击者拿去做 DNS 放大攻击(DDoS 反射),你的 IP 很快会被各大黑名单收录,甚至被机房封停。
PowerDNS 默认的行为是正确的:对不属于自己 zone 的查询返回 REFUSED,不做递归。确认配置里没有开启 recursor 相关选项,也不要误装 pdns-recursor 到同一台权威服务器上(两者不能共用 53 端口)。
GeoDNS 实战:按国家返回不同 IP
现在到重点。要做到「国内用户解析到 IP-A,海外用户解析到 IP-B」,需要三样东西:
- GeoIP 数据库:根据客户端 IP 判断其所在国家。用 MaxMind 的 GeoLite2-Country(免费)。
- geoip 后端:让 PowerDNS 能加载这个库。
- LUA 记录:在记录内容里写判断逻辑,根据国家返回不同 IP。
先装 GeoIP 支持并准备数据库:
apt install -y pdns-backend-geoip mkdir -p /opt/geoip # 把 GeoLite2-Country.mmdb 放到 /opt/geoip/ 下 ls -la /opt/geoip/GeoLite2-Country.mmdb
然后在后端配置里加上 geoip 后端和 LUA 支持:
# /etc/powerdns/pdns.d/gmysql.conf launch=gmysql,geoip geoip-database-files=/opt/geoip/GeoLite2-Country.mmdb enable-lua-records=yes consistent-backends=no
注意最后那行 consistent-backends=no——这个配置是 GEO 分流能否生效的生死线。PowerDNS 默认认为「所有后端对同一个查询返回相同结果」,因此会缓存第一个查询的响应。但 GEO 分流的本质就是「不同客户端得到不同答案」,如果开了 consistent-backends,第一个美国用户查询后,后面所有中国用户都会被返回美国节点的 IP,GEO 直接失效。这个坑极度隐蔽,很多人配完发现只有第一个访客的解析是对的,就是这个原因。
重启 PowerDNS 并在 zone 上启用 LUA 记录:
pdnsutil set-meta example.com ENABLE-LUA-RECORDS 1 systemctl restart pdns
这里又有一个坑:启用 LUA 的元数据键是 ENABLE-LUA-RECORDS,值必须是 1。如果你用 pdnsutil clear-meta example.com LUA-RECORDS 去「清理」LUA-RECORDS 键,它会留下一个空字符串,而空字符串意味着「禁用 LUA」——即使全局配置 enable-lua-records=yes,这个 zone 的 LUA 记录依然不生效,查询返回 SERVFAIL。记住:要启用就用 ENABLE-LUA-RECORDS=1,不要碰 LUA-RECORDS。
现在写 LUA 记录。LUA 记录的内容格式是「先写记录类型,然后空格,然后一个 Lua 表达式(用双引号包起来)」。在 MySQL 后端里直接插:
INSERT INTO records (domain_id, name, type, content, ttl)
VALUES (1, 'www.example.com', 'LUA',
'A ";if(country(''CN'')) then return ''1.1.1.1'' else return ''2.2.2.2'' end"',
60);
拆解这一行:
- 内容以
A开头,表示这条 LUA 记录最终返回的是 A 记录(IPv4 地址)。 - 后面是 Lua 表达式,用双引号包起来。
- 表达式里的单引号,在 MySQL 字符串里要写成两个单引号
''(MySQL 的转义方式),最终存进数据库的内容里是正常的单引号。 - 开头的
;告诉 PowerDNS「这是一段完整的 Lua 脚本」,而不是简单的返回值表达式。
country('CN') 是 PowerDNS 提供的函数,判断客户端是否来自中国(需要 geoip 后端加载了 GeoIP 库)。更完整的写法:
INSERT INTO records (domain_id, name, type, content, ttl)
VALUES (1, 'www.example.com', 'LUA',
'A ";if(country(''CN'')) then return {''104.18.1.1''} elseif(country(''US'')) then return {''104.18.2.2''} else return {''104.18.3.3''} end"',
300);
用花括号 {...} 返回的是一个 IP 数组,可以用来做同一分支的多 IP 负载均衡(客户端会随机挑一个)。
写完后验证。本地测试没法模拟外国 IP,所以要么用在线工具,要么从不同地区的机器 dig:
dig @你的服务器IP www.example.com A +short # 从中国服务器查应返回 104.18.1.1,从海外服务器查应返回 104.18.2.2
LUA 记录的几个实用函数
除了按国家判断,PowerDNS 的 LUA 还提供了这些函数,做故障转移和智能调度非常有用:
country('CN'):按国家匹配。continent('EU'):按大洲匹配。ifportup(443, {'1.2.3.4', '5.6.7.8'}):检测这些 IP 的 443 端口是否可连,返回第一个活着的。这就是基于健康检查的故障转移——主服务器挂了自动切到备用。pickclosest({'1.2.3.4', '5.6.7.8'}):返回地理距离最近的 IP(需要更精确的 GeoIP 数据库)。
一个故障转移的例子:
INSERT INTO records (domain_id, name, type, content, ttl)
VALUES (1, 'api.example.com', 'LUA',
'A ";return ifportup(443, {''1.2.3.4'', ''5.6.7.8''})"',
30);
注意这种健康检查做故障转移时,TTL 要设得短(比如 30 秒),否则客户端和递归服务器会一直缓存着挂掉的旧 IP。
部署两台、互为备份
DNS 单点是致命的。至少部署两台,一台主一台备(或者双主)。两种同步方式:
方式一:共享数据库。 两台 PowerDNS 都连同一个 MySQL(数据库放在第三方或者其中一台),数据天然一致。缺点是数据库本身成了单点。
方式二:主从复制。 每台各有一个本地 MySQL,通过 MySQL 主从复制同步数据。PowerDNS 官方推荐用这种(gmysql-dnssec 也要求如此)。配置 MySQL 主从,PowerDNS 侧什么都不用改。
还有一种最省事的做法适合定制程度高的场景:两台服务器跑完全相同的配置(用版本控制或 rsync 保持同步),zone 数据也保持一致,这样不需要任何运行时的同步机制,配置的同步就是数据的同步。适合用文件后端或自研 DNS 的场景。
无论哪种方式,都要确保两台机器上 pdns 都能正常启动、都能回答查询。别配了一台忘了另一台,切换 NS 的时候才发现备机是坏的。
五个必踩的坑
- 多后端用加号分隔:
launch=gmysql+geoip启动失败,必须用逗号launch=gmysql,geoip。 - 忘设 consistent-backends=no:GEO 分流失效的元凶。默认值是 yes,会缓存第一个响应,导致所有用户拿到同一个答案。
- 用 LUA-RECORDS 空值禁用 LUA:启用必须用
ENABLE-LUA-RECORDS 1,清空 LUA-RECORDS 会留下空字符串 = 禁用,查询直接 SERVFAIL。 - 变成开放递归解析器:权威服务器只该回答自己的 zone,对其他查询返回 REFUSED。开放递归会被拿去做 DDoS 放大攻击,IP 很快被封。
- 单台部署:DNS 一挂全站解析失败,用户直接打不开网站。至少两台,且都要验证可用再切 NS。
小结
自建 PowerDNS 权威服务器,让你对自己的域名解析有了完全的掌控力:可以按国家分流、可以做健康检查故障转移、可以掌控 TTL 和日志。核心配置其实不多——gmysql 后端 + geoip 后端 + LUA 记录,就能实现 GeoDNS。
最后提醒一句:DNS 是「一旦出问题影响面 100%」的基础设施,改动务必谨慎。第一次上线时,先把新服务器配好、用 dig @新IP 验证所有记录都正确,再改注册商的 NS 指向。改完之后保留旧 DNS 一段时间,万一新服务器出问题可以快速切回。稳妥,永远比激进重要。