Обхід блокувань РКН за допомогою DNSTap та BGP

Обхід блокувань РКН за допомогою DNSTap та BGP

Тема досить виїжджена, знаю. Наприклад, є чудова стаття, але там розглядається лише IP-частина блокліста. Ми додамо ще й домени.

У зв'язку з тим, що суди та РКН блокують все праворуч і ліворуч, а провайдери посилено намагаються не потрапити під штрафи, виписані «Ревізорро» — супутні втрати від блокувань досить великі. Та й серед «правомірно» заблокованих сайтів багато корисних (привіт, rutracker)

Я живу поза юрисдикцією РКН, але на батьківщині залишилися батьки, родичі та друзі. Тож було вирішено вигадати легкий для далеких від ІТ особистостей спосіб обходу блокувань, бажано зовсім без їхньої участі.

У цій нотатці я не розписуватиму базові мережеві речі по кроках, а опишу загальні принципи як можна реалізувати цю схему. Так що знання як працює мережа взагалі і в Linux зокрема – must have.

Типи блокувань

Для початку освіжаємо в пам'яті що блокується.

У XML, що вивантажується, від РКН кілька типів блокувань:

  • IP
  • домен
  • URL

Ми їх зведемо для простоти до двох: IP і домен, а з блокувань по URL просто витягуватимемо домен (точніше за нас це вже зробили).

Хороші люди з Роскомсвободи реалізували прекрасний APIчерез який можна отримувати те, що нам потрібно:

Доступ до заблокованих сайтів

Для цього нам потрібен якийсь невеликий закордонний VPS, бажано з безлімітним трафіком - таких багато по 3-5 доларів. Брати потрібно в ближньому зарубіжжі, щоб пінг був не дуже великий, але знов-таки враховувати, що інтернет та географія не завжди збігаються. А оскільки ніякого SLA за 5 доларів немає - краще взяти 2 + штуки у різних провайдерів для стійкості до відмови.

Далі нам потрібно налаштувати зашифрований тунель від клієнтського роутера до VPS. Я використовую Wireguard як найшвидший і найпростіший у налаштуванні т.к. клієнтські роутери у мене також на базі Linux (APU2 або щось на OpenWRT). У разі будь-яких Mikrotik/Cisco можна використовувати доступні на них протоколи типу OpenVPN і GRE-over-IPSEC.

Ідентифікація та перенаправлення цікавого трафіку

Можна, звичайно, завернути взагалі весь інтернет трафік через зарубіжжя. Але, швидше за все, через це сильно постраждає швидкість роботи з локальним контентом. Плюс вимоги до смуги пропускання на VPS будуть значно вищими.

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

Для управління трафіком ми будемо використовувати протокол BGP та анонсувати маршрути до необхідних мереж із нашого VPS на клієнтів. Як BGP-демона візьмемо BIRD, як один із найбільш функціональних та зручних.

IP

Із блокуваннями по IP все зрозуміло: просто анонсуємо всі заблоковані IP з VPS. Проблема в тому, що підмереж у списку, який віддає API, близько 600 тисяч, і переважна більшість із них — це хости /32. Така кількість маршрутів може збентежити слабкі клієнтські роутери.

Тому було вирішено при обробці списку сумувати до мережі /24 якщо в ній 2 і більше хоста. Таким чином, кількість маршрутів скоротилася до ~100 тисяч. Скрипт буде для цього далі.

Домени

Тут складніше і методів є кілька. Наприклад, можна поставити прозорий Squid на кожному роутері клієнта і робити там перехоплення HTTP і підглядання в TLS-хендшейк з метою отримання запитуваного URL в першому випадку і домену з SNI в другому.

Але через будь-які новомодні TLS1.3+eSNI аналіз HTTPS з кожним днем ​​стає все менш реальним. Та й інфраструктура з боку клієнта ускладнюється - доведеться використовувати як мінімум OpenWRT.

Тому я вирішив піти шляхом перехоплення відповідей на DNS-запити. Тут теж над головою починає витати будь-який DNS-over-TLS/HTTPS, але цю частину ми можемо (поки що) контролювати на клієнті або відключити, або використовувати свій сервер для DoT/DoH.

Як перехоплювати DNS?

Тут також може бути кілька підходів.

  • Перехоплення DNS-трафіку через PCAP або NFLOG
    Обидва ці способи перехоплення реалізовані в утиліті sidmat. Але вона давно не підтримується і функціонал дуже примітивний, так що до неї потрібно все одно писати обв'язку.
  • Аналіз логів DNS-сервера
    На жаль, відомі мені рекурсори не вміють логувати відповіді, а лише запити. У принципі це логічно, тому що на відміну від запитів відповіді мають складну структуру і писати їх у текстовій формі важкувато.
  • DNSTap
    На щастя, багато хто з них вже підтримує DNSTap для цих цілей.

Що таке DNSTap?

Обхід блокувань РКН за допомогою DNSTap та BGP

Це клієнт-серверний протокол, заснований на Protocol Buffers і Frame Streams для передачі з DNS-сервера на колектор структурованих DNS-запитів та відповідей. По суті DNS-сервер передає метадані запитів та відповідей (тип повідомлення, IP клієнта/сервера і так далі) плюс повні DNS-повідомлення в тому (бінарному) вигляді, в якому він працює з ними по мережі.

Важливо розуміти, що у парадигмі DNSTap DNS-сервер виступає у ролі клієнта, а колектор – у ролі сервера. Тобто DNS-сервер підключається до колектора, а чи не навпаки.

На сьогоднішній день DNSTap підтримується у всіх популярних DNS-серверах. Але, наприклад, BIND у багатьох дистрибутивах (на зразок Ubuntu LTS) часто зібрано чомусь без його підтримки. Тож не морочитимемося перескладанням, а візьмемо легший і швидший рекурсор — Unbound.

Чим ловити DNSTap?

є деяке кількість CLI-утиліт для роботи з потоком DNSTap-подій, але для вирішення нашого завдання вони підходять погано. Тому я вирішив винайти свій велосипед, який робитиме все, що необхідно: dnstap-bgp

Алгоритм роботи:

  • При запуску завантажує з текстового файлу список доменів, інвертує їх (habr.com -> com.habr), виключає биті рядки, дублікати та піддомени (тобто якщо у списку є habr.com та www.habr.com) — буде завантажено тільки перший) і будує префіксне дерево для швидкого пошуку цього списку
  • Виступаючи в ролі DNSTap-сервера чекає на підключення від DNS-сервера. В принципі він підтримує як UNIX-так і TCP-сокети, але відомі мені DNS-сервера вміють тільки в UNIX-сокети
  • DNSTap-пакети, що надходять, десеріалізуються спочатку в структуру Protobuf, а потім саме бінарне DNS-повідомлення, що знаходиться в одному з Protobuf-полів, парситься до рівня записів DNS RR
  • Перевіряється чи є хост, що запитується (або його батьківський домен) у завантаженому списку, якщо ні — відповідь ігнорується
  • З відповіді вибираються тільки A/AAAA/CNAME RR та з них витягуються відповідні IPv4/IPv6 адреси
  • IP-адреси кешуються з TTL і анонсуються у всі налаштовані BGP-піри
  • При отриманні відповіді, що вказує на вже закешований IP, його TTL оновлюється
  • Після закінчення TTL запис видаляється з кешу та з BGP-анонсів

Додатковий функціонал:

  • Перечитування списку доменів по SIGHUP
  • Синхронізація кешу з іншими екземплярами dnstap-bgp через HTTP/JSON
  • Дублювання кеша на диску (в базі BoltDB) для відновлення його вмісту після перезапуску
  • Підтримка перемикання в інший network namespace (навіщо це потрібно буде описано нижче)
  • Підтримка IPv6

обмеження:

  • IDN домени поки не підтримуються
  • Мало налаштувань BGP

Я зібрав RPM та DEB пакети для зручного встановлення. Повинні працювати усім відносно нових OS з systemd, т.к. залежностей у них немає.

Схема

Отже, приступимо до збирання всіх компонентів докупи. В результаті у нас має вийти приблизно така мережева топологія:
Обхід блокувань РКН за допомогою DNSTap та BGP

Логіка роботи, гадаю, зрозуміла з діаграми:

  • У клієнта налаштований наш сервер як DNS, причому DNS запити теж повинні ходити за VPN. Це потрібно для того, щоб провайдер не міг використовувати перехоплення DNS для блокування.
  • Клієнт при відкритті сайту посилає DNS-запит виду «а які IP у xxx.org»
  • непов'язаний резолвіт xxx.org (або бере з кеша) і відправляє відповідь клієнту «у xxx.org такі IP», паралельно дублюючи його через DNSTap
  • dnstap-bgp анонсує ці адреси в BIRD по BGP у тому випадку, якщо домен є в списку заблокованих
  • BIRD анонсує маршрут до цих IP з next-hop self клієнтського роутера
  • Наступні пакети від клієнта до цих IP йдуть вже через тунель.

На сервері для маршрутів до заблокованих сайтів у мене всередині BIRD використовується окрема таблиця і з ОС вона не перетинається.

У цій схемі є недолік: перший SYN пакет від клієнта, швидше за все, встигне піти через вітчизняного провайдера. маршрут анонсується не миттєво. І тут можливі варіанти в залежності від того, як провайдер робить блокування. Якщо він просто тремтить трафік, то проблем немає. А якщо він редиректит його на якийсь DPI, то (теоретично) можливі спецефекти.

Також можливі дива з недотриманням клієнтами DNS TTL, що може призвести до того, що клієнт юзатиме якісь застарілі записи зі свого протухлого кешу замість того, щоб запитати Unbound.

На практиці у мене ні перше, ні друге не викликало проблем, але ваш mileage may vary.

Налаштування сервера

Для зручності розкочування я написав роль для Ansible. Вона може налаштовувати як сервера, і клієнти з урахуванням Linux (розраховано на deb-based дистрибутиви). Всі налаштування досить очевидні і задаються в inventory.yml. Ця роль вирізана з мого великого плейбука, тому може містити помилки. тягнуть запити welcome 🙂

Пройдемося по основним компонентам.

bgp

При запуску двох BGP-демонів на одному хості виникає фундаментальна проблема: BIRD ніяк не хоче піднімати BGP-піринг з локалхостом (або будь-яким локальним інтерфейсом). Від слова зовсім. Гуглення та читання mailing-lists не допомогло, там стверджують що це by design. Можливо, є якийсь спосіб, але я його не знайшов.

Можна спробувати інший BGP демон, але мені подобається BIRD і він використовується скрізь у мене, не хочеться плодити сутності.

Тому я сховав dnstap-bgp всередину network namespace, яке пов'язане з кореневим через veth інтерфейс: це як труба, кінці якої стирчать у різних namespace. На кожен із цих кінців ми вішаємо приватні p2p IP-адреси, які за межі хоста не виходять, тому можуть бути будь-якими. Це той же механізм, який використовується для доступу до процесів усередині. коханого всіма Docker та інші контейнери.

Для цього було написано сценарій і в dnstap-bgp було додано вже описаний вище функціонал перетягування себе за волосся в інший namespace. Через це його потрібно запускати під root або видати бінарнику CAP_SYS_ADMIN через команду setcap.

Приклад скрипту для створення namespace

#!/bin/bash

NS="dtap"

IP="/sbin/ip"
IPNS="$IP netns exec $NS $IP"

IF_R="veth-$NS-r"
IF_NS="veth-$NS-ns"

IP_R="192.168.149.1"
IP_NS="192.168.149.2"

/bin/systemctl stop dnstap-bgp || true

$IP netns del $NS > /dev/null 2>&1
$IP netns add $NS

$IP link add $IF_R type veth peer name $IF_NS
$IP link set $IF_NS netns $NS

$IP addr add $IP_R remote $IP_NS dev $IF_R
$IP link set $IF_R up

$IPNS addr add $IP_NS remote $IP_R dev $IF_NS
$IPNS link set $IF_NS up

/bin/systemctl start dnstap-bgp

dnstap-bgp.conf

namespace = "dtap"
domains = "/var/cache/rkn_domains.txt"
ttl = "168h"

[dnstap]
listen = "/tmp/dnstap.sock"
perm = "0666"

[bgp]
as = 65000
routerid = "192.168.149.2"

peers = [
    "192.168.149.1",
]

bird.conf

router id 192.168.1.1;

table rkn;

# Clients
protocol bgp bgp_client1 {
    table rkn;
    local as 65000;
    neighbor 192.168.1.2 as 65000;
    direct;
    bfd on;
    next hop self;
    graceful restart;
    graceful restart time 60;
    export all;
    import none;
}

# DNSTap-BGP
protocol bgp bgp_dnstap {
    table rkn;
    local as 65000;
    neighbor 192.168.149.2 as 65000;
    direct;
    passive on;
    rr client;
    import all;
    export none;
}

# Static routes list
protocol static static_rkn {
    table rkn;
    include "rkn_routes.list";
    import all;
    export none;
}

rkn_routes.list

route 3.226.79.85/32 via "ens3";
route 18.236.189.0/24 via "ens3";
route 3.224.21.0/24 via "ens3";
...

DNS

За замовчуванням в Ubuntu бінарник Unbound затиснутий AppArmor-профілем, який забороняє йому коннектитися до будь-яких там DNSTap-сокетів. Можна або видалити цей профіль, або відключити його:

# cd /etc/apparmor.d/disable && ln -s ../usr.sbin.unbound .
# apparmor_parser -R /etc/apparmor.d/usr.sbin.unbound

Це, мабуть, треба додати до плейбуку. Ідеально, звичайно, поправити профіль і видати потрібні права, але мені було ліньки.

unbound.conf

server:
    chroot: ""
    port: 53
    interface: 0.0.0.0
    root-hints: "/var/lib/unbound/named.root"
    auto-trust-anchor-file: "/var/lib/unbound/root.key"
    access-control: 192.168.0.0/16 allow

remote-control:
    control-enable: yes
    control-use-cert: no

dnstap:
    dnstap-enable: yes
    dnstap-socket-path: "/tmp/dnstap.sock"
    dnstap-send-identity: no
    dnstap-send-version: no

    dnstap-log-client-response-messages: yes

Скачування та обробка списків

Скрипт для скачування та обробки списку IP-адрес
Він завантажує список, сумаризує до префіксу pfx. У dont_add и dont_summarize можна сказати IP та мережі, які потрібно пропустити або не сумаризувати. Мені це потрібно було т.к. підмережа мого VPS опинилася в блоклисті 🙂

Найсмішніше що API РосКомСвободи блокує запити з дефолтним користувачем-агентом Пітона. Мабуть скрипт-кідді дістали. Тому міняємо його на Огнеліса.

Поки що працює тільки з IPv4 т.к. Частка IPv6 невелика, але це буде легко виправити. Хіба що доведеться використовувати ще bird6.

rkn.py

#!/usr/bin/python3

import json, urllib.request, ipaddress as ipa

url = 'https://api.reserve-rbl.ru/api/v2/ips/json'
pfx = '24'

dont_summarize = {
    # ipa.IPv4Network('1.1.1.0/24'),
}

dont_add = {
    # ipa.IPv4Address('1.1.1.1'),
}

req = urllib.request.Request(
    url,
    data=None, 
    headers={
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36'
    }
)

f = urllib.request.urlopen(req)
ips = json.loads(f.read().decode('utf-8'))

prefix32 = ipa.IPv4Address('255.255.255.255')

r = {}
for i in ips:
    ip = ipa.ip_network(i)
    if not isinstance(ip, ipa.IPv4Network):
        continue

    addr = ip.network_address

    if addr in dont_add:
        continue

    m = ip.netmask
    if m != prefix32:
        r[m] = [addr, 1]
        continue

    sn = ipa.IPv4Network(str(addr) + '/' + pfx, strict=False)

    if sn in dont_summarize:
        tgt = addr
    else:
        tgt = sn

    if not sn in r:
        r[tgt] = [addr, 1]
    else:
        r[tgt][1] += 1

o = []
for n, v in r.items():
    if v[1] == 1:
        o.append(str(v[0]) + '/32')
    else:
        o.append(n)

for k in o:
    print(k)

Скрипт для оновлення
Він у мене запускається по крону раз на добу, може варто смикати раз на 4 години. це, на мою думку, період оновлення який РКН вимагає від провайдерів. Плюс, є якісь ще суперстрокові блокування у них, які, може, й швидше прилітають.

Робить таке:

  • Запускає перший скрипт та оновлює список маршрутів (rkn_routes.list) для BIRD
  • Релоадит BIRD
  • Оновлює та підчищає список доменів для dnstap-bgp
  • Релоадит dnstap-bgp

rkn_update.sh

#!/bin/bash

ROUTES="/etc/bird/rkn_routes.list"
DOMAINS="/var/cache/rkn_domains.txt"

# Get & summarize routes
/opt/rkn.py | sed 's/(.*)/route 1 via "ens3";/' > $ROUTES.new

if [ $? -ne 0 ]; then
    rm -f $ROUTES.new
    echo "Unable to download RKN routes"
    exit 1
fi

if [ -e $ROUTES ]; then
    mv $ROUTES $ROUTES.old
fi

mv $ROUTES.new $ROUTES

/bin/systemctl try-reload-or-restart bird

# Get domains
curl -s https://api.reserve-rbl.ru/api/v2/domains/json -o - | jq -r '.[]' | sed 's/^*.//' | sort | uniq > $DOMAINS.new

if [ $? -ne 0 ]; then
    rm -f $DOMAINS.new
    echo "Unable to download RKN domains"
    exit 1
fi

if [ -e $DOMAINS ]; then
    mv $DOMAINS $DOMAINS.old
fi

mv $DOMAINS.new $DOMAINS

/bin/systemctl try-reload-or-restart dnstap-bgp

Вони були написані не особливо замислюючись, тому якщо бачите, що можна покращити — дерзайте.

Налаштування клієнта

Тут я наведу приклади для Linux-роутерів, але у разі Mikrotik/Cisco це має бути ще простіше.

Для початку налаштовуємо BIRD:

bird.conf

router id 192.168.1.2;
table rkn;

protocol device {
    scan time 10;
};

# Servers
protocol bgp bgp_server1 {
    table rkn;
    local as 65000;
    neighbor 192.168.1.1 as 65000;
    direct;
    bfd on;
    next hop self;
    graceful restart;
    graceful restart time 60;
    rr client;
    export none;
    import all;
}

protocol kernel {
    table rkn;
    kernel table 222;
    scan time 10;
    export all;
    import none;
}

Таким чином ми синхронізуватимемо маршрути, отримані з BGP, з таблицею маршрутизації ядра за номером 222.

Після цього достатньо попросити ядро ​​дивитися в цю табличку перед тим, як заглядати в дефолтну:

# ip rule add from all pref 256 lookup 222
# ip rule
0:  from all lookup local
256:    from all lookup 222
32766:  from all lookup main
32767:  from all lookup default

Все, залишилося налаштувати DHCP на роутері на роздачу тунельної IP-адреси сервера як DNS і готова схема.

Недоліки

При поточному алгоритмі формування та обробки списку доменів до нього потрапляє, зокрема, youtube.com та його CDNи.

А це призводить до того, що всі відео будуть їхати через VPN, що може забити весь канал. Можливо, варто скласти якийсь список популярних доменів-виключень, які блокувати у РКН поки що кишка тонка. І пропускати їх за парсингу.

Висновок

Описаний спосіб дозволяє обходити будь-які практичні блокування, які реалізують провайдери на даний момент.

В принципі, dnstap-bgp можна використовувати для будь-яких інших цілей, де необхідний певний рівень управління трафіком на основі доменного імені. Тільки потрібно враховувати, що в наш час на одній і тій же IP-адресі може висіти тисяча сайтів (за яким-небудь Cloudflare, наприклад), тому цей спосіб має досить низьку точність.

Але для потреб обходу блокувань цього цілком достатньо.

Доповнення, правки, пуллреквести – вітаються!

Джерело: habr.com

Додати коментар або відгук