Add package backup and project notes
ober
c132cd2d21f84c22fe377f4591159d841b404017
new file mode 100644 --- /dev/null +++ b/jpkg.sexp.bak @@ -0,0 +1,11 @@ +(package + (name "@lisp/jerboa-dns") + (version "0.1.0") + (description "DNS client and server for Jerboa") + (license "MIT") + (source "https://git.sr.ht/~lisp/jerboa-dns") + (jerboa ">=0.2.0") + (modules ((root "."))) + (dependencies ()) + (dev-dependencies ()) + (capabilities ())) new file mode 100644 --- /dev/null +++ b/todo @@ -0,0 +1,4921 @@ +# Security review for tinydns replacement + +**Session ID:** ses_04a469936ffeYZJHIiIYFbb2rp +**Created:** 7/30/2026, 7:10:54 PM +**Updated:** 7/30/2026, 8:41:16 PM + +--- + +## Assistant (Build · Kimi K3 · 20.4s) + +_Thinking:_ + +The user wants a `deploy.md` with the steps required to deploy jerboa-dns properly "with the right files". This is based on my security review findings. Let me create a deployment guide that covers: + +1. Building the static binary for the target platform +2. The files needed (binary, wasm sandbox modules, zone data) +3. The chroot layout issue (wasm files must be inside $ROOT) +4. Environment variables / config +5. Privilege drop requirements (UID/GID non-zero) +6. Sandbox settings (fail closed, no JDNS_ALLOW_SANDBOX_FALLBACK) +7. Zone compilation (jdns-data) +8. Edge rate limiting requirement +9. Startup verification / smoke test +10. Rollback considerations + +Let me first check some facts so the doc is accurate: +- What wasm files exist and where (lib/jerboa-dns/sandbox/) +- The static build targets (make static-linux etc.) +- How jdns-data is shipped/run (bin/jdns-data.ss needs jerboa libdirs, or static binaries via make targets) +- The Makefile static targets +- docs/runtime-hardening.md content to align +- Check what files the dist/ or static/ dir has for releases + +Let me gather these facts quickly. + +I'll gather the exact facts first (file paths, build targets, wasm module locations) so the deploy doc is accurate. + +**Tool: bash** + +**Input:** +```json +{ + "command": "find . -name \"*.wasm\" -not -path \"./fuzz/target/*\" -not -path \"./wasm/target/*\" 2>/dev/null; echo ---; grep -n \"static-linux\\|static-freebsd\\|static-macos\\|static-\\*\" Makefile | head -10; echo ---; ls static/", + "workdir": "/Users/user/mine/jerboa-dns" +} +``` + +**Output:** +``` +./lib/jerboa-dns/sandbox/dns_parser.wasm +./lib/jerboa-dns/sandbox/cdb_parser.wasm +--- +--- +build-common.ss +build-jdns-freebsd.ss +build-jdns-linux-arm64.ss +build-jdns-linux.ss +build-jdns-macos.ss +petite_boot.h +program_boot.h +scheme_boot.h + +``` + +--- + +## Assistant (Build · Kimi K3 · 16.8s) + +**Tool: bash** + +**Input:** +```json +{ + "command": "grep -n \"static\" Makefile | head -20; echo ---; grep -rn \"sandbox\" Makefile | head; echo ---; ls lib/jerboa-dns/sandbox/", + "workdir": "/Users/user/mine/jerboa-dns" +} +``` + +**Output:** +``` +32:# FFI symbol list from that archive and relink. A static main.c sets +--- +Makefile:3:# wasm32-unknown-unknown target for the sandbox payloads). No jerboa checkout. +Makefile:23:WASM_OUTPUTS = lib/jerboa-dns/sandbox/dns_parser.wasm lib/jerboa-dns/sandbox/cdb_parser.wasm +Makefile:42:# Build just the sandboxed DNS/CDB WebAssembly modules. +Makefile:112: find lib/jerboa-dns/sandbox -type f -name '*.wasm' -exec shasum -a 256 {} \; > "$(DIST_DIR)/wasm-sha256.txt" +--- +cdb_parser.wasm +dns_parser.wasm + +``` + +--- + +## Assistant (Build · Kimi K3 · 11.0s) + +_Thinking:_ + +The wasm outputs exist at `lib/jerboa-dns/sandbox/dns_parser.wasm` and `cdb_parser.wasm`. Let me check how static binaries get built — the Makefile mentions "static-*" targets per README but grep didn't show them. Let me look at the whole Makefile to see the targets, and check .builds or dist for release packaging. Also check docs/runtime-hardening.md for the deployment-relevant invariants to align with. + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/user/mine/jerboa-dns/Makefile" +} +``` + +**Output:** +``` +<path>/Users/user/mine/jerboa-dns/Makefile</path> +<type>file</type> +<content> +1: # jerbuild bundles Chez Scheme + the jerboa stdlib + the jerboa-native Rust +2: # crate, so building jdns needs only jerbuild + a C compiler + cargo (with the +3: # wasm32-unknown-unknown target for the sandbox payloads). No jerboa checkout. +4: JERBUILD ?= $(shell if [ -x "$(CURDIR)/../jerboa/dist/jerbuild" ]; then printf '%s\n' "$(CURDIR)/../jerboa/dist/jerbuild"; elif command -v jerbuild >/dev/null 2>&1; then command -v jerbuild; else printf '%s\n' jerbuild; fi) +5: JH := $(shell $(JERBUILD) --jerboa-home 2>/dev/null) +6: ifeq ($(JH),) +7: $(error jerbuild not found on PATH (or '$(JERBUILD) --jerboa-home' failed). Install jerbuild, or set JERBUILD=/path/to/jerbuild) +8: endif +9: +10: NATIVE_DIR := $(JH)/jerboa-native-rs/target/release +11: NATIVE_A := $(NATIVE_DIR)/libjerboa_native.a +12: LIBDIRS := --libdirs lib:$(JH)/lib +13: JEXEC := $(JERBUILD) exec --unsafe-prelude $(LIBDIRS) +14: BIN := jdns +15: CARGO_AUDIT ?= $(shell command -v cargo-audit 2>/dev/null || printf '%s/.cargo/bin/cargo-audit' "$$HOME") +16: DIST_DIR ?= dist/release-evidence +17: SBOM_DIR ?= dist/sbom +18: REPRO_DIR ?= dist/reproducibility +19: SOAK_DIR ?= dist/soak +20: FUZZ_EVIDENCE_DIR ?= dist/fuzz-evidence +21: +22: WASM_DIR = wasm +23: WASM_OUTPUTS = lib/jerboa-dns/sandbox/dns_parser.wasm lib/jerboa-dns/sandbox/cdb_parser.wasm +24: +25: .PHONY: all build binary wasm wasm-clean test security audit fuzz-check fuzz-evidence sbom reproducibility-report soak-evidence verify release-evidence clean lint +26: +27: all: binary +28: +29: # Standalone native binary via .jerbuild (entry bin/jdns.ss -> jdns). +30: # Two passes: pass 1 builds the wasm payloads (pre-build) + cargo-builds +31: # jerboa-native (wasm feature) into jerbuild's cache; we then regenerate the +32: # FFI symbol list from that archive and relink. A static main.c sets +33: # JERBOA_STATIC=1 so the std modules use the registered symbols, not dlopen. +34: binary: +35: @touch support/ffi-symbols.gen +36: $(JERBUILD) build +37: sh support/gen-ffi-symbols.sh +38: $(JERBUILD) build +39: +40: build: binary +41: +42: # Build just the sandboxed DNS/CDB WebAssembly modules. +43: wasm: +44: @$(WASM_DIR)/build.sh +45: +46: wasm-clean: +47: rm -rf $(WASM_DIR)/target $(WASM_OUTPUTS) +48: +49: # Run the test suite in interpreter mode. The std wasm/sqlite modules dlopen +50: # libjerboa_native from jerbuild's cache, so point the loader there. +51: test: binary +52: @for f in tests/*-test.ss; do \ +53: echo " $$f"; \ +54: DYLD_FALLBACK_LIBRARY_PATH="$(NATIVE_DIR)" \ +55: LD_LIBRARY_PATH="$(NATIVE_DIR):$$LD_LIBRARY_PATH" \ +56: $(JEXEC) "$$f" || exit 1; \ +57: done +58: @echo "All tests passed." +59: +60: lint: security +61: +62: security: +63: scripts/daemon-security-check.sh +64: +65: audit: +66: @if ! [ -x "$(CARGO_AUDIT)" ]; then \ +67: echo "cargo-audit is required. Install with: cargo install cargo-audit --locked"; \ +68: exit 1; \ +69: fi +70: (cd $(WASM_DIR) && "$(CARGO_AUDIT)" audit) +71: +72: fuzz-check: +73: cargo run --manifest-path fuzz/Cargo.toml --bin corpus_smoke +74: @if rustup toolchain list 2>/dev/null | grep -q '^nightly' && \ +75: { command -v cargo-fuzz >/dev/null 2>&1 || [ -x "$$HOME/.cargo/bin/cargo-fuzz" ]; }; then \ +76: NIGHTLY_BIN=$$(dirname "$$(rustup which cargo --toolchain nightly)"); \ +77: PATH="$$NIGHTLY_BIN:$$HOME/.cargo/bin:$$PATH" cargo fuzz build --fuzz-dir fuzz; \ +78: else \ +79: echo "cargo-fuzz with nightly Rust unavailable; falling back to cargo check"; \ +80: cargo check --manifest-path fuzz/Cargo.toml --bins; \ +81: fi +82: +83: fuzz-evidence: +84: FUZZ_EVIDENCE_DIR="$(FUZZ_EVIDENCE_DIR)" bash scripts/fuzz-evidence.sh +85: +86: sbom: binary +87: JERBUILD="$(JERBUILD)" SBOM_DIR="$(SBOM_DIR)" bash scripts/sbom.sh +88: +89: reproducibility-report: +90: JERBUILD="$(JERBUILD)" REPRO_DIR="$(REPRO_DIR)" bash scripts/reproducibility-report.sh +91: +92: soak-evidence: +93: SOAK_EVIDENCE_DIR="$(SOAK_DIR)" bash scripts/soak-evidence.sh +94: +95: verify: security test fuzz-check audit sbom reproducibility-report +96: +97: release-evidence: verify +98: rm -rf "$(DIST_DIR)" +99: mkdir -p "$(DIST_DIR)" +100: git rev-parse HEAD > "$(DIST_DIR)/git-commit.txt" +101: git status --short > "$(DIST_DIR)/git-status.txt" +102: { \ +103: echo "os=$$(uname -s 2>/dev/null || printf unknown)"; \ +104: echo "kernel_release=$$(uname -r 2>/dev/null || printf unknown)"; \ +105: echo "machine=$$(uname -m 2>/dev/null || printf unknown)"; \ +106: $(JERBUILD) --version 2>/dev/null | sed 's/^/jerbuild_version=/'; \ +107: if $(JERBUILD) --jerboa-home >/dev/null 2>&1; then echo "jerboa_home_status=present"; else echo "jerboa_home_status=missing"; fi; \ +108: } > "$(DIST_DIR)/build-environment.txt" +109: cargo metadata --manifest-path wasm/Cargo.toml --locked --format-version 1 > "$(DIST_DIR)/cargo-metadata-wasm.json" +110: cargo metadata --manifest-path fuzz/Cargo.toml --locked --format-version 1 > "$(DIST_DIR)/cargo-metadata-fuzz.json" +111: (cd wasm && "$(CARGO_AUDIT)" audit > "../$(DIST_DIR)/rustsec-wasm.txt") +112: find lib/jerboa-dns/sandbox -type f -name '*.wasm' -exec shasum -a 256 {} \; > "$(DIST_DIR)/wasm-sha256.txt" +113: rm -rf "$(DIST_DIR)/sbom" "$(DIST_DIR)/reproducibility" +114: cp -R "$(SBOM_DIR)" "$(DIST_DIR)/sbom" +115: cp -R "$(REPRO_DIR)" "$(DIST_DIR)/reproducibility" +116: cat "$(SBOM_DIR)/status.txt" > "$(DIST_DIR)/sbom.log" +117: cat "$(REPRO_DIR)/result.txt" > "$(DIST_DIR)/reproducibility.log" +118: grep -q '^artifact_archive_status=hashes-only$$' "$(REPRO_DIR)/result.txt" +119: grep -q '^scratch_build_logs_status=not-archived$$' "$(REPRO_DIR)/result.txt" +120: $(MAKE) --no-print-directory soak-evidence +121: rm -rf "$(DIST_DIR)/soak" +122: cp -R "$(SOAK_DIR)" "$(DIST_DIR)/soak" +123: cat "$(SOAK_DIR)/status.txt" > "$(DIST_DIR)/soak.log" +124: @if [ "$${JDNS_RUN_RELEASE_SOAK:-0}" = "1" ]; then \ +125: grep -q '^tcp_smoke_status=pass$$' "$(SOAK_DIR)/status.txt"; \ +126: fi +127: $(MAKE) --no-print-directory fuzz-evidence +128: rm -rf "$(DIST_DIR)/fuzz-evidence" +129: cp -R "$(FUZZ_EVIDENCE_DIR)" "$(DIST_DIR)/fuzz-evidence" +130: cat "$(FUZZ_EVIDENCE_DIR)/status.txt" > "$(DIST_DIR)/fuzz-evidence.log" +131: @if [ "$${JDNS_RUN_COVERAGE_FUZZ:-0}" = "1" ]; then \ +132: grep -q '^coverage_fuzz_status=local-smoke-recorded$$' "$(FUZZ_EVIDENCE_DIR)/status.txt"; \ +133: fi +134: { \ +135: printf 'abuse_policy_status=present\n'; \ +136: printf 'authoritative_only_status=documented\n'; \ +137: printf 'recursion_status=disabled\n'; \ +138: printf 'any_query_policy_status=minimized-refused\n'; \ +139: printf 'udp_payload_policy_status=capped-512-with-truncation\n'; \ +140: printf 'tcp_resource_policy_status=documented\n'; \ +141: printf 'edge_rate_limit_status=external-required-until-native-limit\n'; \ +142: printf 'source=docs/rate-limit-amplification.md\n'; \ +143: } > "$(DIST_DIR)/abuse-policy.txt" +144: grep -q '^abuse_policy_status=present$$' "$(DIST_DIR)/abuse-policy.txt" +145: grep -q '^any_query_policy_status=minimized-refused$$' "$(DIST_DIR)/abuse-policy.txt" +146: grep -q '^edge_rate_limit_status=external-required-until-native-limit$$' "$(DIST_DIR)/abuse-policy.txt" +147: { \ +148: printf 'cdb_snapshot_reload_status=validated-atomic-swap\n'; \ +149: printf 'cdb_request_path_status=in-memory-only\n'; \ +150: printf 'cdb_retired_reader_status=lease-drained\n'; \ +151: printf 'privilege_root_policy_status=uid-gid-required\n'; \ +152: printf 'supplementary_groups_status=clear-and-verify\n'; \ +153: printf 'saved_identity_status=verified-or-regain-probed\n'; \ +154: printf 'source=docs/runtime-hardening.md\n'; \ +155: } > "$(DIST_DIR)/runtime-hardening.txt" +156: grep -q '^cdb_request_path_status=in-memory-only$$' "$(DIST_DIR)/runtime-hardening.txt" +157: grep -q '^supplementary_groups_status=clear-and-verify$$' "$(DIST_DIR)/runtime-hardening.txt" +158: sh scripts/sanitize-evidence.sh "$(DIST_DIR)" +159: sh scripts/sanitize-evidence.sh "$(SBOM_DIR)" +160: sh scripts/sanitize-evidence.sh "$(REPRO_DIR)" +161: sh scripts/sanitize-evidence.sh "$(SOAK_DIR)" +162: sh scripts/sanitize-evidence.sh "$(FUZZ_EVIDENCE_DIR)" +163: +164: clean: +165: rm -f $(BIN) \ +166: jdns-macos-arm64 jdns-data-macos-arm64 \ +167: jdns-linux-amd64 jdns-data-linux-amd64 \ +168: jdns-linux-arm64 jdns-data-linux-arm64 \ +169: jdns-freebsd-amd64 jdns-data-freebsd-amd64 \ +170: jdns*-main.c jdns*-libs.boot \ +171: petite_boot.h scheme_boot.h app_boot.h program_boot.h \ +172: support/ffi-symbols.gen +173: find lib \( -name '*.so' -o -name '*.wpo' \) -delete 2>/dev/null || true + +(End of file - total 173 lines) +</content> +``` + +--- + +## Assistant (Build · Kimi K3 · 12.8s) + +_Thinking:_ + +The `clean` target references `jdns-linux-amd64`, `jdns-data-linux-amd64` etc. — static cross-build artifacts. Where are those targets defined? Maybe in `.jerbuild` config or build scripts. The README says "Static release targets live behind the `static-*` Make targets, for example: make static-freebsd". But the Makefile has no static-* targets! Maybe stale README, or the targets come from an included file. Let me check .jerbuild and the static/*.ss build scripts — those are jerbuild build scripts (build-jdns-linux.ss). Let me check how they're invoked. + +**Tool: bash** + +**Input:** +```json +{ + "command": "cat .jerbuild 2>/dev/null | head -40; echo ===; head -50 static/build-jdns-linux.ss", + "workdir": "/Users/user/mine/jerboa-dns" +} +``` + +**Output:** +``` +;; Build jdns with a standalone jerbuild binary: `jerbuild build`. +;; +;; jerbuild bundles Chez Scheme + the jerboa stdlib + the jerboa-native Rust +;; crate, so this needs only jerbuild + a C compiler + cargo (with the +;; wasm32-unknown-unknown target for the sandbox payloads). No jerboa source +;; checkout. The generated FFI table also includes the TLS and crypto surface +;; pulled in by the server, so all three feature groups are linked statically. + +(entry "bin/jdns.ss") +(output "jdns") +(libdirs "lib") +(ffi-symbols "support/ffi-symbols.gen") +;; Build the sandboxed DNS/CDB wasm payloads before embedding. +(pre-build "sh wasm/build.sh") +(rust-crates + ("@bundle/jerboa-native-rs/Cargo.toml" + features: "tls,crypto,wasm" + no-default-features: #t)) +=== +#!chezscheme +;;; build-jdns-linux.ss — Cross-compile jdns + jdns-data from macOS arm64 +;;; to Linux x86_64 musl (fully static). +;;; +;;; Usage: +;;; JERBOA_HOME=/Users/user/mine/jerboa \ +;;; scheme --libdirs "lib:$JERBOA_HOME/lib" \ +;;; --script static/build-jdns-linux.ss +;;; +;;; Uses: +;;; $JERBOA_HOME/.chez-cross-ta6le/ — cross-built Chez install +;;; $JERBOA_HOME/build/chez/xc-ta6le/s/xpatch — host compiler → ta6le mode +;;; x86_64-linux-musl-gcc — C compile + static link +;;; +;;; Produces (in repo root): +;;; jdns-linux-amd64 +;;; jdns-data-linux-amd64 + +(import + (scheme) + (only (std security taint) safe-system)) + +(load "static/build-common.ss") + +(define jerboa-home (or (getenv "JERBOA_HOME") "/Users/user/mine/jerboa")) +(define cross-prefix (format "~a/.chez-cross-ta6le" jerboa-home)) +(define xpatch (format "~a/build/chez/xc-ta6le/s/xpatch" jerboa-home)) +(define cross-cc (or (getenv "CROSS_CC") "x86_64-linux-musl-gcc")) +(define csv-dir (find-cross-csv-dir cross-prefix "ta6le")) + +(define (require-file p) + (unless (file-exists? p) + (error 'build-jdns-linux "missing file" p))) + +(require-file xpatch) +(require-file (format "~a/libkernel.a" csv-dir)) +(require-file (format "~a/scheme.h" csv-dir)) +(require-file (format "~a/petite.boot" csv-dir)) +(require-file (format "~a/scheme.boot" csv-dir)) +(require-file "bin/jdns.ss") +(require-file "bin/jdns-data.ss") + +(printf "==> build-jdns-linux (cross to ta6le, static musl)~n") +(printf " JERBOA_HOME: ~a~n" jerboa-home) +(printf " csv-dir: ~a~n" csv-dir) +(printf " xpatch: ~a~n" xpatch) +(printf " cross-cc: ~a~n" cross-cc) +(printf "~n") + +;; Stage 1: load xpatch — switches the host compiler into ta6le emit mode. + +``` + +--- + +## Assistant (Build · Kimi K3 · 56.9s) + +_Thinking:_ + +The static cross-compile scripts exist (static/build-jdns-linux.ss etc.) but require a jerboa checkout with cross toolchain (JERBOA_HOME, xpatch, musl-gcc). These are the maintainer's release pipeline — a server operator won't run these. For the deploy doc, the important paths: + +1. **Get the binaries**: either use a released static `jdns-linux-amd64` + `jdns-data-linux-amd64` (built by maintainer's static pipeline), or `make binary` on the target for a local build. Also jdns-data is built how? The .jerbuild builds only jdns (entry bin/jdns.ss → jdns). How does one get jdns-data as a binary? The static scripts build both jdns and jdns-data. On a server, zone compilation can happen elsewhere (compile on a build machine, scp data.cdb). That's actually the cleanest deployment story: compile zones off-box, ship data.cdb. + +Let me verify: does `jerbuild build` also produce jdns-data? .jerbuild has a single (entry "bin/jdns.ss") (output "jdns"). So only jdns. The zone compiler runs via `jerbuild exec` on a machine with jerboa installed (as I did), or via the static jdns-data-* binary from the cross pipeline. + +2. **Server layout**: The daemon needs: + - `jdns` binary (static, no deps) + - `$ROOT/data.cdb` (compiled zone) + - `$ROOT/lib/jerboa-dns/sandbox/{dns_parser.wasm,cdb_parser.wasm}` — because resolve-wasm-path checks `lib/jerboa-dns/sandbox/...` relative to CWD at first use, which is post-chroot, or absolute `/etc/jdns/dns_parser.wasm` and `/etc/jdns/cdb_parser.wasm` — wait! resolve-wasm-path also checks `/etc/jdns/cdb_parser.wasm` as an absolute path. But after chroot, /etc/jdns resolves INSIDE the chroot. Hmm — when is wasm-cdb-init! called? From open-sandboxed-cdb ← open-cdb-cache ← run-server! line 925, AFTER chroot (line 891) and privdrop (910). And wasm-dns-init! lazily at first query. So both resolve after chroot: all three candidate paths ("lib/jerboa-dns/sandbox/x", "sandbox/x", "/etc/jdns/x") resolve inside the chroot. + + So the correct deployment: place wasm files at `$ROOT/lib/jerboa-dns/sandbox/` (or `$ROOT/etc/jdns/`). Alternatively `JDNS_CDB_WASM`/`JDNS_PARSER_WASM` env vars pointing at paths inside the chroot. + + Wait — actually, can the wasm sandbox even work in the static binary? My test run logged "wasm module missing on disk" because CWD was the test dir without lib/. The binary is JERBOA_STATIC=1 with registered FFI symbols; wasmi is in the native crate (features "wasm" per .jerbuild). The wasm-cdb tests passed in interpreter mode ("sandbox-available? => #t"). For the deployed static binary with wasm files present in the chroot, it should work. I can verify quickly by re-running my smoke test with lib/jerboa-dns/sandbox populated in the --root dir! Let me do that to confirm the doc's claim. + +3. **Env**: IP, PORT, ROOT, UID, GID, DATA, JDNS_CDB_RELOAD_MS, JDNS_REQUIRE_WASM_PARSER/CDB, JDNS_LOG_QUERIES. Never JDNS_ALLOW_SANDBOX_FALLBACK / JDNS_RETAIN_PRIVILEGES in prod. + +4. **User creation**: dedicated uid/gid (e.g. `jdns`), root launch (for port 53) with UID/GID set to that user. + +5. **chroot contents**: data.cdb + wasm files. Note the chroot must be writable only by root (jdns-data runs elsewhere as a different user; the reloader only reads). + +6. **Edge rate limiting**: example nftables/iptables hashlimit rules for UDP/53; note TCP too. + +7. **Smoke test**: dig commands + expected log lines. + +8. **Zone updates**: compile off-box or on-box with jdns-data to a temp file, atomic mv into $ROOT/data.cdb; watcher picks it up within JDNS_CDB_RELOAD_MS; malformed → keeps last good. + +9. **Rollback**: keep tinydns running config; switch by IP/port. + +Let me verify the wasm-in-chroot behavior with a live test before writing the doc. Also verify setuid behavior quickly? Can't easily as non-root. The privilege-policy tests cover the planner. + +Also: is jdns-data runnable from the static repo binary set? For the doc I'll present two options: (a) compile zones on a build machine with jerboa installed via `make jdns-data`-style jerbuild exec (I verified this works), or (b) use the static jdns-data-<platform> binary from the release pipeline. + +Actually wait — let me check the wasm path search order again: resolve-wasm-path checks `"lib/jerboa-dns/sandbox/cdb_parser.wasm"` FIRST, then "sandbox/cdb_parser.wasm", then "/etc/jdns/cdb_parser.wasm". All relative to CWD (post-chroot = chroot root) except the absolute /etc/jdns one (also post-chroot). Also JDNS_CDB_WASM env overrides. Good. + +Let me run the live verification with wasm files staged in the root dir. + +Before writing the doc, let me verify the key deployment claim — that the WASM sandbox activates when the modules are staged inside the chroot `$ROOT`: + +**Tool: bash** + +**Input:** +```json +{ + "command": "mkdir -p root/lib/jerboa-dns/sandbox && cp /Users/user/mine/jerboa-dns/lib/jerboa-dns/sandbox/*.wasm root/lib/jerboa-dns/sandbox/ && cp /Users/user/mine/jerboa-dns/bench/zones/zone-1000.cdb root/data.cdb && JDNS_ALLOW_SANDBOX_FALLBACK=1 JDNS_REQUIRE_WASM_PARSER=1 JDNS_REQUIRE_WASM_CDB=1 /Users/user/mine/jerboa-dns/jdns --ip 127.0.0.1 --port 15360 --root root --data data.cdb >server8.log 2>&1 & SPID=$!; sleep 1.5; dig @127.0.0.1 -p 15360 host-1.bench.local A +time=2 +tries=1 +short; dig @127.0.0.1 -p 15360 bench.local MX +tcp +time=2 +tries=1 +short; kill $SPID 2>/dev/null; cat server8.log", + "workdir": "/var/folders/01/7797pkc13nq0x1fl7wxp1s0c0000gn/T/opencode/jdns-test", + "timeout": 30000 +} +``` + +**Output:** +``` +10.0.0.1 +10 mx.bench.local. +ts=2026-07-31T01:57:11.330Z lvl=info evt=tcp_fibers_unavailable reason="library ~s not found" +ts=2026-07-31T01:57:11.330Z lvl=info evt=startup ip=127.0.0.1 port=15360 udp=enabled tcp=enabled +ts=2026-07-31T01:57:11.330Z lvl=info evt=chdir dir=root reason=chroot_unavailable +ts=2026-07-31T01:57:11.331Z lvl=info evt=cdb_opened file=data.cdb parser=sandboxed +ts=2026-07-31T01:57:11.331Z lvl=info evt=cdb_reload_watcher interval_ms=1000 +ts=2026-07-31T01:57:11.331Z lvl=info evt=tcp_listening ip=127.0.0.1 port=15360 mode=threads + +``` + +--- + +## Assistant (Build · Kimi K3 · 1.6s) + +--- + +## User + +the wasm is built IN to the binary! + +--- + +## Assistant (Build · Kimi K3 · 41.4s) + +_Thinking:_ + +The user is correcting me: the WASM is built INTO the binary. Let me verify this claim. Looking at what I know: + +1. The `.jerbuild` has `(pre-build "sh wasm/build.sh")` — builds the wasm payloads before embedding. +2. The Makefile comment says "pass 1 builds the wasm payloads (pre-build) + cargo-builds jerboa-native". +3. But my test showed `wasm_cdb_fallback reason="wasm module missing on disk"` when the wasm files weren't staged, and `parser=sandboxed` when I staged them. + +So the runtime evidence contradicts "built into the binary" — the resolver looks on disk. BUT — maybe there's an embedded copy AND a disk override? Let me look at how `resolve-wasm-path` and `load-wasm-bytes` work, and check if there's an embedded wasm mechanism in the jerboa runtime (e.g., `(std wasm sandbox)` might have embedded module support, or the static build embeds the wasm files as C arrays). + +Let me check: +- `wasm/build.sh` — what it does with the outputs +- `static/build-common.ss` — might embed wasm as boot files or C arrays +- `lib/jerboa-dns/wasm-cdb.ss` resolve-wasm-path — maybe there's an env or embedded fallback I missed +- `(std wasm sandbox)` module — wasm-sandbox-load from bytes; maybe there's a registered embedded copy + +The user knows their codebase. They say the wasm is built into the binary. My earlier test showed "wasm module missing on disk" — which suggests the resolver failed to find it. But maybe that log message is misleading: maybe the static binary DOES embed the wasm, and the fallback was for another reason... no wait, when I staged the files, it went `parser=sandboxed`. So disk presence changed behavior. + +Hmm, but maybe what happened: the embedded binary has the wasm embedded, and `file-exists?` checks are only for the *override* path. Let me re-read wasm-cdb.ss: + +```scheme +(def (resolve-wasm-path) + (or (getenv "JDNS_CDB_WASM") + (find file-exists? + '("lib/jerboa-dns/sandbox/cdb_parser.wasm" + "sandbox/cdb_parser.wasm" + "/etc/jdns/cdb_parser.wasm")) + "lib/jerboa-dns/sandbox/cdb_parser.wasm")) + +(def (wasm-cdb-init!) + (cond + [%module #t] + [(not (wasm-sandbox-available?)) #f] + [else + (try + (let* ([path (resolve-wasm-path)] + [bytes (and (file-exists? path) (load-wasm-bytes path))]) + ... +``` + +So it loads bytes from disk. `sandbox-available?` = `(and (wasm-sandbox-available?) (file-exists? (resolve-wasm-path)))`. + +Unless... `wasm-sandbox-load` on the jerboa side has an embedded registry keyed by path? Let me check `(std wasm sandbox)` source in the jerboa libdir cache. Maybe in static builds, `load-wasm-bytes` or `wasm-sandbox-load` has special handling... Actually — maybe the static build embeds the .wasm files and file-exists? is virtualized? No, file-exists? hits the real FS. + +Wait — maybe the user means: in the STATIC builds (static/build-jdns-linux.ss etc.), the wasm files get embedded as C arrays or boot files, and there's a mechanism where `file-exists?`/`load-wasm-bytes` for those paths reads from the embedded image. Let me check static/build-common.ss for wasm embedding. + +I should verify before writing deploy.md — the user is telling me my "copy wasm files into $ROOT" advice is wrong. Let me look at the actual mechanism. + +Let me check how the static build embeds the WASM payloads — my "copy them into the chroot" advice may be wrong. + +**Tool: bash** + +**Input:** +```json +{ + "command": "grep -n \"wasm\\|embed\" static/build-common.ss | head -20; echo ===; cat wasm/build.sh 2>/dev/null | head -40", + "workdir": "/Users/user/mine/jerboa-dns" +} +``` + +**Output:** +``` +4:;;; Defines: embed-as-c-array, find-cross-csv-dir, jdns-posix-symbols, +5:;;; jdns-posix-wrapped-symbols, jdns-wasm-weak-symbols, emit-jdns-main-c. +13:(define (embed-as-c-array in-path stem out-path) +33: (printf " embed ~a (~a bytes) -> ~a~n" in-path n out-path))) +84:;; Libraries needed by the embedded jdns and jdns-data programs. The order is +96: (std wasm sandbox) +107: (jerboa-dns wasm-cdb) +108: (jerboa-dns wasm-dns) +146:;; Wasm-sandbox symbols pulled from (std wasm sandbox). The Scheme side +147:;; guards every call with wasm-sandbox-available?, so weak stubs that +149:(define jdns-wasm-weak-symbols +150: '("jerboa_wasm_module_new" "jerboa_wasm_module_free" +151: "jerboa_wasm_instance_new" "jerboa_wasm_instance_new_hosted" +152: "jerboa_wasm_instance_free" "jerboa_wasm_call" +153: "jerboa_wasm_memory_read" "jerboa_wasm_memory_write" "jerboa_wasm_memory_size" +154: "jerboa_wasm_add_fuel" "jerboa_wasm_fuel_remaining" "jerboa_wasm_get_log" +169:;; runs the embedded program via Sscheme_program. +221: ;; Weak stubs for wasm symbols not linked into this build. __attribute__((weak)) +222: ;; lets the link succeed; at runtime Scheme guards wasm-sandbox-available? so +224: (display "/* Weak stubs for wasm sandbox symbols (libjerboa_native not linked) */\n" out) +=== +#!/usr/bin/env bash +# Build the sandboxed WebAssembly modules for jerboa-dns. +# +# Outputs (after `cp` to the project tree): +# lib/jerboa-dns/sandbox/dns_parser.wasm +# lib/jerboa-dns/sandbox/cdb_parser.wasm +# +# Uses rustup's toolchain explicitly to avoid clashes with a Homebrew +# rustc that lacks the wasm32 target. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +HOST="$(rustc -vV 2>/dev/null | awk '/^host:/ {print $2}')" +if [[ -n "${HOST:-}" && -x "$HOME/.rustup/toolchains/stable-$HOST/bin/cargo" ]]; then + export PATH="$HOME/.rustup/toolchains/stable-$HOST/bin:$PATH" +fi + +cd "$SCRIPT_DIR" + +cargo build --release --target wasm32-unknown-unknown -p jerboa-dns-parser +cargo build --release --target wasm32-unknown-unknown -p jerboa-cdb-parser + +DEST_DIR="$PROJECT_ROOT/lib/jerboa-dns/sandbox" +mkdir -p "$DEST_DIR" + +cp "target/wasm32-unknown-unknown/release/jerboa_dns_parser.wasm" \ + "$DEST_DIR/dns_parser.wasm" +cp "target/wasm32-unknown-unknown/release/jerboa_cdb_parser.wasm" \ + "$DEST_DIR/cdb_parser.wasm" + +echo "Built: $DEST_DIR/dns_parser.wasm ($(wc -c < "$DEST_DIR/dns_parser.wasm") bytes)" +echo "Built: $DEST_DIR/cdb_parser.wasm ($(wc -c < "$DEST_DIR/cdb_parser.wasm") bytes)" + +``` + +--- + +## Assistant (Build · Kimi K3 · 17.3s) + +_Thinking:_ + +The static build scripts use `embed-as-c-array` — let me see what's embedded. Line 13 defines embed-as-c-array. Let me look at where it's called in build-common.ss / build-jdns-linux.ss to see if the .wasm files are embedded into the binary, and how the runtime finds them (maybe `file-exists?` is bypassed via an embedded registry, or load-wasm-bytes reads from embedded memory in static builds). + +**Tool: bash** + +**Input:** +```json +{ + "command": "grep -n \"embed-as-c-array\\|dns_parser\\|cdb_parser\\|sandbox\" static/build-jdns-linux.ss static/build-common.ss | head -20", + "workdir": "/Users/user/mine/jerboa-dns" +} +``` + +**Output:** +``` +static/build-common.ss:4:;;; Defines: embed-as-c-array, find-cross-csv-dir, jdns-posix-symbols, +static/build-common.ss:13:(define (embed-as-c-array in-path stem out-path) +static/build-common.ss:96: (std wasm sandbox) +static/build-common.ss:146:;; Wasm-sandbox symbols pulled from (std wasm sandbox). The Scheme side +static/build-common.ss:147:;; guards every call with wasm-sandbox-available?, so weak stubs that +static/build-common.ss:222: ;; lets the link succeed; at runtime Scheme guards wasm-sandbox-available? so +static/build-common.ss:224: (display "/* Weak stubs for wasm sandbox symbols (libjerboa_native not linked) */\n" out) +static/build-common.ss:321: (embed-as-c-array (format "~a/petite.boot" csv-dir) "petite_boot" "petite_boot.h") +static/build-common.ss:322: (embed-as-c-array (format "~a/scheme.boot" csv-dir) "scheme_boot" "scheme_boot.h") +static/build-common.ss:323: (embed-as-c-array app-boot "app_boot" "app_boot.h") +static/build-common.ss:324: (embed-as-c-array program-output "program_boot" "program_boot.h") + +``` + +--- + +## Assistant (Build · Kimi K3 · 28.2s) + +_Thinking:_ + +So the static build embeds only the boot files (petite.boot, scheme.boot, app_boot, program_boot) as C arrays — NOT the .wasm files. The wasm symbols are "weak stubs" in some builds ("libjerboa_native not linked"). + +Hmm — but wait, that's `jdns-wasm-weak-symbols` for builds where libjerboa_native is NOT linked. In the main `make binary` build, libjerboa_native IS linked with the "wasm" feature. So wasmi is available. + +The key question remains: in the embedded binary, does `wasm-cdb-init!` read the .wasm from disk? From the code I read: yes — `resolve-wasm-path` + `file-exists?` + `load-wasm-bytes` from disk. My live test confirmed: without files staged → "wasm module missing on disk"; with files staged in $ROOT/lib/jerboa-dns/sandbox → "parser=sandboxed". + +But the user says "the wasm is built IN to the binary!" Maybe they mean: +1. The wasmi RUNTIME is built into the binary (libjerboa_native.a with wasm feature) — true, but that's the engine, not the modules. +2. OR maybe in the jerbuild-based `make binary` build (not the static/*.ss cross builds), there IS wasm embedding — the Makefile comment says "pass 1 builds the wasm payloads (pre-build)". "pre-build" just builds them to disk before compiling. Let me check what the jerbuild binary does with pre-build and whether .jerbuild supports embedding data files. Let me check if there's an "embed" directive in .jerbuild (I saw the full file: entry, output, libdirs, ffi-symbols, pre-build, rust-crates — no embed directive). + +Wait — but let me look again at the whole .jerbuild file; I only saw the head -40. Let me check if there are more lines. + +**Tool: bash** + +**Input:** +```json +{ + "command": "wc -l .jerbuild && cat .jerbuild; echo ===; strings jdns | grep -c \"wasi\\|wasm\" 2>/dev/null; ls -la lib/jerboa-dns/sandbox/", + "workdir": "/Users/user/mine/jerboa-dns" +} +``` + +**Output:** +``` + 18 .jerbuild +;; Build jdns with a standalone jerbuild binary: `jerbuild build`. +;; +;; jerbuild bundles Chez Scheme + the jerboa stdlib + the jerboa-native Rust +;; crate, so this needs only jerbuild + a C compiler + cargo (with the +;; wasm32-unknown-unknown target for the sandbox payloads). No jerboa source +;; checkout. The generated FFI table also includes the TLS and crypto surface +;; pulled in by the server, so all three feature groups are linked statically. + +(entry "bin/jdns.ss") +(output "jdns") +(libdirs "lib") +(ffi-symbols "support/ffi-symbols.gen") +;; Build the sandboxed DNS/CDB wasm payloads before embedding. +(pre-build "sh wasm/build.sh") +(rust-crates + ("@bundle/jerboa-native-rs/Cargo.toml" + features: "tls,crypto,wasm" + no-default-features: #t)) +=== +84 +total 32 +drwxr-xr-x@ 4 user staff 128 May 16 15:40 . +drwxr-xr-x@ 15 user staff 480 Jul 30 19:31 .. +-rwxr-xr-x@ 1 user staff 6475 Jul 30 19:45 cdb_parser.wasm +-rwxr-xr-x@ 1 user staff 5434 Jul 30 19:45 dns_parser.wasm + +``` + +--- + +## Assistant (Build · Kimi K3 · 39.9s) + +_Thinking:_ + +The `.jerbuild` says "Build the sandboxed DNS/CDB wasm payloads before embedding." — the word "embedding" is ambiguous. The pre-build builds the wasm files to `lib/jerboa-dns/sandbox/`. There's no embed directive in .jerbuild. + +Let me definitively test the user's claim: does the `jdns` binary contain the wasm modules? The wasm files are small (6475 and 5434 bytes). Let me search the binary for wasm magic bytes (\0asm) and check whether removing the .wasm files from the repo still allows sandboxed mode. The decisive test: run the binary in a directory WITHOUT the wasm files and see if it says "sandboxed" or "missing on disk". My earlier test already showed "wasm module missing on disk" in a bare dir — that directly disproves embedding... UNLESS the binary looks relative to some other location (e.g., relative to the binary's own path or an extracted boot dir). + +Wait — actually, when I ran from /var/folders/.../jdns-test with --root . the log said "wasm module missing on disk". The binary was at /Users/user/mine/jerboa-dns/jdns and CWD had no lib/. So it does NOT find them relative to the binary either (CWD-relative only). If they were embedded, it would have said "sandboxed". + +But hold on — maybe `wasm-sandbox-load` (in (std wasm sandbox)) does something special in static builds: maybe there's a jerboa mechanism `Sregister_embedded_file` or the boot image has the files... Let me check the (std wasm sandbox) source for an embedded-module registry, and grep the jdns binary for the wasm magic header. + +The user says the WASM is embedded. My runtime test suggested otherwise — let me settle this definitively: check the binary for the wasm module bytes and test with the files absent. + +**Tool: bash** + +**Input:** +```json +{ + "command": "xxd lib/jerboa-dns/sandbox/dns_parser.wasm | head -3; echo ---; # search binary for wasm magic (\\0asm\\x01)\npython3 -c \"\ndata = open('jdns','rb').read()\nmagic = b'\\x00asm\\x01\\x00\\x00\\x00'\nidx = []\nstart = 0\nwhile True:\n i = data.find(magic, start)\n if i < 0: break\n idx.append(i); start = i+1\nprint('wasm magic occurrences in jdns:', len(idx), idx[:5])\n# also compare against the actual module bytes\ndns = open('lib/jerboa-dns/sandbox/dns_parser.wasm','rb').read()\ncdb = open('lib/jerboa-dns/sandbox/cdb_parser.wasm','rb').read()\nprint('dns_parser.wasm embedded verbatim:', data.find(dns[:512]) >= 0)\nprint('cdb_parser.wasm embedded verbatim:', data.find(cdb[:512]) >= 0)\n\"", + "workdir": "/Users/user/mine/jerboa-dns" +} +``` + +**Output:** +``` +00000000: 0061 736d 0100 0000 0153 0c60 027f 7f01 .asm.....S.`.... +00000010: 7f60 037f 7f7f 017f 6001 7f00 6006 7f7f .`......`...`... +00000020: 7f7f 7f7f 0060 037f 7f7f 0060 057f 7f7f .....`.....`.... +--- +wasm magic occurrences in jdns: 0 [] +dns_parser.wasm embedded verbatim: False +cdb_parser.wasm embedded verbatim: False + +``` + +--- + +## Assistant (Build · Kimi K3 · 121.0s) + +_Thinking:_ + +Definitive: **the wasm modules are NOT in the `jdns` binary**. Zero wasm magic bytes, and the module contents don't appear. + +What IS built into the binary is the **wasmi runtime** (via libjerboa_native.a with the "wasm" feature) — that's the engine that executes wasm, not the modules. The `.jerbuild` comment "Build the sandboxed DNS/CDB wasm payloads before embedding" is misleading — the pre-build builds them to disk, but nothing embeds them into the binary. + +And the runtime code path confirms: `resolve-wasm-path` looks on disk (CWD-relative, post-chroot), and my live test showed "wasm module missing on disk" without the files and "parser=sandboxed" with them staged in $ROOT. + +The static cross-build scripts (static/*.ss) also only embed boot files as C arrays, not the wasm. + +So the user is partially right: the wasm **runtime/engine** is built into the binary (no dlopen of a wasm engine), but the wasm **modules** (the parsers) are loaded from disk at runtime. I should tell them this clearly and respectfully, with the evidence — this matters a lot for the deploy doc: if they deploy assuming the sandbox is active because "it's built in", they'll silently run the in-process parser everywhere. + +Actually wait — let me double-check one more possibility: maybe the wasm is LZ4-compressed inside the boot image (program_boot.h)? The boot files are embedded as C arrays, and jerbuild uses liblz4 (linked per build output). If the wasm files were bundled into the program boot or as a virtual filesystem, they wouldn't appear verbatim. Let me check program_boot.h for compressed data that might contain wasm. Actually simpler: the runtime test is the ground truth. Without files on disk → "wasm module missing on disk" + "parser=in-process". If the modules were embedded and loadable, `sandbox-available?` wouldn't depend on `file-exists?`. The code explicitly checks `(file-exists? (resolve-wasm-path))` which hits the real filesystem. There's no embedded-file virtualization in wasm-cdb.ss/wasm-dns.ss — I read both files fully; `load-wasm-bytes` uses `open-file-input-port`. + +Unless `(std wasm sandbox)` has an embedded registry: `wasm-sandbox-load bytes` receives bytes read from disk regardless. The bottleneck is the bytes: they come from disk. No disk file → no bytes → fallback. Definitive. + +So: the user is mistaken about the modules (though the engine is indeed linked in). I'll present this evidence gently but directly, since it's deployment-critical, then write deploy.md with the correct file layout. Let me also update the README's misleading comment? The .jerbuild comment "before embedding" — hmm, the pre-build comment says "Build the sandboxed DNS/CDB wasm payloads before embedding." That comment probably means "before embedding the program" (the Scheme program gets embedded; the wasm is built first so it COULD be... but isn't). I won't touch .jerbuild; just note it. + +Now write deploy.md. Content plan: + +# Deploying jdns (production) + +## 0. Build / obtain binaries +- On target arch: `make binary` → `./jdns` (static, self-contained; needs no jerboa install at runtime) +- Cross static builds for Linux/FreeBSD: static/build-jdns-*.ss (maintainer pipeline, needs cross toolchain) → jdns-linux-amd64 etc. +- Verify: `shasum -a 256 jdns`, run `./jdns --help`? (prints usage, exit 1 — fine) + +## 1. Files the server needs (and ONLY these) +Layout on the server: +``` +/srv/jdns/ ← $ROOT (chroot) + data.cdb ← compiled zone (atomically replaced for updates) + lib/jerboa-dns/sandbox/ + dns_parser.wasm ← REQUIRED for sandboxed query parsing + cdb_parser.wasm ← REQUIRED for sandboxed CDB walking +``` +Critical explanation: the wasmi ENGINE is linked into the binary, but the wasm MODULES are read from disk AFTER chroot (paths resolve inside the chroot). Without them, the daemon silently falls back to in-process parsing (logged once as wasm_cdb_fallback). Pin with JDNS_REQUIRE_WASM_*=1 to fail closed instead. + +## 2. Create the service user +``` +pw useradd jdns -s /usr/sbin/nologin -d /nonexistent # FreeBSD +useradd --system --shell /usr/sbin/nologin jdns # Linux +``` +Record its numeric uid/gid. + +## 3. Compile the zone (off-box or on-box) +- tinydns data format or s-expr format; compile with jdns-data: + - build machine with jerboa: `make jdns-data`-style jerbuild exec command (verified) + - or the static `jdns-data-<platform>` artifact from the release pipeline +- Ship data.cdb; atomic replace: write data.cdb.tmp + mv → watcher swaps within JDNS_CDB_RELOAD_MS (default 1s), malformed file keeps last good snapshot serving. + +## 4. Permissions +``` +install -d -o root -g wheel -m 755 /srv/jdns +install -o root -m 644 data.cdb /srv/jdns/data.cdb +install -d -o root -m 755 /srv/jdns/lib/jerboa-dns/sandbox +install -o root -m 444 dns_parser.wasm cdb_parser.wasm /srv/jdns/lib/jerboa-dns/sandbox/ +``` +ROOT owned by root, NOT writable by the jdns user (daemon never writes; reloads are read-only). + +## 5. Run (env config, djbdns-style) +``` +IP=0.0.0.0 PORT=53 ROOT=/srv/jdns DATA=data.cdb UID=<uid> GID=<gid> \ +JDNS_REQUIRE_WASM_PARSER=1 JDNS_REQUIRE_WASM_CDB=1 \ +JDNS_CDB_RELOAD_MS=1000 \ +/srv/jdns/bin/jdns >>/var/log/jdns.log 2>&1 +``` +- Must start as root for :53; root start REQUIRES non-zero UID+GID (refuses otherwise); supplementary groups cleared+verified; regain-of-root probed. +- NEVER set JDNS_ALLOW_SANDBOX_FALLBACK or JDNS_RETAIN_PRIVILEGES in prod — chroot/Landlock must fail closed. +- chroot order: bind → chroot(ROOT) → privdrop → Landlock/Capsicum → load CDB → serve. +- JDNS_LOG_QUERIES=1 optional (noisy). + +## 6. Edge rate limiting (REQUIRED — no native RRL) +The daemon minimizes ANY (REFUSED) and caps UDP at 512B, but has no response-rate limiting. Authoritative DNS without RRL = reflection amplifier (~8x worst case here). MUST rate limit at edge/host firewall: +- Linux nftables example: meter on udp dport 53 +- FreeBSD ipfw/pf example +- TCP dport 53: limit new connections per source + +## 7. Verify the deployment (smoke test) +- `dig @127.0.0.1 -p 53 <zone-apex> SOA` etc. +- Check startup log lines: expected sequence and what each means; specifically: + - `evt=cdb_opened parser=sandboxed` ← REQUIRED; if `parser=in-process`, wasm modules missing + - absence of `wasm_cdb_fallback`, `chdir ... reason=chroot_unavailable` + - `evt=chroot`, `evt=privdrop_verified`, `evt=landlock_active` (Linux) / `capsicum_applied` (FreeBSD) +- TCP: `dig +tcp` +- reload: `touch` a new data.cdb → expect `evt=cdb_reloaded` within ~1s +- malformed: send garbage, expect silent drop + `evt=malformed` warn line + +## 8. Monitoring +- stderr logfmt; watch for servfail, recv_loop_error, cdb_reload_failed +- the daemon is single-process; supervise with daemontools/runit/systemd (examples) + +## 9. Zone updates runbook +- compile → validate → scp → atomic mv → confirm cdb_reloaded in log + +## 10. Rollback +- keep tinydns config intact; switch back by stopping jdns and restarting tinydns on :53. + +Also include the current caveats from my review: test on one server, keep rollback ready, experimental status. + +Note about macOS: chroot won't work as non-root; deploy target is Linux/FreeBSD. Also note port 53 needs root or CAP_NET_BIND_SERVICE — but jdns must start as root anyway for chroot; privdrop happens after bind+chroot. Actually can you chroot without root? No. So root start is mandatory for the full sandbox model; the binary refuses root retention without UID/GID. + +Also mention: `JDNS_PARSER_WASM` / `JDNS_CDB_WASM` env vars can point at explicit paths (inside chroot) as an alternative to the fixed layout. + +One more check for accuracy: resolve-wasm-path also tries "sandbox/cdb_parser.wasm" (second choice) and "/etc/jdns/cdb_parser.wasm". So an alternate layout is $ROOT/etc/jdns/. I'll document the lib/ layout as primary. + +Also verify the exact startup log event names I quote: chroot, privdrop_verified, landlock_active, capsicum_applied, cdb_opened (parser=...), cdb_reload_watcher, tcp_listening, startup. From server.ss code: log-startup 'ip 'port 'udp 'tcp → evt=startup. 'chroot event: (log-info 'chroot 'dir root-dir). privdrop_verified: yes. landlock_active: yes. capsicum_applied: yes. cdb_opened with parser=sandboxed|in-process: yes. Good. + +UDP/TCP note: both bind same port; SO_REUSEADDR set — for systemd-based restart orchestration, quick restarts won't fail on TIME_WAIT. Fine. + +For nftables rate limit example, keep it short and standard: +``` +table inet filter { + chain input { + type filter hook input priority 0; + udp dport 53 meter dnsflood { ip saddr limit rate 50/second burst 100 packets } counter drop + } +} +``` +Hmm, meter syntax varies; use a widely-correct form: +`nft add rule inet filter input udp dport 53 ct state new meter dns-rate { ip saddr timeout 10s limit rate 100/second } drop` — I should keep it indicative, marked "adapt to your firewall". For ipfw: `ipfw add allow udp from any to me 53 limit src-addr 50`. Keep simple and clearly marked as examples. + +systemd unit example: +``` +[Unit] +Description=jdns authoritative DNS +After=network.target + +[Service] +Type=simple +Environment=IP=0.0.0.0 PORT=53 ROOT=/srv/jdns DATA=data.cdb +Environment=UID=995 GID=993 +Environment=JDNS_REQUIRE_WASM_PARSER=1 JDNS_REQUIRE_WASM_CDB=1 +ExecStart=/usr/local/sbin/jdns +Restart=always +# no Protect* that would break chroot/setuid; AmbientCapabilities not needed (starts as root) +User=root + +[Install] +WantedBy=multi-user.target +``` +Note: don't add NoNewPrivileges — chroot+setuid needs... actually NoNewPrivileges doesn't block chroot or setuid-from-root. But systematic hardening like ProtectSystem would conflict with the chroot path setup; keep the unit minimal and say harden separately after confirming it runs. + +Also daemontools-style (tinydns users often use daemontools): /service/jdns/run script. Mention briefly. + +Where to put deploy.md: repo root (user said "a deploy.md"). Root it is. + +Length: comprehensive but tight. Let me write it. + +Definitive answer: **the wasmi *engine* is linked into the binary (statically, via `libjerboa_native.a`), but the wasm *modules* are not** — zero wasm magic bytes in `jdns`, and the modules are read from disk at runtime *after* chroot. That's why my test flipped from `parser=in-process` to `parser=sandboxed` only when I staged the files. This distinction is deployment-critical, so I'll document it prominently. Now writing `deploy.md`: + +**Tool: write** + +**Input:** +```json +{} +``` + +**Error:** +``` +Tool execution aborted +``` + +--- + +## User + +Let's fix this so that all wasm files are in the compiled binary!!!!!! + +--- + +## Assistant (Build · Kimi K3 · 120.3s) + +_Thinking:_ + +The user wants the wasm modules embedded into the compiled binary so no on-disk files are needed. Let me design this properly. + +Current state: +- `wasm-cdb.ss` and `wasm-dns.ss` each have `resolve-wasm-path` → finds a file path on disk, then `load-wasm-bytes` reads it, then `wasm-sandbox-load bytes` parses/validates the module into the wasmi runtime. +- The modules are ~5-6.5 KB each. Embedding them as Scheme bytevector literals in source is totally feasible. + +Design options: + +**Option A: Embed as bytevector constants in the Scheme library.** +Create a new library `(jerboa-dns wasm-embedded)` that contains the wasm bytes as bytevectors, e.g. via a generated `.ss` file with `(def dns-parser-wasm-bytes #vu8(0 97 115 109 ...))`. Then `wasm-cdb-init!`/`wasm-dns-init!` try embedded bytes first, fall back to disk (env override first for development/patching). + +But: the build compiles with jerbuild — a 6.5KB bytevector literal = ~26KB of text in decimal, fine for compilation. The generation step: a script that reads the .wasm files and emits the .ss file. Hook into the build: `.jerbuild` has `pre-build "sh wasm/build.sh"` — extend that script (or add a step) to also generate the embedded module .ss from the built wasm. But careful: `make binary` runs jerbuild build → pre-build runs wasm/build.sh → produces .wasm files → then compile. If the .ss is generated during pre-build, the library graph must include it... it should work since pre-build completes before compilation starts. + +But wait — the AGENTS.md rules: "Never Reference Sibling Checkouts in Build Files" — fine, we're generating from wasm/ outputs in-repo. + +**Option B: Embed via the FFI/C layer** — more complex, no. + +**Option C: Load from embedded bytes with disk override.** +Priority order should be: +1. `JDNS_CDB_WASM` / `JDNS_PARSER_WASM` env var (explicit override, for emergency patching) +2. Embedded bytes (always available) +3. Disk fallback paths (for development when the embedded copy might be stale) + +Actually, hold on. Think about security: an embedded module is the one validated at build time (sha256 recorded in release evidence). The disk files can be replaced (but only inside chroot, and only by root). Making embedded the default removes a whole deployment footgun (the chroot path issue) and removes the silent-fallback-to-in-process issue in the default configuration. + +Design decision: **embedded first, env/disk as explicit override.** The disk search previously existing behavior remains as a development/override path, but the default no longer needs any files on disk. + +Implementation plan: + +1. **New generator script** `wasm/embed-wasm.ss` (or a shell script `wasm/gen-embedded.sh`) that reads `lib/jerboa-dns/sandbox/*.wasm` and emits `lib/jerboa-dns/wasm-embedded.ss` with the bytes. Scheme format: `(def dns-parser-wasm-bytes (bytevector ...))` or `#vu8(...)`. For 6.5KB, `#vu8(0 97 115 109 ...)` is ~26KB text — compiles fine. + +Actually simpler and more robust: emit the bytes as a hex string and decode at load time? A bytevector literal is cleaner and avoids a decode step. Chez compiles bytevector literals fine. + +2. **Hook generation into the build**: + - `wasm/build.sh` already copies the .wasm into lib/jerboa-dns/sandbox/. Add a step that regenerates `lib/jerboa-dns/wasm-embedded.ss` from those files. + - But `wasm-embedded.ss` is a generated file checked into the repo? Or generated at build time? If generated at build time only, `(jerboa-dns wasm-embedded)` won't exist for `jerbuild exec` in tests unless the build ran. The test target depends on `binary` which runs the build (pre-build included), so it'll exist. But for cleanliness: generate it and COMMIT it, with a `make` check that it's up to date? Committing generated files is what they already do with `support/ffi-symbols.gen` (autogenerated, committed). And the wasm files themselves are committed in lib/jerboa-dns/sandbox/. So generating `wasm-embedded.ss` in wasm/build.sh and committing it fits repo conventions. + + Wait, but there's a subtlety: `wasm/build.sh` requires the wasm32 rust target. On machines without it, the build fails anyway (pre-build). OK, consistent. + + Also, the `static/*.ss` cross builds don't run .jerbuild's pre-build (they use scheme --script). Do they include (jerboa-dns wasm-embedded)? They compile the library set from lib/ — the file will be there (committed). Fine. + +3. **Modify `wasm-cdb.ss` and `wasm-dns.ss`**: + - Import `(jerboa-dns wasm-embedded)`. + - Change init: bytes = env-var path → embedded → disk search. + - Keep `sandbox-available?` semantics: currently `(and (wasm-sandbox-available?) (file-exists? (resolve-wasm-path)))`. New semantics: `(wasm-sandbox-available?)` alone, since embedded bytes always exist. But wait — `sandbox-available?` is also used in `parse-query-sandboxed` dispatch and `open-sandboxed-cdb` flow. With embedded modules, sandbox-available? becomes just "wasmi runtime present". That's the desired new behavior: sandbox always on unless the runtime is missing. + - The log reason "wasm module missing on disk" goes away; fallback only when libjerboa_native/wasmi unavailable. + - `resolve-wasm-path` stays for the override path. + + Also `load-wasm-bytes` for overrides. + +4. **Version skew concern**: embedded module could differ from on-disk. Embedded wins by default → deterministic, matches the binary. Good for reproducibility. + +5. **Update `resolve-wasm-path` consumers** — keep for JDNS_*_WASM override only. + +6. **Rebuild + verify**: run the server from an empty dir with NO wasm files on disk and JDNS_REQUIRE_WASM_*=1 → expect `parser=sandboxed`. Also run the full test suite + fuzz-check. + +7. **Update README** (it documents the wasm-on-disk behavior implicitly via JDNS_REQUIRE_WASM_*), and docs/runtime-hardening.md if it mentions paths. Also the release-evidence records wasm-sha256.txt from the disk files — that's still fine as provenance of what got embedded. Maybe add verification that embedded == disk files (the generator guarantees it). + +8. **AGENTS.md**: it mentions build workflows; the wasm-embedded.ss generation is a build detail — README/Makefile are the right places. AGENTS.md in this repo doesn't document wasm layout, so probably no update needed. Let me check if AGENTS.md mentions wasm... The provided AGENTS.md content doesn't mention wasm at all. Skip. + +9. **deploy.md** — the user earlier asked for deploy.md but then pivoted to "fix this so all wasm files are in the compiled binary". So after this fix, deploy gets simpler. They'll probably want deploy.md after. Focus on the embedding now; the doc can mention that no wasm files are needed on disk. + +Let me also think about the generator format. Jerboa/Chez bytevector literal: `#vu8(0 97 115 109 1 0 0 0 ...)`. 6.5KB → ~6500 numbers. That's a big literal but fine. Actually — should I embed as a string with hex? `(utf8->string ...)` no. Bytevector literal is direct. + +Hmm, one concern: `#!chezscheme` library files in this repo use `(library (jerboa-dns wasm-embedded) (export ...) (import ...))` form. The generated file must follow that. Since it's generated, parens are machine-balanced — no manual editing risk. Per AGENTS.md I must use jerboa_write_file for creating .ss files; a generator script emitting the file is different (machine-generated) — but to be safe I can have the generator be a shell/python script (not hand-edited scheme), and validate with jerboa_check_balance + jerboa_verify after. + +Actually, simplest generator: a small shell script using python3 or xxd to format. Or write it in Scheme (wasm/embed.ss run via jerbuild exec). A Python one-liner is simplest and has no bootstrap problem: + +```python +for each module: read bytes, emit "(library (jerboa-dns wasm-embedded) (export ...) (import (chezscheme)) (def name #vu8(...)))" +``` + +Wait — should the two modules be in one library or two? One library `(jerboa-dns wasm-embedded)` exporting both `dns-parser-wasm-bytes` and `cdb-parser-wasm-bytes` plus maybe sha256 strings. Simple. + +Where to hook generation: `wasm/build.sh` end → call `python3 wasm/embed-gen.py` or embed the python inline in build.sh? A separate script file `wasm/embed-gen.py` is cleaner. But build.sh requires python3 then — available on macOS/Linux build machines typically. Alternatively pure shell with od/xxd... python3 is a fair build dep (cargo already required). Hmm, but static build machines might be minimal... cargo + rustup already needed; python3 fine. + +Actually — even better: make it a Scheme script run by jerbuild exec? jerbuild is guaranteed present (it's the build tool). But jerbuild exec with libdirs etc. is more verbose. The static/*.ss scripts don't run wasm/build.sh — they rely on committed files. Since we'll commit the generated .ss, either generator works. I'll use python3 (ubiquitous) and make build.sh call it. Also add a `make wasm-embed` target? Keep simple: generation happens inside wasm/build.sh (which is the pre-build step of `jerbuild build`). + +Wait — one more consideration: if the generated file is committed, developers editing wasm Rust sources must remember to rebuild+recommit. Same workflow as the .wasm files themselves (they're committed too). Consistent. + +Also: checksum self-verification? Could embed expected sha256 and verify at load... overkill; the bytes are the bytes. Skip. + +Now the code changes in wasm-cdb.ss: + +```scheme +(def (load-cdb-module-bytes)