Маршрутизируем отдельные сервисы через внешний шлюз на MikroTik: BGP + DNS FWD + xray core

от автора

Как-то раз в мою сисадминскую жизнь пришел простой и понятный, но, как оказалось, не самый тривиальный запрос — как сделать так, чтобы на WiFi клиентам не нужно было включать ВПН/прокси. Подключился к сети — [Вставьте свой любимый сервис] заработал. Красота. И чтобы надежно было, не отваливалось. И админить удобно.

Основная идея — сделать все на минималках. Без скриптинга, без тяжелого mangle. Чем ближе к голой маршрутизации — тем лучше.

В общем, что у меня получилось, тем и хочу поделиться. В этой статье не будет ничего про аренду и настройку VPS, про обзоры прокси/ВПН технологий и всего такого. Полагаю, что все уже готово и настроено.

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

MikroTik

Почему именно микротик? Ответ простой — у меня он стоит абсолютно везде. И, по моему скромному мнению, это все еще безусловный король малого и среднего бизнеса по цене/возможностям. Ну и часть устройств досталась по наследству, не менять же.

За основу берем RouterOS 7.22 (на момент написания статьи). Начиная с версии 7.19, семерка стала вполне пригодна к употреблению, без особых багов и отвалов. Для сомневающихся есть long-term прошивка на базе 7.21.

Главная его проблема в связке с xray — это то, что он не умеет быть инициатором SOCKS/HTTP. Сервером — пожалуйста, но клиентом, увы. Тут нас выручит Wireguard из ROS7, это единственный протокол, который есть и там, и там. Плюс — можно тянуть туннель куда угодно, хоть в локалку, хоть на внешний сервер, хоть в контейнер, разницы никакой.

BGP

BGP нам нужен лишь с одной целью — забирать маршруты/префиксы из внешнего источника. Какого именно — выбирать по ситуации. Можно и свой собрать, но это уже на вкус и цвет.

Злые языки утверждают, что бюджетный микрот не вывезет 100к+ префиксов, которые он получит по BGP, но это актуально только для старых прошивок. Видимо, что-то подкрутили в новых и даже на скромном hEX 120к маршрутов держатся вполне себе стабильно, хоть и со скрипом.

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

DNS Forward

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

Логика простая. Клиент захотел на сайт. Роутер, как обычно, рекурсивно резолвит домен, отдает клиенту адрес, но заодно копирует его себе в табличку и все запросы на эти адреса маршрутизирует на нужный шлюз.

Главная фича — возможность одной галочкой закрыть сразу все поддомены указанной зоны, чего не может, например, чистый address-list. Более того, оно еще и обновляется в зависимости от того, какой именно адрес тебе отдает сейчас домен. Минимум ручного труда, никакого мусора, лучше не придумаешь. Списки доменов конкретного сервиса частенько можно нагуглить как «настройка за корпоративным прокси».

Xray

Почти что золотой стандарт в наши дни, ничего не скажешь. На Хабре только ленивый не прошелся по его настройке (и я в том числе). Как правило, это простенькие конфиги из-под панелей по типу 3X, либо взятый пример из документации. HTTP/SOCKS in > VLESS out. Хорошо, но уже не вдохновляет.

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

В нашем случае именно он является ВПН/прокси-клиентом для всей нашей локальной сети.

Подготовка маршрутизатора

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

Из IP-адресов нас интересуют только 3: адрес сервера с xray на борту и 2 адреса, которые мы назначим на концах туннеля. Первый мы укажем как точку подключения по WG, а туннельный адрес xray послужит шлюзом для всех кастомных маршрутов на микротике. В примере даны 192.168.0.0/24 для локалки и 172.16.0.0/30 для туннельных адресов.

1. Создаем routing table

Сюда мы будем складывать наши маршруты. Можно и в main, но это доставит неудобства в быту.

/routing table add disabled=no fib name=rt-proxy

2. Routing rule

Логика такая: хост ищет в кастомной таблице маршрут до узла. Если не находит — fallback в main, где есть шлюз последней надежды, он же default gateway. На случай, если xray в локалке, нужно добавить правило, которое запретит ему все таблицы, кроме main, чтобы избежать петли.

/routing ruleadd action=lookup-only-in-table chain=user comment="PROXY LOOKUP ONLY IN MAIN" disabled=no src-address=192.168.0.10/32 table=mainadd action=lookup chain=user comment="LOOKUP FOR PROXY" disabled=no src-address=192.168.0.0/24 table=rt-proxyadd action=lookup chain=user comment="FALLBACK TO MAIN" disabled=no src-address=192.168.0.0/24 table=main

3.Routing filter

Полученные по BGP маршруты имеют кривой шлюз. Меняем его на правильный. Опционально можем задать проверку на доступность. Если шлюз отвалится, проверка отключит маршруты и пустит трафик по умолчанию. Может быть полезно, если используются несколько шлюзов.

/routing filter rule add chain=bgp-in disabled=no rule="set gw 172.16.0.2;set gw-check icmp; accept;"

4.BGP connection

Для примера привожу подключение к сферическому BGP-хосту в вакууме. Если источников несколько, предпочтение отдается более точному маршруту, поэтому в большинстве случаев конфликтов быть не должно. Не забудьте поменять router-id на корректный. После создания наша кастомная таблица начнет наполняться маршрутами.

/routing bgp instanceadd as=64999 disabled=no name=bgp-instance router-id=192.168.0.1 routing-table=rt-proxy/routing bgp connectionadd as=64999 connect=yes disabled=no hold-time=4m input.filter=bgp-in instance=bgp-instance listen=no local.role=ebgp multihop=yes name=bgp-routes \    output.no-client-to-client-reflection=yes remote.address=10.0.0.1 .as=65000 .port=179 routing-table=rt-proxy vrf=main

5. Wireguard

Создаем интерфейс и peer. Не забываем подставить публичный ключ из xray.

/interface wireguardadd listen-port=7443 mtu=1420 name=wg-xray/interface wireguard peersadd allowed-address=0.0.0.0/0 client-allowed-address=0.0.0.0/0 endpoint-address=192.168.0.2 endpoint-port=2389 interface=wg-xray name=xray-proxy persistent-keepalive=25s \    public-key=""/ip address add address=172.16.0.1/30 interface=wg-xray network=172.16.0.0

6. DNS static + FWD

Осталось дело за малым: настроить кастомные домены для маршрутизации. DNS даны для примера, лучше, конечно, использовать DOH/DOT. Параметр address-list-extra-time определяет, сколько времени адрес будет висеть в листе. Я бы не делал слишком много, чтобы не засорять, но и не слишком мало.

/ip dnsset address-list-extra-time=30m allow-remote-requests=yes doh-max-concurrent-queries=100 doh-max-server-connections=20 servers=1.1.1.1,8.8.8.8/ip dns staticadd address-list=proxy-dns match-subdomain=yes name=chatgpt.com type=FWD

7. Mangle rule

Теперь нужно как-то превратить address-list в настоящие маршруты. В mangle action появилось новое действие route. Согласно документации оно игнорирует все настройки роутинга и принудительно задает пакету шлюз. Что нам более чем подходит. Убиваем сразу двух зайцев, не нужно создавать отдельную таблицу и правило для нее.

/ip firewall mangle add action=route chain=prerouting dst-address-list=proxy-dns passthrough=no route-dst=172.16.0.2 src-address=192.168.0.0/24

На этом подготовка роутера завершена. Таблицы ломятся от маршрутов, все кастомные домены выписаны, осталось только открыть врата нашего ВПН-туннеля.

Подготовка xray

Теперь настало время заняться самим прокси. Базовая схема выглядит так: WG in > VLESS1/VLESS2/…/VLESSN out. Outbound, в принципе, может быть любым, не обязательно именно VLESS.

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

config.json
{  "log": {    "loglevel": "warning"  },  "dns": {    "servers": [      "https+local://dns.google/dns-query",      "https+local://cloudflare-dns.com/dns-query"    ],    "queryStrategy": "UseIP"  },  "observatory": {    "subjectSelector": [      "proxy-"    ],    "probeUrl": "https://www.google.com/generate_204",    "probeInterval": "60s",    "enableConcurrency": false  },  "inbounds": [    {      "tag": "wg-in",      "listen": "0.0.0.0",      "port": 2389,      "protocol": "wireguard",      "settings": {        "address": [          "172.16.0.2"        ],        "secretKey": "",        "mtu": 1420,        "peers": [          {            "publicKey": "",            "allowedIPs": [              "0.0.0.0/0",              "::/0"            ]          }        ]      },      "sniffing": {        "enabled": true,        "destOverride": [          "http",          "tls",          "quic"        ],        "routeOnly": true      }    }  ],  "outbounds": [    {      "tag": "proxy-nl",      "protocol": "vless",      "settings": {        "address": "",        "port": 443,        "id": "",        "encryption": "none",        "flow": "xtls-rprx-vision"      },      "streamSettings": {        "network": "raw",        "security": "reality",        "realitySettings": {          "show": false,          "fingerprint": "chrome",          "serverName": "",          "password": "",          "shortId": ""        }      }    },    {      "tag": "proxy-us",      "protocol": "vless",      "settings": {        "address": "",        "port": 443,        "id": "",        "encryption": "none",        "flow": "xtls-rprx-vision"      },      "streamSettings": {        "network": "raw",        "security": "reality",        "realitySettings": {          "show": false,          "fingerprint": "chrome",          "serverName": "",          "password": "",          "shortId": ""        }      }    },    {      "tag": "proxy-se",      "protocol": "vless",      "settings": {        "address": "",        "port": 443,        "id": "",        "encryption": "none",        "flow": "xtls-rprx-vision"      },      "streamSettings": {        "network": "raw",        "security": "reality",        "realitySettings": {          "show": false,          "fingerprint": "chrome",          "serverName": "",          "password": "",          "shortId": ""        }      }    },    {      "tag": "direct",      "protocol": "freedom"    },    {      "tag": "block",      "protocol": "blackhole"    }  ],  "routing": {    "domainStrategy": "AsIs",    "rules": [      {        "type": "field",        "protocol": [          "bittorrent"        ],        "outboundTag": "block",        "ruleTag": "block-bittorrent"      },      {        "type": "field",        "inboundTag": [          "wg-in"        ],        "ip": [          "geoip:private",          "100.64.0.0/10",          "169.254.0.0/16",          "fe80::/10"        ],        "network": "tcp,udp",        "outboundTag": "direct",        "ruleTag": "wg-private-addresses-direct"      },      {        "type": "field",        "inboundTag": [          "wg-in"        ],        "domain": [          "geosite:category-ru",          "regexp:\\.ru$"        ],        "network": "tcp,udp",        "outboundTag": "direct",        "ruleTag": "wg-ru-domains-direct"      },      {        "type": "field",        "inboundTag": [          "wg-in"        ],        "ip": [          "geoip:ru"        ],        "network": "tcp,udp",        "outboundTag": "direct",        "ruleTag": "wg-ru-ip-direct"      },      {        "type": "field",        "inboundTag": [          "wg-in"        ],        "network": "tcp,udp",        "balancerTag": "proxy-balance",        "ruleTag": "wg-proxy-balance"      }    ],    "balancers": [      {        "tag": "proxy-balance",        "selector": [          "proxy-"        ],        "fallbackTag": "proxy-us",        "strategy": {          "type": "leastLoad",          "settings": {            "expected": 3,            "maxRTT": "10s",            "tolerance": 0.01,            "baselines": [              "1s"            ],            "costs": [              {                "regexp": false,                "match": "proxy-se",                "value": 0.1              },              {                "regexp": false,                "match": "proxy-nl",                "value": 1.0              },              {                "regexp": false,                "match": "proxy-us",                "value": 0.5              }            ]          }        }      }    ]  }}
  • «observatory» — отправляет healthcheck-запрос на outbounds. Если хост недоступен — исключает его.

  • “inbounds” — задаем настройки для WG. В данном случае он работает только как сервер, сам он подключения, если верить документации, устанавливать не может. Пару ключей генерируем сами и меняемся публичными с микротиком.

  • “outbounds” — как обычно, перечисляем список всех наших конечных узлов.

  • “routing” — из экзотики правила теперь ссылаются на тег балансировщика, а не на чистый outbound. Также добавил на всякий случай исключение для ру-доменов, если какой-то случайно затесался на микроте.

  • “balancers” — fallbackTag можете не смотреть, это заглушка. По задумке тут должен быть какой-то 4-й сервак, который не участвует в основной движухе, либо freedom/blackhole.

  • Блок «costs» работает только с типом leastLoad. Настройки дефолтные. Чем ниже вес — тем выше приоритет.

ПНР

Собственно, запускаем xray, смотрим логи на предмет ошибок в конфиге или проблем с серверами. Поглядываем на микротик, счетчик last handshake должен начать тикать и обнуляться каждые 1.5-2 минуты. Значит ВПН-соединение установлено. Обязательно пингануть адрес шлюза, чтобы убедиться, что пакеты уходят и приходят.

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

Итоги

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

ссылка на оригинал статьи https://habr.com/ru/articles/1046514/