diff --git a/.github/workflows/cf.yml b/.github/workflows/cf.yml
index 77b8c83db1..6a30eb7396 100644
--- a/.github/workflows/cf.yml
+++ b/.github/workflows/cf.yml
@@ -1,5 +1,9 @@
name: β
CF
on:
+ # github.com/serverless-dns/blocklists/blob/6021f80f/.github/workflows/createUploadBlocklistFilter.yml#L4-L6
+ # schedule:
+ # at 7:53 on 3rd, 10th, 18th, 26th of every month
+ # - cron: '53 7 3,10,18,26 * *'
# docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow
# docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#workflow_dispatch
# docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#onworkflow_dispatchinputs
@@ -48,6 +52,7 @@ on:
env:
GIT_REF: ${{ github.event.inputs.commit || github.ref }}
+ WRANGLER_VER: '3.56.0'
# default is 'dev' which is really empty/no env
WORKERS_ENV: ''
@@ -57,8 +62,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- - name: Checkout
- uses: actions/checkout@v3.3.0
+ - name: π Checkout
+ uses: actions/checkout@v4
with:
ref: ${{ env.GIT_REF }}
fetch-depth: 0
@@ -86,20 +91,33 @@ jobs:
WENV: 'prod'
COMMIT_SHA: ${{ github.sha }}
+ - name: π½ Cron?
+ if: github.event.schedule == '53 7 3,10,18,26 * *'
+ run: |
+ echo "WORKERS_ENV=${WENV}" >> $GITHUB_ENV
+ echo "COMMIT_SHA=${COMMIT_SHA}" >> $GITHUB_ENV
+ shell: bash
+ env:
+ # cron deploys always deploy to prod
+ WENV: 'prod'
+ COMMIT_SHA: ${{ github.sha }}
+
# npm (and node16) are installed by wrangler-action in a pre-job setup
- name: π Get dependencies
run: npm i
- name: π Wrangler publish
# github.com/cloudflare/wrangler-action
- uses: cloudflare/wrangler-action@2.0.0
+ uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
# input overrides env-defaults, regardless
environment: ${{ env.WORKERS_ENV }}
+ wranglerVersion: ${{ env.WRANGLER_VER }}
+ accountId: ${{ secrets.CF_ACCOUNT_ID }}
env:
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
- GIT_COMMIT_ID: ${{ env.GIT_REF }}
+ # setting CLOUDFLARE_ACCOUNT_ID no longer works
+ GIT_COMMIT_ID: ${{ env.COMMIT_SHA }}
- name: π€ Notice
run: |
diff --git a/.github/workflows/deno-deploy.yml b/.github/workflows/deno-deploy.yml
index 8e4b9622b0..5fb1f57fa5 100644
--- a/.github/workflows/deno-deploy.yml
+++ b/.github/workflows/deno-deploy.yml
@@ -64,7 +64,7 @@ jobs:
contents: read
steps:
- name: π Fetch code
- uses: actions/checkout@v3.3.0
+ uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.git-ref || github.ref }}
fetch-depth: 0
@@ -85,17 +85,17 @@ jobs:
git reset
git merge origin/${BUILD_BRANCH} || :
- - name: π¦ Install Deno @1.29
+ - name: π¦ Install Deno@2.x
uses: denoland/setup-deno@main
with:
- deno-version: 1.29.3
+ deno-version: 2.x
- name: π¦ Bundle up
if: ${{ env.DEPLOY_MODE == 'action' }}
run: |
echo "::notice::do not forget to set DENO_PROJECT_NAME via github secrets!"
deno task prepare
- deno bundle ${IN_FILE} ${OUT_FILE}
+ # todo: deno bundle ${IN_FILE} ${OUT_FILE}
shell: bash
# github.com/denoland/deployctl/blob/febd898/action.yml
@@ -103,16 +103,18 @@ jobs:
- name: π€ΈπΌ Deploy to deno.com
id: dd
if: ${{ env.DEPLOY_MODE == 'action' }}
- uses: denoland/deployctl@1.4.0
+ uses: denoland/deployctl@v1
with:
project: ${{ env.PROJECT_NAME }}
- entrypoint: ${{ env.OUT_FILE }}
+ # todo: if bundling, replace IN_FILE w/ OUT_FILE
+ entrypoint: ${{ env.IN_FILE }}
- name: π’ Merge latest code into deploy-branch
if: ${{ env.DEPLOY_MODE == 'auto' }}
run: |
git config --local user.name 'github-actions[bot]'
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
+ # todo: deno bundle has been deprecated
git add ${OUT_FILE}
git commit -m "Update bundle for ${GITHUB_SHA}" && \
echo "::notice::Pushing to ${BUILD_BRANCH}" || \
diff --git a/.github/workflows/fly.yml b/.github/workflows/fly.yml
index 088db6ab6f..f1628bef29 100644
--- a/.github/workflows/fly.yml
+++ b/.github/workflows/fly.yml
@@ -1,6 +1,10 @@
name: πͺ Fly
on:
+ # github.com/serverless-dns/blocklists/blob/6021f80f/.github/workflows/createUploadBlocklistFilter.yml#L4-L6
+ schedule:
+ # at 7:53 on 2nd, 9th, 17th, 25th of every month
+ - cron: '53 7 2,9,17,25 * *'
push:
branches:
- "main"
@@ -77,7 +81,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: π Checkout
- uses: actions/checkout@v3.3.0
+ uses: actions/checkout@v4
with:
ref: ${{ env.GIT_REF }}
fetch-depth: 0
@@ -121,6 +125,15 @@ jobs:
env:
COMMIT_SHA: ${{ github.sha }}
+ - name: π¨π½ Prod via cron?
+ if: github.event.schedule == '53 7 2,9,17,25 * *'
+ run: |
+ echo "FLY_APP=${FLY_PROD_APP}" >> $GITHUB_ENV
+ echo "::notice::Deploying PROD / ${GIT_REF} @ ${COMMIT_SHA}"
+ shell: bash
+ env:
+ COMMIT_SHA: ${{ github.sha }}
+
- name: ππ¨βπ Onebox via dispatch?
if: ${{ github.event_name == 'workflow_dispatch' &&
github.event.inputs.deployment-type == 'onebox' }}
diff --git a/.github/workflows/ghcr.yml b/.github/workflows/ghcr.yml
new file mode 100644
index 0000000000..5fb5c22bb9
--- /dev/null
+++ b/.github/workflows/ghcr.yml
@@ -0,0 +1,107 @@
+name: π runc
+
+on:
+ push:
+ tags:
+ - "v*"
+ workflow_dispatch:
+
+env:
+ REGISTRY: "ghcr.io"
+ IMAGE_NAME: ${{ github.repository }}
+ IMAGE_NAME_BUN: ${{ github.repository }}-bun
+ GIT_REF: ${{ github.event.inputs.git-ref || github.ref }}
+
+# docs.github.com/en/actions/publishing-packages/publishing-docker-images
+jobs:
+ nodejs:
+ name: π Node on Alpine
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ attestations: write
+ id-token: write
+
+ steps:
+ - name: π Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ env.GIT_REF }}
+ fetch-depth: 0
+
+ - name: π Login
+ uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: π·οΈ Metadata
+ id: meta
+ uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+
+ - name: π Build
+ id: push
+ uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
+ with:
+ context: .
+ file: ./node.Dockerfile
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ - name: π Attest
+ uses: actions/attest-build-provenance@v1
+ with:
+ subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ subject-digest: ${{ steps.push.outputs.digest }}
+ push-to-registry: true
+
+ bunjs:
+ name: π Bun on Alpine
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ attestations: write
+ id-token: write
+
+ steps:
+ - name: π Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ env.GIT_REF }}
+ fetch-depth: 0
+
+ - name: π Login
+ uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: π·οΈ Metadata
+ id: meta
+ uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BUN }}
+
+ - name: π Build
+ id: push
+ uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
+ with:
+ context: .
+ file: ./bun.Dockerfile
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ - name: π Attest
+ uses: actions/attest-build-provenance@v1
+ with:
+ subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BUN }}
+ subject-digest: ${{ steps.push.outputs.digest }}
+ push-to-registry: true
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index d5ab340d78..77d7712996 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -32,7 +32,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: π Get latest code
- uses: actions/checkout@v3.3.0
+ uses: actions/checkout@v4
with:
# Checkout base (target) repo
ref: ${{ env.GH_REF }}
@@ -89,7 +89,9 @@ jobs:
- name: π’ Push to origin?
if: ${{ env.BASE_REPO == env.PR_HEAD_REPO }}
run: |
- git push origin HEAD:${{ env.PR_HEAD_REF }}
+ git push origin HEAD:$PR_REF
+ env:
+ PR_REF: ${{ env.PR_HEAD_REF }}
# `GITHUB_TOKEN` owned by `github-actions[bot]` has write access to
# origin in this `PR-target` workflow but not the write access to fork
@@ -99,5 +101,9 @@ jobs:
- name: π’ Push to fork?
if: ${{ env.BASE_REPO != env.PR_HEAD_REPO }}
run: |
- git remote add fork ${{ env.PR_HEAD_REPO }}
- git push fork HEAD:${{ env.PR_HEAD_REF }}
+ git remote add fork $PR_REPO
+ git push fork HEAD:$PR_REF
+ env:
+ # ref: docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
+ PR_REF: ${{ env.PR_HEAD_REF }}
+ PR_REPO: ${{ env.PR_HEAD_REPO }}
diff --git a/.github/workflows/profiler.yml b/.github/workflows/profiler.yml
index 03abc64574..447198d17d 100644
--- a/.github/workflows/profiler.yml
+++ b/.github/workflows/profiler.yml
@@ -15,13 +15,14 @@ on:
default: 'main'
# docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_dispatchinputs
js-runtime:
- description: "proc: deno/node"
+ description: "proc: deno/node/bun"
required: false
default: 'node'
type: choice
options:
- node
- deno
+ - bun
mode:
description: "p1 (fetch) / p2 (http2) / p3 (udp/tcp)"
required: false
@@ -40,8 +41,9 @@ env:
GIT_REF: ${{ github.event.inputs.git-ref || github.ref }}
JS_RUNTIME: 'node'
MAXTIME_SEC: '30s'
- NODE_VER: '19.x'
- DENO_VER: '1.29.3'
+ NODE_VER: '22.x'
+ DENO_VER: '2.x'
+ BUN_VER: '1.x'
MODE: 'p1'
QDOH: 'q'
@@ -52,7 +54,7 @@ jobs:
steps:
- name: π Checkout
- uses: actions/checkout@v3.3.0
+ uses: actions/checkout@v4
with:
ref: ${{ env.GIT_REF }}
fetch-depth: 0
@@ -66,9 +68,9 @@ jobs:
JSR: ${{ github.event.inputs.js-runtime || env.JS_RUNTIME }}
# docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs-or-python
- - name: π Setup Node @v19
+ - name: π Setup Node @v22
if: env.JS_RUNTIME == 'node'
- uses: actions/setup-node@v3.6.0
+ uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}
@@ -80,7 +82,7 @@ jobs:
npm run build --if-present
# deno.com/blog/deploy-static-files#example-a-statically-generated-site
- - name: π¦ Setup Deno @1.29.3
+ - name: π¦ Setup Deno @2.x
if: env.JS_RUNTIME == 'deno'
uses: denoland/setup-deno@main
with:
@@ -89,8 +91,22 @@ jobs:
- name: π₯ Deno deps
if: env.JS_RUNTIME == 'deno'
run: |
+ deno task prepare
deno cache ./src/server-deno.ts
+ # bun.sh/docs/cli/install#ci-cd
+ - name: π Setup Bun @ latest
+ if: env.JS_RUNTIME == 'bun'
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ # github.com/oven-sh/setup-bun
+ - name: π₯ Bun deps
+ if: env.JS_RUNTIME == 'bun'
+ run: |
+ bun i
+
# if non-interactive, prefer apt-get: unix.stackexchange.com/a/590703
# github.com/natesales/repo
# docs.github.com/en/actions/using-github-hosted-runners/customizing-github-hosted-runners#installing-software-on-ubuntu-runners
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 38c85d8126..e2c1eb66ee 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -1,14 +1,10 @@
-# This workflow uses actions that are not certified by GitHub. They are provided
-# by a third-party and are governed by separate terms of service, privacy
-# policy, and support documentation.
-
-name: supply-chain scorecard
+name: supply-chain scorecard
on:
# For Branch-Protection check. Only the default branch is supported. See
- # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
+ # github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
branch_protection_rule:
# To guarantee Maintained check is occasionally updated. See
- # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
+ # github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '53 21 * * 2'
push:
@@ -30,27 +26,28 @@ jobs:
# contents: read
# actions: read
+ # ref: github.com/ossf/scorecard/blob/main/.github/workflows/scorecard-analysis.yml
steps:
- name: "Checkout code"
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
+ uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Run analysis"
- uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2
+ uses: ossf/scorecard-action@v2.4.1
with:
results_file: results.sarif
results_format: sarif
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
# - you want to enable the Branch-Protection check on a *public* repository, or
# - you are installing Scorecard on a *private* repository
- # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
+ # To create the PAT, follow the steps in github.com/ossf/scorecard-action#authentication-with-pat.
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
# Public repositories:
# - Publish results to OpenSSF REST API for easy access by consumers
# - Allows the repository to include the Scorecard badge.
- # - See https://github.com/ossf/scorecard-action#publishing-results.
+ # - See github.com/ossf/scorecard-action#publishing-results.
# For private repositories:
# - `publish_results` will always be set to `false`, regardless
# of the value entered here.
@@ -59,7 +56,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
- uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0
+ uses: actions/upload-artifact@v4
with:
name: SARIF file
path: results.sarif
@@ -67,6 +64,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4
+ uses: github/codeql-action/upload-sarif@v3.28.16
with:
sarif_file: results.sarif
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b25b899e22..a9e384c85d 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -12,5 +12,6 @@
],
"cSpell.enableFiletypes": [
"env"
- ]
+ ],
+ "deno.enable": true
}
diff --git a/README.md b/README.md
index 3329779cf2..b7b6309150 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,12 @@ RethinkDNS runs `serverless-dns` in production at these endpoints:
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).
+[
](https://fossunited.org/grants)
+
+The *Rethink DNS* resolver on Fly.io is sponsored by [FOSS United](https://fossunited.org/grants).
+
### Self-host
Cloudflare Workers is the easiest platform to setup `serverless-dns`:
@@ -57,7 +63,7 @@ cd ./serverless-dns
Node:
```bash
-# install node v19+ via nvm, if required
+# 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
@@ -77,7 +83,7 @@ npm update
Deno:
```bash
-# install deno.land v1.22+
+# install deno.land v2+
# https://github.com/denoland/deno/#install
curl -fsSL https://deno.land/install.sh | sh
@@ -87,7 +93,7 @@ curl -fsSL https://deno.land/install.sh | sh
Fastly:
```bash
-# install node v18+ via nvm, if required
+# install node v22+ via nvm, if required
# install the Fastly CLI
# https://developer.fastly.com/learning/tools/cli
diff --git a/bun.Dockerfile b/bun.Dockerfile
index 91f4cde21a..8d54600ea7 100644
--- a/bun.Dockerfile
+++ b/bun.Dockerfile
@@ -4,7 +4,7 @@ COPY . .
RUN bun build ./src/server-node.js --target node --outdir ./dist --entry-naming bun.mjs --format esm
RUN export BLOCKLIST_DOWNLOAD_ONLY=true && node ./dist/bun.mjs
-FROM oven/bun AS runner
+FROM oven/bun:alpine AS runner
# env vals persist even at run-time: archive.is/QpXp2
# and overrides fly.toml env values
# get working dir in order
diff --git a/deno.Dockerfile b/deno.Dockerfile
index a7a7d65e6c..d356478342 100644
--- a/deno.Dockerfile
+++ b/deno.Dockerfile
@@ -1,6 +1,6 @@
# Based on github.com/denoland/deno_docker/blob/main/alpine.dockerfile
-ARG DENO_VERSION=1.29.2
+ARG DENO_VERSION=1.44.4
ARG BIN_IMAGE=denoland/deno:bin-${DENO_VERSION}
FROM ${BIN_IMAGE} AS bin
diff --git a/deno.json b/deno.json
index 37dbb01bac..64670e2022 100644
--- a/deno.json
+++ b/deno.json
@@ -3,7 +3,8 @@
"allowJs": true
},
"importMap": "import_map.json",
- "nodeModulesDir": true,
+ "nodeModulesDir": "auto",
+ "lock": false,
"lint": {
"files": {
"exclude": ["**/**"]
diff --git a/fly.tls.toml b/fly.tls.toml
index b2fbd69fc4..30240c3e91 100644
--- a/fly.tls.toml
+++ b/fly.tls.toml
@@ -2,7 +2,8 @@ app = ""
kill_signal = "SIGINT"
kill_timeout = "15s"
-swap_size_mb = 152
+# swap must be disabled when using "suspend"
+# swap_size_mb = 152
[build]
dockerfile = "node.Dockerfile"
@@ -20,11 +21,18 @@ swap_size_mb = 152
[experimental]
auto_rollback = true
+# community.fly.io/t/19180
+# fly.io/docs/machines/guides-examples/machine-restart-policy
+[[restart]]
+ policy = "on-failure"
+ retries = 3
+
# DNS over HTTPS (well, h2c and http1.1)
[[services]]
internal_port = 8055
protocol = "tcp"
- auto_stop_machines = true
+ # community.fly.io/t/20672
+ auto_stop_machines = "suspend"
auto_start_machines = true
[services.concurrency]
@@ -57,7 +65,7 @@ auto_rollback = true
[[services]]
internal_port = 10555
protocol = "tcp"
- auto_stop_machines = true
+ auto_stop_machines = "suspend"
auto_start_machines = true
[services.concurrency]
diff --git a/fly.toml b/fly.toml
index 40fd0455ba..c23a31e2e1 100644
--- a/fly.toml
+++ b/fly.toml
@@ -2,7 +2,9 @@ app = ""
kill_signal = "SIGINT"
kill_timeout = "15s"
-swap_size_mb = 152
+# swap cannot be used with "suspend"
+# community.fly.io/t/20672
+# swap_size_mb = 152
[build]
dockerfile = "node.Dockerfile"
@@ -16,12 +18,20 @@ swap_size_mb = 152
DENO_ENV = "production"
NODE_ENV = "production"
LOG_LEVEL = "info"
+ # in weeks
+ AUTO_RENEW_BLOCKLISTS_OLDER_THAN = "2"
+
+# community.fly.io/t/19180
+# fly.io/docs/machines/guides-examples/machine-restart-policy
+[[restart]]
+ policy = "on-failure"
+ retries = 3
# DNS over HTTPS
[[services]]
protocol = "tcp"
internal_port = 8080
- auto_stop_machines = true
+ auto_stop_machines = "suspend"
auto_start_machines = true
[[services.ports]]
@@ -33,7 +43,7 @@ swap_size_mb = 152
[services.concurrency]
type = "connections"
hard_limit = 775
- soft_limit = 700
+ soft_limit = 600
[[services.tcp_checks]]
# super aggressive interval and timeout because
@@ -50,7 +60,7 @@ swap_size_mb = 152
[[services]]
protocol = "tcp"
internal_port = 10000
- auto_stop_machines = true
+ auto_stop_machines = "suspend"
auto_start_machines = true
[[services.ports]]
@@ -62,7 +72,7 @@ swap_size_mb = 152
[services.concurrency]
type = "connections"
hard_limit = 775
- soft_limit = 700
+ soft_limit = 600
[[services.tcp_checks]]
# super aggressive interval and timeout because
diff --git a/import_map.json b/import_map.json
index ac37fef4f6..f68cf57a01 100644
--- a/import_map.json
+++ b/import_map.json
@@ -1,11 +1,11 @@
{
"imports": {
- "buffer": "https://deno.land/std@0.177.0/node/buffer.ts",
- "node:buffer": "https://deno.land/std@0.177.0/node/buffer.ts",
- "os" : "https://deno.land/std@0.177.0/node/os.ts",
- "process": "https://deno.land/std@0.177.0/node/process.ts",
+ "buffer": "node:buffer",
+ "os" : "node:os",
+ "process": "node:process",
"@serverless-dns/dns-parser": "https://github.com/serverless-dns/dns-parser/raw/v2.1.2/index.js",
- "@serverless-dns/lfu-cache": "https://github.com/serverless-dns/lfu-cache/raw/v3.4.1/lfu.js",
- "@serverless-dns/trie/": "https://github.com/serverless-dns/trie/raw/v0.0.13/src/"
+ "@serverless-dns/lfu-cache": "https://github.com/serverless-dns/lfu-cache/raw/v3.5.2/lfu.js",
+ "@serverless-dns/trie/": "https://github.com/serverless-dns/trie/raw/v0.0.17/src/",
+ "@riaskov/mmap-io": "https://github.com/ARyaskov/mmap-io/raw/v1.4.3/src/"
}
}
diff --git a/node.Dockerfile b/node.Dockerfile
index 0a499f772d..6f91660e13 100644
--- a/node.Dockerfile
+++ b/node.Dockerfile
@@ -1,27 +1,34 @@
-FROM node:20 as setup
+FROM node:22 as setup
# git is required if any of the npm packages are git[hub] packages
RUN apt-get update && apt-get install git -yq --no-install-suggests --no-install-recommends
-WORKDIR /node-dir
+WORKDIR /app
COPY . .
# get deps, build, bundle
RUN npm i
+# webpack externalizes native modules (@riaskov/mmap-io)
RUN npm run build:fly
# or RUN npx webpack --config webpack.fly.cjs
# download blocklists and bake them in the img
RUN export BLOCKLIST_DOWNLOAD_ONLY=true && node ./dist/fly.mjs
+# or RUN export BLOCKLIST_DOWNLOAD_ONLY=true && node ./src/server-node.js
# stage 2
-FROM node:alpine AS runner
+# pin to node22 for native deps (@ariaskov/mmap-io)
+FROM node:22-alpine AS runner
# env vals persist even at run-time: archive.is/QpXp2
# and overrides fly.toml env values
ENV NODE_ENV production
-ENV NODE_OPTIONS="--max-old-space-size=320 --heapsnapshot-signal=SIGUSR2"
+ENV NODE_OPTIONS="--max-old-space-size=200 --heapsnapshot-signal=SIGUSR2"
# get working dir in order
WORKDIR /app
-COPY --from=setup /node-dir/dist ./
-COPY --from=setup /node-dir/blocklists__ ./blocklists__
-COPY --from=setup /node-dir/dbip__ ./dbip__
+# external deps not bundled by webpack
+RUN npm i @riaskov/mmap-io@v1.4.3
+
+COPY --from=setup /app/dist ./
+COPY --from=setup /app/blocklists__ ./blocklists__
+COPY --from=setup /app/dbip__ ./dbip__
+
# print files in work dir, must contain blocklists
RUN ls -Fla
# run with the default entrypoint (usually, bash or sh)
diff --git a/package.json b/package.json
index c8374d148c..71229e3d9b 100644
--- a/package.json
+++ b/package.json
@@ -30,10 +30,11 @@
"node": ">=16"
},
"dependencies": {
+ "@riaskov/mmap-io": "^1.4.3",
"@serverless-dns/dns-parser": "github:serverless-dns/dns-parser#v2.1.2",
"@serverless-dns/lfu-cache": "github:serverless-dns/lfu-cache#v3.5.2",
"@serverless-dns/trie": "github:serverless-dns/trie#v0.0.17",
- "httpx-server": "^1.4.4",
+ "httpx-server": "^2.0.0",
"node-polyfill-webpack-plugin": "^2.0.1",
"proxy-protocol-js": "^4.0.5"
},
@@ -49,10 +50,12 @@
"eslint-plugin-prettier": "^4.0.0",
"husky": "^7.0.4",
"lint-staged": "^12.1.4",
+ "node-loader": "^2.0.0",
"prettier": "2.5.1",
- "webpack": "^5.65.0",
+ "webpack": "^5.92.1",
"webpack-cli": "^4.10.0",
- "wrangler": "^2.1.15"
+ "why-is-node-running": "^3.2.0",
+ "wrangler": "^3.0.0"
},
"lint-staged": {
"*.?(m|c)js": "eslint --cache --fix",
diff --git a/run b/run
index f2ea1f8c30..beec07c3c9 100755
--- a/run
+++ b/run
@@ -40,7 +40,7 @@ cleanup() {
jobs -p > jobfile
j=$(cat jobfile)
echo "kill... $j"
- kill -INT $j
+ kill -INT $j || true
# does not work... jobs -p | xargs -r kill -9
rm jobfile
}
@@ -94,21 +94,26 @@ greqs() {
if [ $runtime = "help" ] || [ $runtime = "h" ]; then
echo "note: make sure node / deno / wrangler are in path";
- echo "usage: $0 [node|deno|workers] [[p1|p2] [waitsec]]";
+ echo "usage: $0 [node|bun|deno|workers|fly] [[p1|p2|p3] [waitsec]]";
exit 0;
fi
-if [ $runtime = "deno" ] || [ $runtime = "d" ]; then
- echo "note: deno v1.17+ required";
+if [ $runtime = "bun" ] || [ $runtime = "b" ]; then
+ echo "note: bun v1+ required";
+ echo "using `which bun`";
+ start="bun run src/server-node.js";
+elif [ $runtime = "deno" ] || [ $runtime = "d" ]; then
+ echo "note: deno v2+ required";
echo "using `which deno`";
start="deno run --unstable \
+ --allow-import \
--allow-env \
--allow-net \
--allow-read \
--allow-write \
src/server-deno.ts";
elif [ $runtime = "workers" ] || [ $runtime = "w" ]; then
- echo "note: wrangler v1.16+ required";
+ echo "note: wrangler v1.40+ required";
echo "using `which wrangler`";
start="npx wrangler dev --local";
elif [ $runtime = "fastly" ] || [ $runtime = "f" ]; then
@@ -121,7 +126,7 @@ elif [ $runtime = "fly" ] || [ $runtime = "ff" ]; then
export NODE_OPTIONS="--trace-warnings --max-old-space-size=320 --heapsnapshot-signal=SIGUSR2 --heapsnapshot-near-heap-limit=2"
start="node ./dist/fly.mjs"
else
- echo "note: nodejs v19+ required";
+ echo "note: nodejs v22+ required";
echo "using `which node`";
# verbose: NODE_DEBUG=http2,http,tls,net... ref: stackoverflow.com/a/46858827
export NODE_OPTIONS="--trace-warnings --max-old-space-size=320 --heapsnapshot-signal=SIGUSR2 --heapsnapshot-near-heap-limit=2"
@@ -208,8 +213,8 @@ elif [ $profiler = "profile3" ] || [ $profiler = "p3" ]; then
echo "Specify env QDOH path"
exit 1
fi
- if [ $runtime != "node" ] && [ $runtime != "n" ]; then
- echo "Profile3 only valid on Node"
+ if [ $runtime != "node" ] && [ $runtime != "n" ] && [ $runtime != "bun" ] && [ $runtime != "b" ]; then
+ echo "Profile3 only valid on Node & Bun"
exit 1
fi
diff --git a/src/build/pre.sh b/src/build/pre.sh
index 2651b110e8..24c2a8315d 100755
--- a/src/build/pre.sh
+++ b/src/build/pre.sh
@@ -76,7 +76,7 @@ do
# TODO: check if the timestamp within the json file is more recent
# file/symlink exists? stackoverflow.com/a/44679975
if [ -f "${out}" ] || [ -L "${out}" ]; then
- echo "=x== pre.sh: no op"
+ echo "=x== pre.sh: no op ${out}"
exit 0
else
wget $wgetopts -q "${burl}/${yyyy}/${dir}/${mm}-${wk}/${codec}/${f}" -O "${out}"
diff --git a/src/commons/bufutil.js b/src/commons/bufutil.js
index 56eb33625c..e7de6eb069 100644
--- a/src/commons/bufutil.js
+++ b/src/commons/bufutil.js
@@ -52,7 +52,7 @@ export function hex(b) {
*/
export function len(b) {
if (emptyBuf(b)) return 0;
- return b.byteLength;
+ return b.byteLength || 0;
}
export function bytesToBase64Url(b) {
@@ -112,11 +112,19 @@ export function decodeFromBinaryArray(b) {
return decodeFromBinary(b, u8);
}
+/**
+ * @param {ArrayBufferLike} b
+ * @returns {boolean}
+ */
export function emptyBuf(b) {
return !b || b.byteLength <= 0;
}
-// returns underlying buffer prop when b is TypedArray or node:Buffer
+/**
+ * Returns underlying buffer prop when b is TypedArray or node:Buffer
+ * @param {Uint8Array|Buffer} b
+ * @returns {ArrayBufferLike}
+ */
export function raw(b) {
if (!b || b.buffer == null) b = ZERO;
@@ -169,11 +177,19 @@ export function bufferOf(arrayBuf) {
return Buffer.from(new Uint8Array(arrayBuf));
}
+/**
+ * @param {Buffer} b
+ * @returns {int}
+ */
export function recycleBuffer(b) {
b.fill(0);
return 0;
}
+/**
+ * @param {int} size
+ * @returns {Buffer}
+ */
export function createBuffer(size) {
return Buffer.allocUnsafe(size);
}
diff --git a/src/commons/dnsutil.js b/src/commons/dnsutil.js
index 4449b6e4ee..ca872915f8 100644
--- a/src/commons/dnsutil.js
+++ b/src/commons/dnsutil.js
@@ -7,9 +7,9 @@
*/
import * as dnslib from "@serverless-dns/dns-parser";
+import * as bufutil from "./bufutil.js";
import * as envutil from "./envutil.js";
import * as util from "./util.js";
-import * as bufutil from "./bufutil.js";
// dns packet constants (in bytes)
// tcp msgs prefixed with 2-octet headers indicating request len in bytes
@@ -61,7 +61,7 @@ export function servfailQ(q) {
try {
const p = decode(q);
return servfail(p.id, p.questions);
- } catch (e) {
+ } catch (_) {
return bufutil.ZEROAB;
}
}
@@ -220,12 +220,12 @@ export function optAnswer(a) {
return a.type.toUpperCase() === "OPT";
}
-export function decode(arrayBuffer) {
- if (!validResponseSize(arrayBuffer)) {
- throw new Error("failed decoding an invalid dns-packet");
+export function decode(arrbuf) {
+ if (!validResponseSize(arrbuf)) {
+ throw new Error("decoding oversized dns-packet: " + bufutil.len(arrbuf));
}
- const b = bufutil.bufferOf(arrayBuffer);
+ const b = bufutil.bufferOf(arrbuf);
return dnslib.decode(b);
}
@@ -377,9 +377,19 @@ export function isAnswerQuad0(packet) {
return isAnswerBlocked(packet.answers);
}
+export function ttl(packet) {
+ if (!hasAnswers(packet)) return 0;
+ return packet.answers[0].ttl || 0;
+}
+
+/**
+ * @param {any} dnsPacket
+ * @returns {string[]}
+ */
export function extractDomains(dnsPacket) {
if (!hasSingleQuestion(dnsPacket)) return [];
+ /** @type {string} */
const names = new Set();
const answers = dnsPacket.answers;
@@ -416,7 +426,7 @@ export function extractDomains(dnsPacket) {
export function getInterestingAnswerData(packet, maxlen = 80, delim = "|") {
if (!hasAnswers(packet)) {
- return !util.emptyObj(packet) ? packet.rcode || "WTF" : "WTF";
+ return !util.emptyObj(packet) ? packet.rcode || "WTF1" : "WTF2";
}
// set to true if at least one ip has been captured from ans
@@ -535,6 +545,10 @@ export function getQueryType(packet) {
return util.emptyString(qt) ? false : qt;
}
+/**
+ * @param {string?} n
+ * @returns {string}
+ */
export function normalizeName(n) {
if (util.emptyString(n)) return n;
diff --git a/src/commons/envutil.js b/src/commons/envutil.js
index b2489caf9c..1284713c29 100644
--- a/src/commons/envutil.js
+++ b/src/commons/envutil.js
@@ -8,6 +8,12 @@
// musn't import /depend on anything.
+export function isProd() {
+ if (!envManager) return false;
+
+ return envManager.determineEnvStage() === "production";
+}
+
export function onFly() {
if (!envManager) return false;
@@ -45,6 +51,11 @@ export function hasDisk() {
return onFly() || onLocal();
}
+export function useMmap() {
+ // got disk on fly and local deploys
+ return (onFly() || onLocal()) && (isNode() || isBun());
+}
+
export function hasDynamicImports() {
if (onDenoDeploy() || onCloudflare() || onFastly()) return false;
return true;
@@ -84,6 +95,9 @@ export function isDeno() {
return envManager.r() === "deno";
}
+/**
+ * in milliseconds
+ */
export function workersTimeout(missing = 0) {
if (!envManager) return missing;
return envManager.get("WORKER_TIMEOUT") || missing;
@@ -191,14 +205,14 @@ export function isCleartext() {
// sysctl get net.ipv4.tcp_syn_backlog
export function tcpBacklog() {
- if (!envManager) return 100;
+ if (!envManager) return 600; // same as fly.service soft_limit
- return envManager.get("TCP_BACKLOG") || 200;
+ return envManager.get("TCP_BACKLOG") || 600;
}
// don't forget to update the fly.toml too
export function maxconns() {
- if (!envManager) return 1000;
+ if (!envManager) return 1000; // 25% higher than fly.service hard_limit
return envManager.get("MAXCONNS") || 1000;
}
@@ -222,12 +236,10 @@ export function shutdownTimeoutMs() {
}
export function measureHeap() {
- // disable; webpack can't bundle memwatch; see: server-node.js
- return false;
if (!envManager) return false;
const reg = region();
if (
- reg === "maa" ||
+ reg === "bom" ||
reg === "sin" ||
reg === "fra" ||
reg === "ams" ||
@@ -248,6 +260,12 @@ export function blocklistDownloadOnly() {
return envManager.get("BLOCKLIST_DOWNLOAD_ONLY");
}
+export function renewBlocklistsThresholdInWeeks() {
+ if (!envManager) return false;
+
+ return envManager.get("AUTO_RENEW_BLOCKLISTS_OLDER_THAN") || -1;
+}
+
// Ports which the services are exposed on. Corresponds to fly.toml ports.
export function dohBackendPort() {
return 8080;
diff --git a/src/commons/util.js b/src/commons/util.js
index 05cbafdb23..517942b41d 100644
--- a/src/commons/util.js
+++ b/src/commons/util.js
@@ -13,7 +13,8 @@
// musn't import any non-std modules
export function fromBrowser(ua) {
- return ua && ua.startsWith("Mozilla/5.0");
+ if (emptyString(ua)) return false;
+ return ua.startsWith("Mozilla/5.0") || ua.startsWith("dohjs/");
}
export function jsonHeaders() {
@@ -83,6 +84,34 @@ export function regionFromCf(req) {
return req.cf.colo || "";
}
+/**
+ * returns true if tstamp is of form yyyy/epochMs
+ * ex: 2025/1740866164283
+ * @param {string} tstamp
+ * @returns {boolean}
+ */
+function isValidFullTimestamp(tstamp) {
+ if (typeof tstamp !== "string") return false;
+ return tstamp.indexOf("/") === 4;
+}
+
+/**
+ * from: github.com/celzero/downloads/blob/main/src/timestamp.js
+ * @param {string} tstamp is of form epochMs ("1740866164283") or yyyy/epochMs ("2025/1740866164283")
+ * @returns {int} blocklist create time (unix epoch) in millis (-1 on errors)
+ */
+export function bareTimestampFrom(tstamp) {
+ // strip out "/" if tstamp is of form yyyy/epochMs: "2025/1740866164283"
+ if (isValidFullTimestamp(tstamp)) {
+ tstamp = tstamp.split("/")[1];
+ }
+ const t = parseInt(tstamp);
+ if (isNaN(t)) {
+ return -1;
+ }
+ return t;
+}
+
/**
* @param {Request} request - Request
* @return {Object} - Headers
@@ -117,7 +146,13 @@ export function objOf(map) {
return map.entries ? Object.fromEntries(map) : {};
}
-export function timedOp(op, ms, cleanup = () => {}) {
+/**
+ * @param {(function((out, err) => void))} op
+ * @param {int} ms
+ * @param {function(any)} cleanup
+ * @returns {Promise}
+ */
+export function timedOp(op, ms, cleanup = (x) => {}) {
return new Promise((resolve, reject) => {
let timedout = false;
const tid = timeout(ms, () => {
@@ -142,6 +177,7 @@ export function timedOp(op, ms, cleanup = () => {}) {
}
});
} catch (e) {
+ clearTimeout(tid);
if (!timedout) reject(e);
}
});
@@ -149,6 +185,13 @@ export function timedOp(op, ms, cleanup = () => {}) {
// TODO: Use AbortSignal.timeout (supported on Node and Deno, too)?
// developers.cloudflare.com/workers/platform/changelog#2021-12-10
+/**
+ *
+ * @param {(...args: any[]) => Promise<*>} promisedOp
+ * @param {number} ms
+ * @param {(...args: any[]) => Promise<*>} defaultOp
+ * @returns
+ */
export function timedSafeAsyncOp(promisedOp, ms, defaultOp) {
// aggregating promises is a valid use-case for the otherwise
// "deferred promise anti-pattern". That is, using promise
@@ -179,13 +222,20 @@ export function timedSafeAsyncOp(promisedOp, ms, defaultOp) {
resolve(out);
}
})
- .catch((ignored) => {
+ .catch((_) => {
+ clearTimeout(tid);
if (!timedout) deferredOp();
// else: handled by timeout
});
});
}
+/**
+ *
+ * @param {number} ms
+ * @param {(...args: any[]) => void} fn
+ * @returns
+ */
export function timeout(ms, fn) {
if (typeof fn !== "function") return -1;
const timer = setTimeout(fn, ms);
@@ -195,12 +245,24 @@ export function timeout(ms, fn) {
export function repeat(ms, fn) {
if (typeof fn !== "function") return -1;
- setImmediate(fn);
+
+ next(fn);
+
const timer = setInterval(fn, ms);
if (typeof timer.unref === "function") timer.unref();
+
return timer;
}
+export function next(...fns) {
+ for (const fn of fns) {
+ if (typeof fn === "function") {
+ if (typeof setImmediate === "function") setImmediate(fn);
+ else timeout(0, fn);
+ }
+ }
+}
+
// min inclusive, max exclusive
export function rand(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
@@ -210,6 +272,11 @@ export function rolldice(sides = 6) {
return rand(1, sides + 1);
}
+export function yyyymm() {
+ const d = new Date();
+ return d.getUTCFullYear() + "/" + (d.getUTCMonth() + 1);
+}
+
// stackoverflow.com/a/8084248
export function uid(prefix = "") {
// ex: ".ww8ja208it"
diff --git a/src/core/cfg.js b/src/core/cfg.js
index cc24ae7e82..5d30d00221 100644
--- a/src/core/cfg.js
+++ b/src/core/cfg.js
@@ -7,8 +7,9 @@
*/
/* eslint-disabled */
// eslint, no import-assert: github.com/eslint/eslint/discussions/15305
-import u6cfg from "../u6-basicconfig.json" assert { type: 'json' };
-import u6filetag from "../u6-filetag.json" assert { type: 'json' };
+import u6cfg from "../u6-basicconfig.json" with { type: 'json' };
+import u6filetag from "../u6-filetag.json" with { type: 'json' };
+// nodejs.org/docs/latest-v22.x/api/esm.html#json-modules
export function timestamp() {
return u6cfg.timestamp;
@@ -17,7 +18,7 @@ export function timestamp() {
export function tdNodeCount() {
return u6cfg.nodecount;
}
-
+
export function tdParts() {
return u6cfg.tdparts;
}
@@ -25,7 +26,7 @@ export function tdParts() {
export function tdCodec6() {
return u6cfg.useCodec6;
}
-
+
export function orig() {
return u6cfg;
}
diff --git a/src/core/deno/blocklists.ts b/src/core/deno/blocklists.ts
index e7b5074cef..db2c7c7986 100644
--- a/src/core/deno/blocklists.ts
+++ b/src/core/deno/blocklists.ts
@@ -13,56 +13,58 @@ import { BlocklistWrapper } from "../../plugins/rethinkdns/main.js";
const blocklistsDir = "blocklists__";
const tdFile = "td.txt";
const rdFile = "rd.txt";
+const bcFile = "basicconfig.json";
+const ftFile = "filetag.json";
-export async function setup(bw: any) {
+export async function setup(bw: BlocklistWrapper) {
if (!bw || !envutil.hasDisk()) return false;
- const now = Date.now();
- const timestamp = cfg.timestamp() as string;
- const url = envutil.blocklistUrl() + timestamp + "/";
- const nodecount = cfg.tdNodeCount() as number;
- const tdparts = cfg.tdParts() as number;
- const tdcodec6 = cfg.tdCodec6() as boolean;
- const codec = tdcodec6 ? "u6" : "u8";
-
- const ok = setupLocally(bw, timestamp, codec);
+ const ok = setupLocally(bw);
if (ok) {
- console.info("bl setup locally tstamp/nc", timestamp, nodecount);
return true;
}
- console.info("dowloading bl url/codec?", url, codec);
- await bw.initBlocklistConstruction(
- /* rxid*/ "bl-download",
- now,
- url,
- nodecount,
- tdparts,
- tdcodec6
- );
+ console.info("dowloading blocklists");
+ await bw.init(/* rxid*/ "bl-download", /* wait */ true);
- save(bw, timestamp, codec);
+ return save(bw);
}
-function save(bw: BlocklistWrapper, timestamp: string, codec: string) {
+function save(bw: BlocklistWrapper) {
if (!bw.isBlocklistFilterSetup()) return false;
+ const timestamp = bw.timestamp();
+ const codec = bw.codec();
+
mkdirsIfNeeded(timestamp, codec);
- const [tdfp, rdfp] = getFilePaths(timestamp, codec);
+ const [tdfp, rdfp, bcfp, ftfp] = getFilePaths(timestamp, codec);
const td = bw.triedata();
const rd = bw.rankdata();
+ const bc = bw.basicconfig();
+ const ft = bw.filetag();
// Deno only writes uint8arrays to disk, never raw arraybuffers
Deno.writeFileSync(tdfp, new Uint8Array(td));
Deno.writeFileSync(rdfp, new Uint8Array(rd));
+ // write the basic config and file tag as json; may overwrite existing
+ Deno.writeTextFileSync(bcfp, JSON.stringify(bc));
+ Deno.writeTextFileSync(ftfp, JSON.stringify(ft));
- console.info("blocklists written to disk");
+ console.info("blocklist files written to disk", tdfp, rdfp, bcfp, ftfp);
return true;
}
-function setupLocally(bw: any, ts: string, codec: string) {
+/**
+ * Loads the blocklist files & configuration from disk, if any.
+ * TODO: return false if blocklists age > AUTO_RENEW_BLOCKLISTS_OLDER_THAN
+ */
+function setupLocally(bw: BlocklistWrapper) {
+ const ts = cfg.timestamp() as string;
+ const tdcodec6 = cfg.tdCodec6() as boolean;
+ const codec = tdcodec6 ? "u6" : "u8";
+
if (!hasBlocklistFiles(ts, codec)) return false;
const [td, rd] = getFilePaths(ts, codec);
@@ -102,18 +104,26 @@ function hasBlocklistFiles(timestamp: string, codec: string) {
const rdinfo = Deno.statSync(rd);
return tdinfo.isFile && rdinfo.isFile;
- } catch (ignored) {}
+ } catch (_) {
+ /* no-op */
+ }
return false;
}
-function getFilePaths(t: string, c: string) {
+/**
+ * Returns the file paths for the blocklists.
+ * @returns {string[]} [td, rd, bc, ft]
+ */
+function getFilePaths(t: string, c: string): string[] {
const cwd = Deno.cwd();
const td = cwd + "/" + blocklistsDir + "/" + t + "/" + c + "/" + tdFile;
const rd = cwd + "/" + blocklistsDir + "/" + t + "/" + c + "/" + rdFile;
+ const bc = cwd + "/" + c + "-" + bcFile;
+ const ft = cwd + "/" + c + "-" + ftFile;
- return [td, rd];
+ return [td, rd, bc, ft];
}
function getDirPaths(t: string, c: string) {
@@ -138,7 +148,9 @@ function mkdirsIfNeeded(timestamp: string, codec: string) {
dinfo1 = Deno.statSync(dir1);
dinfo2 = Deno.statSync(dir2);
dinfo3 = Deno.statSync(dir3);
- } catch (ignored) {}
+ } catch (_) {
+ /* no-op */
+ }
if (!dinfo1 || !dinfo1.isDirectory) {
console.info("creating dir", dir1);
diff --git a/src/core/deno/config.ts b/src/core/deno/config.ts
index e8829a9955..42ee59494e 100644
--- a/src/core/deno/config.ts
+++ b/src/core/deno/config.ts
@@ -1,21 +1,19 @@
+// deno-lint-ignore-file no-var
import * as system from "../../system.js";
import * as blocklists from "./blocklists.ts";
import * as dbip from "./dbip.ts";
import { services, stopAfter } from "../svc.js";
import Log, { LogLevels } from "../log.js";
import EnvManager from "../env.js";
-import { signal } from "https://deno.land/std@0.171.0/signal/mod.ts";
// In global scope.
declare global {
// TypeScript must know type of every var / property. Extend Window
// (globalThis) with declaration merging (archive.is/YUWh2) to define types
// Ref: www.typescriptlang.org/docs/handbook/declaration-merging.html
- interface Window {
- envManager?: EnvManager;
- log?: Log;
- env?: any;
- }
+ var envManager: EnvManager | null;
+ var log: Log | null;
+ var env: any | null;
}
((main) => {
@@ -23,25 +21,18 @@ declare global {
system.when("steady").then(up);
})();
-async function sigctrl() {
- const sigs = signal("SIGINT");
- for await (const _ of sigs) {
- stopAfter();
- }
-}
-
-async function prep() {
+function prep() {
// if this file execs... assume we're on deno.
if (!Deno) throw new Error("failed loading deno-specific config");
- const isProd = Deno.env.get("DENO_ENV") === "production";
+ const isProd = Deno.env.get("DENO_ENV_DOMAIN") === "production";
const onDenoDeploy = Deno.env.get("CLOUD_PLATFORM") === "deno-deploy";
const profiling = Deno.env.get("PROFILE_DNS_RESOLVES") === "true";
- window.envManager = new EnvManager();
+ globalThis.envManager = new EnvManager();
- window.log = new Log({
- level: window.envManager.get("LOG_LEVEL") as LogLevels,
+ globalThis.log = new Log({
+ level: globalThis.envManager.get("LOG_LEVEL") as LogLevels,
levelize: isProd || profiling, // levelize if prod or profiling
withTimestamps: !onDenoDeploy, // do not log ts on deno-deploy
});
@@ -72,7 +63,12 @@ async function up() {
} else {
console.warn("Config", "logpusher unavailable");
}
- sigctrl();
+
+ // docs.deno.com/runtime/tutorials/os_signals
+ Deno.addSignalListener("SIGINT", () => {
+ stopAfter();
+ });
+
// signal all system are-a go
system.pub("go");
}
diff --git a/src/core/dns/conns.js b/src/core/dns/conns.js
index d79b5c92d9..af022cee7b 100644
--- a/src/core/dns/conns.js
+++ b/src/core/dns/conns.js
@@ -13,20 +13,36 @@ import * as util from "../../commons/util.js";
*/
export class TcpConnPool {
+ /**
+ * @param {int} size
+ * @param {int} ttl
+ */
constructor(size, ttl) {
+ /** @type {int} */
this.size = size;
- // max sweeps per give/take
+ /**
+ * max sweeps per give/take
+ * @type {int}
+ */
this.maxsweep = Math.max((size / 4) | 0, 20);
+ /** @type {int} */
this.ttl = ttl; // ms
const quarterttl = (ttl / 4) | 0;
+ /** @type {int} */
this.keepalive = Math.min(/* 60s*/ 60000, quarterttl); // ms
+ /** @type {int} */
this.lastSweep = 0;
+ /** @type {int} */
this.sweepGapMs = Math.max(/* 10s*/ 10000, quarterttl); // ms
/** @type {Map} */
this.pool = new Map();
log.d("tcp-pool psz:", size, "msw:", this.maxsweep, "t:", ttl);
}
+ /**
+ * @param {AnySock} socket
+ * @returns {boolean}
+ */
give(socket) {
if (socket.pending) return false;
if (!socket.writable) return false;
@@ -40,6 +56,9 @@ export class TcpConnPool {
return this.checkin(socket);
}
+ /**
+ * @returns {AnySock?}
+ */
take() {
const thres = this.maxsweep / 2;
let out = null;
@@ -68,9 +87,9 @@ export class TcpConnPool {
}
/**
- * @param {import("net").Socket} sock
+ * @param {AnySock} sock
* @param {Report} report
- * @returns {import("net").Socket}
+ * @returns {AnySock}
*/
checkout(sock, report) {
log.d(report.id, "checkout, size:", this.pool.size);
@@ -88,6 +107,10 @@ export class TcpConnPool {
return sock;
}
+ /**
+ * @param {AnySock} socket
+ * @returns {boolean}
+ */
checkin(sock) {
const report = this.mkreport();
@@ -102,6 +125,10 @@ export class TcpConnPool {
return true;
}
+ /**
+ * @param {boolean} clear
+ * @returns {boolean}
+ */
sweep(clear = false) {
const sz = this.pool.size;
if (sz <= 0) return false;
@@ -122,11 +149,21 @@ export class TcpConnPool {
return sz > this.pool.size; // size decreased post-sweep?
}
+ /**
+ * @param {AnySock?} socket
+ * @returns {boolean}
+ */
ready(sock) {
- return sock.readyState === "open";
+ return sock && sock.readyState === "open";
}
+ /**
+ * @param {AnySock?} sock
+ * @param {Report} report
+ * @returns {boolean}
+ */
healthy(sock, report) {
+ if (!sock) return false;
const destroyed = !sock.writable;
const open = this.ready(sock);
const fresh = report.fresh(this.ttl);
@@ -136,10 +173,18 @@ export class TcpConnPool {
return fresh; // healthy if not expired
}
+ /**
+ * @param {AnySock} sock
+ * @param {Report} report
+ * @returns {boolean}
+ */
dead(sock, report) {
return !this.healthy(sock, report);
}
+ /**
+ * @param {AnySock?} sock
+ */
evict(sock) {
this.pool.delete(sock);
@@ -148,6 +193,7 @@ export class TcpConnPool {
} catch (ignore) {}
}
+ /** @return {Report} */
mkreport() {
return new Report(util.uid("tcp"));
}
@@ -164,24 +210,39 @@ class Report {
this.lastuse = Date.now();
}
+ /** @param {number} since */
fresh(since) {
return this.lastuse + since >= Date.now();
}
}
export class UdpConnPool {
+ /**
+ * @param {int} size
+ * @param {int} ttl
+ */
constructor(size, ttl) {
+ /** @type {int} */
this.size = size;
+ /** @type {int} */
this.maxsweep = Math.max((size / 4) | 0, 20);
+ /** @type {int} */
this.ttl = Math.max(/* 60s*/ 60000, ttl); // no more than 60s
+ /** @type {int} */
this.lastSweep = 0;
+ /** @type {int} */
this.sweepGapMs = Math.max(/* 10s*/ 10000, (ttl / 2) | 0); // ms
/** @type {Map} */
this.pool = new Map();
log.d("udp-pool psz:", size, "msw:", this.maxsweep, "t:", ttl);
}
+ /**
+ * @param {AnySock?} socket
+ * @returns {boolean}
+ */
give(socket) {
+ if (!socket) return false;
if (this.pool.has(socket)) return true;
const free = this.pool.size < this.size || this.sweep();
@@ -190,6 +251,9 @@ export class UdpConnPool {
return this.checkin(socket);
}
+ /**
+ * @returns {AnySock?}
+ */
take() {
const thres = this.maxsweep / 2;
let out = null;
@@ -217,9 +281,9 @@ export class UdpConnPool {
}
/**
- * @param {import("dgram").Socket} sock
+ * @param {AnySock} sock
* @param {Report} report
- * @returns {import("dgram").Socket}
+ * @returns {AnySock}
*/
checkout(sock, report) {
log.d(report.id, "checkout, size:", this.pool.size);
@@ -231,6 +295,10 @@ export class UdpConnPool {
return sock;
}
+ /**
+ * @param {AnySock} socket
+ * @returns {boolean}
+ */
checkin(sock) {
const report = this.mkreport();
@@ -243,6 +311,10 @@ export class UdpConnPool {
return true;
}
+ /**
+ * @param {boolean} clear
+ * @returns {boolean}
+ */
sweep(clear = false) {
const sz = this.pool.size;
if (sz <= 0) return false;
@@ -263,6 +335,10 @@ export class UdpConnPool {
return sz > this.pool.size; // size decreased post-sweep?
}
+ /**
+ * @param {Report} report
+ * @returns {boolean}
+ */
healthy(report) {
const fresh = report.fresh(this.ttl);
const id = report.id;
@@ -270,10 +346,17 @@ export class UdpConnPool {
return fresh; // healthy if not expired
}
+ /**
+ * @param {Report} report
+ * @returns {boolean}
+ */
dead(report) {
return !this.healthy(report);
}
+ /**
+ * @param {AnySock?} sock
+ */
evict(sock) {
if (!sock) return;
this.pool.delete(sock);
@@ -282,6 +365,7 @@ export class UdpConnPool {
sock.close();
}
+ /** @return {Report} */
mkreport() {
return new Report(util.uid("udp"));
}
diff --git a/src/core/dns/transact.js b/src/core/dns/transact.js
index 53debb5db9..cb525b6503 100644
--- a/src/core/dns/transact.js
+++ b/src/core/dns/transact.js
@@ -9,25 +9,44 @@ import * as bufutil from "../../commons/bufutil.js";
import * as util from "../../commons/util.js";
import * as dnsutil from "../../commons/dnsutil.js";
+/**
+ * @typedef {import("net").Socket} TcpSock
+ * @typedef {import("dgram").Socket} UdpSock
+ * @typedef {import("net").AddressInfo} AddrInfo
+ */
+
// TcpTx implements a single DNS question-answer exchange over TCP. It doesn't
// multiplex multiple DNS questions over the same socket. It doesn't take the
// ownership of the socket, but requires exclusive use of it. The socket may
// close itself on errors, however.
export class TcpTx {
+ /** @param {TcpSock} socket */
constructor(socket) {
+ /** @type {TcpSock} */
this.sock = socket;
- // only one transaction allowed
- // then done gates all other requests
- this.done = false;
+ /** @type {boolean} */
+ this.done = false || socket == null; // done gates all other requests
+ /** @type {ScratchBuffer} */
// reads from the socket is buffered into scratch
this.readBuffer = this.makeReadBuffer();
+ /** @type {function(Buffer)} */
+ this.resolve = null;
+ /** @type {function(string?)} */
+ this.reject = null;
this.log = log.withTags("TcpTx");
}
+ /** @param {TcpSock} sock */
static begin(sock) {
return new TcpTx(sock);
}
+ /**
+ * @param {string} rxid
+ * @param {Buffer} query
+ * @param {int} timeout
+ * @returns {Promise|null}
+ */
async exchange(rxid, query, timeout) {
if (this.done) {
this.log.w(rxid, "no exchange, tx is done");
@@ -44,7 +63,7 @@ export class TcpTx {
this.onTimeout(rxid);
};
const onError = (err) => {
- this.onError(rxid);
+ this.onError(rxid, err);
};
try {
@@ -68,16 +87,22 @@ export class TcpTx {
}
}
- // TODO: Same code as in server.js, merge them
+ /**
+ * @param {string} rxid
+ * @param {Buffer} chunk
+ * @returns
+ */
onData(rxid, chunk) {
+ const cl = bufutil.len(chunk);
+
+ // TODO: Same code as in server.js, merge them
if (this.done) {
- this.log.w(rxid, "on reads, tx is closed for business");
+ this.log.w(rxid, "on reads, tx closed; discard", cl);
return chunk;
}
const sb = this.readBuffer;
- const cl = chunk.byteLength;
if (cl <= 0) return;
// read header first which contains length(dns-query)
@@ -130,22 +155,23 @@ export class TcpTx {
} // continue reading from socket
}
- onClose(err) {
+ onClose(rxid, err) {
if (this.done) return; // no-op
- return err ? this.no("error") : this.no("close");
+ return err ? this.no(err.message) : this.no("close");
}
- onError(err) {
+ onError(rxid, err) {
if (this.done) return; // no-op
this.log.e(rxid, "udp err", err.message);
this.no(err.message);
}
- onTimeout() {
+ onTimeout(rxid) {
if (this.done) return; // no-op
this.no("timeout");
}
+ /** @returns {Promise} */
promisedRead() {
const that = this;
return new Promise((resolve, reject) => {
@@ -154,44 +180,62 @@ export class TcpTx {
});
}
+ /**
+ * @param {string} rxid
+ * @param {Buffer} query
+ */
write(rxid, query) {
+ const qlen = bufutil.len(query);
if (this.done) {
- this.log.w(rxid, "no writes, tx is done working");
+ this.log.w(rxid, "no writes, tx is done; discard", qlen);
return query;
}
const header = bufutil.createBuffer(dnsutil.dnsHeaderSize);
+ const hlen = bufutil.len(header);
bufutil.recycleBuffer(header);
- header.writeUInt16BE(query.byteLength);
+ header.writeUInt16BE(qlen);
this.sock.write(header, () => {
- this.log.d(rxid, "len(header):", header.byteLength);
+ this.log.d(rxid, "tcp write hdr:", hlen);
});
this.sock.write(query, () => {
- this.log.d(rxid, "len(query):", query.byteLength);
+ this.log.d(rxid, "tcp write q:", qlen);
});
}
+ /**
+ * @param {Buffer} val
+ */
yes(val) {
this.done = true;
this.resolve(val);
}
+ /**
+ * @param {string?|Error} reason
+ */
no(reason) {
this.done = true;
this.reject(reason);
}
+ /** @returns {ScratchBuffer} */
makeReadBuffer() {
- const qlenBuf = bufutil.createBuffer(dnsutil.dnsHeaderSize);
- const qlenBufOffset = bufutil.recycleBuffer(qlenBuf);
-
- return {
- qlenBuf: qlenBuf,
- qlenBufOffset: qlenBufOffset,
- qBuf: null,
- qBufOffset: 0,
- };
+ return new ScratchBuffer();
+ }
+}
+
+class ScratchBuffer {
+ constructor() {
+ /** @type {Buffer} */
+ this.qlenBuf = bufutil.createBuffer(dnsutil.dnsHeaderSize);
+ /** @type {int} */
+ this.qlenBufOffset = bufutil.recycleBuffer(this.qlenBuf);
+ /** @type {Buffer} */
+ this.qBuf = null;
+ /** @type {int} */
+ this.qBufOffset = 0;
}
}
@@ -200,19 +244,32 @@ export class TcpTx {
// ownership of the socket, but requires exclusive access to it. The socket
// may close itself on errors, however.
export class UdpTx {
+ /** @param {UdpSock} socket */
constructor(socket) {
+ /** @type {UdpSock} */
this.sock = socket;
- // only one transaction allowed
- this.done = false;
- // ticks socket io timeout
- this.timeoutTimerId = null;
+ /** @type {boolean} */
+ this.done = false || socket == null; // only one transaction allowed
+ /** @type {NodeJS.Timeout|-1} */
+ this.timeoutTimerId = null; // ticks socket io timeout
+ /** @type {function(Buffer)} */
+ this.resolve = null;
+ /** @type {function(string)} */
+ this.reject = null;
this.log = log.withTags("UdpTx");
}
+ /** @param {UdpSock} sock */
static begin(sock) {
return new UdpTx(sock);
}
+ /**
+ * @param {string} rxid
+ * @param {Buffer} query
+ * @param {int} timeout
+ * @returns {Promise|null}
+ */
async exchange(rxid, query, timeout) {
if (this.done) {
this.log.w(rxid, "no exchange, tx is done");
@@ -246,15 +303,26 @@ export class UdpTx {
}
}
+ /**
+ * @param {string} rxid
+ * @param {Buffer} query
+ * @returns
+ */
write(rxid, query) {
if (this.done) return; // discard
- this.log.d(rxid, "udp write");
+ this.log.d(rxid, "udp write", bufutil.len(query));
this.sock.send(query); // err-on-write handled by onError
}
+ /**
+ * @param {string} rxid
+ * @param {Buffer} b
+ * @param {AddrInfo} addrinfo
+ * @returns
+ */
onMessage(rxid, b, addrinfo) {
if (this.done) return; // discard
- this.log.d(rxid, "udp read");
+ this.log.d(rxid, "udp read", bufutil.len(b));
this.yes(b);
}
@@ -270,6 +338,10 @@ export class UdpTx {
return err ? this.no("error") : this.no("close");
}
+ /**
+ * @param {int} timeout
+ * @returns {Promise}
+ */
promisedRead(timeout = 0) {
const that = this;
if (timeout > 0) {
@@ -283,6 +355,7 @@ export class UdpTx {
});
}
+ /** @param {Buffer} val */
yes(val) {
if (this.done) return;
@@ -291,6 +364,7 @@ export class UdpTx {
this.resolve(val);
}
+ /** @param {string|Error} reason */
no(reason) {
if (this.done) return;
diff --git a/src/core/doh.js b/src/core/doh.js
index 619815c3af..f3ab3481d7 100644
--- a/src/core/doh.js
+++ b/src/core/doh.js
@@ -6,11 +6,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import RethinkPlugin from "./plugin.js";
-import * as pres from "../plugins/plugin-response.js";
-import * as util from "../commons/util.js";
import * as dnsutil from "../commons/dnsutil.js";
+import * as util from "../commons/util.js";
+import * as pres from "../plugins/plugin-response.js";
import IOState from "./io-state.js";
+import RethinkPlugin from "./plugin.js";
// TODO: define FetchEventLike
/**
@@ -45,9 +45,9 @@ async function proxyRequest(event) {
}
await util.timedSafeAsyncOp(
- /* op*/ async () => plugin.execute(),
+ /* op*/ () => plugin.execute(),
/* waitMs*/ dnsutil.requestTimeout(),
- /* onTimeout*/ async () => errorResponse(io)
+ /* onTimeout*/ () => Promise.resolve(errorResponse(io))
);
} catch (err) {
log.e("doh", "proxy-request error", err.stack);
@@ -61,11 +61,20 @@ function optionsRequest(request) {
return request.method === "OPTIONS";
}
+/**
+ * @param {IOState} io
+ * @param {Error} err
+ */
function errorResponse(io, err = null) {
const eres = pres.errResponse("doh.js", err);
io.dnsExceptionResponse(eres);
}
+/**
+ * @param {IOState} io
+ * @param {string} ua
+ * @returns {Response}
+ */
function withCors(io, ua) {
if (util.fromBrowser(ua)) io.setCorsHeadersIfNeeded();
return io.httpResponse;
diff --git a/src/core/env.js b/src/core/env.js
index b2823ba928..b6b48f807c 100644
--- a/src/core/env.js
+++ b/src/core/env.js
@@ -31,8 +31,9 @@ const defaults = new Map(
type: "string",
default: "development",
},
- // the env stage deno is running in
- DENO_ENV: {
+ // the env stage deno is running in; "deno_env" seems to name-conflict
+ // github.com/serverless-dns/serverless-dns/issues/185
+ DENO_ENV_DOMAIN: {
type: "string",
default: "development",
},
@@ -46,6 +47,11 @@ const defaults = new Map(
type: "string",
default: "development",
},
+ // the env stage bun is running in
+ BUN_ENV: {
+ type: "string",
+ default: "development",
+ },
// the cloud-platform code is deployed on (cloudflare, fly, deno-deploy, fastly)
CLOUD_PLATFORM: {
type: "string",
@@ -145,6 +151,11 @@ const defaults = new Map(
type: "boolean",
default: false,
},
+ // auto renew blocklists if they are older than these many weeks
+ AUTO_RENEW_BLOCKLISTS_OLDER_THAN: {
+ type: "number",
+ default: 42, // in weeks; negative or 0 means, never auto-renew
+ },
// courtesy db-ip.com/db/download/ip-to-country-lite
GEOIP_URL: {
type: "string",
@@ -292,7 +303,7 @@ export default class EnvManager {
if (this.runtime === "node") return this.get("NODE_ENV");
if (this.runtime === "bun") return this.get("BUN_ENV");
if (this.runtime === "worker") return this.get("WORKER_ENV");
- if (this.runtime === "deno") return this.get("DENO_ENV");
+ if (this.runtime === "deno") return this.get("DENO_ENV_DOMAIN");
if (this.runtime === "fastly") return this.get("FASTLY_ENV");
return null;
}
diff --git a/src/core/io-state.js b/src/core/io-state.js
index 1c56ff5494..bd33e9f7a2 100644
--- a/src/core/io-state.js
+++ b/src/core/io-state.js
@@ -8,22 +8,36 @@
import * as bufutil from "../commons/bufutil.js";
import * as dnsutil from "../commons/dnsutil.js";
+import * as envutil from "../commons/envutil.js";
import * as util from "../commons/util.js";
export default class IOState {
constructor() {
+ /** @type {string} */
this.flag = "";
+ /** @type {any} */
this.decodedDnsPacket = this.emptyDecodedDnsPacket();
- /** @type {Response} */
- this.httpResponse = undefined;
+ /** @type {Response?} */
+ this.httpResponse = null;
+ /** @type {boolean} */
+ this.isProd = envutil.isProd();
+ /** @type {boolean} */
this.isException = false;
- this.exceptionStack = undefined;
+ /** @type {string} */
+ this.exceptionStack = null;
+ /** @type {string} */
this.exceptionFrom = "";
+ /** @type {boolean} */
this.isDnsBlock = false;
+ /** @type {boolean} */
this.alwaysGatewayAnswer = false;
+ /** @type {string} */
this.gwip4 = "";
+ /** @type {string} */
this.gwip6 = "";
+ /** @type {string} */
this.region = "";
+ /** @type {boolean} */
this.stopProcessing = false;
this.log = log.withTags("IOState");
}
@@ -80,11 +94,13 @@ export default class IOState {
exceptionFrom: this.exceptionFrom,
exceptionStack: this.exceptionStack,
};
+ this.decodedDnsPacket = dnsutil.decode(servfail);
+ this.logDnsPkt();
this.httpResponse = new Response(servfail, {
headers: util.concatHeaders(
this.headers(servfail),
- this.additionalHeader(JSON.stringify(ex))
+ this.debugHeaders(JSON.stringify(ex))
),
status: servfail ? 200 : 408, // rfc8484 section-4.2.1
});
@@ -123,11 +139,24 @@ export default class IOState {
this.decodedDnsPacket = dnsPacket || dnsutil.decode(arrayBuffer);
}
+ this.logDnsPkt();
this.httpResponse = new Response(arrayBuffer, {
headers: this.headers(arrayBuffer),
});
}
+ logDnsPkt() {
+ if (this.isProd) return;
+ this.log.d(
+ "domains",
+ dnsutil.extractDomains(this.decodedDnsPacket),
+ dnsutil.getQueryType(this.decodedDnsPacket) || "",
+ "data",
+ dnsutil.getInterestingAnswerData(this.decodedDnsPacket),
+ dnsutil.ttl(this.decodedDnsPacket)
+ );
+ }
+
dnsBlockResponse(blockflag) {
this.initDecodedDnsPacketIfNeeded();
this.stopProcessing = true;
@@ -148,7 +177,7 @@ export default class IOState {
this.httpResponse = new Response(null, {
headers: util.concatHeaders(
this.headers(),
- this.additionalHeader(JSON.stringify(this.exceptionStack))
+ this.debugHeaders(JSON.stringify(this.exceptionStack))
),
status: 503,
});
@@ -174,7 +203,7 @@ export default class IOState {
this.httpResponse = new Response(null, {
headers: util.concatHeaders(
this.headers(),
- this.additionalHeader(JSON.stringify(this.exceptionStack))
+ this.debugHeaders(JSON.stringify(this.exceptionStack))
),
status: 503,
});
@@ -182,8 +211,11 @@ export default class IOState {
}
headers(b = null) {
- const xNileFlags = this.isDnsBlock ? { "x-nile-flags": this.flag } : null;
- const xNileFlagsOk = !xNileFlags ? { "x-nile-flags-dn": this.flag } : null;
+ const hasBlockFlag = !util.emptyString(this.flag);
+ const isBlocked = hasBlockFlag && this.isDnsBlock;
+ const couldBlock = hasBlockFlag && !this.isDnsBlock;
+ const xNileFlags = isBlocked ? { "x-nile-flags": this.flag } : null;
+ const xNileFlagsOk = couldBlock ? { "x-nile-flags-dn": this.flag } : null;
const xNileRegion = !util.emptyString(this.region)
? { "x-nile-region": this.region }
: null;
@@ -191,13 +223,15 @@ export default class IOState {
return util.concatHeaders(
util.dnsHeaders(),
util.contentLengthHeader(b),
+ this.cacheHeaders(),
xNileRegion,
xNileFlags,
xNileFlagsOk
);
}
- additionalHeader(json) {
+ debugHeaders(json) {
+ if (this.isProd) return null;
if (!json) return null;
return {
@@ -215,6 +249,16 @@ export default class IOState {
}
}
+ // set cache from ttl in decoded-dns-packet
+ cacheHeaders() {
+ const ttl = dnsutil.ttl(this.decodedDnsPacket);
+ if (ttl <= 0) return null;
+
+ return {
+ "cache-control": "public, max-age=" + ttl,
+ };
+ }
+
assignBlockResponse() {
let done = this.initFlagsAndAnswers();
done = done && this.addData();
diff --git a/src/core/linux/swap.js b/src/core/linux/swap.js
deleted file mode 100644
index 724cfa0e12..0000000000
--- a/src/core/linux/swap.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (c) 2021 RethinkDNS and its authors.
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-import { spawnSync } from "node:child_process";
-
-const swapfile = "swap__";
-const swapsize = "152M";
-
-// linuxize.com/post/create-a-linux-swap-file
-export function mkswap() {
- return (
- !hasanyswap() &&
- sh("fallocate", ["-l", swapsize, swapfile]) &&
- sh("chmod", ["600", swapfile]) &&
- sh("mkswap", [swapfile]) &&
- sh("swapon", [swapfile]) &&
- sh("sysctl", ["vm.swappiness=20"])
- );
-}
-
-export function rmswap() {
- return hasswap() && sh("swapoff", ["-v", swapfile]) && sh("rm", [swapfile]);
-}
-
-function hasanyswap() {
- // cat /proc/swaps
- // Filename Type Size Used Priority
- // /swap__ file 155644 99968 -2
- const pswaps = shout("cat", ["/proc/swaps"]);
- const lines = pswaps && pswaps.split("\n");
- return lines && lines.length > 1;
-}
-
-// stackoverflow.com/a/53222213
-function hasswap() {
- return sh("test", ["-e", swapfile]);
-}
-
-function shout(cmd, args) {
- return shx(cmd, args, true);
-}
-
-function sh(cmd, args) {
- return shx(cmd, args) === 0;
-}
-
-function shx(cmd, args, out = false) {
- if (!cmd) return false;
- args = args || [];
- const opts = {
- cwd: "/",
- uid: 0,
- shell: true,
- encoding: "utf8",
- };
- const proc = spawnSync(cmd, args, opts);
- if (proc.error) log.i(cmd, args, opts, "error", proc.error);
- if (proc.stderr) log.e(cmd, args, opts, proc.stderr);
- if (proc.stdout) log.l(proc.stdout);
- return !out ? proc.status : proc.stdout;
-}
diff --git a/src/core/log.js b/src/core/log.js
index 7a22b580a8..a1daa79b3b 100644
--- a/src/core/log.js
+++ b/src/core/log.js
@@ -9,7 +9,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import { uid, stub } from "../commons/util.js";
+import { stub } from "../commons/util.js";
/**
* @typedef {'error'|'logpush'|'warn'|'info'|'timer'|'debug'} LogLevels
@@ -82,9 +82,6 @@ export default class Log {
_resetLevel() {
this.d = stub();
this.debug = stub();
- this.lapTime = stub();
- this.startTime = stub();
- this.endTime = stub();
this.i = stub();
this.info = stub();
this.w = stub();
@@ -94,42 +91,29 @@ export default class Log {
}
withTags(...tags) {
- const that = this;
return {
- lapTime: (n, ...r) => {
- return that.lapTime(n, ...tags, ...r);
- },
- startTime: (n, ...r) => {
- const tid = that.startTime(n);
- that.d(that.now() + " T", ...tags, "create", tid, ...r);
- return tid;
- },
- endTime: (n, ...r) => {
- that.d(that.now() + " T", ...tags, "end", n, ...r);
- return that.endTime(n);
- },
d: (...args) => {
- that.d(that.now() + " D", ...tags, ...args);
+ this.d(this.now() + " D", ...tags, ...args);
},
i: (...args) => {
- that.i(that.now() + " I", ...tags, ...args);
+ this.i(this.now() + " I", ...tags, ...args);
},
w: (...args) => {
- that.w(that.now() + " W", ...tags, ...args);
+ this.w(this.now() + " W", ...tags, ...args);
},
e: (...args) => {
- that.e(that.now() + " E", ...tags, ...args);
+ this.e(this.now() + " E", ...tags, ...args);
},
q: (...args) => {
- that.l(that.now() + " Q", ...tags, ...args);
+ this.l(this.now() + " Q", ...tags, ...args);
},
qStart: (...args) => {
- that.l(that.now() + " Q", ...tags, that.border());
- that.l(that.now() + " Q", ...tags, ...args);
+ this.l(this.now() + " Q", ...tags, this.border());
+ this.l(this.now() + " Q", ...tags, ...args);
},
qEnd: (...args) => {
- that.l(that.now() + " Q", ...tags, ...args);
- that.l(that.now() + " Q", ...tags, that.border());
+ this.l(this.now() + " Q", ...tags, ...args);
+ this.l(this.now() + " Q", ...tags, this.border());
},
tag: (t) => {
tags.push(t);
@@ -163,13 +147,7 @@ export default class Log {
this.d = console.debug;
this.debug = console.debug;
case "timer":
- this.lapTime = console.timeLog || stub(); // Stubbing required for Fastly as they do not currently support this method.
- this.startTime = function (name) {
- name = uid(name);
- if (console.time) console.time(name);
- return name;
- };
- this.endTime = console.timeEnd || stub(); // Stubbing required for Fastly as they do not currently support this method.
+ // deprecated; fallthrough
case "info":
this.i = console.info;
this.info = console.info;
diff --git a/src/core/node/blocklists.js b/src/core/node/blocklists.js
index 111914f53d..1bd3cbe2c8 100644
--- a/src/core/node/blocklists.js
+++ b/src/core/node/blocklists.js
@@ -5,74 +5,128 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
+
import * as fs from "node:fs";
import * as path from "node:path";
import * as bufutil from "../../commons/bufutil.js";
import * as envutil from "../../commons/envutil.js";
import * as cfg from "../../core/cfg.js";
+import { BlocklistWrapper } from "../../plugins/rethinkdns/main.js";
+// import mmap from "@riaskov/mmap-io";
const blocklistsDir = "./blocklists__";
const tdFile = "td.txt";
const rdFile = "rd.txt";
+const bcFile = "basicconfig.json";
+const ftFile = "filetag.json";
+/**
+ *
+ * @param {BlocklistWrapper} bw
+ * @returns
+ */
export async function setup(bw) {
if (!bw || !envutil.hasDisk()) return false;
- const now = Date.now();
- // timestamp is of form yyyy/epochMs
- const timestamp = cfg.timestamp();
- const url = envutil.blocklistUrl() + timestamp + "/";
- const nodecount = cfg.tdNodeCount();
- const tdparts = cfg.tdParts();
- const tdcodec6 = cfg.tdCodec6();
- const codec = tdcodec6 ? "u6" : "u8";
-
- const ok = setupLocally(bw, timestamp, codec);
+ const ok = await setupLocally(bw);
if (ok) {
- log.i("bl setup locally tstamp/nc", timestamp, nodecount);
return true;
}
- log.i("dowloading bl u/u6?/nc/parts", url, tdcodec6, nodecount, tdparts);
- await bw.initBlocklistConstruction(
- /* rxid*/ "bl-download",
- now,
- url,
- nodecount,
- tdparts,
- tdcodec6
- );
+ log.i("dowloading blocklists");
+ await bw.init(/* rxid */ "bl-download", /* wait */ true);
- return save(bw, timestamp, codec);
+ return save(bw);
}
-function save(bw, timestamp, codec) {
+/**
+ * @param {BlocklistWrapper} bw
+ * @returns {boolean}
+ */
+function save(bw) {
if (!bw.isBlocklistFilterSetup()) return false;
+ const timestamp = bw.timestamp();
+ const codec = bw.codec();
mkdirsIfNeeded(timestamp, codec);
- const [tdfp, rdfp] = getFilePaths(timestamp, codec);
+ const [tdfp, rdfp, bcfp, ftfp] = getFilePaths(timestamp, codec);
const td = bw.triedata();
const rd = bw.rankdata();
+ const filetag = bw.filetag();
+ const basicconfig = bw.basicconfig();
// write out array-buffers to disk
fs.writeFileSync(tdfp, bufutil.bufferOf(td));
fs.writeFileSync(rdfp, bufutil.bufferOf(rd));
+ // write out json objects to disk; may overwrite existing files
+ fs.writeFileSync(ftfp, JSON.stringify(filetag));
+ fs.writeFileSync(bcfp, JSON.stringify(basicconfig));
- log.i("blocklists written to disk");
+ log.i("blocklist files written to disk", tdfp, rdfp, bcfp, ftfp);
return true;
}
-function setupLocally(bw, timestamp, codec) {
+/**
+ * fmmap mmaps file at fp for random reads, returns a Buffer backed by the file.
+ * @param {string} fp
+ * @returns {Buffer?}
+ */
+async function fmmap(fp) {
+ const dynimports = envutil.hasDynamicImports();
+ const isNode = envutil.isNode();
+ const isBun = envutil.isBun();
+ const isDeno = envutil.isDeno();
+
+ if (dynimports && isNode) {
+ try {
+ const mmap = (await import("@riaskov/mmap-io")).default;
+ const fd = fs.openSync(fp, "r+");
+ const fsize = fs.fstatSync(fd).size;
+ const rxprot = mmap.PROT_READ; // protection
+ const mpriv = mmap.MAP_SHARED; // privacy
+ const madv = mmap.MADV_RANDOM; // madvise
+ const offset = 0;
+ log.i("mmap f:", fp, "size:", fsize, "\nNOTE: md5 checks will fail");
+ return mmap.map(fsize, rxprot, mpriv, fd, offset, madv);
+ } catch (ex) {
+ log.e("mmap f:", fp, "import failed", ex);
+ return null;
+ }
+ } else if (isBun) {
+ log.i("mmap f:", fp, "on bun");
+ return Bun.mmap(fp);
+ } else if (isDeno) {
+ log.i("mmap f:", fp, "unavailable on deno");
+ }
+ return null;
+}
+
+/**
+ * setupLocally loads blocklist files and configurations from disk.
+ * TODO: return false if blocklists age > AUTO_RENEW_BLOCKLISTS_OLDER_THAN
+ * @param {BlocklistWrapper} bw
+ * @returns
+ */
+async function setupLocally(bw) {
+ // timestamp is of form yyyy/epochMs
+ const timestamp = cfg.timestamp();
+ const tdcodec6 = cfg.tdCodec6();
+ const codec = tdcodec6 ? "u6" : "u8";
+ const useMmap = envutil.useMmap();
+
const ok = hasBlocklistFiles(timestamp, codec);
log.i(timestamp, codec, "has bl files?", ok);
if (!ok) return false;
const [td, rd] = getFilePaths(timestamp, codec);
- log.i("on-disk codec/td/rd", codec, td, rd);
+ log.i("on-disk ts/codec/td/rd", timestamp, codec, td, rd, "mmap?", useMmap);
- const tdbuf = fs.readFileSync(td);
+ let tdbuf = useMmap ? await fmmap(td) : null;
+ if (bufutil.emptyBuf(tdbuf)) {
+ tdbuf = fs.readFileSync(td);
+ }
const rdbuf = fs.readFileSync(rd);
// TODO: file integrity checks
@@ -98,11 +152,24 @@ function hasBlocklistFiles(timestamp, codec) {
return fs.existsSync(td) && fs.existsSync(rd);
}
+/**
+ *
+ * @param {string} t
+ * @param {string} codec
+ * @returns {string[]} array of file paths [td, rd, bc, ft]
+ */
function getFilePaths(t, codec) {
const td = blocklistsDir + "/" + t + "/" + codec + "/" + tdFile;
const rd = blocklistsDir + "/" + t + "/" + codec + "/" + rdFile;
-
- return [path.normalize(td), path.normalize(rd)];
+ const bc = codec + "-" + bcFile;
+ const ft = codec + "-" + ftFile;
+
+ return [
+ path.normalize(td),
+ path.normalize(rd),
+ path.normalize(bc),
+ path.normalize(ft),
+ ];
}
function getDirPaths(t, codec) {
diff --git a/src/core/node/config.js b/src/core/node/config.js
index 59efdfa4fa..89520bd851 100644
--- a/src/core/node/config.js
+++ b/src/core/node/config.js
@@ -13,15 +13,14 @@
*/
import { atob, btoa } from "node:buffer";
import process from "node:process";
-import * as util from "./util.js";
-import * as blocklists from "./blocklists.js";
-import * as dbip from "./dbip.js";
-import Log from "../log.js";
+import * as dnst from "../../core/node/dns-transport.js";
import * as system from "../../system.js";
-import { services, stopAfter } from "../svc.js";
import EnvManager from "../env.js";
-import * as swap from "../linux/swap.js";
-import * as dnst from "../../core/node/dns-transport.js";
+import Log from "../log.js";
+import { services, stopAfter } from "../svc.js";
+import * as blocklists from "./blocklists.js";
+import * as dbip from "./dbip.js";
+import * as util from "./util.js";
// some of the cjs node globals aren't available in esm
// nodejs.org/docs/latest/api/globals.html
@@ -31,7 +30,7 @@ import * as dnst from "../../core/node/dns-transport.js";
// globalThis.__filename = fileURLToPath(import.meta.url);
// globalThis.__dirname = path.dirname(__filename);
-(async (main) => {
+((main) => {
system.when("prepare").then(prep);
system.when("steady").then(up);
})();
@@ -102,7 +101,7 @@ async function prep() {
log.i("dev (local) tls setup from tls_key_path", l1, l2);
} catch (ex) {
// this can happen when running server in BLOCKLIST_DOWNLOAD_ONLY mode
- log.w("Skipping TLS: test TLS crt/key missing; enable TLS offload");
+ log.w("Skipping TLS: test TLS crt/key missing; enable TLS offload", ex);
tlsoffload = true;
}
}
@@ -120,18 +119,10 @@ async function prep() {
// TODO: move dns* related settings to env
// flydns is always ipv6 (fdaa::53)
const plainOldDnsIp = onFly ? "fdaa::3" : "1.1.1.2";
- let dns53 = null;
- /** swap space and recursive resolver on Fly */
- if (onFly || true) {
- const ok = swap.mkswap();
- log.i("mkswap done?", ok);
- dns53 = dnst.makeTransport(plainOldDnsIp);
- log.i("imported udp/tcp dns transport", plainOldDnsIp);
- } else {
- log.i("no swap required");
- }
+ const dns53 = dnst.makeTransport(plainOldDnsIp);
+ log.i("imported udp/tcp dns transport", plainOldDnsIp);
- /** signal ready */
+ // signal ready
system.pub("ready", [dns53]);
}
@@ -165,6 +156,8 @@ async function up() {
process.on("SIGINT", (sig) => stopAfter());
+ process.on("warning", (e) => console.warn(e.stack));
+
// signal all system are-a go
system.pub("go");
}
diff --git a/src/core/node/dns-transport.js b/src/core/node/dns-transport.js
index 12ff0067ae..490c181060 100644
--- a/src/core/node/dns-transport.js
+++ b/src/core/node/dns-transport.js
@@ -11,6 +11,19 @@ import * as util from "../../commons/util.js";
import { TcpConnPool, UdpConnPool } from "../dns/conns.js";
import { TcpTx, UdpTx } from "../dns/transact.js";
+/**
+ * @typedef {import("net").Socket | import("dgram").Socket} AnySock
+ * @typedef {import("net").Socket} TcpSock
+ * @typedef {import("dgram").Socket} UdpSock
+ */
+
+/**
+ *
+ * @param {string} host
+ * @param {int} port
+ * @param {any} opts
+ * @returns {Transport}
+ */
export function makeTransport(host, port = 53, opts = {}) {
return new Transport(host, port, opts);
}
@@ -25,14 +38,21 @@ export function makeTransport(host, port = 53, opts = {}) {
export class Transport {
constructor(host, port, opts = {}) {
if (util.emptyString(host)) throw new Error("invalid host" + host);
+ /** @type {string} */
this.host = host;
+ /** @type {int} */
this.port = port || 53;
+ /** @type {int} */
this.connectTimeout = opts.connectTimeout || 3000; // 3s
+ /** @type {int} */
this.ioTimeout = opts.ioTimeout || 10000; // 10s
+ /** @type {int} */
this.ipproto = net.isIP(host); // 4, 6, or 0
const sz = opts.poolSize || 500; // conns
const ttl = opts.poolTtl || 60000; // 1m
+ /** @type {TcpConnPool} */
this.tcpconns = new TcpConnPool(sz, ttl);
+ /** @type {UdpConnPool} */
this.udpconns = new UdpConnPool(sz, ttl);
this.log = log.withTags("DnsTransport");
@@ -45,52 +65,55 @@ export class Transport {
this.log.i("transport teardown (tcp | udp) done?", r1, "|", r2);
}
+ /**
+ * @param {string} rxid
+ * @param {Buffer} q
+ * @returns {Promise|null}
+ */
async udpquery(rxid, q) {
let sock = this.udpconns.take();
this.log.d(rxid, "udp pooled?", sock !== null);
- const t = this.log.startTime("udp-query");
+ /** @type {Buffer?} */
let ans = null;
try {
sock = sock || (await this.makeConn("udp"));
- this.log.lapTime(t, rxid, "make-conn");
-
ans = await UdpTx.begin(sock).exchange(rxid, q, this.ioTimeout);
- this.log.lapTime(t, rxid, "get-ans");
-
this.parkConn(sock, "udp");
} catch (ex) {
this.closeUdp(sock);
this.log.e(rxid, ex);
}
- this.log.endTime(t);
-
return ans;
}
+ /**
+ * @param {string} rxid
+ * @param {Buffer} q
+ * @returns {Promise|null}
+ */
async tcpquery(rxid, q) {
let sock = this.tcpconns.take();
- this.log.d(rxid, "tcp pooled?", sock !== null);
+ this.log.d(rxid, "tcp pooled?", sock != null);
- const t = this.log.startTime("tcp-query");
+ /** @type {Buffer?} */
let ans = null;
try {
sock = sock || (await this.makeConn("tcp"));
- log.lapTime(t, rxid, "make-conn");
-
ans = await TcpTx.begin(sock).exchange(rxid, q, this.ioTimeout);
- log.lapTime(t, rxid, "get-ans");
-
this.parkConn(sock, "tcp");
} catch (ex) {
this.closeTcp(sock);
this.log.e(rxid, ex);
}
- this.log.endTime(t);
return ans;
}
+ /**
+ * @param {AnySock} sock
+ * @param {string} proto
+ */
parkConn(sock, proto) {
if (proto === "tcp") {
const ok = this.tcpconns.give(sock);
@@ -101,6 +124,11 @@ export class Transport {
}
}
+ /**
+ * @param {string} proto
+ * @returns {Promise}
+ * @throws {Error}
+ */
makeConn(proto) {
if (proto === "tcp") {
const tcpconnect = (cb) => {
@@ -128,17 +156,18 @@ export class Transport {
}
/**
- * @param {import("net").Socket} sock
+ * @param {TcpSock?} sock
*/
closeTcp(sock) {
+ if (!sock) return;
// the socket is not expected to have any error-listeners
// so we add one to avoid unhandled errors
sock.on("error", util.stub);
- if (sock && !sock.destroyed) sock.destroySoon();
+ if (!sock.destroyed) sock.destroySoon();
}
/**
- * @param {import("dgram").Socket} sock
+ * @param {UdpSock?} sock
*/
closeUdp(sock) {
if (!sock || sock.destroyed) return;
diff --git a/src/core/plugin.js b/src/core/plugin.js
index 12c6bdbeea..a64d05dd8e 100644
--- a/src/core/plugin.js
+++ b/src/core/plugin.js
@@ -53,7 +53,7 @@ export default class RethinkPlugin {
this.registerPlugin(
"userOp",
services.userOp,
- ["rxid", "request", "isDnsMsg"],
+ ["rxid", "request", "requestDecodedDnsPacket", "isDnsMsg"],
this.userOpCallback
);
@@ -142,10 +142,7 @@ export default class RethinkPlugin {
async execute() {
const io = this.io;
- const rxid = this.ctx.get("rxid");
-
- const t = this.log.startTime("exec-plugin-" + rxid);
-
+ // const rxid = this.ctx.get("rxid");
for (const p of this.plugin) {
if (io.stopProcessing && !p.continueOnStopProcess) {
continue;
@@ -154,19 +151,12 @@ export default class RethinkPlugin {
continue;
}
- this.log.lapTime(t, rxid, p.name, "send-io");
-
const res = await p.module.exec(makectx(this.ctx, p.pctx));
- this.log.lapTime(t, rxid, p.name, "got-res");
-
if (typeof p.callback === "function") {
await p.callback.call(this, res, io);
}
-
- this.log.lapTime(t, rxid, p.name, "post-callback");
}
- this.log.endTime(t);
}
/**
@@ -187,7 +177,7 @@ export default class RethinkPlugin {
/**
* Adds "userBlocklistInfo", "userBlocklistInfo", and "dnsResolverUrl"
* to RethinkPlugin ctx.
- * @param {RResp} response - Contains data: userBlocklistInfo / userBlockstamp
+ * @param {RResp} response
* @param {IOState} io
*/
async userOpCallback(response, io) {
diff --git a/src/core/workers/config.js b/src/core/workers/config.js
index 34c920f71d..b765a5bce0 100644
--- a/src/core/workers/config.js
+++ b/src/core/workers/config.js
@@ -5,8 +5,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import EnvManager from "../env.js";
import * as system from "../../system.js";
+import EnvManager from "../env.js";
import Log from "../log.js";
import { services } from "../svc.js";
diff --git a/src/plugins/cache-util.js b/src/plugins/cache-util.js
index 5bdc5800c7..4590eeea9b 100644
--- a/src/plugins/cache-util.js
+++ b/src/plugins/cache-util.js
@@ -5,10 +5,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import * as cfg from "../core/cfg.js";
-import * as util from "../commons/util.js";
import * as dnsutil from "../commons/dnsutil.js";
import * as envutil from "../commons/envutil.js";
+import * as util from "../commons/util.js";
import * as pres from "./plugin-response.js";
const minTtlSec = 30; // 30s
@@ -118,7 +117,7 @@ function updateTtl(packet, end) {
}
}
-function makeId(packet) {
+export function makeId(packet) {
// multiple questions are kind of an undefined behaviour
// stackoverflow.com/a/55093896
if (!dnsutil.hasSingleQuestion(packet)) return null;
@@ -164,13 +163,15 @@ export function makeHttpCacheValue(data) {
/**
* @param {any} packet
+ * @param {ts} string
* @returns {URL}
*/
-export function makeHttpCacheKey(packet) {
+export function makeHttpCacheKey(packet, ts) {
const id = makeId(packet); // ex: domain.tld:A:dnssec
if (util.emptyString(id)) return null;
+ if (util.emptyString(ts)) ts = util.yyyymm();
- return new URL(_cacheurl + cfg.timestamp() + "/" + id);
+ return new URL(_cacheurl + ts + "/" + id);
}
/**
diff --git a/src/plugins/command-control/cc.js b/src/plugins/command-control/cc.js
index cc08580deb..801a2223ad 100644
--- a/src/plugins/command-control/cc.js
+++ b/src/plugins/command-control/cc.js
@@ -5,21 +5,19 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import * as cfg from "../../core/cfg.js";
-import * as util from "../../commons/util.js";
-import * as rdnsutil from "../rdns-util.js";
+import { flagsToTags, tagsToFlags } from "@serverless-dns/trie/stamp.js";
import * as dnsutil from "../../commons/dnsutil.js";
+import * as util from "../../commons/util.js";
+import { DNSResolver } from "../dns-op/dns-op.js";
+import { LogPusher } from "../observability/log-pusher.js";
import * as pres from "../plugin-response.js";
-import { flagsToTags, tagsToFlags } from "@serverless-dns/trie/stamp.js";
-import * as token from "../users/auth-token.js";
+import * as rdnsutil from "../rdns-util.js";
import { BlocklistFilter } from "../rethinkdns/filter.js";
-import { LogPusher } from "../observability/log-pusher.js";
import { BlocklistWrapper } from "../rethinkdns/main.js";
-import { DNSResolver } from "../dns-op/dns-op.js";
+import * as token from "../users/auth-token.js";
export class CommandControl {
constructor(blocklistWrapper, resolver, logPusher) {
- this.latestTimestamp = rdnsutil.bareTimestampFrom(cfg.timestamp());
this.log = log.withTags("CommandControl");
/** @type {BlocklistWrapper} */
this.bw = blocklistWrapper;
@@ -125,9 +123,8 @@ export class CommandControl {
// blocklistFilter may not have been setup, so set it up
await this.bw.init(rxid, /* force-wait */ true);
const blf = this.bw.getBlocklistFilter();
- const isBlfSetup = rdnsutil.isBlocklistFilterSetup(blf);
-
- if (!isBlfSetup) throw new Error("no blocklist-filter");
+ if (!rdnsutil.isBlocklistFilterSetup(blf)) throw new Error("no blf");
+ const blfts = this.bw.timestamp(); // throws err if basicconfig is not set
if (command === "listtob64") {
// convert blocklists (tags) to blockstamp (b64)
@@ -140,10 +137,10 @@ export class CommandControl {
response.data.httpResponse = await domainNameToList(
rxid,
this.resolver,
+ blfts,
req,
queryString,
- blf,
- this.latestTimestamp
+ blf
);
} else if (command === "dntouint") {
// convert names to flags
@@ -177,7 +174,7 @@ export class CommandControl {
response.data.httpResponse = configRedirect(
b64UserFlag,
reqUrl.origin,
- this.latestTimestamp,
+ rdnsutil.bareTimestampFrom(blfts),
!isDnsCmd
);
} else {
@@ -282,21 +279,22 @@ async function analytics(lp, reqUrl, auth, lid) {
/**
* @param {string} rxid
* @param {DNSResolver} resolver
+ * @param {string} ts
* @param {Request} req
* @param {string} queryString
* @param {BlocklistFilter} blocklistFilter
- * @param {number} latestTimestamp
* @returns {Promise}
*/
async function domainNameToList(
rxid,
resolver,
+ ts,
req,
queryString,
- blocklistFilter,
- latestTimestamp
+ blocklistFilter
) {
const domainName = queryString.get("dn") || "";
+ const latestTimestamp = util.bareTimestampFrom(ts);
const r = {
domainName: domainName,
version: latestTimestamp,
@@ -318,6 +316,7 @@ async function domainNameToList(
const rmax = resolver.determineDohResolvers(resolver.ofMax(), forcedoh);
const res = await resolver.resolveDnsUpstream(
rxid,
+ ts,
req,
rmax,
query,
diff --git a/src/plugins/dns-op/blocker.js b/src/plugins/dns-op/blocker.js
index 2265250898..0852f74520 100644
--- a/src/plugins/dns-op/blocker.js
+++ b/src/plugins/dns-op/blocker.js
@@ -15,6 +15,12 @@ export class DnsBlocker {
this.log = log.withTags("DnsBlocker");
}
+ /**
+ * @param {string} rxid
+ * @param {pres.RespData} req
+ * @param {pres.BlockstampInfo} blockInfo
+ * @returns {pres.RespData}
+ */
blockQuestion(rxid, req, blockInfo) {
const dnsPacket = req.dnsPacket;
const stamps = req.stamps;
@@ -40,6 +46,12 @@ export class DnsBlocker {
return pres.copyOnlyBlockProperties(req, bres);
}
+ /**
+ * @param {string} rxid
+ * @param {pres.RespData} res
+ * @param {pres.BlockstampInfo} blockInfo
+ * @returns {pres.RespData}
+ */
blockAnswer(rxid, res, blockInfo) {
const dnsPacket = res.dnsPacket;
const stamps = res.stamps;
@@ -71,6 +83,12 @@ export class DnsBlocker {
return pres.copyOnlyBlockProperties(res, bres);
}
+ /**
+ * @param {string[]} names
+ * @param {pres.BlockstampInfo} blockInfo
+ * @param {pres.BStamp} blockstamps
+ * @returns {pres.RespData}
+ */
block(names, blockInfo, blockstamps) {
let r = pres.rdnsNoBlockResponse();
for (const n of names) {
diff --git a/src/plugins/dns-op/cache-resolver.js b/src/plugins/dns-op/cache-resolver.js
index 61240e1870..8e4f9a0033 100644
--- a/src/plugins/dns-op/cache-resolver.js
+++ b/src/plugins/dns-op/cache-resolver.js
@@ -6,12 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import { DnsBlocker } from "./blocker.js";
-import * as cacheutil from "../cache-util.js";
-import * as rdnsutil from "../rdns-util.js";
-import * as pres from "../plugin-response.js";
import * as dnsutil from "../../commons/dnsutil.js";
import * as util from "../../commons/util.js";
+import * as cacheutil from "../cache-util.js";
+import * as pres from "../plugin-response.js";
+import * as rdnsutil from "../rdns-util.js";
+import { DnsBlocker } from "./blocker.js";
export class DNSCacheResponder {
constructor(blocklistWrapper, cache) {
@@ -48,6 +48,12 @@ export class DNSCacheResponder {
return response;
}
+ /**
+ * @param {string} rxid
+ * @param {any} packet
+ * @param {pres.BStamp} blockInfo
+ * @returns {Promise}
+ */
async resolveFromCache(rxid, packet, blockInfo) {
const noAnswer = pres.rdnsNoBlockResponse();
// if blocklist-filter is setup, then there's no need to query http-cache
@@ -61,16 +67,19 @@ export class DNSCacheResponder {
// on Cloudflare, which not only has "free" egress, but also different
// runtime (faster hw and sw) and deployment model (v8 isolates).
const blf = this.bw.getBlocklistFilter();
- const onlyLocal =
- this.bw.disabled() || rdnsutil.isBlocklistFilterSetup(blf);
+ const hasblf = rdnsutil.isBlocklistFilterSetup(blf);
+ const onlyLocal = this.bw.disabled() || hasblf;
+ const ts = hasblf ? this.bw.timestamp(util.yyyymm()) : util.yyyymm();
- const k = cacheutil.makeHttpCacheKey(packet);
+ const k = cacheutil.makeHttpCacheKey(packet, ts);
if (!k) return noAnswer;
const cr = await this.cache.get(k, onlyLocal);
- this.log.d(rxid, onlyLocal, "cache k/m", k.href, cr && cr.metadata);
+ const hascr = !util.emptyObj(cr);
+ const hasm = hascr && cr.metadata != null;
+ this.log.d(rxid, "l/b?", onlyLocal, hasblf, "cache k/m", k.href, hasm);
- if (util.emptyObj(cr)) return noAnswer;
+ if (!hascr) return noAnswer;
// note: stamps in cr may be out-of-date; for ex, consider a
// scenario where v6.example.com AAAA to fda3:: today,
@@ -101,10 +110,16 @@ export class DNSCacheResponder {
return pres.dnsResponse(res.dnsPacket, reencoded, res.stamps);
}
+ /**
+ * @param {string} rxid
+ * @param {pres.RespData} r
+ * @param {pres.BStamp} blockInfo
+ * @returns {pres.RespData}
+ */
makeCacheResponse(rxid, r, blockInfo) {
// check incoming dns request against blocklists in cache-metadata
this.blocker.blockQuestion(rxid, /* out*/ r, blockInfo);
- this.log.d(rxid, blockInfo, "question blocked?", r.isBlocked);
+ this.log.d(rxid, blockInfo, "q block?", r.isBlocked);
if (r.isBlocked) {
return r;
}
@@ -117,7 +132,7 @@ export class DNSCacheResponder {
// check outgoing cached dns-packet against blocklists
this.blocker.blockAnswer(rxid, /* out*/ r, blockInfo);
- this.log.d(rxid, "answer block?", r.isBlocked);
+ this.log.d(rxid, "a block?", r.isBlocked);
return r;
}
diff --git a/src/plugins/dns-op/prefilter.js b/src/plugins/dns-op/prefilter.js
index 806f9c81f0..122dea2b6f 100644
--- a/src/plugins/dns-op/prefilter.js
+++ b/src/plugins/dns-op/prefilter.js
@@ -197,7 +197,8 @@ export class DNSPrefilter {
const subdomains = d.split(".");
do {
if (util.emptyArray(subdomains)) break;
- if (undelegated.has(subdomains.join("."))) {
+ const fqdn = subdomains.join(".");
+ if (undelegated.has(fqdn)) {
return block;
}
} while (subdomains.shift() != null);
diff --git a/src/plugins/dns-op/resolver.js b/src/plugins/dns-op/resolver.js
index edf04cae9a..f5ba2a72af 100644
--- a/src/plugins/dns-op/resolver.js
+++ b/src/plugins/dns-op/resolver.js
@@ -5,15 +5,16 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import { DnsBlocker } from "./blocker.js";
-import * as pres from "../plugin-response.js";
-import * as rdnsutil from "../rdns-util.js";
-import * as cacheutil from "../cache-util.js";
-import * as dnsutil from "../../commons/dnsutil.js";
import * as bufutil from "../../commons/bufutil.js";
-import * as util from "../../commons/util.js";
+import * as dnsutil from "../../commons/dnsutil.js";
import * as envutil from "../../commons/envutil.js";
+import * as util from "../../commons/util.js";
+import * as system from "../../system.js";
+import * as cacheutil from "../cache-util.js";
+import * as pres from "../plugin-response.js";
+import * as rdnsutil from "../rdns-util.js";
import { BlocklistFilter } from "../rethinkdns/filter.js";
+import { DnsBlocker } from "./blocker.js";
export default class DNSResolver {
/**
@@ -33,9 +34,11 @@ export default class DNSResolver {
this.log = log.withTags("DnsResolver");
this.measurements = [];
+ this.coalstats = { tot: 0, pub: 0, empty: 0, try: 0 };
this.profileResolve = envutil.profileDnsResolves();
// only valid on nodejs
this.forceDoh = envutil.forceDoh();
+ this.timeout = (envutil.workersTimeout() / 2) | 0;
// only valid on workers
// bg-bw-init results in higher io-wait, not lower
@@ -137,7 +140,7 @@ export default class DNSResolver {
* @param {Request} ctx.request
* @param {ArrayBuffer} ctx.requestBodyBuffer
* @param {Object} ctx.requestDecodedDnsPacket
- * @param {Object} ctx.userBlocklistInfo
+ * @param {pres.BlockstampInfo} ctx.userBlocklistInfo
* @param {String} ctx.userDnsResolverUrl
* @param {string} ctx.userBlockstamp
* @param {pres.BStamp?} ctx.domainBlockstamp
@@ -151,6 +154,7 @@ export default class DNSResolver {
const rawpacket = ctx.requestBodyBuffer;
const decodedpacket = ctx.requestDecodedDnsPacket;
const userDns = ctx.userDnsResolverUrl;
+ const forceUserDns = this.forceDoh || !util.emptyString(userDns);
const dispatcher = ctx.dispatcher;
const userBlockstamp = ctx.userBlockstamp;
// may be null or empty-obj (stamp then needs to be got from blf)
@@ -160,16 +164,17 @@ export default class DNSResolver {
let blf = this.bw.getBlocklistFilter();
const isBlfDisabled = this.bw.disabled();
let isBlfSetup = rdnsutil.isBlocklistFilterSetup(blf);
+ const ts = this.bw.timestamp(util.yyyymm());
// if both blocklist-filter (blf) and stamps are not setup, question-block
// is a no-op, while we expect answer-block to catch the block regardless.
- const q = await this.makeRdnsResponse(rxid, rawpacket, blf, stamps);
+ const q = this.makeRdnsResponse(rxid, rawpacket, blf, stamps);
this.blocker.blockQuestion(rxid, /* out*/ q, blInfo);
- this.log.d(rxid, "q block?", q.isBlocked, "blf?", isBlfSetup);
+ this.log.d(rxid, "q block?", q.isBlocked, "blf?", isBlfSetup, "ts?", ts);
if (q.isBlocked) {
- this.primeCache(rxid, q, dispatcher);
+ this.primeCache(rxid, ts, q, dispatcher);
return q;
}
@@ -190,6 +195,7 @@ export default class DNSResolver {
Promise.resolve(), // placeholder promise that never rejects
this.resolveDnsUpstream(
rxid,
+ ts,
req,
this.determineDohResolvers(alt, /* forceDoh */ true),
rawpacket,
@@ -211,8 +217,9 @@ export default class DNSResolver {
this.bw.init(rxid),
this.resolveDnsUpstream(
rxid,
+ ts,
req,
- this.determineDohResolvers(userDns),
+ this.determineDohResolvers(userDns, forceUserDns),
rawpacket,
decodedpacket
),
@@ -232,6 +239,7 @@ export default class DNSResolver {
}
// developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled#return_value
+ /** @type{Response} */
const res = promisedTasks[1].value;
if (fromMax) {
@@ -251,24 +259,24 @@ export default class DNSResolver {
if (!res.ok) {
const txt = res.text && (await res.text());
- this.log.d(rxid, "!OK", res.status, txt);
- throw new Error(txt + " http err: " + res);
+ this.log.w(rxid, "!OK", res.status, txt);
+ throw new Error(txt + " http err: " + res.status + " " + res.statusText);
}
const ans = await res.arrayBuffer();
- const r = await this.makeRdnsResponse(rxid, ans, blf, stamps);
+ const r = this.makeRdnsResponse(rxid, ans, blf, stamps);
// blockAnswer is a no-op if the ans is already quad0
// check outgoing cached dns-packet against blocklists
this.blocker.blockAnswer(rxid, /* out*/ r, blInfo);
const fromCache = cacheutil.hasCacheHeader(res.headers);
- this.log.d(rxid, "ans block?", r.isBlocked, "from cache?", fromCache);
+ this.log.d(rxid, "a block?", r.isBlocked, "c?", fromCache, "max?", fromMax);
// if res was got from caches or if res was got from max doh (ie, blf
// wasn't used to retrieve stamps), then skip hydrating the cache
if (!fromCache && !fromMax) {
- this.primeCache(rxid, r, dispatcher);
+ this.primeCache(rxid, ts, r, dispatcher);
}
return r;
}
@@ -278,9 +286,10 @@ export default class DNSResolver {
* @param {ArrayBuffer} raw
* @param {BlocklistFilter} blf
* @param {pres.BStamp?} stamps
- * @returns
+ * @returns {pres.RespData}
+ * @throws if raw is a malformed dns packet or not a dns packet.
*/
- async makeRdnsResponse(rxid, raw, blf, stamps = null) {
+ makeRdnsResponse(rxid, raw, blf, stamps = null) {
if (!raw) throw new Error(rxid + " mk-res no upstream result");
const dnsPacket = dnsutil.decode(raw);
@@ -297,24 +306,21 @@ export default class DNSResolver {
/**
* @param {string} rxid
+ * @param {string} ts
* @param {pres.RespData} r
* @param {function(function):void} dispatcher
* @returns {Promise}
*/
- async primeCache(rxid, r, dispatcher) {
+ primeCache(rxid, ts, r, dispatcher) {
const blocked = r.isBlocked;
-
- const k = cacheutil.makeHttpCacheKey(r.dnsPacket);
-
- this.log.d(rxid, "primeCache: block?", blocked, "k", k.href);
-
+ const k = cacheutil.makeHttpCacheKey(r.dnsPacket, ts);
if (!k) {
- this.log.d(rxid, "no cache-key, url/query missing?", k, r.stamps);
+ this.log.d(rxid, "primeCache: no key, url/query missing?", k, r.stamps);
return;
}
+ this.log.d(rxid, "primeCache: block?", blocked, "k", k.href);
const v = cacheutil.cacheValueOf(r);
-
this.cache.put(k, v, dispatcher);
}
@@ -327,37 +333,54 @@ export default class DNSResolver {
/**
* @param {String} rxid
+ * @param {String} ts
* @param {Request} request
- * @param {Array} resolverUrls
+ * @param {String[]} resolverUrls
* @param {ArrayBuffer} query
* @param {any} packet
* @returns {Promise}
*/
DNSResolver.prototype.resolveDnsUpstream = async function (
rxid,
+ ts,
request,
resolverUrls,
query,
packet
) {
- // Promise.any on promisedPromises[] only works if there are
- // zero awaits in this function or any of its downstream calls.
- // Otherwise, the first reject in promisedPromises[], before
- // any statement in the call-stack awaits, would throw unhandled
- // error, since the event loop would have 'ticked' and Promise.any
- // on promisedPromises[] would still not have been executed, as it
- // is the last statement of this function (which would have eaten up
- // all rejects as long as there was one resolved promise).
- const promisedPromises = [];
-
// if no doh upstreams set, resolve over plain-old dns
if (util.emptyArray(resolverUrls)) {
+ const eid = cacheutil.makeId(packet);
+ /** @type {ArrayBuffer[]?} */
+ let parcel = null;
+
+ try {
+ const g = await system.when(eid, this.timeout);
+ this.coalstats.tot += 1;
+ if (!util.emptyArray(g) && g[0] != null) {
+ const sz = bufutil.len(g[0]);
+ this.log.d(rxid, "coalesced", eid, sz, this.coalstats);
+ if (sz > 0) return Promise.resolve(new Response(g[0]));
+ }
+ this.coalstats.empty += 1;
+ this.log.e(rxid, "empty coalesced", eid, this.coalstats);
+ return Promise.resolve(util.respond503());
+ } catch (reason) {
+ // happens on timeout or if new event, eid
+ this.coalstats.try += 1;
+ this.log.d(rxid, "not coalesced", eid, reason, this.coalstats);
+ }
+
if (this.transport == null) {
this.log.e(rxid, "plain dns transport not set");
+ this.coalstats.pub += 1;
+ system.pub(eid, parcel);
return Promise.reject(new Error("plain dns transport not set"));
}
- // do not let exceptions passthrough to the caller
+
+ let promisedResponse = null;
try {
+ // do not let exceptions passthrough to the caller
const q = bufutil.bufferOf(query);
let ans = await this.transport.udpquery(rxid, q);
@@ -367,28 +390,40 @@ DNSResolver.prototype.resolveDnsUpstream = async function (
}
if (ans) {
- const r = new Response(bufutil.arrayBufferOf(ans));
- promisedPromises.push(Promise.resolve(r));
+ const ab = bufutil.arrayBufferOf(ans);
+ parcel = [ab];
+ promisedResponse = Promise.resolve(new Response(ab));
} else {
- promisedPromises.push(Promise.resolve(util.respond503()));
+ promisedResponse = Promise.resolve(util.respond503());
}
} catch (e) {
this.log.e(rxid, "err when querying plain old dns", e.stack);
- promisedPromises.push(Promise.reject(e));
+ promisedResponse = Promise.reject(e);
}
- return Promise.any(promisedPromises);
+ this.coalstats.pub += 1;
+ system.pub(eid, parcel);
+ return promisedResponse;
}
+ // Promise.any on promisedPromises[] only works if there are
+ // zero awaits in this function or any of its downstream calls.
+ // Otherwise, the first reject in promisedPromises[], before
+ // any statement in the call-stack awaits, would throw unhandled
+ // error, since the event loop would have 'ticked' and Promise.any
+ // on promisedPromises[] would still not have been executed, as it
+ // is the last statement of this function (which would have eaten up
+ // all rejects as long as there was one resolved promise).
+ const promisedPromises = [];
try {
// upstream to cache
this.log.d(rxid, "upstream cache");
- promisedPromises.push(this.resolveDnsFromCache(rxid, packet));
+ promisedPromises.push(this.resolveDnsFromCache(rxid, ts, packet));
// upstream to resolvers
for (const rurl of resolverUrls) {
if (util.emptyString(rurl)) {
- this.log.w(rxid, "missing resolver url", rurl);
+ this.log.w(rxid, "missing resolver url", rurl, "among", resolverUrls);
continue;
}
@@ -432,12 +467,19 @@ DNSResolver.prototype.resolveDnsUpstream = async function (
return Promise.any(promisedPromises);
};
-DNSResolver.prototype.resolveDnsFromCache = async function (rxid, packet) {
- const k = cacheutil.makeHttpCacheKey(packet);
+/**
+ * resolveDnsFromCache answers query requested by packet from local or remote cache.
+ * @param {string} rxid
+ * @param {string} ts
+ * @param {any} packet
+ * @returns {Promise} with the answer as buffer of the dns packet or error
+ */
+DNSResolver.prototype.resolveDnsFromCache = async function (rxid, ts, packet) {
+ const k = cacheutil.makeHttpCacheKey(packet, ts);
if (!k) throw new Error("resolver: no cache-key");
const cr = await this.cache.get(k);
- const isAns = cr && dnsutil.isAnswer(cr.dnsPacket);
+ const isAns = cr != null && dnsutil.isAnswer(cr.dnsPacket);
const hasAns = isAns && dnsutil.hasAnswers(cr.dnsPacket);
// if cr has answers, use probablistic expiry; otherwise prefer actual ttl
const fresh = isAns && cacheutil.isAnswerFresh(cr.metadata, hasAns ? 0 : 6);
diff --git a/src/plugins/plugin-response.js b/src/plugins/plugin-response.js
index c11c185fad..adebaf2cb0 100644
--- a/src/plugins/plugin-response.js
+++ b/src/plugins/plugin-response.js
@@ -9,6 +9,8 @@
import * as util from "../commons/util.js";
import * as bufutil from "../commons/bufutil.js";
+/** @typedef {import("./users/auth-token.js").Outcome} AuthOutcome */
+
export class RResp {
constructor(data = null, hasex = false, exfrom = "", exstack = "") {
/** @type {RespData?} */
@@ -30,10 +32,27 @@ export class RespData {
this.flag = flag || "";
/** @type {Object} */
this.dnsPacket = packet || null;
- /** @type {ArrayBuffer} */
+ /** @type {ArrayBuffer?} */
this.dnsBuffer = raw || null;
- /** @type {BStamp?} */
+ /** @type {BStamp|boolean} */
this.stamps = stamps || {};
+ /** @type {AuthOutcome?} */
+ this.userAuth = null;
+ /** @type {BlockstampInfo?} */
+ this.userBlocklistInfo = null;
+ /** @type {String} */
+ this.dnsResolverUrl = "";
+ /** @type {string} */
+ this.userBlocklistFlag = "";
+ }
+}
+
+export class BlockstampInfo {
+ constructor() {
+ /** @type {Uint16Array} */
+ this.userBlocklistFlagUint = null;
+ /** @type {String} - mosty 0 or 1 */
+ this.flagVersion = "0";
}
}
diff --git a/src/plugins/rdns-util.js b/src/plugins/rdns-util.js
index f9d93677c7..2c80bfe48d 100644
--- a/src/plugins/rdns-util.js
+++ b/src/plugins/rdns-util.js
@@ -5,15 +5,15 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
+import * as trie from "@serverless-dns/trie/stamp.js";
import { rbase32 } from "../commons/b32.js";
-import * as util from "../commons/util.js";
import * as bufutil from "../commons/bufutil.js";
import * as dnsutil from "../commons/dnsutil.js";
import * as envutil from "../commons/envutil.js";
+import * as util from "../commons/util.js";
+import { DnsCacheData } from "./cache-util.js";
import * as pres from "./plugin-response.js";
-import * as trie from "@serverless-dns/trie/stamp.js";
import { BlocklistFilter } from "./rethinkdns/filter.js";
-import { DnsCacheData } from "./cache-util.js";
// doh uses b64url encoded blockstamp, while dot uses lowercase b32.
const _b64delim = ":";
@@ -27,11 +27,6 @@ const emptystr = "";
// delim, version, blockstamp (flag), accesskey
const emptystamp = [emptystr, emptystr, emptystr, emptystr];
-// deprecated: all lists are trreated as wildcards
-const _wildcardUint16 = new Uint16Array([
- 64544, 18431, 8191, 65535, 64640, 1, 128, 16320,
-]);
-
// pec: parental control, rec: recommended, sec: security
const recBlockstamps = new Map();
// oisd, 1hosts:mini, cpbl:light, anudeep, yhosts, tiuxo, adguard
@@ -57,28 +52,55 @@ recBlockstamps.set("pr", "1:eMYB-ACgAQAgARAwIABhUgCA");
// pec, sec
recBlockstamps.set("ps", "1:GNwB-ACgeQKr7cg3YXoAgA==");
+/**
+ * @param {BlocklistFilter} blf
+ * @returns {boolean} true if blf is setup
+ */
export function isBlocklistFilterSetup(blf) {
return blf && !util.emptyObj(blf.ftrie);
}
+/**
+ * alias for util#bareTimestampFrom
+ * @type {string} tstamp is of form epochMs ("1740866164283") or yyyy/epochMs ("2025/1740866164283")
+ * @returns {int} blocklist create time (unix epoch) in millis (-1 on errors)
+ */
+export function bareTimestampFrom(tstamp) {
+ return util.bareTimestampFrom(tstamp);
+}
+
+/**
+ * @param {string} p
+ * @returns {boolean}
+ */
export function isStampQuery(p) {
return stampPrefix.test(p);
}
+/**
+ * @param {string} p
+ * @returns {boolean}
+ */
export function isLogQuery(p) {
return logPrefix.test(p);
}
-// dn -> domain name, ex: you.and.i.example.com
-// userBlInfo -> user-selected blocklist-stamp
-// {userBlocklistFlagUint, userServiceListUint}
-// dnBlInfo -> obj of blocklists stamps for dn and all its subdomains
-// {string(sub/domain-name) : u16(blocklist-stamp) }
-// FIXME: return block-dnspacket depending on altsvc/https/svcb or cname/a/aaaa
+/**
+ * dn -> domain name, ex: you.and.i.example.com
+ * userBlInfo -> user-selected blocklist-stamp
+ * {BlockstampInfo}
+ * dnBlInfo -> obj of blocklists stamps for dn and all its subdomains
+ * {string(sub/domain-name) : u16(blocklist-stamp) }
+ * FIXME: return block-dnspacket depending on altsvc/https/svcb or cname/a/aaaa
+ * @param {string} dn domain name
+ * @param {pres.BlockstampInfo} userBlInfo user blocklist info
+ * @param {pres.BStamp} dnBlInfo domain blockstamp map
+ */
export function doBlock(dn, userBlInfo, dnBlInfo) {
const blockSubdomains = envutil.blockSubdomains();
const version = userBlInfo.flagVersion;
const noblock = pres.rdnsNoBlockResponse();
+ const userUint = userBlInfo.userBlocklistFlagUint;
if (
util.emptyString(dn) ||
util.emptyObj(dnBlInfo) ||
@@ -89,32 +111,14 @@ export function doBlock(dn, userBlInfo, dnBlInfo) {
// treat every blocklist as a wildcard blocklist
if (blockSubdomains) {
- return applyWildcardBlocklists(
- userBlInfo.userBlocklistFlagUint,
- version,
- dnBlInfo,
- dn
- );
+ return applyWildcardBlocklists(dn, version, userUint, dnBlInfo);
}
- const dnUint = new Uint16Array(dnBlInfo[dn]);
+ const dnUint = dnBlInfo[dn];
// if the domain isn't in block-info, we're done
if (util.emptyArray(dnUint)) return noblock;
// else, determine if user selected blocklist intersect with the domain's
- const r = applyBlocklists(userBlInfo.userBlocklistFlagUint, dnUint, version);
-
- // if response is blocked, we're done
- if (r.isBlocked) return r;
- // if user-blockstamp doesn't contain any wildcard blocklists, we're done
- if (util.emptyArray(userBlInfo.userServiceListUint)) return r;
-
- // check if any subdomain is in blocklists that is also in user-blockstamp
- return applyWildcardBlocklists(
- userBlInfo.userServiceListUint,
- version,
- dnBlInfo,
- dn
- );
+ return applyBlocklists(version, userUint, dnUint);
}
/**
@@ -131,7 +135,7 @@ export function blockstampFromCache(cr) {
}
/**
- * @param {*} dnsPacket
+ * @param {any} dnsPacket
* @param {BlocklistFilter} blocklistFilter
* @returns {pres.BStamp|boolean}
*/
@@ -156,7 +160,14 @@ export function blockstampFromBlocklistFilter(dnsPacket, blocklistFilter) {
return util.emptyMap(m) ? false : util.objOf(m);
}
-function applyWildcardBlocklists(uint1, flagVersion, dnBlInfo, dn) {
+/**
+ * @param {string} dn domain name
+ * @param {Uint16Array} usrUint user blocklist flags
+ * @param {string} flagVersion mosty 0 or 1
+ * @param {pres.BStamp} dnBlInfo subdomain blocklist flag group
+ * @returns {pres.RespData}
+ */
+function applyWildcardBlocklists(dn, flagVersion, usrUint, dnBlInfo) {
const dnSplit = dn.split(".");
// iterate through all subdomains one by one, for ex: a.b.c.ex.com:
@@ -170,7 +181,7 @@ function applyWildcardBlocklists(uint1, flagVersion, dnBlInfo, dn) {
// the subdomain isn't present in any current blocklists
if (util.emptyArray(subdomainUint)) continue;
- const response = applyBlocklists(uint1, subdomainUint, flagVersion);
+ const response = applyBlocklists(flagVersion, usrUint, subdomainUint);
// if any subdomain is in any blocklist, block the current request
if (!util.emptyObj(response) && response.isBlocked) {
@@ -181,7 +192,13 @@ function applyWildcardBlocklists(uint1, flagVersion, dnBlInfo, dn) {
return pres.rdnsNoBlockResponse();
}
-function applyBlocklists(uint1, uint2, flagVersion) {
+/**
+ * @param {string} flagVersion
+ * @param {Uint16Array} uint1
+ * @param {Uint16Array} uint2
+ * @returns {pres.RespData}
+ */
+function applyBlocklists(flagVersion, uint1, uint2) {
// uint1 -> user blocklists; uint2 -> blocklists including sub/domains
const blockedUint = intersect(uint1, uint2);
@@ -194,6 +211,11 @@ function applyBlocklists(uint1, uint2, flagVersion) {
}
}
+/**
+ * @param {Uint16Array} flag1
+ * @param {Uint16Array} flag2
+ * @returns {Uint16Array|null}
+ */
function intersect(flag1, flag2) {
if (util.emptyArray(flag1) || util.emptyArray(flag2)) return null;
@@ -253,6 +275,11 @@ function intersect(flag1, flag2) {
return Uint16Array.of(commonHeader, ...commonBody.reverse());
}
+/**
+ * @param {int} uint
+ * @param {int} pos
+ * @returns
+ */
function clearbit(uint, pos) {
return uint & ~(1 << pos);
}
@@ -326,7 +353,7 @@ export function recBlockstampFrom(url) {
/**
* @param {string} u - Request URL string
- * @returns {Array} s - delim, version, blockstamp (flag), accesskey
+ * @returns {string[]} s - delim, version, blockstamp (flag), accesskey
*/
export function extractStamps(u) {
const url = new URL(u);
@@ -373,6 +400,10 @@ export function extractStamps(u) {
return emptystamp;
}
+/**
+ * @param {string} b64Flag
+ * @returns {Uint16Array}
+ */
export function base64ToUintV0(b64Flag) {
// TODO: v0 not in use, remove all occurences
// FIXME: Impl not accurate
@@ -381,32 +412,45 @@ export function base64ToUintV0(b64Flag) {
return bufutil.base64ToUint16(f);
}
+/**
+ * @param {string} b64Flag
+ * @returns {Uint16Array}
+ */
export function base64ToUintV1(b64Flag) {
// TODO: check for empty b64Flag
return bufutil.base64ToUint16(b64Flag);
}
+/**
+ * @param {string} b64Flag
+ * @returns {Uint16Array}
+ */
export function base32ToUintV1(flag) {
// TODO: check for empty flag
const b32 = decodeURI(flag);
return bufutil.decodeFromBinaryArray(rbase32(b32));
}
+/**
+ * @param {string} s
+ * @returns {string[]} [delim, ver, blockstamp, accesskey]
+ */
function splitBlockstamp(s) {
- // delim, version, blockstamp, accesskey
-
if (util.emptyString(s)) return emptystamp;
if (!isStampQuery(s)) return emptystamp;
if (isB32Stamp(s)) {
+ // delim, version, blockstamp, accesskey
return [_b32delim, ...s.split(_b32delim)];
} else {
return [_b64delim, ...s.split(_b64delim)];
}
-
- return out;
}
+/**
+ * @param {string} s
+ * @returns {boolean}
+ */
export function isB32Stamp(s) {
const idx32 = s.indexOf(_b32delim);
const idx64 = s.indexOf(_b64delim);
@@ -416,21 +460,27 @@ export function isB32Stamp(s) {
else return idx32 < idx64;
}
-// s[0] is version field, if it doesn't exist
-// then treat it as if version 0.
+/**
+ *
+ * @param {string[]} s
+ * @returns {string}
+ */
export function stampVersion(s) {
+ // s[0] is version field, if it doesn't exist
+ // then treat it as if version 0.
if (!util.emptyArray(s)) return s[0];
else return "0";
}
// TODO: The logic to parse stamps must be kept in sync with:
// github.com/celzero/website-dns/blob/8e6056bb/src/js/flag.js#L260-L425
+/**
+ *
+ * @param {string} flag
+ * @returns {pres.BlockstampInfo}
+ */
export function unstamp(flag) {
- const r = {
- userBlocklistFlagUint: null,
- flagVersion: "0",
- userServiceListUint: null,
- };
+ const r = new pres.BlockstampInfo();
if (util.emptyString(flag)) return r;
@@ -440,29 +490,26 @@ export function unstamp(flag) {
const isFlagB32 = isB32Stamp(flag);
// "v:b64" or "v-b32" or "uriencoded(b64)", where v is uint version
const s = flag.split(isFlagB32 ? _b32delim : _b64delim);
- let convertor = (x) => ""; // empty convertor
- let f = ""; // stamp flag
const v = stampVersion(s);
+ r.flagVersion = v;
if (v === "0") {
- // version 0
- convertor = base64ToUintV0;
- f = s[0];
+ const f = s[0];
+ r.userBlocklistFlagUint = base64ToUintV0(f) || null;
} else if (v === "1") {
- convertor = isFlagB32 ? base32ToUintV1 : base64ToUintV1;
- f = s[1];
+ const convertor = isFlagB32 ? base32ToUintV1 : base64ToUintV1;
+ const f = s[1];
+ r.userBlocklistFlagUint = convertor(f) || null;
} else {
log.w("Rdns:unstamp", "unknown blocklist stamp version in " + s);
- return r;
}
-
- r.flagVersion = v;
- r.userBlocklistFlagUint = convertor(f) || null;
- r.userServiceListUint = intersect(r.userBlocklistFlagUint, _wildcardUint16);
-
return r;
}
+/**
+ * @param {pres.BlockstampInfo} blockInfo
+ * @returns {boolean}
+ */
export function hasBlockstamp(blockInfo) {
return (
!util.emptyObj(blockInfo) &&
@@ -470,33 +517,15 @@ export function hasBlockstamp(blockInfo) {
);
}
-// returns true if tstamp is of form yyyy/epochMs
-function isValidFullTimestamp(tstamp) {
- if (typeof tstamp !== "string") return false;
- return tstamp.indexOf("/") === 4;
-}
-
-// from: github.com/celzero/downloads/blob/main/src/timestamp.js
-export function bareTimestampFrom(tstamp) {
- // strip out "/" if tstamp is of form yyyy/epochMs
- if (isValidFullTimestamp(tstamp)) {
- tstamp = tstamp.split("/")[1];
- }
- const t = parseInt(tstamp);
- if (isNaN(t)) {
- log.w("Rdns bareTstamp: NaN", tstamp);
- return 0;
- }
- return t;
-}
-
+/**
+ * @param {string} strflag
+ * @returns {string[]} blocklist names
+ */
export function blocklists(strflag) {
const { userBlocklistFlagUint, flagVersion } = unstamp(strflag);
const blocklists = [];
if (flagVersion === "1") {
return trie.flagsToTags(userBlocklistFlagUint);
- } else {
- throw new Error("unknown blocklist version: " + flagVersion);
- }
+ } // unknown blocklist version
return blocklists;
}
diff --git a/src/plugins/rethinkdns/filter.js b/src/plugins/rethinkdns/filter.js
index 12f2769bd5..a72d378280 100644
--- a/src/plugins/rethinkdns/filter.js
+++ b/src/plugins/rethinkdns/filter.js
@@ -6,15 +6,22 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
+import { FrozenTrie } from "@serverless-dns/trie/ftrie.js";
import * as dnsutil from "../../commons/dnsutil.js";
export class BlocklistFilter {
constructor() {
// see: src/helpers/node/blocklists.js:hasBlocklistFiles
+ /** @type {FrozenTrie} */
this.ftrie = null;
+ /** @type {Object} */
this.filetag = null;
}
+ /**
+ * @param {FrozenTrie} frozentrie
+ * @param {Object} filetag
+ */
load(frozentrie, filetag) {
this.ftrie = frozentrie;
this.filetag = filetag;
@@ -28,6 +35,11 @@ export class BlocklistFilter {
lookup(n) {
const t = this.ftrie;
+ if (t == null) {
+ log.w("blocklist filter not loaded");
+ return null;
+ }
+
try {
n = t.transform(n);
return t.lookup(n);
@@ -55,7 +67,9 @@ export class BlocklistFilter {
extract(ids) {
const r = {};
- for (const id of ids) r[id] = this.filetag[id];
+ if (this.filetag) {
+ for (const id of ids) r[id] = this.filetag[id];
+ }
return r;
}
}
diff --git a/src/plugins/rethinkdns/main.js b/src/plugins/rethinkdns/main.js
index f2b3ae0dad..485a1a2fc7 100644
--- a/src/plugins/rethinkdns/main.js
+++ b/src/plugins/rethinkdns/main.js
@@ -7,26 +7,39 @@
*/
import { createTrie } from "@serverless-dns/trie/ftrie.js";
-import { BlocklistFilter } from "./filter.js";
-import { withDefaults } from "./trie-config.js";
-import * as pres from "../plugin-response.js";
-import * as cfg from "../../core/cfg.js";
import * as bufutil from "../../commons/bufutil.js";
-import * as util from "../../commons/util.js";
import * as envutil from "../../commons/envutil.js";
+import * as util from "../../commons/util.js";
+import * as cfg from "../../core/cfg.js";
+import * as pres from "../plugin-response.js";
import * as rdnsutil from "../rdns-util.js";
+import { BlocklistFilter } from "./filter.js";
+import { withDefaults } from "./trie-config.js";
// number of range fetches for trie.txt; -1 to disable
const maxrangefetches = 2;
+const basicconfigDir = "bc";
+const bcFilename = "basicconfig.json";
+const ftFilename = "filetag.json";
+const defaultCodec = "u6";
+const maxRenewAttempts = 5;
+
export class BlocklistWrapper {
constructor() {
+ /** @type {BlocklistFilter} */
this.blocklistFilter = new BlocklistFilter();
+ /** @type {number} */
this.startTime = Date.now(); // blocklist download timestamp
+ /** @type {boolean} */
this.isBlocklistUnderConstruction = false;
+ /** @type {string} */
this.exceptionFrom = "";
+ /** @type {string} */
this.exceptionStack = "";
+ /** @type {boolean} */
this.noop = envutil.disableBlocklists();
+ /** @type {boolean} */
this.nowait = envutil.bgDownloadBlocklistWrapper();
this.log = log.withTags("BlocklistWrapper");
@@ -50,11 +63,7 @@ export class BlocklistWrapper {
now - this.startTime > envutil.downloadTimeout() * 2
) {
this.log.i(rxid, "download blocklists", now, this.startTime);
- const url = envutil.blocklistUrl() + cfg.timestamp() + "/";
- const nc = cfg.tdNodeCount();
- const parts = cfg.tdParts();
- const u6 = cfg.tdCodec6();
- return this.initBlocklistConstruction(rxid, now, url, nc, parts, u6);
+ return this.initBlocklistConstruction(rxid, now);
} else if (this.nowait && !forceget) {
// blocklist-construction is in progress, but we don't have to
// wait for it to finish. So, return an empty response.
@@ -62,7 +71,7 @@ export class BlocklistWrapper {
return pres.emptyResponse();
} else {
// someone's constructing... wait till finished
- return this.waitUntilDone();
+ return this.waitUntilDone(rxid);
}
} catch (e) {
this.log.e(rxid, "main", e.stack);
@@ -82,7 +91,7 @@ export class BlocklistWrapper {
return rdnsutil.isBlocklistFilterSetup(this.blocklistFilter);
}
- async waitUntilDone() {
+ async waitUntilDone(rxid) {
// res.arrayBuffer() is the most expensive op, taking anywhere
// between 700ms to 1.2s for trie. But: We don't want all incoming
// reqs to wait until the trie becomes available. 400ms is 1/3rd of
@@ -96,6 +105,7 @@ export class BlocklistWrapper {
const response = pres.emptyResponse();
while (totalWaitms < envutil.downloadTimeout()) {
if (this.isBlocklistFilterSetup()) {
+ this.log.i(rxid, "blocklistWrapper: download done:", totalWaitms);
response.data.blocklistFilter = this.blocklistFilter;
return response;
}
@@ -103,12 +113,20 @@ export class BlocklistWrapper {
totalWaitms += waitms;
}
+ this.log.e(rxid, "blocklistWrapper", "download timed out:", totalWaitms);
response.isException = true;
response.exceptionStack = this.exceptionStack || "download timeout";
response.exceptionFrom = this.exceptionFrom || "blocklistWrapper.js";
return response;
}
+ /**
+ *
+ * @param {ArrayBufferLike} td
+ * @param {ArrayBufferLike} rd
+ * @param {Object} ftags
+ * @param {Object} bconfig
+ */
buildBlocklistFilter(td, rd, ftags, bconfig) {
this.isBlocklistUnderConstruction = true;
this.startTime = Date.now();
@@ -124,28 +142,44 @@ export class BlocklistWrapper {
return createTrie(tdbuf, rdbuf, bconfig);
}
- async initBlocklistConstruction(
- rxid,
- when,
- url,
- tdNodecount,
- tdParts,
- tdCodec6
- ) {
+ /**
+ * @param {string} rxid
+ * @param {int} when
+ * @returns {Promise}
+ */
+ async initBlocklistConstruction(rxid, when) {
this.isBlocklistUnderConstruction = true;
this.startTime = when;
+ const baseurl = envutil.blocklistUrl();
+
+ let bconfig = withDefaults(cfg.orig());
+ let ft = cfg.filetag();
+ // if bconfig.timestamp is older than AUTO_RENEW_BLOCKLISTS_OLDER_THAN
+ // then download the latest filetag (ft) and basicconfig (bconfig).
+ if (!envutil.disableBlocklists()) {
+ const blocklistAgeThresWeeks = envutil.renewBlocklistsThresholdInWeeks();
+ const bltimestamp = util.bareTimestampFrom(cfg.timestamp());
+ if (isPast(bltimestamp, blocklistAgeThresWeeks)) {
+ const [renewCfg, renewedFt] = await renew(baseurl);
+
+ if (renewCfg != null && renewedFt != null) {
+ this.log.i(rxid, "r:", bconfig.timestamp, "=>", renewCfg.timestamp);
+ bconfig = withDefaults(renewCfg);
+ ft = renewedFt;
+ } else {
+ this.log.w(rxid, "r: failed; got:", renewCfg);
+ }
+ } else {
+ this.log.d(rxid, "r: not needed for:", bltimestamp);
+ }
+ }
+
let response = pres.emptyResponse();
try {
- await this.downloadAndBuildBlocklistFilter(
- rxid,
- url,
- tdNodecount,
- tdParts,
- tdCodec6
- );
-
- this.log.i(rxid, "blocklist-filter setup; u6?", tdCodec6);
+ await this.downloadAndBuildBlocklistFilter(rxid, bconfig, ft);
+
+ this.log.i(rxid, "blocklist-filter setup; u6?", bconfig.useCodec6);
if (false) {
// test
const result = this.blocklistFilter.blockstamp("google.com");
@@ -165,21 +199,15 @@ export class BlocklistWrapper {
return response;
}
- async downloadAndBuildBlocklistFilter(rxid, url, tdNodecount, tdParts, u6) {
- !tdNodecount && this.log.e(rxid, "tdNodecount zero or missing!");
+ async downloadAndBuildBlocklistFilter(rxid, bconfig, ft) {
+ const tdNodecount = bconfig.nodecount; // or: cfg.tdNodeCount();
+ const tdParts = bconfig.tdparts; // or: cfg.tdParts();
+ const u6 = bconfig.useCodec6; // or: cfg.tdCodec6();
- const bconfig = withDefaults(cfg.orig());
- const ft = cfg.filetag();
+ let url = envutil.blocklistUrl() + bconfig.timestamp + "/";
+ url += u6 ? "u6/" : "u8/";
- if (
- bconfig.useCodec6 !== u6 ||
- bconfig.nodecount !== tdNodecount ||
- bconfig.tdparts !== tdParts
- ) {
- throw new Error(bconfig + "<=cfg; in=>" + u6 + " " + tdNodecount);
- }
-
- url += bconfig.useCodec6 ? "u6/" : "u8/";
+ !tdNodecount && this.log.e(rxid, "tdNodecount zero or missing!");
this.log.d(rxid, url, tdNodecount, tdParts);
const buf0 = fileFetch(url + "rd.txt", "buffer");
@@ -195,11 +223,12 @@ export class BlocklistWrapper {
const ftrie = this.makeTrie(td, rd, bconfig);
this.blocklistFilter.load(ftrie, ft);
-
- return;
}
triedata() {
+ if (!rdnsutil.isBlocklistFilterSetup(this.blocklistFilter)) {
+ throw new Error("no triedata: blocklistFilter not loaded");
+ }
const blf = this.blocklistFilter;
const ftrie = blf.ftrie;
const rdir = ftrie.directory;
@@ -208,12 +237,63 @@ export class BlocklistWrapper {
}
rankdata() {
+ if (!rdnsutil.isBlocklistFilterSetup(this.blocklistFilter)) {
+ throw new Error("no rankdata: blocklistFilter not loaded");
+ }
const blf = this.blocklistFilter;
const ftrie = blf.ftrie;
const rdir = ftrie.directory;
const d = rdir.directory;
return bufutil.raw(d.bytes);
}
+
+ filetag() {
+ if (!rdnsutil.isBlocklistFilterSetup(this.blocklistFilter)) {
+ throw new Error("no filetag: blocklistFilter not loaded");
+ }
+ const blf = this.blocklistFilter;
+ return blf.filetag;
+ }
+
+ basicconfig() {
+ if (!rdnsutil.isBlocklistFilterSetup(this.blocklistFilter)) {
+ throw new Error("no basicconfig: blocklistFilter not loaded");
+ }
+ const blf = this.blocklistFilter;
+ const ftrie = blf.ftrie;
+ const rdir = ftrie.directory;
+ return rdir.config;
+ }
+
+ /**
+ * Returns the timestamp of the blocklist (epochMillis or yyyy/epochMillis)
+ * @param {string} defaultTimestamp
+ * @returns {string} timestamp
+ * @throws {Error} if timestamp could not be determined and defaultTimestamp is empty.
+ */
+ timestamp(defaultTimestamp = "") {
+ try {
+ const bc = this.basicconfig();
+ if (bc == null) {
+ throw new Error("missing basicconfig");
+ }
+ if (util.emptyString(bc.timestamp)) {
+ throw new Error("basicconfig missing timestamp");
+ }
+ return bc.timestamp;
+ } catch (ex) {
+ // debug: this.log.d("blocklistWrapper: get timestamp", ex);
+ if (util.emptyString(defaultTimestamp)) {
+ throw ex;
+ }
+ }
+ return defaultTimestamp;
+ }
+
+ codec() {
+ const tdcodec6 = this.basicconfig().useCodec6;
+ return tdcodec6 ? "u6" : "u8";
+ }
}
async function fileFetch(url, typ, h = {}) {
@@ -311,3 +391,107 @@ async function makeTd(baseurl, n) {
return bufutil.concat(tds);
}
+
+/**
+ * @typedef {Object} DateInfo
+ * @property {number} day
+ * @property {number} week
+ * @property {number} month
+ * @property {number} year
+ * @property {number} timestamp
+ */
+
+/**
+ * @returns {DateInfo}
+ */
+function todayAsDateInfo() {
+ const date = new Date();
+ const day = date.getUTCDate();
+ const week = Math.ceil(day / 7);
+ const month = date.getUTCMonth() + 1;
+ const year = date.getUTCFullYear();
+ const timestamp = date.getTime();
+ return { day, week, month, year, timestamp };
+}
+
+/**
+ * Main function to prefetch files based on week, month, and year.
+ * @param {string} baseurl
+ */
+async function renew(baseurl) {
+ let { week: wk, month: mm, year: yyyy, timestamp: now } = todayAsDateInfo();
+
+ for (let i = 0; i <= maxRenewAttempts; i++) {
+ const configUrl = `${baseurl}${yyyy}/${basicconfigDir}/${mm}-${wk}/${defaultCodec}/${bcFilename}`;
+ log.i(`attempt ${i}: fetching ${configUrl} at ${now}`);
+
+ try {
+ // {
+ // "version":1,
+ // "nodecount":81551789,
+ // "inspect":false,
+ // "debug":false,
+ // "selectsearch":true,
+ // "useCodec6":true,
+ // "optflags":true,
+ // "tdpartsmaxmb":0,
+ // "timestamp":"2025/1740866164283",
+ // "tdparts":-1,
+ // "tdmd5":"000ed9638e8e0f12e450050997e84365",
+ // "rdmd5":"75e5eebc71be02d8bef47b93ea58c213",
+ // "ftmd5":"8c56effb0f3d73232f7090416bb2e7c1",
+ // "ftlmd5":"54b323eb653451ba8940acb00d20382a"
+ // }
+ const bconfig = await fileFetch(configUrl, "json");
+
+ if (bconfig) {
+ const fullTimestamp = bconfig.timestamp;
+ if (fullTimestamp) {
+ const codec = bconfig.useCodec6 ? "u6" : "u8";
+ const tagUrl = `${baseurl}${fullTimestamp}/${codec}/${ftFilename}`;
+ log.i(`attempt ${i}: fetching ${configUrl} at ${now}`);
+
+ const ft = await fileFetch(tagUrl, "json");
+
+ if (ft) return [bconfig, ft];
+ else log.w(`failed to fetch ${tagUrl}`);
+ }
+ }
+ } catch (ex) {
+ // ex: 4xx, 5xx
+ log.w(`renew #${i} err; retrying...`, ex);
+ }
+
+ // decr week, month, year; try again
+ wk--;
+ if (wk <= 0) {
+ wk = 5;
+ mm--;
+ }
+ if (mm <= 0) {
+ mm = 12;
+ yyyy--;
+ }
+ }
+
+ log.e("no new filetag or basicconfig: exceeded max retries");
+ return [null, null];
+}
+
+/**
+ * @param {int} tsms (in unix millis)
+ * @param {int} wk (in weeks > 0)
+ * @returns {bool}
+ */
+function isPast(tsms, wk) {
+ if (tsms <= 0 || wk <= 0) return false;
+
+ const since = Date.now() - tsms;
+ const sinceWeeks = Math.floor(since / (1000 * 60 * 60 * 24 * 7));
+
+ const y = sinceWeeks > wk;
+ if (y) {
+ log.w("blocklist is old:", sinceWeeks, ">", wk);
+ }
+ return y;
+}
diff --git a/src/plugins/users/user-op.js b/src/plugins/users/user-op.js
index 6f879311ad..3247072af0 100644
--- a/src/plugins/users/user-op.js
+++ b/src/plugins/users/user-op.js
@@ -8,13 +8,18 @@
import { UserCache } from "./user-cache.js";
import * as pres from "../plugin-response.js";
import * as util from "../../commons/util.js";
+import * as envutil from "../../commons/envutil.js";
import * as rdnsutil from "../rdns-util.js";
import * as token from "./auth-token.js";
-import * as bufutil from "../../commons/bufutil.js";
+import * as dnsutil from "../../commons/dnsutil.js";
// TODO: determine an approp cache-size
const cacheSize = 20000;
+// use fixed doh upstream for these domains,
+// instead of either recursing (on Fly.io)
+const delegated = new Set(["ipv4only.arpa"]);
+
export class UserOp {
constructor() {
this.userConfigCache = new UserCache(cacheSize);
@@ -22,7 +27,7 @@ export class UserOp {
}
/**
- * @param {{request: Request, isDnsMsg: Boolean, rxid: string}} ctx
+ * @param {{request: Request, requestDecodedDnsPacket: any, isDnsMsg: Boolean, rxid: string}} ctx
* @returns {Promise}
*/
async exec(ctx) {
@@ -44,11 +49,11 @@ export class UserOp {
}
/**
- * @param {{request: Request, isDnsMsg: Boolean, rxid: string}} ctx
+ * @param {{request: Request, requestDecodedDnsPacket: any, isDnsMsg: Boolean, rxid: string}} ctx
* @returns {pres.RResp}
*/
loadUser(ctx) {
- let response = pres.emptyResponse();
+ const response = pres.emptyResponse();
if (!ctx.isDnsMsg) {
this.log.w(ctx.rxid, "not a dns-msg, ignore");
@@ -56,18 +61,29 @@ export class UserOp {
}
try {
- const blocklistFlag = rdnsutil.blockstampFromUrl(ctx.request.url);
+ const dnsPacket = ctx.requestDecodedDnsPacket;
+ const domains = dnsutil.extractDomains(dnsPacket);
+ for (const d of domains) {
+ if (delegated.has(d)) {
+ // may be overriden by user-preferred doh upstream
+ response.data.dnsResolverUrl = envutil.primaryDohResolver();
+ }
+ }
- if (util.emptyString(blocklistFlag)) {
+ const blocklistFlag = rdnsutil.blockstampFromUrl(ctx.request.url);
+ const hasflag = !util.emptyString(blocklistFlag);
+ if (!hasflag) {
this.log.d(ctx.rxid, "empty blocklist-flag", ctx.request.url);
}
-
// blocklistFlag may be invalid, ref rdnsutil.blockstampFromUrl
let r = this.userConfigCache.get(blocklistFlag);
- if (!util.emptyString(blocklistFlag) && util.emptyObj(r)) {
- r = rdnsutil.unstamp(blocklistFlag);
+ let hasdata = rdnsutil.hasBlockstamp(r);
+ if (hasflag && !hasdata) {
+ // r not in cache
+ r = rdnsutil.unstamp(blocklistFlag); // r is never null, may throw ex
+ hasdata = rdnsutil.hasBlockstamp(r);
- if (!bufutil.emptyBuf(r.userBlocklistFlagUint)) {
+ if (hasdata) {
this.log.d(ctx.rxid, "new cfg cache kv", blocklistFlag, r);
// TODO: blocklistFlag is not normalized, ie b32 used for dot isn't
// converted to its b64 form (which doh and rethinkdns modules use)
@@ -75,16 +91,18 @@ export class UserOp {
this.userConfigCache.put(blocklistFlag, r);
}
} else {
- this.log.d(ctx.rxid, "cfg cache hit?", r != null, blocklistFlag, r);
+ this.log.d(ctx.rxid, "cfg cache hit?", hasdata, blocklistFlag, r);
}
- response.data.userBlocklistInfo = r;
- response.data.userBlocklistFlag = blocklistFlag;
- // sets user-preferred doh upstream
- response.data.dnsResolverUrl = null;
+ if (hasdata) {
+ response.data.userBlocklistInfo = r;
+ response.data.userBlocklistFlag = blocklistFlag;
+ // TODO: override response.data.dnsResolverUrl
+ }
} catch (e) {
this.log.e(ctx.rxid, "loadUser", e);
- response = pres.errResponse("UserOp:loadUser", e);
+ // avoid erroring out on invalid blocklist info & flag
+ // response = pres.errResponse("UserOp:loadUser", e);
}
return response;
diff --git a/src/server-deno.ts b/src/server-deno.ts
index f6c6573a64..91da166928 100644
--- a/src/server-deno.ts
+++ b/src/server-deno.ts
@@ -11,7 +11,6 @@
import "./core/deno/config.ts";
import { handleRequest } from "./core/doh.js";
import { stopAfter, uptime } from "./core/svc.js";
-import { serve, serveTls } from "https://deno.land/std@0.171.0/http/server.ts";
import * as system from "./system.js";
import * as util from "./commons/util.js";
import * as bufutil from "./commons/bufutil.js";
@@ -69,16 +68,25 @@ function systemUp() {
const abortctl = new AbortController();
const onDenoDeploy = envutil.onDenoDeploy() as boolean;
+ const isCleartext = envutil.isCleartext() as boolean;
const dohConnOpts = { port: envutil.dohBackendPort() };
const dotConnOpts = { port: envutil.dotBackendPort() };
const sigOpts = {
signal: abortctl.signal,
onListen: undefined,
};
- const tlsOpts = {
- certFile: envutil.tlsCrtPath() as string,
- keyFile: envutil.tlsKeyPath() as string,
- };
+
+ const crtpath = envutil.tlsCrtPath() as string;
+ const keypath = envutil.tlsKeyPath() as string;
+ const dotls = !onDenoDeploy && !isCleartext;
+
+ const tlsOpts = dotls
+ ? {
+ // docs.deno.com/runtime/reference/migration_guide/
+ cert: Deno.readTextFileSync(crtpath),
+ key: Deno.readTextFileSync(keypath),
+ }
+ : { cert: "", key: "" };
// deno.land/manual@v1.18.0/runtime/http_server_apis_low_level
const httpOpts = {
alpnProtocols: ["h2", "http/1.1"],
@@ -87,19 +95,21 @@ function systemUp() {
startDoh();
startDotIfPossible();
- // deno.land/manual@v1.29.1/runtime/http_server_apis
- async function startDoh() {
+ // docs.deno.com/runtime/fundamentals/http_server
+ // docs.deno.com/api/deno/~/Deno.serve
+ function startDoh() {
if (terminateTls()) {
- // deno.land/std@0.170.0/http/server.ts?s=serveTls
- serveTls(serveDoh, {
- ...dohConnOpts,
- ...tlsOpts,
- ...httpOpts,
- ...sigOpts,
- });
+ Deno.serve(
+ {
+ ...dohConnOpts,
+ ...tlsOpts,
+ ...httpOpts,
+ ...sigOpts,
+ },
+ serveDoh
+ );
} else {
- // deno.land/std@0.171.0/http/server.ts?s=serve
- serve(serveDoh, { ...dohConnOpts, ...sigOpts });
+ Deno.serve({ ...dohConnOpts, ...sigOpts }, serveDoh);
}
up("DoH", abortctl, dohConnOpts);
@@ -134,14 +144,14 @@ function systemUp() {
function terminateTls() {
if (onDenoDeploy) return false;
- if (util.emptyString(tlsOpts.keyFile)) return false;
- if (envutil.isCleartext()) return false;
- if (util.emptyString(tlsOpts.certFile)) return false;
+ if (envutil.isCleartext() as boolean) return false;
+ if (util.emptyString(tlsOpts.key)) return false;
+ if (util.emptyString(tlsOpts.cert)) return false;
return true;
}
}
-async function serveDoh(req: Request) {
+function serveDoh(req: Request) {
try {
// doc.deno.land/deno/stable/~/Deno.RequestEvent
// deno.land/manual/runtime/http_server_apis#http-requests-and-responses
diff --git a/src/server-node.js b/src/server-node.js
index 74bd180108..ad5fa6af79 100644
--- a/src/server-node.js
+++ b/src/server-node.js
@@ -6,31 +6,32 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import net, { isIPv6 } from "node:net";
-import * as tls from "node:tls";
-import http2 from "node:http2";
+// should always be the first import
+// import whyIsNodeRunning from "why-is-node-running";
+import "./core/node/config.js";
+
+import { LfuCache } from "@serverless-dns/lfu-cache";
import * as h2c from "httpx-server";
-import * as os from "node:os";
+import http2 from "node:http2";
+import https from "node:https";
+import net, { isIPv6 } from "node:net";
+import os from "node:os";
+import { finished } from "node:stream";
+import tls, { TLSSocket } from "node:tls";
import v8 from "node:v8";
import { V2ProxyProtocol } from "proxy-protocol-js";
-import * as system from "./system.js";
-import { handleRequest } from "./core/doh.js";
-import { stopAfter, uptime } from "./core/svc.js";
import * as bufutil from "./commons/bufutil.js";
+import * as nodecrypto from "./commons/crypto.js";
import * as dnsutil from "./commons/dnsutil.js";
import * as envutil from "./commons/envutil.js";
-import * as nodeutil from "./core/node/util.js";
import * as util from "./commons/util.js";
-import "./core/node/config.js";
-import { finished } from "node:stream";
-import * as nodecrypto from "./commons/crypto.js";
-// webpack can't handle node-bindings, a dependency of node-memwatch
-// github.com/webpack/webpack/issues/16029
-// import * as memwatch from "@airbnb/node-memwatch";
+import { handleRequest } from "./core/doh.js";
+import * as nodeutil from "./core/node/util.js";
+import { stopAfter, uptime } from "./core/svc.js";
+import * as system from "./system.js";
/**
* @typedef {net.Socket} Socket
- * @typedef {tls.TLSSocket} TLSSocket
* @typedef {http2.Http2ServerRequest} Http2ServerRequest
* @typedef {http2.Http2ServerResponse} Http2ServerResponse
*/
@@ -46,30 +47,55 @@ class Stats {
this.noreqs = -1;
this.nofchecks = 0;
this.tlserr = 0;
+ this.fasttls = 0;
+ this.totfasttls = 0;
+ this.noftlsadjs = 0;
this.nofdrops = 0;
this.nofconns = 0;
this.openconns = 0;
this.noftimeouts = 0;
+ this.nofheapsnaps = 0;
// avg1, avg5, avg15, adj, maxconns
this.bp = [0, 0, 0, 0, 0];
}
str() {
return (
- `reqs=${this.noreqs} checks=${this.nofchecks} ` +
+ `reqs=${this.noreqs} c=${this.nofchecks} ` +
`drops=${this.nofdrops}/tot=${this.nofconns}/open=${this.openconns} ` +
- `timeouts=${this.noftimeouts}/tlserr=${this.tlserr} ` +
+ `to=${this.noftimeouts}/tlserr=${this.tlserr} ` +
+ `tls0=${this.fasttls}/tls0miss=${this.totfasttls}/tlsadjs=${this.noftlsadjs} ` +
`n=${this.bp[4]}/adj=${this.bp[3]} ` +
`load=${this.bp[0]}/${this.bp[1]}/${this.bp[2]}`
);
}
}
+class SoReport {
+ constructor() {
+ /** @type {int} total bytes transferred in preceding 1sec */
+ this.tx = 0;
+ /** @type {int} unix timestamp in millis */
+ this.lastsnd = 0;
+ }
+}
+
+class ConnW {
+ /**
+ * @param {Socket|TLSSocket} socket
+ */
+ constructor(socket) {
+ this.socket = socket;
+ this.rep = new SoReport();
+ }
+}
+
class Tracker {
constructor() {
this.zeroid = "";
- /** @type {Array