docs: lift user-facing guides to recipes/, drop dangling docs/ refs #145
@@ -51,8 +51,8 @@ api_port = 5380
|
|||||||
# relay_ip = "178.104.229.30" # optional: pin IPs so numa doesn't leak the
|
# relay_ip = "178.104.229.30" # optional: pin IPs so numa doesn't leak the
|
||||||
# target_ip = "104.16.249.249" # relay/target hostnames via the bootstrap
|
# target_ip = "104.16.249.249" # relay/target hostnames via the bootstrap
|
||||||
# # resolver on cold boot when numa is its
|
# # resolver on cold boot when numa is its
|
||||||
# # own system DNS. See docs/implementation/
|
# # own system DNS. See
|
||||||
# # bootstrap-resolver.md.
|
# # recipes/odoh-upstream.md.
|
||||||
# root_hints = [ # only used in recursive mode
|
# root_hints = [ # only used in recursive mode
|
||||||
# "198.41.0.4", # a.root-servers.net (Verisign)
|
# "198.41.0.4", # a.root-servers.net (Verisign)
|
||||||
# "199.9.14.201", # b.root-servers.net (USC-ISI)
|
# "199.9.14.201", # b.root-servers.net (USC-ISI)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
Single-container deploy that runs Numa as an ODoH (RFC 9230) client: every
|
Single-container deploy that runs Numa as an ODoH (RFC 9230) client: every
|
||||||
DNS query routes through an independent relay + target so neither operator
|
DNS query routes through an independent relay + target so neither operator
|
||||||
sees both your IP and your question. See the [ODoH integration doc][odoh]
|
sees both your IP and your question. See the [ODoH upstream recipe][odoh]
|
||||||
for the full protocol and privacy trade-offs.
|
for the protocol details and the bootstrap-pinning trade-offs.
|
||||||
|
|
||||||
[odoh]: ../../docs/implementation/odoh-integration.md
|
[odoh]: ../../recipes/odoh-upstream.md
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Numa — ODoH client mode (docker-compose starter).
|
# Numa — ODoH client mode (docker-compose starter).
|
||||||
# Sends every DNS query through an independent relay + target pair so
|
# Sends every DNS query through an independent relay + target pair so
|
||||||
# neither operator sees both your IP and your question. See
|
# neither operator sees both your IP and your question. See
|
||||||
# docs/implementation/odoh-integration.md for the protocol details and
|
# recipes/odoh-upstream.md for the protocol details and
|
||||||
# packaging/client/README.md for deploy notes.
|
# packaging/client/README.md for deploy notes.
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
|
|||||||
11
recipes/README.md
Normal file
11
recipes/README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Recipes
|
||||||
|
|
||||||
|
Scenario-driven configs for common Numa deployments. Each recipe is self-contained: copy the snippet, adjust the marked fields, reload.
|
||||||
|
|
||||||
|
## Transport / encryption
|
||||||
|
|
||||||
|
- [DoH on the LAN](doh-on-lan.md) — expose Numa's built-in DNS-over-HTTPS to local clients.
|
||||||
|
- [dnsdist in front of Numa](dnsdist-front.md) — terminate public TLS externally, keep Numa on loopback.
|
||||||
|
- [ODoH upstream with bootstrap pinning](odoh-upstream.md) — oblivious DNS client mode without leaking the relay/target hostnames.
|
||||||
|
|
||||||
|
Missing a scenario? Open an issue or PR — these are plain Markdown with no build step.
|
||||||
64
recipes/dnsdist-front.md
Normal file
64
recipes/dnsdist-front.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# dnsdist in front of Numa
|
||||||
|
|
||||||
|
For public DoH with a real (ACME-signed) cert, terminate TLS outside Numa and forward plain DNS (or loopback-only DoH) to the resolver. Cert renewal, rate-limiting, and load-balancing live in the front-end; Numa stays focused on resolution.
|
||||||
|
|
||||||
|
## When to use this
|
||||||
|
|
||||||
|
- Public hostname (`dns.example.com`) with a Let's Encrypt or internal PKI cert.
|
||||||
|
- You want a dedicated front-end for DoH/DoT/DoQ while Numa stays loopback-bound.
|
||||||
|
- You plan to run multiple Numa instances behind one endpoint.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
public 443/DoH ┐
|
||||||
|
public 853/DoT ├─► dnsdist ─► 127.0.0.1:53 (Numa UDP/TCP)
|
||||||
|
public 443/DoQ ┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## dnsdist config
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- /etc/dnsdist/dnsdist.conf
|
||||||
|
|
||||||
|
newServer({address="127.0.0.1:53", name="numa", checkType="A", checkName="numa.rs."})
|
||||||
|
|
||||||
|
addDOHLocal(
|
||||||
|
"0.0.0.0:443",
|
||||||
|
"/etc/letsencrypt/live/dns.example.com/fullchain.pem",
|
||||||
|
"/etc/letsencrypt/live/dns.example.com/privkey.pem",
|
||||||
|
"/dns-query",
|
||||||
|
{doTCP=true, reusePort=true}
|
||||||
|
)
|
||||||
|
|
||||||
|
addTLSLocal(
|
||||||
|
"0.0.0.0:853",
|
||||||
|
"/etc/letsencrypt/live/dns.example.com/fullchain.pem",
|
||||||
|
"/etc/letsencrypt/live/dns.example.com/privkey.pem"
|
||||||
|
)
|
||||||
|
|
||||||
|
addAction(AllRule(), PoolAction("", false))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Numa config
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[proxy]
|
||||||
|
enabled = true # keep if you still use *.numa service routing
|
||||||
|
bind_addr = "127.0.0.1" # stays default
|
||||||
|
```
|
||||||
|
|
||||||
|
No changes to `[server]` — Numa keeps serving plain DNS on UDP/TCP 53, which dnsdist forwards.
|
||||||
|
|
||||||
|
## Caveat: client IPs
|
||||||
|
|
||||||
|
Without PROXY protocol support in Numa, the query log shows the front-end's IP on every query, not the real client. dnsdist can emit PROXY v2 (`useProxyProtocol=true` on `newServer`), but Numa doesn't yet parse it — tracked in the wish-list under #143. Until then, accept the blind spot or correlate against dnsdist's own logs.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kdig +https @dns.example.com example.com
|
||||||
|
kdig +tls @dns.example.com example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Both should return clean answers. Numa's `/queries` API should show the request landing, sourced from the front-end IP.
|
||||||
61
recipes/doh-on-lan.md
Normal file
61
recipes/doh-on-lan.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# DoH on the LAN
|
||||||
|
|
||||||
|
Numa ships an RFC 8484 DoH endpoint (`POST /dns-query`) on the `[proxy]` HTTPS listener. By default it binds `127.0.0.1:443` with a self-signed cert — invisible to anything off the box. Three changes make it reachable from the LAN.
|
||||||
|
|
||||||
|
## When to use this
|
||||||
|
|
||||||
|
- Your phone/laptop is on the same network as Numa and you want encrypted DNS without a cloud resolver.
|
||||||
|
- You're OK installing Numa's self-signed CA on every client (one-time, via `/ca.pem` + the mobileconfig flow).
|
||||||
|
|
||||||
|
For a publicly-trusted cert, see [dnsdist in front of Numa](dnsdist-front.md) instead.
|
||||||
|
|
||||||
|
## Minimal config
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[proxy]
|
||||||
|
enabled = true # default
|
||||||
|
bind_addr = "0.0.0.0" # was 127.0.0.1 — expose to LAN
|
||||||
|
tls_port = 443 # default; DoH is served here
|
||||||
|
tld = "numa" # default — self-resolving, see below
|
||||||
|
```
|
||||||
|
|
||||||
|
`tld` is the DoH gate: Numa accepts the DoH request only when the `Host` header is loopback or equals (or is a subdomain of) `tld`. Clients therefore dial `https://numa/dns-query`.
|
||||||
|
|
||||||
|
With the default `tld = "numa"`, there's no DNS bootstrap to configure: Numa already resolves `numa` and `*.numa` to its own LAN IP for remote clients (that's how the `*.numa` service-proxy feature works). Any client that uses Numa as its resolver will resolve `numa` correctly on first try.
|
||||||
|
|
||||||
|
If you'd rather use a hostname that resolves via normal DNS (e.g. you want DoH-only clients that never talk plain DNS to Numa), set `tld = "dns.example.com"` and add a matching A record in whichever DNS your clients consult before reaching Numa.
|
||||||
|
|
||||||
|
## Trust the CA on each client
|
||||||
|
|
||||||
|
Numa generates a self-signed CA at startup. Fetch it once, import it wherever you'll run the DoH client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -o numa-ca.pem http://<numa-ip>:5380/ca.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
- **macOS** — `sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain numa-ca.pem`
|
||||||
|
- **iOS** — install the mobileconfig from the API (same CA, signed profile). Flip *Settings → General → About → Certificate Trust Settings* on after install.
|
||||||
|
- **Linux** — drop into `/usr/local/share/ca-certificates/` and run `sudo update-ca-certificates`.
|
||||||
|
- **Android** — requires the user-installed CA path; browsers may still refuse it for DoH. Consider the [dnsdist front](dnsdist-front.md) route instead.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kdig +https @numa example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Without `+https` kdig uses plain DNS. With `+https` the same answers should flow over port 443.
|
||||||
|
|
||||||
|
Raw check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H 'accept: application/dns-message' \
|
||||||
|
--data-binary @query.bin \
|
||||||
|
https://numa/dns-query
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- Port 443 is privileged on Linux/macOS. Run Numa via the provided service units, or grant `CAP_NET_BIND_SERVICE` (`sudo setcap 'cap_net_bind_service=+ep' /path/to/numa`).
|
||||||
|
- Non-matching `Host` header → HTTP 404 from the proxy's fallback handler. Double-check `tld`.
|
||||||
|
- ChromeOS enrollment rejects user-installed CAs for some flows — known pain point, see issue #136.
|
||||||
59
recipes/odoh-upstream.md
Normal file
59
recipes/odoh-upstream.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# ODoH upstream with bootstrap pinning
|
||||||
|
|
||||||
|
Numa can run as an Oblivious DoH (RFC 9230) client: the relay sees your IP but not the question, the target sees the question but not your IP. Neither party alone can re-identify a query. This recipe covers the minimal config and the bootstrap leak that `relay_ip` / `target_ip` close.
|
||||||
|
|
||||||
|
## When to use this
|
||||||
|
|
||||||
|
- You want split-trust encrypted DNS without a single provider seeing both who you are and what you asked.
|
||||||
|
- Numa is your system resolver (so there's no "other" DNS to ask).
|
||||||
|
|
||||||
|
## Minimal config
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[upstream]
|
||||||
|
mode = "odoh"
|
||||||
|
relay = "https://odoh-relay.numa.rs/relay"
|
||||||
|
target = "https://odoh.cloudflare-dns.com/dns-query"
|
||||||
|
strict = true # refuse to fall back to a non-oblivious path on relay failure
|
||||||
|
```
|
||||||
|
|
||||||
|
`strict = true` means a relay-level HTTPS failure returns SERVFAIL instead of silently downgrading. Set it to `false` and configure `[upstream].fallback` if you'd rather keep resolving (at the cost of the oblivious property).
|
||||||
|
|
||||||
|
## The bootstrap leak
|
||||||
|
|
||||||
|
When Numa is the system resolver and needs to reach the relay/target, *something* has to translate `odoh-relay.numa.rs` → IP. If Numa asks itself, you deadlock. If Numa asks a bootstrap resolver (1.1.1.1, 9.9.9.9), that resolver learns which ODoH endpoint you use in cleartext — it can't see your questions, but it sees the destination. That's the leak ODoH was supposed to close.
|
||||||
|
|
||||||
|
`relay_ip` and `target_ip` tell Numa the IPs directly, so it never asks anyone:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[upstream]
|
||||||
|
mode = "odoh"
|
||||||
|
relay = "https://odoh-relay.numa.rs/relay"
|
||||||
|
target = "https://odoh.cloudflare-dns.com/dns-query"
|
||||||
|
relay_ip = "178.104.229.30" # pin the relay — no hostname lookup
|
||||||
|
target_ip = "104.16.249.249" # pin the target — no hostname lookup
|
||||||
|
```
|
||||||
|
|
||||||
|
Numa still validates TLS against the hostnames in `relay` / `target`, so a hijacked IP can't masquerade — pinning skips only the DNS step.
|
||||||
|
|
||||||
|
## Finding current IPs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dig +short odoh-relay.numa.rs
|
||||||
|
dig +short odoh.cloudflare-dns.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-pin when an operator rotates. The community-maintained list at <https://github.com/DNSCrypt/dnscrypt-resolvers/blob/master/v3/odoh-relays.md> is a useful cross-reference.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kdig @127.0.0.1 example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Numa's `/queries` API and startup banner should label the upstream as `odoh://`. Look for `ODoH relay returned ...` errors in the logs if routing fails.
|
||||||
|
|
||||||
|
## Known gotchas
|
||||||
|
|
||||||
|
- **Same-operator refused.** Numa's eTLD+1 check blocks configs where the relay and target belong to the same operator (pointless — same party sees both sides). Override only when testing.
|
||||||
|
- **Single relay.** Current config accepts one relay and one target. Multi-entry rotation/failover is tracked in #140.
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
//! relay/target, blocklist CDN). When numa is its own system resolver
|
//! relay/target, blocklist CDN). When numa is its own system resolver
|
||||||
//! (`/etc/resolv.conf → 127.0.0.1`, HAOS add-on, Pi-hole-style container),
|
//! (`/etc/resolv.conf → 127.0.0.1`, HAOS add-on, Pi-hole-style container),
|
||||||
//! the default `getaddrinfo` path loops back through numa before numa can
|
//! the default `getaddrinfo` path loops back through numa before numa can
|
||||||
//! answer — a chicken-and-egg that deadlocks cold boot. See issue #122 and
|
//! answer — a chicken-and-egg that deadlocks cold boot. See issue #122.
|
||||||
//! `docs/implementation/bootstrap-resolver.md`.
|
|
||||||
//!
|
//!
|
||||||
//! Resolution order per hostname:
|
//! Resolution order per hostname:
|
||||||
//! 1. Per-hostname overrides (e.g. ODoH `relay_ip` / `target_ip`) → return
|
//! 1. Per-hostname overrides (e.g. ODoH `relay_ip` / `target_ip`) → return
|
||||||
|
|||||||
@@ -175,8 +175,7 @@ pub fn parse_upstream(
|
|||||||
///
|
///
|
||||||
/// Uses the system resolver. Callers running inside `serve::run` pass the
|
/// Uses the system resolver. Callers running inside `serve::run` pass the
|
||||||
/// shared [`crate::bootstrap_resolver::NumaResolver`] via
|
/// shared [`crate::bootstrap_resolver::NumaResolver`] via
|
||||||
/// [`build_https_client_with_resolver`] to avoid the self-loop documented
|
/// [`build_https_client_with_resolver`] to avoid the self-loop (issue #122).
|
||||||
/// in `docs/implementation/bootstrap-resolver.md`.
|
|
||||||
pub fn build_https_client() -> reqwest::Client {
|
pub fn build_https_client() -> reqwest::Client {
|
||||||
build_https_client_with_resolver(1, None)
|
build_https_client_with_resolver(1, None)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,10 @@
|
|||||||
//! Both handlers call [`HealthResponse::build`] to assemble the JSON
|
//! Both handlers call [`HealthResponse::build`] to assemble the JSON
|
||||||
//! response from `HealthMeta` + live inputs.
|
//! response from `HealthMeta` + live inputs.
|
||||||
//!
|
//!
|
||||||
//! JSON schema is documented in `docs/implementation/ios-companion-app.md`
|
//! The iOS companion app's `HealthInfo` struct is the canonical consumer;
|
||||||
//! §4.2. The iOS companion app's `HealthInfo` struct is the canonical
|
//! any change to this response must keep that struct decoding cleanly (all
|
||||||
//! consumer; any change to this response must keep that struct decoding
|
//! consumed fields are optional on the Swift side, but `lan_ip` is
|
||||||
//! cleanly (all consumed fields are optional on the Swift side, but
|
//! load-bearing for the pipeline).
|
||||||
//! `lan_ip` is load-bearing for the pipeline).
|
|
||||||
|
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ pub async fn run(config_path: String) -> crate::Result<()> {
|
|||||||
// Routes numa-originated HTTPS (DoH upstream, ODoH relay/target, blocklist
|
// Routes numa-originated HTTPS (DoH upstream, ODoH relay/target, blocklist
|
||||||
// CDN) away from the system resolver so lookups don't loop back through
|
// CDN) away from the system resolver so lookups don't loop back through
|
||||||
// numa when it's its own system DNS.
|
// numa when it's its own system DNS.
|
||||||
// See `docs/implementation/bootstrap-resolver.md`.
|
|
||||||
let resolver_overrides = match config.upstream.mode {
|
let resolver_overrides = match config.upstream.mode {
|
||||||
crate::config::UpstreamMode::Odoh => config
|
crate::config::UpstreamMode::Odoh => config
|
||||||
.upstream
|
.upstream
|
||||||
|
|||||||
Reference in New Issue
Block a user