惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

M
MIT News - Artificial intelligence
WordPress大学
WordPress大学
GbyAI
GbyAI
S
SegmentFault 最新的问题
量子位
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
MyScale Blog
MyScale Blog
人人都是产品经理
人人都是产品经理
博客园 - 叶小钗
aimingoo的专栏
aimingoo的专栏
V
Visual Studio Blog
U
Unit 42
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The Cloudflare Blog
Stack Overflow Blog
Stack Overflow Blog
博客园 - 聂微东
J
Java Code Geeks
The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
IT之家
IT之家
Martin Fowler
Martin Fowler
宝玉的分享
宝玉的分享
雷峰网
雷峰网

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Я написал свой DNS-резолвер на Go вместо того, чтобы взят...
cyberscoper · 2026-05-15 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение6 мин

Охват и читатели338

Кейс

Привет, Хабр!

Три месяца назад я начал делать NextDNS-clone для Европы. Рекурсивный DNS с фильтрацией рекламы, трекеров и malware. Первый день: открываю Unbound, читаю man, всё понятно. К вечеру понимаю, что не подходит. Через неделю пишу свой резолвер на Go и вспоминаю поговорку про человека, который решил написать почтовый сервер. Никогда такого не было, и вот опять.

Сейчас в проде: 10 нод по миру, отвечает на DoH/DoT, фильтрует по миллионам доменов, RAM 60 МБ на ноду. Расскажу, почему ушёл от готового, что было больно, и где Unbound всё ещё быстрее. Спойлер: почти везде, но в наших условиях это не имеет значения.

В конце ссылки на open-source ядро резолвера и наш бесплатный тариф, можно потыкать.

Зачем вообще свой резолвер, если есть Unbound

Unbound — отличный софт. PowerDNS Recursor тоже. Я их обожаю как обожают старого кота: за то, что они есть и не требуют объяснений. В обычной ситуации просто бы поставил один из них и пошёл пить чай. Но у меня было три требования, которые ни тот, ни другой не закрывают штатно.

Миллионы DoH-эндпойнтов с разными правилами фильтрации

В NextDNS-like продукте каждый юзер получает свой config_id, короткий публичный токен в URL: https://dns.vantagedns.com/<config_id>/dns-query. У одного юзера может быть профиль «дом», «дети», «офис», у каждого свои блок-листы и whitelist. Это не «один резолвер с одним конфигом», это десятки тысяч логических резолверов на одной ноде.

В Unbound views есть, но они не масштабируются на десятки тысяч независимых наборов правил без боли с конфиг-генерацией и SIGHUP’ом каждые пять минут. Я как-то видел подобную конструкцию в одной телекомовской конторе в 2014-м. Там админ носил с собой бумажку с порядком действий, чтобы не забыть. Не хотелось в это ввязываться.

In-memory query log без записи на диск

Edge-нода не должна писать на диск ничего, что относится к юзеру. Это часть позиционирования. Поэтому query log живёт в кольцевом буфере в памяти, async-шипится в ClickHouse в Хельсинки, и всё. Unbound пишет логи в файл; для async-shipping надо городить tail+парсер.

Retention: 24 часа на free, до 30 дней на платных, дальше ClickHouse TTL чистит.

Bloom-фильтры для блок-листов с шарингом между профилями

У нас 10+ блок-листов от 50K до 5M доменов каждый. Если хранить их per-profile в виде set’ов, это сотни МБ RAM на ноду. С Bloom: 8 МБ на лист, шарится между всеми профилями, false-positive rate < 0.1%. В Unbound такого нет, там либо local-zone (медленно на больших списках), либо RPZ (тоже не совсем то).

Долго смотрел на варианты «допилить Unbound через модули» и отказался. Модульный API на C, миллионы конфигов через unix-сокет — это путь в ад дебага, причём сразу пятый круг. У меня нет на это ни нервов, ни жизни.

Почему Go, а не Rust/C++

Короткий ответ: я знаю Go и не знаю Rust на уровне «писать сетевую системную программу под нагрузкой». А C я в последний раз писал в университете, когда ещё думал, что segfault — это интересная диагностическая проблема. Сейчас я знаю, что это просто наказание за гордыню.

Длинный ответ:

Критерий

Go

Rust

C/C++

Скорость старта

✅ < 1 с

✅ < 1 с

✅ < 1 с

Memory safety

✅ GC

✅ borrow check

Время до прототипа

✅ дни

⚠️ недели

❌ месяцы

Library: DNS

miekg/dns (де-факто стандарт)

⚠️ trust-dns ок, но беднее

✅ много

Gorout./async

✅ goroutine на запрос — норма

⚠️ async runtime

❌ epoll руками

miekg/dns решал 80% задач из коробки: парсинг wire-format, сериализация, EDNS, DNSSEC. Я писал поверх неё recursive resolution, кеш, фильтрацию.

С Rust я бы выиграл 10-15% по latency и какие-то МБ по RAM. Но потерял бы 2 месяца на rewriting. Для бутстрап-проекта 1-человека это не выгодно.

Что было больно

Рекурсивная резолюция — это не просто «спросить .com NS, потом NS зоны»

Когда впервые пишешь recursive resolver, кажется: ну подумаешь, рекурсивно ходим по NS, кешируем, всё. Любой первокурсник напишет за вечер. Реальность, как водится, побила меня палкой по голове и обозвала наивным.

Параллельные NS lookups. У зоны обычно 2-4 авторитетных NS. Если последовательно дёргать каждый при таймауте, на холодном кеше один резолв example.com может занять 5+ секунд. Параллельные goroutines, race-to-first-response, отсев медленных.

Glue records. Когда запрашиваешь ns1.example.com для зоны example.com, это chicken-and-egg. Авторитетный сервер отдаёт A-запись для NS прямо в additional section. Если её игнорировать, рекурсия зацикливается. Был баг, где резолвер упирался в loop, потому что пропускал additional.

Negative caching. RFC 2308 (1998 год, между прочим, привет из эпохи, когда DNS ещё считался простым). Если NS вернул NXDOMAIN, надо кешировать с TTL из SOA, а не из ответа. Иначе при первом таймауте кешируем «домен не существует» на стандартный час и юзер ругается, а я смотрю в логи и не понимаю, почему GitHub лежит только у меня одного.

QNAME minimisation. Должно быть включено по умолчанию, это privacy. Запрашиваешь mail.google.com → у . спрашиваешь только com, у com только google.com, у google.com уже полный mail.google.com. Не «у всех NS на пути показываем full qname». Реализовать аккуратно отдельная история: некоторые корявые NS отвечают NODATA на минимизированный запрос, и ты сидишь, ловишь их в логах.

EDNS Client Subnet — отдельный ад

Без ECS Google отдаёт IP CDN из той страны, где стоит твоя нода. Юзер в Берлине через нашу ноду в Хельсинки получает финский IP googlevideo.com. Пинг 80 мс вместо 5 мс. YouTube тормозит, юзер пишет, что у нас «всё сломано».

С ECS публикуешь /24 подсеть юзера авторитетному NS. И вот тут начинается. Часть NS на ECS плюёт. Cloudflare принципиально не использует, потому что anycast. Часть отдаёт «правильный» IP. Часть выдаёт неправильный, а потом ты неделю выясняешь, почему. RFC 7871 формально опционален, и каждый сервер интерпретирует «опционально» по-своему, как бывшие супруги интерпретируют «давай останемся друзьями».

Тестирование на 30+ доменов руками было самым нудным этапом за все три месяца.

DNSSEC validation

Реализовать с нуля validation chain — две недели чистого времени. Я в итоге включил его опционально: для большинства юзеров он сейчас mostly cosmetic (95% доменов всё равно без подписи), а baggage серьёзный.

Cache poisoning — параноя 24/7

Любой кеширующий резолвер — потенциальная цель для cache poisoning. Source port randomization, txn-id randomization, 0x20 case randomization (да-да, тот самый трюк с регистром букв в qname, который выглядит как костыль и им и является), не доверять additional section без подтверждения через bailiwick. Это всё надо сделать до релиза, не после.

Раз 5 переписывал валидацию ответа. Каждый раз находил новый edge case и думал «ну вот теперь точно всё». Так не бывает. Запомните это, дети.

Цифры: что в итоге получилось

Замеры с одной ноды (Hetzner CPX21, 3 vCPU, 4 GB RAM, Helsinki):

QPS sustained:           ~12 000 cached / ~3 500 cold
P50 latency cached:      0.3 ms
P50 latency cold (.com): 38 ms
P99 latency cold:        180 ms
RAM steady state:        62 MB
Binary size:             14 MB (go build -ldflags="-s -w")
Cold start to ready:     0.4 s

Для сравнения, Unbound на той же ноде с похожим конфигом:

QPS sustained:           ~18 000 cached / ~5 000 cold
P50 latency cached:      0.18 ms
RAM steady state:        85 MB

Unbound быстрее на 30-40% по cached lookups. Это ожидаемо: он на C, код вылизан 20 лет. На холодных запросах разница меньше, потому что упираемся в сеть.

Для нашей нагрузки (~500 QPS на ноду пиково на текущем этапе) этой разницы не существует.

Что бы я сделал по-другому

Включил бы pprof и continuous profiling с первого дня. Первые два месяца дебажил allocations через runtime.ReadMemStats и top как пещерный человек. С pprof + Parca находил бы лики за минуты, а не за вечера. В оправдание скажу, что в три ночи кажется, будто top — это нормальный инструмент.

Не писал бы сразу Bloom-фильтры, взял бы суффиксные деревья. Bloom нужен на масштабе миллионов доменов. На старте у меня было 200K записей в листе, обычный suffix-trie дал бы exact matching без false positives, проще дебажить. Bloom добавил бы потом, когда уже припекает. Это, кстати, главное правило: не оптимизируй то, что не болит. Я его знаю с 2010 года и регулярно нарушаю.

Сделал бы feature flags с первого дня. Сейчас у меня 4 разных пути в коде «если ECS включён — так, иначе так», и они переплетены, как кабели за рабочим столом любого старого админа. С флагами можно было бы тестировать в проде на 1% трафика. Без них тестирую на себе, что довольно унизительно.

Open-source

Edge-резолвер — open source, MIT. Ссылка в конце статьи. Что НЕ open-source: control plane (биллинг, дашборд, blocklist-каталог) — это бизнес.

Зачем open-source edge: я хочу, чтобы юзер мог поднять свой собственный экземпляр и убедиться, что мы не врём про «не пишем на диск». В transparency report — статистика запросов на правоохранителей. Warrant canary обновляем еженедельно.

Что дальше

В следующих постах планирую разобрать конкретику:

  • как делал Bloom-фильтры с шарингом между профилями;

  • почему отказался от anycast в пользу geo-DNS на 10 PoPs и сколько это реально стоит;

  • как async-шипим query log в ClickHouse без потерь и без диска на edge.

Если интересно что-то конкретное — пишите в комментарии, постараюсь приоритизировать.


Ссылки:

Спасибо, что дочитали. Замечания и вопросы пишите в комменты.