From 47a9bf1aafd53c37656addc06ad452261b255e46 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 23 Sep 2023 16:37:39 +0530 Subject: [PATCH 001/126] gh-action: weekly workers & fly prod deploys runs a day or two after serverless-dns/blocklists generates new blocklist trie --- .github/workflows/cf.yml | 21 ++++++++++++++++++--- .github/workflows/fly.yml | 13 +++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cf.yml b/.github/workflows/cf.yml index 77b8c83db1..459f71fbdb 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 @@ -57,7 +61,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Checkout + - name: πŸ›’ Checkout uses: actions/checkout@v3.3.0 with: ref: ${{ env.GIT_REF }} @@ -86,20 +90,31 @@ 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 }} env: CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} - GIT_COMMIT_ID: ${{ env.GIT_REF }} + GIT_COMMIT_ID: ${{ env.COMMIT_SHA }} - name: 🎀 Notice run: | diff --git a/.github/workflows/fly.yml b/.github/workflows/fly.yml index 088db6ab6f..15bba278fa 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" @@ -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' }} From 8e4be2ae5fd70879fc4162ceb39065e6925e3a4e Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 18 Nov 2023 22:54:12 +0530 Subject: [PATCH 002/126] fly: reduce concurrent conns --- fly.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fly.toml b/fly.toml index 40fd0455ba..bfd8d08d19 100644 --- a/fly.toml +++ b/fly.toml @@ -32,8 +32,8 @@ swap_size_mb = 152 [services.concurrency] type = "connections" - hard_limit = 775 - soft_limit = 700 + hard_limit = 375 + soft_limit = 300 [[services.tcp_checks]] # super aggressive interval and timeout because @@ -61,8 +61,8 @@ swap_size_mb = 152 [services.concurrency] type = "connections" - hard_limit = 775 - soft_limit = 700 + hard_limit = 375 + soft_limit = 300 [[services.tcp_checks]] # super aggressive interval and timeout because From 0579ed35d3aa8e42e050c3061a11f8bf968cf1fa Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 18 Nov 2023 23:10:51 +0530 Subject: [PATCH 003/126] node: tune tcp backlog, max conns, load adj --- src/commons/envutil.js | 8 ++++---- src/server-node.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commons/envutil.js b/src/commons/envutil.js index b2489caf9c..f07b8e9e56 100644 --- a/src/commons/envutil.js +++ b/src/commons/envutil.js @@ -191,16 +191,16 @@ export function isCleartext() { // sysctl get net.ipv4.tcp_syn_backlog export function tcpBacklog() { - if (!envManager) return 100; + if (!envManager) return 300; - return envManager.get("TCP_BACKLOG") || 200; + return envManager.get("TCP_BACKLOG") || 300; } // don't forget to update the fly.toml too export function maxconns() { - if (!envManager) return 1000; + if (!envManager) return 500; - return envManager.get("MAXCONNS") || 1000; + return envManager.get("MAXCONNS") || 500; } export function minconns() { diff --git a/src/server-node.js b/src/server-node.js index 74bd180108..379cdc3473 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -1132,8 +1132,8 @@ function adjustMaxConns(n) { } // adjustMaxConns is called every adjPeriodSec - const breakpoint = 10 * adjsPerSec; // 10 mins - const stresspoint = 5 * adjsPerSec; // 5 mins + const breakpoint = 6 * adjsPerSec; // 6 mins + const stresspoint = 4 * adjsPerSec; // 4 mins const nstr = stats.openconns + "/" + n; if (adj > breakpoint) { log.w("load: stopping; n:", nstr, "adjs:", adj); From 4bad1fa6e7a37c5eddc0164e411323c07a0122a3 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Mon, 20 Nov 2023 13:08:21 +0530 Subject: [PATCH 004/126] node: re-tune maxconns and fly limits --- fly.toml | 8 ++++---- src/commons/envutil.js | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/fly.toml b/fly.toml index bfd8d08d19..38fc89f208 100644 --- a/fly.toml +++ b/fly.toml @@ -32,8 +32,8 @@ swap_size_mb = 152 [services.concurrency] type = "connections" - hard_limit = 375 - soft_limit = 300 + hard_limit = 775 + soft_limit = 600 [[services.tcp_checks]] # super aggressive interval and timeout because @@ -61,8 +61,8 @@ swap_size_mb = 152 [services.concurrency] type = "connections" - hard_limit = 375 - soft_limit = 300 + hard_limit = 775 + soft_limit = 600 [[services.tcp_checks]] # super aggressive interval and timeout because diff --git a/src/commons/envutil.js b/src/commons/envutil.js index f07b8e9e56..b270992d3a 100644 --- a/src/commons/envutil.js +++ b/src/commons/envutil.js @@ -191,16 +191,16 @@ export function isCleartext() { // sysctl get net.ipv4.tcp_syn_backlog export function tcpBacklog() { - if (!envManager) return 300; + if (!envManager) return 600; // same as fly.service soft_limit - return envManager.get("TCP_BACKLOG") || 300; + return envManager.get("TCP_BACKLOG") || 600; } // don't forget to update the fly.toml too export function maxconns() { - if (!envManager) return 500; + if (!envManager) return 1000; // 25% higher than fly.service hard_limit - return envManager.get("MAXCONNS") || 500; + return envManager.get("MAXCONNS") || 1000; } export function minconns() { From b6aceee90ab95e3b54f00537505629ec42b269ce Mon Sep 17 00:00:00 2001 From: ignoramous Date: Wed, 22 Nov 2023 05:27:50 +0530 Subject: [PATCH 005/126] gh-action: disable auto deploy for cf workers rdns is not functional on cloudflare due to an ongoing billing dispute --- .github/workflows/cf.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cf.yml b/.github/workflows/cf.yml index 459f71fbdb..a91232c99a 100644 --- a/.github/workflows/cf.yml +++ b/.github/workflows/cf.yml @@ -1,9 +1,9 @@ name: β›… CF on: # github.com/serverless-dns/blocklists/blob/6021f80f/.github/workflows/createUploadBlocklistFilter.yml#L4-L6 - schedule: + # schedule: # at 7:53 on 3rd, 10th, 18th, 26th of every month - - cron: '53 7 3,10,18,26 * *' + # - 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 From 7c1ef6245fc54c2c7b59ae1ffbc046df09d1f067 Mon Sep 17 00:00:00 2001 From: ignoramous Date: Tue, 20 Feb 2024 17:45:27 +0530 Subject: [PATCH 006/126] gh-action: node v21 and deno v1.40 for profiler --- .github/workflows/profiler.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/profiler.yml b/.github/workflows/profiler.yml index 03abc64574..045ed7044b 100644 --- a/.github/workflows/profiler.yml +++ b/.github/workflows/profiler.yml @@ -40,8 +40,8 @@ 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: '21.x' + DENO_VER: '1.40.x' MODE: 'p1' QDOH: 'q' @@ -89,6 +89,7 @@ jobs: - name: πŸ₯ Deno deps if: env.JS_RUNTIME == 'deno' run: | + deno task prepare deno cache ./src/server-deno.ts # if non-interactive, prefer apt-get: unix.stackexchange.com/a/590703 From f247f75d31a1939fc57be0aa05893f041c4dbfa5 Mon Sep 17 00:00:00 2001 From: ignoramous Date: Tue, 20 Feb 2024 17:49:08 +0530 Subject: [PATCH 007/126] run: ignore kill trap fails --- run | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run b/run index f2ea1f8c30..bee3764a36 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 } From b003bd93f546f7ce7a1d19a8fb83c6071f70f4b4 Mon Sep 17 00:00:00 2001 From: ignoramous Date: Wed, 12 Jun 2024 21:15:15 +0530 Subject: [PATCH 008/126] gh-action: https://github.com/ossf/scorecard-action/issues/997 --- .github/workflows/scorecard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 38c85d8126..36792dd644 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -32,12 +32,12 @@ jobs: 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.3.1 with: results_file: results.sarif results_format: sarif From 4d7b6090fe77c11b00403552a2ea86dc52d56333 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 4 May 2024 20:38:26 +0530 Subject: [PATCH 009/126] dockerfile: node 22 --- node.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node.Dockerfile b/node.Dockerfile index 0a499f772d..590731b12f 100644 --- a/node.Dockerfile +++ b/node.Dockerfile @@ -1,4 +1,4 @@ -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 From 178d1f2003de7196e174e4e91fdf99e124f35605 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 19 Jun 2024 21:48:09 +0530 Subject: [PATCH 010/126] gh-action: wrangler 3.56 --- .github/workflows/cf.yml | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cf.yml b/.github/workflows/cf.yml index a91232c99a..d15aff0900 100644 --- a/.github/workflows/cf.yml +++ b/.github/workflows/cf.yml @@ -52,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: '' @@ -112,6 +113,7 @@ jobs: apiToken: ${{ secrets.CF_API_TOKEN }} # input overrides env-defaults, regardless environment: ${{ env.WORKERS_ENV }} + wranglerVersion: ${{ env.WRANGLER_VER }} env: CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} GIT_COMMIT_ID: ${{ env.COMMIT_SHA }} diff --git a/package.json b/package.json index c8374d148c..49bfb1f582 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "prettier": "2.5.1", "webpack": "^5.65.0", "webpack-cli": "^4.10.0", - "wrangler": "^2.1.15" + "wrangler": "^3.0.0" }, "lint-staged": { "*.?(m|c)js": "eslint --cache --fix", From 3578d5d448e4cbff3dde2b23f3d10ad5d9f0e6ed Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 19 Jun 2024 21:48:44 +0530 Subject: [PATCH 011/126] wrangler: upload source maps --- wrangler.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/wrangler.toml b/wrangler.toml index c2aefe4a09..767ebbd9c4 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -8,6 +8,7 @@ logpush = false compatibility_date = "2023-03-21" send_metrics = false minify = false +upload_source_maps = true # uncomment to enable analytics on serverless-dns # this binding is not inherited by other worker-envs From 5f38bdc9c54834850437833f3d15b3da8bdf0e3b Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Mon, 24 Jun 2024 17:31:15 +0530 Subject: [PATCH 012/126] fly: mmap to read trie on disk --- package.json | 1 + src/commons/envutil.js | 5 +++++ src/core/node/blocklists.js | 26 ++++++++++++++++++++++---- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 49bfb1f582..25eab949c5 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@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", + "mmap-utils": "^1.0.0", "node-polyfill-webpack-plugin": "^2.0.1", "proxy-protocol-js": "^4.0.5" }, diff --git a/src/commons/envutil.js b/src/commons/envutil.js index b270992d3a..834fb3c31e 100644 --- a/src/commons/envutil.js +++ b/src/commons/envutil.js @@ -45,6 +45,11 @@ export function hasDisk() { return onFly() || onLocal(); } +export function useMmap() { + // got disk on fly and local deploys + return onFly() || onLocal(); +} + export function hasDynamicImports() { if (onDenoDeploy() || onCloudflare() || onFastly()) return false; return true; diff --git a/src/core/node/blocklists.js b/src/core/node/blocklists.js index 111914f53d..05407b3d88 100644 --- a/src/core/node/blocklists.js +++ b/src/core/node/blocklists.js @@ -10,10 +10,12 @@ 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 mmap from "mmap-utils"; const blocklistsDir = "./blocklists__"; const tdFile = "td.txt"; const rdFile = "rd.txt"; +const useMmap = true; export async function setup(bw) { if (!bw || !envutil.hasDisk()) return false; @@ -26,8 +28,9 @@ export async function setup(bw) { const tdparts = cfg.tdParts(); const tdcodec6 = cfg.tdCodec6(); const codec = tdcodec6 ? "u6" : "u8"; + const useMmap = envutil.useMmap(); - const ok = setupLocally(bw, timestamp, codec); + const ok = setupLocally(bw, timestamp, codec, useMmap); if (ok) { log.i("bl setup locally tstamp/nc", timestamp, nodecount); return true; @@ -64,15 +67,30 @@ function save(bw, timestamp, codec) { return true; } -function setupLocally(bw, timestamp, codec) { +// fmmap mmaps file at fp for random reads, returns a Buffer backed by the file. +function fmmap(fp) { + 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); +} + +function setupLocally(bw, timestamp, codec, 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 codec/td/rd", codec, td, rd, "mmap?", useMmap); - const tdbuf = fs.readFileSync(td); + let tdbuf = useMmap ? fmmap(td) : null; + if (bufutil.emptyBuf(tdbuf)) { + tdbuf = fs.readFileSync(td); + } const rdbuf = fs.readFileSync(rd); // TODO: file integrity checks From 6405fe4c124613b387cf3e966e2f2c779b00cb22 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Mon, 24 Jun 2024 18:43:53 +0530 Subject: [PATCH 013/126] node: rmv webpack for backend --- node.Dockerfile | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/node.Dockerfile b/node.Dockerfile index 590731b12f..7a21c8139d 100644 --- a/node.Dockerfile +++ b/node.Dockerfile @@ -5,10 +5,12 @@ WORKDIR /node-dir COPY . . # get deps, build, bundle RUN npm i -RUN npm run build:fly +# our webpack config yet cannot bundle native modules (mmap-utils) +# 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 +# RUN export BLOCKLIST_DOWNLOAD_ONLY=true && node ./dist/fly.mjs +RUN export BLOCKLIST_DOWNLOAD_ONLY=true && node ./src/server-node.js # stage 2 FROM node:alpine AS runner @@ -19,10 +21,11 @@ ENV NODE_ENV production ENV NODE_OPTIONS="--max-old-space-size=320 --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__ +# COPY --from=setup /node-dir/dist ./ +# COPY --from=setup /node-dir/blocklists__ ./blocklists__ +# COPY --from=setup /node-dir/dbip__ ./dbip__ +COPY --from=setup . . # print files in work dir, must contain blocklists RUN ls -Fla # run with the default entrypoint (usually, bash or sh) -CMD ["node", "./fly.mjs"] +CMD ["node", "./src/server-node.js"] From df8003e6d68ac66e41d0bdaeef6cac4bd3c0217f Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Mon, 24 Jun 2024 19:26:11 +0530 Subject: [PATCH 014/126] use mmap for node 22 --- node.Dockerfile | 3 ++- package.json | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/node.Dockerfile b/node.Dockerfile index 7a21c8139d..9fdd0faa03 100644 --- a/node.Dockerfile +++ b/node.Dockerfile @@ -13,7 +13,8 @@ RUN npm i RUN export BLOCKLIST_DOWNLOAD_ONLY=true && node ./src/server-node.js # stage 2 -FROM node:alpine AS runner +# pin to node22 for native deps (mmap-io) +FROM node:22-alpine AS runner # env vals persist even at run-time: archive.is/QpXp2 # and overrides fly.toml env values diff --git a/package.json b/package.json index 25eab949c5..c411a21b9d 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@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", - "mmap-utils": "^1.0.0", + "@riaskov/mmap-io": "^1.4.3", "node-polyfill-webpack-plugin": "^2.0.1", "proxy-protocol-js": "^4.0.5" }, @@ -50,8 +50,9 @@ "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": "^3.0.0" }, From 825df13fdda3312791128130673873a29ef9762d Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Mon, 24 Jun 2024 19:30:21 +0530 Subject: [PATCH 015/126] node: type assertions with type accessors --- src/core/cfg.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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; } From 08ddc4a26f98eb256e19ee58b9ae0a610a4e2f02 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Mon, 24 Jun 2024 19:31:37 +0530 Subject: [PATCH 016/126] node: use @aryaskov/mmap-io --- src/core/node/blocklists.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/node/blocklists.js b/src/core/node/blocklists.js index 05407b3d88..fd2f8ab2b1 100644 --- a/src/core/node/blocklists.js +++ b/src/core/node/blocklists.js @@ -10,7 +10,7 @@ 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 mmap from "mmap-utils"; +import mmap from "@riaskov/mmap-io"; const blocklistsDir = "./blocklists__"; const tdFile = "td.txt"; From 84fd119a0977b77e1d7021fc8ff5df248539f41a Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Mon, 24 Jun 2024 20:05:01 +0530 Subject: [PATCH 017/126] deno: v1.44.4 --- .github/workflows/deno-deploy.yml | 8 ++++---- deno.Dockerfile | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deno-deploy.yml b/.github/workflows/deno-deploy.yml index 8e4b9622b0..be9c4b8220 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,10 +85,10 @@ jobs: git reset git merge origin/${BUILD_BRANCH} || : - - name: πŸ¦• Install Deno @1.29 + - name: πŸ¦• Install Deno @1.44 uses: denoland/setup-deno@main with: - deno-version: 1.29.3 + deno-version: 1.44.4 - name: πŸ“¦ Bundle up if: ${{ env.DEPLOY_MODE == 'action' }} @@ -103,7 +103,7 @@ jobs: - name: 🀸🏼 Deploy to deno.com id: dd if: ${{ env.DEPLOY_MODE == 'action' }} - uses: denoland/deployctl@1.4.0 + uses: denoland/deployctl@1.20.0 with: project: ${{ env.PROJECT_NAME }} entrypoint: ${{ env.OUT_FILE }} 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 From 151827be1637d9fa3736cbe2ee7dd21e672c8485 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Mon, 24 Jun 2024 20:16:53 +0530 Subject: [PATCH 018/126] node: fix container cwd --- node.Dockerfile | 10 +++++----- webpack.fly.cjs | 4 ++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/node.Dockerfile b/node.Dockerfile index 9fdd0faa03..f6295bb683 100644 --- a/node.Dockerfile +++ b/node.Dockerfile @@ -1,7 +1,7 @@ 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 @@ -22,10 +22,10 @@ ENV NODE_ENV production ENV NODE_OPTIONS="--max-old-space-size=320 --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__ -COPY --from=setup . . +# COPY --from=setup /app/dist ./ +# COPY --from=setup /app/blocklists__ ./blocklists__ +# COPY --from=setup /app/dbip__ ./dbip__ +COPY --from=setup /app . # print files in work dir, must contain blocklists RUN ls -Fla # run with the default entrypoint (usually, bash or sh) diff --git a/webpack.fly.cjs b/webpack.fly.cjs index bd339b6fc4..ea2594625e 100644 --- a/webpack.fly.cjs +++ b/webpack.fly.cjs @@ -17,6 +17,10 @@ module.exports = { }), ], + module: { + rules: [{ test: /\.node$/, loader: "node-loader" }], + }, + optimization: { usedExports: true, minimize: false, From 0be6b391c67c2bf2ee120e34222d8dc7cef1656d Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Mon, 24 Jun 2024 20:18:16 +0530 Subject: [PATCH 019/126] gh-action: denoland/deployctl version --- .github/workflows/deno-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deno-deploy.yml b/.github/workflows/deno-deploy.yml index be9c4b8220..a09228871a 100644 --- a/.github/workflows/deno-deploy.yml +++ b/.github/workflows/deno-deploy.yml @@ -103,7 +103,7 @@ jobs: - name: 🀸🏼 Deploy to deno.com id: dd if: ${{ env.DEPLOY_MODE == 'action' }} - uses: denoland/deployctl@1.20.0 + uses: denoland/deployctl@1.12.0 with: project: ${{ env.PROJECT_NAME }} entrypoint: ${{ env.OUT_FILE }} From 3d9a50c5c3063ac4acad40d4e45ffd9878c78a59 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 25 Jun 2024 17:53:22 +0530 Subject: [PATCH 020/126] deno: import_map mmap-io --- import_map.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/import_map.json b/import_map.json index ac37fef4f6..47bb695696 100644 --- a/import_map.json +++ b/import_map.json @@ -6,6 +6,7 @@ "process": "https://deno.land/std@0.177.0/node/process.ts", "@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/trie/": "https://github.com/serverless-dns/trie/raw/v0.0.13/src/", + "@riaskov/mmap-io": "https://github.com/ARyaskov/mmap-io/raw/v1.4.3/src" } } From 204d94df8a975ca96d017fdadd20998dea25cc4c Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 25 Jun 2024 17:58:50 +0530 Subject: [PATCH 021/126] fly: webpack bundle target node 22 --- webpack.fly.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.fly.cjs b/webpack.fly.cjs index ea2594625e..3e31989c46 100644 --- a/webpack.fly.cjs +++ b/webpack.fly.cjs @@ -2,7 +2,7 @@ const webpack = require("webpack"); module.exports = { entry: "./src/server-node.js", - target: ["node", "es2022"], + target: ["node22", "es2022"], mode: "production", // enable devtool in development // devtool: 'eval-cheap-module-source-map', From b861b287eea04c77919bbb506f932020f962a368 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 25 Jun 2024 17:59:15 +0530 Subject: [PATCH 022/126] fly: webpack externalize native module mmap --- webpack.fly.cjs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/webpack.fly.cjs b/webpack.fly.cjs index 3e31989c46..427b82ab3e 100644 --- a/webpack.fly.cjs +++ b/webpack.fly.cjs @@ -17,9 +17,11 @@ module.exports = { }), ], - module: { - rules: [{ test: /\.node$/, loader: "node-loader" }], - }, + /* externalsType: 'module', + externals: { + '@riaskov/mmap-io': '@riaskov/mmap-io', + },*/ + externals: /@riaskov/, optimization: { usedExports: true, @@ -51,7 +53,8 @@ module.exports = { module: true, }, - /* or, cjs: stackoverflow.com/a/68916455 + // or, cjs: stackoverflow.com/a/68916455 + /* output: { filename: "fly.cjs", clean: true, // empty dist before output From c526a05ac35693c4b07137544cd5525dc9c5c014 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 25 Jun 2024 17:59:45 +0530 Subject: [PATCH 023/126] node: rmv unused var from blocklists.js --- src/core/node/blocklists.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/node/blocklists.js b/src/core/node/blocklists.js index fd2f8ab2b1..6cb7b3bfb3 100644 --- a/src/core/node/blocklists.js +++ b/src/core/node/blocklists.js @@ -15,7 +15,6 @@ import mmap from "@riaskov/mmap-io"; const blocklistsDir = "./blocklists__"; const tdFile = "td.txt"; const rdFile = "rd.txt"; -const useMmap = true; export async function setup(bw) { if (!bw || !envutil.hasDisk()) return false; From 5e32ac73d9e5e1289c5f67ab540b334c6da63508 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 25 Jun 2024 18:00:23 +0530 Subject: [PATCH 024/126] node: omit dev deps in docker --- node.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node.Dockerfile b/node.Dockerfile index f6295bb683..c852b28999 100644 --- a/node.Dockerfile +++ b/node.Dockerfile @@ -4,7 +4,7 @@ RUN apt-get update && apt-get install git -yq --no-install-suggests --no-install WORKDIR /app COPY . . # get deps, build, bundle -RUN npm i +RUN npm i --omit=dev # our webpack config yet cannot bundle native modules (mmap-utils) # RUN npm run build:fly # or RUN npx webpack --config webpack.fly.cjs From 9bc73b346462dec250bcad75a749bd5013d20dae Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 25 Jun 2024 18:19:46 +0530 Subject: [PATCH 025/126] fly: bundle node_modules instead of copying brings down image size down from 900+mb (400mb+ due to node_modules, mostly devDependencies) to 300mb+ --- node.Dockerfile | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/node.Dockerfile b/node.Dockerfile index c852b28999..dc87de6516 100644 --- a/node.Dockerfile +++ b/node.Dockerfile @@ -5,15 +5,15 @@ WORKDIR /app COPY . . # get deps, build, bundle RUN npm i --omit=dev -# our webpack config yet cannot bundle native modules (mmap-utils) -# RUN npm run build:fly +# 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 -RUN export BLOCKLIST_DOWNLOAD_ONLY=true && node ./src/server-node.js +RUN export BLOCKLIST_DOWNLOAD_ONLY=true && node ./dist/fly.mjs +# or RUN export BLOCKLIST_DOWNLOAD_ONLY=true && node ./src/server-node.js # stage 2 -# pin to node22 for native deps (mmap-io) +# 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 @@ -22,11 +22,14 @@ ENV NODE_ENV production ENV NODE_OPTIONS="--max-old-space-size=320 --heapsnapshot-signal=SIGUSR2" # get working dir in order WORKDIR /app -# COPY --from=setup /app/dist ./ -# COPY --from=setup /app/blocklists__ ./blocklists__ -# COPY --from=setup /app/dbip__ ./dbip__ -COPY --from=setup /app . +# 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) -CMD ["node", "./src/server-node.js"] +CMD ["node", "./dist/fly.mjs"] From f9ef39079c718a5b6e264a959d8b94d4f1f1b7b4 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 25 Jun 2024 18:24:29 +0530 Subject: [PATCH 026/126] fly: do not omit-dev in setup (dep: webpack) --- node.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node.Dockerfile b/node.Dockerfile index dc87de6516..8ff38daf43 100644 --- a/node.Dockerfile +++ b/node.Dockerfile @@ -4,7 +4,7 @@ RUN apt-get update && apt-get install git -yq --no-install-suggests --no-install WORKDIR /app COPY . . # get deps, build, bundle -RUN npm i --omit=dev +RUN npm i # webpack externalizes native modules (@riaskov/mmap-io) RUN npm run build:fly # or RUN npx webpack --config webpack.fly.cjs From 8b4623867bad42afa6c390abb8fab232f2c39d3f Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 25 Jun 2024 18:45:05 +0530 Subject: [PATCH 027/126] fly: fix entrypoint --- node.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node.Dockerfile b/node.Dockerfile index 8ff38daf43..0680f23584 100644 --- a/node.Dockerfile +++ b/node.Dockerfile @@ -32,4 +32,4 @@ 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) -CMD ["node", "./dist/fly.mjs"] +CMD ["node", "./fly.mjs"] From 76eb69f1955bad3b9c1448698f3932e069fe595a Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 27 Jun 2024 20:41:31 +0530 Subject: [PATCH 028/126] gh-action: ghcr for node-alpine mk1 --- .github/workflows/ghcr.yml | 60 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/ghcr.yml diff --git a/.github/workflows/ghcr.yml b/.github/workflows/ghcr.yml new file mode 100644 index 0000000000..c3b5b3d733 --- /dev/null +++ b/.github/workflows/ghcr.yml @@ -0,0 +1,60 @@ +name: πŸ”„ runc + +on: + push: + tags: + - "v*" + workflow_dispatch: + +env: + REGISTRY: "ghcr.io" + IMAGE_NAME: ${{ github.repository }} + 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 From d4772c79300bea9a59d1d3a8c88d52b9afed9b10 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 24 Sep 2024 20:52:40 +0530 Subject: [PATCH 029/126] util: always clear timeouts on errors --- src/commons/util.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commons/util.js b/src/commons/util.js index 05cbafdb23..95517150c1 100644 --- a/src/commons/util.js +++ b/src/commons/util.js @@ -142,6 +142,7 @@ export function timedOp(op, ms, cleanup = () => {}) { } }); } catch (e) { + clearTimeout(tid); if (!timedout) reject(e); } }); @@ -180,6 +181,7 @@ export function timedSafeAsyncOp(promisedOp, ms, defaultOp) { } }) .catch((ignored) => { + clearTimeout(tid); if (!timedout) deferredOp(); // else: handled by timeout }); From bf96425b37132143f8a85b176516a12132b47a89 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 24 Sep 2024 20:54:38 +0530 Subject: [PATCH 030/126] log: rmv log timers post arb delay (2m) to avoid leak --- src/commons/util.js | 7 +++++++ src/core/log.js | 28 +++++++++++++++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/commons/util.js b/src/commons/util.js index 95517150c1..405baf2f18 100644 --- a/src/commons/util.js +++ b/src/commons/util.js @@ -519,6 +519,13 @@ export function stub(...args) { }; } +export function stubr(r, ...args) { + return (...args) => { + /* no-op */ + return r; + }; +} + export function stubAsync(...args) { return async (...args) => { /* no-op */ diff --git a/src/core/log.js b/src/core/log.js index 7a22b580a8..12d8f32ee7 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 { uid, stub, stubr } from "../commons/util.js"; /** * @typedef {'error'|'logpush'|'warn'|'info'|'timer'|'debug'} LogLevels @@ -83,8 +83,8 @@ export default class Log { this.d = stub(); this.debug = stub(); this.lapTime = stub(); - this.startTime = stub(); - this.endTime = stub(); + this.startTime = stubr(""); + this.endTime = stubr(false); this.i = stub(); this.info = stub(); this.w = stub(); @@ -97,16 +97,22 @@ export default class Log { const that = this; return { lapTime: (n, ...r) => { + // returns void return that.lapTime(n, ...tags, ...r); }, startTime: (n, ...r) => { const tid = that.startTime(n); that.d(that.now() + " T", ...tags, "create", tid, ...r); + const tim = setTimeout(() => { + that.endTime(tid); + }, /* 2mins*/ 2 * 60 * 1000); + if (typeof tim.unref === "function") tim.unref(); return tid; }, endTime: (n, ...r) => { - that.d(that.now() + " T", ...tags, "end", n, ...r); - return that.endTime(n); + if (that.endTime(n)) { + that.d(that.now() + " T", ...tags, "end", n, ...r); + } // else: already ended or invalid timer }, d: (...args) => { that.d(that.now() + " D", ...tags, ...args); @@ -163,13 +169,21 @@ 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. + // stub() for Fastly as it does not support console timers. + this.lapTime = console.timeLog || stub(); 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. + // stub() for Fastly as it does not support console timers. + this.endTime = function (uid) { + try { + if (console.timeEnd) console.timeEnd(uid); + return true; + } catch (ignore) {} + return false; + }; case "info": this.i = console.info; this.info = console.info; From b4c13cbcd2d34651130fbe570237c6c397d03f4a Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 24 Sep 2024 20:56:36 +0530 Subject: [PATCH 031/126] node: add on demand heap measurements --- src/server-node.js | 48 ++++++++++++++++------------------------------ 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index 379cdc3473..c47cb307f4 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -24,9 +24,6 @@ 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"; /** * @typedef {net.Socket} Socket @@ -50,6 +47,7 @@ class Stats { this.nofconns = 0; this.openconns = 0; this.noftimeouts = 0; + this.nofheapsnaps = 0; // avg1, avg5, avg15, adj, maxconns this.bp = [0, 0, 0, 0, 0]; } @@ -169,9 +167,8 @@ const tracker = new Tracker(); const stats = new Stats(); const cpucount = os.cpus().length || 1; const adjPeriodSec = 5; +const maxHeapSnaps = 10; let adjTimer = null; -/** @type {memwatch.HeapDiff} */ -let heapdiff = null; ((main) => { // listen for "go" and start the server @@ -340,7 +337,6 @@ function systemUp() { trapServerEvents(hcheck); }); - // if (envutil.measureHeap()) heapdiff = new memwatch.HeapDiff(); heartbeat(); } @@ -1061,6 +1057,7 @@ function trapRequestResponseEvents(req, res) { } function heartbeat() { + const isNode = envutil.isNode(); const maxc = envutil.maxconns(); const minc = envutil.minconns(); const measureHeap = envutil.measureHeap(); @@ -1068,18 +1065,19 @@ function heartbeat() { // increment no of requests stats.noreqs += 1; - if (!measureHeap) { - endHeapDiffIfNeeded(heapdiff); - heapdiff = null; - } else if (heapdiff == null) { - // heapdiff = new memwatch.HeapDiff(); - } else if (stats.noreqs % (maxc * 10) === 0) { - endHeapDiffIfNeeded(heapdiff); - // heapdiff = new memwatch.HeapDiff(); - } if (stats.noreqs % (minc * 2) === 0) { log.i(stats.str(), "in", (uptime() / 60000) | 0, "mins"); } + + if (measureHeap && stats.noreqs !== 0 && stats.noreqs % (maxc * 10) === 0) { + if (isNode && stats.nofheapsnaps < maxHeapSnaps) { + stats.nofheapsnaps += 1; + const nom = + "snap" + stats.nofheapsnaps + "." + stats.noreqs + ".heapsnapshot"; + log.i("heap snapshot #", stats.nofheapsnaps, nom); + v8.writeHeapSnapshot(nom); + } + } } function adjustMaxConns(n) { @@ -1160,27 +1158,13 @@ function adjustMaxConns(n) { } } -/** - * @param {memwatch.HeapDiff} h - * @returns void - */ -function endHeapDiffIfNeeded(h) { - // disabled; memwatch is not bundled due to a webpack bug - if (!h || true) return; - try { - const diff = h.end(); - log.i("heap before", diff.before); - log.i("heap after", diff.after); - log.i("heap details", diff.change.details); - } catch (ex) { - log.w("heap-diff err", ex.message); - } -} - function bye() { // in some cases, node stops listening but the process doesn't exit because // of other unreleased resources (see: svc.js#systemStop); and so exit with // success (exit code 0) regardless; ref: community.fly.io/t/4547/6 console.warn("W game over"); + + if (envutil.isNode()) v8.writeHeapSnapshot("snap.end.heapsnapshot"); + process.exit(0); } From 051b8d2557d614ad5a8d4184e41e39464d3a7cd3 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 24 Sep 2024 21:59:40 +0530 Subject: [PATCH 032/126] env: measure heap in prod for select Fly regions --- src/commons/envutil.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/commons/envutil.js b/src/commons/envutil.js index 834fb3c31e..f308a5128d 100644 --- a/src/commons/envutil.js +++ b/src/commons/envutil.js @@ -227,12 +227,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" || From 9e0b52bd9f21d03cc58be60bd3e9005478492bb1 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 24 Sep 2024 22:00:07 +0530 Subject: [PATCH 033/126] node: incr total heapsnapshots from 10 to 20 --- src/server-node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server-node.js b/src/server-node.js index c47cb307f4..92c5244976 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -167,7 +167,7 @@ const tracker = new Tracker(); const stats = new Stats(); const cpucount = os.cpus().length || 1; const adjPeriodSec = 5; -const maxHeapSnaps = 10; +const maxHeapSnaps = 20; let adjTimer = null; ((main) => { From 356beac548c37c3192bab0807e7ff36601ee44c2 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 24 Sep 2024 22:01:05 +0530 Subject: [PATCH 034/126] node: measure heap on low ram --- src/server-node.js | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index 92c5244976..301141c9f1 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -1057,10 +1057,7 @@ function trapRequestResponseEvents(req, res) { } function heartbeat() { - const isNode = envutil.isNode(); - const maxc = envutil.maxconns(); const minc = envutil.minconns(); - const measureHeap = envutil.measureHeap(); // increment no of requests stats.noreqs += 1; @@ -1068,22 +1065,13 @@ function heartbeat() { if (stats.noreqs % (minc * 2) === 0) { log.i(stats.str(), "in", (uptime() / 60000) | 0, "mins"); } - - if (measureHeap && stats.noreqs !== 0 && stats.noreqs % (maxc * 10) === 0) { - if (isNode && stats.nofheapsnaps < maxHeapSnaps) { - stats.nofheapsnaps += 1; - const nom = - "snap" + stats.nofheapsnaps + "." + stats.noreqs + ".heapsnapshot"; - log.i("heap snapshot #", stats.nofheapsnaps, nom); - v8.writeHeapSnapshot(nom); - } - } } function adjustMaxConns(n) { const isNode = envutil.isNode(); const maxc = envutil.maxconns(); const minc = envutil.minconns(); + const measureHeap = envutil.measureHeap(); const adjsPerSec = 60 / adjPeriodSec; // caveats: @@ -1095,6 +1083,11 @@ function adjustMaxConns(n) { avg5 = ((avg5 * 100) / cpucount) | 0; avg15 = ((avg15 * 100) / cpucount) | 0; + const freemem = os.freemem(); + const totmem = os.totalmem(); + const lowram = freemem < 0.1 * totmem; + const verylowram = freemem < 0.025 * totmem; + let adj = stats.bp[3] || 0; // increase in load if (avg5 > 90) { @@ -1109,7 +1102,7 @@ function adjustMaxConns(n) { n = maxc; if (avg1 > 100) { n = minc; - } else if (avg1 > 90 || avg5 > 80) { + } else if (avg1 > 90 || avg5 > 80 || lowram) { n = Math.max((n * 0.2) | 0, minc); } else if (avg1 > 80 || avg5 > 75) { n = Math.max((n * 0.4) | 0, minc); @@ -1133,8 +1126,8 @@ function adjustMaxConns(n) { const breakpoint = 6 * adjsPerSec; // 6 mins const stresspoint = 4 * adjsPerSec; // 4 mins const nstr = stats.openconns + "/" + n; - if (adj > breakpoint) { - log.w("load: stopping; n:", nstr, "adjs:", adj); + if (adj > breakpoint || verylowram) { + log.w("load: stopping lowram?", verylowram, "; n:", nstr, "adjs:", adj); stopAfter(0); return; } else if (adj > stresspoint) { @@ -1144,6 +1137,12 @@ function adjustMaxConns(n) { log.d("load: high; n:", nstr, "adjs:", adj, "avgs:", avg1, avg5, avg15); } + stats.bp = [avg1, avg5, avg15, adj, n]; + for (const s of tracker.servers()) { + if (!s || !s.listening) continue; + s.maxConnections = n; + } + // nodejs.org/en/docs/guides/diagnostics/memory/using-gc-traces if (adj > 0) { if (isNode) v8.setFlagsFromString("--trace-gc"); @@ -1151,10 +1150,15 @@ function adjustMaxConns(n) { if (isNode) v8.setFlagsFromString("--notrace-gc"); } - stats.bp = [avg1, avg5, avg15, adj, n]; - for (const s of tracker.servers()) { - if (!s || !s.listening) continue; - s.maxConnections = n; + const ramthres = freemem < 0.2 * totmem; + const reqthres = stats.noreqs > 0 && stats.noreqs % (maxc * 10) === 0; + const withinLimit = stats.nofheapsnaps < maxHeapSnaps; + if (isNode && measureHeap && withinLimit && reqthres && ramthres) { + stats.nofheapsnaps += 1; + const nom = "s" + stats.nofheapsnaps + "." + stats.noreqs + ".heapsnapshot"; + log.i("heap snapshot #", stats.nofheapsnaps, nom); + // nodejs.org/en/learn/diagnostics/memory/using-heap-snapshot + v8.writeHeapSnapshot(nom); // blocks event loop! } } From 472bc8c09102adddfbe531a0b80803f8a0828e7a Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 24 Sep 2024 22:12:24 +0530 Subject: [PATCH 035/126] node: log elapsed time to write a heapsnapshot --- src/server-node.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server-node.js b/src/server-node.js index 301141c9f1..b1de922cf9 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -1156,9 +1156,11 @@ function adjustMaxConns(n) { if (isNode && measureHeap && withinLimit && reqthres && ramthres) { stats.nofheapsnaps += 1; const nom = "s" + stats.nofheapsnaps + "." + stats.noreqs + ".heapsnapshot"; - log.i("heap snapshot #", stats.nofheapsnaps, nom); + const start = Date.now(); // nodejs.org/en/learn/diagnostics/memory/using-heap-snapshot v8.writeHeapSnapshot(nom); // blocks event loop! + const end = Date.now(); + log.i("heap snapshot #", stats.nofheapsnaps, nom, "in", end - start, "ms"); } } From 99898d50a0955bff5226d68c3b791123bfaf912e Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 25 Sep 2024 05:15:17 +0530 Subject: [PATCH 036/126] dnsop/resolver: m warn log --- src/plugins/dns-op/resolver.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/dns-op/resolver.js b/src/plugins/dns-op/resolver.js index edf04cae9a..e9f6af0191 100644 --- a/src/plugins/dns-op/resolver.js +++ b/src/plugins/dns-op/resolver.js @@ -232,6 +232,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,8 +252,8 @@ 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(); From d336c4642979f26796e0806437881a4294b165ea Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 25 Sep 2024 05:16:22 +0530 Subject: [PATCH 037/126] node: lower heap snapshot thresholds for local env --- src/server-node.js | 60 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index b1de922cf9..855d925d47 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -1058,20 +1058,40 @@ function trapRequestResponseEvents(req, res) { function heartbeat() { const minc = envutil.minconns(); - + const maxc = envutil.maxconns(); + const isNode = envutil.isNode(); + const notCloud = envutil.onLocal(); + const measureHeap = envutil.measureHeap(); + const freemem = os.freemem() / (1024 * 1024); // in mb + const totmem = os.totalmem() / (1024 * 1024); // in mb // increment no of requests stats.noreqs += 1; if (stats.noreqs % (minc * 2) === 0) { log.i(stats.str(), "in", (uptime() / 60000) | 0, "mins"); } + + const mul = notCloud ? 2 : 10; + const writeSnap = notCloud || measureHeap; + const ramthres = notCloud || freemem < 0.2 * totmem; + const reqthres = stats.noreqs > 0 && stats.noreqs % (maxc * mul) === 0; + const withinLimit = stats.nofheapsnaps < maxHeapSnaps; + if (isNode && writeSnap && withinLimit && reqthres && ramthres) { + stats.nofheapsnaps += 1; + const n = "s" + stats.nofheapsnaps + "." + stats.noreqs + ".heapsnapshot"; + const start = Date.now(); + // nodejs.org/en/learn/diagnostics/memory/using-heap-snapshot + v8.writeHeapSnapshot(n); // blocks event loop! + const elapsed = (Date.now() - start) / 1000; + log.i("heap snapshot #", stats.nofheapsnaps, n, "in", elapsed, "s"); + } } function adjustMaxConns(n) { const isNode = envutil.isNode(); + const notCloud = envutil.onLocal(); const maxc = envutil.maxconns(); const minc = envutil.minconns(); - const measureHeap = envutil.measureHeap(); const adjsPerSec = 60 / adjPeriodSec; // caveats: @@ -1083,8 +1103,8 @@ function adjustMaxConns(n) { avg5 = ((avg5 * 100) / cpucount) | 0; avg15 = ((avg15 * 100) / cpucount) | 0; - const freemem = os.freemem(); - const totmem = os.totalmem(); + const freemem = os.freemem() / (1024 * 1024); // in mb + const totmem = os.totalmem() / (1024 * 1024); // in mb const lowram = freemem < 0.1 * totmem; const verylowram = freemem < 0.025 * totmem; @@ -1126,14 +1146,31 @@ function adjustMaxConns(n) { const breakpoint = 6 * adjsPerSec; // 6 mins const stresspoint = 4 * adjsPerSec; // 4 mins const nstr = stats.openconns + "/" + n; - if (adj > breakpoint || verylowram) { + if (adj > breakpoint || (verylowram && !notCloud)) { + log.w("load: verylowram! freemem:", freemem, "totmem:", totmem); log.w("load: stopping lowram?", verylowram, "; n:", nstr, "adjs:", adj); stopAfter(0); return; } else if (adj > stresspoint) { + log.w( + "load: stress; lowram?", + lowram, + "freemem:", + freemem, + "totmem:", + totmem + ); log.w("load: stress; n:", nstr, "adjs:", adj, "avgs:", avg1, avg5, avg15); n = (minc / 2) | 0; } else if (adj > 0) { + log.d( + "load: high; lowram?", + lowram, + "freemem:", + freemem, + "totmem:", + totmem + ); log.d("load: high; n:", nstr, "adjs:", adj, "avgs:", avg1, avg5, avg15); } @@ -1149,19 +1186,6 @@ function adjustMaxConns(n) { } else { if (isNode) v8.setFlagsFromString("--notrace-gc"); } - - const ramthres = freemem < 0.2 * totmem; - const reqthres = stats.noreqs > 0 && stats.noreqs % (maxc * 10) === 0; - const withinLimit = stats.nofheapsnaps < maxHeapSnaps; - if (isNode && measureHeap && withinLimit && reqthres && ramthres) { - stats.nofheapsnaps += 1; - const nom = "s" + stats.nofheapsnaps + "." + stats.noreqs + ".heapsnapshot"; - const start = Date.now(); - // nodejs.org/en/learn/diagnostics/memory/using-heap-snapshot - v8.writeHeapSnapshot(nom); // blocks event loop! - const end = Date.now(); - log.i("heap snapshot #", stats.nofheapsnaps, nom, "in", end - start, "ms"); - } } function bye() { From f4452565c77d75b78b9af836c5a5da05468bba1c Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 25 Sep 2024 05:48:55 +0530 Subject: [PATCH 038/126] fly: disable swap, enable auto suspend --- fly.tls.toml | 5 +++-- fly.toml | 8 +++++--- node.Dockerfile | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/fly.tls.toml b/fly.tls.toml index b2fbd69fc4..098b00b126 100644 --- a/fly.tls.toml +++ b/fly.tls.toml @@ -24,7 +24,8 @@ auto_rollback = true [[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 +58,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 38fc89f208..fb98996262 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" @@ -21,7 +23,7 @@ swap_size_mb = 152 [[services]] protocol = "tcp" internal_port = 8080 - auto_stop_machines = true + auto_stop_machines = "suspend" auto_start_machines = true [[services.ports]] @@ -50,7 +52,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]] diff --git a/node.Dockerfile b/node.Dockerfile index 0680f23584..6f91660e13 100644 --- a/node.Dockerfile +++ b/node.Dockerfile @@ -19,7 +19,7 @@ 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 # external deps not bundled by webpack From 68597f2aaff96a7025f5da41e8081e981f51bec4 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 25 Sep 2024 05:54:47 +0530 Subject: [PATCH 039/126] plugin: rmv log timers for execute() --- src/core/plugin.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/core/plugin.js b/src/core/plugin.js index 12c6bdbeea..b12e4c6fbf 100644 --- a/src/core/plugin.js +++ b/src/core/plugin.js @@ -142,10 +142,8 @@ 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"); + // const t = this.log.startTime("exec-plugin-" + rxid); for (const p of this.plugin) { if (io.stopProcessing && !p.continueOnStopProcess) { continue; @@ -154,19 +152,16 @@ export default class RethinkPlugin { continue; } - this.log.lapTime(t, rxid, p.name, "send-io"); - + // 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"); + // 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.lapTime(t, rxid, p.name, "post-callback"); } - this.log.endTime(t); + // this.log.endTime(t); } /** From 716f21bbe7d1bc40ad1d1256906f9e46b5d6fa30 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 25 Sep 2024 06:29:29 +0530 Subject: [PATCH 040/126] fly: rmv non-functional swapon --- src/core/linux/swap.js | 65 ----------------------------------------- src/core/node/config.js | 10 ++----- 2 files changed, 3 insertions(+), 72 deletions(-) delete mode 100644 src/core/linux/swap.js 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/node/config.js b/src/core/node/config.js index 59efdfa4fa..d1b7e9bc26 100644 --- a/src/core/node/config.js +++ b/src/core/node/config.js @@ -20,7 +20,6 @@ import Log from "../log.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"; // some of the cjs node globals aren't available in esm @@ -121,14 +120,11 @@ async function prep() { // 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); + if (onFly) { + // recursive resolver on Fly + // swapon won't work on fly: community.fly.io/t/19196/13 dns53 = dnst.makeTransport(plainOldDnsIp); log.i("imported udp/tcp dns transport", plainOldDnsIp); - } else { - log.i("no swap required"); } /** signal ready */ From e5246c0223eddf5d3d5e6feda7a22372522b5e7d Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 25 Sep 2024 07:01:43 +0530 Subject: [PATCH 041/126] fly: restart policy; on-fail --- fly.tls.toml | 6 ++++++ fly.toml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/fly.tls.toml b/fly.tls.toml index 098b00b126..592e042ea9 100644 --- a/fly.tls.toml +++ b/fly.tls.toml @@ -20,6 +20,12 @@ 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-fail" + retries = 3 + # DNS over HTTPS (well, h2c and http1.1) [[services]] internal_port = 8055 diff --git a/fly.toml b/fly.toml index fb98996262..d4f921fe54 100644 --- a/fly.toml +++ b/fly.toml @@ -19,6 +19,12 @@ kill_timeout = "15s" NODE_ENV = "production" LOG_LEVEL = "info" +# community.fly.io/t/19180 +# fly.io/docs/machines/guides-examples/machine-restart-policy +[[restart]] + policy = "on-fail" + retries = 3 + # DNS over HTTPS [[services]] protocol = "tcp" From bf3443873a7999afe1441858cbe8d6f4388c2e47 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 25 Sep 2024 09:24:46 +0530 Subject: [PATCH 042/126] plugins: do not err out user-op on malformed blockstamp --- src/plugins/users/user-op.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/users/user-op.js b/src/plugins/users/user-op.js index 6f879311ad..6259d82ff5 100644 --- a/src/plugins/users/user-op.js +++ b/src/plugins/users/user-op.js @@ -48,7 +48,7 @@ export class UserOp { * @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"); @@ -84,7 +84,8 @@ export class UserOp { response.data.dnsResolverUrl = null; } 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; From 84a7ee12969c9384203b12d26eca43724b442d4e Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 25 Sep 2024 21:06:36 +0530 Subject: [PATCH 043/126] all: m jsdoc --- src/commons/dnsutil.js | 9 ++ src/core/plugin.js | 2 +- src/plugins/dns-op/blocker.js | 18 +++ src/plugins/dns-op/cache-resolver.js | 12 ++ src/plugins/dns-op/resolver.js | 2 +- src/plugins/plugin-response.js | 23 +++- src/plugins/rdns-util.js | 176 +++++++++++++++++---------- 7 files changed, 175 insertions(+), 67 deletions(-) diff --git a/src/commons/dnsutil.js b/src/commons/dnsutil.js index 4449b6e4ee..c40fab6020 100644 --- a/src/commons/dnsutil.js +++ b/src/commons/dnsutil.js @@ -377,9 +377,14 @@ export function isAnswerQuad0(packet) { return isAnswerBlocked(packet.answers); } +/** + * @param {any} dnsPacket + * @returns {string[]} + */ export function extractDomains(dnsPacket) { if (!hasSingleQuestion(dnsPacket)) return []; + /** @type {string} */ const names = new Set(); const answers = dnsPacket.answers; @@ -535,6 +540,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/core/plugin.js b/src/core/plugin.js index b12e4c6fbf..ec39e20f1b 100644 --- a/src/core/plugin.js +++ b/src/core/plugin.js @@ -182,7 +182,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/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..41617f68ac 100644 --- a/src/plugins/dns-op/cache-resolver.js +++ b/src/plugins/dns-op/cache-resolver.js @@ -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 @@ -101,6 +107,12 @@ 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); diff --git a/src/plugins/dns-op/resolver.js b/src/plugins/dns-op/resolver.js index e9f6af0191..6d85529933 100644 --- a/src/plugins/dns-op/resolver.js +++ b/src/plugins/dns-op/resolver.js @@ -137,7 +137,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 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..f1c1f89873 100644 --- a/src/plugins/rdns-util.js +++ b/src/plugins/rdns-util.js @@ -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,46 @@ recBlockstamps.set("pr", "1:eMYB-ACgAQAgARAwIABhUgCA"); // pec, sec recBlockstamps.set("ps", "1:GNwB-ACgeQKr7cg3YXoAgA=="); +/** + * @param {BlocklistFilter} blf + * @returns {boolean} + */ export function isBlocklistFilterSetup(blf) { return blf && !util.emptyObj(blf.ftrie); } +/** + * @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 +102,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 +126,7 @@ export function blockstampFromCache(cr) { } /** - * @param {*} dnsPacket + * @param {any} dnsPacket * @param {BlocklistFilter} blocklistFilter * @returns {pres.BStamp|boolean} */ @@ -156,7 +151,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 +172,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 +183,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 +202,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 +266,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 +344,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 +391,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 +403,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 +451,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 +481,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,13 +508,21 @@ export function hasBlockstamp(blockInfo) { ); } -// returns true if tstamp is of form yyyy/epochMs +/** + * returns true if tstamp is of form yyyy/epochMs + * @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 +/** + * from: github.com/celzero/downloads/blob/main/src/timestamp.js + * @param {string} tstamp + * @returns {int} epoch + */ export function bareTimestampFrom(tstamp) { // strip out "/" if tstamp is of form yyyy/epochMs if (isValidFullTimestamp(tstamp)) { @@ -490,6 +536,10 @@ export function bareTimestampFrom(tstamp) { return t; } +/** + * @param {string} strflag + * @returns {string[]} blocklist names + */ export function blocklists(strflag) { const { userBlocklistFlagUint, flagVersion } = unstamp(strflag); const blocklists = []; From 151af4c001ae033b9761112f71736c7ecd799235 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 25 Sep 2024 22:39:05 +0530 Subject: [PATCH 044/126] dns-op: always delegate ipv4only.arpa to doh upstreams --- src/core/plugin.js | 2 +- src/plugins/dns-op/prefilter.js | 3 ++- src/plugins/dns-op/resolver.js | 3 ++- src/plugins/users/user-op.js | 42 +++++++++++++++++++++++---------- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/core/plugin.js b/src/core/plugin.js index ec39e20f1b..9be0d79021 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 ); 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 6d85529933..20dfe3cc98 100644 --- a/src/plugins/dns-op/resolver.js +++ b/src/plugins/dns-op/resolver.js @@ -151,6 +151,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) @@ -212,7 +213,7 @@ export default class DNSResolver { this.resolveDnsUpstream( rxid, req, - this.determineDohResolvers(userDns), + this.determineDohResolvers(userDns, forceUserDns), rawpacket, decodedpacket ), diff --git a/src/plugins/users/user-op.js b/src/plugins/users/user-op.js index 6259d82ff5..1012113dbf 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) { @@ -56,18 +61,28 @@ export class UserOp { } try { - const blocklistFlag = rdnsutil.blockstampFromUrl(ctx.request.url); + 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,13 +90,14 @@ 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); // avoid erroring out on invalid blocklist info & flag From b032f571e43e132787de11abfeb1eb392928c025 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 25 Sep 2024 22:51:45 +0530 Subject: [PATCH 045/126] node: m logs fmt --- src/server-node.js | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index 855d925d47..66cc421d8b 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -1152,25 +1152,11 @@ function adjustMaxConns(n) { stopAfter(0); return; } else if (adj > stresspoint) { - log.w( - "load: stress; lowram?", - lowram, - "freemem:", - freemem, - "totmem:", - totmem - ); + log.w("load: stress; lowram?", lowram, "mem:", freemem, " / ", totmem); log.w("load: stress; n:", nstr, "adjs:", adj, "avgs:", avg1, avg5, avg15); n = (minc / 2) | 0; } else if (adj > 0) { - log.d( - "load: high; lowram?", - lowram, - "freemem:", - freemem, - "totmem:", - totmem - ); + log.d("load: high; lowram?", lowram, "mem:", freemem, " / ", totmem); log.d("load: high; n:", nstr, "adjs:", adj, "avgs:", avg1, avg5, avg15); } From 9cec6c2ce7c7315ede376b0ee3ee29d44449e87c Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 25 Sep 2024 22:53:58 +0530 Subject: [PATCH 046/126] fly: m fix invalid restart policy --- fly.tls.toml | 2 +- fly.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fly.tls.toml b/fly.tls.toml index 592e042ea9..1e3b420251 100644 --- a/fly.tls.toml +++ b/fly.tls.toml @@ -23,7 +23,7 @@ auto_rollback = true # community.fly.io/t/19180 # fly.io/docs/machines/guides-examples/machine-restart-policy [[restart]] - policy = "on-fail" + policy = "on-failure" retries = 3 # DNS over HTTPS (well, h2c and http1.1) diff --git a/fly.toml b/fly.toml index d4f921fe54..e548706e8e 100644 --- a/fly.toml +++ b/fly.toml @@ -22,7 +22,7 @@ kill_timeout = "15s" # community.fly.io/t/19180 # fly.io/docs/machines/guides-examples/machine-restart-policy [[restart]] - policy = "on-fail" + policy = "on-failure" retries = 3 # DNS over HTTPS From b9735502a7ccd9df9e32c36d886f09076579cc9c Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 25 Sep 2024 23:34:29 +0530 Subject: [PATCH 047/126] user-op: m assign missing var --- src/plugins/users/user-op.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/users/user-op.js b/src/plugins/users/user-op.js index 1012113dbf..3247072af0 100644 --- a/src/plugins/users/user-op.js +++ b/src/plugins/users/user-op.js @@ -49,7 +49,7 @@ 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) { @@ -61,6 +61,7 @@ export class UserOp { } try { + const dnsPacket = ctx.requestDecodedDnsPacket; const domains = dnsutil.extractDomains(dnsPacket); for (const d of domains) { if (delegated.has(d)) { From a17874230e589dc536e30e5cc0b9a08c8b6c74cd Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 25 Sep 2024 23:34:48 +0530 Subject: [PATCH 048/126] all: rmv log timers --- src/core/log.js | 37 +--------------------------------- src/core/node/dns-transport.js | 13 ------------ src/core/plugin.js | 5 ----- src/server-node.js | 17 ---------------- 4 files changed, 1 insertion(+), 71 deletions(-) diff --git a/src/core/log.js b/src/core/log.js index 12d8f32ee7..13df003294 100644 --- a/src/core/log.js +++ b/src/core/log.js @@ -82,9 +82,6 @@ export default class Log { _resetLevel() { this.d = stub(); this.debug = stub(); - this.lapTime = stub(); - this.startTime = stubr(""); - this.endTime = stubr(false); this.i = stub(); this.info = stub(); this.w = stub(); @@ -96,24 +93,6 @@ export default class Log { withTags(...tags) { const that = this; return { - lapTime: (n, ...r) => { - // returns void - return that.lapTime(n, ...tags, ...r); - }, - startTime: (n, ...r) => { - const tid = that.startTime(n); - that.d(that.now() + " T", ...tags, "create", tid, ...r); - const tim = setTimeout(() => { - that.endTime(tid); - }, /* 2mins*/ 2 * 60 * 1000); - if (typeof tim.unref === "function") tim.unref(); - return tid; - }, - endTime: (n, ...r) => { - if (that.endTime(n)) { - that.d(that.now() + " T", ...tags, "end", n, ...r); - } // else: already ended or invalid timer - }, d: (...args) => { that.d(that.now() + " D", ...tags, ...args); }, @@ -169,21 +148,7 @@ export default class Log { this.d = console.debug; this.debug = console.debug; case "timer": - // stub() for Fastly as it does not support console timers. - this.lapTime = console.timeLog || stub(); - this.startTime = function (name) { - name = uid(name); - if (console.time) console.time(name); - return name; - }; - // stub() for Fastly as it does not support console timers. - this.endTime = function (uid) { - try { - if (console.timeEnd) console.timeEnd(uid); - return true; - } catch (ignore) {} - return false; - }; + // deprecated; fallthrough case "info": this.i = console.info; this.info = console.info; diff --git a/src/core/node/dns-transport.js b/src/core/node/dns-transport.js index 12ff0067ae..a351d21092 100644 --- a/src/core/node/dns-transport.js +++ b/src/core/node/dns-transport.js @@ -49,22 +49,15 @@ export class Transport { let sock = this.udpconns.take(); this.log.d(rxid, "udp pooled?", sock !== null); - const t = this.log.startTime("udp-query"); 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; } @@ -72,21 +65,15 @@ export class Transport { let sock = this.tcpconns.take(); this.log.d(rxid, "tcp pooled?", sock !== null); - const t = this.log.startTime("tcp-query"); 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; } diff --git a/src/core/plugin.js b/src/core/plugin.js index 9be0d79021..a64d05dd8e 100644 --- a/src/core/plugin.js +++ b/src/core/plugin.js @@ -143,7 +143,6 @@ export default class RethinkPlugin { async execute() { const io = this.io; // const rxid = this.ctx.get("rxid"); - // const t = this.log.startTime("exec-plugin-" + rxid); for (const p of this.plugin) { if (io.stopProcessing && !p.continueOnStopProcess) { continue; @@ -152,16 +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); } /** diff --git a/src/server-node.js b/src/server-node.js index 66cc421d8b..fc61a0436b 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -841,7 +841,6 @@ async function handleTCPQuery(q, socket, host, flag) { if (bufutil.emptyBuf(q) || !tcpOkay(socket)) return; const rxid = util.xid(); - const t = log.startTime("handle-tcp-query-" + rxid); try { const r = await resolveQuery(rxid, q, host, flag); if (bufutil.emptyBuf(r)) { @@ -856,7 +855,6 @@ async function handleTCPQuery(q, socket, host, flag) { ok = false; log.w(rxid, "tcp: send fail, err", e); } - log.endTime(t); // close socket when !ok if (!ok) { @@ -941,8 +939,6 @@ async function serveHTTPS(req, res) { const buffers = []; - const t = log.startTime("recv-https"); - // if using for await loop, then it must be wrapped in a // try-catch block: stackoverflow.com/questions/69169226 // if not, errors from reading req escapes unhandled. @@ -954,8 +950,6 @@ async function serveHTTPS(req, res) { const b = bufutil.concatBuf(buffers); const bLen = b.byteLength; - log.endTime(t); - if (util.isPostRequest(req) && !dnsutil.validResponseSize(b)) { res.writeHead(dnsutil.dohStatusCode(b), util.corsHeadersIfNeeded(ua)); res.end(); @@ -976,7 +970,6 @@ async function handleHTTPRequest(b, req, res) { heartbeat(); const rxid = util.xid(); - const t = log.startTime("handle-http-req-" + rxid); try { let host = req.headers.host || req.headers[":authority"]; if (isIPv6(host)) host = `[${host}]`; @@ -995,26 +988,18 @@ async function handleHTTPRequest(b, req, res) { body: req.method === "POST" ? b : null, }); - log.lapTime(t, "upstream-start"); - const fRes = await handleRequest(util.mkFetchEvent(fReq)); - log.lapTime(t, "upstream-end"); - if (!resOkay(res)) { throw new Error("res not writable 1"); } res.writeHead(fRes.status, util.copyHeaders(fRes)); - log.lapTime(t, "send-head"); - // ans may be null on non-2xx responses, such as redirects (3xx) by cc.js // or 4xx responses on timeouts or 5xx on invalid http method const ans = await fRes.arrayBuffer(); - log.lapTime(t, "recv-ans"); - if (!resOkay(res)) { throw new Error("res not writable 2"); } else if (!bufutil.emptyBuf(ans)) { @@ -1030,8 +1015,6 @@ async function handleHTTPRequest(b, req, res) { if (!ok) resClose(res); log.w(e); } - - log.endTime(t); } /** From 4f25b7463aeb40d4f2ec120848f6de35ddf13e3e Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 26 Sep 2024 22:04:59 +0530 Subject: [PATCH 049/126] node: m server event logs --- src/server-node.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index fc61a0436b..5be9f9b1b3 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -356,7 +356,7 @@ function trapServerEvents(s) { const id = tracker.trackConn(s, socket); if (!tracker.valid(id)) { - log.i("tcp: not tracking; server shutting down?"); + log.i("tcp: not tracking; server shutting down?", id); close(socket); return; } @@ -368,7 +368,7 @@ function trapServerEvents(s) { }); socket.on("error", (err) => { - log.d("tcp: incoming conn closed with err; " + err.message); + log.d("tcp: incoming conn", id, "closed:", err.message); close(socket); }); @@ -411,7 +411,7 @@ function trapSecureServerEvents(s) { const id = tracker.trackConn(s, socket); if (!tracker.valid(id)) { - log.i("tls: not tracking; server shutting down?"); + log.i("tls: not tracking; server shutting down?", id); close(socket); return; } @@ -458,11 +458,23 @@ function trapSecureServerEvents(s) { s.on("tlsClientError", (err, /** @type {TLSSocket} */ tlsSocket) => { stats.tlserr += 1; // fly tcp healthchecks also trigger tlsClientErrors - log.d("tls: client err; " + err.message); + log.d("tls: client err;", err.message, addrstr(tlsSocket)); close(tlsSocket); }); } +/** + * @param {TLSSocket|Socket} sock + */ +function addrstr(sock) { + if (!sock) return ""; + return ( + `[${sock.localAddress}]:${sock.localPort}` + + "->" + + `[${sock.remoteAddress}]:${sock.remotePort}` + ); +} + /** * @param {tls.Server} s * @returns {void} From 36b178ad50353df617bb03e5e67de4aaaa62b38f Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 26 Sep 2024 22:06:26 +0530 Subject: [PATCH 050/126] core/dns: m handle transacts for null sockets --- src/core/dns/transact.js | 46 ++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/core/dns/transact.js b/src/core/dns/transact.js index 53debb5db9..8145229ff9 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"); @@ -200,19 +219,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"); From 433e493f5085ce295cf2ba0ce64ef3aaa449998c Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 26 Sep 2024 22:07:10 +0530 Subject: [PATCH 051/126] core/dns: m jsdocs --- src/commons/bufutil.js | 8 +++ src/commons/util.js | 8 ++- src/core/dns/conns.js | 96 +++++++++++++++++++++++++++++++--- src/core/dns/transact.js | 59 +++++++++++++++++---- src/core/node/dns-transport.js | 48 +++++++++++++++-- 5 files changed, 199 insertions(+), 20 deletions(-) diff --git a/src/commons/bufutil.js b/src/commons/bufutil.js index 56eb33625c..8897091476 100644 --- a/src/commons/bufutil.js +++ b/src/commons/bufutil.js @@ -169,11 +169,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/util.js b/src/commons/util.js index 405baf2f18..a639e13908 100644 --- a/src/commons/util.js +++ b/src/commons/util.js @@ -117,7 +117,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, () => { 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 8145229ff9..79b122101d 100644 --- a/src/core/dns/transact.js +++ b/src/core/dns/transact.js @@ -87,8 +87,13 @@ export class TcpTx { } } - // TODO: Same code as in server.js, merge them + /** + * @param {string} rxid + * @param {Buffer} chunk + * @returns + */ onData(rxid, chunk) { + // TODO: Same code as in server.js, merge them if (this.done) { this.log.w(rxid, "on reads, tx is closed for business"); return chunk; @@ -165,6 +170,7 @@ export class TcpTx { this.no("timeout"); } + /** @returns {Promise} */ promisedRead() { const that = this; return new Promise((resolve, reject) => { @@ -173,6 +179,10 @@ export class TcpTx { }); } + /** + * @param {string} rxid + * @param {Buffer} query + */ write(rxid, query) { if (this.done) { this.log.w(rxid, "no writes, tx is done working"); @@ -191,26 +201,38 @@ export class TcpTx { }); } + /** + * @param {Buffer} val + */ yes(val) { this.done = true; this.resolve(val); } + /** + * @param {string?} 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; } } @@ -278,12 +300,23 @@ 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.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"); @@ -302,6 +335,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) { @@ -315,6 +352,7 @@ export class UdpTx { }); } + /** @param {Buffer} val */ yes(val) { if (this.done) return; @@ -323,6 +361,7 @@ export class UdpTx { this.resolve(val); } + /** @param {string} reason */ no(reason) { if (this.done) return; diff --git a/src/core/node/dns-transport.js b/src/core/node/dns-transport.js index a351d21092..061eea73ba 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,10 +65,16 @@ 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); + /** @type {Buffer?} */ let ans = null; try { sock = sock || (await this.makeConn("udp")); @@ -61,10 +87,16 @@ export class Transport { 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); + /** @type {Buffer?} */ let ans = null; try { sock = sock || (await this.makeConn("tcp")); @@ -78,6 +110,10 @@ export class Transport { return ans; } + /** + * @param {AnySock} sock + * @param {string} proto + */ parkConn(sock, proto) { if (proto === "tcp") { const ok = this.tcpconns.give(sock); @@ -88,6 +124,11 @@ export class Transport { } } + /** + * @param {string} proto + * @returns {Promise} + * @throws {Error} + */ makeConn(proto) { if (proto === "tcp") { const tcpconnect = (cb) => { @@ -115,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; From cc6e7988aaf453f10d16fac080a592204adb5996 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 27 Sep 2024 06:50:26 +0530 Subject: [PATCH 052/126] doh: add cors for user-agent=dohjs --- src/commons/util.js | 3 ++- src/core/doh.js | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/commons/util.js b/src/commons/util.js index a639e13908..5c53da34b4 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() { diff --git a/src/core/doh.js b/src/core/doh.js index 619815c3af..fc0749409b 100644 --- a/src/core/doh.js +++ b/src/core/doh.js @@ -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; From 45c079957f09ce86f03f2f00c32f0bcf7612d60e Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 27 Sep 2024 07:03:05 +0530 Subject: [PATCH 053/126] core/io: doh cache headers --- src/commons/dnsutil.js | 5 +++++ src/core/io-state.js | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/commons/dnsutil.js b/src/commons/dnsutil.js index c40fab6020..5967327c6d 100644 --- a/src/commons/dnsutil.js +++ b/src/commons/dnsutil.js @@ -377,6 +377,11 @@ 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[]} diff --git a/src/core/io-state.js b/src/core/io-state.js index 1c56ff5494..45d3ac822b 100644 --- a/src/core/io-state.js +++ b/src/core/io-state.js @@ -191,6 +191,7 @@ export default class IOState { return util.concatHeaders( util.dnsHeaders(), util.contentLengthHeader(b), + this.cacheHeaders(), xNileRegion, xNileFlags, xNileFlagsOk @@ -215,6 +216,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(); From ad46d2e1aea53fb4f838cdb24fc1a8b85ea5232f Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 27 Sep 2024 07:08:12 +0530 Subject: [PATCH 054/126] core/io: add block flag headers iff non-empty --- src/core/io-state.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/io-state.js b/src/core/io-state.js index 45d3ac822b..56199b020a 100644 --- a/src/core/io-state.js +++ b/src/core/io-state.js @@ -182,8 +182,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; From 138cbc5a6434672bbb96d5ed4e8695f2619e6d5a Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 27 Sep 2024 18:46:58 +0530 Subject: [PATCH 055/126] core/io: add debug headers iff not prod --- src/commons/envutil.js | 6 ++++++ src/commons/util.js | 7 ------- src/core/io-state.js | 29 ++++++++++++++++++++++------- src/core/log.js | 2 +- src/core/node/config.js | 2 +- 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/commons/envutil.js b/src/commons/envutil.js index f308a5128d..9d94412154 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; diff --git a/src/commons/util.js b/src/commons/util.js index 5c53da34b4..bc0c51934b 100644 --- a/src/commons/util.js +++ b/src/commons/util.js @@ -526,13 +526,6 @@ export function stub(...args) { }; } -export function stubr(r, ...args) { - return (...args) => { - /* no-op */ - return r; - }; -} - export function stubAsync(...args) { return async (...args) => { /* no-op */ diff --git a/src/core/io-state.js b/src/core/io-state.js index 56199b020a..57dae0b25f 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"); } @@ -84,7 +98,7 @@ export default class IOState { 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 }); @@ -148,7 +162,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 +188,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, }); @@ -201,7 +215,8 @@ export default class IOState { ); } - additionalHeader(json) { + debugHeaders(json) { + if (this.isProd) return null; if (!json) return null; return { diff --git a/src/core/log.js b/src/core/log.js index 13df003294..f841cbec26 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, stubr } from "../commons/util.js"; +import { uid, stub } from "../commons/util.js"; /** * @typedef {'error'|'logpush'|'warn'|'info'|'timer'|'debug'} LogLevels diff --git a/src/core/node/config.js b/src/core/node/config.js index d1b7e9bc26..31328fad71 100644 --- a/src/core/node/config.js +++ b/src/core/node/config.js @@ -127,7 +127,7 @@ async function prep() { log.i("imported udp/tcp dns transport", plainOldDnsIp); } - /** signal ready */ + // signal ready system.pub("ready", [dns53]); } From 7ecb04b3a0e8b674a917273958751966357e831c Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 27 Sep 2024 22:05:09 +0530 Subject: [PATCH 056/126] core/dns: missing socket callback args --- src/core/dns/transact.js | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/core/dns/transact.js b/src/core/dns/transact.js index 79b122101d..3a57d9deb6 100644 --- a/src/core/dns/transact.js +++ b/src/core/dns/transact.js @@ -63,7 +63,7 @@ export class TcpTx { this.onTimeout(rxid); }; const onError = (err) => { - this.onError(rxid); + this.onError(rxid, err); }; try { @@ -93,15 +93,16 @@ export class TcpTx { * @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) @@ -154,18 +155,18 @@ 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"); } @@ -184,20 +185,22 @@ export class TcpTx { * @param {Buffer} query */ write(rxid, query) { + const qlen = bufutil.len(query); + const hlen = bufutil.len(header); 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); 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); }); } @@ -210,7 +213,7 @@ export class TcpTx { } /** - * @param {string?} reason + * @param {string?|Error} reason */ no(reason) { this.done = true; @@ -307,7 +310,7 @@ export class UdpTx { */ 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 } @@ -319,7 +322,7 @@ export class UdpTx { */ 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); } @@ -361,7 +364,7 @@ export class UdpTx { this.resolve(val); } - /** @param {string} reason */ + /** @param {string|Error} reason */ no(reason) { if (this.done) return; From 1d7101e85d0bf262c424d9435c619b2af9d22aea Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 27 Sep 2024 22:05:35 +0530 Subject: [PATCH 057/126] core/io: m debug log dns response --- src/core/io-state.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/core/io-state.js b/src/core/io-state.js index 57dae0b25f..06ad5d19f3 100644 --- a/src/core/io-state.js +++ b/src/core/io-state.js @@ -95,6 +95,7 @@ export default class IOState { exceptionStack: this.exceptionStack, }; + this.logDnsPkt(); this.httpResponse = new Response(servfail, { headers: util.concatHeaders( this.headers(servfail), @@ -137,11 +138,23 @@ export default class IOState { this.decodedDnsPacket = dnsPacket || dnsutil.decode(arrayBuffer); } + this.logDnsPkt(); this.httpResponse = new Response(arrayBuffer, { headers: this.headers(arrayBuffer), }); } + logDnsPkt() { + this.log.d( + "domains", + dnsutil.extractDomains(this.decodedDnsPacket), + "data", + dnsutil.getInterestingAnswerData(this.decodedDnsPacket), + "ttl", + dnsutil.ttl(this.decodedDnsPacket) + ); + } + dnsBlockResponse(blockflag) { this.initDecodedDnsPacketIfNeeded(); this.stopProcessing = true; From 73f8d2035f0530c7fd0f228b997f99f20065136c Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 27 Sep 2024 22:06:06 +0530 Subject: [PATCH 058/126] node: sock addrs may be undefined --- src/server-node.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server-node.js b/src/server-node.js index 5be9f9b1b3..dd687e4772 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -468,6 +468,7 @@ function trapSecureServerEvents(s) { */ function addrstr(sock) { if (!sock) return ""; + if (sock.localAddress == null || sock.remoteAddress == null) return ""; return ( `[${sock.localAddress}]:${sock.localPort}` + "->" + From 953e0d19fd8a2049df53a2632244ab2d36b44bdf Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 28 Sep 2024 06:10:45 +0530 Subject: [PATCH 059/126] Fly: disable swap when using auto suspend --- fly.tls.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fly.tls.toml b/fly.tls.toml index 1e3b420251..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" From a03377662c878e15ac0fd6d066efd9e8dea3e472 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 28 Sep 2024 06:12:00 +0530 Subject: [PATCH 060/126] dnsutil: distinct err values for getInterestingAnswerData --- src/commons/dnsutil.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/dnsutil.js b/src/commons/dnsutil.js index 5967327c6d..37b9006db3 100644 --- a/src/commons/dnsutil.js +++ b/src/commons/dnsutil.js @@ -426,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 From 71bf7a09d8c5b1a8b8446dd5ccb30fdf4558d35b Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 28 Sep 2024 06:12:51 +0530 Subject: [PATCH 061/126] various: m logs --- src/core/io-state.js | 3 ++- src/plugins/dns-op/resolver.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/io-state.js b/src/core/io-state.js index 06ad5d19f3..686a7898b8 100644 --- a/src/core/io-state.js +++ b/src/core/io-state.js @@ -145,12 +145,13 @@ export default class IOState { } logDnsPkt() { + if (this.isProd) return; this.log.d( "domains", dnsutil.extractDomains(this.decodedDnsPacket), + dnsutil.getQueryType(this.decodedDnsPacket) || "", "data", dnsutil.getInterestingAnswerData(this.decodedDnsPacket), - "ttl", dnsutil.ttl(this.decodedDnsPacket) ); } diff --git a/src/plugins/dns-op/resolver.js b/src/plugins/dns-op/resolver.js index 20dfe3cc98..a4b3ac0f7d 100644 --- a/src/plugins/dns-op/resolver.js +++ b/src/plugins/dns-op/resolver.js @@ -265,7 +265,7 @@ export default class DNSResolver { // 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, "ansblock?", r.isBlocked, "fromcache?", fromCache); // 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 @@ -311,7 +311,7 @@ export default class DNSResolver { this.log.d(rxid, "primeCache: block?", blocked, "k", k.href); 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; } From 2905c688157046a82637abfc0a3f608ee961ea8b Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sun, 29 Sep 2024 02:18:25 +0530 Subject: [PATCH 062/126] core/dns: m uninit var --- src/core/dns/transact.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/dns/transact.js b/src/core/dns/transact.js index 3a57d9deb6..cb525b6503 100644 --- a/src/core/dns/transact.js +++ b/src/core/dns/transact.js @@ -186,13 +186,13 @@ export class TcpTx { */ write(rxid, query) { const qlen = bufutil.len(query); - const hlen = bufutil.len(header); if (this.done) { 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(qlen); From 953059059816bbe3277d133658a97ede8b36df08 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sun, 29 Sep 2024 02:19:10 +0530 Subject: [PATCH 063/126] core/node: dns53 default on node --- src/core/node/config.js | 5 ++--- src/core/node/dns-transport.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/core/node/config.js b/src/core/node/config.js index 31328fad71..1210a48b48 100644 --- a/src/core/node/config.js +++ b/src/core/node/config.js @@ -119,12 +119,11 @@ 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; + const dns53 = dnst.makeTransport(plainOldDnsIp); + log.i("imported udp/tcp dns transport", plainOldDnsIp); if (onFly) { // recursive resolver on Fly // swapon won't work on fly: community.fly.io/t/19196/13 - dns53 = dnst.makeTransport(plainOldDnsIp); - log.i("imported udp/tcp dns transport", plainOldDnsIp); } // signal ready diff --git a/src/core/node/dns-transport.js b/src/core/node/dns-transport.js index 061eea73ba..490c181060 100644 --- a/src/core/node/dns-transport.js +++ b/src/core/node/dns-transport.js @@ -94,7 +94,7 @@ export class Transport { */ 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); /** @type {Buffer?} */ let ans = null; From 781083667c9bc63ca24427f3f97adabc8cff458f Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sun, 29 Sep 2024 08:43:05 +0530 Subject: [PATCH 064/126] system: sticky parcel & jsdocs --- src/system.js | 93 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 75 insertions(+), 18 deletions(-) diff --git a/src/system.js b/src/system.js index 91997f239f..622d11d7ab 100644 --- a/src/system.js +++ b/src/system.js @@ -9,6 +9,10 @@ import * as util from "./commons/util.js"; // Evaluate if EventTarget APIs can replace this hand-rolled impl // developers.cloudflare.com/workers/platform/changelog#2021-09-24 + +/** @typedef {any[]?} parcel */ +/** @typedef {function(parcel)} listenfn */ + // once emitted, they stick; firing off new listeners forever, just the once. const stickyEvents = new Set([ // when process bring-up is done @@ -21,12 +25,17 @@ const stickyEvents = new Set([ "go", ]); +/** @type {Map} */ +const stickyParcels = new Map(); + const events = new Set([ // when server should cease "stop", ]); +/** @type {Map>} */ const listeners = new Map(); +/** @type {Map>} */ const waitGroup = new Map(); (() => { @@ -41,40 +50,65 @@ const waitGroup = new Map(); } })(); -// fires an event +/** + * Fires event. + * @param {string} event + * @param {parcel} parcel + */ export function pub(event, parcel = undefined) { awaiters(event, parcel); callbacks(event, parcel); } -// invokes cb when event is fired -export function sub(event, cb) { +/** + * Invokes cb when event is fired. + * @param {string} event + * @param {listenfn} cb + * @param {int} timeout + * @returns {boolean} + */ +export function sub(event, cb, timeout = 0) { const eventCallbacks = listeners.get(event); // if such even callbacks don't exist if (!eventCallbacks) { // but event is sticky, fire off the listener at once if (stickyEvents.has(event)) { - microtaskBox(cb); + const parcel = stickyParcels.get(event); // may be null + microtaskBox(cb, parcel); return true; } // but event doesn't exist, then there's nothing to do return false; } - eventCallbacks.add(cb); + const tid = timeout > 0 ? util.timeout(timeout, cb) : -2; + const fulfiller = + tid > 0 + ? (parcel) => { + clearTimeout(tid); + cb(parcel); + } + : cb; + eventCallbacks.add(fulfiller); return true; } -// waits till event fires or timesout +/** + * Waits till event fires or timesout. + * @param {string} event + * @param {int} timeout + * @returns {Promise} + */ export function when(event, timeout = 0) { const wg = waitGroup.get(event); if (!wg) { // if stick event, fulfill promise right away if (stickyEvents.has(event)) { - return Promise.resolve(event); + const parcel = stickyParcels.get(event); // may be null + return Promise.resolve(parcel); } // no such event return Promise.reject(new Error(event + " missing")); @@ -87,15 +121,20 @@ export function when(event, timeout = 0) { reject(new Error(event + " elapsed " + timeout)); }) : -2; - const fulfiller = function (parcel) { + /** @type {listenfn} */ + const fulfiller = (parcel) => { if (tid >= 0) clearTimeout(tid); - accept(parcel, event); + accept(parcel); }; wg.add(fulfiller); }); } -function awaiters(event, parcel) { +/** + * @param {string} event + * @param {parcel} parcel + */ +function awaiters(event, parcel = null) { const g = waitGroup.get(event); if (!g) return; @@ -103,12 +142,17 @@ function awaiters(event, parcel) { // listeners valid just the once for stickyEvents if (stickyEvents.has(event)) { waitGroup.delete(event); + stickyParcels.set(event, parcel); } safeBox(g, parcel); } -function callbacks(event, parcel) { +/** + * @param {string} event + * @param {parcel} parcel + */ +function callbacks(event, parcel = null) { const cbs = listeners.get(event); if (!cbs) return; @@ -116,6 +160,7 @@ function callbacks(event, parcel) { // listeners valid just the once for stickyEvents if (stickyEvents.has(event)) { listeners.delete(event); + stickyParcels.set(event, parcel); } // callbacks are queued async and don't block the caller. On Workers, @@ -126,18 +171,25 @@ function callbacks(event, parcel) { microtaskBox(cbs, parcel); } -// TODO: could be replaced with scheduler.wait -// developers.cloudflare.com/workers/platform/changelog#2021-12-10 -// queues fn in a macro-task queue of the event-loop -// exec order: github.com/nodejs/node/issues/22257 +/** + * Queues fn in a macro-task queue of the event-loop + * exec order: github.com/nodejs/node/issues/22257 + * @param {listenfn} fn + */ export function taskBox(fn) { + // TODO: could be replaced with scheduler.wait + // developers.cloudflare.com/workers/platform/changelog#2021-12-10 util.timeout(/* with 0ms delay*/ 0, () => safeBox(fn)); } -// queues fn in a micro-task queue // ref: MDN: Web/API/HTML_DOM_API/Microtask_guide/In_depth // queue-task polyfill: stackoverflow.com/a/61605098 const taskboxPromise = { p: Promise.resolve() }; +/** + * Queues fns in a micro-task queue + * @param {listenfn[]} fns + * @param {parcel} arg + */ function microtaskBox(fns, arg) { let enqueue = null; if (typeof queueMicrotask === "function") { @@ -149,9 +201,14 @@ function microtaskBox(fns, arg) { enqueue(() => safeBox(fns, arg)); } -// TODO: safeBox for async fns with r.push(await f())? -// stackoverflow.com/questions/38508420 +/** + * stackoverflow.com/questions/38508420 + * @param {listenfn[]|listenfn?} fns + * @param {parcel} arg + * @returns {any[]} + */ function safeBox(fns, arg) { + // TODO: safeBox for async fns with r.push(await f())? if (typeof fns === "function") { fns = [fns]; } From 10dbc94bdc287149118a0bf9bb3546759e380b1a Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sun, 29 Sep 2024 12:29:09 +0530 Subject: [PATCH 065/126] commons/envutil: m jsdoc --- src/commons/envutil.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/commons/envutil.js b/src/commons/envutil.js index 9d94412154..e931590256 100644 --- a/src/commons/envutil.js +++ b/src/commons/envutil.js @@ -95,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; From 8b1bc5c1a8f03633a56e605e33a22c92e7ad18f1 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sun, 29 Sep 2024 12:29:45 +0530 Subject: [PATCH 066/126] core/io: m decode synth servfails --- src/core/io-state.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/io-state.js b/src/core/io-state.js index 686a7898b8..bd33e9f7a2 100644 --- a/src/core/io-state.js +++ b/src/core/io-state.js @@ -94,6 +94,7 @@ export default class IOState { exceptionFrom: this.exceptionFrom, exceptionStack: this.exceptionStack, }; + this.decodedDnsPacket = dnsutil.decode(servfail); this.logDnsPkt(); this.httpResponse = new Response(servfail, { From 180d5c9dc0dddf19987b941dc8fda1d7e3b5838b Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sun, 29 Sep 2024 12:30:09 +0530 Subject: [PATCH 067/126] core/node: rmv no-op code --- src/core/node/config.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/node/config.js b/src/core/node/config.js index 1210a48b48..3caacb6cbb 100644 --- a/src/core/node/config.js +++ b/src/core/node/config.js @@ -121,10 +121,6 @@ async function prep() { const plainOldDnsIp = onFly ? "fdaa::3" : "1.1.1.2"; const dns53 = dnst.makeTransport(plainOldDnsIp); log.i("imported udp/tcp dns transport", plainOldDnsIp); - if (onFly) { - // recursive resolver on Fly - // swapon won't work on fly: community.fly.io/t/19196/13 - } // signal ready system.pub("ready", [dns53]); From bd725aba29f1f308c921751b4fa03c6f0ed8e33f Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sun, 29 Sep 2024 12:51:07 +0530 Subject: [PATCH 068/126] resolver: coalesce requests --- src/plugins/cache-util.js | 2 +- src/plugins/dns-op/resolver.js | 62 +++++++++++++++++++++++++--------- src/system.js | 61 +++++++++++++++++++++++++-------- 3 files changed, 93 insertions(+), 32 deletions(-) diff --git a/src/plugins/cache-util.js b/src/plugins/cache-util.js index 5bdc5800c7..561d76597b 100644 --- a/src/plugins/cache-util.js +++ b/src/plugins/cache-util.js @@ -118,7 +118,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; diff --git a/src/plugins/dns-op/resolver.js b/src/plugins/dns-op/resolver.js index a4b3ac0f7d..d37d0c543e 100644 --- a/src/plugins/dns-op/resolver.js +++ b/src/plugins/dns-op/resolver.js @@ -13,6 +13,7 @@ import * as dnsutil from "../../commons/dnsutil.js"; import * as bufutil from "../../commons/bufutil.js"; import * as util from "../../commons/util.js"; import * as envutil from "../../commons/envutil.js"; +import * as system from "../../system.js"; import { BlocklistFilter } from "../rethinkdns/filter.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 @@ -342,24 +345,39 @@ DNSResolver.prototype.resolveDnsUpstream = async function ( 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); @@ -369,19 +387,31 @@ 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"); diff --git a/src/system.js b/src/system.js index 622d11d7ab..62481fe266 100644 --- a/src/system.js +++ b/src/system.js @@ -33,6 +33,9 @@ const events = new Set([ "stop", ]); +/** @type {Set} */ +const ephemeralEvents = new Set(); + /** @type {Map>} */ const listeners = new Map(); /** @type {Map>} */ @@ -54,10 +57,13 @@ const waitGroup = new Map(); * Fires event. * @param {string} event * @param {parcel} parcel + * @returns {int} */ -export function pub(event, parcel = undefined) { - awaiters(event, parcel); - callbacks(event, parcel); +export function pub(event, parcel = null) { + if (util.emptyString(event)) return; + + const tot = awaiters(event, parcel); + return tot + callbacks(event, parcel); } /** @@ -68,17 +74,21 @@ export function pub(event, parcel = undefined) { * @returns {boolean} */ export function sub(event, cb, timeout = 0) { + if (util.emptyString(event)) return; + if (typeof cb !== "function") return; + const eventCallbacks = listeners.get(event); - // if such even callbacks don't exist if (!eventCallbacks) { - // but event is sticky, fire off the listener at once + // event is sticky, fire off the listener at once if (stickyEvents.has(event)) { const parcel = stickyParcels.get(event); // may be null microtaskBox(cb, parcel); return true; } - // but event doesn't exist, then there's nothing to do + // event doesn't exist so make it ephemeral + ephemeralEvents.add(event); + listeners.set(event, new Set()); return false; } @@ -102,6 +112,10 @@ export function sub(event, cb, timeout = 0) { * @returns {Promise} */ export function when(event, timeout = 0) { + if (util.emptyString(event)) { + return Promise.reject(new Error("empty event")); + } + const wg = waitGroup.get(event); if (!wg) { @@ -110,15 +124,17 @@ export function when(event, timeout = 0) { const parcel = stickyParcels.get(event); // may be null return Promise.resolve(parcel); } - // no such event - return Promise.reject(new Error(event + " missing")); + // no such event so make it ephemeral + ephemeralEvents.add(event); + waitGroup.set(event, new Set()); + return Promise.reject(new Error(event + " missing event")); } return new Promise((accept, reject) => { const tid = timeout > 0 ? util.timeout(timeout, () => { - reject(new Error(event + " elapsed " + timeout)); + reject(new Error(event + " event elapsed " + timeout)); }) : -2; /** @type {listenfn} */ @@ -133,34 +149,47 @@ export function when(event, timeout = 0) { /** * @param {string} event * @param {parcel} parcel + * @returns {int} */ function awaiters(event, parcel = null) { - const g = waitGroup.get(event); + if (util.emptyString(event)) return 0; + const wg = waitGroup.get(event); - if (!g) return; + if (!wg) return 0; - // listeners valid just the once for stickyEvents + // listeners valid just the once for stickyEvents & ephemeralEvents if (stickyEvents.has(event)) { waitGroup.delete(event); stickyParcels.set(event, parcel); + } else if (ephemeralEvents.has(event)) { + // log.d("sys: wg ephemeralEvents", event, parcel); + waitGroup.delete(event); + ephemeralEvents.delete(event); } - safeBox(g, parcel); + safeBox(wg, parcel); + return wg.size; } /** * @param {string} event * @param {parcel} parcel + * @returns {int} */ function callbacks(event, parcel = null) { + if (util.emptyString(event)) return 0; const cbs = listeners.get(event); - if (!cbs) return; + if (!cbs) return 0; - // listeners valid just the once for stickyEvents + // listeners valid just the once for stickyEvents & ephemeralEvents if (stickyEvents.has(event)) { listeners.delete(event); stickyParcels.set(event, parcel); + } else if (ephemeralEvents.has(event)) { + // log.d("sys: cb ephemeralEvents", event, parcel); + listeners.delete(event); + ephemeralEvents.delete(event); } // callbacks are queued async and don't block the caller. On Workers, @@ -169,6 +198,7 @@ function callbacks(event, parcel = null) { // incoming request (through the fetch event handler), such callbacks // may not even fire. Instead use: awaiters and not callbacks. microtaskBox(cbs, parcel); + return cbs.size; } /** @@ -226,6 +256,7 @@ function safeBox(fns, arg) { try { r.push(f(arg)); } catch (ignore) { + // log.e("sys: safeBox err", ignore); r.push(null); } } From 3fcf47d754391198b594c0c56d6d90fbd79622c2 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Tue, 1 Oct 2024 03:00:56 +0530 Subject: [PATCH 069/126] system: ephemeral event common for both listeners & awaiters --- src/system.js | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/system.js b/src/system.js index 62481fe266..89871b53be 100644 --- a/src/system.js +++ b/src/system.js @@ -62,8 +62,10 @@ const waitGroup = new Map(); export function pub(event, parcel = null) { if (util.emptyString(event)) return; - const tot = awaiters(event, parcel); - return tot + callbacks(event, parcel); + const hadEphemeralEvent = ephemeralEvents.delete(event); + + const tot = awaiters(event, parcel, hadEphemeralEvent); + return tot + callbacks(event, parcel, hadEphemeralEvent); } /** @@ -89,6 +91,7 @@ export function sub(event, cb, timeout = 0) { // event doesn't exist so make it ephemeral ephemeralEvents.add(event); listeners.set(event, new Set()); + waitGroup.set(event, new Set()); return false; } @@ -126,6 +129,7 @@ export function when(event, timeout = 0) { } // no such event so make it ephemeral ephemeralEvents.add(event); + listeners.set(event, new Set()); waitGroup.set(event, new Set()); return Promise.reject(new Error(event + " missing event")); } @@ -149,9 +153,10 @@ export function when(event, timeout = 0) { /** * @param {string} event * @param {parcel} parcel + * @param {boolean} ephemeralEvent * @returns {int} */ -function awaiters(event, parcel = null) { +function awaiters(event, parcel = null, ephemeralEvent = false) { if (util.emptyString(event)) return 0; const wg = waitGroup.get(event); @@ -161,12 +166,13 @@ function awaiters(event, parcel = null) { if (stickyEvents.has(event)) { waitGroup.delete(event); stickyParcels.set(event, parcel); - } else if (ephemeralEvents.has(event)) { - // log.d("sys: wg ephemeralEvents", event, parcel); + } else if (ephemeralEvent) { + // log.d("sys: wg ephemeralEvent", event, parcel); waitGroup.delete(event); - ephemeralEvents.delete(event); } + if (wg.size === 0) return 0; + safeBox(wg, parcel); return wg.size; } @@ -174,9 +180,10 @@ function awaiters(event, parcel = null) { /** * @param {string} event * @param {parcel} parcel + * @param {boolean} ephemeralEvent * @returns {int} */ -function callbacks(event, parcel = null) { +function callbacks(event, parcel = null, ephemeralEvent = false) { if (util.emptyString(event)) return 0; const cbs = listeners.get(event); @@ -186,12 +193,12 @@ function callbacks(event, parcel = null) { if (stickyEvents.has(event)) { listeners.delete(event); stickyParcels.set(event, parcel); - } else if (ephemeralEvents.has(event)) { - // log.d("sys: cb ephemeralEvents", event, parcel); + } else if (ephemeralEvent) { + // log.d("sys: cb ephemeralEvent", event, parcel); listeners.delete(event); - ephemeralEvents.delete(event); } + if (cbs.size === 0) return 0; // callbacks are queued async and don't block the caller. On Workers, // where IOs or timers require event-context aka network-context, // which is only available when fns are invoked in response to an From f2ff91096898688b2ab5fda6d288894a4bb75f3a Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 12 Oct 2024 19:14:06 +0530 Subject: [PATCH 070/126] node: missing rottm var --- src/server-node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server-node.js b/src/server-node.js index dd687e4772..5679753c6e 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -440,7 +440,7 @@ function trapSecureServerEvents(s) { }); }); - util.repeat(86400000 * 7, () => rotateTkt(s)); // 7d + const rottm = util.repeat(86400000 * 7, () => rotateTkt(s)); // 7d s.on("error", (err) => { log.e("tls: stop! server error; " + err.message, err); From 70ebcddd98c85f9f0992ed8cd06c5502bc63842d Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 12 Oct 2024 19:15:04 +0530 Subject: [PATCH 071/126] node: m logs tracking servers --- src/commons/util.js | 6 +++++ src/server-node.js | 56 ++++++++++++++++++++++++++------------------- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/commons/util.js b/src/commons/util.js index bc0c51934b..6c4692a40e 100644 --- a/src/commons/util.js +++ b/src/commons/util.js @@ -210,6 +210,12 @@ export function repeat(ms, fn) { return timer; } +export function next(...fns) { + for (const fn of fns) { + if (typeof fn === "function") setImmediate(fn); + } +} + // min inclusive, max exclusive export function rand(min, max) { return Math.floor(Math.random() * (max - min)) + min; diff --git a/src/server-node.js b/src/server-node.js index 5679753c6e..1787382f9b 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -6,24 +6,27 @@ * 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 * as h2c from "httpx-server"; +import http2 from "node:http2"; +import net, { isIPv6 } from "node:net"; import * as os from "node:os"; +import { finished } from "node:stream"; +import * as tls 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 { handleRequest } from "./core/doh.js"; import "./core/node/config.js"; -import { finished } from "node:stream"; -import * as nodecrypto from "./commons/crypto.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 @@ -101,17 +104,21 @@ class Tracker { else return sock.remoteAddress + "|" + sock.remotePort; } - trackServer(s) { + trackServer(id, s) { if (!s) return this.zeroid; const mapid = this.sid(s); - if (!this.valid(mapid)) return this.zeroid; + if (!this.valid(mapid)) { + log.w("trackServer: server not tracked", id, mapid); + return this.zeroid; + } const cmap = this.connmap[mapid]; if (cmap) { - log.w("trackServer: server already tracked?", sid); + log.w("trackServer: server already tracked?", id, mapid); return mapid; } + log.i("trackServer: new server", id, mapid); this.connmap[mapid] = new Map(); this.srvs.push(s); } @@ -216,7 +223,8 @@ async function systemDown() { s.unref(); } - bye(); + // test: util.next(whyIsNodeRunning, bye); + util.next(bye); } function systemUp() { @@ -271,7 +279,7 @@ function systemUp() { .createServer(serverOpts, serveTCP) .listen(portdot, zero6, tcpbacklog, () => { up("DoT Cleartext", dotct.address()); - trapServerEvents(dotct); + trapServerEvents("dotct", dotct); }); // DNS over HTTPS Cleartext @@ -287,7 +295,7 @@ function systemUp() { .createServer(serverOpts, serveHTTPS) .listen(portdoh, zero6, tcpbacklog, () => { up("DoH Cleartext", dohct.address()); - trapServerEvents(dohct); + trapServerEvents("dohct", dohct); }); } else { // terminate tls ourselves @@ -307,7 +315,7 @@ function systemUp() { .createServer(secOpts, serveTLS) .listen(portdot1, zero6, tcpbacklog, () => { up("DoT", dot1.address()); - trapSecureServerEvents(dot1); + trapSecureServerEvents("dot1", dot1); }); // DNS over TLS w ProxyProto @@ -318,7 +326,7 @@ function systemUp() { .createServer(serverOpts, serveDoTProxyProto) .listen(portdot2, zero6, tcpbacklog, () => { up("DoT ProxyProto", dot2.address()); - trapServerEvents(dot2); + trapServerEvents("dot2", dot2); }); // DNS over HTTPS @@ -327,28 +335,29 @@ function systemUp() { .createSecureServer({ ...secOpts, ...h2Opts }, serveHTTPS) .listen(portdoh, zero6, tcpbacklog, () => { up("DoH", doh.address()); - trapSecureServerEvents(doh); + trapSecureServerEvents("doh", doh); }); } const portcheck = envutil.httpCheckPort(); const hcheck = h2c.createServer(serve200).listen(portcheck, () => { up("http-check", hcheck.address()); - trapServerEvents(hcheck); + trapServerEvents("hcheck", hcheck); }); heartbeat(); } /** + * @param {string} id * @param {... import("http2").Http2Server | net.Server} s */ -function trapServerEvents(s) { +function trapServerEvents(id, s) { const ioTimeoutMs = envutil.ioTimeoutMs(); if (!s) return; - tracker.trackServer(s); + tracker.trackServer(id, s); s.on("connection", (/** @type {Socket} */ socket) => { stats.nofconns += 1; @@ -395,14 +404,15 @@ function trapServerEvents(s) { } /** + * @param {string} id * @param {http2.Http2SecureServer | tls.Server} s */ -function trapSecureServerEvents(s) { +function trapSecureServerEvents(id, s) { const ioTimeoutMs = envutil.ioTimeoutMs(); if (!s) return; - tracker.trackServer(s); + tracker.trackServer(id, s); // github.com/grpc/grpc-node/blob/e6ea6f517epackages/grpc-js/src/server.ts#L392 s.on("secureConnection", (socket) => { From cfa036a52b3023e1018d0aae4de5b2534b0bafcc Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 12 Oct 2024 19:17:42 +0530 Subject: [PATCH 072/126] package: httpx-server 2.0 The previous version of httpx-server (1.4.4) held tls socket without releasing it, thereby causing node to never quit. The newer version (2.0.0) does away with that code (which was a hack) and targets newer node version that does not require it. --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c411a21b9d..71229e3d9b 100644 --- a/package.json +++ b/package.json @@ -30,11 +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", - "@riaskov/mmap-io": "^1.4.3", + "httpx-server": "^2.0.0", "node-polyfill-webpack-plugin": "^2.0.1", "proxy-protocol-js": "^4.0.5" }, @@ -54,6 +54,7 @@ "prettier": "2.5.1", "webpack": "^5.92.1", "webpack-cli": "^4.10.0", + "why-is-node-running": "^3.2.0", "wrangler": "^3.0.0" }, "lint-staged": { From 439fc9cbe23c9fd344901521d1a00ddd95b7f569 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 05:36:47 +0530 Subject: [PATCH 073/126] core/env: bun_env var --- src/core/env.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/env.js b/src/core/env.js index b2823ba928..2176368150 100644 --- a/src/core/env.js +++ b/src/core/env.js @@ -46,6 +46,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", From c8f019b5717b76e1b4a8b351b13273b4e04a8b67 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 05:39:23 +0530 Subject: [PATCH 074/126] bun: use mmap for blocklists --- src/commons/envutil.js | 2 +- src/core/node/blocklists.js | 43 ++++++++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/commons/envutil.js b/src/commons/envutil.js index e931590256..bcaef53364 100644 --- a/src/commons/envutil.js +++ b/src/commons/envutil.js @@ -53,7 +53,7 @@ export function hasDisk() { export function useMmap() { // got disk on fly and local deploys - return onFly() || onLocal(); + return (onFly() || onLocal()) && (isNode() || isBun()); } export function hasDynamicImports() { diff --git a/src/core/node/blocklists.js b/src/core/node/blocklists.js index 6cb7b3bfb3..85a9933455 100644 --- a/src/core/node/blocklists.js +++ b/src/core/node/blocklists.js @@ -10,7 +10,7 @@ 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 mmap from "@riaskov/mmap-io"; +// import mmap from "@riaskov/mmap-io"; const blocklistsDir = "./blocklists__"; const tdFile = "td.txt"; @@ -29,7 +29,7 @@ export async function setup(bw) { const codec = tdcodec6 ? "u6" : "u8"; const useMmap = envutil.useMmap(); - const ok = setupLocally(bw, timestamp, codec, useMmap); + const ok = await setupLocally(bw, timestamp, codec, useMmap); if (ok) { log.i("bl setup locally tstamp/nc", timestamp, nodecount); return true; @@ -67,18 +67,35 @@ function save(bw, timestamp, codec) { } // fmmap mmaps file at fp for random reads, returns a Buffer backed by the file. -function fmmap(fp) { - 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); +async function fmmap(fp) { + const dynimports = envutil.hasDynamicImports(); + const isNode = envutil.isNode(); + const isBun = envutil.isBun(); + + if (dynimports && isNode) { + try { + // const mmap = await import("@riaskov/mmap-io"); + const mmap = null; + 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); + } + return null; } -function setupLocally(bw, timestamp, codec, useMmap) { +async function setupLocally(bw, timestamp, codec, useMmap) { const ok = hasBlocklistFiles(timestamp, codec); log.i(timestamp, codec, "has bl files?", ok); if (!ok) return false; @@ -86,7 +103,7 @@ function setupLocally(bw, timestamp, codec, useMmap) { const [td, rd] = getFilePaths(timestamp, codec); log.i("on-disk codec/td/rd", codec, td, rd, "mmap?", useMmap); - let tdbuf = useMmap ? fmmap(td) : null; + let tdbuf = useMmap ? await fmmap(td) : null; if (bufutil.emptyBuf(tdbuf)) { tdbuf = fs.readFileSync(td); } From 01e12f49998eb592162e4cbc86ced97f7d6b21ae Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 05:40:42 +0530 Subject: [PATCH 075/126] node: 1.3kb max tls record size --- src/plugins/rdns-util.js | 6 +++--- src/server-node.js | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/plugins/rdns-util.js b/src/plugins/rdns-util.js index f1c1f89873..4a6e3c1073 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 = ":"; diff --git a/src/server-node.js b/src/server-node.js index 1787382f9b..61f7e58826 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -409,13 +409,12 @@ function trapServerEvents(id, s) { */ function trapSecureServerEvents(id, s) { const ioTimeoutMs = envutil.ioTimeoutMs(); - if (!s) return; tracker.trackServer(id, s); // github.com/grpc/grpc-node/blob/e6ea6f517epackages/grpc-js/src/server.ts#L392 - s.on("secureConnection", (socket) => { + s.on("secureConnection", (/** @type {TLSSocket} */ socket) => { stats.nofconns += 1; stats.openconns += 1; @@ -426,6 +425,9 @@ function trapSecureServerEvents(id, s) { return; } + // blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency + socket.setMaxSendFragment(1149); // 1369 - (1500 - 1280) + socket.setTimeout(ioTimeoutMs, () => { stats.noftimeouts += 1; log.d("tls: incoming conn timed out; " + id); From f6ebd2b99aaec7812bcbdc1d9c9663b17546f3e0 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 05:41:33 +0530 Subject: [PATCH 076/126] bun: tls ticket keys not supported --- src/server-node.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server-node.js b/src/server-node.js index 61f7e58826..67e7b713f4 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -493,6 +493,7 @@ function addrstr(sock) { * @returns {void} */ function rotateTkt(s) { + if (envutil.isBun()) return; if (!s || !s.listening) return; let seed = bufutil.fromB64(envutil.secretb64()); @@ -508,7 +509,7 @@ function rotateTkt(s) { nodecrypto .tkt48(seed, ctx) - .then((k) => s.setTicketKeys(k)) + .then((k) => s.setTicketKeys(k)) // not supported on bun .catch((err) => log.e("tls: ticket rotation failed:", err)); } From d29940c7bd894880854de1d818af80ccd42b60a5 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 06:01:49 +0530 Subject: [PATCH 077/126] node: 4kb max tls record size --- src/server-node.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server-node.js b/src/server-node.js index 67e7b713f4..2a7e5d8de2 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -426,7 +426,8 @@ function trapSecureServerEvents(id, s) { } // blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency - socket.setMaxSendFragment(1149); // 1369 - (1500 - 1280) + // 1369 - (1500 - 1280) = 1149 + socket.setMaxSendFragment(4096); // DoT errors out at 1149. socket.setTimeout(ioTimeoutMs, () => { stats.noftimeouts += 1; From 8498359e9eb286600ece0fb3e670f72deee4117f Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 06:44:18 +0530 Subject: [PATCH 078/126] run: bun --- run | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/run b/run index bee3764a36..a3fce0fb19 100755 --- a/run +++ b/run @@ -94,11 +94,15 @@ 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 +if [ $runtime = "bun" ] || [ $runtime = "b" ]; then + echo "note: deno v1.1+ required"; + echo "using `which bun`"; + start="bun run src/server-node.js"; +elif [ $runtime = "deno" ] || [ $runtime = "d" ]; then echo "note: deno v1.17+ required"; echo "using `which deno`"; start="deno run --unstable \ @@ -208,8 +212,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 From aeb052b5999d493808720be955b2010b1c155c74 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 06:47:24 +0530 Subject: [PATCH 079/126] bun: for now supports http1 only --- src/server-node.js | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index 2a7e5d8de2..63aa6ae86a 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -11,6 +11,7 @@ import * as h2c from "httpx-server"; import http2 from "node:http2"; +import https from "node:https"; import net, { isIPv6 } from "node:net"; import * as os from "node:os"; import { finished } from "node:stream"; @@ -234,10 +235,13 @@ function systemUp() { const downloadmode = envutil.blocklistDownloadOnly(); const profilermode = envutil.profileDnsResolves(); const tlsoffload = envutil.isCleartext(); + // todo: tcp backlog for doh/dot servers not supported on bun 1.1 const tcpbacklog = envutil.tcpBacklog(); const maxconns = envutil.maxconns(); // see also: dns-transport.js:ioTimeout const ioTimeoutMs = envutil.ioTimeoutMs(); + const isNode = envutil.isNode(); + const isBun = envutil.isBun(); if (downloadmode) { log.i("in download mode, not running the dns resolver"); @@ -277,7 +281,7 @@ function systemUp() { const dotct = net // serveTCP must eventually call machines-heartbeat .createServer(serverOpts, serveTCP) - .listen(portdot, zero6, tcpbacklog, () => { + .listen(portdot, zero6, () => { up("DoT Cleartext", dotct.address()); trapServerEvents("dotct", dotct); }); @@ -293,7 +297,7 @@ function systemUp() { const dohct = h2c // serveHTTPS must eventually invoke machines-heartbeat .createServer(serverOpts, serveHTTPS) - .listen(portdoh, zero6, tcpbacklog, () => { + .listen(portdoh, zero6, () => { up("DoH Cleartext", dohct.address()); trapServerEvents("dohct", dohct); }); @@ -313,7 +317,7 @@ function systemUp() { const dot1 = tls // serveTLS must eventually invoke machines-heartbeat .createServer(secOpts, serveTLS) - .listen(portdot1, zero6, tcpbacklog, () => { + .listen(portdot1, zero6, () => { up("DoT", dot1.address()); trapSecureServerEvents("dot1", dot1); }); @@ -324,19 +328,30 @@ function systemUp() { net // serveDoTProxyProto must evenually invoke machines-heartbeat .createServer(serverOpts, serveDoTProxyProto) - .listen(portdot2, zero6, tcpbacklog, () => { + .listen(portdot2, zero6, () => { up("DoT ProxyProto", dot2.address()); trapServerEvents("dot2", dot2); }); // DNS over HTTPS - const doh = http2 - // serveHTTPS must eventually invoke machines-heartbeat - .createSecureServer({ ...secOpts, ...h2Opts }, serveHTTPS) - .listen(portdoh, zero6, tcpbacklog, () => { - up("DoH", doh.address()); - trapSecureServerEvents("doh", doh); - }); + if (isNode) { + const doh = http2 + // serveHTTPS must eventually invoke machines-heartbeat + .createSecureServer({ ...secOpts, ...h2Opts }, serveHTTPS) + .listen(portdoh, zero6, () => { + up("DoH2", doh.address()); + trapSecureServerEvents("doh2", doh); + }); + } else if (isBun) { + const doh = https + .createServer(secOpts, serveHTTPS) + .listen(portdoh, zero6, () => { + up("DoH1", doh.address()); + trapSecureServerEvents("doh1", doh); + }); + } else { + console.log("unsupported runtime for doh"); + } } const portcheck = envutil.httpCheckPort(); @@ -427,7 +442,7 @@ function trapSecureServerEvents(id, s) { // blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency // 1369 - (1500 - 1280) = 1149 - socket.setMaxSendFragment(4096); // DoT errors out at 1149. + socket.setMaxSendFragment(1149); socket.setTimeout(ioTimeoutMs, () => { stats.noftimeouts += 1; From cbc60cd365920699b81d3943dc3a7e84c1929b97 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 07:05:20 +0530 Subject: [PATCH 080/126] gh-actions: bun ghcr --- .github/workflows/ghcr.yml | 49 +++++++++++++++++++++++++++++++++++++- bun.Dockerfile | 2 +- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ghcr.yml b/.github/workflows/ghcr.yml index c3b5b3d733..5fb5c22bb9 100644 --- a/.github/workflows/ghcr.yml +++ b/.github/workflows/ghcr.yml @@ -9,6 +9,7 @@ on: 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 @@ -55,6 +56,52 @@ jobs: - name: πŸ“• Attest uses: actions/attest-build-provenance@v1 with: - subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + 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/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 From 15274827c4f125b75c80299cb66a84b91a5066b4 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 08:07:05 +0530 Subject: [PATCH 081/126] gh-actions: bun profiler --- .github/workflows/profiler.yml | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/profiler.yml b/.github/workflows/profiler.yml index 045ed7044b..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: '21.x' - DENO_VER: '1.40.x' + 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: @@ -92,6 +94,19 @@ jobs: 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 From 0d994f37a04c42ae0e5ad7c911485855c399a7e8 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 08:28:57 +0530 Subject: [PATCH 082/126] run: deno --allow-import --- run | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/run b/run index a3fce0fb19..beec07c3c9 100755 --- a/run +++ b/run @@ -99,20 +99,21 @@ if [ $runtime = "help" ] || [ $runtime = "h" ]; then fi if [ $runtime = "bun" ] || [ $runtime = "b" ]; then - echo "note: deno v1.1+ required"; + 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 v1.17+ required"; + 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 @@ -125,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" From 8264fcd393d900081b2de15c307d27a7827be2f5 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 09:46:31 +0530 Subject: [PATCH 083/126] deno: v2 auto detects node modules --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 37dbb01bac..7bda94852c 100644 --- a/deno.json +++ b/deno.json @@ -3,7 +3,7 @@ "allowJs": true }, "importMap": "import_map.json", - "nodeModulesDir": true, + "nodeModulesDir": "auto", "lint": { "files": { "exclude": ["**/**"] From c5b49eb9c54566403e3131f9421ba8ea419281db Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 09:47:03 +0530 Subject: [PATCH 084/126] node/config: dynamically import the mmap default export --- src/core/node/blocklists.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/node/blocklists.js b/src/core/node/blocklists.js index 85a9933455..6153ce9ee8 100644 --- a/src/core/node/blocklists.js +++ b/src/core/node/blocklists.js @@ -74,8 +74,7 @@ async function fmmap(fp) { if (dynimports && isNode) { try { - // const mmap = await import("@riaskov/mmap-io"); - const mmap = null; + 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 From d71e2dc9007c2725763fe6aea98d77ef0f4856eb Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 09:47:44 +0530 Subject: [PATCH 085/126] node/config: m print stacktrace on uncaught warnings --- src/core/node/config.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/core/node/config.js b/src/core/node/config.js index 3caacb6cbb..b69cab9c29 100644 --- a/src/core/node/config.js +++ b/src/core/node/config.js @@ -13,14 +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 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 @@ -156,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"); } From f3b5de67614fde5657b83db86f10d083b2554164 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 09:48:47 +0530 Subject: [PATCH 086/126] node: never re-track an existing server --- src/server-node.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index 63aa6ae86a..e8c3b39775 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -105,6 +105,11 @@ class Tracker { else return sock.remoteAddress + "|" + sock.remotePort; } + /** + * @param {string} id + * @param {net.Server} s + * @returns {string} sid + */ trackServer(id, s) { if (!s) return this.zeroid; const mapid = this.sid(s); @@ -117,11 +122,13 @@ class Tracker { const cmap = this.connmap[mapid]; if (cmap) { log.w("trackServer: server already tracked?", id, mapid); - return mapid; + return this.zeroid; } + log.i("trackServer: new server", id, mapid); this.connmap[mapid] = new Map(); this.srvs.push(s); + return mapid; } *servers() { @@ -372,7 +379,12 @@ function trapServerEvents(id, s) { if (!s) return; - tracker.trackServer(id, s); + const sid = tracker.trackServer(id, s); + + if (sid === tracker.zeroid) { + log.w("tcp: may be already tracking server", id); + return; + } s.on("connection", (/** @type {Socket} */ socket) => { stats.nofconns += 1; @@ -424,9 +436,15 @@ function trapServerEvents(id, s) { */ function trapSecureServerEvents(id, s) { const ioTimeoutMs = envutil.ioTimeoutMs(); + if (!s) return; - tracker.trackServer(id, s); + const sid = tracker.trackServer(id, s); + + if (sid === tracker.zeroid) { + log.w("tls: may be already tracking server", id); + return; + } // github.com/grpc/grpc-node/blob/e6ea6f517epackages/grpc-js/src/server.ts#L392 s.on("secureConnection", (/** @type {TLSSocket} */ socket) => { From 973002ee10c9ed7619a19e797104be09af0c27a5 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 09:50:35 +0530 Subject: [PATCH 087/126] node: m jsdoc --- src/server-node.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index e8c3b39775..0ac24674e6 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -282,13 +282,17 @@ function systemUp() { // fly.io terminated tls? const portdoh = envutil.dohCleartextBackendPort(); const portdot = envutil.dotCleartextBackendPort(); + /** @type {net.ListenOptions} */ + const dohOpts = { port: portdoh, host: zero6, backlog: tcpbacklog }; + /** @type {net.ListenOptions} */ + const dotOpts = { port: portdot, host: zero6, backlog: tcpbacklog }; // TODO: ProxyProtoV2 with TLS ClientHello (unsupported by Fly.io, rn) // DNS over TLS Cleartext const dotct = net // serveTCP must eventually call machines-heartbeat .createServer(serverOpts, serveTCP) - .listen(portdot, zero6, () => { + .listen(dotOpts, () => { up("DoT Cleartext", dotct.address()); trapServerEvents("dotct", dotct); }); @@ -304,7 +308,7 @@ function systemUp() { const dohct = h2c // serveHTTPS must eventually invoke machines-heartbeat .createServer(serverOpts, serveHTTPS) - .listen(portdoh, zero6, () => { + .listen(dohOpts, () => { up("DoH Cleartext", dohct.address()); trapServerEvents("dohct", dohct); }); @@ -319,12 +323,17 @@ function systemUp() { const portdot1 = envutil.dotBackendPort(); const portdot2 = envutil.dotProxyProtoBackendPort(); const portdoh = envutil.dohBackendPort(); - + /** @type {net.ListenOptions} */ + const dohOpts = { port: portdoh, host: zero6, backlog: tcpbacklog }; + /** @type {net.ListenOptions} */ + const dot1Opts = { port: portdot1, host: zero6, backlog: tcpbacklog }; + /** @type {net.ListenOptions} */ + const dot2Opts = { port: portdot2, host: zero6, backlog: tcpbacklog }; // DNS over TLS const dot1 = tls // serveTLS must eventually invoke machines-heartbeat .createServer(secOpts, serveTLS) - .listen(portdot1, zero6, () => { + .listen(dot1Opts, () => { up("DoT", dot1.address()); trapSecureServerEvents("dot1", dot1); }); @@ -335,7 +344,7 @@ function systemUp() { net // serveDoTProxyProto must evenually invoke machines-heartbeat .createServer(serverOpts, serveDoTProxyProto) - .listen(portdot2, zero6, () => { + .listen(dot2Opts, () => { up("DoT ProxyProto", dot2.address()); trapServerEvents("dot2", dot2); }); @@ -345,14 +354,14 @@ function systemUp() { const doh = http2 // serveHTTPS must eventually invoke machines-heartbeat .createSecureServer({ ...secOpts, ...h2Opts }, serveHTTPS) - .listen(portdoh, zero6, () => { + .listen(dohOpts, () => { up("DoH2", doh.address()); trapSecureServerEvents("doh2", doh); }); } else if (isBun) { const doh = https .createServer(secOpts, serveHTTPS) - .listen(portdoh, zero6, () => { + .listen(dohOpts, () => { up("DoH1", doh.address()); trapSecureServerEvents("doh1", doh); }); From cdcb201bce250e582de507c6dc5f0cc7c6d7e1db Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 19:38:27 +0530 Subject: [PATCH 088/126] gh-action: deno deploy for deno v2 --- .github/workflows/deno-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deno-deploy.yml b/.github/workflows/deno-deploy.yml index a09228871a..b24b1b2e1f 100644 --- a/.github/workflows/deno-deploy.yml +++ b/.github/workflows/deno-deploy.yml @@ -88,7 +88,7 @@ jobs: - name: πŸ¦• Install Deno @1.44 uses: denoland/setup-deno@main with: - deno-version: 1.44.4 + deno-version: 2.x - name: πŸ“¦ Bundle up if: ${{ env.DEPLOY_MODE == 'action' }} From e4d82eb6c2e11b66f8ea5648d1f6e21469171a8c Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 19:44:40 +0530 Subject: [PATCH 089/126] gh-actions: use actions/checkout@v4 --- .github/workflows/cf.yml | 2 +- .github/workflows/fly.yml | 2 +- .github/workflows/pr.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cf.yml b/.github/workflows/cf.yml index d15aff0900..00b920706f 100644 --- a/.github/workflows/cf.yml +++ b/.github/workflows/cf.yml @@ -63,7 +63,7 @@ jobs: timeout-minutes: 60 steps: - name: πŸ›’ Checkout - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4 with: ref: ${{ env.GIT_REF }} fetch-depth: 0 diff --git a/.github/workflows/fly.yml b/.github/workflows/fly.yml index 15bba278fa..f1628bef29 100644 --- a/.github/workflows/fly.yml +++ b/.github/workflows/fly.yml @@ -81,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 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d5ab340d78..d0e2eb4fcc 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 }} From 10ef12ffae336acb5e0213a2bebdddafb42a8127 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Wed, 16 Oct 2024 23:28:12 +0530 Subject: [PATCH 090/126] node: prefer aes128 ciphers --- src/server-node.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/server-node.js b/src/server-node.js index 0ac24674e6..580a3971a5 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -267,8 +267,28 @@ function systemUp() { keepAlive: true, noDelay: true, }; + // default cipher suites + // nodejs.org/api/tls.html#modifying-the-default-tls-cipher-suite + let defaultTlsCiphers = ""; + if (!util.emptyString(tls.DEFAULT_CIPHERS)) { + // nodejs.org/api/tls.html#tlsdefault_ciphers + defaultTlsCiphers = tls.DEFAULT_CIPHERS; + } else { + // nodejs.org/api/tls.html#tlsgetciphers + defaultTlsCiphers = tls + .getCiphers() + .map((c) => c.toUpperCase()) + .join(":"); + } + // aes128 is a 'cipher string' for tls1.2 and below + // docs.openssl.org/1.1.1/man1/ciphers/#cipher-strings + const preferAes128 = + "AES128:TLS_AES_128_CCM_SHA256:TLS_AES_128_CCM_8_SHA256:TLS_AES_128_GCM_SHA256"; // nodejs.org/api/tls.html#tlscreateserveroptions-secureconnectionlistener + /** @type {tls.SecureContextOptions} */ const tlsOpts = { + ciphers: preferAes128 + ":" + defaultTlsCiphers, + honorCipherOrder: true, handshakeTimeout: Math.max((ioTimeoutMs / 2) | 0, 3 * 1000), // 3s in ms // blog.cloudflare.com/tls-session-resumption-full-speed-and-secure sessionTimeout: 60 * 60 * 24 * 7, // 7d in secs From 8f8a887ac883c55d12112e390412718a39e23fc6 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 17 Oct 2024 05:11:51 +0530 Subject: [PATCH 091/126] util: setImmediate is an optional node global --- src/commons/util.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commons/util.js b/src/commons/util.js index 6c4692a40e..3d1d49b9c8 100644 --- a/src/commons/util.js +++ b/src/commons/util.js @@ -204,15 +204,21 @@ 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") setImmediate(fn); + if (typeof fn === "function") { + if (typeof setImmediate === "function") setImmediate(fn); + else timeout(0, fn); + } } } From 03b51cd6f19c14ea79165c458da1aac3a8ee8e8e Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 17 Oct 2024 08:46:25 +0530 Subject: [PATCH 092/126] all: Deno v2 --- .github/workflows/deno-deploy.yml | 8 +++-- .vscode/settings.json | 3 +- README.md | 6 ++-- import_map.json | 13 ++++---- src/core/deno/config.ts | 32 +++++++++----------- src/core/node/blocklists.js | 3 ++ src/core/node/config.js | 2 +- src/server-deno.ts | 50 ++++++++++++++++++------------- src/server-node.js | 4 +-- 9 files changed, 66 insertions(+), 55 deletions(-) diff --git a/.github/workflows/deno-deploy.yml b/.github/workflows/deno-deploy.yml index b24b1b2e1f..91b26f6559 100644 --- a/.github/workflows/deno-deploy.yml +++ b/.github/workflows/deno-deploy.yml @@ -85,7 +85,7 @@ jobs: git reset git merge origin/${BUILD_BRANCH} || : - - name: πŸ¦• Install Deno @1.44 + - name: πŸ¦• Install Deno@2.x uses: denoland/setup-deno@main with: deno-version: 2.x @@ -95,7 +95,7 @@ jobs: 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 @@ -106,13 +106,15 @@ jobs: uses: denoland/deployctl@1.12.0 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/.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..0a5a766bf6 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,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 +77,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 +87,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/import_map.json b/import_map.json index 47bb695696..f68cf57a01 100644 --- a/import_map.json +++ b/import_map.json @@ -1,12 +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/", - "@riaskov/mmap-io": "https://github.com/ARyaskov/mmap-io/raw/v1.4.3/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/src/core/deno/config.ts b/src/core/deno/config.ts index e8829a9955..7c0d0e0441 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,14 +21,7 @@ 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"); @@ -38,10 +29,10 @@ async function prep() { 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/node/blocklists.js b/src/core/node/blocklists.js index 6153ce9ee8..3d28c3f251 100644 --- a/src/core/node/blocklists.js +++ b/src/core/node/blocklists.js @@ -71,6 +71,7 @@ async function fmmap(fp) { const dynimports = envutil.hasDynamicImports(); const isNode = envutil.isNode(); const isBun = envutil.isBun(); + const isDeno = envutil.isDeno(); if (dynimports && isNode) { try { @@ -90,6 +91,8 @@ async function fmmap(fp) { } 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; } diff --git a/src/core/node/config.js b/src/core/node/config.js index b69cab9c29..cce606263e 100644 --- a/src/core/node/config.js +++ b/src/core/node/config.js @@ -30,7 +30,7 @@ import * as util from "./util.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); })(); 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 580a3971a5..b6bd2c2936 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -13,9 +13,9 @@ import * as h2c from "httpx-server"; import http2 from "node:http2"; import https from "node:https"; import net, { isIPv6 } from "node:net"; -import * as os from "node:os"; +import os from "node:os"; import { finished } from "node:stream"; -import * as tls from "node:tls"; +import tls from "node:tls"; import v8 from "node:v8"; import { V2ProxyProtocol } from "proxy-protocol-js"; import * as bufutil from "./commons/bufutil.js"; From c4748e3b9fcbf9513d0d06bdb7b591005788a540 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 17 Oct 2024 09:49:11 +0530 Subject: [PATCH 093/126] log: m lint --- src/core/log.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/core/log.js b/src/core/log.js index f841cbec26..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 @@ -91,30 +91,29 @@ export default class Log { } withTags(...tags) { - const that = this; return { 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); From c7abdc29c7feea6b99f65f2a701a413c7e2b4b63 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 17 Oct 2024 21:07:01 +0530 Subject: [PATCH 094/126] node: tls session ids --- src/server-node.js | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index b6bd2c2936..974ddc3742 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -8,7 +8,9 @@ // 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 http2 from "node:http2"; import https from "node:https"; @@ -24,7 +26,6 @@ import * as dnsutil from "./commons/dnsutil.js"; import * as envutil from "./commons/envutil.js"; import * as util from "./commons/util.js"; import { handleRequest } from "./core/doh.js"; -import "./core/node/config.js"; import * as nodeutil from "./core/node/util.js"; import { stopAfter, uptime } from "./core/svc.js"; import * as system from "./system.js"; @@ -47,6 +48,9 @@ class Stats { this.noreqs = -1; this.nofchecks = 0; this.tlserr = 0; + this.fasttls = 0; + this.totfasttls = 0; + this.fasttlssize = 0; this.nofdrops = 0; this.nofconns = 0; this.openconns = 0; @@ -401,7 +405,7 @@ function systemUp() { /** * @param {string} id - * @param {... import("http2").Http2Server | net.Server} s + * @param {... http2.Http2Server | net.Server} s */ function trapServerEvents(id, s) { const ioTimeoutMs = envutil.ioTimeoutMs(); @@ -516,13 +520,36 @@ function trapSecureServerEvents(id, s) { }); const rottm = util.repeat(86400000 * 7, () => rotateTkt(s)); // 7d + s.on("close", () => clearInterval(rottm)); s.on("error", (err) => { log.e("tls: stop! server error; " + err.message, err); stopAfter(0); }); - s.on("close", () => clearInterval(rottm)); + // bajtos.net/posts/2013-08-07-improve-the-performance-of-the-node-js-https-server + // session tickets take precedence over session ids; -no_ticket is needed + // openssl s_client -connect :10000 -reconnect -tls1_2 -no_ticket + // on bun, since session tickets cannot be set by programs (though may be vended + // by the runtime itself), session ids may come in handy. + s.on("newSession", (id, data, next) => { + const hid = bufutil.hex(id); + tlsSessions.put(hid, data); + // log.d("tls: new session;", hid); + next(); + }); + + s.on("resumeSession", (id, next) => { + const hid = bufutil.hex(id); + + const data = tlsSessions.get(hid) || null; + // log.d("tls: resume;", hid, "ok?", data != null); + if (data) stats.fasttls += 1; + else stats.totfasttls += 1; + stats.fasttlssize += bufutil.len(data); + + next(/* err*/ null, null); + }); // emitted when the req is discarded due to maxConnections s.on("drop", (data) => { @@ -570,6 +597,8 @@ function rotateTkt(s) { ctx = cur + ctx; } + // tls session resumption with tickets (or ids) reduce the 3.5kb to 6.5kb + // overhead associated with tls handshake: netsekure.org/2010/03/tls-overhead nodecrypto .tkt48(seed, ctx) .then((k) => s.setTicketKeys(k)) // not supported on bun @@ -1010,7 +1039,7 @@ async function resolveQuery(rxid, q, host, flag) { } } -async function serve200(req, res) { +function serve200(req, res) { log.d("-------------> http-check req", req.method, req.url); stats.nofchecks += 1; res.writeHead(200); @@ -1022,10 +1051,9 @@ async function serve200(req, res) { * @param {Http2ServerRequest} req * @param {Http2ServerResponse} res */ -async function serveHTTPS(req, res) { +function serveHTTPS(req, res) { trapRequestResponseEvents(req, res); const ua = req.headers["user-agent"]; - const buffers = []; // if using for await loop, then it must be wrapped in a From aec46693b413e712358edfd5c8c573859e43b66a Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 17 Oct 2024 21:08:36 +0530 Subject: [PATCH 095/126] node: dynamically size tls record fragment --- src/server-node.js | 140 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 114 insertions(+), 26 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index 974ddc3742..79c8ecd099 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -50,7 +50,7 @@ class Stats { this.tlserr = 0; this.fasttls = 0; this.totfasttls = 0; - this.fasttlssize = 0; + this.noftlsadjs = 0; this.nofdrops = 0; this.nofconns = 0; this.openconns = 0; @@ -62,20 +62,41 @@ class Stats { 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>} */ + /** @type {Array>} */ this.connmap = []; + this.reports = []; /** @type {Array} */ this.srvs = []; } @@ -101,7 +122,7 @@ class Tracker { } /** - * @param {Socket} sock + * @param {Socket?} sock * @returns {string} */ cid(sock) { @@ -161,16 +182,36 @@ class Tracker { const connid = this.cid(sock); const cmap = this.connmap[mapid]; if (!this.valid(mapid) || !this.valid(connid) || !cmap) { - log.w("trackConn: server/socket not tracked?", mapid, connid); + log.d("trackConn: server/socket not tracked?", mapid, connid); return this.zeroid; } - cmap.set(connid, sock); + cmap.set(connid, new ConnW(sock)); sock.on("close", (haderr) => cmap.delete(connid)); return connid; } + /** + * + * @param {Socket|TLSSocket|null} sock + * @returns {SoReport?} rep + */ + sorep(sock) { + const connid = this.cid(sock); + if (!this.valid(connid)) return null; // unlikely + + for (const cmap of this.connmap) { + if (!cmap) continue; + const connw = cmap.get(connid); + if (connw != null) return connw.rep; + } + return null; // sock not tracked! + } + + /** + * @returns {[Array, Array>]} + */ end() { const srvs = this.srvs; const cmap = this.connmap; @@ -184,6 +225,11 @@ class Tracker { const zero6 = "::"; const tracker = new Tracker(); const stats = new Stats(); +// blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency +// 1369 - (1500 - 1280) = 1149 +const tlsStartFragmentSize = 1149; // bytes +const tlsMaxFragmentSize = 16 << 10; // 16kb +const tlsSessions = new LfuCache("tlsSessions", 10000); const cpucount = os.cpus().length || 1; const adjPeriodSec = 5; const maxHeapSnaps = 20; @@ -198,7 +244,7 @@ let adjTimer = null; system.pub("prepare"); })(); -async function systemDown() { +function systemDown() { // system-down even may arrive even before the process has had the chance // to start, in which case globals like env and log may not be available const upmins = (uptime() / 60000) | 0; @@ -220,8 +266,8 @@ async function systemDown() { for (const m of cmap) { if (!m) continue; console.warn("W closing...", m.size, "connections"); - for (const sock of m.values()) { - close(sock); + for (const v of m.values()) { + close(v.socket); } } @@ -491,9 +537,8 @@ function trapSecureServerEvents(id, s) { return; } - // blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency - // 1369 - (1500 - 1280) = 1149 - socket.setMaxSendFragment(1149); + // github.com/nodejs/node-v0.x-archive/issues/6889 + socket.setMaxSendFragment(tlsStartFragmentSize); socket.setTimeout(ioTimeoutMs, () => { stats.noftimeouts += 1; @@ -546,7 +591,6 @@ function trapSecureServerEvents(id, s) { // log.d("tls: resume;", hid, "ok?", data != null); if (data) stats.fasttls += 1; else stats.totfasttls += 1; - stats.fasttlssize += bufutil.len(data); next(/* err*/ null, null); }); @@ -864,7 +908,9 @@ function serveTLS(socket) { log.d("----> dot request", host, flag); socket.on("data", (data) => { - handleTCPData(socket, data, sb, host, flag); + const len = handleTCPData(socket, data, sb, host, flag); + + adjustTLSFragAfterWrites(socket, len); }); } @@ -891,10 +937,11 @@ function serveTCP(socket) { * @param {ScratchBuffer} sb - Scratch buffer * @param {String} host - Hostname * @param {String} flag - Blocklist Flag + * @returns {int} n - bytes sent */ function handleTCPData(socket, chunk, sb, host, flag) { const cl = chunk.byteLength; - if (cl <= 0) return; + if (cl <= 0) return 0; // read header first which contains length(dns-query) const rem = dnsutil.dnsHeaderSize - sb.qlenBufOffset; @@ -907,18 +954,18 @@ function handleTCPData(socket, chunk, sb, host, flag) { // header has not been read fully, yet; expect more data // www.rfc-editor.org/rfc/rfc7766#section-8 - if (sb.qlenBufOffset !== dnsutil.dnsHeaderSize) return; + if (sb.qlenBufOffset !== dnsutil.dnsHeaderSize) return 0; const qlen = sb.qlenBuf.readUInt16BE(); if (!dnsutil.validateSize(qlen)) { log.w(`tcp: query size err: ql:${qlen} cl:${cl} rem:${rem}`); close(socket); - return; + return 0; } // rem bytes already read, is any more left in chunk? const size = cl - rem; - if (size <= 0) return; + if (size <= 0) return 0; // gobble up at most qlen bytes from chunk starting rem-th byte const qlimit = rem + Math.min(qlen - sb.qBufOffset, size); // hopefully fast github.com/nodejs/node/issues/20130#issuecomment-382417255 @@ -933,17 +980,20 @@ function handleTCPData(socket, chunk, sb, host, flag) { sb.qBufOffset += data.byteLength; log.d(`tcp: q: ${qlen}, sb.q: ${sb.qBufOffset}, cl: ${cl}, sz: ${size}`); + let n = 0; // exactly qlen bytes read till now, handle the dns query if (sb.qBufOffset === qlen) { // extract out the query and reset the scratch-buffer const b = sb.reset(); - handleTCPQuery(b, socket, host, flag); + n += handleTCPQuery(b, socket, host, flag); + // if there is any out of band data, handle it if (!bufutil.emptyBuf(oob)) { log.d(`tcp: pipelined, handle oob: ${oob.byteLength}`); - handleTCPData(socket, oob, sb, host, flag); + n += handleTCPData(socket, oob, sb, host, flag); } } // continue reading from socket + return n; } /** @@ -951,23 +1001,27 @@ function handleTCPData(socket, chunk, sb, host, flag) { * @param {TLSSocket} socket * @param {String} host * @param {String} flag + * @returns {int} n - bytes sent */ async function handleTCPQuery(q, socket, host, flag) { heartbeat(); + let n = 0; let ok = true; - if (bufutil.emptyBuf(q) || !tcpOkay(socket)) return; + if (bufutil.emptyBuf(q) || !tcpOkay(socket)) return 0; + /** @type {Uint8Array?} */ + let r = null; const rxid = util.xid(); try { - const r = await resolveQuery(rxid, q, host, flag); + r = await resolveQuery(rxid, q, host, flag); if (bufutil.emptyBuf(r)) { log.w(rxid, "tcp: empty ans from resolver"); ok = false; } else { const rlBuf = bufutil.encodeUint8ArrayBE(r.byteLength, 2); const data = new Uint8Array([...rlBuf, ...r]); - measuredWrite(rxid, socket, data); + n = measuredWrite(rxid, socket, data); } } catch (e) { ok = false; @@ -978,12 +1032,15 @@ async function handleTCPQuery(q, socket, host, flag) { if (!ok) { close(socket); } // else: expect pipelined queries on the same socket + + return n; } /** * @param {string} rxid * @param {Socket} socket * @param {Uint8Array} data + * @param {int} n - bytes written to socket */ function measuredWrite(rxid, socket, data) { let ok = tcpOkay(socket); @@ -991,7 +1048,7 @@ function measuredWrite(rxid, socket, data) { if (!ok) { log.w(rxid, "tcp: send fail, socket not writable", bufutil.len(data)); close(socket); - return; + return 0; } // nodejs.org/en/docs/guides/backpressuring-in-streams // stackoverflow.com/a/18933853 @@ -1004,6 +1061,7 @@ function measuredWrite(rxid, socket, data) { socket.resume(); }); } + return bufutil.len(data); } /** * @param {String} rxid @@ -1116,11 +1174,13 @@ async function handleHTTPRequest(b, req, res) { // ans may be null on non-2xx responses, such as redirects (3xx) by cc.js // or 4xx responses on timeouts or 5xx on invalid http method const ans = await fRes.arrayBuffer(); + const sz = bufutil.len(ans); if (!resOkay(res)) { throw new Error("res not writable 2"); - } else if (!bufutil.emptyBuf(ans)) { + } else if (sz > 0) { res.end(bufutil.normalize8(ans)); + adjustTLSFragAfterWrites(res.socket, sz); } else { // expect fRes.status to be set to non 2xx above res.end(); @@ -1187,6 +1247,34 @@ function heartbeat() { } } +/** + * github.com/nodejs/node-v0.x-archive/issues/6889 + * github.com/golang/go/blob/ef3e1dae2f/src/crypto/tls/conn.go#L895 + * @param {TLSSocket?} socket + * @param {SoReport?} rep + * @param {int} sz + */ +function adjustTLSFragAfterWrites(socket, sz, rep = tracker.sorep(socket)) { + if (sz <= 0) return; // also skip lastsnd + if (socket == null) return; + if (rep == null) return; + + const now = Date.now(); + if (now - rep.lastsnd > 1000) { + // reset tx time threshold: 1s + socket.setMaxSendFragment(tlsStartFragmentSize); + rep.tx = sz; + } else if (rep.tx > tlsStartFragmentSize * 1000) { + stats.noftlsadjs += 1; + + // boost upto max thres: 1139 * 1000 = ~128kb or ~1000 frags + socket.setMaxSendFragment(tlsMaxFragmentSize); + } // else: adaptively set to (sz * est fragments rcvd so far) + rep.lastsnd = now; + rep.tx += sz; + // socket.setMaxSendFragment(sz * sb.tx/tlsStartFragmentSize) +} + function adjustMaxConns(n) { const isNode = envutil.isNode(); const notCloud = envutil.onLocal(); From 9bb30dfdcaf0ac6abb203474807a75430c573b34 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Thu, 17 Oct 2024 22:15:22 +0530 Subject: [PATCH 096/126] node: rmv irrelevant comment --- src/server-node.js | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index 79c8ecd099..bee5dd7dbb 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -313,6 +313,7 @@ function systemUp() { } // nodejs.org/api/net.html#netcreateserveroptions-connectionlistener + /** @type {net.ServerOpts} */ const serverOpts = { keepAlive: true, noDelay: true, @@ -359,13 +360,10 @@ function systemUp() { // TODO: ProxyProtoV2 with TLS ClientHello (unsupported by Fly.io, rn) // DNS over TLS Cleartext - const dotct = net - // serveTCP must eventually call machines-heartbeat - .createServer(serverOpts, serveTCP) - .listen(dotOpts, () => { - up("DoT Cleartext", dotct.address()); - trapServerEvents("dotct", dotct); - }); + const dotct = net.createServer(serverOpts, serveTCP).listen(dotOpts, () => { + up("DoT Cleartext", dotct.address()); + trapServerEvents("dotct", dotct); + }); // DNS over HTTPS Cleartext // Same port for http1.1/h2 does not work on node without tls, that is, @@ -376,7 +374,6 @@ function systemUp() { // Ref (for clients): github.com/nodejs/node/issues/31759 // Impl: stackoverflow.com/a/42019773 const dohct = h2c - // serveHTTPS must eventually invoke machines-heartbeat .createServer(serverOpts, serveHTTPS) .listen(dohOpts, () => { up("DoH Cleartext", dohct.address()); @@ -400,29 +397,22 @@ function systemUp() { /** @type {net.ListenOptions} */ const dot2Opts = { port: portdot2, host: zero6, backlog: tcpbacklog }; // DNS over TLS - const dot1 = tls - // serveTLS must eventually invoke machines-heartbeat - .createServer(secOpts, serveTLS) - .listen(dot1Opts, () => { - up("DoT", dot1.address()); - trapSecureServerEvents("dot1", dot1); - }); + const dot1 = tls.createServer(secOpts, serveTLS).listen(dot1Opts, () => { + up("DoT", dot1.address()); + trapSecureServerEvents("dot1", dot1); + }); // DNS over TLS w ProxyProto const dot2 = envutil.isDotOverProxyProto() && - net - // serveDoTProxyProto must evenually invoke machines-heartbeat - .createServer(serverOpts, serveDoTProxyProto) - .listen(dot2Opts, () => { - up("DoT ProxyProto", dot2.address()); - trapServerEvents("dot2", dot2); - }); + net.createServer(serverOpts, serveDoTProxyProto).listen(dot2Opts, () => { + up("DoT ProxyProto", dot2.address()); + trapServerEvents("dot2", dot2); + }); // DNS over HTTPS if (isNode) { const doh = http2 - // serveHTTPS must eventually invoke machines-heartbeat .createSecureServer({ ...secOpts, ...h2Opts }, serveHTTPS) .listen(dohOpts, () => { up("DoH2", doh.address()); From 74bdcf62c6ce4d77d68c54a1b9f8d8f14867771a Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 18 Oct 2024 00:26:31 +0530 Subject: [PATCH 097/126] deno: v2 supports node:http2 --- src/server-node.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index bee5dd7dbb..42d84b2719 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -297,7 +297,7 @@ function systemUp() { const maxconns = envutil.maxconns(); // see also: dns-transport.js:ioTimeout const ioTimeoutMs = envutil.ioTimeoutMs(); - const isNode = envutil.isNode(); + const supportsHttp2 = envutil.isNode() || envutil.isDeno(); const isBun = envutil.isBun(); if (downloadmode) { @@ -411,7 +411,7 @@ function systemUp() { }); // DNS over HTTPS - if (isNode) { + if (supportsHttp2) { const doh = http2 .createSecureServer({ ...secOpts, ...h2Opts }, serveHTTPS) .listen(dohOpts, () => { From d09e8998bf88f69ed64de3c23866efcce6cfae56 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 18 Oct 2024 00:27:45 +0530 Subject: [PATCH 098/126] node: await on tcp-tls write promise --- src/server-node.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index 42d84b2719..6e8476b0f1 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -897,9 +897,8 @@ function serveTLS(socket) { const sb = new ScratchBuffer(); log.d("----> dot request", host, flag); - socket.on("data", (data) => { - const len = handleTCPData(socket, data, sb, host, flag); - + socket.on("data", async (data) => { + const len = await handleTCPData(socket, data, sb, host, flag); adjustTLSFragAfterWrites(socket, len); }); } @@ -927,9 +926,9 @@ function serveTCP(socket) { * @param {ScratchBuffer} sb - Scratch buffer * @param {String} host - Hostname * @param {String} flag - Blocklist Flag - * @returns {int} n - bytes sent + * @returns {Promise} n - bytes sent */ -function handleTCPData(socket, chunk, sb, host, flag) { +async function handleTCPData(socket, chunk, sb, host, flag) { const cl = chunk.byteLength; if (cl <= 0) return 0; @@ -975,7 +974,7 @@ function handleTCPData(socket, chunk, sb, host, flag) { if (sb.qBufOffset === qlen) { // extract out the query and reset the scratch-buffer const b = sb.reset(); - n += handleTCPQuery(b, socket, host, flag); + n += await handleTCPQuery(b, socket, host, flag); // if there is any out of band data, handle it if (!bufutil.emptyBuf(oob)) { @@ -991,7 +990,7 @@ function handleTCPData(socket, chunk, sb, host, flag) { * @param {TLSSocket} socket * @param {String} host * @param {String} flag - * @returns {int} n - bytes sent + * @returns {Promise} n - bytes sent */ async function handleTCPQuery(q, socket, host, flag) { heartbeat(); @@ -1245,7 +1244,7 @@ function heartbeat() { * @param {int} sz */ function adjustTLSFragAfterWrites(socket, sz, rep = tracker.sorep(socket)) { - if (sz <= 0) return; // also skip lastsnd + if (typeof sz !== "number" || sz <= 0) return; // also skip lastsnd if (socket == null) return; if (rep == null) return; From 7b55ed8574e91c30b295d9933eb0635b48138f7f Mon Sep 17 00:00:00 2001 From: ignoramous Date: Tue, 12 Nov 2024 22:50:37 +0530 Subject: [PATCH 099/126] readme: fossunited --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 0a5a766bf6..5f72d0fe0a 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). +[FOSS United](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`: From 4f89da42ed589393f385054a0655c5fa9b05082f Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 26 Apr 2025 20:52:04 +0530 Subject: [PATCH 100/126] build/pre: m debug log --- src/build/pre.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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}" From b0b1a1538aeb1991b5bc13dfe4e18e686913b12e Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 26 Apr 2025 20:53:08 +0530 Subject: [PATCH 101/126] readme: fossunited b&w png logo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f72d0fe0a..b7b6309150 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ 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). -[FOSS United](https://fossunited.org/grants)  From c5537dd7f203c59f2b86d1e295c2371f3533946a Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 26 Apr 2025 21:04:49 +0530 Subject: [PATCH 102/126] gh-action: harden pr.yml --- .github/workflows/pr.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d0e2eb4fcc..77d7712996 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -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 }} From 2d3f7b57ba7ae1db0affae04207c09c5e0aab863 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 2 May 2025 21:19:14 +0530 Subject: [PATCH 103/126] bufutil: m jsdoc --- src/commons/bufutil.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/commons/bufutil.js b/src/commons/bufutil.js index 8897091476..516fe6a31d 100644 --- a/src/commons/bufutil.js +++ b/src/commons/bufutil.js @@ -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; From 9fb8b710fc3d238e40d099da121db06033ef7866 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 2 May 2025 21:20:31 +0530 Subject: [PATCH 104/126] rdnsutil: mv blocklist timestamp fn to util --- src/commons/util.js | 28 ++++++++++++++++++++++++++ src/plugins/rdns-util.js | 43 ++++++++++------------------------------ 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/commons/util.js b/src/commons/util.js index 3d1d49b9c8..f526fe4346 100644 --- a/src/commons/util.js +++ b/src/commons/util.js @@ -84,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 + * @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) { + // 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 diff --git a/src/plugins/rdns-util.js b/src/plugins/rdns-util.js index 4a6e3c1073..2c80bfe48d 100644 --- a/src/plugins/rdns-util.js +++ b/src/plugins/rdns-util.js @@ -54,12 +54,21 @@ recBlockstamps.set("ps", "1:GNwB-ACgeQKr7cg3YXoAgA=="); /** * @param {BlocklistFilter} blf - * @returns {boolean} + * @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} @@ -508,34 +517,6 @@ export function hasBlockstamp(blockInfo) { ); } -/** - * returns true if tstamp is of form yyyy/epochMs - * @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 - * @returns {int} epoch - */ -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 @@ -545,8 +526,6 @@ export function blocklists(strflag) { const blocklists = []; if (flagVersion === "1") { return trie.flagsToTags(userBlocklistFlagUint); - } else { - throw new Error("unknown blocklist version: " + flagVersion); - } + } // unknown blocklist version return blocklists; } From 35ec217f218662ab932ea902c935005655b54824 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 3 May 2025 00:12:51 +0530 Subject: [PATCH 105/126] server-workers: m unused var --- src/server-workers.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server-workers.js b/src/server-workers.js index ed1d86a083..22ae7506e7 100644 --- a/src/server-workers.js +++ b/src/server-workers.js @@ -6,10 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import "./core/workers/config.js"; +import * as util from "./commons/util.js"; import { handleRequest } from "./core/doh.js"; +import "./core/workers/config.js"; import * as system from "./system.js"; -import * as util from "./commons/util.js"; export default { // workers/runtime-apis/fetch-event#syntax-module-worker @@ -34,7 +34,7 @@ function serveDoh(request, env, ctx) { return new Promise((accept) => { system .when("go") - .then((v) => { + .then((_v) => { return handleRequest(event); }) .then((response) => { From 5220a63e9e3f0f65a7419e5dbdb5663ddd28f4f5 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 3 May 2025 00:13:19 +0530 Subject: [PATCH 106/126] system: m unused var --- src/system.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/system.js b/src/system.js index 89871b53be..3fe0130a7f 100644 --- a/src/system.js +++ b/src/system.js @@ -29,7 +29,7 @@ const stickyEvents = new Set([ const stickyParcels = new Map(); const events = new Set([ - // when server should cease + // when process should cease "stop", ]); @@ -262,8 +262,8 @@ function safeBox(fns, arg) { } try { r.push(f(arg)); - } catch (ignore) { - // log.e("sys: safeBox err", ignore); + } catch (_) { + // log.e("sys: safeBox err", _); r.push(null); } } From a69cd2c4e17ab127de07362000ee31f792fb9f31 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 3 May 2025 00:14:27 +0530 Subject: [PATCH 107/126] env: auto renew old blocklist files --- src/commons/envutil.js | 6 ++++++ src/core/env.js | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/commons/envutil.js b/src/commons/envutil.js index bcaef53364..1284713c29 100644 --- a/src/commons/envutil.js +++ b/src/commons/envutil.js @@ -260,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/core/env.js b/src/core/env.js index 2176368150..e0ac7da4d1 100644 --- a/src/core/env.js +++ b/src/core/env.js @@ -150,6 +150,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: -1, // in weeks; negative or 0 means, never auto-renew + }, // courtesy db-ip.com/db/download/ip-to-country-lite GEOIP_URL: { type: "string", From e39d2de5bc639c359fe2202936f7a9f0473fcd75 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 3 May 2025 00:18:49 +0530 Subject: [PATCH 108/126] all: auto renew blocklists --- src/commons/util.js | 5 + src/core/deno/blocklists.ts | 70 ++++--- src/core/node/blocklists.js | 91 ++++++--- src/core/workers/config.js | 2 +- src/plugins/cache-util.js | 9 +- src/plugins/command-control/cc.js | 20 +- src/plugins/dns-op/cache-resolver.js | 11 +- src/plugins/dns-op/resolver.js | 51 +++--- src/plugins/rethinkdns/main.js | 264 ++++++++++++++++++++++----- 9 files changed, 381 insertions(+), 142 deletions(-) diff --git a/src/commons/util.js b/src/commons/util.js index f526fe4346..b430a66fd5 100644 --- a/src/commons/util.js +++ b/src/commons/util.js @@ -259,6 +259,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/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/node/blocklists.js b/src/core/node/blocklists.js index 3d28c3f251..1bd3cbe2c8 100644 --- a/src/core/node/blocklists.js +++ b/src/core/node/blocklists.js @@ -5,68 +5,74 @@ * 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 useMmap = envutil.useMmap(); - - const ok = await setupLocally(bw, timestamp, codec, useMmap); + 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; } -// fmmap mmaps file at fp for random reads, returns a Buffer backed by the file. +/** + * 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(); @@ -97,13 +103,25 @@ async function fmmap(fp) { return null; } -async function setupLocally(bw, timestamp, codec, useMmap) { +/** + * 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, "mmap?", useMmap); + log.i("on-disk ts/codec/td/rd", timestamp, codec, td, rd, "mmap?", useMmap); let tdbuf = useMmap ? await fmmap(td) : null; if (bufutil.emptyBuf(tdbuf)) { @@ -134,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/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 561d76597b..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 @@ -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..a9765e0833 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,11 @@ 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 cfg = this.bw.basicconfig(); const isBlfSetup = rdnsutil.isBlocklistFilterSetup(blf); if (!isBlfSetup) throw new Error("no blocklist-filter"); + if (!cfg) throw new Error("no basic-config"); if (command === "listtob64") { // convert blocklists (tags) to blockstamp (b64) @@ -143,7 +143,7 @@ export class CommandControl { req, queryString, blf, - this.latestTimestamp + rdnsutil.bareTimestampFrom(cfg.timestamp) ); } else if (command === "dntouint") { // convert names to flags @@ -177,7 +177,7 @@ export class CommandControl { response.data.httpResponse = configRedirect( b64UserFlag, reqUrl.origin, - this.latestTimestamp, + rdnsutil.bareTimestampFrom(cfg.timestamp), !isDnsCmd ); } else { diff --git a/src/plugins/dns-op/cache-resolver.js b/src/plugins/dns-op/cache-resolver.js index 41617f68ac..79c4a53cc1 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) { @@ -69,8 +69,9 @@ export class DNSCacheResponder { const blf = this.bw.getBlocklistFilter(); const onlyLocal = this.bw.disabled() || rdnsutil.isBlocklistFilterSetup(blf); + const timestamp = this.bw.timestamp(util.yyyymm()); - const k = cacheutil.makeHttpCacheKey(packet); + const k = cacheutil.makeHttpCacheKey(packet, timestamp); if (!k) return noAnswer; const cr = await this.cache.get(k, onlyLocal); diff --git a/src/plugins/dns-op/resolver.js b/src/plugins/dns-op/resolver.js index d37d0c543e..cf0a58051b 100644 --- a/src/plugins/dns-op/resolver.js +++ b/src/plugins/dns-op/resolver.js @@ -5,16 +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 { /** @@ -164,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); if (q.isBlocked) { - this.primeCache(rxid, q, dispatcher); + this.primeCache(rxid, ts, q, dispatcher); return q; } @@ -194,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, @@ -215,6 +217,7 @@ export default class DNSResolver { this.bw.init(rxid), this.resolveDnsUpstream( rxid, + ts, req, this.determineDohResolvers(userDns, forceUserDns), rawpacket, @@ -262,7 +265,7 @@ export default class DNSResolver { 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 @@ -273,7 +276,7 @@ export default class DNSResolver { // 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; } @@ -285,7 +288,7 @@ export default class DNSResolver { * @param {pres.BStamp?} stamps * @returns */ - 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); @@ -302,24 +305,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, "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); } @@ -332,6 +332,7 @@ export default class DNSResolver { /** * @param {String} rxid + * @param {String} ts * @param {Request} request * @param {Array} resolverUrls * @param {ArrayBuffer} query @@ -340,6 +341,7 @@ export default class DNSResolver { */ DNSResolver.prototype.resolveDnsUpstream = async function ( rxid, + ts, request, resolverUrls, query, @@ -415,7 +417,7 @@ DNSResolver.prototype.resolveDnsUpstream = async function ( 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) { @@ -464,8 +466,15 @@ 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); diff --git a/src/plugins/rethinkdns/main.js b/src/plugins/rethinkdns/main.js index f2b3ae0dad..8ec78fa4dd 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. @@ -109,6 +118,13 @@ export class BlocklistWrapper { 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 +140,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 [renewedBconfig, renewedFt] = await renew(baseurl); + + if (renewedBconfig != null && renewedFt != null) { + log.i("renewed:", bconfig.timestamp, "=>", renewedBconfig.timestamp); + bconfig = withDefaults(renewedBconfig); + ft = renewedFt; + } else { + log.w("renew failed; got: ", renewedBconfig); + } + } else { + log.d("renew 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 +197,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 +221,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 +235,61 @@ 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"); + } + } catch (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 +387,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; +} From 73b01bc52ec468eabef285765c90a1b8e3580b0d Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 3 May 2025 00:19:13 +0530 Subject: [PATCH 109/126] node/config: m log exception --- src/core/node/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/node/config.js b/src/core/node/config.js index cce606263e..89520bd851 100644 --- a/src/core/node/config.js +++ b/src/core/node/config.js @@ -101,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; } } From 56e0defe83d25197fc5deb63babe6a44c9605b79 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 3 May 2025 00:19:41 +0530 Subject: [PATCH 110/126] rdns: m jsdocs and logs --- src/plugins/rethinkdns/filter.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) 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; } } From 0476a425699770a76a2a1dda0f8a3bb91cc0f4ee Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 3 May 2025 00:21:12 +0530 Subject: [PATCH 111/126] node: adjust TLS frag size iff TLSSocket --- src/server-node.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/server-node.js b/src/server-node.js index 6e8476b0f1..ad5fa6af79 100644 --- a/src/server-node.js +++ b/src/server-node.js @@ -17,7 +17,7 @@ import https from "node:https"; import net, { isIPv6 } from "node:net"; import os from "node:os"; import { finished } from "node:stream"; -import tls from "node:tls"; +import tls, { TLSSocket } from "node:tls"; import v8 from "node:v8"; import { V2ProxyProtocol } from "proxy-protocol-js"; import * as bufutil from "./commons/bufutil.js"; @@ -32,7 +32,6 @@ import * as system from "./system.js"; /** * @typedef {net.Socket} Socket - * @typedef {tls.TLSSocket} TLSSocket * @typedef {http2.Http2ServerRequest} Http2ServerRequest * @typedef {http2.Http2ServerResponse} Http2ServerResponse */ @@ -1168,8 +1167,8 @@ async function handleHTTPRequest(b, req, res) { if (!resOkay(res)) { throw new Error("res not writable 2"); } else if (sz > 0) { - res.end(bufutil.normalize8(ans)); adjustTLSFragAfterWrites(res.socket, sz); + res.end(bufutil.normalize8(ans)); } else { // expect fRes.status to be set to non 2xx above res.end(); @@ -1245,7 +1244,7 @@ function heartbeat() { */ function adjustTLSFragAfterWrites(socket, sz, rep = tracker.sorep(socket)) { if (typeof sz !== "number" || sz <= 0) return; // also skip lastsnd - if (socket == null) return; + if (socket == null || !(socket instanceof TLSSocket)) return; if (rep == null) return; const now = Date.now(); From 9c3f0db0e89ec4b0caacf4ebc6cf3a72298d9625 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 3 May 2025 00:21:52 +0530 Subject: [PATCH 112/126] wrangler: set auto renew to 42 weeks forks of this project will be stuck in some desolate blocklist ver if they do not deploy (and thus run build/pre.sh) script regularly (weekly, for ex). setting auto renewal for blocklist files to 42 weeks, to make sure these don't continue to use very old blocklist versions. In fact, older blocklists files are auto-deleted by our project after 300 days. --- src/plugins/dns-op/cache-resolver.js | 18 ++++++++++-------- src/plugins/dns-op/resolver.js | 6 +++--- src/plugins/rethinkdns/main.js | 20 ++++++++++++-------- wrangler.toml | 3 +++ 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/plugins/dns-op/cache-resolver.js b/src/plugins/dns-op/cache-resolver.js index 79c4a53cc1..8e4f9a0033 100644 --- a/src/plugins/dns-op/cache-resolver.js +++ b/src/plugins/dns-op/cache-resolver.js @@ -67,17 +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 timestamp = this.bw.timestamp(util.yyyymm()); + 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, timestamp); + 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, @@ -117,7 +119,7 @@ export class DNSCacheResponder { 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; } @@ -130,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/resolver.js b/src/plugins/dns-op/resolver.js index cf0a58051b..3c046e899c 100644 --- a/src/plugins/dns-op/resolver.js +++ b/src/plugins/dns-op/resolver.js @@ -171,7 +171,7 @@ export default class DNSResolver { 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, ts, q, dispatcher); @@ -271,7 +271,7 @@ export default class DNSResolver { // check outgoing cached dns-packet against blocklists this.blocker.blockAnswer(rxid, /* out*/ r, blInfo); const fromCache = cacheutil.hasCacheHeader(res.headers); - this.log.d(rxid, "ansblock?", r.isBlocked, "fromcache?", 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 @@ -478,7 +478,7 @@ DNSResolver.prototype.resolveDnsFromCache = async function (rxid, ts, packet) { 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/rethinkdns/main.js b/src/plugins/rethinkdns/main.js index 8ec78fa4dd..485a1a2fc7 100644 --- a/src/plugins/rethinkdns/main.js +++ b/src/plugins/rethinkdns/main.js @@ -71,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); @@ -91,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 @@ -105,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; } @@ -112,6 +113,7 @@ 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"; @@ -159,17 +161,17 @@ export class BlocklistWrapper { const blocklistAgeThresWeeks = envutil.renewBlocklistsThresholdInWeeks(); const bltimestamp = util.bareTimestampFrom(cfg.timestamp()); if (isPast(bltimestamp, blocklistAgeThresWeeks)) { - const [renewedBconfig, renewedFt] = await renew(baseurl); + const [renewCfg, renewedFt] = await renew(baseurl); - if (renewedBconfig != null && renewedFt != null) { - log.i("renewed:", bconfig.timestamp, "=>", renewedBconfig.timestamp); - bconfig = withDefaults(renewedBconfig); + if (renewCfg != null && renewedFt != null) { + this.log.i(rxid, "r:", bconfig.timestamp, "=>", renewCfg.timestamp); + bconfig = withDefaults(renewCfg); ft = renewedFt; } else { - log.w("renew failed; got: ", renewedBconfig); + this.log.w(rxid, "r: failed; got:", renewCfg); } } else { - log.d("renew not needed for:", bltimestamp); + this.log.d(rxid, "r: not needed for:", bltimestamp); } } @@ -278,7 +280,9 @@ export class BlocklistWrapper { 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; } diff --git a/wrangler.toml b/wrangler.toml index 767ebbd9c4..0ed6a8dc2f 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -28,6 +28,7 @@ globs = ["**/*.js"] LOG_LEVEL = "debug" WORKER_ENV = "development" CLOUD_PLATFORM = "cloudflare" +AUTO_RENEW_BLOCKLISTS_OLDER_THAN = "42" # weeks ################## #------PROD------# @@ -52,6 +53,7 @@ routes = [ LOG_LEVEL = "info" WORKER_ENV = "production" CLOUD_PLATFORM = "cloudflare" +AUTO_RENEW_BLOCKLISTS_OLDER_THAN = "42" # weeks ################## #------ONE-------# @@ -75,6 +77,7 @@ LOG_LEVEL = "logpush" WORKER_ENV = "production" CLOUD_PLATFORM = "cloudflare" CF_LOGPUSH_R2_PATH = "qlog/" +AUTO_RENEW_BLOCKLISTS_OLDER_THAN = "42" # weeks ################## #-----SECRETS----# From cc9b438d01417621f301abcb0c8d1d67a5b60269 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 3 May 2025 04:46:46 +0530 Subject: [PATCH 113/126] gh-actions: update ossf/scorecard versions --- .github/workflows/scorecard.yml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 36792dd644..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,6 +26,7 @@ jobs: # contents: read # actions: read + # ref: github.com/ossf/scorecard/blob/main/.github/workflows/scorecard-analysis.yml steps: - name: "Checkout code" uses: actions/checkout@v4 @@ -37,20 +34,20 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@v2.3.1 + 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 From a6405a17da0d975cf666b5f8e369784d6289cc77 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 3 May 2025 22:50:35 +0530 Subject: [PATCH 114/126] gh-action: m fix account id input for cf --- .github/workflows/cf.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cf.yml b/.github/workflows/cf.yml index 00b920706f..6a30eb7396 100644 --- a/.github/workflows/cf.yml +++ b/.github/workflows/cf.yml @@ -114,8 +114,9 @@ jobs: # 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 }} + # setting CLOUDFLARE_ACCOUNT_ID no longer works GIT_COMMIT_ID: ${{ env.COMMIT_SHA }} - name: 🎀 Notice From 6d37a7a1508d5a326a5f12a6c6afdbed56f122be Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 3 May 2025 22:55:20 +0530 Subject: [PATCH 115/126] deno: s/deno_env/deno_env_domain/g --- src/core/deno/config.ts | 2 +- src/core/env.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/core/deno/config.ts b/src/core/deno/config.ts index 7c0d0e0441..42ee59494e 100644 --- a/src/core/deno/config.ts +++ b/src/core/deno/config.ts @@ -25,7 +25,7 @@ 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"; diff --git a/src/core/env.js b/src/core/env.js index e0ac7da4d1..72f08b418e 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", }, @@ -302,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; } From 5cd15f91384835ae91adde9d675a86f3b2ab6793 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sun, 4 May 2025 00:23:24 +0530 Subject: [PATCH 116/126] dns/resolver: m jsdoc --- src/plugins/dns-op/resolver.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/dns-op/resolver.js b/src/plugins/dns-op/resolver.js index 3c046e899c..00b667d8ce 100644 --- a/src/plugins/dns-op/resolver.js +++ b/src/plugins/dns-op/resolver.js @@ -286,7 +286,8 @@ 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. */ makeRdnsResponse(rxid, raw, blf, stamps = null) { if (!raw) throw new Error(rxid + " mk-res no upstream result"); From 9c1d6dc564b3d0ddd52fd914d6bc24ddbf9adc0e Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sun, 4 May 2025 00:34:09 +0530 Subject: [PATCH 117/126] gh-actions: deno deployctl ver upgrade --- .github/workflows/deno-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deno-deploy.yml b/.github/workflows/deno-deploy.yml index 91b26f6559..5fb1f57fa5 100644 --- a/.github/workflows/deno-deploy.yml +++ b/.github/workflows/deno-deploy.yml @@ -103,7 +103,7 @@ jobs: - name: 🀸🏼 Deploy to deno.com id: dd if: ${{ env.DEPLOY_MODE == 'action' }} - uses: denoland/deployctl@1.12.0 + uses: denoland/deployctl@v1 with: project: ${{ env.PROJECT_NAME }} # todo: if bundling, replace IN_FILE w/ OUT_FILE From f82ceffb3d5f326c74993f8db707053bdee3f36f Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sun, 4 May 2025 00:37:48 +0530 Subject: [PATCH 118/126] deno: skip deno-specific dep lockfile --- deno.json | 1 + 1 file changed, 1 insertion(+) diff --git a/deno.json b/deno.json index 7bda94852c..64670e2022 100644 --- a/deno.json +++ b/deno.json @@ -4,6 +4,7 @@ }, "importMap": "import_map.json", "nodeModulesDir": "auto", + "lock": false, "lint": { "files": { "exclude": ["**/**"] From 586546dfc134796dd3c8266e4b3590b83270f1be Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 17 May 2025 21:28:43 +0530 Subject: [PATCH 119/126] core/doh: m lint, jsdocs --- src/commons/util.js | 15 ++++++++++++++- src/core/doh.js | 10 +++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/commons/util.js b/src/commons/util.js index b430a66fd5..51cad1d94a 100644 --- a/src/commons/util.js +++ b/src/commons/util.js @@ -185,6 +185,13 @@ export function timedOp(op, ms, cleanup = (x) => {}) { // 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 @@ -215,7 +222,7 @@ export function timedSafeAsyncOp(promisedOp, ms, defaultOp) { resolve(out); } }) - .catch((ignored) => { + .catch((_) => { clearTimeout(tid); if (!timedout) deferredOp(); // else: handled by timeout @@ -223,6 +230,12 @@ export function timedSafeAsyncOp(promisedOp, ms, defaultOp) { }); } +/** + * + * @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); diff --git a/src/core/doh.js b/src/core/doh.js index fc0749409b..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); From 0ae09de42d5b197961e32b527a21b7d3f677d8e3 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 17 May 2025 21:29:29 +0530 Subject: [PATCH 120/126] fly: attempt blocklist auto-renew if older than 2w --- fly.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fly.toml b/fly.toml index e548706e8e..c23a31e2e1 100644 --- a/fly.toml +++ b/fly.toml @@ -18,6 +18,8 @@ kill_timeout = "15s" 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 From 4cedb70abac3f56ac265817813ffc6015de3c0e2 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Sat, 17 May 2025 21:32:39 +0530 Subject: [PATCH 121/126] all: global auto blocklist renewals at 42 weeks --- src/core/env.js | 2 +- wrangler.toml | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core/env.js b/src/core/env.js index 72f08b418e..b6b48f807c 100644 --- a/src/core/env.js +++ b/src/core/env.js @@ -154,7 +154,7 @@ const defaults = new Map( // auto renew blocklists if they are older than these many weeks AUTO_RENEW_BLOCKLISTS_OLDER_THAN: { type: "number", - default: -1, // in weeks; negative or 0 means, never auto-renew + default: 42, // in weeks; negative or 0 means, never auto-renew }, // courtesy db-ip.com/db/download/ip-to-country-lite GEOIP_URL: { diff --git a/wrangler.toml b/wrangler.toml index 0ed6a8dc2f..69ce1cb9bf 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -28,7 +28,6 @@ globs = ["**/*.js"] LOG_LEVEL = "debug" WORKER_ENV = "development" CLOUD_PLATFORM = "cloudflare" -AUTO_RENEW_BLOCKLISTS_OLDER_THAN = "42" # weeks ################## #------PROD------# @@ -53,7 +52,6 @@ routes = [ LOG_LEVEL = "info" WORKER_ENV = "production" CLOUD_PLATFORM = "cloudflare" -AUTO_RENEW_BLOCKLISTS_OLDER_THAN = "42" # weeks ################## #------ONE-------# From ee765cce745b991266f1084cd06901368162d772 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 6 Jun 2025 21:12:25 +0530 Subject: [PATCH 122/126] lint: commons --- src/commons/bufutil.js | 2 +- src/commons/dnsutil.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commons/bufutil.js b/src/commons/bufutil.js index 516fe6a31d..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) { diff --git a/src/commons/dnsutil.js b/src/commons/dnsutil.js index 37b9006db3..35c7723800 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; } } From a5d4b0cd40176910baff753ac9688c55c85e7f50 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 6 Jun 2025 21:13:13 +0530 Subject: [PATCH 123/126] dnsutil: m add buf len to decode errs --- src/commons/dnsutil.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commons/dnsutil.js b/src/commons/dnsutil.js index 35c7723800..ca872915f8 100644 --- a/src/commons/dnsutil.js +++ b/src/commons/dnsutil.js @@ -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); } From 50e4ef13af8d6fffabe1fe910343995e76927e61 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 6 Jun 2025 21:13:39 +0530 Subject: [PATCH 124/126] util: m jsdoc --- src/commons/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/util.js b/src/commons/util.js index 51cad1d94a..517942b41d 100644 --- a/src/commons/util.js +++ b/src/commons/util.js @@ -97,7 +97,7 @@ function isValidFullTimestamp(tstamp) { /** * from: github.com/celzero/downloads/blob/main/src/timestamp.js - * @type {string} tstamp is of form epochMs ("1740866164283") or yyyy/epochMs ("2025/1740866164283") + * @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) { From 070ac8d9bb2a609d6e848fb7c8d08720527ee965 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 6 Jun 2025 21:14:21 +0530 Subject: [PATCH 125/126] cc: use refactored blocklist filter timestamp api --- src/plugins/command-control/cc.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/plugins/command-control/cc.js b/src/plugins/command-control/cc.js index a9765e0833..801a2223ad 100644 --- a/src/plugins/command-control/cc.js +++ b/src/plugins/command-control/cc.js @@ -123,11 +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 cfg = this.bw.basicconfig(); - const isBlfSetup = rdnsutil.isBlocklistFilterSetup(blf); - - if (!isBlfSetup) throw new Error("no blocklist-filter"); - if (!cfg) throw new Error("no basic-config"); + 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, - rdnsutil.bareTimestampFrom(cfg.timestamp) + blf ); } else if (command === "dntouint") { // convert names to flags @@ -177,7 +174,7 @@ export class CommandControl { response.data.httpResponse = configRedirect( b64UserFlag, reqUrl.origin, - rdnsutil.bareTimestampFrom(cfg.timestamp), + 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, From d8868b26837cda83752ca025aac50d3b29d102f5 Mon Sep 17 00:00:00 2001 From: Murtaza Aliakbar Date: Fri, 6 Jun 2025 21:14:37 +0530 Subject: [PATCH 126/126] dns-op: m jsdoc --- src/plugins/dns-op/resolver.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/dns-op/resolver.js b/src/plugins/dns-op/resolver.js index 00b667d8ce..f5ba2a72af 100644 --- a/src/plugins/dns-op/resolver.js +++ b/src/plugins/dns-op/resolver.js @@ -335,7 +335,7 @@ 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} @@ -423,7 +423,7 @@ DNSResolver.prototype.resolveDnsUpstream = async function ( // 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; }