曾经我觉得有 uBlock Origin 之类的浏览器插件,不需要在网关上做广告和追踪器的过滤。但随着手机使用量的增加,我逐渐意识到在网关上做集中化管理还是有好处的。正好 Raspberry Pi 4 已经闲置了两个月,那就来试试 Pi-Hole 吧。

本文介绍如何使用 Pi-Hole 过滤广告、追踪器和恶意软件的域名,并使用 DoH / DoT 对 DNS 请求加密。最后,Android 设备可以通过 Private DNS 功能,即使不在家,也可以享受 Pi-Hole 的好处。

一、Pi-Hole 简介

在使用浏览器网上冲浪的过程中,DNS 的作用是把域名翻译成 IP 地址,以供浏览器连接。网页上有各种广告和追踪器,这些东西往往是专营此业的第三方服务提供的(如 Google AdSenseGoogle Analytics),与用户浏览的网站并不属于同一域名。那么,将这些域名在 DNS 层面上屏蔽掉,就达到了过滤广告和追踪器的效果。上了年纪的中国网民一定很熟悉十几年前流行的使用 hosts 文件屏蔽广告域名,或是绕过 GFW 的 DNS 污染的套路,其原理是类似的。只是随着个人电脑和手机等移动设备的增多,在多设备之间维护一份屏蔽列表是一件非常费劲(如果不是不可能)的事情。这种时候,在网关上部署一个集中化管理的屏蔽列表便是一种省力的方式。Pi-Hole 便是这样一套广告、追踪器与恶意软件域名过滤及管理的方案。

二、Pi-Hole 的安装与部署

正如其名字所暗示的,Pi-Hole 最早是为 Raspberry Pi 设计的。Pi-Hole 虽然较轻量,但是对于一般的家庭路由器来说还是太重了,因此跑在 Raspberry Pi 上是个很不错的选择。当然,Pi-Hole 的负载能力与硬件有关,如果你有诸如 Microserver Gen8 / Gen10 / Gen10+ 之类的家用服务器的话,Pi-Hole 在其上可以发挥更大的效能。

很遗憾地,Pi-Hole 没有提供通过包管理器的安装方式,而是通过「一键脚本」这种我个人很不喜欢的方式实现安装。官方推荐的方式是用 curl | bash 这种极富争议的代码执行方式:

curl -sSL https://install.pi-hole.net | bash

更谨慎的方式则是下载并检视其安装脚本,然后再安装:

wget -O basic-install.sh https://install.pi-hole.net
sudo bash basic-install.sh

安装过程中,脚本会通过 TUI 询问一些问题。安装完成后,屏幕上会打印随机生成的网页后台的密码。Pi-Hole 会为自己配置一个静态 IP 地址。本文中以 192.168.2.191 为例。

Pi-Hole 推荐用户立刻将其设置为局域网 DHCP 服务(通常由路由器提供)所分配的 DNS 地址,但你也许想要先配置一下 Pi-Hole,并确保其可用,再将其设置为局域网内的 DNS 地址。

三、重要的 Pi-Hole 配置

域名屏蔽方式

默认配置下,Pi-Hole 对被屏蔽的域名的 A 记录返回 0.0.0.0。这一行为在某些情况下并不是最优的。对于 Windows 来说,0.0.0.0 的确是一个不可达的地址,而对 GNU/Linux 和 macOS 来说,这个地址等同于 127.0.0.1。如果你使用 GNU/Linux 或 macOS,而本机又恰好运行了一个 HTTP 服务器监听了 80/tcp 和 443/tcp 端口,那么在你使用 Pi-Hole 之后,你本机的 HTTP 服务器会接到大量试图访问广告和追踪器域名的无效请求,并且你在浏览器里可能还会遇到莫名其妙的证书错误。比如当你访问 https://analytics.google.com/ 的时候,由于 analytics.google.com 这个域名默认被 Pi-Hole 屏蔽从而解析到 0.0.0.0,你的浏览器会拿到你本机 HTTP 服务器所用的 TLS 证书。

如果不希望使用返回 0.0.0.0 作为屏蔽方式,可以更改 /etc/pihole/pihole-FTL.conf 文件,改用别的屏蔽方式:

# 对被屏蔽域名返回 NXDOMAIN(该域名不存在)
BLOCKINGMODE=NXDOMAIN
# 对被屏蔽域名返回 NODATA(该域名存在,但是没有 A / AAAA 记录)
BLOCKINGMODE=NODATA

当然,如果你本机并没有运行监听 80/tcp 和 443/tcp 的服务,那么默认的屏蔽方式也是适合你的——浏览器只是会得到一个 connection refused 而已。

日志记录

如果你没有在安装过程中关闭日志的话,那么 Pi-Hole 默认是会记录所有 DNS 请求的。这也是我推荐新晋 Pi-Hole 用户的设置,因为这有助于发现局域网内各种奇怪的请求。比如若不是 Pi-Hole,我是绝对不会想到 Velop 在 24 小时内请求 www.belkin.com 这个域名超过 2500 次的

待使用一段时间后,你可以调低 Pi-Hole 的日志数据库(SQLite)写入频次,以减少对 Raspberry Pi 的存储卡的损耗:

# 每小时写入一次,而不是默认的每分钟
DBINTERVAL=60

四、使用 dnscrypt-proxy 加密从 Pi-Hole 发出的请求

在互联网的田园时代,一切都是明文的。后来虽然有了 HTTPS,但是明文的 HTTP 依然是主流。从大约十年前开始,在 PRISM 的余波之下,以 2010 年 Gmail 默认以 HTTPS 加载为标志性事件,各大互联网公司开始推崇 HTTPS by Default 的理念。十年后的今天,绝大部分主流网站都已支持 HTTPS 并以此为默认,但大部分的用户仍在使用未加密的 DNS 协议。是时候改变了。

早在 2012 年,出于翻墙的原因,我就用 dnscrypt-proxy 来实现抗污染 DNS。当时的 DNSCrypt 协议后来被 DNS over HTTPS (DoH) 所取代,上游服务器也从 OpenDNS 换成了 Cloudflare。如今想要实现加密 DNS,我第一时间想到的仍然是它。

现在的 dnscrypt-proxy 已经改用 Golang 重写,这使得它能更容易地支持 x86_64 以外的平台。在 Raspberry Pi 上安装 dnscrypt-proxy 之后,可使用如下最小配置运行起来:

# /etc/dnscrypt-proxy/dnscrypt-proxy.toml

listen_addresses = ['0.0.0.0:44353']
server_names = ['cloudflare']
cache = false

[sources]
  [sources.'public-resolvers']
  url = 'https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md'
  cache_file = '/var/cache/dnscrypt-proxy/public-resolvers.md'
  minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
  refresh_delay = 72
  prefix = ''

注意:有些发行版(如 Raspbian / Debian)默认使用 .socket 方式激活 dnscrypt-proxy.service,你可能需要对此做一些处理才能让 dnscrypt-proxy 监听 53/udp 以外的端口:

$ sudo systemctl mask dnscrypt-proxy.socket
$ sudo systemctl disable dnscrypt-proxy-resolvconf.service
# 去掉对 .socket 的依赖
$ sudo systemctl edit --full dnscrypt-proxy.service

启动 dnscrypt-proxy 之后,可观察到类似这样的日志:

[NOTICE] Source [/var/cache/dnscrypt-proxy/public-resolvers.md] loaded
[NOTICE] dnscrypt-proxy 2.0.19
[NOTICE] Now listening to 0.0.0.0:44353 [UDP]
[NOTICE] Now listening to 0.0.0.0:44353 [TCP]
[NOTICE] [cloudflare] OK (DoH) - rtt: 19ms
[NOTICE] Server with the lowest initial latency: cloudflare (rtt: 19ms)

这代表现在 44353/udp 收到的普通 DNS 请求将会通过 DoH 加密转发给 Cloudlfare 进行解析。

使用 dig @192.168.2.191 -p44353 google.com 确认 dnscrypt-proxy 工作正常之后,即可在 Pi-Hole 的后台将上游 DNS 改成 127.0.0.1#44353

pi-hole-upstream-dnscrypt-proxy

至此,Pi-Hole 对外的 DNS 请求都是加密的了,你的 ISP 不再能看到你局域网内设备的 DNS 请求。

以下命令可用于确认 Pi-Hole 是否工作正常:

# 正常返回
$ dig @192.168.2.191 google.com

# 返回 0.0.0.0 或 NXDOMAIN 或 NODATA(根据不同的屏蔽方式)
# doubleclick.net 是 Google 广告的域名
$ dig @192.168.2.191 doubleclick.net

若一切正常,则可在路由器中将 DHCP 分配的 DNS 地址改成 Pi-Hole 的地址。待局域网内的设备重连,或是 DHCP 租期过期之后,即可 Pi-Hole 中观察到被放行和被屏蔽的域名请求。且通过抓包可观察到,Pi-Hole 对外的 DNS 请求是加密的而非明文的。

五、使用 CoreDNS 加密发往 Pi-Hole 的请求 / 不在家时也能使用 Pi-Hole

自 Android 9 "Pie" 开始,系统里内建了一个 Private DNS 功能。开启之后,Android 设备可以在任意网络(蜂窝网络和 Wi-Fi)下使用加密的 DNS 协议进行域名解析。根据我的测试,除了 IMS (Wi-Fi Calling) 和 RCS 等底层通讯服务之外,几乎所有的 DNS 请求都是加密的,而不再使用手机运营商或是 Wi-Fi 热点所提供的普通 DNS 服务器。

这是 Android 首次可以免 root、免第三方应用的情况下,直接在系统设置里设置一个真正「全局」的 DNS——而且还是加密的。我们可以使用这个功能,让 Android 设备在不连接家里 Wi-Fi 的时候,也可以使用家里的 Pi-Hole。

我一直以为 Private DNS 是用的 DoH (DNS over HTTPS),而 @yegle 提醒我这其实用的是另一个加密的 DNS 协议 DoT (DNS over TLS)。dnscrypt-proxy 只支持 DoH(接收和转发),但不支持 DoT。于是我找到了后起之秀 CoreDNS 用来接收 DoT 请求。

CoreDNS 同样使用 Golang 编写,提供了各平台的预编译单文件可执行程序systemd 文件,因此即使你的发行版没有提供 CoreDNS 的打包,使用 Ansible 快速写一个部署脚本也不费事。

以下是我使用的最小配置(真的很小):

# /etc/coredns/Corefile

tls://.:853 {
  tls /etc/coredns/cert.crt /etc/coredns/cert.key
    forward . 127.0.0.1:53
}

以上配置会让 CoreDNS 监听 853/tcp 的 DoT 请求,并将其转发给 53/udp,也就是 Pi-Hole 的端口。

将自己的域名指向家里的公网 IP 地址,然后在路由器上配置 DNAT 将 853/tcp 转发给 Raspberry Pi,最后将 Android 设备的 Private DNS 设置为自己的域名。设置方法可以参考 Cloudflare Blog 的图文教程,只需要将 Private DNS 的域名改成自己的域名即可。

至此,即使 Android 设备在使用蜂窝网络或是咖啡馆的 Wi-Fi 热点,其 DNS 请求也是加密发给家里的 Pi-Hole 的,时刻享受着其提供的广告、追踪器和恶意软件域名过滤功能。

六、总结

经过以上的折腾,不在家时 Android 的 DNS 请求是这样流转的:

Android ---> CoreDNS ---> Pi-Hole ---> dnscrypt-proxy ---> Cloudflare DNS
        (853/tcp, DoT) (53/udp, DNS)  (44353/udp, DNS)     (443/tcp, DoH)

而局域网内的设备则是直接发往 Pi-Hole。无论哪一条路,在公网上的部分(到达 Cloudflare 之前)都是加密的,对 ISP 不可见。

CoreDNS 和 Cloudflare DNS 都是既支持 DoH 也支持 DoT 的,所以上图中的 dnscrypt-proxy 完全可以拿 CoreDNS 替换,这就留给读者当作练习了。

任何暴露在公网的服务皆有被攻击的风险,CoreDNS 也不例外。如果你有被害妄想症的话,你可以通过其 acl 插件进行一定的限制。

出于隐私问题的顾虑,Cloudflare DNS 并不支持 EDNS,这意味如果你在离你较远的位置搭建了本文所述的方案的话(比如你在欧洲,而在一台美国 VPS 上安装 Pi-Hole),那么你得到的 DNS 解析可能不是地理位置最优的。因此如果有条件的话,推荐还是在家庭宽带环境下搭建本文所述的方案,或者至少在离你(网络上)较近的服务器上搭建。

本文地址: https://wzyboy.im/post/1372.html 。转载请注明出处。


欢迎留下评论。评论前,请先阅读《隐私声明》。