自动化研究产物 — 非上游项目。
本仓库是由自动化工具链为拉瓦尔大学(Université Laval)硕士论文构建的一次性实验环境,用于复现已发表的 GitHub Actions 工作流漏洞。它是
serverless-dns/serverless-dns在提交b0b1a1538aeb1991b5bc13dfe4e18e686913b12e(2025-04-26)时的逐字快照,依据该项目自身的许可证重新分发,该许可证文件已原样包含在本快照中。上游项目未参与其中,也从未成为攻击目标,此处研究的漏洞已公开。本仓库中的每个密钥和变量均为随机生成的虚拟值——不存在任何真实凭据。操作引用和运行器镜像均固定为 2025-04-26 时解析到的版本;有关对快照所做的每项更改,请参阅工具链输出中的
pinning.md。如有疑问或异议,请联系:[email protected]
serverless-dns 是一个类 Pi-Hole 的内容拦截、无服务器、存根 DNS-over-HTTPS (DoH) 和 DNS-over-TLS (DoT) 解析器。开箱即用地运行于 Cloudflare Workers、Deno Deploy、Fastly Compute@Edge 和 Fly.io。所有这些服务的免费套餐应足以覆盖每月 10 到 20 台设备的 DNS 流量。
RethinkDNS 在生产环境中于以下端点运行 serverless-dns:
服务端处理耗时从 0 毫秒(ms)到 2ms(中位数),端到端延迟(因地区和网络而异)介于 10ms 到 30ms(中位数)之间。
Fly.io 上的 Rethink DNS 解析器由 FOSS United 赞助。
Cloudflare Workers 是搭建 serverless-dns 最简单的平台:
如需分步说明,请参阅:
要设置拦截列表,请从浏览器访问 https://<my-domain>.tld/configure(应加载与 RethinkDNS 的 configure 页面 类似的内容)。
代码:
# 导航到工作目录
cd /my/work/dir
# 克隆本仓库
git clone https://github.com/serverless-dns/serverless-dns.git
# 导航到 serverless-dns
cd ./serverless-dns
Node:
# 如有需要,通过 nvm 安装 node v22+
# https://github.com/nvm-sh/nvm#installing-and-updating
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
nvm install --lts
# 下载依赖
npm i
# (可选)更新依赖
npm update
# 在 node 上运行 serverless-dns
./run n
# 运行 clinicjs.org 分析器
./run n [cpu|fn|mem]
Deno:
# 安装 deno.land v2+
# https://github.com/denoland/deno/#install
curl -fsSL https://deno.land/install.sh | sh
# 在 deno 上运行 serverless-dns
./run d
Fastly:
# 如有需要,通过 nvm 安装 node v22+
# 安装 Fastly CLI
# https://developer.fastly.com/learning/tools/cli
# 在 Fastly Compute@Edge 上运行 serverless-dns
./run f
Wrangler:
# 安装 Cloudflare Workers (cli),即 Wrangler
# https://developers.cloudflare.com/workers/cli-wrangler/install-update
npm i wrangler --save-dev
# 在 Cloudflare Workers (cli) 上运行 serverless-dns
# 请确保先配置好 Wrangler:
# https://developers.cloudflare.com/workers/cli-wrangler/authentication
./run w
# 使用 Chrome DevTools 分析 wrangler
# blog.cloudflare.com/profiling-your-workers-with-wrangler
本仓库的提交强制执行 Google JavaScript 风格指南(参考:.eslintrc.cjs)。
一个 git pre-commit 钩子会对 .js 文件运行 linter(eslint)和 formatter(prettier)。使用 git commit --no-verify
可绕过此钩子。
拉取请求也会检查代码风格违规,并在可能的情况下自动修复。
如需调整默认值,请配置 env.js。
对于 Cloudflare Workers,请在 wrangler.toml 中设置环境变量。
对于 Fastly Compute@Edge,请在 fastly.toml 中设置环境变量。
src/server-[node|workers|deno] <-> doh.js <-> plugin.jsplugin.js 流程:user-op.js -> cache-resolver.js -> cc.js -> resolver.jsserverless-dns 支持对 DoH 和 DoT 使用字母数字 bearer token 进行认证。对于 token,msg-key(密钥),将 hex(hmac-sha256(msg-key|domain.tld), msg) 的输出以 csv 格式追加到 ACCESS_KEYS 环境变量中。注意:msg 当前固定为 sdns-public-auth-info。
msg-key 放在 blockstamp 末尾,如下所示:
1:1:4AIggAABEGAgAA:<msg-key>(此处 1 是版本号,1:4AIggAABEGAgAA
是 blockstamp,<msg-key> 是认证密钥,: 是分隔符)。msg-key 放在包含 blockstamp 的 SNI(域名)末尾:
1-4abcbaaaaeigaiaa-<msg-key>(此处 1 是版本号,4abcbaaaaeigaiaa
是 blockstamp,<msg-key> 是认证密钥,- 是分隔符)。如果打算同时对 DoT 使用认证,请将 msg-key 保持较短(8 到 24 个字符),因为子域总长度可能仅限 63 个字符。
你可以从 max.rethinkdns.com 为你的 fork 生成访问密钥,如下所示:
msgkey="ShortAlphanumericSecret"
domain="my-serverless-dns-domain.tld"
curl 'https://max.rethinkdns.com/genaccesskey?key='"$msgkey"'&dom='"$domain"
# 输出
# {"accesskey":["my-serverless-dns-domain.tld|deadbeefd3adb33fa2bb33fd3eadf084beef3b152beefdead49bbb2b33fdead83d3adbeefdeadb33f"],"context":"sdns-public-auth-info"}
serverless-dns 可通过 Cloudflare Logpush 上传日志。
CF_ACCOUNT_ID=<hex-cloudflare-account-id>
CF_API_KEY=<api-key-with-logs-edit-permission-at-account-level>
R2_BUCKET=<r2-bucket-name>
R2_ACCESS_KEY=<r2-access-key-for-the-bucket>
R2_SECRET_KEY=<r2-secret-key-with-read-write-permissions>
# 可选,设置过滤器,使得仅此 worker 的日志被推送;但如果你
# 不需要按 Worker 名称(script-name)过滤,请相应编辑下面的 "filter" 字段。
SCRIPT_NAME=<name-of-the-worker-as-in-wrangler-toml>
# 更多选项,参考:developers.cloudflare.com/logs/get-started/api-configuration
# 使用 cURL 的 Logpush API:developers.cloudflare.com/logs/tutorials/examples/example-logpush-curl
# 可用的 Logpull 字段:developers.cloudflare.com/logs/reference/log-fields/account/workers_trace_events
curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/logpush/jobs" \
-H "Authorization: Bearer ${CF_API_KEY}" \
-H 'Content-Type: application/json' \
-d '{
"name": "dns-logpush",
"logpull_options": "fields=EventTimestampMs,Outcome,Logs,ScriptName×tamps=rfc3339",
"destination_conf": "r2://'"$R2_BUCKET"'/{DATE}?access-key-id='"${R2_ACCESS_KEY}"'&secret-access-key='"${R2_SECRET_KEY}"'&account-id='"{$CF_ACCOUNT_ID}"',
"dataset": "workers_trace_events",
"filter": "{\"where\":{\"and\":[{\"key\":\"ScriptName\",\"operator\":\"contains\",\"value\":\"'"${SCRIPT_NAME}"'\"},{\"key\":\"Outcome\",\"operator\":\"eq\",\"value\":\"ok\"}]}}",
"enabled": true,
"frequency": "low"
}'
wrangler.toml 属性 ,以启用 。发布到 R2 的日志可通过 R2 Workers、R2 API 或 Logpush API 检索。
Workers Analytics(若启用)会推送到一个日志键 lid,若未指定,则设置为无服务器部署的主机名,其中句点 . 替换为下划线 _。通过 API 查询 Analytics 时必须设置认证,API 返回 json;例如:https://max.rethinkdns.com/1:<optional-stamp>:<msg-key>/analytics?t=<time-interval-in-mins>&f=<field-name>。可能的 fields 包括 ip(客户端 ip)、qname(DNS 查询名称)、region(解析器区域)、qtype(DNS 查询类型)、dom(顶级域名)、ansip(DNS 应答 ip)和 cc(应答 ip 国家代码)。
Fly 和 Deno Deploy 尚未实现日志捕获和分析。
Deno Deploy(云)和 Deno(运行时)并不暴露相同的 API 接口(例如,Deno Deploy 仅 支持 HTTP/S 服务器监听器;而 Deno 除普通 HTTP 和 HTTP/S 外还支持原始 TCP/UDP/TLS)。
除 Node 外,serverless-dns 使用由环境变量 CF_DNS_RESOLVER_URL / CF_DNS_RESOLVER_URL_2 定义的 DoH 上游。
在 Node 上,默认 DNS 上游是 1.1.1.2(参考),或在 Fly.io 上运行时为 fdaa::3 处的递归 DNS 解析器。
Node 和 Deno 的入口点分别是 src/server-node.js、src/server-deno.ts,
两者都监听 TCP-over-TLS、HTTP/S 连接;而 Cloudflare Workers 的入口点(仅监听 HTTP(cli)或
HTTP/S(生产))是 src/server-workers.js;Fastly 的入口点是 src/server-fastly.js。
Node 上的本地(非生产)设置中,key(私钥)和 cert(公钥链)文件默认从
环境变量 TLS_KEY_PATH 和 TLS_CRT_PATH 定义的路径读取。
而对于 Node 上的生产设置(在 Fly.io 上),要么必须将 TLS_OFFLOAD 设置为 true,要么 key 和 cert _必须_在环境变量 TLS_CERTKEY 中进行 base64 编码(参考),如下所示:
# 或者:将 tls 卸载到 fly.io 并将 tls_offload 设置为 true
TLS_OFFLOAD="true"
# 或者:key(私钥)和 cert(公钥链)的 base64 表示
TLS_CERTKEY="KEY=b64_key_content\nCRT=b64_cert_content"
对于 Deno,key 和 cert 文件从环境变量 TLS_KEY_PATH 和 TLS_CRT_PATH 定义的路径读取(参考)。
每种运行时的_启动_过程各不相同:对于 Node,src/core/node/config.js 控制_启动_;
对于 Deno,是 src/core/deno/config.ts,对于 Workers,是 src/core/workers/config.js。
src/system.js 发布-订阅协调各模块之间的_启动_阶段。
在 Node 和 Deno 上,进程内 DNS 缓存由 @serverless-dns/lfu-cache 支持;Cloudflare Workers 由 Cache Web API 和
进程内 lfu 缓存共同支持。要在所有三个平台上完全禁用缓存,请设置环境变量 PROFILE_DNS_RESOLVES=true。
Cloudflare Workers 和 Deno Deploy 是临时的,即服务客户端请求的“进程”并非长期存活, 事实上,连续两个请求可能由两个不同的 isolates(“进程”)服务。Fastly Compute@Edge 也是临时的,但不使用 isolates,而是 Fastly 为每个请求创建并销毁一个 wasmtime 沙箱。Fly.io 上运行 Node 的解析器由 持久化 VM 支持,因此生命周期更长, 类似于传统的“serverfull”环境。
对于 Deno Deploy,代码库通过 deno bundle 打包成单个 javascript 文件,然后交给
Deno.com。
Cloudflare Workers 的构建时和运行时配置在 wrangler.toml 中定义。
Webpack5 将文件打包 成 ESM 模块,然后由 Wrangler 上传到 Cloudflare。
Fastly Compute@Edge 的构建时和运行时配置在 fastly.toml 中定义。
Webpack5 将文件打包 成 ESM 模块,然后由 npx js-compute-runtime 编译为 WASM,
随后使用 Fastly CLI 打包并发布到 Fastly Compute@Edge。
对于运行 Node 的 Fly.io,运行时指令在 fly.toml 中定义(供 dev 和 live 部署类型使用),
而部署指令在 node.Dockerfile 中。flyctl 相应地
在 Fly.io 的基础设施上设置 serverless-dns。
# 构建并部署到 cloudflare workers.dev
npm run build
# 通常,env-name 为 prod
npx wrangler publish [-e <env-name>]
# 打包、构建并部署到 fastly compute@edge
# developer.fastly.com/reference/cli/compute/publish
fastly compute publish
# 构建并部署到 fly.io
npm run build:fly
flyctl deploy --dockerfile node.Dockerfile --config <fly.toml> [-a <app-name>] [--image-label <some-uniq-label>]
对于将 TLS 终止卸载到 Fly.io 的部署(B1 部署类型),运行时指令则在
fly.tls.toml 中定义,该文件在端口 443 上设置 HTTP2 Cleartext 和 HTTP/1.1,并在端口 853 上设置基于 TCP 的 DNS。
参考:github/workflows。
190+ 个拦截列表被压缩到一个 Succinct Radix Trie(基于 Steve Hanov 的实现)中,并进行了修改
以加快字符串搜索(lookup),代价是牺牲“简洁性”。拦截列表使用
unix 时间戳进行版本控制(定义在由 pre.sh 下载的 src/basicconfig.json 中),每周生成一次,但我们希望每天/每小时生成一次,
如果可能的话参见),并托管在 Cloudflare R2 上(环境变量:CF_BLOCKLIST_URL)。
serverless-dns 下载3 个拦截列表文件,
这些文件在运行时启动期间用于设置 radix-trie,或者在服务 DNS 请求时惰性下载。
serverless-dns 编译了来自 190+ 个拦截列表的约 ~13M 条条目(截至 2023 年 1 月)。这些定义在 serverless-dns/blocklists 仓库中。
| 云平台 | 服务器位置 | 协议 | 域名 | 使用方式 |
|---|
| ⛅ Cloudflare Workers | 280+ (ping) | DoH | sky.rethinkdns.com | 配置 |
| 🦕 Deno Deploy | 30+ (ping) | DoH | 私有测试版 | |
| ⏱️ Fastly Compute@Edge | 80+ (ping) | DoH | 私有测试版 | |
| 🪂 Fly.io | 30+ (ping) | DoH 和 DoT | max.rethinkdns.com | 配置 |
| 平台 | 难度 | 运行时 | 文档 |
|---|
| ⛅ Cloudflare | 简单 | v8 Isolates | 托管于 Cloudflare Workers |
| 🦕 Deno.com | 中等 | Deno Isolates | 托管于 Deno.com |
| ⏱️ Fastly Compute@Edge | 简单 | Fastly JS | 托管于 Fastly Compute@Edge |
| 🪂 Fly.io | 困难 | Node MicroVM | 托管于 Fly.io |
logpush = trueLOG_LEVEL = "logpush",提高日志级别,使得仅输出请求和错误日志。LOGPUSH_SRC = "csv,of,subdomains",使 log-pusher.js 仅在 Workers hostname 包含其中一个子域时输出请求日志。