PowerDNS 权威 DNS 服务器自建实战:gmysql 后端、LUA 记录与 GeoDNS 按国家分流全流程

为什么不满足于域名商自带的 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」,需要三样东西:

  1. GeoIP 数据库:根据客户端 IP 判断其所在国家。用 MaxMind 的 GeoLite2-Country(免费)。
  2. geoip 后端:让 PowerDNS 能加载这个库。
  3. 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 的时候才发现备机是坏的。

五个必踩的坑

  1. 多后端用加号分隔:launch=gmysql+geoip 启动失败,必须用逗号 launch=gmysql,geoip。
  2. 忘设 consistent-backends=no:GEO 分流失效的元凶。默认值是 yes,会缓存第一个响应,导致所有用户拿到同一个答案。
  3. 用 LUA-RECORDS 空值禁用 LUA:启用必须用 ENABLE-LUA-RECORDS 1,清空 LUA-RECORDS 会留下空字符串 = 禁用,查询直接 SERVFAIL。
  4. 变成开放递归解析器:权威服务器只该回答自己的 zone,对其他查询返回 REFUSED。开放递归会被拿去做 DDoS 放大攻击,IP 很快被封。
  5. 单台部署:DNS 一挂全站解析失败,用户直接打不开网站。至少两台,且都要验证可用再切 NS。

小结

自建 PowerDNS 权威服务器,让你对自己的域名解析有了完全的掌控力:可以按国家分流、可以做健康检查故障转移、可以掌控 TTL 和日志。核心配置其实不多——gmysql 后端 + geoip 后端 + LUA 记录,就能实现 GeoDNS。

最后提醒一句:DNS 是「一旦出问题影响面 100%」的基础设施,改动务必谨慎。第一次上线时,先把新服务器配好、用 dig @新IP 验证所有记录都正确,再改注册商的 NS 指向。改完之后保留旧 DNS 一段时间,万一新服务器出问题可以快速切回。稳妥,永远比激进重要。

Last modification:October 12th, 2026 at 09:27 pm

Leave a Comment