Как-то раз в мою сисадминскую жизнь пришел простой и понятный, но, как оказалось, не самый тривиальный запрос - как сделать так, чтобы на 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 rule add action=lookup-only-in-table chain=user comment="PROXY LOOKUP ONLY IN MAIN" disabled=no src-address=192.168.0.10/32 table=main add action=lookup chain=user comment="LOOKUP FOR PROXY" disabled=no src-address=192.168.0.0/24 table=rt-proxy add 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 instance add as=64999 disabled=no name=bgp-instance router-id=192.168.0.1 routing-table=rt-proxy /routing bgp connection add 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 wireguard add listen-port=7443 mtu=1420 name=wg-xray /interface wireguard peers add 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 dns set 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 static add 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 занимается проксированием, а я вспоминаю об этой конструкции только когда нужно добавить очередной домен или поменять сервер.
