Finite: DNS-сервер на Raspberry Pi который убирает рекламу со всех устройств
Я Никита, пишу код на Ruby и JavaScript уже лет десять, в основном финтех и платежи. Сейчас на саббатикале - отдыхаю от работы и ковыряюсь с домашней инфраструктурой.
Почему я вообще этим занялся
Всё началось с телевизора. Купил Samsung, подключил к WiFi, и понял что он показывает рекламу прямо в меню. В меню, Карл! Я заплатил за телевизор, а он мне рекламу крутит.
Ладно, можно пережить. Но потом меня сильно начало доводить другое. Я погуглил кроссовки один раз - и теперь вижу их везде. На телефоне, на ноутбуке, на том же телевизоре в YouTube. Таргетированная реклама следует за мной по всем устройствам.
На компьютере есть AdBlock. На телефоне - с переменным успехом. На телевизоре и умной колонке - никак. Они просто сливают данные о моих запросах куда-то в интернет, и я ничего не могу с этим сделать.
Или могу?
Идея: блокировать рекламу на уровне сети
Если нельзя поставить блокировщик на каждое устройство - можно поставить его на роутер. Точнее, рядом с роутером.
Pi-hole - это штука которая притворяется DNS-сервером. Когда телевизор спрашивает "где находится ads.samsung.com?", Pi-hole отвечает "нигде, такого адреса не существует". Реклама не загружается. Трекер не получает данные.
Это работает для всего что подключено к домашнему WiFi. Телефоны, ноутбуки, телевизоры, умные лампочки - всё проходит через один DNS, и всё фильтруется.
Звучит просто. На практике - неделя боли.
Почему не просто установить Pi-hole по гайду?
Гайдов в интернете миллион. "Установи Raspbian, запусти curl | bash, готово". Я уже так делал очень давно когда впервые начал играться с Raspberry. Работало.
А потом SD-карта умерла. Raspberry Pi любит убивать SD-карты - они не рассчитаны на постоянную запись логов.
Окей, купил новую карту. Сел восстанавливать. И понял что не помню ничего. Какие блоклисты я добавлял? Какой пароль ставил? Как настраивал Unbound? Всё это было в голове год назад, а теперь - чистый лист.
Начал заново. Снова гуглил, снова настраивал, снова забуду через год.
NixOS: вся система в одном файле
На десктопе я уже активно использую NixOS. Это дистрибутив Linux где вся конфигурация системы описана в текстовых файлах. Не "я когда-то запустил команду и что-то установилось", а "вот файл, в нём написано что должно быть
установлено".
Сломал систему? Откатись на предыдущую версию одной командой. Купил новый компьютер? Скопируй конфиг, запусти rebuild, получи идентичную систему.
Подумал - почему бы не сделать то же самое для Raspberry Pi? Один раз написать конфиг, залить в git, и если SD-карта умрёт - пересобрать систему за 5 минут.
Так родился finite.
Что внутри
Скриншот с тестовой конфигурации - 69% блокировки из-за одного агрессивного домена. В реальности за 2 месяца использования держится около 20%.

- Pi-hole в контейнере - фильтрует DNS запросы по спискам известных рекламных и трекерных доменов
- Unbound - локальный DNS резолвер, кэширует запросы и шифрует трафик к внешним серверам через DNS-over-TLS
- Podman вместо Docker - работает без демона, проще интегрируется с systemd
- Статический IP, SSH только по ключам, минимальный файрвол
Вся конфигурация - один файл settings.nix. Меняешь там свой IP, пароль, часовой пояс. Запускаешь make build_image. Получаешь готовый образ для SD-карты.
Unbound: локальный DNS с кэшированием
Pi-hole блокирует рекламу и кэширует DNS через встроенный dnsmasq. Но есть нюанс - все "хорошие" запросы он пересылает на upstream сервер (обычно Google или Cloudflare). Они видят каждый домен который ты посещаешь.
Unbound решает это. Он умеет работать как recursive resolver - сам ходит к корневым DNS серверам и резолвит домены по цепочке. Плюс шифрует трафик через DNS-over-TLS. Никто не видит полный список твоих запросов.
Бонус - более агрессивное кэширование и prefetch популярных доменов.
Unbound решает это локальным кэшированием. Первый запрос к домену идёт наружу. Все последующие - отдаются из памяти мгновенно.
Вот реальные цифры с моей машины:
# Первый запрос - идёт к внешнему серверу
$ dig github.com @192.168.50.2 | grep "Query time"
;; Query time: 51 msec
# Второй запрос - из кэша
$ dig github.com @192.168.50.2 | grep "Query time"
;; Query time: 5 msec
51ms против 5ms. В десять раз быстрее. Для сайтов которые ты открываешь часто - разница ощутимая.
А вот что происходит когда запрашиваешь заблокированный домен:
$ dig analytics.tiktok.com @192.168.50.2
;; ANSWER SECTION:
analytics.tiktok.com. 2 IN A 0.0.0.0
;; Query time: 8 msec
Pi-hole возвращает 0.0.0.0 - запрос даже не выходит из локальной сети. Трекер не получает ничего.
Бонус: приватность
Есть ещё один аргумент за локальный Unbound - приватность.
Когда ты используешь DNS от Google, Cloudflare или других напрямую, они видят каждый домен который ты посещаешь. Это в том числе используется для анализа активности в сети и таргета той-же рекалмы.
Unbound умеет работать в режиме recursive resolver - он сам ходит к корневым DNS серверам и резолвит домены по цепочке, не доверяя никому. В finite я настроил компромиссный вариант: Unbound шифрует запросы через DNS-over-TLS к
Cloudflare. Не идеально, но лучше чем plain text DNS который может читать твой провайдер.
Проблема курицы и яйца
Первая версия собралась, записалась на карту, Pi загрузился. Веб-интерфейс Pi-hole открывается. Красота!
Только блоклисты пустые. И статистика нулевая. Ничего не фильтруется.
Два часа дебага. Логи чистые - никаких ошибок. Сервисы запущены. Всё выглядит нормально. Но не работает.
Потом дошло.
Pi-hole при первом запуске должен скачать списки рекламных доменов из интернета. Чтобы что-то скачать из интернета, нужен DNS. Но Pi-hole и есть DNS-сервер. И он настроен использовать сам себя для резолва.
Классическая проблема курицы и яйца. DNS-сервер не может запуститься, потому что ему нужен DNS-сервер.
Решение получилось костыльным: при первой загрузке специальный скрипт временно переключает DNS на Cloudflare, качает блоклисты, и переключает обратно на localhost. Systemd запускает этот скрипт один раз, создаёт файл-метку, и
при следующих загрузках пропускает.
Звучит просто. На практике - час разбирался с порядком запуска сервисов в systemd.
Вторая курица, второе яйцо
Заработало! Блоклисты скачались. Pi-hole фильтрует.
Перезагрузил Pi для проверки. Unbound упал с TLS ошибкой. Сертификат недействителен.
Ещё час дебага. Оказалось - Raspberry Pi не имеет батарейки для часов. При каждой загрузке время сбрасывается на 1 января 1970 года. TLS сертификаты "ещё не действительны", Unbound отказывается работать.
Окей, нужно синхронизировать время через NTP перед запуском Unbound. Но NTP сервер указан по домену time.cloudflare.com. А DNS ещё не работает, потому что Unbound не запустился. Потому что время неправильное. Потому что NTP не
сработал. Потому что DNS не работает.
Опять курица и яйцо!
Решение тупое: захардкодить IP-адреса NTP серверов. 162.159.200.1 вместо time.cloudflare.com. Никакого DNS, просто чистый IP. Работает.
Вывод: всё что должно работать до DNS - указывай по IP-адресам.
Что получилось в итоге
После недели вечеров и ~20 перепрошивок SD-карты - работающая система.
Реклама блокируется на всех устройствах. Samsung перестал показывать баннеры в меню. YouTube на телевизоре стал чище (прероллы всё ещё есть - они приходят с того же домена что и видео, DNS их не отличит).
Статистика за месяц: ~20% запросов с моей сети - к рекламным и трекерным доменам. Все заблокированы.
Железо и затраты
- Raspberry Pi 4 (4GB) - ~€60
- SD-карта 32GB - ~€10
- Ethernet кабель - был
- Время - неделя вечеров
Про монетизацию
Проект бесплатный и open source под MIT лицензией. Монетизировать не планирую.
Основная аудитория - люди как я 12 лет назад. Которые хотят избавиться от рекламы, имеют Raspberry Pi в ящике, но не хотят разбираться в Linux неделю. Записал образ, воткнул, поменял одну настройку в роутере - работает.
Что дальше
- Декларативные WhiteList и BlackList домены прямо в конфиге - чтобы не лезть в веб-интерфейс Pi-hole
- Документация по кастомизации блоклистов
- Мечта: веб-интерфейс где вводишь настройки, жмёшь кнопку, скачиваешь готовый ISO. Без git, без терминала, без боли.
Вопрос к клубу
Кто ещё ковыряется с домашней инфраструктурой на NixOS?
У меня конкретная проблема с секретами. Пароли лежат в settings.nix, который в .gitignore. Работает, но:
- При git pull новых изменений приходится stash'ить локальный конфиг
- NixOS ожидает все файлы в репозитории - если файл в gitignore, при сборке на чистой машине его не найдёт
На десктопе использую SOPS, но для finite хочу максимально простую настройку - не заставлять пользователей разбираться с GPG.
Может кто-то решал подобное?
Советы тем кто пойдёт тем же путём
- Тестируй с нуля. Проблемы bootstrap появляются только при первой загрузке на чистой системе. Если итерируешь на работающей - никогда их не увидишь.
- Всё что до DNS - по IP-адресам. NTP, начальная загрузка данных, healthcheck'и - всё хардкодь.
- Тишина - худшая ошибка. Нет логов и ничего не работает? Скорее всего где-то DNS timeout. Он не падает с ошибкой, просто висит и ждёт.
- NixOS того стоит. Кривая обучения крутая, но для серверов которые должны "просто работать" - идеально. Один раз настроил, задокументировал в коде, забыл на годы.
GitHub: https://github.com/wh1le/finite
Блог с техническими деталями: https://wh1le.com/finite
