update README, dashboard layout, and version bump to 0.3.0

Add LAN discovery section to README with mesh and hub mode docs.
Update comparison table and roadmap. Move Local Services panel
above Blocking in dashboard for developer-first layout.
Bump version from 0.1.0 to 0.3.0 to match release cadence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Razvan Dimescu
2026-03-22 06:59:47 +02:00
parent 9a3de2f231
commit 5866ff1ba1
3 changed files with 63 additions and 26 deletions

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "numa" name = "numa"
version = "0.1.0" version = "0.3.0"
authors = ["razvandimescu <razvan@dimescu.com>"] authors = ["razvandimescu <razvan@dimescu.com>"]
edition = "2021" edition = "2021"
description = "Ephemeral DNS overrides for development and testing. Point any hostname to any endpoint. Auto-revert when you're done." description = "Ephemeral DNS overrides for development and testing. Point any hostname to any endpoint. Auto-revert when you're done."

View File

@@ -39,9 +39,10 @@ sudo ./target/release/numa
- **Ad blocking that travels with you** — 385K+ domains blocked via [Hagezi Pro](https://github.com/hagezi/dns-blocklists). Works on any network: coffee shops, hotels, airports. - **Ad blocking that travels with you** — 385K+ domains blocked via [Hagezi Pro](https://github.com/hagezi/dns-blocklists). Works on any network: coffee shops, hotels, airports.
- **Local service proxy** — `https://frontend.numa` instead of `localhost:5173`. Auto-generated TLS certs, WebSocket support for HMR. Like `/etc/hosts` but with a dashboard and auto-revert. - **Local service proxy** — `https://frontend.numa` instead of `localhost:5173`. Auto-generated TLS certs, WebSocket support for HMR. Like `/etc/hosts` but with a dashboard and auto-revert.
- **LAN service discovery** — Numa instances on the same network find each other automatically via multicast. Access a teammate's `api.numa` from your machine, zero config.
- **Developer overrides** — point any hostname to any IP, auto-reverts after N minutes. REST API with 22 endpoints. - **Developer overrides** — point any hostname to any IP, auto-reverts after N minutes. REST API with 22 endpoints.
- **Sub-millisecond caching** — cached lookups in 0ms. Faster than any public resolver. - **Sub-millisecond caching** — cached lookups in 0ms. Faster than any public resolver.
- **Live dashboard** — real-time stats, query log, blocking controls, service management. - **Live dashboard** — real-time stats, query log, blocking controls, service management. LAN accessibility badges show which services are reachable from other devices.
- **macOS + Linux** — `numa install` configures system DNS, `numa service start` runs as launchd/systemd service. - **macOS + Linux** — `numa install` configures system DNS, `numa service start` runs as launchd/systemd service.
## Local Service Proxy ## Local Service Proxy
@@ -59,6 +60,7 @@ open http://frontend.numa # → proxied to localhost:5173
- **HTTPS with green lock** — auto-generated local CA + per-service TLS certs - **HTTPS with green lock** — auto-generated local CA + per-service TLS certs
- **WebSocket** — Vite/webpack HMR works through the proxy - **WebSocket** — Vite/webpack HMR works through the proxy
- **Health checks** — dashboard shows green/red status per service - **Health checks** — dashboard shows green/red status per service
- **LAN sharing** — services bound to `0.0.0.0` are automatically discoverable by other Numa instances on the network. Dashboard shows "LAN" or "local only" per service.
- **Persistent** — services survive restarts - **Persistent** — services survive restarts
- Or configure in `numa.toml`: - Or configure in `numa.toml`:
@@ -68,6 +70,39 @@ name = "frontend"
target_port = 5173 target_port = 5173
``` ```
## LAN Service Discovery
Run Numa on multiple machines. They find each other automatically:
```
Machine A (192.168.1.5) Machine B (192.168.1.20)
┌──────────────────────┐ ┌──────────────────────┐
│ Numa │ multicast │ Numa │
│ services: │◄───────────►│ services: │
│ - api (port 8000) │ discovery │ - grafana (3000) │
│ - frontend (5173) │ │ │
└──────────────────────┘ └──────────────────────┘
```
From Machine B:
```bash
dig @127.0.0.1 api.numa # → 192.168.1.5
curl http://api.numa # → proxied to Machine A's port 8000
```
No configuration needed. Multicast announcements on `239.255.70.78:5390`, configurable via `[lan]` in `numa.toml`.
**Hub mode** — don't want to install Numa on every machine? Run one instance as a shared DNS server and point other devices to it:
```bash
# On the hub machine, bind to LAN interface
[server]
bind_addr = "0.0.0.0:53"
# On other devices, set DNS to the hub's IP
# They get .numa resolution, ad blocking, caching — zero install
```
## How It Compares ## How It Compares
| | Pi-hole | AdGuard Home | NextDNS | Cloudflare | Numa | | | Pi-hole | AdGuard Home | NextDNS | Cloudflare | Numa |
@@ -76,6 +111,7 @@ target_port = 5173
| Portable (travels with laptop) | No (appliance) | No (appliance) | Cloud only | Cloud only | Single binary | | Portable (travels with laptop) | No (appliance) | No (appliance) | Cloud only | Cloud only | Single binary |
| Developer overrides | No | No | No | No | REST API + auto-expiry | | Developer overrides | No | No | No | No | REST API + auto-expiry |
| Local service proxy | No | No | No | No | `.numa` + HTTPS + WS | | Local service proxy | No | No | No | No | `.numa` + HTTPS + WS |
| LAN service discovery | No | No | No | No | Multicast, zero config |
| Data stays local | Yes | Yes | Cloud | Cloud | 100% local | | Data stays local | Yes | Yes | Cloud | Cloud | 100% local |
| Zero config | Complex | Docker/setup | Yes | Yes | Works out of the box | | Zero config | Complex | Docker/setup | Yes | Yes | Works out of the box |
| Self-sovereign DNS | No | No | No | No | pkarr/DHT roadmap | | Self-sovereign DNS | No | No | No | No | pkarr/DHT roadmap |
@@ -97,6 +133,7 @@ No DNS libraries. The wire protocol — headers, labels, compression pointers, r
- [x] Ad blocking — 385K+ domains, live dashboard, allowlist - [x] Ad blocking — 385K+ domains, live dashboard, allowlist
- [x] System integration — macOS + Linux, launchd/systemd, Tailscale/VPN auto-discovery - [x] System integration — macOS + Linux, launchd/systemd, Tailscale/VPN auto-discovery
- [x] Local service proxy — `.numa` domains, HTTP/HTTPS proxy, auto TLS, WebSocket - [x] Local service proxy — `.numa` domains, HTTP/HTTPS proxy, auto TLS, WebSocket
- [x] LAN service discovery — multicast auto-discovery, cross-machine DNS + proxy
- [ ] pkarr integration — self-sovereign DNS via Mainline DHT (15M nodes) - [ ] pkarr integration — self-sovereign DNS via Mainline DHT (15M nodes)
- [ ] Global `.numa` names — self-publish, DHT-backed, first-come-first-served - [ ] Global `.numa` names — self-publish, DHT-backed, first-come-first-served

View File

@@ -577,22 +577,26 @@ body {
<!-- Sidebar --> <!-- Sidebar -->
<div class="sidebar"> <div class="sidebar">
<!-- Blocking --> <!-- Local services -->
<div class="panel" id="blockingPanel"> <div class="panel">
<div class="panel-header"> <div class="panel-header">
<span class="panel-title">Blocking</span> <div>
<span class="panel-title" id="blockingRefresh" style="color:var(--text-dim);font-weight:400;"></span> <span class="panel-title">Local Services</span>
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:0.15rem;">Give localhost apps clean .numa URLs. Persistent, with HTTP proxy.</div>
</div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form class="override-form" onsubmit="return checkDomain(event)" style="margin-bottom:0;border-bottom:none;padding-bottom:0;"> <form class="override-form" id="serviceForm" onsubmit="return addService(event)">
<div class="override-form-row"> <div class="override-form-row">
<input type="text" id="checkDomainInput" placeholder="Is this domain blocked?" required style="flex:3"> <input type="text" id="svcName" placeholder="name (becomes name.numa)" required style="flex:2">
<button type="submit" class="btn" style="background:var(--violet);color:white;flex-shrink:0;">Check</button> <input type="number" id="svcPort" placeholder="port (e.g. 3000)" required min="1" max="65535" style="flex:1">
</div> </div>
<button type="submit" class="btn btn-add">Add Service</button>
<div class="override-error" id="serviceError"></div>
</form> </form>
<div id="checkResult" style="display:none;margin-top:0.6rem;padding:0.5rem 0.6rem;border-radius:5px;font-family:var(--font-mono);font-size:0.72rem;"></div> <div id="servicesList">
<div id="blockingSources" style="margin-top:0.8rem;padding-top:0.6rem;border-top:1px solid var(--border);"></div> <div class="empty-state">No services configured</div>
<div id="blockingAllowlist" style="margin-top:0.8rem;padding-top:0.6rem;border-top:1px solid var(--border);"></div> </div>
</div> </div>
</div> </div>
@@ -621,26 +625,22 @@ body {
</div> </div>
</div> </div>
<!-- Local services --> <!-- Blocking -->
<div class="panel"> <div class="panel" id="blockingPanel">
<div class="panel-header"> <div class="panel-header">
<div> <span class="panel-title">Blocking</span>
<span class="panel-title">Local Services</span> <span class="panel-title" id="blockingRefresh" style="color:var(--text-dim);font-weight:400;"></span>
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:0.15rem;">Give localhost apps clean .numa URLs. Persistent, with HTTP proxy.</div>
</div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form class="override-form" id="serviceForm" onsubmit="return addService(event)"> <form class="override-form" onsubmit="return checkDomain(event)" style="margin-bottom:0;border-bottom:none;padding-bottom:0;">
<div class="override-form-row"> <div class="override-form-row">
<input type="text" id="svcName" placeholder="name (becomes name.numa)" required style="flex:2"> <input type="text" id="checkDomainInput" placeholder="Is this domain blocked?" required style="flex:3">
<input type="number" id="svcPort" placeholder="port (e.g. 3000)" required min="1" max="65535" style="flex:1"> <button type="submit" class="btn" style="background:var(--violet);color:white;flex-shrink:0;">Check</button>
</div> </div>
<button type="submit" class="btn btn-add">Add Service</button>
<div class="override-error" id="serviceError"></div>
</form> </form>
<div id="servicesList"> <div id="checkResult" style="display:none;margin-top:0.6rem;padding:0.5rem 0.6rem;border-radius:5px;font-family:var(--font-mono);font-size:0.72rem;"></div>
<div class="empty-state">No services configured</div> <div id="blockingSources" style="margin-top:0.8rem;padding-top:0.6rem;border-top:1px solid var(--border);"></div>
</div> <div id="blockingAllowlist" style="margin-top:0.8rem;padding-top:0.6rem;border-top:1px solid var(--border);"></div>
</div> </div>
</div> </div>