pending-dns 1.2.5 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/codeql/codeql-config.yml +11 -0
- package/.github/workflows/codeql.yml +52 -0
- package/.github/workflows/deploy.yml +16 -3
- package/.github/workflows/release.yaml +43 -0
- package/.github/workflows/test.yml +75 -0
- package/.release-please-manifest.json +3 -0
- package/CHANGELOG.md +8 -0
- package/CLAUDE.md +97 -0
- package/README.md +28 -5
- package/SECURITY.md +88 -0
- package/SECURITY.txt +27 -0
- package/bin/pending-dns.js +1 -1
- package/config/default.toml +5 -0
- package/config/test.toml +25 -0
- package/eslint.config.js +38 -0
- package/lib/api-server.js +13 -6
- package/lib/cached-resolver.js +5 -3
- package/lib/certs.js +11 -4
- package/lib/dns-handler.js +13 -8
- package/lib/dns-server.js +30 -18
- package/lib/dns-tcp-server.js +1 -1
- package/lib/dns-udp-server.js +1 -1
- package/lib/logger.js +3 -0
- package/lib/public-server.js +20 -2
- package/lib/sentry.js +72 -0
- package/lib/tools.js +1 -1
- package/lib/zone-store.js +4 -4
- package/package.json +43 -33
- package/release-please-config.json +13 -0
- package/server.js +5 -24
- package/systemd/pending-dns.service +4 -4
- package/test/api.test.js +139 -0
- package/test/cached-resolver.test.js +57 -0
- package/test/certs.test.js +34 -0
- package/test/dns-handler.test.js +140 -0
- package/test/dns-server.test.js +69 -0
- package/test/helpers.js +25 -0
- package/test/sentry.test.js +21 -0
- package/test/tools.test.js +48 -0
- package/test/zone-store.test.js +209 -0
- package/workers/api.js +3 -1
- package/workers/dns.js +2 -24
- package/workers/health.js +3 -26
- package/workers/public.js +3 -25
- package/.eslintrc +0 -14
- package/Gruntfile.js +0 -16
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
name: "PendingDNS CodeQL config"
|
|
2
|
+
|
|
3
|
+
# Exclude code that is not part of the production runtime from analysis.
|
|
4
|
+
# These paths generate only false-positive noise:
|
|
5
|
+
# - the test suite intentionally disables TLS verification and resolves
|
|
6
|
+
# throwaway domains
|
|
7
|
+
# - the example scripts are developer helpers, not shipped code
|
|
8
|
+
paths-ignore:
|
|
9
|
+
- test
|
|
10
|
+
- '**/test/**'
|
|
11
|
+
- example
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: "CodeQL Advanced"
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ "master" ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ "master" ]
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: '40 17 * * 6'
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
analyze:
|
|
13
|
+
name: Analyze (${{ matrix.language }})
|
|
14
|
+
# Skip release-please's auto-generated release PR: it only changes CHANGELOG/version,
|
|
15
|
+
# so there is no new code to scan. (Matches the guard in test.yml.)
|
|
16
|
+
if: ${{ !startsWith(github.ref_name, 'release-please--') && !startsWith(github.head_ref, 'release-please--') }}
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
permissions:
|
|
19
|
+
# required for all workflows
|
|
20
|
+
security-events: write
|
|
21
|
+
|
|
22
|
+
# required to fetch internal or private CodeQL packs
|
|
23
|
+
packages: read
|
|
24
|
+
|
|
25
|
+
# only required for workflows in private repositories
|
|
26
|
+
actions: read
|
|
27
|
+
contents: read
|
|
28
|
+
|
|
29
|
+
strategy:
|
|
30
|
+
fail-fast: false
|
|
31
|
+
matrix:
|
|
32
|
+
include:
|
|
33
|
+
- language: actions
|
|
34
|
+
build-mode: none
|
|
35
|
+
- language: javascript-typescript
|
|
36
|
+
build-mode: none
|
|
37
|
+
|
|
38
|
+
steps:
|
|
39
|
+
- name: Checkout repository
|
|
40
|
+
uses: actions/checkout@v4
|
|
41
|
+
|
|
42
|
+
- name: Initialize CodeQL
|
|
43
|
+
uses: github/codeql-action/init@v4
|
|
44
|
+
with:
|
|
45
|
+
languages: ${{ matrix.language }}
|
|
46
|
+
build-mode: ${{ matrix.build-mode }}
|
|
47
|
+
config-file: ./.github/codeql/codeql-config.yml
|
|
48
|
+
|
|
49
|
+
- name: Perform CodeQL Analysis
|
|
50
|
+
uses: github/codeql-action/analyze@v4
|
|
51
|
+
with:
|
|
52
|
+
category: "/language:${{matrix.language}}"
|
|
@@ -5,14 +5,27 @@ on:
|
|
|
5
5
|
|
|
6
6
|
name: Deploy instance
|
|
7
7
|
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
8
15
|
jobs:
|
|
9
16
|
deploy:
|
|
10
17
|
name: Deploy
|
|
11
|
-
runs-on: ubuntu-
|
|
18
|
+
runs-on: ubuntu-24.04
|
|
19
|
+
timeout-minutes: 15
|
|
12
20
|
|
|
13
21
|
steps:
|
|
14
22
|
- name: Checkout
|
|
15
|
-
uses: actions/checkout@
|
|
23
|
+
uses: actions/checkout@v6
|
|
24
|
+
|
|
25
|
+
- name: Use Node.js 24
|
|
26
|
+
uses: actions/setup-node@v6
|
|
27
|
+
with:
|
|
28
|
+
node-version: 24
|
|
16
29
|
|
|
17
30
|
- name: Install SSH key
|
|
18
31
|
uses: shimataro/ssh-key-action@v2
|
|
@@ -29,7 +42,7 @@ jobs:
|
|
|
29
42
|
id: deploy
|
|
30
43
|
run: |
|
|
31
44
|
echo $GITHUB_SHA > commit.txt
|
|
32
|
-
npm install --
|
|
45
|
+
npm install --omit=dev
|
|
33
46
|
tar czf /tmp/${SERVICE_NAME}.tar.gz --exclude .git .
|
|
34
47
|
scp /tmp/${SERVICE_NAME}.tar.gz deploy@${TARGET_HOST_01}:
|
|
35
48
|
scp /tmp/${SERVICE_NAME}.tar.gz deploy@${TARGET_HOST_02}:
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
on:
|
|
2
|
+
push:
|
|
3
|
+
branches:
|
|
4
|
+
- master
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: write
|
|
8
|
+
pull-requests: write
|
|
9
|
+
id-token: write
|
|
10
|
+
|
|
11
|
+
name: Manage Release
|
|
12
|
+
jobs:
|
|
13
|
+
release_please:
|
|
14
|
+
name: Create or prepare release
|
|
15
|
+
runs-on: ubuntu-24.04
|
|
16
|
+
outputs:
|
|
17
|
+
major: ${{ steps.release.outputs.major }}
|
|
18
|
+
minor: ${{ steps.release.outputs.minor }}
|
|
19
|
+
patch: ${{ steps.release.outputs.patch }}
|
|
20
|
+
release_created: ${{ steps.release.outputs.release_created }}
|
|
21
|
+
steps:
|
|
22
|
+
# Uses release-please-config.json and .release-please-manifest.json
|
|
23
|
+
# from the repository root (manifest mode).
|
|
24
|
+
- uses: googleapis/release-please-action@v4
|
|
25
|
+
id: release
|
|
26
|
+
|
|
27
|
+
publish:
|
|
28
|
+
name: Publish NPM package
|
|
29
|
+
runs-on: ubuntu-24.04
|
|
30
|
+
needs: release_please
|
|
31
|
+
if: ${{ needs.release_please.outputs.release_created }}
|
|
32
|
+
permissions:
|
|
33
|
+
contents: read
|
|
34
|
+
# Required for npm provenance / OIDC trusted publishing.
|
|
35
|
+
id-token: write
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v6
|
|
38
|
+
- uses: actions/setup-node@v6
|
|
39
|
+
with:
|
|
40
|
+
node-version: 24
|
|
41
|
+
registry-url: "https://registry.npmjs.org"
|
|
42
|
+
- run: npm ci
|
|
43
|
+
- run: npm publish --provenance --access public
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: Run Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
# Skip CI for release-please's auto-generated release PR / branch: it only bumps the
|
|
16
|
+
# version + CHANGELOG (no code change), so re-running the suite is wasted work.
|
|
17
|
+
license_check:
|
|
18
|
+
name: License Compliance Check
|
|
19
|
+
if: ${{ !startsWith(github.ref_name, 'release-please--') && !startsWith(github.head_ref, 'release-please--') }}
|
|
20
|
+
runs-on: ubuntu-24.04
|
|
21
|
+
timeout-minutes: 10
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v6
|
|
24
|
+
- name: Use Node.js 24
|
|
25
|
+
uses: actions/setup-node@v6
|
|
26
|
+
with:
|
|
27
|
+
node-version: 24
|
|
28
|
+
cache: npm
|
|
29
|
+
- run: npm install
|
|
30
|
+
- name: Run License Checks
|
|
31
|
+
run: npm run licenses
|
|
32
|
+
|
|
33
|
+
lint:
|
|
34
|
+
name: Lint
|
|
35
|
+
if: ${{ !startsWith(github.ref_name, 'release-please--') && !startsWith(github.head_ref, 'release-please--') }}
|
|
36
|
+
runs-on: ubuntu-24.04
|
|
37
|
+
timeout-minutes: 10
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v6
|
|
40
|
+
- name: Use Node.js 24
|
|
41
|
+
uses: actions/setup-node@v6
|
|
42
|
+
with:
|
|
43
|
+
node-version: 24
|
|
44
|
+
cache: npm
|
|
45
|
+
- run: npm install
|
|
46
|
+
- name: Run ESLint
|
|
47
|
+
run: npm run lint
|
|
48
|
+
|
|
49
|
+
unit:
|
|
50
|
+
name: Unit Tests
|
|
51
|
+
if: ${{ !startsWith(github.ref_name, 'release-please--') && !startsWith(github.head_ref, 'release-please--') }}
|
|
52
|
+
runs-on: ubuntu-24.04
|
|
53
|
+
timeout-minutes: 10
|
|
54
|
+
services:
|
|
55
|
+
redis:
|
|
56
|
+
image: redis
|
|
57
|
+
options: >-
|
|
58
|
+
--health-cmd "redis-cli ping"
|
|
59
|
+
--health-interval 10s
|
|
60
|
+
--health-timeout 5s
|
|
61
|
+
--health-retries 5
|
|
62
|
+
ports:
|
|
63
|
+
- 6379:6379
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@v6
|
|
66
|
+
- name: Use Node.js 24
|
|
67
|
+
uses: actions/setup-node@v6
|
|
68
|
+
with:
|
|
69
|
+
node-version: 24
|
|
70
|
+
cache: npm
|
|
71
|
+
- run: npm install
|
|
72
|
+
- name: Run tests
|
|
73
|
+
run: npm test
|
|
74
|
+
env:
|
|
75
|
+
NODE_ENV: test
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.3.0](https://github.com/postalsys/pending-dns/compare/pending-dns-v1.2.5...pending-dns-v1.3.0) (2026-06-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* replace Bugsnag with self-hosted Sentry error reporting ([3b374c7](https://github.com/postalsys/pending-dns/commit/3b374c737ff0a22509f79976bee18ab9d425f8bd))
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
PendingDNS is a lightweight, API-driven **authoritative** DNS server (UDP + TCP), bundled with a REST API for managing zone records, a public HTTP/HTTPS redirect/proxy server, automatic Let's Encrypt certificate generation, and periodic health checks. All state lives in Redis.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm start # run the whole app (node server.js)
|
|
11
|
+
npm test # run the test suite (requires a local Redis on 127.0.0.1:6379)
|
|
12
|
+
npm run lint # ESLint 9 (flat config: eslint.config.js)
|
|
13
|
+
npm run licenses # regenerate licenses.txt (license-report)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Running the app or tests requires a reachable Redis and a config with a valid `acme.email` (the bootstrap in `server.js` exits otherwise). For local runs, either set it in `config/development.toml` or pass `--acme.email=you@example.com`.
|
|
17
|
+
|
|
18
|
+
### Tests
|
|
19
|
+
|
|
20
|
+
- Built on the Node.js built-in test runner (`node:test`). `npm test` runs `NODE_ENV=test node --test --test-force-exit --test-concurrency=1 test/*.test.js`.
|
|
21
|
+
- `NODE_ENV=test` loads `config/test.toml`, which points Redis at a **dedicated database (db 15)**. `test/helpers.js#flushTestDb` wipes db 15 between cases and **refuses to run unless the Redis URL ends in `/15`** — so tests never touch dev (db 2) or prod data. Always run tests with `NODE_ENV=test`.
|
|
22
|
+
- `--test-concurrency=1` is required because all test files share db 15. `--test-force-exit` is required because requiring `lib/db` opens persistent ioredis connections; every test file must also call `closeDb()` in an `after()` hook or the process hangs.
|
|
23
|
+
- Some `cached-resolver`/`dns-server` tests do real DNS lookups (e.g. `one.one.one.one`); they are written to skip or tolerate missing network.
|
|
24
|
+
|
|
25
|
+
Run a single file or filter by test name:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
NODE_ENV=test node --test --test-force-exit test/zone-store.test.js
|
|
29
|
+
NODE_ENV=test node --test --test-force-exit --test-name-pattern="wildcard" test/*.test.js
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Building standalone executables
|
|
33
|
+
|
|
34
|
+
Executables are built with `@yao-pkg/pkg` (the maintained `pkg` fork) via the `pkg` block in `package.json`, targeting node24. The org-wide `../emailengine-api/upload.sh <repo>` script installs `@yao-pkg/pkg` globally, then runs `npm run build-source` followed by `npm run build-dist` (or `build-dist-fast` for an unsigned test build) and signs/notarizes/uploads the artifacts. Output binaries are named `pending-dns-<target>` under `ee-dist/`.
|
|
35
|
+
|
|
36
|
+
Because workers are loaded through **dynamic `require()` / `worker_threads`** (see below), `pkg.scripts` must list `lib/**/*.js` and `workers/**/*.js` or the worker code is missing from the snapshot. Runtime files read via `__dirname` (`lib/lua/health.lua`, `config/*.pem`) must be in `pkg.assets`. Do **not** add `pkg.options` like `max_old_space_size`: the workers forward `argv`, and an injected V8 flag leaks in as a bogus module path and crashes every worker.
|
|
37
|
+
|
|
38
|
+
## Architecture
|
|
39
|
+
|
|
40
|
+
### Process model (the most important thing to understand)
|
|
41
|
+
|
|
42
|
+
`server.js` is the supervisor. For each enabled subsystem (`api`, `dns`, `public`, `health`) it spawns a **worker thread** running `workers/<type>.js`, and restarts it on exit. Each `workers/<type>.js` is itself a small bootstrap that:
|
|
43
|
+
|
|
44
|
+
1. installs crash handlers + optional Sentry error reporting via `lib/sentry.js#initSentry` (only when `SENTRY_DSN` / `[sentry] dsn` is set; uses Sentry's uncaught-exception / unhandled-rejection integrations so crashes are reported, then `closeProcess`'s `if (!logger.errorReportingEnabled)` lets Sentry flush+exit),
|
|
45
|
+
2. uses Node `cluster` to fork `config[type].workers` processes (or runs in-thread when `workers === 1` and no user/group drop is configured),
|
|
46
|
+
3. dynamically `require`s the implementation: ``require(`../lib/${type}-server.js`)`` (or `-worker.js` for `health`) and calls it,
|
|
47
|
+
4. drops privileges via `config.process.user`/`group` after ports are bound.
|
|
48
|
+
|
|
49
|
+
So `lib/<type>-server.js` are entry points reached **only through dynamic requires** — static analysis (and `pkg`) won't find them. The implementations are: `lib/api-server.js`, `lib/dns-server.js`, `lib/public-server.js`, `lib/health-worker.js`.
|
|
50
|
+
|
|
51
|
+
### Configuration (`wild-config`)
|
|
52
|
+
|
|
53
|
+
Config is loaded by `wild-config` from `config/default.toml` merged with `config/<NODE_ENV>.toml`, then overridden by CLI args (`--dns.port=53`) and `appconf_*` env vars. `NODE_CONFIG_PATH` points to an external file in production (see `systemd/pending-dns.service`). Values are coerced to the type of the default (a string env var for a numeric default becomes a number). Config is read from `process.cwd()/config`, **not** from `__dirname` — relevant for packaged binaries, which need a `config/` directory next to them.
|
|
54
|
+
|
|
55
|
+
### Redis data model (`lib/zone-store.js`)
|
|
56
|
+
|
|
57
|
+
This is the heart of the system and is non-obvious:
|
|
58
|
+
|
|
59
|
+
- **Domain names are stored label-reversed**: `www.example.com` → `com.example.www` (`domainToName`/`nameToDomain`). This lets `resolveZone` walk *up* from a name to its registered zone by progressively dropping the most-specific label. A "zone" is any name with a `d:<name>:z` set; the shortest possible zone is the 2-label boundary (e.g. `example.com`).
|
|
60
|
+
- Keys: `d:<name>:z` is a **set of record keys** belonging to that zone; `d:<name>:r:<TYPE>` is a **hash** of `hid → JSON.stringify(valueArray)`. Each record's value is a positional array whose meaning depends on type (e.g. A = `[address, healthCheckUri]`, MX = `[exchange, priority]`, CAA = `[value, tag, flags]`, URL = `[url, code, proxy]`).
|
|
61
|
+
- A record's public **ID is `base64url(name \x01 TYPE \x01 hid)`** (`getFullId`/`parseFullId`); `hid` is a `nanoid()`. IDs are opaque and stable only while domain+type are unchanged (an `update` that changes either deletes and re-adds, producing a new ID).
|
|
62
|
+
- **Wildcards** are single-label: a record stored under subdomain `*.foo` matches `anything.foo.<zone>` only (`resolve` retries with the last label replaced by `*`).
|
|
63
|
+
- **Read/write split**: reads go to `db.redisRead`, writes to `db.redisWrite` (configurable as separate master/replica URLs). The health-check Lua script `lib/lua/health.lua` is registered as a custom command `nextHealth` on the write client.
|
|
64
|
+
|
|
65
|
+
### DNS request handling (`lib/dns-handler.js` + custom servers)
|
|
66
|
+
|
|
67
|
+
`lib/dns-udp-server.js` and `lib/dns-tcp-server.js` are hand-rolled servers that parse/serialize with `dns2`'s `Packet` (the project does **not** use dns2's built-in server). `lib/dns-server.js` wires them to `dnsHandler` and returns the bound server handles.
|
|
68
|
+
|
|
69
|
+
`processQuestion` does more than a lookup: for an A/AAAA query it also pulls `CNAME`, `ANAME`, and `URL` records; it follows CNAME chains recursively (depth-limited); A/AAAA results are health-filtered and shuffled; MX is priority-sorted. When nothing is stored it synthesizes fallbacks — `NS` from `config.ns`, a `SOA` from `config.soa`, default Let's Encrypt `CAA` records, and `version.bind`-style CHAOS TXT answers.
|
|
70
|
+
|
|
71
|
+
Two pseudo-record types: **ANAME** (apex alias) is resolved to real A/AAAA at query time via `lib/cached-resolver.js` (a Redis-cached wrapper over Node's `dns.Resolver`, with soft/hard TTLs for both hits and errors). **URL** records answer A/AAAA with the redirect server IPs from `config.public.hosts`; the actual redirect/proxy happens in `lib/public-server.js`.
|
|
72
|
+
|
|
73
|
+
### Certificates & the public server
|
|
74
|
+
|
|
75
|
+
`lib/certs.js` issues Let's Encrypt certs via the **dns-01** challenge, using the zone store itself as the ACME DNS provider (it writes/reads the `_acme-challenge` TXT records). Concurrent issuance is guarded with an `ioredfour` Redis lock; results and a per-domain renewal lock are cached in Redis. `lib/public-server.js` uses an SNI callback to load the right cert per hostname (falling back to a bundled self-signed cert in `config/`), then serves URL-record redirects or reverse-proxies (`proxy=true`), with TLS session tickets stored in Redis.
|
|
76
|
+
|
|
77
|
+
### Testability seams
|
|
78
|
+
|
|
79
|
+
Production code exposes hooks used only by tests: `lib/api-server.js` exports `createServer()` (build the Hapi server without `start()`, for `server.inject()`); `lib/dns-server.js`'s `init()` awaits binding and returns `{ udpServer, tcpServer }`; `lib/dns-handler.js` and `lib/certs.js` attach a `.testables` object to their exported function.
|
|
80
|
+
|
|
81
|
+
## CI / release
|
|
82
|
+
|
|
83
|
+
GitHub Actions mirror the `postalsys/emailengine` conventions and run on Node 24: `test.yml` (license check + lint + tests with a Redis service), `codeql.yml`, and `release.yaml` (release-please in manifest mode — see `release-please-config.json` + `.release-please-manifest.json`; publishes to npm via OIDC trusted publishing, which must be configured registry-side). `deploy.yml` tarballs the repo and ships it to the two name servers. Security policy lives in `SECURITY.md` + a GPG-signed `SECURITY.txt`.
|
|
84
|
+
|
|
85
|
+
## Commit conventions
|
|
86
|
+
|
|
87
|
+
- Use **Conventional Commits** (`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `ci:`, etc.). release-please derives the version bump and `CHANGELOG.md` from these prefixes, so `feat:`/`fix:` commits landing on `master` are what open a release PR.
|
|
88
|
+
- **Do not** add Claude (or any AI assistant) as a co-author / co-contributor in commit messages.
|
|
89
|
+
- For commits that do not change runtime behaviour (docs, comments, CI/workflow tweaks, formatting), append `[skip ci]` to the commit message to avoid triggering the workflows. **Exception:** never add `[skip ci]` to a `feat:`/`fix:` commit — those must run so the release workflow fires.
|
|
90
|
+
- Run `npm run lint` and `npm test` before committing (the test run needs a local Redis; see the Tests section).
|
|
91
|
+
- After pushing, check the workflow runs (`gh run list --branch <branch>`) and report their status. If a run fails for an unrelated infrastructure reason (auth errors, HTTP 403, "account suspended"), check <https://www.githubstatus.com/> for an active incident before assuming the change is at fault.
|
|
92
|
+
|
|
93
|
+
## Code style
|
|
94
|
+
|
|
95
|
+
- No emojis in code or documentation — printable ASCII only.
|
|
96
|
+
- Use a single hyphen-minus (`-`) as a dash in user-facing strings and docs; never em dashes, en dashes, or double hyphens (`--`).
|
|
97
|
+
- Never swallow errors at the global `uncaughtException` / `unhandledRejection` handlers to keep a worker alive — those handlers are the last line of defence and the worker is expected to exit. Fix the error at its source (add the missing `try/catch`, `.catch()`, or `error` listener at the call site) instead.
|
package/README.md
CHANGED
|
@@ -25,13 +25,13 @@ Lightweight API driven Authoritative DNS server.
|
|
|
25
25
|
|
|
26
26
|
## Requirements
|
|
27
27
|
|
|
28
|
-
- **Node.js**,
|
|
28
|
+
- **Node.js**, v18 or newer
|
|
29
29
|
- **Redis**, any version should do as only basic commands are used
|
|
30
30
|
|
|
31
31
|
## Usage
|
|
32
32
|
|
|
33
33
|
```
|
|
34
|
-
$ npm install --
|
|
34
|
+
$ npm install --omit=dev
|
|
35
35
|
$ npm start
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -45,9 +45,9 @@ As root run the following commands to set up PendingDNS:
|
|
|
45
45
|
|
|
46
46
|
```
|
|
47
47
|
$ cd /opt
|
|
48
|
-
$ git clone
|
|
48
|
+
$ git clone https://github.com/postalsys/pending-dns.git
|
|
49
49
|
$ cd pending-dns
|
|
50
|
-
$ npm install --
|
|
50
|
+
$ npm install --omit=dev
|
|
51
51
|
$ cp systemd/pending-dns.service /etc/systemd/system
|
|
52
52
|
$ cp config/default.toml /etc/pending-dns.toml
|
|
53
53
|
```
|
|
@@ -110,6 +110,28 @@ Without proper setup domain registrars do not allow your name server domain name
|
|
|
110
110
|
|
|
111
111
|

|
|
112
112
|
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
Install all dependencies (including dev dependencies):
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
$ npm install
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Tests
|
|
122
|
+
|
|
123
|
+
The test suite runs with the built-in Node.js test runner and needs a **local Redis** instance listening on `127.0.0.1:6379`. Tests use a dedicated database (`db 15`) which is flushed between runs, so it does not touch development or production data.
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
$ npm test
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Linting
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
$ npm run lint
|
|
133
|
+
```
|
|
134
|
+
|
|
113
135
|
## API
|
|
114
136
|
|
|
115
137
|
You can see the entire API docs from the swagger page at http://127.0.0.1:5080/docs
|
|
@@ -203,7 +225,8 @@ All record types have the following properties
|
|
|
203
225
|
**CAA**
|
|
204
226
|
|
|
205
227
|
- **value** is the domain name of the provider, eg. `letsencrypt.org`
|
|
206
|
-
- **tag** is the CAA tag,
|
|
228
|
+
- **tag** is the CAA tag, one of `issue`, `issuewild` or `iodef`
|
|
229
|
+
- **flags** (Number, default is `0`) is the CAA flags octet (0-255)
|
|
207
230
|
|
|
208
231
|
**URL**
|
|
209
232
|
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
PendingDNS is a lightweight, API-driven authoritative DNS server. It also runs a
|
|
4
|
+
public HTTP/HTTPS redirect/proxy server and generates Let's Encrypt certificates
|
|
5
|
+
over an ACME flow, and it stores all zone data in Redis. Because it is exposed to
|
|
6
|
+
untrusted DNS and HTTP traffic and handles certificate private keys, we take
|
|
7
|
+
security reports seriously and aim to respond quickly.
|
|
8
|
+
|
|
9
|
+
## Supported Versions
|
|
10
|
+
|
|
11
|
+
Security fixes are released only against the latest version. We do not backport
|
|
12
|
+
patches to older releases - upgrading to the current release line is the
|
|
13
|
+
supported way to receive security updates.
|
|
14
|
+
|
|
15
|
+
| Version | Supported |
|
|
16
|
+
| ------- | ------------------ |
|
|
17
|
+
| 1.x | :white_check_mark: |
|
|
18
|
+
| < 1.0 | :x: |
|
|
19
|
+
|
|
20
|
+
If you are on an older version, please upgrade. See the release notes at
|
|
21
|
+
<https://github.com/postalsys/pending-dns/releases> before updating.
|
|
22
|
+
|
|
23
|
+
## Reporting a Vulnerability
|
|
24
|
+
|
|
25
|
+
**Please do not report security vulnerabilities through public GitHub issues,
|
|
26
|
+
pull requests, or discussions.**
|
|
27
|
+
|
|
28
|
+
Report privately through one of the following channels:
|
|
29
|
+
|
|
30
|
+
1. **GitHub Security Advisories (preferred).** Open a private report at
|
|
31
|
+
<https://github.com/postalsys/pending-dns/security/advisories/new>. This keeps
|
|
32
|
+
the discussion private until a fix is published and lets us credit you.
|
|
33
|
+
2. **Email.** Send details to **andris@postalsys.com** (the contact listed in
|
|
34
|
+
[`SECURITY.txt`](SECURITY.txt)). Encrypt sensitive details if possible using
|
|
35
|
+
the key referenced there.
|
|
36
|
+
|
|
37
|
+
When reporting, please include as much of the following as you can:
|
|
38
|
+
|
|
39
|
+
- The affected version(s) and environment (PendingDNS version, Node.js version,
|
|
40
|
+
OS, deployment method - npm or prebuilt binary).
|
|
41
|
+
- The component involved (e.g. the UDP/TCP DNS server, the REST API, the public
|
|
42
|
+
HTTP/HTTPS redirect and proxy server, ACME certificate generation, health
|
|
43
|
+
checks, or the Redis-backed zone store).
|
|
44
|
+
- A clear description of the issue and its impact (e.g. cache poisoning, DNS
|
|
45
|
+
amplification, request smuggling, SSRF via the URL/proxy records or health
|
|
46
|
+
checks, certificate mis-issuance, credential or private-key disclosure,
|
|
47
|
+
injection, information disclosure, denial of service).
|
|
48
|
+
- A minimal proof of concept or reproduction steps.
|
|
49
|
+
- Any suggested remediation, if you have one.
|
|
50
|
+
|
|
51
|
+
We are a small team, so there is no guaranteed response time - sometimes reports
|
|
52
|
+
are handled within hours, sometimes they take longer. Accepted issues are fixed
|
|
53
|
+
in a new release and coordinated through a GitHub Security Advisory, and
|
|
54
|
+
reporters who wish to be named are credited.
|
|
55
|
+
|
|
56
|
+
## CVEs
|
|
57
|
+
|
|
58
|
+
We track and disclose vulnerabilities through GitHub Security Advisories. We do
|
|
59
|
+
not request or manage CVE identifiers ourselves. If you need a CVE assigned for a
|
|
60
|
+
reported issue, please request one yourself - for example, through GitHub's own
|
|
61
|
+
CVE request flow on the published advisory, or another CNA.
|
|
62
|
+
|
|
63
|
+
## Scope
|
|
64
|
+
|
|
65
|
+
In scope: the PendingDNS application source in this repository - the
|
|
66
|
+
authoritative DNS server (UDP and TCP), the REST API for managing zone records,
|
|
67
|
+
the public HTTP/HTTPS redirect and proxy server, ACME/Let's Encrypt certificate
|
|
68
|
+
generation and storage, the health-check subsystem, and the Redis-backed zone
|
|
69
|
+
store.
|
|
70
|
+
|
|
71
|
+
Out of scope:
|
|
72
|
+
|
|
73
|
+
- Vulnerabilities in your own application code or automation that integrates
|
|
74
|
+
with the PendingDNS API.
|
|
75
|
+
- Misconfiguration of your deployment - for example, exposing the management API
|
|
76
|
+
to untrusted networks, an unauthenticated or publicly reachable Redis instance,
|
|
77
|
+
binding the DNS server in a way that enables open-resolver-style abuse, or
|
|
78
|
+
missing TLS on the API.
|
|
79
|
+
- The inherent properties of plain DNS over UDP/TCP without DNSSEC (PendingDNS
|
|
80
|
+
does not implement DNSSEC, DoH, or DoT by design).
|
|
81
|
+
- Issues that require an already-compromised host or pre-existing administrator
|
|
82
|
+
access.
|
|
83
|
+
- Vulnerabilities in third-party services PendingDNS connects to (Let's Encrypt,
|
|
84
|
+
upstream resolvers, health-check targets).
|
|
85
|
+
- Social-engineering reports and missing security headers without a
|
|
86
|
+
demonstrated, concrete impact.
|
|
87
|
+
|
|
88
|
+
Thank you for helping keep PendingDNS and its users safe.
|
package/SECURITY.txt
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-----BEGIN PGP SIGNED MESSAGE-----
|
|
2
|
+
Hash: SHA256
|
|
3
|
+
|
|
4
|
+
Contact: https://github.com/postalsys/pending-dns/security/advisories/new
|
|
5
|
+
Contact: mailto:andris@postalsys.com
|
|
6
|
+
Expires: 2027-06-01T00:00:00.000Z
|
|
7
|
+
Encryption: https://keys.openpgp.org/vks/v1/by-fingerprint/5D952A46E1D8C931F6364E01DC6C83F4D584D364
|
|
8
|
+
Preferred-Languages: en, et
|
|
9
|
+
Canonical: https://github.com/postalsys/pending-dns/blob/master/SECURITY.txt
|
|
10
|
+
Policy: https://github.com/postalsys/pending-dns/blob/master/SECURITY.md
|
|
11
|
+
-----BEGIN PGP SIGNATURE-----
|
|
12
|
+
|
|
13
|
+
iQJPBAEBCAA5FiEEXZUqRuHYyTH2Nk4B3GyD9NWE02QFAmozqqUbFIAAAAAABAAO
|
|
14
|
+
bWFudTIsMi41KzEuMTIsMCwzAAoJENxsg/TVhNNkUlUQAL8mZHOiH2ZVgsrJanW6
|
|
15
|
+
58YQ5DTNfLL0CO5Qwo1j9XijmvF7dMwDsNTvQ2hDBV+Okz7mSfDBUaofgT6kIBQz
|
|
16
|
+
Qk0yMyOp1Um+l4rF7/J2d/9ABBnsu4Le59Mu87qIgziLCu3YarheThiPCQvFzRfH
|
|
17
|
+
A/vmGOu3/+gcHdF2GrlEk8xfFNdIqwdhuW8oTiR18WqUARe3S+wQdfumDX41gVRL
|
|
18
|
+
y0Q8eGXb3j8jLXp9E21ePlPdxtIQC4fZSexd64IEKv7kuVp9vDpai0jdi5SQSmCA
|
|
19
|
+
gOPp5f9jR0B6GCbRfCRYUHsrQlwOZPtCq46IGKCnrCd2wI7RHTeCUwtzOQsqod7n
|
|
20
|
+
u+GE/YAZZuck9OI6oDZ3klGXUNAi5RO16ix80rybFfVA2MmNdCDHDwTmlysHli8E
|
|
21
|
+
9FjakLcnF/5eH5NlH579IhQIx1exmE9Q+ZyCwaNQ2uGIujxy6bay3cxvXXeecrJM
|
|
22
|
+
c0MMNgyh7CCfmHShoXfQ2JnFTlVycgqZetLySBxGzkgj5mczLKHviAjaoM3Hw2K6
|
|
23
|
+
znct7z4aE0yY+tCItdIHTdPV5NxR319HZRE980h2yxt92aWmhZn/dW/4fAEwqzFU
|
|
24
|
+
C6Ghu37qs9JwvyBZImH92fc9+27Xgx0Ahfb9RoXM/+TulXAdrQxcK+th9ye40GfJ
|
|
25
|
+
otvIQGxzLN1kyCWhQCqusEJ4
|
|
26
|
+
=QK6n
|
|
27
|
+
-----END PGP SIGNATURE-----
|
package/bin/pending-dns.js
CHANGED
package/config/default.toml
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
[log]
|
|
3
3
|
level = "trace"
|
|
4
4
|
|
|
5
|
+
[sentry]
|
|
6
|
+
# Error reporting DSN for the self-hosted Sentry server. Leave empty to disable.
|
|
7
|
+
# The production value is set via the SENTRY_DSN environment variable.
|
|
8
|
+
dsn = ""
|
|
9
|
+
|
|
5
10
|
[dbs]
|
|
6
11
|
|
|
7
12
|
# By default all redis commands are sent against the same instance
|
package/config/test.toml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Configuration used by the automated test suite (NODE_ENV=test).
|
|
2
|
+
# It is never loaded in production/development and keeps tests isolated
|
|
3
|
+
# from real data by pointing Redis at a dedicated database that the test
|
|
4
|
+
# setup is free to flush.
|
|
5
|
+
|
|
6
|
+
[log]
|
|
7
|
+
level = "silent"
|
|
8
|
+
|
|
9
|
+
[dbs]
|
|
10
|
+
# Dedicated Redis database for tests. The test bootstrap flushes this DB,
|
|
11
|
+
# so it must not be shared with development (db 2) or production data.
|
|
12
|
+
redis = "redis://127.0.0.1:6379/15"
|
|
13
|
+
|
|
14
|
+
[acme]
|
|
15
|
+
# A syntactically valid address is required for the server bootstrap check.
|
|
16
|
+
email = "test@example.com"
|
|
17
|
+
|
|
18
|
+
[dns]
|
|
19
|
+
# Avoid clashing with a locally running instance if the DNS server is started.
|
|
20
|
+
port = 15353
|
|
21
|
+
host = "127.0.0.1"
|
|
22
|
+
|
|
23
|
+
[api]
|
|
24
|
+
port = 15080
|
|
25
|
+
host = "127.0.0.1"
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { FlatCompat } = require('@eslint/eslintrc');
|
|
4
|
+
const js = require('@eslint/js');
|
|
5
|
+
const prettier = require('eslint-config-prettier');
|
|
6
|
+
|
|
7
|
+
const compat = new FlatCompat({
|
|
8
|
+
baseDirectory: __dirname,
|
|
9
|
+
recommendedConfig: js.configs.recommended,
|
|
10
|
+
allConfig: js.configs.all
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
module.exports = [
|
|
14
|
+
{
|
|
15
|
+
ignores: ['node_modules/', 'ee-dist/', 'coverage/', 'views/', '.prettierrc.js']
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
// Shared Nodemailer ESLint rules (eslintrc format, wrapped for flat config)
|
|
19
|
+
...compat.extends('eslint-config-nodemailer'),
|
|
20
|
+
|
|
21
|
+
// Disable stylistic rules that conflict with Prettier
|
|
22
|
+
prettier,
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
languageOptions: {
|
|
26
|
+
ecmaVersion: 2023,
|
|
27
|
+
sourceType: 'commonjs'
|
|
28
|
+
},
|
|
29
|
+
rules: {
|
|
30
|
+
indent: 'off',
|
|
31
|
+
'no-await-in-loop': 'off',
|
|
32
|
+
'require-atomic-updates': 'off',
|
|
33
|
+
// Preserve the long-standing project convention of `catch (err) { /* ignore */ }`.
|
|
34
|
+
// ESLint 9 changed the no-unused-vars `caughtErrors` default to 'all'.
|
|
35
|
+
'no-unused-vars': ['error', { caughtErrors: 'none' }]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
];
|