NGINX Full Version

将 NGINX 用作 DoT 或 DoH 网关

目前,有关域名系统(DNS)的讨论如火如荼,有人建议对这个实行 36 年之久的协议进行大刀阔斧的修改。互联网名称服务起源于 ARPANET,自问世以来从未遇到过任何向后兼容问题。但是修改 DNS 传输机制的新提议可能会改变这一点。

本文将介绍两项新兴的 DNS 安全技术 — DNS over TLS (DoT) 和 DNS over HTTPS (DoH),并展示如何使用 NGINX 开源版本和 NGINX Plus版本来实现这两项技术。

[编者按 – 本文是探讨 NGINX JavaScript 模块用例的系列文章之一。查看完整列表,请参阅《NGINX JavaScript 模块的用例》。

本文中的代码已更新,在 NGINX Plus R23 及更高版本中,使用 js_import 指令取代已弃用的 js_include 指令。更多信息,请参阅 NGINX JavaScript 模块的参考文档 —“示例配置”一节显示了 NGINX 配置和 JavaScript 文件的正确语法。]

 

DNS 发展简史

美国高级研究计划署(ARPA)的早期互联网曾有先后两版命名服务,第一版是 John Postel 于 1979 年发布的互联网名称服务器协议 IEN-116第二版就是 DNS。DNS 采用分层结构,可将主机名分散到不同区域并由多个独立机构进行管理。首批 DNS RFC 于 1983 年发布(RFC 882883),虽然多年来进行了多次扩展,但按照当时定义的标准编写的客户端如今仍可使用。

那么,为何现在要更改该协议呢?DNS 显然仍能如期运行,正如其制定者所确信的那样 — 他们对自己的工作如此有信心,故而没有将版本号加入 DNS 数据包中;据我所知,没有其他任何协议能够做到这一点。DNS 诞生于一个比较淳朴的年代,当时大多数协议都是明文,通常是 7 位 ASCII 码,而如今的互联网比二十世纪 80 年代的 ARPANET 要复杂得多。如今,大多数协议都使用传输层安全(TLS)进行加密和验证。DNS 的批评者认为早就应该增强安全防护了。

因此,正如 DNS 是第二版互联网名称服务协议一样,DoT 和 DoH 也有望成为第二版更安全的 DNS 协议。第一版是一种称为 DNSSEC 的扩展,虽然大多数顶级域名(TLD)都使用 DNSSEC,但它无法对 DNS 数据包中的数据进行加密,只能验证数据是否未被篡改。DoT 和 DoH 是将 DNS 封装在 TLS 隧道中的协议扩展,一旦采纳,36 年的向后兼容性将告终。

 

详细介绍 DoT 和 DoH

我认为,DoT 在很大程度上是一种合理的扩展。它已经获得了互联网数字分配机构(IANA)分配的端口号(TCP/853),只需将 TCP DNS 数据包封装在 TLS 加密隧道中即可。许多协议此前都是这样操作:HTTPS 是 TLS 隧道内的 HTTP,SMTPS、IMAPS 及 LDAPS 是相应协议的安全版本。DNS 一直使用 UDP(或在某些情况下使用 TCP)作为传输协议,因此添加 TLS 封装器并非重大变更。

相比之下,DoH 更具争议性。DoH 接受 DNS 数据包,并将其封装在 HTTP GETPOST 请求中,然后使用 HTTP/2 或更高版本通过 HTTPS 连接发送请求。这似乎与其他任何 HTTPS 连接一样,企业或服务提供商无法看到正在发出的请求。Mozilla 及其他支持者表示,这种做法能够保护用户访问的网站免受窥探,从而提高用户隐私防护。

但事实并非如此。DoH 的批评者指出,它不是 DNS 版的 Tor,因为当浏览器最终与使用 DoH 查询到的主机建立连接时,请求肯定会使用 TLS 服务器名称指示(SNI)扩展 — 后者包含主机名,并以明文形式发送。此外,如果浏览器尝试使用在线证书状态协议(OSCP)验证服务器的证书,那么该流程很可能也会以明文形式进行。因此,任何能够监控 DNS 查询的人员也能够读取连接中的 SNI 或 OCSP 验证中的证书名称。

对于许多人而言,DoH 最大的问题在于浏览器厂商会选择其用户发出的 DoH 请求默认送达的 DNS 服务器(以美国 Firefox 用户为例,DNS 服务器属于 Cloudflare)。DNS 服务器的运营商能够看到用户的 IP 地址及其请求访问的网站域名。这看似没什么,但伊利诺伊大学的研究人员发现,仅凭对网页元素的请求的目标地址,即所谓的“页面加载指纹”便可推断出一个人访问过哪些网站。然后,这些信息就可以用于“对用户进行画像和瞄准,以便投放广告”。

 

NGINX 如何助一臂之力?

DoT 和 DoH 本身无害,而且在某些用例中,它们还能增强用户隐私保护。不过,越来越多的人认为,集中式公共 DoH 服务不利于用户隐私保护,因此我们建议您尽量避免使用这种服务。

大多数时候,您都会为自己的站点和应用管理自己的 DNS 区域,包括公共区域、专用区域和水平分割区域。有时候,您可能会决定运行自己的 DoT 或 DoH 服务。对此,NGINX 能够助您一臂之力。

DoT 提供的隐私增强功能为 DNS 安全防护提供了一些优势,但如果您当前的 DNS 服务器不支持 DoT 怎么办?NGINX 可在 DoT 和标准 DNS 之间提供一个网关。

或者,在 DoT 端口可能被屏蔽的情况下,您或许希望发挥 DoH 的防火墙扩展潜力。同样,NGINX 可提供 DoH 到 DoT/DNS 网关,助您一臂之力。

 

部署简单的 DoT-DNS 网关

NGINX Stream (TCP/UDP) 模块支持 SSL 卸载,因此设置 DoT 服务其实非常简单。只需几行 NGINX 配置,即可创建一个简单的 DoT 网关。

您需要一个 upstream 块来配置 DNS 服务器,以及一个 server 块来配置 TLS 卸载:

当然,我们也可以将传入的 DNS 请求转发到上游 DoT 服务器。不过,这用处不大,因为大多数 DNS 流量都使用 UDP 进行传输,而 NGINX 只能在 DoT 和其他 TCP 服务(如基于 TCP 的 DNS)之间进行转换。

 

简单的 DoH-DNS 网关

与 DoT 网关相比,简单 DoH 网关的配置稍显复杂。我们需要一个 HTTPS 服务和一个 Stream 服务,并使用 JavaScript 代码和 NGINX JavaScript 模块(njs)在两个协议之间进行转换。最简单的配置是:

这种配置只需进行最少处理,即可将数据包发送到 DNS 服务。此用例假定上游 DNS 服务器执行其他任何过滤、日志记录或安全功能。

该配置中使用的 JavaScript 脚本(nginx_stream.js)包含各种 DNS 库模块文件。在 dns.js 模块中,dns_decode_level 变量设置了 DNS 数据包处理量。处理 DNS 数据包显然会降低性能。如果您使用的是类似于上面的配置,请将 dns_decode_level 设置为 0

 

更高级的 DoH 网关

NGINX 有着强大的 HTTP 处理能力,因此我们认为仅将 NGINX 用作简单的 DoH 网关是大材小用。

此处使用的 JavaScript 代码可以设置为对 DNS 数据包进行全部或局部解码,以便我们为 DoH 查询构建一个 HTTP 内容缓存,并根据 DNS 响应的最小 TTL 设置 Expires 和 Cache-Control 请求头。

下面是一个更完整的示例,其中包括额外的连接优化以及对内容缓存和日志记录的支持:

 

使用 NGINX Plus 功能的高级 DNS 过滤器

如果您订阅了 NGINX Plus,则可结合使用上述示例和一些更高级的 NGINX Plus 功能(如主动健康检查和高可用性),甚至可以使用缓存清除 API 来管理缓存的 DoH 响应。

亦或使用 NGINX Plus 键值存储构建 DNS 过滤系统,通过返回可有效阻止后续访问的 DNS 响应,保护用户远离恶意域名。您可以使用 RESTful NGINX Plus API 来动态管理键值存储的内容。

我们定义了两类恶意域名,即“拦截(blocked)”域名和“黑名单(blackhole)”域名。DNS 过滤系统会根据域名的类别,以不同的方式处理有关域名的 DNS 查询:

当我们的 DNS 过滤器收到请求时,它会首先检查是否有与所查询的 FQDN 精准匹配的键。如果有匹配的键,那么它将按相关值设置对请求进行“清洗”(拦截或列入黑名单)。如果没有精准匹配的键,那么它会在两个列表(一个是拦截域名,另一个是黑名单域名)中查找域名,并在找到后以适当的方式清洗请求。

如果查询的域名不是恶意域名,系统会将其转发到 Google DNS 服务器进行常规处理。

设置键值存储

我们在 NGINX Plus 键值存储的两种条目中定义了我们所认为的恶意域名:

以下键值存储配置在 stream 上下文中进行。keyval_zone 指令为键值存储分配一个内存块,称为 dns_config 指令加载任何已设置的匹配 FQDN 键值对,第二个和第三个指令定义了两个域名列表:

然后,我们可以使用 NGINX Plus API 为我们明确想要清洗的任何 FQDN 键赋值 blockedblackhole,或者修改与 blocked_domainsblackhole_domains 键关联的 CSV 格式列表。我们可以随时修改或删除确切的 FQDN,或者修改任一列表中的域名集,DNS 过滤器会随更改立即更新。

选择上游服务器处理请求

下面的配置加载了 $dns_response 变量,该变量由下文“配置 DNS 查询监听服务器”一节中定义的 server 块中的 js_preread 指令填充。当调用的 JavaScript 代码确定需要清洗请求时,它会视情况将变量设置为 blockedblackhole

我们使用 map 指令将 $dns_response 变量的值赋给 $upstream_pool 变量,从而控制下面“ 定义上游服务器”一节中的某个上游组来处理请求。非恶意域名的查询会被转发到默认的 Google DNS 服务器,而对拦截域名或黑名单域名的查询则由这些类别的上游服务器进行处理。

定义上游服务器

该配置定义了上游服务器的 blockedblackholegoogle 组,用于分别处理对拦截域名、黑名单域名及非恶意域名的请求。

配置 DNS 查询监听服务器

此处,我们在 stream 上下文中定义监听传入 DNS 请求的服务器。js_preread 指令调用 Javascript 代码来解码 DNS 数据包,从数据包的 NAME 字段中检索域名,并在键值存储中查找域名。如果域名与 FQDN 键相匹配,或者在 blocked_domainsblackhole_domains 列表中某个域名的区域内,则会被清理。否则,该域名将被发送到 Google DNS 服务器进行解析。

最后,我们只需添加最后的 server 块即可,它将用 blackholed 或 blocked 响应来响应查询。

请注意,此块中的服务器与上面的实际 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 server found)
;; global options: +mcd
;; Got answer:
;; ->>HEADER,,- opcode: QUERY, status: NOERROR, id: 58558
;; flags: qr aa rd ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
; www.foo.com.                  IN      A

;; ANSWER SECTION:
www.foo.com.           300      IN      A      0.0.0.0

;; Query time: 0 msec
;; SERVER: 172.0.0.1#53(126.0.0.1)
;; WHEN: Mon Dec 2 14:31:35 UTC 2019
;; MSG SIZEW  rcvd: 45

 

获取代码

DOH 和 DOT 的文件可在我的 GitHub 仓库中找到:

如欲试用 NGINX Plus,请立即下载 30 天免费试用版,或与我们联系以讨论您的用例