Последние новости о блокировке Российским пользователями доступа к рутрекеру в очередной раз заострили злободневную тему. По нашему мнению это вопиющее нарушение человеческих прав и свобод но мы не юристы чтобы доказывать это в суде. Как говорят в одной хорошой пословице на каждый хитрую «гайку» найдется болт с левой резьбой 🙂 Поэтому сегодня мы поговорим о том как посещать рутрекер через торент при этом путь к остальным ресурсам оставив без изменений.
Нужно простое и прозрачное для пользователей решение, которое, будучи единожды настроенным, позволит просто пользоваться интернетом, не задумываясь, что же сегодня заблокировали по заявкам очередных копирастов-плагиаторов.
Сама собой напрашивается мысль о том, чтобы обходить блокировку уже на домашнем роутере.
Собственно, поднять на роутере и гонять весь траффик через 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 адрес, по которому будет идти обращение к заблокированному ресурсу.
[ad name=»Responbl»]
У меня были сомнения, что 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>¶ms=<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 (либо отключить опции в конфиге скрипта).
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/%sn", fqdn, config.torDnsAddr)) end configFile:write(string.format("ipset=/%s/%sn", 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-tmpn", config.ipsetIp)) for ipaddr in pairs(ipList) do configFile:write(string.format("add %s %sn", config.ipsetIp, ipaddr)) end configFile:write(string.format("swap %s %s-tmpn", 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
[spoiler title=’Добавить в /etc/config/firewall’ style=’default’ collapse_link=’true’]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'[/spoiler]
Добавить в /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 (пару раз в сутки — вполне достаточно) и забыть о роскомнадзоре. Ну до тех пор, пока не начнут блокировать тор, или сайты, публикующие реестр).
3 comments On Как посещать рутрекер через торент
Pingback: Бесплатный VPN для андроид. Обзор сервисов. - Cryptoworld ()
Pingback: Как обойти блокировку торрентов. Погружение в SeedBOX. - Cryptoworld ()
Pingback: Обход блокировки TOR в Белорусии и других странах. - Cryptoworld ()