
Портативный DNS-резолвер на Rust — локальные домены .numa, блокировка рекламы, переопределения для разработчиков
# Numa
[](https://github.com/razvandimescu/numa/actions)
[](https://crates.io/crates/numa)
[](LICENSE)
**DNS, которым владеете вы. Везде, куда бы вы ни пошли.** — [numa.rs](https://numa.rs)
Портативный DNS-резолвер в одном бинарнике. Блокируйте рекламу в любой сети, давайте имена локальным сервисам (`frontend.numa`), переопределяйте любое имя хоста с автооткатом и запечатывайте каждый исходящий запрос с помощью **ODoH (RFC 9230)**, чтобы ни одна сторона не видела одновременно, кто вы и что вы запросили — всё с вашего ноутбука, без облачного аккаунта и без Raspberry Pi.
Написан с нуля на Rust. Ноль DNS-библиотек. Кэширование, блокировка рекламы и локальные домены сервисов из коробки. Опциональное рекурсивное разрешение от корневых серверов имён с полной валидацией цепочки доверия DNSSEC, плюс слушатель DNS-over-TLS для зашифрованных клиентских подключений (iOS Private DNS, systemd-resolved и т. д.). Запустите `numa relay` — и тот же бинарник становится публичной конечной точкой ODoH: в курируемом списке DNSCrypt сейчас остался всего один работающий релей, так что каждый развёрнутый Numa ощутимо расширяет экосистему. Один бинарник ~8 МБ, всё встроено. Парсер протокола был написан вручную как учебный проект; более поздние возможности (рекурсивный резолвер, DNSSEC, дашборд) создавались с помощью ИИ.

## Быстрый старт
Три способа — от наименее до наиболее основательного. Только третий меняет системный DNS.
### 1. Попробовать в Docker (на хосте ничего не устанавливается)
```bash
docker run -d --name numa -p 127.0.0.1:5553:53/udp -p 127.0.0.1:5553:53/tcp \
-p 127.0.0.1:5380:5380 ghcr.io/razvandimescu/numa
dig @127.0.0.1 -p 5553 example.com
docker exec numa numa token # пароль к дашборду
```
Откройте `http://localhost:5380` и войдите с любым именем пользователя и этим токеном. Порт 5553 обходит всё, что уже занимает 53 на хосте. `numa.numa` здесь не разрешится, потому что хост не использует Numa для DNS. Удалить: `docker rm -f numa`.
### 2. Запуск на переднем плане (системный DNS не трогается)
Установите бинарник:
```bash
# macOS
brew install razvandimescu/tap/numa
# Linux
curl -fsSL https://raw.githubusercontent.com/razvandimescu/numa/main/install.sh | sh
# Arch Linux
pacman -S numa
# Windows — скачайте из GitHub Releases
# Все платформы
cargo install numa
# Nix
nix run github:razvandimescu/numa
```
```bash
sudo numa # Ctrl-C для остановки (порт 53 требует root/администратора)
```
Numa слушает порт 53, но система сохраняет текущий резолвер, пока вы не выполните `numa install`, так что проверяйте через `dig @127.0.0.1 example.com`. Дашборд доступен по адресу `http://localhost:5380`. Если порт 53 занят (systemd-resolved в Ubuntu/Mint), задайте `bind_addr` в [`numa.toml`](https://github.com/razvandimescu/numa/blob/main/numa.toml) или используйте Docker.
### 3. Назначить системным DNS
| Платформа | Установка | Удаление |
|----------|---------|-----------|
| macOS | `sudo numa install` | `sudo numa uninstall` |
| Linux | `sudo numa install` | `sudo numa uninstall` |
| Windows | `numa install` (администратор) + перезагрузка | `numa uninstall` (администратор) + перезагрузка |
`install` регистрирует службу, направляет системный DNS на Numa и добавляет его локальный CA в доверенные. `uninstall` отменяет все три действия. После установки дашборд также доступен по адресу **http://numa.numa**.
В macOS и Linux numa работает как системная служба (launchd/systemd). Юнит systemd непривилегированный (`DynamicUser=yes`, только `CAP_NET_BIND_SERVICE`); демон launchd работает от root. `numa install` перенастраивает systemd-resolved через drop-in, который удаляется при `numa uninstall`; любой другой процесс, занимающий порт 53 (dnsmasq, включая NetworkManager), нужно останавливать вручную. В Windows numa автоматически запускается при входе через реестр. Windows также привязывается к `127.0.0.2:53` (встроенный Dnscache владеет `127.0.0.1:53`) и устанавливает правило NRPT для маршрутизации запросов туда — поэтому правьте `bind_addr`/`api_bind_addr` относительно `127.0.0.2`, а не `127.0.0.1`.
### Вход в систему
Через loopback (`localhost`, `127.0.0.1`, `numa.numa`) вход не требуется. В любом другом случае, включая проброс портов Docker или LAN-адрес этой машины, запрашивается API-токен, который Numa генерирует при первом запуске. Выведите его командой `sudo numa token` (в Windows — из оболочки администратора) и войдите с любым именем пользователя. Задайте свой собственный через `[server] api_token` или `NUMA_API_TOKEN`.
### Удаление всех следов
`uninstall` восстанавливает DNS, но сохраняет каталог данных, поэтому при переустановке сохраняются тот же токен и CA. Чтобы удалить всё, сначала выполните uninstall, затем удалите:
| Платформа | Что остаётся |
|----------|-------------|
| macOS | бинарник, `/usr/local/var/numa`, `/usr/local/var/log/numa.log` |
| Linux | бинарник (`/usr/local/bin/numa` из install.sh), `/var/lib/numa`, `/etc/numa` |
| Windows | бинарник, `%PROGRAMDATA%\numa` |
Установки через пакетные менеджеры удаляют бинарник с помощью `brew uninstall`, `pacman -R` или `cargo uninstall`. В Linux `numa install` копирует бинарник, который не может запустить из исходного расположения (например, `~/.cargo/bin`), в `/usr/local/bin/numa`, и uninstall оставляет эту копию.
Также удалите `~/.config/numa`, если вы создавали пользовательский конфиг, и путь `[server] data_dir`, если вы его задавали.
## Локальные сервисы
Давайте имена своим dev-сервисам вместо запоминания номеров портов:
```bash
curl -X POST localhost:5380/services \
-d '{"name":"frontend","target_port":5173}'
```
Теперь `https://frontend.numa` работает в браузере — зелёный замок, валидный сертификат, проброс WebSocket для HMR. Без mkcert, без nginx, без `/etc/hosts`.
Добавьте маршрутизацию по путям (`app.numa/api → :5001`), делитесь сервисами между машинами через LAN-обнаружение или настройте всё в [`numa.toml`](https://github.com/razvandimescu/numa/blob/main/numa.toml).
## Блокировка рекламы и приватность
Блокировка рекламы и трекеров через [Hagezi Pro](https://github.com/hagezi/dns-blocklists), обновляется ежедневно. Работает в любой сети — кофейни, отели, аэропорты. Путешествует вместе с вашим ноутбуком.
Три режима разрешения:
- **`forward`** (по умолчанию) — прозрачный прокси к вашему текущему системному DNS. Всё работает как раньше, только с кэшированием и блокировкой рекламы поверх. Captive-порталы, VPN, корпоративный DNS — всё учитывается.
- **`recursive`** — разрешение напрямую от корневых серверов имён. Никакой зависимости от вышестоящих, ни одна сущность не видит полную картину ваших запросов. Добавьте `[dnssec] enabled = true` для полной валидации цепочки доверия.
- **`auto`** — проверка корневых серверов при запуске: рекурсивно, если доступны, иначе forward через DoH к Quad9 (`https://9.9.9.9/dns-query`), который тогда видит ваши запросы. Используйте `forward` со своим `[upstream]`, чтобы выбрать другого провайдера.
DNSSEC проверяет полную цепочку доверия: подписи RRSIG, верификацию DNSKEY, делегирование DS, доказательства отсутствия NSEC/NSEC3. [Читайте, как это работает →](https://numa.rs/blog/posts/dnssec-from-scratch.html)
**Слушатель DNS-over-TLS** (RFC 7858) — принимает зашифрованные запросы на порту 853 от строгих клиентов, таких как iOS Private DNS, systemd-resolved или stubby. Два режима:
- **Самоподписанный** (по умолчанию) — numa автоматически генерирует локальный CA. `numa install` добавляет его в системное хранилище доверенных сертификатов в macOS, Linux (Debian/Ubuntu, Fedora/RHEL/SUSE, Arch) и Windows, а `numa uninstall` удаляет. В iOS установите `.mobileconfig` из `numa setup-phone`. Firefox использует собственное хранилище NSS и игнорирует системное — доверьте CA там вручную, если вам нужен HTTPS для сервисов `.numa` в Firefox.
- **Свой сертификат** — укажите `[dot] cert_path` / `key_path` на публично доверенный сертификат (например, Let's Encrypt через DNS-01 challenge на домене, указывающем на ваш экземпляр numa). Клиенты подключаются без настройки хранилища доверия — тот же UX, что у AdGuard Home или Cloudflare `1.1.1.1`.
ALPN `"dot"` объявляется и проверяется в обоих режимах; рукопожатие с несовпадающим ALPN отклоняется как защита от cross-protocol confusion.
**Oblivious DoH** (RFC 9230) — при `[upstream] mode = "odoh"` ([рецепт](https://github.com/razvandimescu/numa/blob/main/recipes/odoh-upstream.md)) каждый запрос запечатывается HPKE для цели и отправляется через релей. Релей видит ваш IP и шифротекст, цель видит вопрос и IP релея, а релей, перенаправляющий запрос, передаёт новому получателю нечто, что тот не может расшифровать. Numa отказывается от релея и цели, имеющих общий регистрируемый домен. ODoH не скрывает соединение, которое вы открываете после: ваш провайдер всё равно видит IP назначения и, без ECH, имя хоста в TLS-рукопожатии.
**Настройка телефона** — направьте iPhone или Android на Numa за один шаг:
```bash
numa setup-phone
```
Выводит QR-код. Отсканируйте его, установите профиль, включите доверие к сертификату — DNS вашего телефона теперь идёт через Numa по TLS. Требуется `[mobile] enabled = true` в `numa.toml`.
## LAN-обнаружение
Запустите Numa на нескольких машинах. Они находят друг друга автоматически через mDNS:
```
Machine A (192.168.1.5) Machine B (192.168.1.20)
┌──────────────────────┐ ┌──────────────────────┐
│ Numa │ mDNS │ Numa │
│ - api (port 8000) │◄───────────►│ - grafana (3000) │
│ - frontend (5173) │ discovery │ │
└──────────────────────┘ └──────────────────────┘
```
С машины B: `curl http://api.numa` → проксируется на порт 8000 машины A. Включается через `numa lan on`.
**Режим хаба**: запустите один экземпляр с `bind_addr = "0.0.0.0:53"` и направьте DNS других устройств на него — они получат блокировку рекламы + разрешение `.numa` без установки чего-либо. `bind_addr` также принимает список, чтобы привязаться к определённому подмножеству интерфейсов. Пошагово: [рецепт для всей сети](https://github.com/razvandimescu/numa/blob/main/recipes/network-wide.md).
## Docker
```bash
# Рекомендуется — host networking (Linux)
docker run -d --name numa --network host -v numa-data:/var/lib/numa ghcr.io/razvandimescu/numa
# Проброс портов (macOS/Windows Docker Desktop)
docker run -d --name numa -p 53:53/udp -p 53:53/tcp -p 5380:5380 -v numa-data:/var/lib/numa ghcr.io/razvandimescu/numa
```
Дашборд по адресу `http://localhost:5380`. При пробросе портов или с другого устройства войдите с любым именем пользователя и токеном из `docker exec numa numa token`. Том `numa-data` сохраняет токен и локальный CA при пересоздании контейнера. Образ по умолчанию привязывает API и прокси к `0.0.0.0`. Переопределите своим конфигом:
```bash
docker run -d --name numa --network host -v numa-data:/var/lib/numa \
-v /path/to/numa.toml:/root/.config/numa/numa.toml \
ghcr.io/razvandimescu/numa
```
Мультиархитектура: `linux/amd64` и `linux/arm64`.
Готовые compose-рецепты:
- [`packaging/client/`](https://github.com/razvandimescu/numa/blob/main/packaging/client) — режим клиента ODoH (анонимный DNS), Numa + стартовый `numa.toml`.
- [`packaging/relay/`](https://github.com/razvandimescu/numa/blob/main/packaging/relay) — публичный релей ODoH, Numa + Caddy + ACME.
## Сравнение
| | Pi-hole | AdGuard Home | Unbound | Numa |
|---|---|---|---|---|
| Прокси локальных сервисов + авто TLS | — | — | — | домены `.numa`, HTTPS, WebSocket |
| LAN-обнаружение сервисов | — | — | — | mDNS, без настройки |
| Переопределения для разработчиков (REST API) | — | — | — | Автооткат, скриптуемость |
| Рекурсивный резолвер | — | — | Да | Да, с выбором по SRTT |
| Валидация DNSSEC | — | — | Да | Да (RSA, ECDSA, Ed25519) |
| Блокировка рекламы | Да | Да | — | Hagezi Pro |
| Правила для отдельных клиентов | Группы | Да | Views / tags | По CIDR (`[[client_policy]]`), только в конфиге |
| Веб-интерфейс администратора | Полный | Полный | — | Дашборд |
| Зашифрованный upstream (DoH/DoT) | Нужен cloudflared | Только DoH | Только DoT | DoH + DoT (`tls://`) |
| Зашифрованные клиенты (слушатель DoT) | Нужен sidecar stunnel | Да | Да | Нативно (RFC 7858) |
| Конечная точка DoH-сервера | — | Да | — | Да (RFC 8484) |
| Хеджирование запросов | — | — | — | Все протоколы (UDP, DoH, DoT) |
| Serve-stale + prefetch | — | — | Prefetch при 90% TTL | RFC 8767, prefetch при 90% TTL |
| Условный форвардинг | — | Да | Да | Да (правила по суффиксам) |
| Портативность (ноутбук) | Нет (устройство) | Нет (устройство) | Сервер | Один бинарник, macOS/Linux/Windows |
| Зрелость сообщества | 56K звёзд, 10 лет | 33K звёзд | 20 лет | Новый |
## Использование Numa как основного DNS
**Если Numa остановится.** `numa install` регистрирует Numa в launchd (macOS) или systemd (Linux), которые перезапускают его при выходе. Если Numa — ваш единственный резолвер, DNS-запросы не работают, пока он не перезапустится. Чтобы перестать использовать Numa и восстановить прежние настройки DNS машины, выполните `sudo numa uninstall`.
Если Numa работает, но вышестоящие серверы недоступны, он может отдавать кэшированные ответы до часа после истечения их TTL (RFC 8767). Имена, которых нет в кэше, не разрешатся.
**Тестирование.** DNS-парсер фаззится при каждом pull request, затрагивающем его, с более длительными прогонами еженедельно ([`fuzz.yml`](https://github.com/razvandimescu/numa/blob/main/.github/workflows/fuzz.yml)). CI запускает `cargo audit` по зависимостям и устанавливает, переустанавливает и удаляет Numa в macOS и Linux.
**Укрепление резолвера.** В рекурсивном режиме Numa отбрасывает записи ответа за пределами зоны, по которой идёт запрос, отказывается запрашивать серверы имён по приватным или loopback-адресам и ограничивает число вышестоящих запросов и глубину рефералов для каждого поиска. Ответы по обычному UDP должны совпадать со случайным ID транзакции и вопросом запроса. Запросы ANY отклоняются. Валидация DNSSEC по умолчанию выключена; `numa dnssec on` включает её. Чтобы сообщить об уязвимости приватно, см. [SECURITY.md](https://github.com/razvandimescu/numa/blob/main/SECURITY.md).
**Чего Numa не делает.** Ни DHCP, ни кластеризации, ни синхронизации конфигурации между экземплярами. Большинство настроек живёт в [`numa.toml`](https://github.com/razvandimescu/numa/blob/main/numa.toml), а не в дашборде. Для всей сети запускайте его на машине, которая постоянно включена.
## Производительность
0.1 мс для кэшированных запросов — на уровне Unbound и AdGuard Home. Кэш на уровне wire хранит сырые байты с правкой TTL на месте. Хеджирование запросов устраняет всплески p99: холодный рекурсивный p99 538 мс против 748 мс у Unbound (−28%), σ в 4 раза плотнее. [Бенчмарки →](https://github.com/razvandimescu/numa/blob/main/benches)
## Узнать больше
- [Блог: Numa как резолвер для вашего tailnet](https://numa.rs/blog/posts/numa-tailnet-resolver.html)
- [Блог: DNS-over-TLS с нуля на Rust](https://numa.rs/blog/posts/dot-from-scratch.html)
- [Блог: Реализация DNSSEC с нуля на Rust](https://numa.rs/blog/posts/dnssec-from-scratch.html)
- [Блог: Я написал DNS-резолвер с нуля](https://numa.rs/blog/posts/dns-from-scratch.html)
- [Справочник по конфигурации](https://github.com/razvandimescu/numa/blob/main/numa.toml) — все опции документированы inline; `numa config path` показывает, какой файл использует ваша установка, `numa config edit` открывает его
- [REST API](https://github.com/razvandimescu/numa/blob/main/src/api.rs) — переопределения, кэш, блокировка, сервисы, диагностика
- [numa-metrics](https://github.com/razvandimescu/numa-metrics) — долговременная история запросов и аналитика, по замыслу вне хоста (без записи на SD-карту)
## План развития
- [x] Форвардинг DNS, кэширование, блокировка рекламы, переопределения для разработчиков
- [x] Локальные домены `.numa` — авто TLS, маршрутизация по путям, прокси WebSocket
- [x] LAN-обнаружение сервисов — mDNS, кросс-машинный DNS + прокси
- [x] DNS-over-HTTPS — зашифрованный upstream + конечная точка сервера (RFC 8484)
- [x] DNS-over-TLS — слушатель зашифрованных клиентов (RFC 7858) + форвардинг upstream (`tls://`)
- [x] Oblivious DoH — анонимизированный клиентский режим + публичный релей (`numa relay`, RFC 9230)
- [x] Рекурсивное разрешение + DNSSEC — цепочка доверия, NSEC/NSEC3
- [x] Выбор серверов имён на основе SRTT
- [x] Отказоустойчивость нескольких форвардеров — несколько upstream с ранжированием по SRTT, резервный пул
- [x] Хеджирование запросов — параллельные запросы спасают от потери пакетов и хвостовых задержек (все протоколы)
- [x] Serve-stale + prefetch — RFC 8767, фоновое обновление при <10% TTL и при отдаче устаревшего
- [x] Условный форвардинг — правила по суффиксам для split-horizon DNS (Tailscale, VPN)
- [x] Прогрев кэша — проактивное разрешение для настроенных доменов
- [x] Мобильный онбординг — QR-поток `setup-phone`, мобильный API, профили mobileconfig
- [ ] Интеграция pkarr — самосуверенный DNS через Mainline DHT
- [ ] Глобальные имена `.numa` — на базе DHT, без регистратора
## Лицензия
MIT