Commit Graph

275 Commits

Author SHA1 Message Date
Razvan Dimescu
d3eab73a31 fix: use sort_by_key to satisfy clippy unnecessary_sort_by 2026-04-16 16:13:15 +03:00
Razvan Dimescu
22ec684e48 Merge remote-tracking branch 'origin/main' into feat/windows-service
# Conflicts:
#	src/main.rs
2026-04-16 16:06:49 +03:00
Razvan Dimescu
aa040fd8a4 Merge pull request #111 from razvandimescu/fix/allowlist-input-focus
fix(dashboard): allowlist input erased by polling refresh
2026-04-16 15:27:02 +03:00
Razvan Dimescu
b69cc89d38 fix(dashboard): skip allowlist re-render while input has focus
The polling refresh replaced the entire allowlist panel innerHTML every
2 seconds, destroying the input field mid-typing. Users had to
paste-and-enter faster than the refresh interval — #106 reported this
as text "timing out and erasing."

Guard: skip renderAllowlist() when allowDomainInput has focus.
2026-04-16 15:12:00 +03:00
Razvan Dimescu
ebb801650e Merge pull request #110 from razvandimescu/feat/build-version
feat: embed git SHA in version string
2026-04-16 13:41:23 +03:00
Razvan Dimescu
30bb7365c9 refactor: robust git-describe parsing for pre-release tags
Switch to --long flag so format is always TAG-N-gSHA[-dirty], then
split from the right. Handles pre-release tags (v0.14.0-rc1) that
broke the previous left-split approach. Remove ineffective directory
watch on .git/refs/tags/. Trim comments.
2026-04-16 13:18:56 +03:00
Razvan Dimescu
0118ab0f44 feat: embed git SHA in version string via build.rs
Adds a build.rs that runs `git describe --tags --always --dirty` and
sets NUMA_BUILD_VERSION at compile time. A new `numa::version()` helper
returns the build version, falling back to CARGO_PKG_VERSION when git
is unavailable (source tarballs, Docker builds without .git).

Version strings:
  tagged release:      0.13.1
  commits ahead:       0.13.1+a87f907
  uncommitted changes: 0.13.1+a87f907-dirty
  no git:              0.13.1

Replaces all 6 inline env!("CARGO_PKG_VERSION") call sites with the
single version() function.
2026-04-16 13:02:25 +03:00
Razvan Dimescu
a87f907d20 Merge pull request #109 from razvandimescu/feat/dashboard-version
feat(dashboard): version in header, restructure footer
2026-04-16 11:29:54 +03:00
Razvan Dimescu
1c5e703330 fix(dashboard): collapse header on mobile (≤700px)
Hide tagline, version tag, and Phone Setup on narrow viewports so
the header stays single-row: logo + status dot + blocking toggle.
Reduces logo font-size from 1.8rem to 1.4rem on mobile.
2026-04-16 06:39:29 +03:00
Razvan Dimescu
cc635f2f73 feat(dashboard): show version in header, restructure footer
Closes #108.

- Add `version` field to /stats (from CARGO_PKG_VERSION).
- Show `v0.13.1` next to the Numa wordmark in the dashboard header.
- Restructure the footer into two semantic rows:
  Row 1 (paths): Config · Data · Logs (platform-detected)
  Row 2 (runtime): Upstream · DNSSEC · SRTT · GitHub
- Drop Mode from the footer (redundant with Upstream label).
- Show only the matching-platform log path instead of both
  macOS and Linux unconditionally.
2026-04-16 06:15:48 +03:00
Razvan Dimescu
7bb484ada3 refactor(windows): deduplicate after simplify review
- Drop the duplicate WINDOWS_SERVICE_NAME constant; call sites use the
  single source of truth at windows_service::SERVICE_NAME.
- windows_service_exe_path and service_config_path now compose from
  crate::data_dir() instead of re-parsing %PROGRAMDATA% locally.
- Factor the 6× sc.exe invocation boilerplate into a run_sc helper.
- Replace the 200ms try_recv polling loop in the service dispatcher
  with a recv_timeout wait — cuts shutdown latency and idle CPU.
- stop_service_scm/delete_service_scm now log warnings instead of
  silently swallowing failures, so unexpected errors are visible.
2026-04-15 23:48:09 +03:00
Razvan Dimescu
b610160cd1 feat(windows): run numa as a real SCM service, drop Run-key autostart
Hooks the service-dispatcher scaffolding from the previous commit to
actually serve DNS, and replaces the HKLM\…\Run login-time autostart
with a proper Windows service created via sc.exe.

**Refactor**
- Extract main.rs's inline server body (~500 lines) into `numa::serve::run`
  so both the interactive CLI entry and the service dispatcher drive the
  same startup/serve loop. main.rs is now a thin subcommand router.
- main.rs goes sync (no #[tokio::main]); each branch that needs async
  builds its own runtime and block_on's. Required so the --service path
  can hand off to SCM without fighting tokio for the entry thread.

**Windows service wrapper**
- `numa::windows_service::run_service` now builds a multi-thread tokio
  runtime on a dedicated thread and runs `serve::run` inside it. Stop/
  Shutdown from SCM aborts the wait loop and reports SERVICE_STOPPED.
- Config path resolves to `%PROGRAMDATA%\numa\numa.toml` when running
  under SCM (SYSTEM's cwd is System32, relative paths don't work).

**Install/uninstall**
- `install_windows` now copies numa.exe to a stable
  `%PROGRAMDATA%\numa\bin\numa.exe` and registers it via `sc create`
  with start=auto, obj=LocalSystem, and a failure policy of
  restart/5000/restart/5000/restart/10000. Starts the service
  immediately when no reboot is pending.
- `uninstall_windows` stops + deletes the service and removes the
  binary copy before restoring DNS.
- Drops the old `register_autostart` / `remove_autostart` helpers that
  wrote to `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run` — that
  path runs at user login in the user's session with no stderr capture
  and no crash-restart policy, which is why we've been flying blind in
  every Windows debug session.

DNS-set bugs (netsh destructive static, IPv6 not touched, uninstall
secondary-drop) and file logging are orthogonal — tracked for follow-up.
2026-04-15 22:24:23 +03:00
Razvan Dimescu
cea4b0ef88 feat(windows): add windows-service crate + SCM dispatcher scaffold
Lets numa.exe act as a real Windows service registered with the SCM,
replacing the HKLM\...\Run login-time autostart that runs in the user
session without stderr capture.

- New `numa::windows_service` module (cfg(windows)) wraps Mullvad's
  `windows-service` crate: registers with SCM, reports Running, handles
  Stop/Shutdown, reports Stopped.
- `numa.exe --service` is the entry point SCM uses
  (`sc create … binPath="numa.exe --service"`); interactive invocations
  are unchanged.
- Dep is gated `[target.'cfg(windows)'.dependencies]` — zero impact on
  macOS/Linux builds or binary size.

Scaffold only. The service currently blocks on an mpsc channel until
Stop arrives; the actual serve loop will hook in once main.rs's inline
server body is extracted into `numa::serve(config_path)` in a follow-up.
This lets `sc start Numa` / `sc stop Numa` be verified end to end today.
2026-04-15 22:14:36 +03:00
Razvan Dimescu
43a5ca4bd5 Merge pull request #105 from razvandimescu/chore/audit-rustls-webpki
chore(deps): bump rustls-webpki to 0.103.12
2026-04-15 14:41:19 +03:00
Razvan Dimescu
b403671e11 chore(deps): bump rustls-webpki to 0.103.12
Patches RUSTSEC-2026-0098 (URI name constraints incorrectly accepted)
and RUSTSEC-2026-0099 (wildcard cert name constraints), both published
2026-04-14. Transitive via reqwest / rustls / hickory / quinn.
2026-04-15 14:27:17 +03:00
Razvan Dimescu
6f0144b237 Merge pull request #103 from razvandimescu/feat/upstream-log-label
feat: distinguish UPSTREAM vs FORWARD in logs and stats
2026-04-15 14:00:28 +03:00
Razvan Dimescu
4bd08e206d feat(dashboard): hide zero-count path and transport rows 2026-04-14 21:25:11 +03:00
Razvan Dimescu
ebb2a5db39 refactor: simplify upstream-path test — reuse pool mutex, drop narrating comment 2026-04-14 18:26:45 +03:00
Razvan Dimescu
e0e0f50838 feat: distinguish UPSTREAM vs FORWARD in logs and stats
Queries matching a [[forwarding]] suffix rule now log as FORWARD;
queries resolved via the default [upstream] pool log as UPSTREAM.
Previously both paths shared the FORWARD label, making it impossible
to tell from logs whether a rule matched.

Adds QueryPath::Upstream, a queries.upstream stats counter exposed
via /stats, plus a matching dashboard filter, bar, and path tag.

Closes part of #102.
2026-04-14 18:18:32 +03:00
Razvan Dimescu
120ba5200e chore: bump version to 0.13.1 v0.13.1 2026-04-14 13:31:35 +03:00
Razvan Dimescu
45046bcf6e Merge pull request #101 from razvandimescu/fix/forward-tls-upstream
fix: accept tls:// and https:// in [[forwarding]] upstreams
2026-04-14 13:09:58 +03:00
Razvan Dimescu
b4b939c78b fix: accept tls:// and https:// in [[forwarding]] upstreams
Config-level forwarding rules were parsed with the UDP-only
`parse_upstream_addr` helper, silently rejecting the DoT/DoH schemes
that the rest of the forwarding pipeline already supports.

Widen `ForwardingRule.upstream` from `SocketAddr` to `Upstream` so
config rules reuse the same parser as `[upstream].address` and
`fallback`. Demote `parse_upstream_addr` to `pub(crate)` to prevent
the same mistake recurring.

Closes #100.
2026-04-14 09:22:24 +03:00
Razvan Dimescu
9a85e271ec Merge pull request #99 from razvandimescu/fix/aur-llvm-libs
fix: add llvm-libs to AUR makedepends
2026-04-13 17:09:08 +03:00
Razvan Dimescu
7dc1a0686f fix: add llvm-libs to AUR makedepends
Fixes #97 — on minimal Arch installs, rustc fails with
"error while loading shared libraries: libLLVM.so" because
llvm-libs isn't pulled in transitively.
2026-04-13 15:58:52 +03:00
Razvan Dimescu
a02722cdf9 Merge pull request #98 from razvandimescu/docker-support
feat: Docker support with multi-arch GHCR images
2026-04-13 15:53:56 +03:00
Razvan Dimescu
3b77dcff61 feat: Docker support — multi-arch GHCR images on release
Add CI workflow to build linux/amd64 + linux/arm64 images and push to
ghcr.io/razvandimescu/numa on tag. Fix Dockerfile (missing benches/),
bake container-aware config (API + proxy bind 0.0.0.0), add Docker
section to README.
2026-04-13 15:48:29 +03:00
Razvan Dimescu
7cc110a0a1 ci: skip CI and AUR builds for blog/site-only changes
Add paths-ignore for site/, blog/, drafts/, *.md, and blog scripts
so content-only pushes don't trigger cargo builds or AUR publishes.
2026-04-13 15:02:19 +03:00
Razvan Dimescu
75fe625f39 blog: drop redundant Numa intro from opening paragraph 2026-04-13 14:48:34 +03:00
Razvan Dimescu
908d076d9b blog: pain-first opening, I-voice, forward-looking close
- Open with shared reqwest pain, not the tool name
- Switch "we" to "I" for personal voice (playbook: solo dev > corporate)
- Replace Unbound feature-gap excuses with what I'm exploring next
  (persistent SRTT, aggressive NSEC, adaptive hedge delays)
- Add context line linking hero cards to the recursive section
2026-04-13 14:37:24 +03:00
Razvan Dimescu
5381e65be4 Merge pull request #96 from razvandimescu/blog/fixing-doh-tail-latency
blog: fixing DoH tail latency post
2026-04-13 14:08:40 +03:00
Razvan Dimescu
6b0a30d004 blog: add fixing DoH tail latency post + blog infrastructure
New post on reqwest HTTP/2 window tuning and request hedging
(Dean & Barroso's "The Tail at Scale" applied to DNS forwarding).
Covers DoH forwarding p99 improvement and cold recursive
resolution from 2.3s to 538ms.

Also adds blog build infrastructure: index generation script,
draft preview server, hero metrics/before-after CSS, and
normalizes date format across existing posts.
2026-04-13 13:49:40 +03:00
Razvan Dimescu
169679bfe4 Merge pull request #95 from razvandimescu/fix/forwarding-precedes-special-use
fix: forwarding rules override special-use NXDOMAIN
2026-04-13 09:37:19 +03:00
Razvan Dimescu
d3f046da4c style: assert loopback addr in subdomain test, trim verbose comment 2026-04-13 08:10:26 +03:00
Razvan Dimescu
0bdde40f40 test: verify forwarded response content from mock upstream 2026-04-13 08:07:58 +03:00
Razvan Dimescu
155c1c4da0 test: full-pipeline coverage for every resolve_query step
Test each pipeline stage in isolation through resolve_query:
- override takes precedence over all other paths
- localhost and *.localhost resolve to loopback
- local zone returns configured records
- .tld proxy resolves registered services to loopback
- blocklist sinkholes to 0.0.0.0
- cache hit returns stored response without upstream
2026-04-13 08:04:59 +03:00
Razvan Dimescu
b40004fe5e refactor: extract shared test infrastructure into testutil module
- test_ctx(): single ServerCtx builder, replaces 3 copies (ctx/api/dot)
- mock_upstream(): canned DNS response server for forwarding tests
- blackhole_upstream(): unresponsive socket for timeout tests
- Removes ~100 lines of duplicated 30-field struct literals
2026-04-13 07:56:47 +03:00
Razvan Dimescu
b8ddc16027 refactor: return QueryPath from resolve_query, add mock upstream to tests
resolve_query now returns (BytePacketBuffer, QueryPath) so callers
and tests can inspect the resolution path without reading the query
log. Production call sites (UDP, DoT, DoH) destructure and ignore it.

The forwarding test now uses a mock UDP upstream that replies with a
canned response, asserting QueryPath::Forwarded instead of != Local.
2026-04-13 07:51:14 +03:00
Razvan Dimescu
48f67be2f1 refactor: deduplicate test_ctx by delegating to test_ctx_with_forwarding 2026-04-13 07:39:55 +03:00
Razvan Dimescu
ca00846393 fix: forwarding rules override special-use NXDOMAIN for private PTR zones
Explicit [[forwarding]] rules now take precedence over the RFC 6303
special-use domain intercept. Previously, PTR queries for private
ranges (e.g. 168.192.in-addr.arpa) always returned local NXDOMAIN
even when a forwarding rule pointed them at a corporate DNS server.

Add full-pipeline resolve_query test harness (test_ctx + resolve_in_test)
and two tests covering both the default behavior and the override.

Closes #94
2026-04-13 07:36:53 +03:00
Razvan Dimescu
4d4e48bbd6 chore: bump version to 0.13.0 v0.13.0 2026-04-13 01:05:20 +03:00
Razvan Dimescu
724c4a6017 Merge pull request #91 from razvandimescu/docs/readme-update
docs: update README with v0.13.0 features
2026-04-13 01:03:16 +03:00
Razvan Dimescu
2b29a44ee0 docs: remove unfair NextDNS comparison from performance section
Comparing local cache (0.8ms) vs a remote service (37ms) measures
network latency, not resolver quality. Any local resolver would
show the same advantage. Replaced with AdGuard Home comparison
which is a fair local-to-local benchmark.
2026-04-13 01:02:10 +03:00
Razvan Dimescu
588e5226fd Merge pull request #92 from razvandimescu/bench/vs-adguard
bench: add --vs-adguard comparison mode
2026-04-13 01:00:20 +03:00
Razvan Dimescu
501902d569 bench: add --vs-adguard mode for Numa vs AdGuard Home comparison
AdGuard Home on port 5457, both forwarding via DoH. Cached queries
tied at 0.1ms. On degraded networks hedging hurts p99 (28ms vs 10ms
without) — both requests pay the same high RTT with no random spikes
to rescue. On clean networks hedging wins.
2026-04-13 00:56:58 +03:00
Razvan Dimescu
77d2c8bbcd docs: update README comparison table, performance, and roadmap
- Comparison table: add DoH/DoT upstream, DoH server, request hedging,
  serve-stale + prefetch, conditional forwarding rows
- Performance: update with current benchmark numbers (0.1ms cached,
  47x NextDNS, p99 -28% vs Unbound)
- Roadmap: add hedging, serve-stale, conditional forwarding, DoT upstream
- Fix broken benchmarks link (bench/ → benches/)
2026-04-13 00:18:52 +03:00
Razvan Dimescu
274338e7f9 Merge pull request #88 from razvandimescu/fix/doh-loopback-san
fix: DoH endpoint accepts loopback, TLS cert includes IP SANs
2026-04-13 00:03:30 +03:00
Razvan Dimescu
305935ed98 style: rustfmt strip_port 2026-04-12 23:59:51 +03:00
Razvan Dimescu
bd505813b6 test: verify TLS cert SANs (wildcard, services, loopback, localhost, bare TLD)
Parse the generated DER cert with x509-parser to assert the exact SAN
set, catching silent try_into() failures that a params-level test
would miss.
2026-04-12 23:54:55 +03:00
Razvan Dimescu
115a55b199 fix: bracketed IPv6, localhost SAN, split host-check helpers
- is_doh_host split into strip_port + is_loopback_host + is_tld_match
- strip_port handles bracketed IPv6 ([::1]:443) and rejects bare IPv6
- Add [::1] to accepted loopback hosts, add localhost DNS SAN to cert
- Remove dead sans.is_empty() guard (loopback IPs always present)
2026-04-12 23:54:26 +03:00
Razvan Dimescu
3665deb56b fix: accept loopback addresses for DoH and add IP SANs to TLS cert
The DoH endpoint rejected requests with Host: 127.0.0.1/::1/localhost,
and the generated TLS cert had no IP SANs — so browsers couldn't use
https://127.0.0.1/dns-query even with the CA trusted.

- is_doh_host now accepts 127.0.0.1, ::1, localhost (with optional port)
- TLS cert includes 127.0.0.1 and ::1 IP SANs, plus bare TLD DNS SAN

Closes #87
2026-04-12 23:54:26 +03:00