Commit Graph

86 Commits

Author SHA1 Message Date
Razvan Dimescu
4c58ff49b0 reduce network change detection to 5s with tiered polling
LAN IP checked every 5s (cheap UDP socket call). Full upstream
re-detection runs every 30s as safety net, or immediately when
LAN IP changes. Reduces worst-case network switch recovery from
30s to 5s.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:36:03 +02:00
Razvan Dimescu
5810ee5aac show upstream DNS in stats API and dashboard footer
Expose current upstream address in /stats response. Dashboard footer
now shows "Upstream: x.x.x.x:53" — updates live when the network
watcher swaps the upstream.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 11:04:54 +02:00
Razvan Dimescu
06850de728 fix circular reference: detect DHCP DNS when scutil shows loopback
When numa install is active, scutil --dns only returns 127.0.0.1.
Previously fell back to 9.9.9.9 (Quad9) which fails on networks
that block external DNS. Now reads DHCP-provided DNS from
ipconfig getpacket en0/en1 as intermediate fallback before Quad9.

Tested on a network that blocks 8.8.8.8, 9.9.9.9, 1.1.1.1 but
allows ISP DNS (213.154.124.25) — Numa now auto-detects and uses it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 10:24:54 +02:00
Razvan Dimescu
995916d01b generalize upstream re-detection into network change watcher
Always detect network changes (LAN IP, upstream, peers) regardless
of upstream config. LAN IP is now tracked in ServerCtx and updated
every 30s — multicast announcements use the current IP instead of
the startup IP. Upstream re-detection still only runs when
auto-detected. Peer flush triggers on any network change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 09:38:09 +02:00
Razvan Dimescu
7aca3b1991 fix DNS failure on network change with upstream re-detection
Upstream DNS was resolved once at startup and never updated. Switching
Wi-Fi networks made all queries fail until restart.

Now spawns a background task (every 30s) that re-runs system DNS
discovery and swaps the upstream atomically if it changed. Also flushes
stale LAN peers from the old network on change.

Only activates when upstream is auto-detected (not explicitly configured).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 09:31:49 +02:00
Razvan Dimescu
c333705a0e fix needless return in trust_ca for Windows clippy
On Windows, the not(macos/linux) cfg block is the only path, so
clippy flags the return as needless. Use expression form instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 08:29:28 +02:00
Razvan Dimescu
50d17ae118 fix Windows clippy errors and unreachable code
Gate version detection behind cfg(unix), fix unreachable Ok(()) after
return in trust_ca, use next_back() and is_some_and() per clippy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 08:23:25 +02:00
Razvan Dimescu
5495107c9e add Windows support (Phase 1)
Cross-platform paths: config_dir() uses %APPDATA%, data_dir() uses
%PROGRAMDATA% on Windows. TLS cert directory uses data_dir() instead
of hardcoded /usr/local/var/numa. Windows DNS discovery via ipconfig.
Fixed cfg gates from not(macos) to explicit linux to prevent Linux
code compiling on Windows. Added Windows target to CI and release
workflows with zip packaging.

System integration (numa install/service) not yet supported on Windows
— users run numa.exe manually.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 08:13:53 +02:00
Razvan Dimescu
9a3de2f231 add LAN accessibility indicator for services
Show whether each service is reachable from the network or bound to
localhost only. Dashboard displays green "LAN" or amber "local only"
badge next to each healthy service. Unified TCP check function,
concurrent health+LAN probes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 06:35:12 +02:00
Razvan Dimescu
6fdadd637c fix LAN discovery: instance-based self-filter and multicast port reuse
Replace IP-based self-announcement filtering with a per-process instance
ID (pid ^ timestamp) so multiple instances on the same host can discover
each other. Enable SO_REUSEPORT for multicast socket binding on Unix.
Add multicast address validation on configured group.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 00:20:33 +02:00
Razvan Dimescu
9041ccc2e1 fix rustfmt formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 16:54:03 +02:00
Razvan Dimescu
c9f1d98f45 add LAN service discovery via UDP multicast
Numa instances on the same network auto-discover each other's .numa
services. No config, no cloud — just multicast on 239.255.70.78:5390.

- PeerStore with lazy expiry (90s timeout, 30s broadcast interval)
- DNS resolves remote .numa services to peer's LAN IP (not localhost)
- Proxy forwards to peer IP for remote services
- Graceful degradation if multicast bind fails

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 16:45:46 +02:00
Razvan Dimescu
285778b646 add styled 404 page for unregistered .numa domains
Roman Stone themed 404 with Instrument Serif heading, JetBrains Mono
domain badge, brick pattern background, syntax-highlighted curl
example, and a delayed easter egg. Also updates dashboard link in
README to numa.numa.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 04:31:17 +02:00
Razvan Dimescu
7a64e7c4aa fix truncation check: use == instead of >= for buffer-full detection
recv_from can never return more bytes than the buffer size — the kernel
truncates silently. == is the correct heuristic for detecting truncation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 03:35:21 +02:00
Razvan Dimescu
2cb87bbe83 fix rustfmt formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 03:31:44 +02:00
Razvan Dimescu
4b60a4b49c launch hardening: TC bit, Dockerfile, platform-aware deploy
- Set TC (truncation) bit when response exceeds 4096-byte buffer
  instead of dropping the response silently. Clients can retry via TCP.
- Log when upstream response is truncated in forward.rs.
- Dockerfile: bump to Rust 1.88, include site/service files, use
  alpine runtime instead of scratch, add cmake/perl for aws-lc-sys.
- Makefile deploy: platform-aware — codesign on macOS, systemctl on Linux.
- README: trim roadmap to near-term items only.
- Verified: Docker build + smoke test passes on Linux (Alpine musl).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 03:31:15 +02:00
Razvan Dimescu
cfd9a562af fix CA removal: delete by SHA-1 hash, update README with TLS
security delete-certificate -c fails when multiple certs match.
Now finds all certs by hash and deletes each individually.
Also updated README with HTTPS, service persistence, and TLS mentions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 01:35:14 +02:00
Razvan Dimescu
5e7a653f9c fix rustfmt formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 01:15:51 +02:00
Razvan Dimescu
3bfcd827ac add TLS, service persistence, blocking panel, query types
- Local TLS: auto-generated CA + per-service certs (explicit SANs, not
  wildcards — browsers reject *.numa under single-label TLDs). HTTPS
  proxy on :443 via rustls/tokio-rustls. `numa install` trusts CA in
  macOS Keychain / Linux ca-certificates.
- Service persistence: user-added services saved to
  ~/.config/numa/services.json, survive restarts.
- Blocking panel: renamed "Check Domain" to "Blocking" with sources
  display, allowlist management UI, unpause button.
- Query types: recognize SOA, PTR, TXT, SRV, HTTPS (type 65) instead
  of logging as UNKNOWN.
- Blocklist gzip: reqwest now decompresses gzip responses from CDNs.
- Unified config_dir() in lib.rs for consistent path resolution under
  sudo and launchd. TLS certs use /usr/local/var/numa/ (writable as
  root daemon).
- Dashboard UX: panel subtitles differentiating overrides vs services,
  better placeholders, proxy route display, 600px query log height.
- Deploy: make deploy handles build+copy+codesign+restart cycle.
- Demo: scripts/record-demo.sh for recording hero GIF with CDP.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 01:15:07 +02:00
Razvan Dimescu
10502f2db2 fix service restart: add codesign and make deploy target
macOS kills unsigned binaries, so numa service restart failed after
copying a new build. Added ad-hoc codesign to restart flow and a
make deploy target that handles the full build-copy-sign-restart cycle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 15:22:33 +02:00
Razvan Dimescu
754b064570 fix rustfmt formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 15:07:42 +02:00
Razvan Dimescu
8f959ce0a5 add local service proxy with .numa domains
HTTP reverse proxy on port 80 lets developers use clean domain names
(frontend.numa, api.numa) instead of localhost:PORT. Includes WebSocket
upgrade support for HMR, TCP health checks, dashboard UI panel, and
REST API for service management. numa.numa is preconfigured for the
dashboard itself.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 15:07:15 +02:00
Razvan Dimescu
14a9e9e7e3 add numa service restart command
Kills the running process and lets launchd/systemd respawn it
with the updated binary. DNS stays configured throughout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 14:26:56 +02:00
Razvan Dimescu
ee776938c5 add auto-detect upstream, install script, release workflow
- Default upstream auto-detected from system resolver (scutil/resolv.conf)
  instead of hardcoding Google 8.8.8.8. Falls back to Quad9 (9.9.9.9).
- Single scutil --dns pass for both upstream detection and forwarding rules
- Linux: reads backup resolv.conf if current only has loopback
- Service start/stop now couples DNS config (install on start, uninstall on stop)
- Install script for one-line binary install from GitHub Releases
- GitHub Actions release workflow: builds for macOS/Linux x86_64/aarch64

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 14:14:20 +02:00
Razvan Dimescu
5eec8915d4 fix FORMERR: filter UNKNOWN records and increase buffer to 4096
Root cause: upstream resolvers return EDNS OPT records (type 41) in
the additional section. Our parser reads them as UNKNOWN, but write()
silently skips them — creating a header that claims N additional records
but a body with 0, producing FORMERR on the client side.

Fix: filter out UNKNOWN records before serialization and adjust header
counts to match. Also increase BytePacketBuffer from 512 to 4096 bytes
to handle modern DNS responses with many records.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 14:11:46 +02:00
Razvan Dimescu
7e29f3cb57 add domain check endpoint and dashboard search box
GET /blocking/check/{domain} — returns whether a domain is blocked,
the reason (exact match, parent domain, allowlist, disabled), and
the matching rule. Dashboard sidebar has a "Check Domain" search
box with inline results and one-click allow button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 12:58:25 +02:00
Razvan Dimescu
0658ed7310 add service management CLI, log path in dashboard footer
- numa service start/stop/status commands (launchd + systemd)
- Dashboard footer shows log paths (macOS + Linux) and GitHub link
- Help text updated with all service commands

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 12:41:16 +02:00
Razvan Dimescu
57c4742f09 harden Linux DNS config and fix review findings
- Detect systemd-resolved: use drop-in config instead of overwriting
  /etc/resolv.conf (which gets regenerated)
- Warn if /etc/resolv.conf is a symlink (NetworkManager, etc.)
- Fix TOCTOU: attempt copy/remove directly, handle NotFound
- Remove side-effect from backup_path_linux (no eager mkdir)
- Fix macOS $HOME fallback: /var/root instead of /tmp
- Log warnings on launchctl/systemctl failures instead of silencing
- Delete plist before unloading (prevents zombie restarts)
- Extract ensure_binary_installed helper on Linux

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 12:32:20 +02:00
Razvan Dimescu
4645df50e0 add Linux systemd service and DNS configuration
Linux:
- numa install: backs up /etc/resolv.conf, sets nameserver to 127.0.0.1
- numa uninstall: restores original /etc/resolv.conf from backup
- numa service start: installs systemd unit, enables + starts
- numa service stop: stops, disables, removes unit file
- numa service status: shows systemctl status

macOS: launchd plist (already working)

Both platforms: Restart=always / KeepAlive=true for crash recovery.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 12:24:03 +02:00
Razvan Dimescu
ae9edb3593 fix CI: gate macOS-only helpers behind cfg(target_os = macos)
Move HashMap, PathBuf, numa_data_dir, backup_path inside macOS
cfg blocks so Linux builds don't see unused imports/functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 11:48:33 +02:00
Razvan Dimescu
2db44bd7d0 add system DNS auto-configuration (install/uninstall)
numa install  — saves current DNS, sets all network services to 127.0.0.1
numa uninstall — restores original DNS from ~/.numa/original-dns.json
numa help — shows usage

macOS: uses networksetup to enumerate services and set/restore DNS.
Linux: stubs with instructions for manual setup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 11:39:30 +02:00
Razvan Dimescu
87ca4f095d fix CI: gate macOS-only imports and functions behind cfg
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 11:11:32 +02:00
Razvan Dimescu
4dc5b94c7a add ad blocking, live dashboard, system DNS auto-discovery
- DNS-level ad blocking: 385K+ domains via Hagezi Pro blocklist, subdomain
  matching, one-click allowlist, pause/toggle, background refresh every 24h
- Live dashboard at :5380 with real-time stats, query log, override
  management (create/edit/delete), blocking controls
- System DNS auto-discovery: parses scutil --dns on macOS to find
  conditional forwarding rules (Tailscale, VPN split-DNS)
- REST API expanded to 18 endpoints (blocking, overrides, diagnostics)
- Startup banner with colored system info
- Performance benchmarks (bench/dns-bench.sh)
- Landing page updated with new positioning and comparison table
- CI, Dockerfile, LICENSE, development plan docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 10:54:23 +02:00
Razvan Dimescu
89e7cbd989 add Makefile with clippy/rustfmt linting, fix all warnings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 05:04:31 +02:00
Razvan Dimescu
9c71e9bb3f refactor to async tokio with modular architecture
- Replace synchronous std::net::UdpSocket with tokio async runtime
- Spawn concurrent task per incoming DNS query via tokio::spawn
- Extract monolithic main.rs into modules: buffer, header, question,
  record, packet, config, cache, forward, stats
- Share state across tasks via Arc<ServerCtx> with scoped Mutex locks
- Add TOML config loading, TTL-aware cache, structured logging, stats
- Add CLAUDE.md, README, dns_fun.toml config, and design docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 04:50:16 +02:00
razvandimescu
4e61caac45 first commit 2020-12-29 12:29:09 +02:00