The Jerusalem PostUnwanted endorsement: Political ad causes uproar at Jerusalem's Lev Academic Center - opinionPunchTope Alabi threatens legal action over Toluwanisings’ death claimsCNN TürkSavaşta sona doğru mu? İkinci perdede Trump'tan kritik adım!Daily MaverickResponse to sardine mortality must be grounded in science and evidenceESPNUpsets and chaos reign supreme in college football's Week 8한겨레잠수교, 50년만에 보행전용교로…26일 0시부터 차량 통행 금지ZDF heuteAktuelle Pressemitteilungen des ZDFCumhuriyetRAMS Park'a gidecek mi? Aziz Yıldırım, derbi için kararını verdiRapplerWhat schools will do amid intense heat during El NiñoTRT HaberTürkçülük fikrinin mimarı: Ziya GökalpCNN بالعربيةمن يقطينتين إلى 400.. كيف حوّل هذا الرجل منزله إلى مزار للهالوين؟Lenta.ruБывший военный предрек поражение Зеленскому
The Daily Newsstand · Free, Always
Sunday, October 25, 2026

Бесплатный блокировщик рекламы: DNS-парсер без библиотек, VPN, который всё ломает, и реклама блогеров с нулевой секунды

Translate

Недавно писал про Качалку — программу, которая скачивает видео. Следующая боль ещё проще: реклама. На сайтах — баннеры и всплывашки. На YouTube из России обычной рекламы от самого YouTube почти не осталось, зато блогеры вшивают её прямо в ролик: минута про банк, маркетплейс, сервис объявлений. Её не вырезать никаким блокировщиком, потому что она часть видео.

Так появилось «Чисто»: одна ссылка, на ней кнопка под каждое устройство. Бесплатно, без регистрации и без аккаунта, код открыт. Ниже — как это устроено и на какие грабли я наступил.

Знак у «Чисто» — мангуст против кобры. Мангуст славится тем, что змеиный яд на него почти не действует. Реклама, трекеры и принудительные подписки — такой же яд для привычного сайта, а «Чисто» должно вести себя как мангуст: спокойно делать своё и не бояться.

Как это устроено

Одной программы на iPhone, Android и компьютер не бывает: у каждой платформы свой способ влезть в сеть. Общее у всех — DNS. Телефон спрашивает у моего сервера, где находится ad.doubleclick.net. Домен в списке — отвечаю 0.0.0.0, и баннер просто не грузится. Иначе пересылаю вопрос публичному DNS. Через сервер идут только имена сайтов, сами страницы — напрямую.

  • iPhone — профиль DNS: скачал, ввёл код-пароль, готово. Работает во всех приложениях.

  • Android — «Безопасный DNS» в Chrome: вставил адрес, три касания.

  • Компьютер — расширение для Chrome, Яндекс Браузера, Edge и Firefox.

  • С VPN — отдельная вкладка, про неё ниже.

  • Telegram — бот с теми же кнопками, расширение присылает файлом.

DNS видит только домен. Рекламу с адреса самого сайта (site.ru/ads/banner.js) он не отличит от статьи — это работа расширения: оно режет по пути и типу файла и прячет пустые места.

Стена №1: 10 мс на запрос

У бесплатного воркера Cloudflare на один запрос 10 мс процессора. А в списке рекламных доменов 222 980 строк. Разбирать такой список из хранилища на каждом холодном старте — не успеть.

Решение — вшить список прямо в код. Это JSON на 1,7 МБ в сжатом виде при лимите 3 МБ. Он разбирается один раз при запуске, дальше проверка домена — пара обращений к Set:

import LIST from './blocklist.json';
const BLOCK = new Set(LIST.block);

// ads.tracker.example.com → проверяю сам домен и всех родителей
function isBlocked(name) {
  for (let d = name; d; d = d.slice(d.indexOf('.') + 1 || d.length)) {
    if (BLOCK.has(d)) return true;
  }
  return false;
}

Отвечает воркер в среднем за 127 мс, Cloudflare напрямую — за 98 мс. Разница только на первом заходе на сайт: дальше телефон берёт ответ из своей памяти. А так как реклама перестаёт грузиться, страницы часто открываются даже быстрее.

Грабли с маскировкой. Трекеры давно прячутся под поддоменом самого сайта: stat.site.ru на самом деле ведёт на site.tracker.net. По имени такой не поймать. Поэтому сервер разбирает ответ публичного DNS и проверяет, куда ведёт цепочка. Сначала я проверял только CNAME и забыл, что тот же трюк делается через DNAME и через запись HTTPS — теперь смотрю все три.

Стена №2: DNS-пакет, который читается по кругу

DNS-ответы я собираю руками по RFC 1035, без библиотек. Потом сел и прогнал по коду проверку, как будто его писал кто-то другой. Нашлось три дыры.

  • Петля. В DNS имя может ссылаться на кусок, который уже был в пакете раньше. Ссылка на саму себя давала петлю. Код упирался в счётчик и возвращал половину имени, которая шла дальше как нормальная. Теперь ссылка принимается только назад:

const ptr = ((len & 0x3f) << 8) | buf[off + 1];
if (ptr >= off) return null; // вперёд или на себя — мусор
  • Точка внутри имени. Кусок имени в DNS — это просто байты, и в нём может стоять точка. Один кусок ad.doubleclick склеивался в строку, неотличимую от двух. Это либо ложная блокировка, либо обход списка. Такие запросы теперь отклоняются.

  • Два вопроса в одном запросе. Я отвечал на первый, а счётчик вопросов копировал из запроса — ответ получался битым. Теперь принимаю ровно один вопрос.

На каждую дыру — тест.

Стена №3: кнопка, которая отключила бы интернет

В Android-приложении была удобная кнопка «Включить Частный DNS» — без VPN, во всех приложениях сразу. Я перечитывал код перед выкладкой APK и понял, что она сделала бы.

«Частный DNS» в Android работает по отдельному протоколу на порту 853. А Cloudflare Workers этот порт не умеет вообще. Человек нажал бы кнопку, Android начал бы ждать ответа, которого никогда не будет, — и не открылся бы ни один сайт. Причём человек даже не понял бы, что виновато моё приложение.

До людей это не дошло: APK ещё не был на сайте. Теперь блок «Частный DNS» появляется, только если сервер его поддерживает.

Вывод тот же, что с Качалкой: проверять надо не «работает ли у меня», а «что будет у человека, который нажмёт».

Стена №4: у всех включён VPN

Это я недооценил сильнее всего. Почти у каждого, кто сидит в YouTube из России, постоянно включён VPN: Shadowrocket, Happ, sing-box, v2rayN. А VPN-клиент забирает DNS себе. Профиль на iPhone в лучшем случае молча перестаёт работать, в худшем — сеть подвисает, потому что два механизма спорят, кто отвечает на вопрос «где сайт».

Времена такие: VPN и блокировщик у людей теперь стоят одновременно, и они не должны спорить. Бороться с туннелем бессмысленно, поэтому «Чисто» встраивается в него. На сайте появилась вкладка «Использую VPN»: выбираешь клиент — Shadowrocket, sing-box, Happ, V2Ray/Xray или Clash — и получаешь две вещи:

  • адрес DoH, который вставляется в настройки DNS самого клиента, копируется в одно касание;

  • там, где клиент это умеет, — готовый список доменов для правила REJECT и кусок конфига. Реклама режется внутри туннеля, и DNS вообще не участвует.

Списка два: облегчённый на 46 тысяч доменов и полный на 223 тысячи. По умолчанию предлагаю облегчённый — полный на iPhone заметно тяжелее для клиента. Профиль тем, кто выбрал VPN, сайт не предлагает вообще.

Стена №5: бот, которому Telegram повторяет одно и то же

Бот живёт в том же воркере. Если он отвечает Telegram ошибкой, тот считает сообщение недоставленным и присылает его снова, и снова. Кривое сообщение о платеже роняло код — и начинался круг.

Правило простое: Telegram всегда получает «ок», даже на сломанном сообщении, а ошибку я разбираю у себя. Заодно бот перестал отвечать в группах на обычный текст — там он слушает только свои команды.

Стена №6: а если сервер угонят?

Расширение умеет больше, чем помещается внутрь. В Chrome лимит — 30 000 встроенных правил, а в списках их около 130 тысяч. Поэтому внутри лежит 29 499 правил, а ещё 28 999 расширение раз в 12 часов забирает с сервера. Новое правило доезжает до всех без новой версии расширения.

Удобно, пока сервер мой. Но правила с сервера умеют не только блокировать: они могут перенаправлять страницы и снимать защиту сайта. А хитрый CSS вроде input[value^="a"]{background:url(...)} по одной букве утаскивает то, что человек вводит в поле. Если бы мой аккаунт Cloudflare угнали, это ударило бы по всем сразу.

Поэтому всё, что приходит с сервера, проходит фильтр: только «заблокировать», «пропустить» и подмена на пустышки внутри самого расширения. CSS без url(, @import и селекторов по значению поля. Проверил на настоящих правилах: из 28 999 фильтр не выкинул ни одного.

Мелкие грабли: регулярка на 2 КБ. После выкладки на странице расширений в Chrome появилась ошибка: правило 262 из списка AdGuard пропущено, потому что \.com\/jsonp\.php\?[A-Za-z0-9]{300,} после компиляции не влезает в 2 КБ памяти. Расширение работало, но красная надпись пугает. Теперь при сборке выкидываю регулярки с повтором от 100, а все оставшиеся прогоняю через declarativeNetRequest.isRegexSupported в самом браузере: 13 из 13 проходят.

Стена №7: блокировщик, который тормозит сильнее рекламы

Сетевые правила в расширении — это DeclarativeNetRequest: их применяет сам браузер, и они почти ничего не стоят. А вот скрытие пустых мест — CSS, и общий файл весил 600 КБ. Он вставлялся в каждую страницу в момент открытия, и на тяжёлых сайтах это было видно глазом.

Самыми дорогими оказались селекторы по подстроке вроде [class*="ad-"]: браузер проверяет их на каждом элементе при каждом изменении страницы. Из общего файла я убрал 532 таких селектора — по подстроке теперь прячу только на конкретных сайтах. Правила конкретного сайта вставляются сразу, общий файл — после DOMContentLoaded. А 2 МБ правил по сайтам разбиты на 32 файла: расширение читает один-два, а не всё целиком.

Главное: реклама внутри роликов

Ради этого всё и затевалось. Вырезать интеграцию блогера нельзя, но её можно промотать. Где она, расширение узнаёт из трёх мест.

База SponsorBlock. Это открытая база, где люди уже отметили миллионы рекламных кусков в роликах YouTube. Подвох: если спросить «где реклама в ролике abc123», база узнает, что ты смотришь. Поэтому спрашиваю только по первым 4 символам хеша от номера ролика. Под них подходят тысячи разных видео, а нужное выбираю у себя:

const prefix = (await sha256Hex(id)).slice(0, 4);
const list = await fetch(`${API}/skipSegments/${prefix}`).then((r) => r.json());
const hit = list.find((v) => v.videoID === id);

Таймкоды в описании. По закону рекламу обязаны помечать, и многие блогеры сами пишут «02:15 Реклама» или «05:00 Партнёр выпуска». Строка с таким словом — начало, следующий таймкод — конец. Если следующего нет или кусок длиннее 10 минут, не трогаю: лучше не промотать рекламу, чем промотать половину ролика.

Своя отметка. Нет вставки нигде — в углу плеера есть «Отметить рекламу». Нажал в начале, нажал в конце, и у тебя она пропускается сразу. А по кнопке уходит в SponsorBlock и пропускается у всех.

Грабли: реклама с нулевой секунды. Многие начинают ролик сразу с интеграции. Ответ SponsorBlock приходит через сеть, а плеер стартует раньше. Это место я чинил четыре раза.

  1. Первая версия. Проверка висела на событии timeupdate, которое браузер шлёт примерно 4 раза в секунду, а запрос к базе уходил, когда плеер уже появился. Первые 5–10 секунд рекламы человек успевал увидеть — ровно то, от чего хотел избавиться.

  2. Вторая. Запрос к базе ушёл в момент открытия страницы, ещё до плеера. Если ролик стартует с нуля, а ответа нет, плеер держался на паузе до 350 мс, дальше проверка шла на каждом кадре через requestVideoFrameCallback. У меня всё перематывалось до первого кадра, но под VPN реклама всё равно шла 5 секунд: столько занимал сам ответ базы.

  3. Третья. Спрашивать ещё раньше и ждать дольше, пока это незаметно. Соединение с SponsorBlock открывается сразу через preconnect — на медленном VPN одно TLS-рукопожатие легко занимает секунду. Ответы кэшируются, а если навести мышь на ролик в ленте и задержаться на 150 мс, расширение спрашивает про него заранее — к клику ответ уже лежит в кэше. Уходят те же 4 символа хеша. Пока кадра ещё нет и плеер сам грузится, жду до 1,5 секунды.

  4. Четвёртая. Пауза спорила с плеером YouTube: в логах он то снимал её сам, то ставил обратно, и был риск, что ролик так и останется стоять. Теперь паузы нет: пока ждём ответа, ролик прозрачный и без звука, а выглядит это как обычная загрузка. Как только ответ пришёл и вставка начинается не позже 0,5 секунды, перематываю на её конец до первого кадра.

Замерил на ролике, где интеграция идёт с 0:00 до 0:20, подменяя задержку ответа базы. Было: 5–10 секунд рекламы. Стало: при ответе до 1,5 секунды — ни одного кадра, плеер сразу открывается на 0:20; при ответе через 3,5 секунды — видно меньше половины секунды. На ещё трёх роликах того же автора с рекламой с нуля (0:00–0:31, 0:00–0:20, 0:00–0:17) плеер открывается сразу после неё. Вставку в середине ролика плеер перепрыгивает сразу. Честная оговорка: YouTube иногда сам возвращает звук, так что при медленной базе звук рекламы может проскочить на секунду.

Проверил на живом ролике: на 22-й секунде плеер сам перепрыгнул на 81-ю, а в углу появилось «Пропущена реклама 0:22–1:21 · база SponsorBlock · Вернуть». Кнопка «Вернуть» на случай, если промотало лишнее.

Сколько это стоит и почему оно не упадёт

Бесплатный тариф Cloudflare Workers — 100 000 запросов в сутки на весь аккаунт. Звучит много, пока не посчитаешь: телефон делает тысячи DNS-запросов в день, и лимит закончится на нескольких десятках телефонов. После этого фильтр перестал бы отвечать у всех сразу — а вместе с ним и интернет у тех, кто поставил профиль.

Что я сделал:

  • TTL. Заблокированный адрес раньше жил в кэше телефона 10 секунд, и телефон переспрашивал про один и тот же трекер шесть раз в минуту. Теперь — час: реклама за час никуда не переедет. Обычным адресам поднял нижнюю границу до 5 минут — CDN любят отдавать 20–60 секунд.

  • Лимит на адрес. Было 600 запросов в минуту с одного IP — при таком лимите один человек сжигал суточную норму за три минуты. Стало 120: обычному телефону с кэшем хватает с запасом.

  • Свой сервер для DNS. Телефоны теперь ходят на отдельный сервер, а воркер остался для сайта, бота и как запасной вход. Код тот же самый — worker.js запускается под Deno, снаружи его видно только через Cloudflare Tunnel: входящих портов нет, IP сервера не светится. Служба ограничена одним ядром и 400 МБ памяти, чтобы не мешать соседям по серверу, а раз в минуту сторож проверяет, что DNS отвечает и сам, и через туннель, и перезапускает то, что упало.

  • Кэш на сервере. Ответы публичного DNS держу в памяти столько, сколько разрешает их TTL: повторный вопрос про habr.com — 1 мс вместо 500.

  • Сколько выдерживает. Нагрузочный тест на одном ядре: 4 400 запросов в секунду, медиана 1 мс, 95% — 7 мс, памяти 80 МБ. Без кэша — 2 600 в секунду. Телефон в среднем делает меньше одного запроса в секунду, так что это тысячи телефонов. Цена — лишний прыжок через туннель: с тестового сервера в Европе ответ приходит за 136 мс против 67 мс у воркера. Телефон кэширует ответы, так что разницу видно только на первом заходе на сайт.

  • Расширению сервер не нужен. Все правила внутри, сервер нужен только раз в 12 часов за свежими.

Что хранится

  • На сервере — ничего. Нет журналов, нет базы, отладочные логи Cloudflare выключены. Лимит запросов Cloudflare держит в памяти, адреса никуда не пишутся.

  • В боте — ничего. Он не ведёт ни списков пользователей, ни истории.

  • В расширении всё лежит в браузере. Наружу уходят только 4 символа хеша ролика к SponsorBlock.

  • Чего я не могу обещать: через сервер проходят имена сайтов, и дальше их видит публичный DNS, которому я пересылаю вопрос, — сейчас это Cloudflare и Google. Если это неприемлемо, сервер поднимается у себя, адрес пересылки меняется одной строкой.

Чего «Чисто» не умеет

  • Официальные приложения YouTube, TikTok и Instagram — их реклама идёт с тех же серверов, что и видео. Пропуск интеграций работает в браузере на компьютере.

  • Свежий ролик, который ещё никто не отметил, — первые зрители рекламу увидят.

  • Android «Частный DNS» — пока нет: нужен отдельный порт, а он есть не везде.

  • Расширения пока нет в магазинах: ставится из архива с сайта или файлом из бота.

Если однажды я перестану этим заниматься

Бесплатно — пока я жив и сервер работает. Если перестанет — код открыт целиком: сайт, сервер, расширение, списки. Поднять свой «Чисто» — одна команда docker compose up -d на любом сервере или wrangler deploy на своём аккаунте Cloudflare. Инструкция в README.

Что поменялось с первой версии

Версия

Что исправлено

0.1.0

Запуск: DNS на воркере, профиль iPhone, Chrome на Android, расширение, бот

0.2.0

Дыры в DNS-парсере, бот без повторов от Telegram, фильтр правил с сервера, Android без «Частного DNS», пропуск интеграций на YouTube

0.2.1

Свой сервер для DNS с кэшем и сторожем, TTL и лимиты, вкладка «Использую VPN», лёгкий общий CSS, реклама с нуля под медленным VPN

0.2.2

Старт ролика без паузы, тяжёлые регулярки больше не попадают в правила

Чтобы не проверять руками

После каждого изменения GitHub сам прогоняет 32 теста и собирает APK для Android. Своего Android SDK у меня нет, так что APK я ни разу не собирал сам — как и exe для Качалки. А раз в 6 часов отдельная проверка стучится в живой сервер: реклама должна блокироваться, обычные сайты — открываться. Если что-то упадёт, придёт письмо, а не сообщение от друга со скрином.

Поставить: chisto.flownote.cc. Бот — @chisto_ads_bot, исходники — github.com/kozliq/chisto.

Если знаете сайты, где реклама пролезает или что-то ломается, — киньте ссылку в комментарии, добавлю правило.

Качалка и «Чисто» — не последние. Есть ещё пара вещей, которые в Рунете почему-то принято делать с подписками на каналы и баннерами. Расскажу, когда будет что показать.

А тем, кто привык зарабатывать на навязчивых баннерах и платных стенах там, где можно обойтись без них, — привыкайте: пока у меня есть время, этих мест будет становиться всё меньше.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.