serverless-dns is a Pi-Hole esque content-blocking, serverless, stub DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT) resolver. Runs out-of-the-box on Cloudflare Workers, Deno Deploy, Fastly Compute@Edge, and Fly.io. Free tiers of all these services should be enough to cover 10 to 20 devices worth of DNS traffic per month.
RethinkDNS runs serverless-dns
in production at these endpoints:
Cloud platform | Server locations | Protocol | Domain | Usage |
---|---|---|---|---|
⛅ Cloudflare Workers | 280+ (ping) | DoH | sky.rethinkdns.com |
configure |
🦕 Deno Deploy | 30+ (ping) | DoH | private beta | |
⏱️ Fastly Compute@Edge | 80+ (ping) | DoH | private beta | |
🪂 Fly.io | 30+ (ping) | DoH and DoT | max.rethinkdns.com |
configure |
Server-side processing takes from 0 milliseconds (ms) to 2ms (median), and end-to-end latency (varies across regions and networks) is between 10ms to 30ms (median).
The Rethink DNS resolver on Fly.io is sponsored by FOSS United.
Cloudflare Workers is the easiest platform to setup serverless-dns
:
For step-by-step instructions, refer:
Platform | Difficulty | Runtime | Doc |
---|---|---|---|
⛅ Cloudflare | Easy | v8 Isolates | Hosting on Cloudflare Workers |
🦕 Deno.com | Moderate | Deno Isolates | Hosting on Deno.com |
⏱️ Fastly Compute@Edge | Easy | Fastly JS | Hosting on Fastly Compute@Edge |
🪂 Fly.io | Hard | Node MicroVM | Hosting on Fly.io |
To setup blocklists, visit https://<my-domain>.tld/configure
from your browser (it should load something similar to RethinkDNS' configure page).
For help or assistance, feel free to open an issue or submit a patch.
Code:
# navigate to work dir
cd /my/work/dir
# clone this repository
git clone https://github.com/serverless-dns/serverless-dns.git
# navigate to serverless-dns
cd ./serverless-dns
Node:
# install node v22+ via nvm, if required
# 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
# download dependencies
npm i
# (optional) update dependencies
npm update
# run serverless-dns on node
./run n
# run a clinicjs.org profiler
./run n [cpu|fn|mem]
Deno:
# install deno.land v2+
# https://github.com/denoland/deno/#install
curl -fsSL https://deno.land/install.sh | sh
# run serverless-dns on deno
./run d
Fastly:
# install node v22+ via nvm, if required
# install the Fastly CLI
# https://developer.fastly.com/learning/tools/cli
# run serverless-dns on Fastly Compute@Edge
./run f
Wrangler:
# install Cloudflare Workers (cli) aka Wrangler
# https://developers.cloudflare.com/workers/cli-wrangler/install-update
npm i wrangler --save-dev
# run serverless-dns on Cloudflare Workers (cli)
# Make sure to setup Wrangler first:
# https://developers.cloudflare.com/workers/cli-wrangler/authentication
./run w
# profile wrangler with Chrome DevTools
# blog.cloudflare.com/profiling-your-workers-with-wrangler
Commits on this repository enforces the Google JavaScript style guide (ref: .eslintrc.cjs).
A git pre-commit
hook that runs linter (eslint) and formatter (prettier) on .js
files. Use git commit --no-verify
to bypass this hook.
Pull requests are also checked for code style violations and fixed automatically where possible.
Configure env.js
if you need to tweak the defaults.
For Cloudflare Workers, setup env vars in wrangler.toml
, instead.
For Fastly Compute@Edge, setup env vars in fastly.toml
, instead.
- The request/response flow: client <->
src/server-[node|workers|deno]
<->doh.js
<->plugin.js
- The
plugin.js
flow:user-op.js
->cache-resolver.js
->cc.js
->resolver.js
serverless-dns supports authentication with an alpha-numeric bearer token for both DoH and DoT. For a token, msg-key
(secret), append the output of hex(hmac-sha256(msg-key|domain.tld), msg)
to ACCESS_KEYS
env var in csv format. Note: msg
is currently fixed to sdns-public-auth-info
.
- DoH: place the
msg-key
at the end of the blockstamp, like so:1:1:4AIggAABEGAgAA:<msg-key>
(here,1
is the version,1:4AIggAABEGAgAA
is the blockstamp,<msg-key>
is the auth secret, and:
is the delimiter). - DoT: place the
msg-key
at the end of the SNI (domain-name) containing the blockstamp:1-4abcbaaaaeigaiaa-<msg-key>
(here1
is the version,4abcbaaaaeigaiaa
is the blockstamp,<msg-key>
is the auth secret, and-
is the delimeter).
If the intention is to use auth with DoT too, keep msg-key
shorter (8 to 24 chars), since subdomains may only be 63 chars long in total.
You can generate the access keys for your fork from max.rethinkdns.com
, like so:
msgkey="ShortAlphanumericSecret"
domain="my-serverless-dns-domain.tld"
curl 'https://max.rethinkdns.com/genaccesskey?key='"$msgkey"'&dom='"$domain"
# output
# {"accesskey":["my-serverless-dns-domain.tld|deadbeefd3adb33fa2bb33fd3eadf084beef3b152beefdead49bbb2b33fdead83d3adbeefdeadb33f"],"context":"sdns-public-auth-info"}
serverless-dns can be setup to upload logs via Cloudflare Logpush.
- Setup a Logpush job:
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> # optional, setup a filter such that only logs form this worker ends up being pushed; but if you # do not need a filter on Worker name (script-name), edit the "filter" field below accordingly. SCRIPT_NAME=<name-of-the-worker-as-in-wrangler-toml> # for more options, ref: developers.cloudflare.com/logs/get-started/api-configuration # Logpush API with cURL: developers.cloudflare.com/logs/tutorials/examples/example-logpush-curl # Available Logpull fields: 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" }'
- Set
wrangler.toml
propertylogpush = true
, which enables Logpush. - (Optional) env var
LOG_LEVEL = "logpush"
, which raises the log-level such that only request and error logs are emitted. - (Optional) Set env var
LOGPUSH_SRC = "csv,of,subdomains"
, which makeslog-pusher.js
emit request logs only if Workershostname
contains one of the subdomains.
Logs published to R2 can be retrieved either using R2 Workers, the R2 API, or the Logpush API.
Workers Analytics, if enabled, is pushed against a log-key, lid
, which if unspecified is set to hostname of the serverless deployment with periods, .
, replaced with underscores, _
. Auth must be setup when querying for Analytics via the API which returns a json; ex: https://max.rethinkdns.com/1:<optional-stamp>:<msg-key>/analytics?t=<time-interval-in-mins>&f=<field-name>
. Possible fields
are ip
(client ip), qname
(dns query name), region
(resolver region), qtype
(dns query type), dom
(top-level domains), ansip
(dns answer ips), and cc
(ans ip country codes).
Log capture and analytics isn't yet implemented for Fly and Deno Deploy.
Deno Deploy (cloud) and Deno (the runtime) do not expose the same API surface (for example, Deno Deploy only supports HTTP/S server-listeners; whereas, Deno suports raw TCP/UDP/TLS in addition to plain HTTP and HTTP/S).
Except on Node, serverless-dns
uses DoH upstreams defined by env vars, CF_DNS_RESOLVER_URL
/ CF_DNS_RESOLVER_URL_2
.
On Node, the default DNS upstream is 1.1.1.2
(ref) or the recursive DNS resolver at fdaa::3
when running on Fly.io.
The entrypoints for Node and Deno are src/server-node.js
, src/server-deno.ts
respectively,
and both listen for TCP-over-TLS, HTTP/S connections; whereas, the entrypoint for Cloudflare Workers, which only listens over HTTP (cli) or
over HTTP/S (prod), is src/server-workers.js
; and for Fastly its src/server-fastly.js
.
Local (non-prod) setups on Node, key
(private) and cert
(public chain) files, by default, are read from
paths defined in env vars, TLS_KEY_PATH
and TLS_CRT_PATH
.
Whilst for prod setup on Node (on Fly.io), either TLS_OFFLOAD
must be set to true
or key
and cert
must be
base64 encoded in env var TLS_CERTKEY
(ref), like so:
# EITHER: offload tls to fly.io and set tls_offload to true
TLS_OFFLOAD="true"
# OR: base64 representation of both key (private) and cert (public chain)
TLS_CERTKEY="KEY=b64_key_content\nCRT=b64_cert_content"
For Deno, key
and cert
files are read from paths defined in env vars, TLS_KEY_PATH
and TLS_CRT_PATH
(ref).
Process bringup is different for each of these runtimes: For Node, src/core/node/config.js
governs the bringup;
while for Deno, it is src/core/deno/config.ts
, and for Workers it is src/core/workers/config.js
.
src/system.js
pub-sub co-ordinates the bringup phase among various modules.
On Node and Deno, in-process DNS caching is backed by @serverless-dns/lfu-cache
; Cloudflare Workers is backed by both Cache Web API and
in-process lfu caches. To disable caching altogether on all three platfroms, set env var, PROFILE_DNS_RESOLVES=true
.
Cloudflare Workers, and Deno Deploy are ephemeral, as in, the "process" that serves client requests is not long-lived, and in fact, two back-to-back requests may be served by two different isolates ("processes"). Fastly Compute@Edge is the also ephemeral but does not use isolates, instead Fastly creates and destroys a wasmtime sandbox for each request. Resolver on Fly.io, running Node, is backed by persistent VMs and is hence longer-lived, like traditional "serverfull" environments.
For Deno Deploy, the code-base is bundled up in a single javascript file with deno bundle
and then handed off
to Deno.com.
Cloudflare Workers build-time and runtime configurations are defined in wrangler.toml
.
Webpack5 bundles the files in an ESM module which is then uploaded to Cloudflare by Wrangler.
Fastly Compute@Edge build-time and runtime configurations are defined in fastly.toml
.
Webpack5 bundles the files in an ESM module which is then compiled to WASM by npx js-compute-runtime
and subsequently packaged and published to Fastly Compute@Edge with the Fastly CLI.
For Fly.io, which runs Node, the runtime directives are defined in fly.toml
(used by dev
and live
deployment-types),
while deploy directives are in node.Dockerfile
. flyctl
accordingly sets
up serverless-dns
on Fly.io's infrastructure.
# build and deploy for cloudflare workers.dev
npm run build
# usually, env-name is prod
npx wrangler publish [-e <env-name>]
# bundle, build, and deploy for fastly compute@edge
# developer.fastly.com/reference/cli/compute/publish
fastly compute publish
# build and deploy to fly.io
npm run build:fly
flyctl deploy --dockerfile node.Dockerfile --config <fly.toml> [-a <app-name>] [--image-label <some-uniq-label>]
For deploys offloading TLS termination to Fly.io (B1
deployment-type), the runtime directives are instead defined in
fly.tls.toml
, which sets up HTTP2 Cleartext and HTTP/1.1 on port 443
, and DNS over TCP on port 853
.
Ref: github/workflows.
190+ blocklists are compressed in a Succinct Radix Trie (based on Steve Hanov's impl) with modifications
to speed up string search (lookup
) at the expense of "succintness". The blocklists are versioned
with unix timestamp (defined in src/basicconfig.json
downloaded by pre.sh
), which is generated once every week, but we'd like to generate 'em daily / hourly,
if possible see), and hosted on Cloudflare R2 (env var: CF_BLOCKLIST_URL
).
serverless-dns
downloads 3 blocklist files
required to setup the radix-trie during runtime bring-up or, downloads them lazily,
when serving a DNS request.
serverless-dns
compiles around ~13M entries (as of Jan 2023) from around 190+ blocklists. These are defined in the serverless-dns/blocklists repository.