Adds a persistent read-only HTTP listener (default port 8765, LAN-bound)
serving a dedicated subset of Numa's API for iOS/Android companion apps
and as a replacement for the one-shot server setup_phone used to spin up:
GET /health — enriched JSON with version, hostname, LAN IP,
SNI, DoT config, mobile API port, CA
fingerprint, features (shared handler with
the main API on port 5380)
GET /ca.pem — public CA certificate (shared handler)
GET /mobileconfig — full iOS profile (CA trust + DNS settings
pinned to current LAN IP)
GET /ca.mobileconfig — CA-only iOS profile (trust anchor without
DNS override — for the iOS companion app's
programmatic DNS flow via NEDNSSettingsManager)
All routes are idempotent GETs. The mobile API never serves the
state-mutating routes that live on the main API (overrides, blocking
toggle, service CRUD, cache flush), so it is safe to expose on the LAN
regardless of the main API's bind address. The CA private key is never
served by any route.
Opt-in via `[mobile] enabled = true`. Default is false so new installs
do not silently expose a LAN listener after upgrading; our committed
numa.toml template enables it explicitly for spike testing.
New modules:
- src/mobileconfig.rs — ProfileMode::{Full, CaOnly} enum with plist
builder lifted from setup_phone.rs. Full and CaOnly share the CA
payload UUID (same trust anchor) but have distinct top-level UUIDs
so they coexist as separate installable profiles on iOS.
- src/health.rs — HealthMeta cached metadata built once at startup
from config + CA fingerprint (SHA-256 of the PEM via ring), and the
HealthResponse JSON shape shared between the main and mobile APIs.
- src/mobile_api.rs — axum Router for the persistent listener. Reuses
api::health and api::serve_ca from the main API; owns the two
mobileconfig handlers.
Modified:
- src/api.rs — health() returns the enriched HealthResponse, now pub.
serve_ca is now pub so mobile_api can reuse it.
- src/config.rs — MobileConfig section (enabled, port, bind_addr).
- src/ctx.rs — health_meta: HealthMeta field on ServerCtx.
- src/main.rs — builds HealthMeta at startup, spawns mobile API
listener if enabled.
- src/lan.rs — build_announcement takes &HealthMeta and writes
enriched TXT records (version, api_port, proto, dot_port, ca_fp).
SRV port now reports the mobile API port; peer discovery still
reads TXT `services=` so this is backwards compatible. Always
announces even when no .numa services are registered, so the iOS
companion app can discover Numa via mDNS regardless of service
state.
- src/setup_phone.rs — reduced from 267 to 100 lines. The CLI is now
a thin QR wrapper over the persistent /mobileconfig endpoint; the
hand-rolled one-shot HTTP server (accept_loop, RUST_OK_HEADERS,
RUST_NOT_FOUND, download counter) is gone.
- src/dot.rs — test fixture updated with HealthMeta::test_fixture().
- numa.toml — commented [mobile] section, enabled = true for spike.
Tests: 136 unit tests passing (5 new in mobileconfig, 3 new in health).
cargo clippy clean. Integration sanity check: curl'd /health, /ca.pem,
/mobileconfig, /ca.mobileconfig against a running numa — all return
200 with correct content types and valid response bodies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
124 lines
5.1 KiB
TOML
124 lines
5.1 KiB
TOML
[server]
|
|
bind_addr = "0.0.0.0:53"
|
|
api_port = 5380
|
|
# api_bind_addr = "127.0.0.1" # default; set to "0.0.0.0" for LAN dashboard access
|
|
# data_dir = "/var/lib/numa" # where numa stores TLS CA and cert material
|
|
# Defaults: /var/lib/numa on linux (FHS),
|
|
# /usr/local/var/numa on macos (homebrew prefix),
|
|
# %PROGRAMDATA%\numa on windows. Override for
|
|
# containerized deploys or tests that can't
|
|
# write to the system path.
|
|
|
|
# [upstream]
|
|
# mode = "forward" # "forward" (default) — relay to upstream
|
|
# # "recursive" — resolve from root hints (no address needed)
|
|
# address = "https://dns.quad9.net/dns-query" # DNS-over-HTTPS (encrypted)
|
|
# address = "https://cloudflare-dns.com/dns-query" # Cloudflare DoH
|
|
# address = "9.9.9.9" # plain UDP
|
|
# port = 53 # only for forward mode, plain UDP
|
|
# timeout_ms = 3000
|
|
# root_hints = [ # only used in recursive mode
|
|
# "198.41.0.4", # a.root-servers.net (Verisign)
|
|
# "199.9.14.201", # b.root-servers.net (USC-ISI)
|
|
# "192.33.4.12", # c.root-servers.net (Cogent)
|
|
# "199.7.91.13", # d.root-servers.net (UMD)
|
|
# "192.203.230.10", # e.root-servers.net (NASA)
|
|
# "192.5.5.241", # f.root-servers.net (ISC)
|
|
# "192.112.36.4", # g.root-servers.net (US DoD)
|
|
# "198.97.190.53", # h.root-servers.net (US Army)
|
|
# "192.36.148.17", # i.root-servers.net (Netnod)
|
|
# "192.58.128.30", # j.root-servers.net (Verisign)
|
|
# "193.0.14.129", # k.root-servers.net (RIPE NCC)
|
|
# "199.7.83.42", # l.root-servers.net (ICANN)
|
|
# "202.12.27.33", # m.root-servers.net (WIDE)
|
|
# ]
|
|
# prime_tlds = [ # TLDs to pre-warm on startup (recursive mode)
|
|
# "com", "net", "org", "info", # gTLDs
|
|
# "io", "dev", "app", "xyz", "me",
|
|
# "eu", "uk", "de", "fr", "nl", # EU + European ccTLDs
|
|
# "it", "es", "pl", "se", "no",
|
|
# "dk", "fi", "at", "be", "ie",
|
|
# "pt", "cz", "ro", "gr", "hu",
|
|
# "bg", "hr", "sk", "si", "lt",
|
|
# "lv", "ee", "ch", "is",
|
|
# "co", "br", "au", "ca", "jp", # other major ccTLDs
|
|
# ]
|
|
|
|
# [blocking]
|
|
# enabled = true # set to false to disable ad blocking
|
|
# refresh_hours = 24
|
|
# lists = ["https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/hosts/pro.txt"]
|
|
# allowlist = ["example.com"] # domains to never block
|
|
|
|
[cache]
|
|
max_entries = 10000
|
|
min_ttl = 60
|
|
max_ttl = 86400
|
|
|
|
[proxy]
|
|
enabled = true
|
|
port = 80
|
|
tls_port = 443
|
|
tld = "numa"
|
|
# bind_addr = "127.0.0.1" # default; set to "0.0.0.0" for LAN access to .numa services
|
|
|
|
# Pre-configured services (numa.numa is always added automatically)
|
|
# [[services]]
|
|
# name = "frontend"
|
|
# target_port = 5173
|
|
#
|
|
# [[services]]
|
|
# name = "api"
|
|
# target_port = 8000
|
|
|
|
# Example zone records:
|
|
# [[zones]]
|
|
# domain = "dimescu.ro"
|
|
# record_type = "A"
|
|
# value = "3.120.139.105"
|
|
# ttl = 30
|
|
|
|
# [[zones]]
|
|
# domain = "test.local"
|
|
# record_type = "A"
|
|
# value = "127.0.0.1"
|
|
# ttl = 60
|
|
|
|
# DNSSEC signature validation (requires mode = "recursive")
|
|
# [dnssec]
|
|
# enabled = false # opt-in: verify chain of trust from root KSK
|
|
# strict = false # true = SERVFAIL on bogus signatures
|
|
|
|
# DNS-over-TLS listener (RFC 7858) — encrypted DNS on port 853
|
|
# [dot]
|
|
# enabled = false # opt-in: accept DoT queries
|
|
# port = 853 # standard DoT port
|
|
# bind_addr = "0.0.0.0" # IPv4 or IPv6; unspecified binds all interfaces
|
|
# cert_path = "/etc/numa/dot.crt" # PEM cert; omit to use self-signed (proxy CA if available)
|
|
# key_path = "/etc/numa/dot.key" # PEM private key; must be set together with cert_path
|
|
|
|
# LAN service discovery via mDNS (disabled by default — no network traffic unless enabled)
|
|
# [lan]
|
|
# enabled = true # discover other Numa instances via mDNS (_numa._tcp.local)
|
|
# broadcast_interval_secs = 30
|
|
# peer_timeout_secs = 90
|
|
|
|
# Mobile API — persistent HTTP listener serving read-only routes
|
|
# (/health, /ca.pem, /mobileconfig, /ca.mobileconfig) on a LAN-reachable
|
|
# port. Consumed by the iOS/Android companion apps for discovery and
|
|
# profile fetching, and by `numa setup-phone` for QR-based onboarding.
|
|
#
|
|
# Opt-in because the listener binds to the LAN by default. None of the
|
|
# exposed routes are cryptographically sensitive (no private keys, no
|
|
# state mutations, all idempotent GETs), but enabling it does add a new
|
|
# listener to any device on the LAN that scans port 8765.
|
|
#
|
|
# Safe for home LANs. Think twice before enabling on untrusted LANs
|
|
# (office Wi-Fi, coffee shops, etc.) — an attacker on the same network
|
|
# could run a competing Numa instance that shadows yours via mDNS and
|
|
# trick companion apps into installing their profile instead of yours.
|
|
[mobile]
|
|
enabled = true # opt-in to the mobile API listener
|
|
# port = 8765 # default; matches Discovery.swift defaultAPIPort
|
|
# bind_addr = "0.0.0.0" # default; set to "127.0.0.1" for localhost-only
|