Why

有个域名托管在 Namesilo 上,对应的主机则是通过 DDNS 穿透到内网的虚拟机上,是没有公网 IP 的,解析则是靠 DDNS 提供的域名作为 CNAME 记录。而大部分域名托管商是不支持给裸域(Apex Domain) 添加 CNAME 记录的。首先,RFC 1034 声明了 CNAME 记录应该是排他的:

If a CNAME RR is present at a node, no other data should be present; this ensures that the data for a canonical name and its aliases cannot be different.

RFC 2181 中进一步说明:

...That is, for any label in the DNS (any domain name) exactly one of the following is true:
    + one CNAME record exists, optionally accompanied by SIG, NXT, and KEY RRs,
    + one or more records exist, none being CNAME records,
    + the name exists, but has no associated RRs of any type,
    + the name does not exist at all.

其次在 RFC 1034 阐述了 NS 记录和 SOA 记录对于裸域的必要性:

...Though logically part of the authoritative data, the RRs that describe the top node of the zone are especially important to the zone's management. These RRs are of two types: name server RRs that list, one per RR, all of the servers for the zone, and a single SOA RR that describes zone management parameters.

裸域存在 NS 记录和 SOA 记录是最佳实践。意味着我没办法以指定 CNAME 的方式,通过裸域来访问这个网站,很长一段时间里,我只能通过加 www 来访问。而是否使用 www,知乎上 为什么越来越多的网站域名不加「www」前缀? - Rio的回答 给出的结论是比较中肯且全面的。当然,对于仅仅是用于个人网站的我,可以全凭个人好恶,无须考虑太多。

解决方案

在没有独立公网 IP 的情况下通过裸域正常访问 www 网站, 主要有加钱、 Domain Forwarding 和 CNAME Flattening 三种解决方案。

加钱

买一个带公网 IP 的云主机,把服务部署在上面,或者用 FRP 做穿透,然后将裸域的 A 记录指向该 IP。我太穷了,暂不考虑这条路。

Domain Forwarding

Namesilo 和大部分域名托管方给出了 Domain Forwarding 的解决方案,但实际上使用是有限制的,考虑以下几种可能的场景:

输入试图跳转至可行性
http://foo.comhttp://www.foo.com
http://foo.comhttps://www.foo.com
https://foo.comhttps://www.foo.com

Domain Forwarding 的原理,是让你的裸域指向了域名托管商的一个 web 服务器(指向的方式当然也是添加一条 A 记录啦),访问的时候服务器返回 301 跳转到你指定的地址,那自然通过 HTTP 访问托管商的 web 服务器,再通过 301 跳转到 HTTP/HTTPS 的 www 都没问题。

但是要访问 HTTPS 的裸域时,由于域名托管商并没有你的裸域的 SSL/TLS 证书,自然不能为你的 HTTPS 裸域跳转。所以这种方式并不完全支持所有场景。

CNAME Flattening

Cloudflare 提供了名为 CNAME Flattening 的解决方案,表面上允许域名所有者给裸域添加 CNAME 记录,实际背地里把所有者提供的 CNAME 值转换为 A 记录,就实现了(好阴险的偷梁换柱!)

实际上我也可以 nslookup 我的 DDNS 地址的 A 记录,相应地给我的裸域填上 A 记录,但也如上面知乎回答内所言:

特别是使用第三方托管服务的时候。如果第三方迁移服务器导致 IP 地址变更,你必须自己去更改 DNS 的 A 记录。

所以自定义 CNAME Flatening 是可行的,只是麻烦在“自己去更改”。如果我能在每次 DDNS 地址的 A 记录变更时,自动将我裸域的 A 记录一并变更,问题就解决了。

自定义 CNAME Flattening

Namesilo 允许通过 API 来增删查改 DNS 记录,我要做的便是定期查询 DDNS 的 A 记录,以及我的裸域的 DNS 记录情况,结合两者的情况决定对域名 DNS 记录进行何种操作:

  • 裸域不存在任何记录,直接新增 A 记录为 DDNS A 记录
  • 裸域存在 A 记录,判断是否与 DDNS A 记录相同
    • 相同,无需更新
    • 不同,更新为 DDNS A 记录
  • 裸域存在 CNAME 记录,删除之,新增 A 记录为 DDNS A 记录

将其写在循环中,以 DDNS A 记录的 TTL 来作为线程 Delay 的时间。考虑到可能不止有一个裸域需要同步,写的时候可以做配置文件,开多个线程分别跑。

事实上不只是 Namesilo,只要支持 API 的,或者可以通过爬虫写自动化工具的,皆可以采用此种方法。当然上面只是粗略流程,因为 A 记录是可以有多条的,至于实际使用过程中要保留哪条,以及当 DDNS A 记录也有多条的时候如何应对,需自行斟酌。