Files
numa/site/index.html
Razvan Dimescu 8791198d10 feat: forward-by-default, auto recursive mode, Linux install fixes (#27)
* feat: auto recursive mode, fix Linux install

Auto mode (new default): probes a root server on startup; uses
recursive resolution if outbound DNS works, falls back to Quad9 DoH
if blocked. Dashboard shows mode indicator (green/yellow).

Linux install fixes:
- Add DNSStubListener=no to resolved drop-in (frees port 53)
- Configure DNS before starting service (correct ordering)
- Skip 127.0.0.53 in upstream detection
- `numa install` now does everything (service + DNS + CA)
- `numa uninstall` mirrors install (stop service + restore DNS)
- Extract is_loopback_or_stub() for consistent filtering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: enable DNSSEC validation by default

With recursive as the default mode, DNSSEC validation completes the
trustless resolution chain. Strict mode remains off by default.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: forward search domains to VPC resolver on Linux

Parse search/domain lines from resolv.conf and create conditional
forwarding rules to the original nameserver or AWS VPC resolver
(169.254.169.253). Fixes internal hostname resolution on cloud VMs
where recursive mode can't resolve private DNS zones.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: single-pass resolv.conf parsing, eliminate redundancies

Parse resolv.conf once for both upstream and search domains instead
of 2-3 reads. Extract CLOUD_VPC_RESOLVER constant. Use &'static str
for mode in StatsResponse. Remove dead read_upstream_from_file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: macOS install health check, harden recursive probe

Verify numa is listening (API port) before redirecting system DNS on
macOS — if the service fails to start (e.g. port 53 in use), unload
the service and abort instead of breaking DNS. Probe up to 3 root
hints before declaring recursive mode unavailable. Validate IPs from
resolvectl to avoid IPv6 fragment extraction. Extract DEFAULT_API_PORT
constant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: widen make_rule cfg gate to include Linux

make_rule was gated to macOS-only but discover_linux() calls it for
search domain forwarding rules. CI failed on Linux with E0425.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: forward mode as default, recursive opt-in

Forward mode (transparent proxy to system DNS) is now the default.
Recursive and auto modes are explicit opt-in via config. This avoids
bypassing corporate DNS policies, captive portals, VPC private zones,
and parental controls on first install.

- Move #[default] from Auto to Forward on UpstreamMode
- DNSSEC defaults to off (no-op in forward mode)
- 3-way match in main: Forward/Recursive/Auto with clean separation
- Post-install message suggests mode = "recursive" for sovereignty
- Update README, site, and launch drafts messaging

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 08:49:16 +03:00

1691 lines
51 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Numa — DNS you own. Everywhere you go.</title>
<meta name="description" content="DNS you own. Portable DNS resolver with caching, ad blocking, .numa local domains, developer overrides. Optional recursive resolution with full DNSSEC validation. Built from scratch in Rust.">
<link rel="canonical" href="https://numa.rs">
<meta property="og:title" content="Numa — DNS you own. Everywhere you go.">
<meta property="og:description" content="Portable DNS resolver with caching, ad blocking, .numa local domains, and developer overrides. Optional recursive resolution with full DNSSEC validation. Built from scratch in Rust.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://numa.rs">
<link rel="stylesheet" href="/fonts/fonts.css">
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
/* Roman Stone */
--bg-deep: #f5f0e8;
--bg-surface: #ece5da;
--bg-elevated: #e3dbce;
--bg-card: #faf7f2;
--amber: #c0623a;
--amber-dim: #9e4e2d;
--amber-glow: rgba(192, 98, 58, 0.07);
--amber-glow-strong: rgba(192, 98, 58, 0.14);
--teal: #6b7c4e;
--teal-dim: #566540;
--teal-glow: rgba(107, 124, 78, 0.06);
--violet: #64748b;
--violet-dim: #4a5568;
--violet-glow: rgba(100, 116, 139, 0.06);
--emerald: #527a52;
--emerald-dim: #3f6340;
--rose: #b5443a;
--rose-dim: #943832;
--rose-glow: rgba(181, 68, 58, 0.05);
--cyan: #4a7c8a;
--text-primary: #2c2418;
--text-secondary: #6b5e4f;
--text-dim: #a39888;
--border: rgba(0, 0, 0, 0.08);
--border-amber: rgba(192, 98, 58, 0.22);
--border-teal: rgba(107, 124, 78, 0.20);
--border-violet: rgba(100, 116, 139, 0.18);
--font-display: 'Instrument Serif', Georgia, serif;
--font-body: 'DM Sans', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
html { scroll-behavior: smooth; }
body {
background: var(--bg-deep);
color: var(--text-primary);
font-family: var(--font-body);
font-weight: 400;
line-height: 1.7;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
}
/* --- Grain overlay --- */
body::before {
content: '';
position: fixed;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.025'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 9999;
}
/* --- Layout --- */
.container {
max-width: 1120px;
margin: 0 auto;
padding: 0 2rem;
}
section {
padding: 7rem 0;
position: relative;
}
/* --- Reveal animations --- */
.reveal {
opacity: 0;
transform: translateY(28px);
transition: opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1), transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
.reveal-delay-1 { transition-delay: 0.1s; }
.reveal-delay-2 { transition-delay: 0.2s; }
.reveal-delay-3 { transition-delay: 0.3s; }
.reveal-delay-4 { transition-delay: 0.4s; }
@media (prefers-reduced-motion: reduce) {
.reveal { opacity: 1; transform: none; transition: none; }
.hero .wordmark, .hero .tagline, .hero .description, .hero-actions { opacity: 1; animation: none; }
}
/* --- Accessibility --- */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
.btn:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
.btn-ghost:focus-visible { outline: 2px solid var(--amber-dim); outline-offset: 2px; }
/* --- Roman road brick pattern --- */
.roman-bricks {
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg width='120' height='60' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='1' y='1' width='56' height='27' rx='1' fill='none' stroke='%23a39888' stroke-width='0.5' opacity='0.18'/%3E%3Crect x='61' y='1' width='56' height='27' rx='1' fill='none' stroke='%23a39888' stroke-width='0.5' opacity='0.18'/%3E%3Crect x='31' y='31' width='56' height='27' rx='1' fill='none' stroke='%23a39888' stroke-width='0.5' opacity='0.18'/%3E%3Crect x='-29' y='31' width='56' height='27' rx='1' fill='none' stroke='%23a39888' stroke-width='0.5' opacity='0.18'/%3E%3Crect x='91' y='31' width='56' height='27' rx='1' fill='none' stroke='%23a39888' stroke-width='0.5' opacity='0.18'/%3E%3C/svg%3E");
background-size: 120px 60px;
pointer-events: none;
}
/* Hero bricks: fade from right/bottom, invisible where text sits */
.hero .roman-bricks {
-webkit-mask-image: linear-gradient(135deg, transparent 25%, rgba(0,0,0,0.25) 55%, rgba(0,0,0,0.5) 100%);
mask-image: linear-gradient(135deg, transparent 25%, rgba(0,0,0,0.25) 55%, rgba(0,0,0,0.5) 100%);
}
/* Section divider road band */
.section-road {
height: 28px;
position: relative;
overflow: hidden;
}
.section-road .roman-bricks {
opacity: 0.5;
}
.section-road::before,
.section-road::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
width: 140px;
z-index: 1;
}
.section-road::before { left: 0; background: linear-gradient(to right, var(--bg-deep), transparent); }
.section-road::after { right: 0; background: linear-gradient(to left, var(--bg-deep), transparent); }
.section-road.on-surface::before { background: linear-gradient(to right, var(--bg-surface), transparent); }
.section-road.on-surface::after { background: linear-gradient(to left, var(--bg-surface), transparent); }
/* --- Section labels --- */
.section-label {
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--amber);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.section-label::before {
content: '';
width: 1.5rem;
height: 1px;
background: var(--amber-dim);
}
h2 {
font-family: var(--font-display);
font-weight: 400;
font-size: clamp(2rem, 4vw, 3rem);
line-height: 1.2;
margin-bottom: 1.5rem;
color: var(--text-primary);
}
h3 {
font-family: var(--font-body);
font-weight: 500;
font-size: 1.15rem;
margin-bottom: 0.75rem;
color: var(--text-primary);
}
p.lead {
font-size: 1.1rem;
color: var(--text-secondary);
max-width: 640px;
line-height: 1.8;
}
/* ===========================
HERO
=========================== */
.hero {
min-height: 100vh;
display: flex;
align-items: center;
position: relative;
padding: 6rem 0;
overflow: hidden;
}
/* Radial glows behind hero */
.hero::before {
content: '';
position: absolute;
top: -20%;
right: -10%;
width: 700px;
height: 700px;
background: radial-gradient(circle, rgba(192, 98, 58, 0.06) 0%, transparent 70%);
pointer-events: none;
}
.hero::after {
content: '';
position: absolute;
bottom: -30%;
left: -15%;
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(107, 124, 78, 0.05) 0%, transparent 70%);
pointer-events: none;
}
.hero-content {
position: relative;
z-index: 1;
}
.hero .wordmark {
font-family: var(--font-display);
font-weight: 400;
font-size: clamp(4.5rem, 12vw, 9rem);
line-height: 0.9;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #2c2418 0%, var(--amber) 55%, var(--teal) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 1rem;
opacity: 0;
animation: wordmark-in 1s cubic-bezier(0.22, 1, 0.36, 1) 0.2s forwards;
}
@keyframes wordmark-in {
from { opacity: 0; transform: translateY(40px); }
to { opacity: 1; transform: translateY(0); }
}
.hero .tagline {
font-family: var(--font-display);
font-style: italic;
font-size: clamp(1.3rem, 3vw, 1.8rem);
color: var(--amber);
margin-bottom: 2rem;
opacity: 0;
animation: fade-up 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.5s forwards;
}
@keyframes fade-up {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.hero .epigraph {
font-family: var(--font-display);
font-style: italic;
font-size: 0.92rem;
color: var(--text-dim);
margin-bottom: 1.5rem;
opacity: 0;
animation: fade-up 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.6s forwards;
}
.hero .description {
font-size: 1.1rem;
color: var(--text-secondary);
max-width: 560px;
line-height: 1.8;
margin-bottom: 3rem;
opacity: 0;
animation: fade-up 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.7s forwards;
}
.hero-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
opacity: 0;
animation: fade-up 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.9s forwards;
}
.btn {
font-family: var(--font-body);
font-weight: 500;
font-size: 0.9rem;
padding: 0.75rem 1.75rem;
border-radius: 2px;
text-decoration: none;
transition: all 0.25s ease;
cursor: pointer;
border: none;
}
.btn-primary {
background: var(--amber);
color: #faf7f2;
}
.btn-primary:hover {
background: #a85230;
box-shadow: 0 4px 20px rgba(192, 98, 58, 0.25);
}
.btn-ghost {
background: transparent;
color: var(--text-secondary);
border: 1px solid var(--border);
}
.btn-ghost:hover {
border-color: var(--amber-dim);
color: var(--amber);
}
/* Side decoration — vertical line */
.hero-line {
position: absolute;
right: 12%;
top: 15%;
bottom: 15%;
width: 1px;
background: linear-gradient(to bottom, transparent, var(--teal), var(--amber), var(--violet), transparent);
opacity: 0;
animation: line-in 1.5s ease 1.2s forwards;
}
@keyframes line-in {
to { opacity: 0.35; }
}
.hero-line .dot {
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
left: -2.5px;
}
.hero-line .dot:nth-child(1) { top: 20%; background: var(--teal); animation: pulse-dot-teal 3s ease-in-out infinite 0s; }
.hero-line .dot:nth-child(2) { top: 50%; background: var(--amber); animation: pulse-dot-amber 3s ease-in-out infinite 1s; }
.hero-line .dot:nth-child(3) { top: 80%; background: var(--violet); animation: pulse-dot-violet 3s ease-in-out infinite 2s; }
@keyframes pulse-dot-teal {
0%, 100% { opacity: 0.5; box-shadow: none; }
50% { opacity: 1; box-shadow: 0 0 10px rgba(107, 124, 78, 0.5); }
}
@keyframes pulse-dot-amber {
0%, 100% { opacity: 0.5; box-shadow: none; }
50% { opacity: 1; box-shadow: 0 0 10px rgba(192, 98, 58, 0.5); }
}
@keyframes pulse-dot-violet {
0%, 100% { opacity: 0.5; box-shadow: none; }
50% { opacity: 1; box-shadow: 0 0 10px rgba(100, 116, 139, 0.5); }
}
/* ===========================
THE PROBLEM
=========================== */
.problem {
background: var(--bg-surface);
position: relative;
}
.problem::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 50%;
height: 100%;
background: radial-gradient(ellipse at 80% 30%, var(--rose-glow), transparent 70%);
pointer-events: none;
}
.problem-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
align-items: center;
margin-top: 3rem;
}
.problem-text p {
color: var(--text-secondary);
margin-bottom: 1rem;
font-size: 1rem;
}
/* Centralized DNS diagram */
.dns-diagram {
position: relative;
padding: 2rem;
}
.dns-node {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 1rem;
border: 1px solid var(--border);
border-radius: 2px;
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--text-secondary);
background: var(--bg-elevated);
margin-bottom: 0.5rem;
position: relative;
}
.dns-node.central {
border-color: var(--rose);
background: rgba(181, 68, 58, 0.06);
color: var(--rose);
}
.dns-node .node-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.dns-node .node-dot.red { background: var(--rose); }
.dns-node .node-dot.dim { background: var(--text-dim); }
.bottleneck-label {
text-align: center;
font-family: var(--font-mono);
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--rose);
margin: 1.5rem 0 0.75rem;
}
.dns-arrows {
display: flex;
justify-content: center;
gap: 0.5rem;
margin: 0.75rem 0;
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 0.8rem;
}
/* ===========================
HOW IT WORKS — 3 LAYERS
=========================== */
.layers-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
margin-top: 3rem;
}
.layer-card {
background: var(--bg-card);
border: 1px solid var(--border);
padding: 2rem 1.75rem;
position: relative;
overflow: hidden;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
box-shadow: 0 1px 4px rgba(0,0,0,0.04), 0 4px 14px rgba(44,36,24,0.04);
}
.layer-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.06), 0 8px 24px rgba(44,36,24,0.06);
}
.layer-card:nth-child(1):hover { border-color: var(--border-teal); }
.layer-card:nth-child(2):hover { border-color: var(--border-amber); }
.layer-card:nth-child(3):hover { border-color: var(--border-violet); }
.layer-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
}
.layer-card:nth-child(1)::before { background: var(--teal); }
.layer-card:nth-child(2)::before { background: var(--amber); }
.layer-card:nth-child(3)::before { background: var(--violet); }
/* Subtle glow per card */
.layer-card:nth-child(1)::after { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 80px; background: linear-gradient(to bottom, var(--teal-glow), transparent); pointer-events: none; }
.layer-card:nth-child(2)::after { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 80px; background: linear-gradient(to bottom, var(--amber-glow), transparent); pointer-events: none; }
.layer-card:nth-child(3)::after { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 80px; background: linear-gradient(to bottom, var(--violet-glow), transparent); pointer-events: none; }
.layer-badge {
font-family: var(--font-mono);
font-size: 0.65rem;
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 1rem;
display: inline-block;
padding: 0.2rem 0.5rem;
position: relative;
z-index: 1;
}
.layer-card:nth-child(1) .layer-badge { color: var(--teal); border: 1px solid var(--border-teal); }
.layer-card:nth-child(2) .layer-badge { color: var(--amber); border: 1px solid var(--border-amber); }
.layer-card:nth-child(3) .layer-badge { color: var(--violet); border: 1px solid var(--border-violet); }
.layer-card h3 {
font-family: var(--font-display);
font-size: 1.4rem;
font-weight: 400;
margin-bottom: 1.25rem;
}
.layer-card ul {
list-style: none;
padding: 0;
}
.layer-card li {
font-size: 0.88rem;
color: var(--text-secondary);
padding: 0.35rem 0;
padding-left: 1.2rem;
position: relative;
}
.layer-card li::before {
content: '';
position: absolute;
left: 0;
top: 0.75rem;
width: 4px;
height: 4px;
border-radius: 50%;
}
.layer-card:nth-child(1) li::before { background: var(--teal-dim); }
.layer-card:nth-child(2) li::before { background: var(--amber-dim); }
.layer-card:nth-child(3) li::before { background: var(--violet-dim); }
/* ===========================
ARCHITECTURE
=========================== */
.architecture {
background: var(--bg-surface);
}
.arch-subsection {
margin-top: 4rem;
}
.arch-subsection h3 {
font-family: var(--font-display);
font-size: 1.5rem;
font-weight: 400;
margin-bottom: 2rem;
}
/* Pipeline diagram */
.pipeline {
display: flex;
align-items: center;
gap: 0;
overflow-x: auto;
padding: 2rem 0;
}
.pipeline-node {
display: flex;
flex-direction: column;
align-items: center;
min-width: 100px;
flex-shrink: 0;
}
.pipeline-box {
font-family: var(--font-mono);
font-size: 0.72rem;
padding: 0.6rem 0.9rem;
border: 1px solid var(--border);
background: var(--bg-elevated);
color: var(--text-secondary);
white-space: nowrap;
position: relative;
transition: all 0.3s ease;
}
.pipeline-box:hover {
border-color: var(--amber-dim);
color: var(--amber);
}
.pipeline-box.highlight {
border-color: var(--amber-dim);
background: rgba(192, 98, 58, 0.06);
color: var(--amber);
}
.pipeline-box.hl-teal {
border-color: var(--teal-dim);
background: rgba(107, 124, 78, 0.06);
color: var(--teal);
}
.pipeline-box.hl-teal:hover { border-color: var(--teal); color: var(--teal); }
.pipeline-box.hl-violet {
border-color: var(--violet-dim);
background: rgba(100, 116, 139, 0.06);
color: var(--violet);
}
.pipeline-box.hl-violet:hover { border-color: var(--violet); color: var(--violet); }
.pipeline-box.hl-emerald {
border-color: var(--emerald-dim);
background: rgba(82, 122, 82, 0.06);
color: var(--emerald);
}
.pipeline-box.hl-emerald:hover { border-color: var(--emerald); color: var(--emerald); }
.pipeline-box.hl-cyan {
border-color: rgba(74, 124, 138, 0.35);
background: rgba(74, 124, 138, 0.06);
color: var(--cyan);
}
.pipeline-box.hl-cyan:hover { border-color: var(--cyan); color: var(--cyan); }
.pipeline-arrow {
font-family: var(--font-mono);
color: var(--text-dim);
font-size: 0.9rem;
padding: 0 0.4rem;
flex-shrink: 0;
}
/* Layer stack */
.layer-stack {
display: flex;
flex-direction: column;
gap: 0;
max-width: 700px;
margin: 2rem 0;
}
.stack-row {
display: grid;
grid-template-columns: 180px 1fr;
border: 1px solid var(--border);
border-bottom: none;
transition: background 0.3s ease;
}
.stack-row:last-child { border-bottom: 1px solid var(--border); }
.stack-row:hover { background: var(--bg-elevated); }
.stack-label {
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.05em;
color: var(--amber);
padding: 0.9rem 1.2rem;
border-right: 1px solid var(--border);
display: flex;
align-items: center;
}
.stack-value {
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--text-secondary);
padding: 0.9rem 1.2rem;
display: flex;
align-items: center;
}
/* Network diagram */
.network-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin: 2rem 0;
}
.network-actor {
text-align: center;
padding: 1.5rem 1rem;
border: 1px solid var(--border);
background: var(--bg-elevated);
position: relative;
}
.network-actor .actor-icon {
font-size: 1.5rem;
margin-bottom: 0.75rem;
display: block;
}
.network-actor h4 {
font-family: var(--font-mono);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--amber);
margin-bottom: 0.5rem;
}
.network-actor p {
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.5;
}
/* Connecting lines between network actors */
.network-connections {
display: flex;
justify-content: center;
gap: 2rem;
margin: -0.5rem 0 0;
padding: 0 3rem;
}
.network-conn-line {
flex: 1;
height: 1px;
background: var(--border-amber);
position: relative;
top: 0;
}
/* ===========================
COMPARISON TABLE
=========================== */
.comparison-wrapper {
overflow-x: auto;
margin-top: 3rem;
border: 1px solid var(--border);
}
.comparison-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
min-width: 700px;
}
.comparison-table thead th {
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-dim);
padding: 1rem 1.2rem;
text-align: left;
border-bottom: 1px solid var(--border);
background: var(--bg-elevated);
font-weight: 500;
}
.comparison-table thead th:last-child {
color: var(--emerald);
}
.comparison-table tbody td {
padding: 0.8rem 1.2rem;
border-bottom: 1px solid var(--border);
color: var(--text-secondary);
}
.comparison-table tbody tr:hover {
background: var(--bg-elevated);
}
.comparison-table tbody td:first-child {
font-weight: 400;
color: var(--text-primary);
}
.comparison-table tbody td:last-child {
color: var(--emerald);
font-weight: 400;
}
.check { color: var(--emerald); }
.check::before { content: '\2713\00a0'; }
.cross { color: var(--text-dim); }
.cross::before { content: '\2014\00a0'; }
.muted { color: var(--text-dim); font-style: italic; }
.comparison-table thead th:last-child {
background: rgba(82, 122, 82, 0.07);
}
.comparison-table tbody td:last-child {
background: rgba(82, 122, 82, 0.04);
}
/* ===========================
PERFORMANCE
=========================== */
.perf-section {
background: var(--bg-surface);
}
.perf-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3rem;
margin-top: 3rem;
align-items: start;
}
.perf-table-wrapper {
overflow-x: auto;
border: 1px solid var(--border);
}
.perf-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
min-width: 380px;
}
.perf-table thead th {
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-dim);
padding: 0.8rem 1rem;
text-align: right;
border-bottom: 1px solid var(--border);
background: var(--bg-elevated);
font-weight: 500;
}
.perf-table thead th:first-child {
text-align: left;
}
.perf-table tbody td {
padding: 0.65rem 1rem;
border-bottom: 1px solid var(--border);
color: var(--text-secondary);
text-align: right;
font-family: var(--font-mono);
font-size: 0.82rem;
}
.perf-table tbody td:first-child {
font-family: var(--font-body);
font-size: 0.85rem;
color: var(--text-primary);
text-align: left;
font-weight: 400;
}
.perf-table tbody tr:hover {
background: var(--bg-elevated);
}
.perf-table tbody tr.perf-highlight td {
color: var(--emerald);
font-weight: 500;
}
.perf-table tbody tr.perf-highlight td:first-child {
color: var(--emerald);
}
.perf-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.perf-stat {
background: var(--bg-card);
border: 1px solid var(--border);
padding: 1.5rem;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
}
.perf-stat-value {
font-family: var(--font-display);
font-size: 2.2rem;
font-weight: 400;
line-height: 1.1;
}
.perf-stat-value.emerald { color: var(--emerald); }
.perf-stat-value.teal { color: var(--teal); }
.perf-stat-value.amber { color: var(--amber); }
.perf-stat-label {
font-size: 0.82rem;
color: var(--text-secondary);
margin-top: 0.4rem;
}
.perf-bar-group {
margin-top: 1.5rem;
}
.perf-bar-row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.6rem;
}
.perf-bar-label {
font-size: 0.75rem;
color: var(--text-secondary);
width: 80px;
flex-shrink: 0;
text-align: right;
}
.perf-bar-track {
flex: 1;
height: 18px;
background: var(--bg-elevated);
border-radius: 2px;
overflow: hidden;
position: relative;
}
.perf-bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.6s ease;
}
.perf-bar-fill.emerald { background: var(--emerald); }
.perf-bar-fill.teal { background: var(--teal); }
.perf-bar-fill.dim { background: var(--text-dim); }
.perf-bar-ms {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--text-dim);
width: 42px;
flex-shrink: 0;
}
.perf-note {
font-size: 0.78rem;
color: var(--text-dim);
margin-top: 2rem;
line-height: 1.6;
}
.perf-note a {
color: var(--teal-dim);
text-decoration: none;
border-bottom: 1px solid var(--border-teal);
}
/* ===========================
TECHNICAL
=========================== */
.tech-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3rem;
margin-top: 3rem;
align-items: start;
}
.tech-specs dt {
font-family: var(--font-mono);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--teal-dim);
margin-top: 1.25rem;
}
.tech-specs dt:first-child { margin-top: 0; }
.tech-specs dd {
font-size: 0.92rem;
color: var(--text-secondary);
margin-top: 0.25rem;
}
.code-block {
background: #1a1612;
color: #d6d0c6;
border: 1px solid rgba(44,36,24,0.15);
border-radius: 4px;
padding: 1.5rem;
font-family: var(--font-mono);
font-size: 0.8rem;
line-height: 1.9;
color: var(--text-secondary);
overflow-x: auto;
position: relative;
white-space: pre-wrap;
word-break: break-all;
}
.code-block::before {
content: 'terminal';
position: absolute;
top: 0;
right: 0;
font-family: var(--font-mono);
font-size: 0.6rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #7a7060;
padding: 0.4rem 0.8rem;
background: #241f1a;
border-left: 1px solid rgba(255,255,255,0.06);
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.code-block .prompt { color: #8baa6e; }
.code-block .comment { color: #7a7060; }
.code-block .cmd { color: #d48a5a; }
.code-block .flag { color: #8b9fbb; }
.code-block .str { color: #7dab7d; }
/* ===========================
ROADMAP
=========================== */
.roadmap {
background: var(--bg-surface);
}
.roadmap-list {
margin-top: 3rem;
position: relative;
padding-left: 2rem;
}
/* Vertical line */
.roadmap-list::before {
content: '';
position: absolute;
left: 5px;
top: 0;
bottom: 0;
width: 1px;
background: linear-gradient(to bottom, var(--emerald), var(--teal), var(--amber), var(--violet), var(--border));
}
.roadmap-item {
position: relative;
padding: 0.75rem 0;
padding-left: 1rem;
display: grid;
grid-template-columns: auto 1fr;
gap: 0.75rem;
align-items: baseline;
}
.roadmap-item::before {
content: '';
position: absolute;
left: -2rem;
top: 1.05rem;
width: 11px;
height: 11px;
border-radius: 50%;
border: 2px solid var(--text-dim);
background: var(--bg-surface);
}
.roadmap-item.done::before {
background: var(--emerald);
border-color: var(--emerald);
box-shadow: 0 0 6px rgba(82, 122, 82, 0.4);
}
.roadmap-item.phase-teal::before { border-color: var(--teal-dim); }
.roadmap-item.phase-amber::before { border-color: var(--amber-dim); }
.roadmap-item.phase-violet::before { border-color: var(--violet-dim); }
.roadmap-item .phase {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--text-dim);
white-space: nowrap;
}
.roadmap-item.done .phase {
color: var(--emerald);
}
.roadmap-item.phase-teal .phase { color: var(--teal-dim); }
.roadmap-item.phase-amber .phase { color: var(--amber-dim); }
.roadmap-item.phase-violet .phase { color: var(--violet-dim); }
.roadmap-item .phase-desc {
font-size: 0.92rem;
color: var(--text-secondary);
}
.roadmap-item.done .phase-desc {
color: var(--text-primary);
}
/* ===========================
FOOTER
=========================== */
footer {
padding: 5rem 0 3rem;
text-align: center;
}
footer .origin {
font-family: var(--font-display);
font-style: italic;
font-size: 1.1rem;
color: var(--text-secondary);
max-width: 580px;
margin: 0 auto 2.5rem;
line-height: 1.7;
}
footer .footer-links {
display: flex;
justify-content: center;
gap: 2rem;
margin-bottom: 2.5rem;
}
footer .footer-links a {
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-dim);
text-decoration: none;
transition: color 0.2s ease;
}
footer .footer-links a:hover { color: var(--amber); }
footer .closing {
font-family: var(--font-mono);
font-size: 0.7rem;
background: linear-gradient(90deg, var(--teal), var(--amber), var(--violet));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 0.04em;
}
/* ===========================
RESPONSIVE
=========================== */
@media (max-width: 900px) {
.problem-grid { grid-template-columns: 1fr; gap: 2rem; }
.layers-grid { grid-template-columns: 1fr; }
.tech-grid { grid-template-columns: 1fr; }
.perf-grid { grid-template-columns: 1fr; }
.network-grid { grid-template-columns: repeat(2, 1fr); }
.network-connections { display: none; }
.hero-line { display: none; }
.stack-row { grid-template-columns: 120px 1fr; }
}
@media (max-width: 600px) {
section { padding: 4rem 0; }
.container { padding: 0 1.25rem; }
.network-grid { grid-template-columns: 1fr; }
.pipeline { flex-direction: column; align-items: stretch; gap: 0; }
.pipeline-arrow { transform: rotate(90deg); padding: 0.15rem 0; align-self: center; }
.pipeline-node { min-width: 100%; }
.pipeline-box { width: 100%; text-align: center; }
.comparison-table { font-size: 0.75rem; }
.section-road { height: 20px; }
}
</style>
<noscript><style>.reveal { opacity: 1 !important; transform: none !important; } .hero .wordmark, .hero .tagline, .hero .description, .hero-actions { opacity: 1 !important; animation: none !important; }</style></noscript>
</head>
<body>
<!-- ==================== HERO ==================== -->
<section class="hero">
<div class="roman-bricks" aria-hidden="true"></div>
<div class="container hero-content">
<h1 class="wordmark">Numa</h1>
<div class="tagline">DNS you own. Everywhere you go.</div>
<p class="epigraph">After Numa Pompilius, who built institutions that outlasted kings.</p>
<p class="description">
Block ads and trackers. Override DNS for development. Name your local services with <code style="font-size:0.9em;color:var(--cyan)">.numa</code> domains. A single portable binary built from scratch in Rust &mdash; no Raspberry Pi, no cloud, no account.
</p>
<div class="hero-actions">
<a href="#technical" class="btn btn-primary">Get Started</a>
<a href="#architecture" class="btn btn-ghost">Read the Architecture</a>
<a href="https://github.com/razvandimescu/numa" class="btn btn-ghost" target="_blank" rel="noopener">View on GitHub</a>
</div>
</div>
<div class="hero-line" aria-hidden="true">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</section>
<div class="section-road" aria-hidden="true"><div class="roman-bricks"></div></div>
<!-- ==================== THE PROBLEM ==================== -->
<section class="problem">
<div class="container">
<div class="reveal">
<div class="section-label" style="color: var(--rose)">The Problem</div>
<h2>DNS is a single point of control</h2>
</div>
<div class="problem-grid">
<div class="problem-text reveal reveal-delay-1">
<p>Every time you visit a website, you ask a DNS resolver where to go. That resolver sees every domain you visit, when, and how often. Your ISP logs these queries by default.</p>
<p>Ad blockers work in one browser. Pi-hole needs a Raspberry Pi. Your local dev services live at <code>localhost:5173</code> and you can never remember which port is which.</p>
<p>DNS is the foundation of everything you do on the internet, but the tools for controlling it locally are either too complex (dnsmasq + nginx + mkcert) or too limited (cloud-only, appliance-only).</p>
</div>
<div class="dns-diagram reveal reveal-delay-2">
<div class="dns-node"><span class="node-dot dim"></span>Your browser</div>
<div class="dns-node"><span class="node-dot dim"></span>Your ISP / OS resolver</div>
<div class="dns-arrows">|</div>
<div class="bottleneck-label">Single point of failure</div>
<div class="dns-node central"><span class="node-dot red"></span>Cloudflare 1.1.1.1 / Google 8.8.8.8</div>
<div class="dns-arrows">|</div>
<div class="dns-node"><span class="node-dot dim"></span>ICANN root servers</div>
<div class="dns-node"><span class="node-dot dim"></span>TLD registrars (.com, .io, ...)</div>
<div class="dns-node"><span class="node-dot dim"></span>Authoritative nameservers</div>
</div>
</div>
</div>
</section>
<div class="section-road on-surface" aria-hidden="true"><div class="roman-bricks"></div></div>
<!-- ==================== HOW IT WORKS ==================== -->
<section id="layers">
<div class="container">
<div class="reveal">
<div class="section-label">How It Works</div>
<h2>What it does today</h2>
<p class="lead">A DNS resolver with caching, ad blocking, local service domains, and a REST API. Optional recursive resolution with DNSSEC. Everything runs in a single binary.</p>
</div>
<div class="layers-grid">
<div class="layer-card reveal reveal-delay-1">
<div class="layer-badge">Layer 1</div>
<h3>Resolve &amp; Protect</h3>
<ul>
<li>Forward mode by default &mdash; transparent proxy to your existing DNS, with caching</li>
<li>Ad &amp; tracker blocking &mdash; 385K+ domains, zero config</li>
<li>Recursive resolution &mdash; opt-in, resolve from root nameservers, no upstream needed</li>
<li>DNSSEC validation &mdash; chain-of-trust + NSEC/NSEC3 denial proofs (RSA, ECDSA, Ed25519)</li>
<li>TTL-aware caching (sub-ms lookups)</li>
<li>Single binary, portable &mdash; macOS, Linux, and Windows</li>
</ul>
</div>
<div class="layer-card reveal reveal-delay-2">
<div class="layer-badge">Layer 2</div>
<h3>Developer Tools</h3>
<ul>
<li>Local service proxy &mdash; <code>frontend.numa</code> instead of <code>localhost:5173</code></li>
<li>Path-based routing &mdash; <code>app.numa/api</code> &rarr; <code>:5001</code></li>
<li>Ephemeral DNS overrides with auto-revert</li>
<li>LAN service discovery via mDNS</li>
<li>Conditional forwarding &mdash; plays nice with Tailscale/VPN split-DNS</li>
<li>REST API &mdash; script everything, automate anything</li>
<li>Live dashboard with real-time stats and controls</li>
</ul>
</div>
<div class="layer-card reveal reveal-delay-3">
<div class="layer-badge">Coming Next</div>
<h3>Self-Sovereign DNS</h3>
<ul>
<li>pkarr integration &mdash; DNS via Mainline DHT, no registrar needed</li>
<li>Global <code>.numa</code> names &mdash; self-publish, DHT-backed</li>
<li>.onion bridge &mdash; human-readable names for Tor hidden services</li>
<li>Ed25519 same-key binding &mdash; zero new trust assumptions</li>
<li>No blockchain required for core naming</li>
</ul>
</div>
</div>
</div>
</section>
<div class="section-road" aria-hidden="true"><div class="roman-bricks"></div></div>
<!-- ==================== ARCHITECTURE ==================== -->
<section class="architecture" id="architecture">
<div class="container">
<div class="reveal">
<div class="section-label">Architecture</div>
<h2>Resolution pipeline</h2>
<p class="lead">Every query walks through the same deterministic pipeline. Local data takes priority; the network is the fallback.</p>
</div>
<div class="reveal reveal-delay-1">
<div class="pipeline">
<div class="pipeline-node"><div class="pipeline-box hl-teal">Query</div></div>
<span class="pipeline-arrow">&rarr;</span>
<div class="pipeline-node"><div class="pipeline-box highlight">Overrides</div></div>
<span class="pipeline-arrow">&rarr;</span>
<div class="pipeline-node"><div class="pipeline-box hl-cyan">.numa TLD</div></div>
<span class="pipeline-arrow">&rarr;</span>
<div class="pipeline-node"><div class="pipeline-box">Blocklist</div></div>
<span class="pipeline-arrow">&rarr;</span>
<div class="pipeline-node"><div class="pipeline-box">Local Zones</div></div>
<span class="pipeline-arrow">&rarr;</span>
<div class="pipeline-node"><div class="pipeline-box">Cache</div></div>
<span class="pipeline-arrow">&rarr;</span>
<div class="pipeline-node"><div class="pipeline-box hl-violet">Recursive / Forward (DoH)</div></div>
<span class="pipeline-arrow">&rarr;</span>
<div class="pipeline-node"><div class="pipeline-box highlight">DNSSEC Validate</div></div>
<span class="pipeline-arrow">&rarr;</span>
<div class="pipeline-node"><div class="pipeline-box hl-emerald">Respond</div></div>
</div>
</div>
</div>
</section>
<div class="section-road on-surface" aria-hidden="true"><div class="roman-bricks"></div></div>
<!-- ==================== WHY NUMA IS DIFFERENT ==================== -->
<section>
<div class="container">
<div class="reveal">
<div class="section-label">Comparison</div>
<h2>Why Numa is different</h2>
</div>
<div class="comparison-wrapper reveal reveal-delay-1">
<table class="comparison-table">
<caption class="sr-only">Comparison of Numa with existing DNS solutions</caption>
<thead>
<tr>
<th></th>
<th>Pi-hole</th>
<th>NextDNS</th>
<th>Cloudflare</th>
<th>AdGuard Home</th>
<th>Numa</th>
</tr>
</thead>
<tbody>
<tr>
<td>Recursive resolver</td>
<td class="cross">No (needs Unbound)</td>
<td class="cross">Cloud only</td>
<td class="cross">Cloud only</td>
<td class="cross">No</td>
<td class="check">Root hints + full DNSSEC</td>
</tr>
<tr>
<td>Ad &amp; tracker blocking</td>
<td class="check">Yes</td>
<td class="check">Yes</td>
<td class="muted">Limited</td>
<td class="check">Yes</td>
<td class="check">385K+ domains</td>
</tr>
<tr>
<td>Portable (travels with laptop)</td>
<td class="cross">No (Raspberry Pi)</td>
<td class="muted">Cloud only</td>
<td class="muted">Cloud only</td>
<td class="cross">No (network appliance)</td>
<td class="check">Single binary</td>
</tr>
<tr>
<td>Developer overrides</td>
<td class="cross">No</td>
<td class="cross">No</td>
<td class="cross">No</td>
<td class="cross">No</td>
<td class="check">REST API + auto-expiry</td>
</tr>
<tr>
<td>Local service proxy</td>
<td class="cross">No</td>
<td class="cross">No</td>
<td class="cross">No</td>
<td class="cross">No</td>
<td class="check">.numa domains + WebSocket</td>
</tr>
<tr>
<td>Data stays local</td>
<td class="check">Yes</td>
<td class="cross">Cloud</td>
<td class="cross">Cloud</td>
<td class="check">Yes</td>
<td class="check">100% local</td>
</tr>
<tr>
<td>Live dashboard</td>
<td class="check">Yes</td>
<td class="check">Yes</td>
<td class="cross">No</td>
<td class="check">Yes</td>
<td class="check">Real-time + controls</td>
</tr>
<tr>
<td>DNS-over-HTTPS upstream</td>
<td class="cross">No</td>
<td class="check">Yes</td>
<td class="check">Yes</td>
<td class="cross">No</td>
<td class="check">Built in (HTTP/2 + rustls)</td>
</tr>
<tr>
<td>Conditional forwarding</td>
<td class="cross">No</td>
<td class="cross">No</td>
<td class="cross">No</td>
<td class="muted">Manual</td>
<td class="check">Auto-detects Tailscale/VPN</td>
</tr>
<tr>
<td>Zero config needed</td>
<td class="cross">Complex setup</td>
<td class="check">Yes</td>
<td class="check">Yes</td>
<td class="cross">Docker/setup</td>
<td class="check">Works out of the box</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<div class="section-road" aria-hidden="true"><div class="roman-bricks"></div></div>
<!-- ==================== PERFORMANCE ==================== -->
<section class="perf-section" id="performance">
<div class="container">
<div class="reveal">
<div class="section-label" style="color: var(--emerald)">Performance</div>
<h2>Measured, not claimed</h2>
<p class="lead">Benchmarked with <code style="font-size:0.85em">dig</code> against public resolvers on the same machine. Cached queries resolve in under a microsecond.</p>
</div>
<div class="perf-grid">
<div class="reveal reveal-delay-1">
<div class="perf-table-wrapper">
<table class="perf-table">
<caption class="sr-only">DNS resolver latency comparison</caption>
<thead>
<tr>
<th>Resolver</th>
<th>Avg</th>
<th>P50</th>
<th>P99</th>
</tr>
</thead>
<tbody>
<tr class="perf-highlight">
<td>Numa (cached)</td>
<td>&lt;1ms</td>
<td>&lt;1ms</td>
<td>&lt;1ms</td>
</tr>
<tr>
<td>Numa (cold)</td>
<td>9ms</td>
<td>9ms</td>
<td>18ms</td>
</tr>
<tr>
<td>System resolver</td>
<td>9ms</td>
<td>8ms</td>
<td>44ms</td>
</tr>
<tr>
<td>Quad9</td>
<td>15ms</td>
<td>13ms</td>
<td>43ms</td>
</tr>
<tr>
<td>Cloudflare</td>
<td>19ms</td>
<td>14ms</td>
<td>132ms</td>
</tr>
<tr>
<td>Google</td>
<td>22ms</td>
<td>17ms</td>
<td>37ms</td>
</tr>
</tbody>
</table>
</div>
<div class="perf-bar-group">
<div class="perf-bar-row">
<span class="perf-bar-label">Numa</span>
<div class="perf-bar-track"><div class="perf-bar-fill emerald" style="width: 2%"></div></div>
<span class="perf-bar-ms">&lt;1ms</span>
</div>
<div class="perf-bar-row">
<span class="perf-bar-label">System</span>
<div class="perf-bar-track"><div class="perf-bar-fill dim" style="width: 20%"></div></div>
<span class="perf-bar-ms">9ms</span>
</div>
<div class="perf-bar-row">
<span class="perf-bar-label">Quad9</span>
<div class="perf-bar-track"><div class="perf-bar-fill dim" style="width: 33%"></div></div>
<span class="perf-bar-ms">15ms</span>
</div>
<div class="perf-bar-row">
<span class="perf-bar-label">Cloudflare</span>
<div class="perf-bar-track"><div class="perf-bar-fill dim" style="width: 42%"></div></div>
<span class="perf-bar-ms">19ms</span>
</div>
<div class="perf-bar-row">
<span class="perf-bar-label">Google</span>
<div class="perf-bar-track"><div class="perf-bar-fill dim" style="width: 49%"></div></div>
<span class="perf-bar-ms">22ms</span>
</div>
</div>
</div>
<div class="perf-sidebar reveal reveal-delay-2">
<div class="perf-stat">
<div class="perf-stat-value emerald">689 ns</div>
<div class="perf-stat-label">Cached round-trip &mdash; parse query, cache lookup, serialize response</div>
</div>
<div class="perf-stat">
<div class="perf-stat-value teal">2.0M</div>
<div class="perf-stat-label">Queries per second (single-threaded pipeline throughput, batched)</div>
</div>
<div class="perf-stat">
<div class="perf-stat-value amber">0 allocations</div>
<div class="perf-stat-label">Heap allocations in the I/O path &mdash; 4KB stack buffers, inline serialization</div>
</div>
<div class="perf-stat">
<div class="perf-stat-value teal">174 ns</div>
<div class="perf-stat-label">ECDSA P-256 signature verification (DNSSEC). RSA/SHA-256: 10.9&micro;s. DS digest: 257ns.</div>
</div>
<div class="perf-stat">
<div class="perf-stat-value emerald">~90 ms</div>
<div class="perf-stat-label">Cold-cache DNSSEC validation &mdash; only 1 network fetch needed (TLD chain pre-warmed on startup)</div>
</div>
<p class="perf-note">
Cold queries match system resolver speed &mdash; the bottleneck is upstream RTT, not Numa. We don't claim to be faster when the network is the limit.
<br><br>
Benchmarks are reproducible: <code style="font-size:0.85em">cargo bench</code> for micro-benchmarks, <code style="font-size:0.85em">python3 bench/dns-bench.sh</code> for end-to-end.
<a href="https://github.com/razvandimescu/numa/tree/main/bench">Methodology &rarr;</a>
</p>
</div>
</div>
</div>
</section>
<div class="section-road on-surface" aria-hidden="true"><div class="roman-bricks"></div></div>
<!-- ==================== TECHNICAL ==================== -->
<section id="technical">
<div class="container">
<div class="reveal">
<div class="section-label">Under the Hood</div>
<h2>Technical details</h2>
</div>
<div class="tech-grid">
<dl class="tech-specs reveal reveal-delay-1">
<dt>Runtime</dt>
<dd>Rust + tokio async (rt-multi-thread)</dd>
<dt>DNS Libraries</dt>
<dd>Zero &mdash; wire protocol parsed from scratch</dd>
<dt>Resolution Modes</dt>
<dd>Recursive (iterative from root hints, CNAME chasing, glue extraction) or Forward (DoH / plain UDP)</dd>
<dt>DNSSEC</dt>
<dd>Chain-of-trust via ring &mdash; RSA/SHA-256, ECDSA P-256, Ed25519. NSEC/NSEC3 denial proofs. EDNS0 DO bit, 1232-byte payload (DNS Flag Day 2020).</dd>
<dt>Dependencies</dt>
<dd>19 runtime crates &mdash; tokio, axum, hyper, ring (DNSSEC), reqwest (DoH), rcgen + rustls (TLS), socket2 (multicast), serde, and more</dd>
<dt>Packet Format</dt>
<dd>RFC 1035 compliant. EDNS0 OPT pseudo-record. Parses A, AAAA, NS, CNAME, MX, SOA, SRV, HTTPS, DNSKEY, DS, RRSIG, NSEC, NSEC3.</dd>
<dt>Concurrency</dt>
<dd>Arc&lt;ServerCtx&gt; + RwLock for reads, Mutex for writes (never across .await)</dd>
</dl>
<div class="code-block reveal reveal-delay-2">
<span class="comment"># Install (pick one)</span>
<span class="prompt">$</span> <span class="cmd">brew install</span> razvandimescu/tap/numa
<span class="prompt">$</span> <span class="cmd">cargo install</span> numa
<span class="prompt">$</span> <span class="cmd">curl</span> <span class="flag">-fsSL</span> https://raw.githubusercontent.com/razvandimescu/numa/main/install.sh <span class="flag">|</span> <span class="cmd">sh</span>
<span class="comment"># Run</span>
<span class="prompt">$</span> <span class="cmd">sudo numa</span> <span class="comment"># bind to :53, :80, :5380</span>
<span class="prompt">$</span> <span class="cmd">dig</span> <span class="flag">@127.0.0.1</span> google.com <span class="comment"># test resolution</span>
<span class="prompt">$</span> <span class="cmd">open</span> http://localhost:5380 <span class="comment"># dashboard</span>
<span class="prompt">$</span> <span class="cmd">curl</span> <span class="flag">-X POST</span> localhost:5380/services \
<span class="flag">-d</span> <span class="str">'{"name":"frontend",
"target_port":5173}'</span> <span class="comment"># https://frontend.numa</span>
</div>
</div>
</div>
</section>
<div class="section-road" aria-hidden="true"><div class="roman-bricks"></div></div>
<!-- ==================== ROADMAP ==================== -->
<section class="roadmap">
<div class="container">
<div class="reveal">
<div class="section-label">Roadmap</div>
<h2>Where we're going</h2>
</div>
<div class="roadmap-list reveal reveal-delay-1">
<div class="roadmap-item done">
<span class="phase">Phase 0</span>
<span class="phase-desc">DNS proxy core &mdash; zones, caching, forwarding, async tokio runtime</span>
</div>
<div class="roadmap-item done">
<span class="phase">Phase 1</span>
<span class="phase-desc">Override layer + REST API for programmatic DNS control</span>
</div>
<div class="roadmap-item done">
<span class="phase">Phase 2</span>
<span class="phase-desc">Ad &amp; tracker blocking &mdash; 385K+ domains, live dashboard, one-click allowlist</span>
</div>
<div class="roadmap-item done">
<span class="phase">Phase 3</span>
<span class="phase-desc">System integration &mdash; auto-discovery of OS DNS routing, one-command install</span>
</div>
<div class="roadmap-item done">
<span class="phase">Phase 4</span>
<span class="phase-desc">Local service proxy &mdash; .numa domains, HTTP/HTTPS reverse proxy, auto TLS, WebSocket</span>
</div>
<div class="roadmap-item done">
<span class="phase">Phase 5</span>
<span class="phase-desc">DNS-over-HTTPS &mdash; encrypted upstream, HTTP/2 connection pooling</span>
</div>
<div class="roadmap-item done">
<span class="phase">Phase 6</span>
<span class="phase-desc">Recursive resolution &mdash; resolve from root nameservers, no upstream dependency</span>
</div>
<div class="roadmap-item done">
<span class="phase">Phase 7</span>
<span class="phase-desc">DNSSEC validation &mdash; chain-of-trust, NSEC/NSEC3 denial proofs, RSA + ECDSA + Ed25519</span>
</div>
<div class="roadmap-item phase-teal">
<span class="phase">Phase 8</span>
<span class="phase-desc">pkarr integration &mdash; self-sovereign DNS via Mainline DHT, no registrar needed</span>
</div>
<div class="roadmap-item phase-teal">
<span class="phase">Phase 9</span>
<span class="phase-desc">Global .numa names &mdash; self-publish, DHT-backed, first-come-first-served</span>
</div>
<div class="roadmap-item phase-teal">
<span class="phase">Phase 10</span>
<span class="phase-desc">.onion bridge &mdash; human-readable Tor naming via Ed25519 same-key binding</span>
</div>
</div>
</div>
</section>
<!-- ==================== FOOTER ==================== -->
<footer>
<div class="container">
<p class="origin reveal">
Named after Numa Pompilius &mdash; the second king of Rome who established the institutions, laws, and religious practices that outlasted the monarchy itself.
</p>
<div class="footer-links reveal reveal-delay-1">
<a href="https://github.com/razvandimescu/numa" target="_blank" rel="noopener">GitHub</a>
<a href="/blog/">Blog</a>
<a href="https://github.com/razvandimescu/numa/blob/main/LICENSE" target="_blank" rel="noopener">MIT License</a>
</div>
<p class="closing reveal reveal-delay-2">Built from scratch in Rust. No dependencies on trust.</p>
</div>
</footer>
<script>
// Intersection Observer for reveal animations
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -40px 0px'
});
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
</script>
</body>
</html>