Merge pull request #5 from razvandimescu/feat/network-economics
Launch hardening: TC bit, Dockerfile, deploy, truncation fix
This commit is contained in:
14
Dockerfile
14
Dockerfile
@@ -1,15 +1,17 @@
|
|||||||
FROM rust:1.85-alpine AS builder
|
FROM rust:1.88-alpine AS builder
|
||||||
RUN apk add --no-cache musl-dev
|
RUN apk add --no-cache musl-dev cmake make perl
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
RUN mkdir src && echo 'fn main() {}' > src/main.rs && echo '' > src/lib.rs
|
RUN mkdir src && echo 'fn main() {}' > src/main.rs && echo '' > src/lib.rs
|
||||||
RUN cargo build --release 2>/dev/null || true
|
RUN cargo build --release 2>/dev/null || true
|
||||||
RUN rm -rf src
|
RUN rm -rf src
|
||||||
COPY src/ src/
|
COPY src/ src/
|
||||||
|
COPY site/ site/
|
||||||
|
COPY numa.toml com.numa.dns.plist numa.service ./
|
||||||
RUN touch src/main.rs src/lib.rs
|
RUN touch src/main.rs src/lib.rs
|
||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
|
|
||||||
FROM scratch
|
FROM alpine:3.20
|
||||||
COPY --from=builder /app/target/release/numa /numa
|
COPY --from=builder /app/target/release/numa /usr/local/bin/numa
|
||||||
EXPOSE 53/udp 5380/tcp
|
EXPOSE 53/udp 80/tcp 443/tcp 5380/tcp
|
||||||
ENTRYPOINT ["/numa"]
|
ENTRYPOINT ["numa"]
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -22,7 +22,11 @@ clean:
|
|||||||
deploy:
|
deploy:
|
||||||
cargo build --release
|
cargo build --release
|
||||||
sudo cp target/release/numa /usr/local/bin/numa
|
sudo cp target/release/numa /usr/local/bin/numa
|
||||||
|
ifeq ($(shell uname -s),Darwin)
|
||||||
sudo codesign -f -s - /usr/local/bin/numa
|
sudo codesign -f -s - /usr/local/bin/numa
|
||||||
sudo kill $$(pgrep -f /usr/local/bin/numa) 2>/dev/null || true
|
sudo kill $$(pgrep -f /usr/local/bin/numa) 2>/dev/null || true
|
||||||
|
else
|
||||||
|
sudo systemctl restart numa 2>/dev/null || sudo kill $$(pgrep -f /usr/local/bin/numa) 2>/dev/null || true
|
||||||
|
endif
|
||||||
@sleep 1
|
@sleep 1
|
||||||
@dig @127.0.0.1 google.com +short +time=3 > /dev/null && echo "Service restarted successfully" || echo "Warning: DNS not responding yet"
|
@dig @127.0.0.1 google.com +short +time=3 > /dev/null && echo "Service restarted successfully" || echo "Warning: DNS not responding yet"
|
||||||
|
|||||||
236
README.md
236
README.md
@@ -2,239 +2,99 @@
|
|||||||
|
|
||||||
**DNS you own. Everywhere you go.**
|
**DNS you own. Everywhere you go.**
|
||||||
|
|
||||||
Block ads and trackers. Override DNS for development. Name your local services. Cache for speed. A single portable binary built from scratch in Rust — no Raspberry Pi, no cloud, no account.
|
A portable DNS resolver in a single binary. Block ads on any network, name your local services (`frontend.numa`), and override any hostname with auto-revert — all from your laptop, no cloud account or Raspberry Pi required.
|
||||||
|
|
||||||
|
Built from scratch in Rust. Zero DNS libraries. RFC 1035 wire protocol parsed by hand.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Why
|
|
||||||
|
|
||||||
- **Ad blocking that travels with you** — 385K+ domains blocked out of the box. Works on any network: coffee shops, hotels, airports.
|
|
||||||
- **Developer overrides** — point any hostname to any IP with auto-revert. No more editing `/etc/hosts`.
|
|
||||||
- **Local service proxy** — access `https://frontend.numa` instead of `localhost:5173`. Auto-generated TLS certs, WebSocket support for HMR.
|
|
||||||
- **Sub-millisecond caching** — cached lookups in 0ms. Faster than any public resolver.
|
|
||||||
- **Live dashboard** — real-time query stats, blocking controls, override management, local services at `http://numa.numa` (or `localhost:5380`).
|
|
||||||
- **Single binary, zero config** — just run it.
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### From source
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/razvandimescu/numa.git
|
# Install
|
||||||
cd numa
|
curl -fsSL https://raw.githubusercontent.com/razvandimescu/numa/main/install.sh | sh
|
||||||
cargo build
|
|
||||||
sudo cargo run # binds to port 53, downloads blocklists on first run
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker
|
# Run (port 53 requires root)
|
||||||
|
sudo numa
|
||||||
|
|
||||||
```bash
|
# Try it
|
||||||
docker build -t numa .
|
|
||||||
docker run -p 53:53/udp -p 5380:5380 numa
|
|
||||||
```
|
|
||||||
|
|
||||||
### Try it
|
|
||||||
|
|
||||||
Open the dashboard: **http://numa.numa** (or `http://localhost:5380`)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dig @127.0.0.1 google.com # ✓ resolves normally
|
dig @127.0.0.1 google.com # ✓ resolves normally
|
||||||
dig @127.0.0.1 ads.google.com # ✗ blocked → 0.0.0.0
|
dig @127.0.0.1 ads.google.com # ✗ blocked → 0.0.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Set Numa as your system DNS (all traffic goes through Numa):
|
Open the dashboard: **http://localhost:5380**
|
||||||
```bash
|
|
||||||
sudo cargo run -- install # saves current DNS, sets system to 127.0.0.1
|
|
||||||
sudo cargo run -- uninstall # restores original DNS settings
|
|
||||||
|
|
||||||
# Or if installed to PATH:
|
Or build from source:
|
||||||
sudo cp target/release/numa /usr/local/bin/
|
```bash
|
||||||
sudo numa install
|
git clone https://github.com/razvandimescu/numa.git && cd numa
|
||||||
sudo numa uninstall
|
cargo build --release
|
||||||
|
sudo ./target/release/numa
|
||||||
```
|
```
|
||||||
|
|
||||||
Create an override:
|
## Why Numa
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:5380/overrides \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{"domain":"api.dev","target":"127.0.0.1","ttl":60,"duration_secs":300}'
|
|
||||||
|
|
||||||
dig @127.0.0.1 api.dev # → 127.0.0.1 (auto-reverts in 5 min)
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **Live dashboard** — real-time stats, query log, blocking controls, service management.
|
||||||
|
- **macOS + Linux** — `numa install` configures system DNS, `numa service start` runs as launchd/systemd service.
|
||||||
|
|
||||||
## Local Service Proxy
|
## Local Service Proxy
|
||||||
|
|
||||||
Name your local dev services with `.numa` domains instead of remembering port numbers:
|
Name your local dev services with `.numa` domains:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Register a service via API
|
curl -X POST localhost:5380/services \
|
||||||
curl -X POST http://localhost:5380/services \
|
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"name":"frontend","target_port":5173}'
|
-d '{"name":"frontend","target_port":5173}'
|
||||||
|
|
||||||
# Now access it by name
|
|
||||||
open http://frontend.numa # → proxied to localhost:5173
|
open http://frontend.numa # → proxied to localhost:5173
|
||||||
```
|
```
|
||||||
|
|
||||||
Or configure in `numa.toml`:
|
- **HTTPS with green lock** — auto-generated local CA + per-service TLS certs
|
||||||
|
- **WebSocket** — Vite/webpack HMR works through the proxy
|
||||||
|
- **Health checks** — dashboard shows green/red status per service
|
||||||
|
- **Persistent** — services survive restarts
|
||||||
|
- Or configure in `numa.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[[services]]
|
[[services]]
|
||||||
name = "frontend"
|
name = "frontend"
|
||||||
target_port = 5173
|
target_port = 5173
|
||||||
|
|
||||||
[[services]]
|
|
||||||
name = "api"
|
|
||||||
target_port = 8000
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- `numa.numa` is pre-configured — the dashboard itself, accessible without remembering the port
|
|
||||||
- **HTTPS with green lock** — auto-generated local CA + per-service TLS certs. `sudo numa install` trusts the CA in your system keychain.
|
|
||||||
- WebSocket support — Vite/webpack HMR works through the proxy
|
|
||||||
- Health checks — dashboard shows green/red status for each service
|
|
||||||
- Services persist across restarts (`~/.config/numa/services.json`)
|
|
||||||
- Manage via dashboard UI or REST API
|
|
||||||
|
|
||||||
## Resolution Pipeline
|
|
||||||
|
|
||||||
```
|
|
||||||
Query → Overrides → .numa TLD → Blocklist → Local Zones → Cache → Upstream → Respond
|
|
||||||
```
|
|
||||||
|
|
||||||
1. **Overrides** — ephemeral, time-scoped redirects (highest priority)
|
|
||||||
2. **`.numa` TLD** — synthetic domains for local services → returns `127.0.0.1`
|
|
||||||
3. **Blocklist** — 385K+ ad/tracker domains → returns `0.0.0.0` / `::`
|
|
||||||
4. **Local zones** — records defined in `[[zones]]` config
|
|
||||||
5. **Cache** — TTL-adjusted cached upstream responses (sub-ms)
|
|
||||||
6. **Forward** — query upstream resolver, cache the result
|
|
||||||
7. **SERVFAIL** — returned on upstream failure
|
|
||||||
|
|
||||||
## Dashboard
|
|
||||||
|
|
||||||
Live at `http://localhost:5380` when Numa is running:
|
|
||||||
|
|
||||||
- Total queries, cache hit rate, blocked count, uptime
|
|
||||||
- Resolution path breakdown (forward / cached / local / override / blocked)
|
|
||||||
- Scrolling query log with colored path tags
|
|
||||||
- Active overrides with create/edit/delete
|
|
||||||
- Local services with health status and add/remove
|
|
||||||
- Blocking controls: toggle on/off, pause 5 minutes, one-click allowlist
|
|
||||||
- Cached domains list
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
`numa.toml` (all sections optional, sensible defaults if missing):
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[server]
|
|
||||||
bind_addr = "0.0.0.0:53"
|
|
||||||
api_port = 5380
|
|
||||||
|
|
||||||
[upstream]
|
|
||||||
address = "8.8.8.8"
|
|
||||||
port = 53
|
|
||||||
timeout_ms = 3000
|
|
||||||
|
|
||||||
[cache]
|
|
||||||
max_entries = 10000
|
|
||||||
min_ttl = 60
|
|
||||||
max_ttl = 86400
|
|
||||||
|
|
||||||
[blocking]
|
|
||||||
enabled = true
|
|
||||||
lists = [
|
|
||||||
"https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/hosts/pro.txt",
|
|
||||||
]
|
|
||||||
refresh_hours = 24
|
|
||||||
allowlist = []
|
|
||||||
|
|
||||||
[proxy]
|
|
||||||
enabled = true
|
|
||||||
port = 80
|
|
||||||
tld = "numa"
|
|
||||||
|
|
||||||
[[services]]
|
|
||||||
name = "frontend"
|
|
||||||
target_port = 5173
|
|
||||||
|
|
||||||
[[zones]]
|
|
||||||
domain = "mysite.local"
|
|
||||||
record_type = "A"
|
|
||||||
value = "127.0.0.1"
|
|
||||||
ttl = 60
|
|
||||||
```
|
|
||||||
|
|
||||||
## HTTP API
|
|
||||||
|
|
||||||
REST API on port 5380 (22 endpoints):
|
|
||||||
|
|
||||||
| Endpoint | Method | Description |
|
|
||||||
|----------|--------|-------------|
|
|
||||||
| `/` | GET | Live dashboard |
|
|
||||||
| `/overrides` | POST | Create override(s) |
|
|
||||||
| `/overrides` | GET | List active overrides |
|
|
||||||
| `/overrides` | DELETE | Clear all overrides |
|
|
||||||
| `/overrides/environment` | POST | Batch load overrides |
|
|
||||||
| `/overrides/{domain}` | GET | Get specific override |
|
|
||||||
| `/overrides/{domain}` | DELETE | Remove specific override |
|
|
||||||
| `/services` | GET | List local services (with health status) |
|
|
||||||
| `/services` | POST | Register a local service |
|
|
||||||
| `/services/{name}` | DELETE | Remove a local service |
|
|
||||||
| `/blocking/stats` | GET | Blocklist stats (domains loaded, sources, enabled) |
|
|
||||||
| `/blocking/toggle` | PUT | Enable/disable blocking |
|
|
||||||
| `/blocking/pause` | POST | Pause blocking for N minutes |
|
|
||||||
| `/blocking/allowlist` | GET | List allowlisted domains |
|
|
||||||
| `/blocking/allowlist` | POST | Add domain to allowlist |
|
|
||||||
| `/blocking/allowlist/{domain}` | DELETE | Remove from allowlist |
|
|
||||||
| `/blocking/check/{domain}` | GET | Check if domain is blocked |
|
|
||||||
| `/diagnose/{domain}` | GET | Trace resolution path |
|
|
||||||
| `/query-log` | GET | Recent queries (filterable) |
|
|
||||||
| `/stats` | GET | Server statistics |
|
|
||||||
| `/cache` | GET | List cached entries |
|
|
||||||
| `/cache` | DELETE | Flush cache |
|
|
||||||
| `/cache/{domain}` | DELETE | Flush specific domain |
|
|
||||||
| `/health` | GET | Health check |
|
|
||||||
|
|
||||||
## How It Compares
|
## How It Compares
|
||||||
|
|
||||||
| | Pi-hole | NextDNS | Cloudflare | Numa |
|
| | Pi-hole | AdGuard Home | NextDNS | Cloudflare | Numa |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| Ad blocking | Yes | Yes | Limited | 385K+ domains |
|
| Ad blocking | Yes | Yes | Yes | Limited | 385K+ domains |
|
||||||
| Portable | No (Raspberry Pi) | 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 | REST API + auto-expiry |
|
| Developer overrides | No | No | No | No | REST API + auto-expiry |
|
||||||
| Local service proxy | No | No | No | `.numa` domains + HTTPS + WebSocket |
|
| Local service proxy | No | No | No | No | `.numa` + HTTPS + WS |
|
||||||
| Data stays local | Yes | Cloud | Cloud | 100% local |
|
| Data stays local | Yes | Yes | Cloud | Cloud | 100% local |
|
||||||
| Zero config | Complex 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 | pkarr/DHT roadmap |
|
| Self-sovereign DNS | No | No | No | No | pkarr/DHT roadmap |
|
||||||
|
|
||||||
## Use Cases
|
## How It Works
|
||||||
|
|
||||||
**Block ads everywhere** — Run Numa on your laptop. Your ad blocker works on any network.
|
```
|
||||||
|
Query → Overrides → .numa TLD → Blocklist → Local Zones → Cache → Upstream
|
||||||
|
```
|
||||||
|
|
||||||
**Name your local services** — `frontend.numa` instead of `localhost:5173`. CORS-friendly, HMR-compatible.
|
No DNS libraries. The wire protocol — headers, labels, compression pointers, record types — is parsed and serialized by hand. Runs on `tokio` + `axum`, async per-query task spawning.
|
||||||
|
|
||||||
**Mock external services** — `Point api.stripe.com to localhost:8080 for 30 minutes`
|
[Full API reference (22 endpoints)](docs/development-plan.md) · [Configuration reference](numa.toml)
|
||||||
|
|
||||||
**Provision dev environments** — Create overrides for `db.dev`, `api.dev`, `cache.dev`
|
|
||||||
|
|
||||||
**Debug DNS** — `/diagnose/example.com` traces the full resolution path
|
|
||||||
|
|
||||||
## Built From Scratch
|
|
||||||
|
|
||||||
Zero external DNS libraries. RFC 1035 wire protocol parsed by hand. Dependencies: `tokio`, `axum`, `serde`, `toml`, `reqwest` (for blocklist downloads).
|
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [x] DNS proxy core — forwarding, caching, local zones
|
- [x] DNS proxy core — forwarding, caching, local zones
|
||||||
- [x] Developer overrides — REST API with auto-expiry
|
- [x] Developer overrides — REST API with auto-expiry
|
||||||
- [x] Ad blocking — 385K+ domains, dashboard, allowlist
|
- [x] Ad blocking — 385K+ domains, live dashboard, allowlist
|
||||||
- [x] System DNS auto-discovery — Tailscale, VPN split-DNS
|
- [x] System integration — macOS + Linux, launchd/systemd, Tailscale/VPN auto-discovery
|
||||||
- [x] System DNS auto-configuration — `numa install` / `numa uninstall`
|
- [x] Local service proxy — `.numa` domains, HTTP/HTTPS proxy, auto TLS, WebSocket
|
||||||
- [x] Local service proxy — `.numa` domains with HTTP/HTTPS reverse proxy, auto TLS, WebSocket
|
- [ ] pkarr integration — self-sovereign DNS via Mainline DHT (15M nodes)
|
||||||
- [ ] pkarr integration — resolve Ed25519 keys 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
|
||||||
- [ ] Audit protocol — challenge-based verification of resolver honesty
|
|
||||||
- [ ] Numa Network — proof-of-service consensus, NUMA token, paid `.numa` domains
|
|
||||||
- [ ] `.onion` bridge — human-readable `.numa` names for Tor hidden services
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
13
src/ctx.rs
13
src/ctx.rs
@@ -150,8 +150,17 @@ pub async fn handle_query(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut resp_buffer = BytePacketBuffer::new();
|
let mut resp_buffer = BytePacketBuffer::new();
|
||||||
response.write(&mut resp_buffer)?;
|
if response.write(&mut resp_buffer).is_err() {
|
||||||
ctx.socket.send_to(resp_buffer.filled(), src_addr).await?;
|
// Response too large for UDP — set TC bit and send header + question only
|
||||||
|
debug!("response too large, setting TC bit for {}", qname);
|
||||||
|
let mut tc_response = DnsPacket::response_from(&query, response.header.rescode);
|
||||||
|
tc_response.header.truncated_message = true;
|
||||||
|
let mut tc_buffer = BytePacketBuffer::new();
|
||||||
|
tc_response.write(&mut tc_buffer)?;
|
||||||
|
ctx.socket.send_to(tc_buffer.filled(), src_addr).await?;
|
||||||
|
} else {
|
||||||
|
ctx.socket.send_to(resp_buffer.filled(), src_addr).await?;
|
||||||
|
}
|
||||||
|
|
||||||
// Record stats and query log
|
// Record stats and query log
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,7 +21,15 @@ pub async fn forward_query(
|
|||||||
socket.send_to(send_buffer.filled(), upstream).await?;
|
socket.send_to(send_buffer.filled(), upstream).await?;
|
||||||
|
|
||||||
let mut recv_buffer = BytePacketBuffer::new();
|
let mut recv_buffer = BytePacketBuffer::new();
|
||||||
timeout(timeout_duration, socket.recv_from(&mut recv_buffer.buf)).await??;
|
let (size, _) = timeout(timeout_duration, socket.recv_from(&mut recv_buffer.buf)).await??;
|
||||||
|
|
||||||
|
if size == recv_buffer.buf.len() {
|
||||||
|
log::debug!(
|
||||||
|
"upstream response truncated ({} bytes, buffer {})",
|
||||||
|
size,
|
||||||
|
recv_buffer.buf.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
DnsPacket::from_buffer(&mut recv_buffer)
|
DnsPacket::from_buffer(&mut recv_buffer)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user