К контенту
habrforever
СКРЫТА #router полный ремейк 2026 2015 · ~16 мин чтения

Прозрачный обход блокировок в домашней сети

автор оригинала: Zolg

оригинал на Хабре habr.com/…/270657
снимок Wayback web.archive.org
статус на Хабре (ноябрь 2026) скрыта (403/451)
права © автор, переиздание в архивных целях с указанием источника

Прозрачный обход блокировок в домашней сети: ремейк гайда 2015 года

Архивное переиздание с полной переработкой. Оригинал «Прозрачный обход блокировок в домашней сети» (Zolg, 2015) скрыт на Habr — доступен через Wayback Machine. В 2015-м это была экзотика для гиков с OpenWrt; в 2026-м — самый массовый сценарий в РФ: «поставил на роутер, чтобы дома всё работало само». Идея оригинала («в туннель — только заблокированное, остальное напрямую») не просто жива, она стала стандартом. Ниже — как собрать её на железе и софте 2026 года. Оригинальный текст — в конце, без купюр.

TL;DR — версия 2026

Схема та же: роутер → политика «заблокированные домены/IP в туннель, остальное напрямую». Заменить нужно три вещи: OpenWrt 15 → 24.10/25.x (DSA), iptables → nftables, OpenVPN/Tor → sing-box/Xray в TUN-режиме с VLESS+REALITY или ss2022 (голые VPN-хендшейки режут ТСПУ). Плюс обязательный четвёртый пункт, которого в 2015-м не было: свой DoH/DoT на самом роутере и явная политика для IPv6/QUIC.

Почему идея 2015 года оказалась вечной

Оригинал формулировал три ограничения, которые не устарели:

  1. Не гнать весь трафик через VPN — скорость проседает, сервисы дороги, а «бесплатные» логины кончаются;
  2. Не гнать через Tor — медленно и торренты через Tor — плохая идея;
  3. Не обеспечивать анонимность — «любая внешняя ссылка раскрывает настоящий IP», писал автор, и был прав: политика маршрутизации ≠ приватность.

Поэтому «прозрачный» ≠ «весь трафик в туннель». Прозрачный — значит пользователь ничего не настраивает: роутер сам решает, куда пакет. В 2026 к этому добавился четвёртый принцип: решение принимает роутер, а список «что считать заблокированным» обновляется сам (иначе вы статонетесь администратором собственного дома).

Что изменилось в железе и софте

2015 2026
OpenWrt Chaos Calmer (15.05), netfilter/iptables OpenWrt 24.10/25.x, DSA, nftables, luci-app-sing-box
«поднять на роутере VPN по инструкции провайдера» чужой VPN-сервис в РФ заблокирован: 258 сервисов за 2025 год
Tor / OpenVPN AmneziaWG, VLESS+REALITY, ss2022 за CDN
реестр заблокированных — вручную community-листы (antifilter-подобные) + cron-обновление
DNS от провайдера DoH/DoT на роутере — обязательно
«заблокировали по IP» блокируют по IP, SNI, DPI и «замедляют» по UDP/443
IPv6 почти не встречался IPv6 у части провайдеров не фильтруется — это и дыра, и шанс

Ещё одно изменение не техническое: почти все VPN-провайдеры, на чьих «пошаговых инструкциях для OpenWrt» строился оригинал, в РФ либо заблокированы, либо недоступны для оплаты. Значит туннель теперь свой: VPS за $3–5/мес и 10 минут на конфиг.

Архитектура 2026

[ телефоны / ТВ / ноутбуки / гости ]
            │ DHCP: DNS = 192.168.1.1 (роутер)
            ▼
   ┌─────────────── OpenWrt 24.10 ───────────────┐
   │  dnsmasq → AdGuard Home/https-dns-proxy(DoH)│
   │  sing-box (TUN) + routing-политика          │
   │  nftables: mark по address-list             │
   │  cron: обновление списка доменов 1×/сутки   │
   └───────────────┬─────────────────────────────┘
                   │ «заблокированное» → VLESS/REALITY (свой VPS)
                   │ остальное → напрямую к провайдеру
                   ▼
              Интернет

Ключевое отличие от 2015-го: в туннель уходит не «весь трафик», а только то, что в списке, и туннель при этом маскирован — DPI не видит ни хендшейка, ни « VPN-подозрительного » потока.

Сборка: пять шагов

Шаг 1. Прошивка и база

OpenWrt 24.10 или 25.x под ваше железо; для «из коробки» подходят GL.iNet (Beryl AX / Slate AX) с предустановленным OpenWrt и поддержкой AmneziaWG. Первым делом — сменить пароль, обновить opkg update, поставить luci-app-sing-box/sing-box, https-dns-proxy (или AdGuard Home в контейнере на ARM-моделях), curl, nftables.

Шаг 2. DNS на роутере (обязательно)

opkg install https-dns-proxy luci-app-https-dns-proxy
# в LuCI: Network → DHCP and DNS → Forwarder = 127.0.0.1#5053
#         Services → HTTPS DNS Proxy → upstream = Cloudflare/AdGuard DoH

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

Шаг 3. sing-box в TUN-режиме

/etc/sing-box/config.json (упрощённо, outbound — ваш VPS):

{
  "inbounds": [{ "type": "tun", "tag": "tun-in",
                  "address": ["172.19.0.1/30"],
                  "auto_route": false, "strict_route": false }],
  "outbounds": [
    { "type": "vless", "tag": "proxy",
      "server": "<VPS>", "server_port": 443, "uuid": "<uuid>",
      "flow": "xtls-rprx-vision",
      "tls": { "enabled": true, "server_name": "www.microsoft.com",
               "utls": { "enabled": true, "fingerprint": "chrome" },
               "reality": { "enabled": true, "public_key": "<key>",
                            "short_id": "<sid>" } } },
    { "type": "direct", "tag": "direct" }
  ],
  "route": {
    "rules": [
      { "rule_set": ["blocked-domains"], "outbound": "proxy" },
      { "network": "udp", "port": 443, "outbound": "direct" }
    ],
    "rule_set": [{ "tag": "blocked-domains", "type": "remote",
                   "format": "binary",
                   "url": "<ваш .srs-список>",
                   "update_interval": "1d" }],
    "auto_detect_interface": false,
    "default_outbound": "direct"
  }
}

Важно auto_route: false: маршрутизацию делает nftables (шаг 4), а не sing-box — иначе «прозрачность» превратится в «весь трафик в туннель», от чего автор оригинала предостерегал ещё в 2015-м.

Шаг 4. nftables: маркируем только нужное

table inet policy {
  set blocked4 { type ipv4_addr; flags interval; }
  set blocked6 { type ipv6_addr; flags interval; }

  chain prerouting {
    type filter hook prerouting priority mangle; policy accept;
    ip daddr @blocked4 meta mark set 1
    ip6 daddr @blocked6 meta mark set 1
    tcp dport 443 meta mark set 1      # SNI-блокировки тоже уходят в туннель
  }
}
ip rule add fwmark 1 lookup 100
ip route add default dev tun-in table 100

Списки @blocked4/@blocked6 наполняются cron-скриптом: тянем community-лист доменов → резолвим через DoH → обновляем set’ы через nft -f. Раз в сутки — достаточно.

Шаг 5. QUIC, IPv6, MTU

  • QUIC/UDP-443: либо режем его (nft reject udp dport 443), чтобы браузер ушёл в TCP, который мы маркируем; либо тоже отправляем в туннель — зависит от провайдера. Проверить просто: youtube в браузере с chrome://flags и без.
  • IPv6: если провайдер его выдаёт, политика должна быть симметричной, иначе «заблокированное» откроется по v6 мимо туннеля (или наоборот — v6 станет бесплатным обходом).
  • MTU: 1280 на TUN-интерфейсе + tcp-mss-fix в nftables — типичная причина «сайт открывается, но картинки не грузятся».

Проверка, что схема честно «прозрачная»

  1. С телефона (без всяких настроек) открыть заблокированный домен — должен открыться.
  2. curl ifconfig.me с того же телефона → ваш реальный IP, а не VPS (это и есть «остальное напрямую»).
  3. curl ifconfig.me --interface с роутера через mark 1 → IP VPS.
  4. tcpdump -i tun-in — трафик идёт только по списку, остальное мимо.
  5. dig с клиента → DNS роутера (192.168.1.1), а не провайдера.

Пункт 2 — лучшая проверка того, что вы не построили «ещё один always-on VPN», против которого автор оригинала спорил всю статью.

Ограничения (честно, как в оригинале)

  • схема не даёт анонимности: внешние ссылки, cookies, WebRTC по-прежнему палят настоящий IP —原文 писал это жирным, и в 2026 это правда;
  • DPI-блокировки «по статистике» и чистые IP-баны списком не лечатся — для них нужен ByeDPI на клиенте или смена IP VPS;
  • списки отстают от волн блокировок на часы — держите cron и локальную копию;
  • роутер должен тянуть TUN по CPU: 128 МБ ОЗУ и MT7621 хватает на 100 Мбит, для гигабита берите ARM/фильтр-коробку.

Что изменилось с 2015 года

  • OpenWrt 15 → 24.10/25.x: DSA вместо switch_vlan, nftables вместо iptables, LuCI-приложения для sing-box
  • чужой VPN-провайдер → свой VPS (258 сервисов заблокированы только за 2025 год, оплата картами РФ невозможна)
  • OpenVPN/Tor → VLESS+REALITY / ss2022 / AmneziaWG; голые хендшейки режутся ТСПУ
  • ручные списки → обновляемые rule-set’ы (.srs) с cron’ом
  • DNS и SNI стали полем боя: без DoH на роутере политика не работает
  • добавились IPv6 и QUIC/UDP-443 — в 2015-м их можно было игнорировать

Чек-лист 2026

  • свой DoH/DoT на роутере, DHCP раздаёт только 192.168.1.1
  • туннель маскирован (REALITY/ss2022/AWG), не «голый» WG/OpenVPN
  • в туннель уходит ТОЛЬКО список, остальное — напрямую
  • rule-set обновляется раз в сутки, есть локальная копия
  • QUIC и IPv6 явно решены (а не «само получилось»)
  • MTU 1280 + MSS-clamp
  • ifconfig.me с устройства = ваш IP (проверка прозрачности)

Вердикт

Автор 2015-го защищал простую мысль: «не платите скоростью и деньгами за то, что можно маршрутизировать точечно». Одиннадцать лет спустя ТСПУ, замедления и 258 заблокированных сервисов сделали его правоту очевидной: полностью-VPN-дом — это дорого и медленно, always-on-Tor — невозможно, а точечная политика на роутере — ровно то, что ставят сейчас. Мы заменили три буквы в конфигах, а не идею.


ОРИГИНАЛ СТАТЬИ (без изменений)

Последние новости в очередной раз заострили проблему блокировок интернет-ресурсов. С одной стороны о способах их обхода написано немало, и пережевывать эту тему в очередной раз казалось бы незачем. С другой, регулярно предпринимать какие-то дополнительные действия для посещения нужного ресурса — это не совсем то, что должно удовлетворить айтишника (и не всегда то, с чем может справится человек к айти неблизкий).

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

Сама собой напрашивается мысль о том, чтобы обходить блокировку уже на домашнем маршрутизаторе.

[]

Собственно, поднять на маршрутизаторе и гонять весь траффик через VPN несложно, а у некоторых VPN-провайдеров есть даже пошаговые инструкции по настройке OpenWrt на работу с ними.

Но скорости VPN сервисов все же отстают от скоростей доступа в интернет, да и VPN-сервис либо стоит денег, либо имеет массу ограничений, либо необходимость регулярного получения новых логинов. С точки зрения оптимизации затрат, как финансовых, так и временных, предпочтительней выглядит Tor, но его скорость еще хуже, а гонять через Tor торренты и вовсе идея не лучшая.

Выход — перенаправлять в VPN/Tor только траффик блокируемых ресурсов, пропуская остальной обычным путем.

Внимание: данная схема не обеспечивает анонимности просмотра заблокированных сайтов: любая внешняя ссылка раскрывает ваш настоящий IP.

Конкретная реализация на OpenWrt приведена в конце статьи. Если не интересуют подробности и альтернативные варианты решения, то можно листать сразу до нее.

Туннелирование и перенаправление траффика в туннель

Настройка VPN или Tor’а сложностей представлять не должна. Tor должен быть настроен, как прозрачный proxy (либо настроить связку из tor и tun2socks). Т.к. конечной целью явлется обход блокировок ркн, то в конфиге Tor’а целесообразно запретить использование выходных узлов на территории РФ (<ExcludeExitNodes {RU}).

В Tor’а траффик перенаправляется правилом с REDIRECT’ом на порт прозрачного прокси в цепочке PREROUTING таблицы nat netfilter’а.

Для перенаправления в VPN (или Tor + tun2socks) траффик маркируется в таблице mangle, метка затем используется для выбора таблицы маршрутизации, перенаправляющей траффик на соответствующий интерфейс.

В обоих случаях для классификации траффика используется ipset с хостами, подлежащими (раз)блокировке.

Формирование ipset c (раз)блокируемыми хостами

К сожалению, вариант «загнать все IP из реестра» в ipset не работает как хотелось бы: во-первых в списках присутствуют не все IP адреса блокируемых хостов, во-вторых в попытке уйти от блокировки IP адрес у ресурса может измениться (и провайдер об этом уже знает, а мы – еще нет), ну и в третьих – false positives для находящихся на том же shared hosting’е сайтов.

Городить огород с dpi того или иного вида не очень хочется: как-никак работать это должно на довольно слабом железе. Выход достаточно прост и в какой-то степени элегантен: dnsmasq (DNS сервер, который на маршрутизаторе скорее всего уже установлен) умеет при разрешении имен добавлять ip-адреса в соответствующий ipset (одноименная опция в конфиге). Как раз то, что нужно: вносим в конфиг все домены, которые необходимо разблокировать, и дальше по необходимости dnsmasq сам добавляет в ipset именно тот ip адрес, по которому будет идти обращение к заблокированному ресурсу.

У меня были сомнения, что dnsmasq запустится и будет нормально работать с конфигом в полдесятка тысяч строк (примерно столько записей в реестре после усушки и утряски), однако они к счастью оказались безосновательны.

Ложка дегтя в том, что при обновлении списка dnsmasq придется перезапускать, т.к. по SIGHUP он конфиг не перегружает.

Составление списка доменов

Должно происходить автоматически, насколько это возможно.

Первый вариант (который и реализован в примере): формировать список на основе единого реестра блокировок и обновлять его по cron’у.

Роскомнадзор широкой общественности реестр блокировок не предоставляет, однако мир не без добрых людей и есть минимум два ресурса, где с ним можно ознакомиться. И, что отлично, API у них тоже имеется. При разборе списка нужно учесть, что в списке доменных имен помимо собственно доменных имен присутствуют и IP адреса. Их нужно обрабатывать отдельно (или вообще на них забить: их примерно 0,1% от списка и врядли они ведут на интересующие вас ресурсы). Кириллические домены далеко не всегда представлены в punycode. Немалую часть списка занимают поддомены на одном домене второго уровня, указаны домены с www/без www и просто дублирующиеся записи. Все перечисленное в большей степени относится к списку от rublacklist.net (он в добавок еще и странно, местами некорректно, экранирован). Именно для него пришлось городить монструозный lua-script (приводится ниже), нормализующий и сжимающий список почти в два раза. C antizapret.info ситуация сильно лучше и можно было бы обойтись однострочником на awk.

Можно пойти другим путем: многие провайдеры при обращению к заблокированному ресурсу перенаправляют на заглушку об ограничении доступа. Например http://block.mts.ru/?host=/host/&url=/url/&params=/params/. Подменив с помощью того же dnsmasq (address=/block.mts.ru/192.168.1.1) A-запись block.mts.ru на адрес веб-сервера маршрутизатора (и разместив на нем несложный скрипт) можно локально формировать список запрошенных пользователями сети заблокированных ресурсов, добавлять их в конфиг dnsmasq, повторно делать nslookup (чтобы ip адрес добавился в ipset) и еще раз редиректить пользователя на первоначальный URL. Но необходимость каждый раз при этом перезапускать dnsmasq несколько расхолаживает. Да и работать будет только для http.

Теперь еще об одной ложке дегтя: некоторое провайдеры замечены за тем, что помимо включенных в список ркн сайтов самодеятельно блокируют и официально в списках не значащиеся. При этом блокируют тихой сапой и заглушки не выводят. Так что совсем без ручного привода не обойтись.

Дополнительные замечания

DNS серверы провайдера использовать в качестве апстрим серверов естественно не стоит. Ибо блокировка может произойти еще на стадии разрешения имени ресурса. Отдаст сервер провайдера на искомый адрес, что это CNAME block.mts.ru и все. Наиболее простое решение server=8.8.8.8, server=8.8.4.4. Модификации провайдерами DNS-ответов сторонних серверов лично я пока не наблюдал. В случае, если начнут — можно отправлять запросы доменов из запретного списка на другой апстрим (через VPN/Tor), однако без надобности я бы конфиг не раздувал.

При использовании Tor’a можно бонусом получить возможность серфинга по .onion сайтам: Tor при разрешении имени через встроенный dns-сервер отобразит его на виртуальный адрес из заранее заданного диапазона. Дальше нужно только перенаправить обращение к этому адресу на прокси Tor’а и voila. Но еще раз напомню, что анонимности подключение с избирательным туннелированием трафика не обеспечивает.

Реализация на OpenWrt (15.05)

Сам маршрутизатор должен быть не самый плохой, особенно при использовании Tor’а. MIPS 400MHz@32MB RAM это тот минимум, который стоит рассматривать.

При наличии USB-порта недостаток встроенного флеша можно компенсировать USB-флешкой (вообще мне представляется достаточно здравой идея не использовать встроенный флеш для регулярно перезаписываемых данных).

Штатно в прошивках OpenWrt содержится урезанный dnsmasq, не умеющий ipset. Необходимо заменить его на dnsmasq-full.

Из пакетов, по умолчанию не присутствующих, так же потребуются ipset, tor и tor-geoip.

Так же необходим либо пакет luasocket, либо (в режиме строгой экономии флеша) отдельно ltn12.lua в папке /usr/lib/lua. Для преобразования кириллических доменов из utf8 в punycode нужны idn.lua в /usr/lib/lua и пакет luabitop (либо отключить опции в конфиге скрипта).

Скрипт обновления списков блокировки

/usr/bin/rublupdate.lua

local config = {
    blSource = "antizapret", -- antizapret или rublacklist
    groupBySld = 32, -- количество поддоменов после которого в список вносится весь домен второго уровня целиком
    neverGroupMasks = { "^%a%a%a?.%a%a$" }, -- не распространять на org.ru, net.ua и аналогичные
    neverGroupDomains = { ["livejournal.com"] = true, ["facebook.com"] = true , ["vk.com"] = true },
    stripWww = true,
    convertIdn = true,
    torifyNsLookups = false, -- отправлять DNS запросы заблокированных доменов через TOR
    blMinimumEntries = 1000, -- костыль если список получился короче, значит что-то пошло не так и конфиги не обновляем
    dnsmasqConfigPath = "/etc/runblock/runblock.dnsmasq",
    ipsetConfigPath = "/etc/runblock/runblock.ipset",
    ipsetDns = "rublack-dns",
    ipsetIp = "rublack-ip",
    torDnsAddr = "127.0.0.1#9053"
}

local function prequire(package)
    local result, err = pcall(function() require(package) end)
    if not result then
        return nil, err
    end
    return require(package) -- return the package value
end

local idn = prequire("idn")
if (not idn) and (config.convertIdn) then
    error("you need either put idn.lua (github.com/haste/lua-idn) in script dir  or set 'convertIdn' to false")
end

local http = prequire("socket.http")
if not http then
    local ltn12 = require("ltn12")
end
if not ltn12 then
    error("you need either install luasocket package (prefered) or put ltn12.lua in script dir")
end

local function hex2unicode(code)
    local n = tonumber(code, 16)
    if (n < 128) then
        return string.char(n)
    elseif (n < 2048) then
        return string.char(192 + ((n - (n % 64)) / 64), 128 + (n % 64))
    else
        return string.char(224 + ((n - (n % 4096)) / 4096), 128 + (((n % 4096) - (n % 64)) / 64), 128 + (n % 64))
    end
end

local function rublacklistExtractDomains()
    local currentRecord = ""
    local buffer = ""
    local bufferPos = 1
    local streamEnded = false
    return function(chunk)
        local retVal = ""
        if chunk == nil then
            streamEnded = true
        else
            buffer = buffer .. chunk
        end

        while true do
            local escapeStart, escapeEnd, escapedChar = buffer:find("\\(.)", bufferPos)
            if escapedChar then
                currentRecord = currentRecord .. buffer:sub(bufferPos, escapeStart - 1)
                bufferPos = escapeEnd + 1
                if escapedChar == "n" then
                    retVal = currentRecord
                    break
                elseif escapedChar == "u" then
                    currentRecord = currentRecord .. "\\u"
                else
                    currentRecord = currentRecord .. escapedChar
                end
            else
                currentRecord = currentRecord .. buffer:sub(bufferPos, #buffer)
                buffer = ""
                bufferPos = 1
                if streamEnded then
                    if currentRecord == "" then
                        retVal = nil
                    else
                        retVal = currentRecord
                    end
                end
                break
            end
        end
        if retVal and (retVal ~= "") then
            currentRecord = ""
            retVal = retVal:match("^[^;]*;([^;]+);[^;]*;[^;]*;[^;]*;[^;]*.*$")
            if retVal then
                retVal = retVal:gsub("\\u(%x%x%x%x)", hex2unicode)
            else
                retVal = ""
            end
        end
        return (retVal)
    end
end

local function antizapretExtractDomains()
    local currentRecord = ""
    local buffer = ""
    local bufferPos = 1
    local streamEnded = false
    return function(chunk)
        local haveOutput = 0
        local retVal = ""
        if chunk == nil then
            streamEnded = true
        else
            buffer = buffer .. chunk
        end
        local newlinePosition = buffer:find("\n", bufferPos)
        if newlinePosition then
            currentRecord = currentRecord .. buffer:sub(bufferPos, newlinePosition - 1)
            bufferPos = newlinePosition + 1
            retVal = currentRecord
        else
            currentRecord = currentRecord .. buffer:sub(bufferPos, #buffer)
            buffer = ""
            bufferPos = 1
            if streamEnded then
                if currentRecord == "" then
                    retVal = nil
                else
                    retVal = currentRecord
                end
            end
        end
        if retVal and (retVal ~= "") then
            currentRecord = ""
        end
        return (retVal)
    end
end

local function normalizeFqdn()
    return function(chunk)
        if chunk and (chunk ~= "") then
            if config["stripWww"] then chunk = chunk:gsub("^www%.", "") end
            if idn and config["convertIdn"] then chunk = idn.encode(chunk) end
            if #chunk > 255 then chunk = "" end
            chunk = chunk:lower()
        end
        return (chunk)
    end
end

local function cunstructTables(bltables)
    bltables = bltables or { fqdn = {}, sdcount = {}, ips = {} }
    local f = function(blEntry, err)
        if blEntry and (blEntry ~= "") then
            if blEntry:match("^%d+%.%d+%.%d+%.%d+$") then
                -- ip адреса - в отдельную таблицу для iptables
                if not bltables.ips[blEntry] then
                    bltables.ips[blEntry] = true
                end
            else
                -- как можем проверяем, FQDN ли это. заодно выделяем домен 2 уровня (если в bl станут попадать TLD - дело плохо :))
                local subDomain, secondLevelDomain = blEntry:match("^([a-z0-9%-%.]-)([a-z0-9%-]+%.[a-z0-9%-]+)$")
                if secondLevelDomain then
                    bltables.fqdn[blEntry] = secondLevelDomain
                    if 1 > 0 then
                        bltables.sdcount[secondLevelDomain] = (bltables.sdcount[secondLevelDomain] or 0) + 1
                    end
                end
            end
        end
        return 1
    end
    return f, bltables
end

local function compactDomainList(fqdnList, subdomainsCount)
    local domainTable = {}
    local numEntries = 0
    if config.groupBySld and (config.groupBySld > 0) then
        for sld in pairs(subdomainsCount) do
            if config.neverGroupDomains[sld] then
                subdomainsCount[sld] = 0
                break
            end
            for _, pattern in ipairs(config.neverGroupMasks) do
                if sld:find(pattern) then
                    subdomainsCount[sld] = 0
                    break
                end
            end
        end
    end
    for fqdn, sld in pairs(fqdnList) do
        if (not fqdnList[sld]) or (fqdn == sld) then
            local keyValue;
            if config.groupBySld and (config.groupBySld > 0) and (subdomainsCount[sld] > config.groupBySld) then
                keyValue = sld
            else
                keyValue = fqdn
            end
            if not domainTable[keyValue] then
                domainTable[keyValue] = true
                numEntries = numEntries + 1
            end
        end
    end
    return domainTable, numEntries
end

local function generateDnsmasqConfig(configPath, domainList)
    local configFile = assert(io.open(configPath, "w"), "could not open dnsmasq config")
    for fqdn in pairs(domainList) do
        if config.torifyNsLookups then
            configFile:write(string.format("server=/%s/%s\n", fqdn, config.torDnsAddr))
        end
        configFile:write(string.format("ipset=/%s/%s\n", fqdn, config.ipsetDns))
    end
    configFile:close()
end

local function generateIpsetConfig(configPath, ipList)
    local configFile = assert(io.open(configPath, "w"), "could not open ipset config")
    configFile:write(string.format("flush %s-tmp\n", config.ipsetIp))
    for ipaddr in pairs(ipList) do
        configFile:write(string.format("add %s %s\n", config.ipsetIp, ipaddr))
    end
    configFile:write(string.format("swap %s %s-tmp\n", config.ipsetIp, config.ipsetIp))
    configFile:close()
end

local retVal, retCode, url

local output, bltables = cunstructTables()
if config.blSource == "rublacklist" then
    output = ltn12.sink.chain(ltn12.filter.chain(rublacklistExtractDomains(), normalizeFqdn()), output)
    url = "http://reestr.rublacklist.net/api/current"
elseif config.blSource == "antizapret" then
    output = ltn12.sink.chain(ltn12.filter.chain(antizapretExtractDomains(), normalizeFqdn()), output)
    url = "http://api.antizapret.info/group.php?data=domain"
else
    error("blacklist source should be either 'rublacklist' or 'antizapret'")
end

if http then
    retVal, retCode = http.request { url = url, sink = output }
else
    retVal, retCode = ltn12.pump.all(ltn12.source.file(io.popen("wget -qO- " .. url)), output)
end

if (retVal == 1) and ((retCode == 200) or (http == nil)) then
    local domainTable, recordsNum = compactDomainList(bltables.fqdn, bltables.sdcount)
    if recordsNum > config.blMinimumEntries then
        generateDnsmasqConfig(config.dnsmasqConfigPath, domainTable)
        generateIpsetConfig(config.ipsetConfigPath, bltables.ips)
        print(string.format("blacklists updated. %d entries.", recordsNum))
        os.exit(0)
    end
end
os.exit(1)
Настройки dnsmasq

/etc/dnsmasq.conf

server=/onion/127.0.0.1#9053
ipset=/onion/onion

conf-file=/etc/runblock/runblock.dnsmasq

Добавить в секцию dnsmasq /etc/config/dhcp

    list server '8.8.8.8'
    list server '8.8.4.4'
    list rebind_domain 'onion'
Настройки netfilter

Добавить в /etc/config/firewall

config ipset
	option name 'rublack-dns'
	option storage 'hash'
	option match 'dest_ip'
	option timeout '86400'

config ipset
	option name 'rublack-ip'
	option storage 'hash'
	option match 'dest_ip'

config ipset
	option name 'rublack-ip-tmp'
	option storage 'hash'
	option match 'dest_ip'

config ipset
	option name 'onion'
	option storage 'hash'
	option match 'dest_ip'
	option timeout '86400'

config redirect
	option name 'torify-blocked-dns'
	option src 'lan'
	option proto 'tcp'
	option ipset 'rublack-dns'
	option dest_port '9040'
	option dest 'lan'

config redirect
	option name 'torify-blocked-ip'
	option src 'lan'
	option proto 'tcp'
	option ipset 'rublack-ip'
	option dest_port '9040'
	option dest 'lan'

config redirect
	option name 'torify-onion'
	option src 'lan'
	option proto 'tcp'
	option ipset 'onion'
	option dest_port '9040'
	option dest 'lan'

Добавить в /etc/firewall.user

cat /etc/runblock/runblock.ipset | ipset restore
Настройки Tor

/etc/torrc

User tor
PidFile /var/run/tor.pid
DataDirectory /var/lib/tor
ExcludeExitNodes {RU}
VirtualAddrNetwork 10.254.0.0/16  # виртуальные адреса для .onion ресурсов
AutomapHostsOnResolve 1
TransPort 9040
TransListenAddress 127.0.0.1
TransListenAddress 192.168.1.1    #адрес LAN интерфейса
DNSPort 9053
DNSListenAddress 127.0.0.1
#AvoidDiskWrites 1 # в OpenWrt /var и так в RAM (tmpfs) не уверен, что в опции есть смысл

Осталось создать каталог /etc/runblock, разово запустить вручную скрипт lua /usr/bin/rublupdate.lua, убедиться, что он отработал без ошибок, добавить его в cron (пару раз в сутки — вполне достаточно) и забыть о роскомнадзоре. Ну до тех пор, пока не начнут блокировать тор, или сайты, публикующие реестр).

по теме #router