目前,围绕域名系统 (DNS) 有很多讨论,有人提议对这个已有 36 年历史的协议进行大规模修改。 互联网的名称服务起源于ARPANET ,自诞生以来从未出现过任何向后兼容问题。 但改变 DNS 传输机制的新提案可能即将改变这一状况。
在这篇文章中,我将介绍两种用于保护 DNS 的新兴技术,即 DNS over TLS (DoT) 和 DNS over HTTPS (DoH),并展示如何使用 NGINX 开源和 NGINX Plus 实现它们。
[编辑——这篇文章是探讨 NGINX JavaScript 模块用例的几篇文章之一。 有关完整列表,请参阅NGINX JavaScript 模块的用例。
这篇文章中的代码已更新,以使用 js_导入
指令,取代了已弃用的 js_include
指令 NGINX Plus R23<.htmla> 以及之后。 有关更多信息,请参阅NGINX JavaScript 模块的参考文档 -示例配置部分显示了 NGINX 配置和 JavaScript 文件的正确语法。]
DNS 是第二次为高级研究计划局 (ARPA) 的早期互联网创建命名服务的尝试,第一次是 John Postel 于 1979 年发布的互联网名称服务器协议,即IEN-116 。 DNS 被设计为分层的,并为将主机名分散到区域并由许多独立的机构管理提供一种结构。 DNS 的第一份 RFC 于 1983 年发布(RFC882和883),尽管多年来有过几次扩展,但是按照当时定义的标准编写的客户端今天仍然可以工作。
那么为什么现在要改变协议呢? 它显然按预期工作,证明了其作者的信心是正确的,他们非常确信他们是正确的,以至于他们没有在 DNS 数据包中包含版本号——我想不出有多少其他协议可以做出这样的声明。 DNS 是在一个比较纯真的时代构想出来的,当时大多数协议都是明文形式,通常为 7 位 ASCII ,但今天的互联网比 20 世纪 80 年代的 ARPANET 可怕得多。 如今,大多数协议都采用传输层安全性 (TLS) 来进行加密和验证。 DNS 的批评者认为,它早就需要一些额外的安全保护措施。
因此,正如 DNS 是第二次为互联网提供名称服务协议的尝试一样,DNS over TLS (DoT) 和 DNS over HTTPS (DoH) 也正在成为保护 DNS 协议安全的第二次尝试。 第一次尝试是一种称为DNSSEC 的扩展,虽然大多数顶级域名 (TLD) 都使用 DNSSEC,但它从未打算加密 DNS 数据包中携带的数据;它仅提供数据未被篡改的验证。 DoT 和 DoH 是将 DNS 包装在 TLS 隧道内的协议扩展,如果采用,它们将终结 36 年的向后兼容性。
我认为,DoT 被广泛视为一种合理的延伸。 互联网号码分配机构 (IANA) 已经为它分配了自己的端口号 (TCP/853),并且只是将 TCP DNS 数据包包装在 TLS 加密隧道内。 许多协议之前已经这样做过: HTTPS 是 TLS 隧道内的 HTTP,而 SMTPS、IMAPS 和 LDAPS 是这些协议的安全版本。 DNS 一直使用 UDP(在某些情况下使用 TCP)作为传输协议,因此添加 TLS 包装器并不是一个巨大的改变。
另一方面,DoH 则更具争议。 DoH 接收 DNS 数据包并将其包装在 HTTP GET
或POST
请求中,然后通过 HTTPS 连接使用 HTTP/2 或更高版本发送。 从各方面来看,这似乎都与任何其他 HTTPS 连接一样,企业或服务提供商不可能看到正在发出的请求。 Mozilla 和其他支持者表示,这种做法可以使用户访问的网站不被窥探,从而增强了用户的隐私。
但事实并非如此。 DoH 的批评者指出,它不完全是 DNS 的Tor ,因为当浏览器最终连接到使用 DoH 查找的主机时,请求几乎肯定会使用 TLS服务器名称指示(SNI) 扩展 - 其中包括主机名并以明文形式发送。 此外,如果浏览器尝试使用在线证书状态协议 (OSCP) 验证服务器的证书,则该过程很可能也以明文形式进行。 因此,任何有能力监控 DNS 查找的人也有能力读取连接中的 SNI 或 OCSP 验证中的证书名称。
对于许多人来说,DoH 的最大问题是浏览器供应商选择默认将用户发出的 DoH 请求发送到的 DNS 服务器(例如,对于美国的 Firefox 用户,DNS 服务器属于Cloudflare )。 DNS 服务器的运营商可以看到用户的 IP 地址以及他们发出请求的站点的域名。 这看起来可能不算什么,但伊利诺伊大学的研究人员发现,仅根据网页元素请求的目标地址就可以推断出一个人访问过哪些网站,即所谓的页面加载指纹。 然后可以使用这些信息来“对用户进行分析并定位广告”。
DoT 和 DoH 本质上并不邪恶,并且在某些情况下它们可以增强用户隐私。 然而,越来越多的人认为,公共的、集中的 DoH 服务不利于用户隐私,我们建议您不惜一切代价避免使用这种服务。
无论如何,对于您的网站和应用,您可能管理自己的 DNS 区域 - 一些是公共的,一些是私有的,还有一些是具有水平分割的。 在某些时候,您可能会决定要运行自己的 DoT 或 DoH 服务。 这正是 NGINX 可以提供帮助的地方。
DoT 提供的隐私增强功能为 DNS 安全性提供了一些巨大的优势,但如果您当前的 DNS 服务器不提供对 DoT 的支持怎么办? NGINX 可以通过在 DoT 和标准 DNS 之间提供网关来提供帮助。
或者您可能喜欢 DoH 在 DoT 端口可能被阻止的情况下突破防火墙的潜力。 NGINX 可以再次提供帮助,它提供DoH-to-DoT/DNS网关。
NGINX Stream(TCP/UDP)模块支持 SSL 终止,因此设置 DoT 服务实际上非常简单。 只需几行 NGINX 配置即可创建一个简单的 DoT 网关。
您需要一个用于 DNS 服务器的上游
块和一个用于 TLS 终止的服务器
块:
当然,我们也可以采用另一种方式,将传入的 DNS 请求转发到上游 DoT 服务器。 然而,这没什么用,因为大多数 DNS 流量都是 UDP,而 NGINX 只能在 DoT 和其他 TCP 服务(例如基于 TCP 的 DNS)之间进行转换。
与 DoT 网关相比,简单 DoH 网关的配置稍微复杂一些。 我们同时需要 HTTPS 服务和 Stream 服务,并使用 JavaScript 代码和NGINX JavaScript 模块<.htmla> (njs) 在两种协议之间进行转换。 最简单的配置是:
此配置执行将数据包发送到 DNS 服务所需的最少处理。 此用例假定上游 DNS 服务器执行任何其他过滤、日志记录或安全功能。
此配置中使用的 JavaScript 脚本 ( nginx_stream.js ) 包含各种 DNS 库模块文件。 在dns.js模块内部, dns_decode_level
变量设置对 DNS 数据包进行多少处理。 处理 DNS 数据包显然会影响性能。 如果您使用上述配置,请将dns_decode_level
设置为0
。
在 NGINX 我们非常擅长 HTTP,因此我们认为仅将 NGINX 用作简单的 DoH 网关是浪费机会。
可以设置此处使用的 JavaScript 代码来对 DNS 数据包进行全部或部分解码。 这使我们能够为 DoH 查询构建 HTTP内容缓存,并根据 DNS 响应的最小 TTL 设置 Expires 和Cache-Control
标头。
下面是一个更完整的示例,其中包含额外的连接优化以及对内容缓存和日志记录的支持:
如果您有 NGINX Plus 订阅,那么您可以将上述示例与一些更高级的 NGINX Plus 功能(例如主动健康检查和高可用性)相结合,甚至可以使用缓存清除 API 来管理缓存的 DoH 响应。
还可以使用 NGINX Plus 键值存储来构建 DNS 过滤系统,通过返回有效阻止访问的 DNS 响应来保护用户免受恶意域的侵害。 您可以使用 RESTful NGINX Plus API动态管理键值存储的内容。
我们定义了两类恶意域名,“阻止”和“黑洞”。 DNS 过滤系统根据域的类别以不同方式处理有关域的 DNS 查询:
NXDOMAIN
响应(表示该域不存在)A
记录请求时返回单个 DNS A
记录 0.0.0.0,或在响应AAAA
(IPv6) 记录请求时返回单个AAAA
记录 ::;对于其他记录类型,它会返回包含零个答案的响应当我们的 DNS 过滤器收到请求时,它首先检查与精确查询的 FQDN 匹配的密钥。 如果找到,它会根据关联值指定的方式“清理”(阻止或黑洞)该请求。 如果没有完全匹配,它会在两个列表(一个是被阻止的域名,另一个是黑洞域名)中查找域名,如果匹配,则以适当的方式清理请求。
如果查询的域名不是恶意的,系统会将其转发到 Google DNS 服务器进行常规处理。
我们在 NGINX Plus 键值存储中的两种条目中定义了我们认为恶意的域:
已阻止
或黑洞
blocked_domains
和blackholed_domains
,每个列表都映射到逗号分隔值 (CSV) 格式的域列表 以下针对键值存储的配置位于流
上下文中。 keyval_zone
指令为键值存储分配一块内存,称为dns_config 。第一个keyval
指令加载任何已设置的匹配 FQDN 键值对,而第二个和第三个定义两个域列表:
然后,我们可以使用NGINX Plus API将值block
或blackhole
分配给我们明确想要清理的任何 FQDN 键,或者修改与blocked_domains
或blackhole_domains
键关联的 CSV 格式的列表。 我们可以随时修改或删除精确的 FQDN,或者修改任一列表中的域集,并且 DNS 过滤器会立即更新这些更改。
以下配置加载$dns_response
变量,该变量由下面配置监听 DNS 查询的服务器中定义的服务器
块中的js_preread
指令填充。 当调用的 JavaScript 代码确定请求需要被清理时,它会根据需要将变量设置为受阻
或黑洞
。
我们使用map
指令将$dns_response
变量的值分配给$upstream_pool
变量,从而控制下面定义上游服务器中的哪个上游组处理请求。 对非恶意域名的查询会被转发到默认的 Google DNS 服务器,而对被阻止或黑洞域名的查询则由这些类别的上游服务器处理。
此配置定义了上游服务器的blocked
、 blackhole
、 google
组,分别处理对blocked、blackhole、非恶意域的请求。
在这里,我们定义流
上下文中监听传入 DNS 请求的服务器。 js_preread
指令调用解码 DNS 数据包的 Javascript 代码,从数据包的NAME
字段中检索域名,并在键值存储中查找域。 如果域名与 FQDN 密钥匹配,或者位于blocked_domains
或blackhole_domains
列表中某个域的区域内,则会将其清除。 否则,它最终会被发送到 Google DNS 服务器进行解析。
差不多就这样了——我们只需要添加我们的最终服务器
块,它以黑洞或阻止的响应来响应查询。
请注意,此块中的服务器与其上方的实际 DNS 服务器几乎相同,主要区别在于这些服务器向客户端发送响应数据包,而不是将请求转发到上游的 DNS 服务器组。 我们的 JavaScript 代码会检测它何时从端口 9953 或 9853 运行,并且不会设置标志来指示应阻止数据包,而是用实际的响应数据包填充$dns_response
。 这就是全部内容了。
当然,我们也可以将这种过滤应用于 DoT 和 DoH 服务,但为了简单起见,我们使用了标准 DNS。 DNS 过滤与 DoH 网关的合并留给读者作为练习。
我们之前曾使用NGINX Plus API将两个 FQDN 的条目和一些域添加到两个域列表中:
$ curl -s http://localhost:8080/api/5/stream/keyvals/dns_config | jq { “www.some.bad.host”:“blocked”, “www.some.other.host”:“blackhole”, “blocked_domains”:“bar.com,baz.com”, “blackhole_domains”:“foo.com,nginx.com” }
因此,当我们请求解析www.foo.com (被黑洞的foo.com域内的主机)时,我们会得到一个 IP 地址为 0.0.0.0 的A
记录:
$ dig @localhost www.foo.com ; <<>> DiG 9.11.3-1ubuntu1.9-Ubuntu <<>> @localhost www.foo.com ; (找到 1 个服务器) ;; 全局选项:+mcd ;; 得到答案: ;; ->>HEADER,,- 操作码: 查询,状态: 无错误,标识: 58558;;标志:qr aa rd ad;查询: 1,答案: 1、权威性: 0,附加: 0;;警告:请求递归但不可用;;问题部分:;www.foo.com。 在;; 答案部分:www.foo.com。 300 IN A 0.0.0.0;;查询时间: 0 毫秒;;服务器: 172.0.0.1#53(126.0.0.1) ;; 何时: 2019 年 12 月 2 日星期一 14:31:35 UTC;;MSG SIZEW 已收到: 四十五
DOH 和 DOT 的文件可在我的GitHub 存储库中找到:
要试用 NGINX Plus,请立即开始30 天免费试用或联系我们讨论您的用例。
“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”