Небольшой рассказ - туториал о том, как на MikroTik реализовать удобное управление большим количеством VPN профайлов.
Предыстория
Вспомнил, что в России пылится Xbox. Попросил родных отправить его транспортной компанией в Казахстан. Консоль приехала.
Чтобы активировать подписку или купить новую игру, приобрел redeem code на одном из профильных ресурсов. Именно через redeem, последний раз в России, я пополнял игровой аккаунт. Пытаюсь активировать redeem code в Казахстане, ошибка, еще раз, опять ошибка. Точный текст ошибки не запомнил, но ее содержание не указывало на какую-то конкретную проблему. Поискав причину в сети, пришло понимание, что с высокой долей вероятности, это ограничение по региону. И оно более жесткое чем пару лет назад. Мой аккаунт привязан к Турции. Рекомендации из сети: "Поменять регион на Южную Корею и будет все окэй." не сработали. Варианты типа, купить аккаунт с игрой, или, тем более, передать кому-то свой аккаунт для установки игры (если так до сих пор делают), я никогда не рассматривал. Остается VPN.
Захотел поиграть в Xbox. Xbox без VPN отказывается радовать меня новыми играми.
Ищу VPN сервис, приобретаю. Наивные попытки активировать VPN на iphone и через iphone, как через wifi роутер, завернуть трафик Xbox, проваливаются (исходящий трафик с самого iphone проходит через VPN туннель, а forward трафик от Xbox или любого другого wifi устройства нет). Официально, нужный мне режим работы в IOS не поддерживается. Сюда же можно отнести и MacOS, хотя в MacBook, как я понял, через ethernet + wifi интерфейсы в бридже, может даже что-то получится... но это не точно. И не красиво. Домашний оптический huawei роутер от провайдера VPN функционалом не обладает.
Нужен второй роутер.
Схема физического подключения:
WiFi клиент - - -> Router с VPN ——> Router от провайдера ——> Провайдер
Выбор роутера
Критерии выбора роутера:
WiFi-роутер
поддержка одного и более VPN протоколов (для моего VPN провайдера это OpenVPN, IKEv2 (IPsec) и WireGuard)
цена
компактность
Он может быть дешевым классических размеров SOHO роутером и тогда его не жалко оставить (так как скоро улетать из Казахстана), либо более компактным, возможно более дорогим, и тогда его можно таскать с собой в путешествия, в которых VPN тоже не безполезен.
Открываю маркетплейс, изучаю предложение. Обычные домашние роутеры, ничего интересного... Замечаю это https://mikrotik.com/product/RBmAPL-2nD (mAP lite).

Компактный роутер, на мощной сетевой операционной системе, с поддержкой VPN, со встроенным скриптовым языком для автоматизации и много чего еще.

Позже наткнулся на старшего брата https://mikrotik.com/product/RBmAP2nD (mAP), но это еще более редкий для покупки персонаж.
А что если, взять один из подобных MikroTik'ов, загрузить туда кучу VPN профилей, поднять на нем 2-3, 10-50 виртуальных wifi точек доступа (далее, просто AP), объединить каждую AP со своим VPN профилем, и переключаться между странами просто подключившись к нужной AP? Звучит как план.
Примерно в этом месте Xbox перестает быть интересен.
Конечно, можно было остановиться на любом другом роутере с поддержкой https://dd-wrt.com или https://openwrt.org прошивки, но компактность mAP lite и отсутствие необходимости устанавливать альтернативную прошивку с ненулевой вероятностью окирпичить устройство, подкупили.
Заказываю девайс на маркетплейсе, жду неделю, доставка откладывается еще на неделю, не выдерживаю, ищу такой же роутер в местных интернет-магазинах, нахожу (даже дешевле), через 2 дня mikrotik в руках.
Он действительно мелкий. Даже слишком. И легкий. Очень. Офф сайт не хвастается весом устройства. На одном из ресурс��в нахожу цифру 37 грамм. По ощущениям даже меньше. Можно положить в карман и забыть.

Включаю роутер, подключаюсь к MikroTik-чтототам wifi сети, захожу на http://192.168.88.1. Начинаю изучать возможности устройства.
Виртуальные AP как ожидалось легко поднимаются. Смотрю VPN протоколы, а тут сюрприз: старый OpenVPN со старыми шифрами и только по UDP, IPsec (тут без нареканий) и, кажется, L2TP.
OpenVPN клиент не взлетел - слишком старый, IPsec после часа унижений поднялся, но на стабильную работу так и не вышел. Поведение трафика было похоже на сломанный MTU, хотя fix MSS я естественно настроил :noetonetochno
Было принято решение обновится с текущей RouterOS 6.x на 7.x, в которой обещали "новый", модный WireGuard.
Надо было сделать это сразу же, тем более что данная рекомендация/требование прямо прописаны в инструкции к девайсу (но кто эти инструкции читает?!), правда причина обновления отличается от моей.
Первый абзац инструкции:
This device needs to be upgraded to RouterOS v7.3.1 or the latest stable version to ensure compliance with local authority regulations!
Даты выхода мажорных версий RouterOS
Version 7: December 2021
Version 6: November 2012
Обновляю RouterOS
Процедура апгрейда RouterOS
Это можно сделать двумя путями: либо запустить процедуру апгрейда из соответствующего раздела микротика, либо скачав совместимую прошивку с официального сайта производителя, загрузив ее на роутер и выполнив перезагрузку устройства.
Апгрейд RouterOS осуществляется через промежуточные версии, поэтому чтобы случайно не окирпичить интересный девайс, рекомендую идти первым путем. В этом случае RouterOS сама подберет на какие прошивки можно безопасно обновится.
У мяня была одна промежуточная версия, т.е. до текущей 7.20.8 мой mikrotik апгрейдился 2 раза.
Обновился, отлично.
Смотрю VPN протоколы, WireGuard да, OpenVPN с современными шифрами тоже да.
Пробую поднять OpenVPN клиент, опять не работает. На этот раз проблема с сертификатом. По неведомой причине CA от моего VPN провайдера не импортируется в хранилище сертификатов.
Пробую WireGuard (далее просто WG). Взлетает с пол пинка. Забываю обо всех других VPN протоколах.
Промежуточное видение будущего решения
в качестве VPN протокола выступает WireGuard
под каждый WG туннель поднимается виртуальная AP со своей клиентской wlan сетью
клиентский трафик конкретной AP через policy-based routing (PBR) маршрутизируется в нужный WG туннель
так как VPN провайдер для WG отдает клиенту /16 сеть (10.14.0.0/16), то, в теории, нам даже SNAT/MASQUERADE не требуется (просто все клиентские адреса выделяем в пределах этой сети), что снижает нагрузку на hardware роутера
автоматизация заливки VPN профилей на mikrotik, настройка AP, настройка маршрутизации и всего такого выполняется через набор python скриптов
клиенту, чтобы начать использовать интересующий его VPN туннель, достаточно подключиться к соответствующей WiFi точке доступа
Для того чтобы поднять одну AP, с уникальным паролем, отдельной подсетью, отдельным WG туннелем и настроить маршрутизацию, необходимо потрогать примерно 16 сущностей в RouterOS (здесь и далее подразумевается версия 7.x).
Ниже я распишу что именно нужно настроить, но сначала отпределимся с ip планом сети.
IP план сети
10.14.0.0/16 | общая сеть |
10.14.x.0/24 | сеть клиентского wlan (где 0 < x < 200) |
10.14.x.1 | шлюз клиентского wlan |
10.14.x.10 - 10.14.x.20 | пул адресов клиентсвкого wlan (можно расширить до 1-255) |
10.14.200.x/32 | ip и сеть для WG туннеля со стороны mikrotik |
Пример 1 | |
10.14.1.0/24 | сеть клиентского wlan AP MikroTrIst, трафик которой пойдет в WG туннель tr-ist (Turkey Istanbul) |
10.14.1.1 | шлюз клиентского wlan AP MikroTrIst |
10.14.1.10 - 10.14.1.20 | пул адресов клиентского wlan AP MikroTrIst |
10.14.200.1/32 | ip и сеть для WG туннеля tr-ist |
Пример 2 | |
10.14.77.0/24 | сеть клиентского wlan AP MikroDeFra, трафик которой пойдет в WG туннель de-fra (Germany Frankfurt) |
10.14.77.1 | шлюз клиентского wlan AP MikroDeFra |
10.14.77.10 - 10.14.77.20 | пул адресов клиентского wlan AP MikroDeFra |
10.14.200.77/32 | ip и сеть для WG туннеля de-fra |
Пример настройки mikrotik руками
1. Secure profile для виртуальной точки доступа

или
[admin@RouterOS] > /interface/wireless/security-profiles/add \ name="de-fra" \ mode="dynamic-keys" \ authentication-types="wpa2-psk" \ unicast-ciphers="aes-ccm" \ group-ciphers="aes-ccm" \ wpa2-pre-shared-key="passpass"
2. Интерфейс виртуальной точки доступа

или
[admin@RouterOS] > /interface/wireless/add \ name="wlan-wg-de-fra" \ ssid="MikroDeFra" \ master-interface="wlan1" \ security-profile="de-fra"
3. Бридж для wlan

или
[admin@RouterOS] > /interface/bridge/add name="br-wg-de-fra"
4. Порт в бридже

или
[admin@RouterOS] > /interface/bridge/port/add bridge="br-wg-de-fra" interface="wlan-wg-de-fra"
5. Пул адресов для wlan

или
[admin@RouterOS] > /ip/pool/add name="wlan-wg-de-fra" ranges="10.14.77.10-10.14.77.20"
6. IP адрес шлюза для wlan

или
[admin@RouterOS] > /ip/address/add \ address="10.14.77.1/24" \ network="10.14.77.0" \ interface="br-wg-de-fra"
7. IP адрес шлюза для WG туннеля

или
[admin@RouterOS] > /ip/address/add \ address="10.14.200.77/32" \ network="10.14.200.77" \ interface="wg-de-fra"
8. DHCP сервер для wlan

или
[admin@RouterOS] > /ip/dhcp-server/add \ name="wlan-wg-de-fra" \ interface="br-wg-de-fra" \ address-pool="wlan-wg-de-fra"
9. Сеть для DHCP сервера

или
[admin@RouterOS] > /ip/dhcp-server/network/add \ address="10.14.77.0/24" \ gateway="10.14.77.1" \ dns-server="8.8.4.4"
10. WIreGuard интерфейс

или
[admin@RouterOS] > /interface/wireguard/add \ name="wg-de-fra" \ private-key="privatekeyprivatekeyprivatekeyprivatekey"
11. WireGuard Peer

или
[admin@RouterOS] > /interface/wireguard/peers/add \ interface="wg-de-fra" \ public-key="publickeypublickeypublickeypublickey" \ endpoint-address="vpnproviderservercom" \ endpoint-port="51820" \ allowed-address="0.0.0.0/0" \ persistent-keepalive="20s" \ client-dns="8.8.4.4"
12. Routing Table

или
[admin@RouterOS] > /routing/table/add name="via-wg-de-fra" fib
13. Routing Rule

или
[admin@RouterOS] > /routing/rule/add \ table="via-wg-de-fra" \ src-address="10.14.77.0/24" \ action=lookup
14. Роут через WG туннель

или
[admin@RouterOS] > /ip/route/add \ routing-table="via-wg-de-fra" \ dst-address="0.0.0.0/0" \ gateway="%wg-de-fra"
15. Список WG интерфейсов


или
16. TCP MSS


или
[admin@RouterOS] > /ip/firewall/mangle/add \ chain="forward" \ protocol="tcp" \ tcp-flags="syn" \ out-interface-list="WG" \ action="change-mss" \ new-mss="clamp-to-pmtu"
Теперь автоматизирую это все python скриптами... Готово.
Сейчас можно поднять, теоритически, почти любое количество VPN профилей и AP на MikroTik за пару команд.
Какие у данного решения минусы?
Самый большой минус - необходимость иметь под рукой набор этих самых python скриптов и место, откуда их можно запустить. Неудобно.
Вспоминаю, что RouterOS уже давно имеет встроенный скриптовый язык. Лезу в интернет посмотреть примеры синтаксиса. Натыкаюсь на пару примеров: в одном описывается как повесить событие на нажатие reset/mode button, во втором используется telegram бот.
А что если...
Финальное видение решения
в качестве VPN протокола выступает WireGuard
имеем 2 типа клиентских AP: "MikroWorld" и "Mikro<Country><City>"
"MikroWorld" - AP, которая в один момент времени смотрит в один WG туннель, в другой момент времени может смотреть в другой туннель. Переключение между WG туннелями (или профилями) осуществляется по нажатию физической кнопки reset (или mode) на mikrotik. AP типа "World" может быть только одна
" Mikro<Country><City>" - AP, которая всегда смотрит в свой WG туннель. AP типа "Country" может быть любое количество
под каждую AP выделяется своя клиентская wlan сеть
клиентский трафик конкретной AP через policy-based routing (PBR) маршрутизируется в нужный WG туннель
так как VPN провайдер для WG отдает клиенту /16 сеть (10.14.0.0/16), то все клиентские адреса выделяем в пределах этой сети. SNAT/MASQUERADE не использую, чтобы лишний раз не нагружать hardware роутера
автоматизация заливки VPN профилей на mikrotik, настройка AP, настройка маршрутизации и всего такого выполняется через набор RouterOS скриптов запускаемых на самом mikrotik
управление (включение, выключение, переключение) AP и WG профилями осуществляется через telegram бот, переключение WG профиля для "MikroWorld" AP осуществляется через кнопку rest (или mode)
клиенту, чтобы начать использовать интересующий его VPN туннель, достаточно подключиться к "Mikro<Country><City>" или "MikroWorld" точке доступа
Минус пол недели из своей жизни и все готово.
Установка
functions4vpn.rsc
:put "### START SCRIPT functions4vpn.rsc ###" :global somePrepares do={ :put "Trying to add /interface/list WG ..." :if ([:len [/interface/list/find name="WG"]] = 0) do={ /interface/list/add name="WG" comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } :put "Trying to add /ip/firewall/mangle fix mss rule for WG interfaces ..." :if ([:len [/ip/firewall/mangle/find new-mss=clamp-to-pmtu out-interface-list="WG"]] = 0) do={ /ip/firewall/mangle/add \ chain="forward" \ protocol="tcp" \ tcp-flags="syn" \ out-interface-list="WG" \ action="change-mss" \ new-mss="clamp-to-pmtu" \ comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global wirelessAddSecProfile do={ :put "Trying to add /interface/wireless/secure-profile '$profName' ..." :if ([:len [/interface/wireless/security-profiles/find name="$profName"]] = 0) do={ /interface/wireless/security-profiles/add \ name="$profName" \ mode="$wlanMode" \ authentication-types="$wlanAuthTypes" \ unicast-ciphers="$wlanUnicastCiphers" \ group-ciphers="$wlanGroupCiphers" \ wpa2-pre-shared-key="$wlanPass" \ comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global wirelessAddInterface do={ :put "Trying to add /interface/wireless/interface '$wlanIface' ..." :if ([:len [/interface/wireless/find name="$wlanIface"]] = 0) do={ /interface/wireless/add \ name="$wlanIface" \ ssid="$wlanSsid" \ master-interface="$wlanMasterIface" \ security-profile="$profName" \ comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global bridgeAddInterface do={ :put "Trying to add /interface/bridge '$brIface' ..." :if ([:len [/interface/bridge/find name="$brIface"]] = 0) do={ /interface/bridge/add name="$brIface" comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global bridgeAddPort do={ :put "Trying to add /interface/bridge/port '$wlanIface' into bridge '$brIface' ..." :if ([:len [/interface/bridge/port/find bridge="$brIface"]] = 0) do={ /interface/bridge/port/add \ bridge="$brIface" \ interface="$wlanIface" \ comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global wireguardAddInterface do={ :put "Trying to add /interface/wireguard '$wgIface' ..." :if ([:len [/interface/wireguard/find name="$wgIface"]] = 0) do={ /interface/wireguard/add \ name="$wgIface" \ private-key="$wgPrivateKey" \ comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global wireguardAddPeer do={ :put "Trying to add peer /interface/wireguard/peers for '$wgIface' iface ..." :if ([:len [/interface/wireguard/peers/find interface="$wgIface"]] = 0) do={ /interface/wireguard/peers/add \ interface="$wgIface" \ public-key="$wgPublicKey" \ endpoint-address="$wgEndpointAddress" \ endpoint-port="$wgEndpointPort" \ allowed-address="$wgAllowedAddress" \ persistent-keepalive="$wgPersistentKeepalive" \ client-dns="$dns" \ comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global routingAddTable do={ :put "Trying to add /routing/table '$rtTable' ..." :if ([:len [/routing/table/find name="$rtTable"]] = 0) do={ /routing/table/add name="$rtTable" fib comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global routingAddRule do={ :put "Trying to add /routing/rule '$rtTable' ..." :if ([:len [/routing/rule/find table="$rtTable" src-address="$srcAddress"]] = 0) do={ :if ([:len [/routing/rule/find src-address="$srcAddress"]] != 0) do={ /routing/rule/remove numbers=[find src-address="$srcAddress"] :put $srcAddress } /routing/rule/add \ table="$rtTable" \ src-address="$srcAddress" \ action=lookup \ comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global ipAddPool do={ :put "Trying to add /ip/pool '$ipPool' ..." :if ([:len [/ip/pool/find name="$ipPool"]] = 0) do={ /ip/pool/add \ name="$ipPool" \ ranges="$ipRangeAddresses" \ comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global ipAddRoute do={ :put "Trying to add /ip/route for table '$rtTable' ..." :if ([:len [/ip/route/find routing-table="$rtTable"]] = 0) do={ /ip/route/add \ routing-table="$rtTable" \ dst-address="$dstAddress" \ gateway="$gateway" \ comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global ipAddDhcpServer do={ :put "Trying to add /ip/dhcp-server '$dhcpServer' ..." :if ([:len [/ip/dhcp-server/find name="$dhcpServer"]] = 0) do={ /ip/dhcp-server/add \ name="$dhcpServer" \ interface="$dhcpServerIface" \ address-pool="$dhcpServerAddressPool" \ comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global ipDhcpServerAddNetwork do={ :put "Trying to add /ip/dhcp-server/network '$dhcpNetwork' ..." :if ([:len [/ip/dhcp-server/network/find address="$dhcpNetwork"]] = 0) do={ /ip/dhcp-server/network/add \ address="$dhcpNetwork" \ gateway="$dhcpNetworkGateway" \ dns-server="$dhcpDns" \ comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global ipAddAddress do={ :put "Trying to add /ip/address '$ipAddress' on interface '$ipAddressInterface' ..." :if ([:len [/ip/address/find address="$ipAddress"]] = 0) do={ /ip/address/add \ address="$ipAddress" \ network="$ipAddressNetwork" \ interface="$ipAddressInterface" \ comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global interfaceListAddMember do={ :put "Trying to add /interface/list/member '$listMemeberInterface' to WG list ..." :if ([:len [/interface/list/member/find interface="$listMemeberInterface"]] = 0) do={ /interface/list/member/add \ list=WG \ interface="$listMemeberInterface" \ comment="auto-generated" :put " ↳ Done." } else={ :put " ↳ Already exists. Nothing to do." } } :global enableAP do={ :put "Trying to Enable Virtual Access Point and all related intities ..." /ip/dhcp-server/enable $wlanIface /ip/address/enable numbers=[find interface=$brIface] /interface/bridge/port/enable numbers=[find bridge=$brIface] /interface/bridge/enable numbers=[find name=$brIface] /interface/wireless/enable numbers=[find name=$wlanIface] } :global disableAP do={ :put "Trying to Disable Virtual Access Point and all related intities ..." /ip/dhcp-server/disable $wlanIface /ip/address/disable numbers=[find interface=$brIface] /interface/bridge/port/disable numbers=[find bridge=$brIface] /interface/bridge/disable numbers=[find name=$brIface] /interface/wireless/disable numbers=[find name=$wlanIface] } :global enableWG do={ :put "Trying to Enable WG interface $wgIface and all related intities ..." /interface/list/member/enable numbers=[find interface=$wgIface] /ip/route/enable numbers=[find routing-table=$rtTable] /routing/rule/enable numbers=[find table=$rtTable] /routing/table/enable $rtTable /interface/wireguard/peers/enable numbers=[find interface=$wgIface] /interface/wireguard/enable $wgIface /ip/address/enable numbers=[find interface=$wgIface] :put " ↳ Done." } :global disableWG do={ :put "Trying to Disable WG interface $wgIface and all related intities ..." /interface/list/member/disable numbers=[find interface=$wgIface] /ip/route/disable numbers=[find routing-table=$rtTable] /routing/rule/disable numbers=[find table=$rtTable] /routing/table/disable $rtTable /interface/wireguard/peers/disable numbers=[find interface=$wgIface] /interface/wireguard/disable $wgIface /ip/address/disable numbers=[find interface=$wgIface] :put " ↳ Done." } :global arrToStr do={ :local str "empty" foreach item in=($1) do={ :if ($str != "empty") do={ :set str ($str. $2 .$item) } else={ :set str $item } } :return $str } :global arrKeyToStr do={ :local str "empty" foreach k,v in=($1) do={ :if ($str != "empty") do={ :set str ($str. $2 .$k) } else={ :set str $k } } :return $str } :global strToArr do={ :local str $1 :local sep $2 :local arr ({}) :local idx 0 :local cont 1 while ($cont = 1) do={ :local pos [:find $str $sep] :if ([:typeof $pos] = "num") do={ :set ($arr->$idx) [:pick $str 0 $pos] :set idx ($idx + 1) :set str [:pick $str ($pos+1) [:len $str]] } else={ :set ($arr->$idx) [:pick $str 0 [:len $str]] :set cont 0 } } :return $arr } :global msgToArr do={ :global strToArr :local msg $1 :local prof $2 :local str [:pick $msg [:find $msg $prof] [:len $msg]] :if ([:len [:find $str " "]] = 0) do={ :set str [:pick $str [:len $prof] [:len $str]] } else={ :set str [:pick $str [:len $prof] [:find $str " "]] } :return [$strToArr $str ","] } :put "### END SCRIPT functions4vpn.rsc ###"
setup4vpn.rsc
:put "### START SCRIPT setup4vpn.rsc ###" :global somePrepares :global wirelessAddSecProfile :global wirelessAddInterface :global bridgeAddInterface :global bridgeAddPort :global wireguardAddInterface :global wireguardAddPeer :global routingAddTable :global routingAddRule :global ipAddPool :global ipAddRoute :global ipAddDhcpServer :global ipDhcpServerAddNetwork :global ipAddAddress :global interfaceListAddMember :global enableWgTunnel :global disableWgTunnel :local json [/file get setup_profiles.json contents] ; :local d [:deserialize from=json $json] ; :local wlanMasterIface ($d->"common"->"wlan"->"master-interface") ; :local wlanMode ($d->"common"->"wlan"->"mode") ; :local wlanAuthTypes ($d->"common"->"wlan"->"authentication-types") ; :local wlanUnicastCiphers ($d->"common"->"wlan"->"unicast-ciphers") ; :local wlanGroupCiphers ($d->"common"->"wlan"->"group-ciphers") ; :local wlanNetmask ($d->"common"->"wlan"->"netmask") ; :local dns ($d->"common"->"dns") ; :local wgAllowedAddress ($d->"common"->"wg"->"allowed-address") ; :local wgPersistentKeepalive ($d->"common"->"wg"->"persistent-keepalive") ; :local wgPrivateKey ($d->"common"->"wg"->"private-key") ; :local wgNetwork ($d->"common"->"wg"->"network") ; :local wgNetAddress [:pick $wgNetwork 0 [:find $wgNetwork "/"]] ; :local wgNetmask [:pick $wgNetwork ([:find $wgNetwork "/"] + 1) [:len $wgNetwork]] ; :local profilesArray ({}) :local outFile flash/4vpn/installed_profiles.json $somePrepares foreach p in=($d->"profiles") do={ :local profName ($p->"name") ; :local wlanSsid ($p->"ssid") ; :local wlanPass ($p->"wpa2-pre-shared-key") ; :local wgPublicKey ($p->"wg"->"public-key") ; :local wgEndpointAddress ($p->"wg"->"endpoint-address") ; :local wgEndpointPort ($p->"wg"->"endpoint-port") ; :local netIndex ($p->"netindex") ; :local 3octets [:pick $wgNetAddress 0 ([:len $wgNetAddress] - [:find $wgNetAddress "." -1])] :local 2octets [:pick $3octets 0 ([:len $3octets] - [:find $3octets "." -1])] :local wlanNet3 ($2octets.".".$netIndex) :local wlanNetAddress ($wlanNet3.".0") :local wlanNetwork ($wlanNetAddress.$wlanNetmask) :local wlanGatewayIp ($wlanNet3.".1") :local wgGatewayIp ($2octets.".200.".$netIndex) :put "---- Profile '$profName' ----" # Wireless $wirelessAddSecProfile \ profName=$profName \ wlanMode=$wlanMode \ wlanAuthTypes=$wlanAuthTypes \ wlanUnicastCiphers=$wlanUnicastCiphers \ wlanGroupCiphers=$wlanGroupCiphers \ wlanPass=$wlanPass :local wlanIface "wlan-wg-$profName" $wirelessAddInterface \ wlanIface=$wlanIface \ wlanSsid=$wlanSsid \ wlanMasterIface=$wlanMasterIface \ profName=$profName ## Bridge :local brIface "br-wg-$profName" $bridgeAddInterface brIface=$brIface $bridgeAddPort brIface=$brIface wlanIface=$wlanIface ## IP :local ipPool "wlan-wg-$profName" :local ipRangeAddresses ($wlanNet3 . ".10-" . $wlanNet3 . ".20") $ipAddPool ipPool=$ipPool ipRangeAddresses=$ipRangeAddresses $ipAddAddress \ ipAddress=($wlanGatewayIp . $wlanNetmask) \ ipAddressNetwork=$wlanNetAddress \ ipAddressInterface=$brIface :local dhcpServer "wlan-wg-$profName" $ipAddDhcpServer \ dhcpServer=$dhcpServer \ dhcpServerIface=$brIface \ dhcpServerAddressPool=$ipPool $ipDhcpServerAddNetwork \ dhcpNetwork=$wlanNetwork \ dhcpNetworkGateway=$wlanGatewayIp \ dhcpDns=$dns ## Only for non 'world' profiles :if ($profName != "world") do={ ## Wireguard :local wgIface "wg-$profName" $wireguardAddInterface wgIface=$wgIface wgPrivateKey=$wgPrivateKey $wireguardAddPeer \ wgIface=$wgIface \ wgPublicKey=$wgPublicKey \ wgEndpointAddress=$wgEndpointAddress \ wgEndpointPort=$wgEndpointPort \ wgAllowedAddress=$wgAllowedAddress \ wgPersistentKeepalive=$wgPersistentKeepalive \ dns=$dns ## Routing :local rtTable "via-wg-$profName" $routingAddTable rtTable=$rtTable $routingAddRule rtTable=$rtTable srcAddress=$wlanNetwork :local wgGatewayDev "%wg-$profName" $ipAddRoute rtTable=$rtTable dstAddress="0.0.0.0/0" gateway=$wgGatewayDev ## IP $ipAddAddress \ ipAddress=($wgGatewayIp."/32") \ ipAddressNetwork=$wgGatewayIp \ ipAddressInterface=$wgIface $interfaceListAddMember listMemeberInterface=$wgIface :local a ({}) :set ($a->"wgIface") $wgIface :set ($a->"brIface") $brIface :set ($a->"wlanIface") $wlanIface :set ($a->"rtTable") $rtTable :set ($a->"wlanNetwork") $wlanNetwork :set ($a->"wlanNetAddress") $wlanNetAddress :set ($a->"wlanNetmask") $wlanNetmask :set ($a->"wlanGatewayIp") $wlanGatewayIp :set ($a->"wgGatewayIp") $wgGatewayIp :set ($a->"dhcpServer") $dhcpServer :set ($a->"ipPool") $ipPool :set ($profilesArray->"$profName") $a } else={ ## Routing :local rtTable ("via-wg-".($p->"via")) $routingAddRule rtTable=$rtTable srcAddress=$wlanNetwork :local a ({}) :set ($a->"brIface") $brIface :set ($a->"wlanIface") $wlanIface :set ($a->"wlanNetwork") $wlanNetwork :set ($a->"wlanNetAddress") $wlanNetAddress :set ($a->"wlanNetmask") $wlanNetmask :set ($a->"wlanGatewayIp") $wlanGatewayIp :set ($a->"dhcpServer") $dhcpServer :set ($a->"ipPool") $ipPool :set ($profilesArray->"$profName") $a } } :if ([:len [/file/find name=$outFile]] = 0) do={ /file/add name=$outFile content=[:serialize value=$profilesArray to=json] } else={ /file/set numbers=[find name=$outFile] content=[:serialize value=$profilesArray to=json] } /interface/wireless/enable numbers=[find name=wlan-wg-world] :put "### END SCRIPT setup4vpn.rsc ###"
manage4vpn.rsc
:put "### START SCRIPT manage4vpn.rsc ###" :global enableWG :global disableWG :global enableAP :global disableAP :global routingAddRule :local json [/file get flash/4vpn/desired_profiles.json contents] :local desiredProfiles [:deserialize from=json $json] :local worldApProfile ($desiredProfiles->"worldAP"->"current") :local countryApProfiles ($desiredProfiles->"countryAP") :local json [/file get flash/4vpn/installed_profiles.json contents] :local installedProfiles [:deserialize from=json $json] :local cacheFile "state_cache.json" :local stateCache :if ([:len [/file/find name=$cacheFile]] = 0) do={ :set stateCache ({}) } else={ :local json [/file get $cacheFile contents] :set stateCache [:deserialize from=json $json] } :local undef ({}) :set ($undef->"wg") "undef" :set ($undef->"ap") "undef" foreach profName,p in=($installedProfiles) do={ :local wgIface ($p->"wgIface") ; :local brIface ($p->"brIface") ; :local wlanIface ($p->"wlanIface") ; :local rtTable ($p->"rtTable") ; :local wlanNetwork ($p->"wlanNetwork") ; :local wlanNetAddress ($p->"wlanNetAddress") :local wlanNetmask ($p->"wlanNetmask") :local wlanGatewayIp ($p->"wlanGatewayIp") :local wgGatewayIp ($p->"wgGatewayIp") :local dhcpServer ($p->"dhcpServer") :local dhcpServer ($p->"dhcpServer") :local ipPool ($p->"ipPool") :put "---- Profile '$profName' ----" ## Only for non 'world' profiles :if ($profName != "world") do={ :if ([:typeof ($stateCache->$profName->"wg")] = "nothing") do={ # init stateCache array :set ($stateCache->$profName) $undef } :local res [:find $countryApProfiles $profName] # if desired profile founded if ([:typeof $res] = "num") do={ # Enable WG :if ( (($stateCache->$profName->"wg") = "undef") or (($stateCache->$profName->"wg") = "disabled") ) do={ $enableWG wgIface=$wgIface rtTable=$rtTable srcAddress=$wlanNetwork :set ($stateCache->$profName->"wg") "enabled" } else={ :put "Enable WG. Cache hit. Nothing to do." } # Enable AP :if ( (($stateCache->$profName->"ap") = "undef") or (($stateCache->$profName->"ap") = "disabled") ) do={ $enableAP wgIface=$wgIface rtTable=$rtTable srcAddress=$wlanNetwork wlanIface=$wlanIface brIface=$brIface :set ($stateCache->$profName->"ap") "enabled" } else={ :put "Enable AP. Cache hit. Nothing to do." } } else={ if ($worldApProfile != $profName) do={ # Disable WG :if ( (($stateCache->$profName->"wg") = "undef") or (($stateCache->$profName->"wg") = "enabled") ) do={ $disableWG wgIface=$wgIface rtTable=$rtTable srcAddress=$wlanNetwork :set ($stateCache->$profName->"wg") "disabled" } else={ :put "Disable WG. Cache hit. Nothing to do." } } else={ # if world AP refers to current WG profile # Enable WG :if ( (($stateCache->$profName->"wg") = "undef") or (($stateCache->$profName->"wg") = "disabled") ) do={ $enableWG wgIface=$wgIface rtTable=$rtTable srcAddress=$wlanNetwork :set ($stateCache->$profName->"wg") "enabled" } else={ :put "Enable WG. Cache hit. Nothing to do." } } # Disable AP :if ( (($stateCache->$profName->"ap") = "undef") or (($stateCache->$profName->"ap") = "enabled") ) do={ $disableAP wgIface=$wgIface rtTable=$rtTable srcAddress=$wlanNetwork wlanIface=$wlanIface brIface=$brIface :set ($stateCache->$profName->"ap") "disabled" } else={ :put "Disable AP. Cache hit. Nothing to do." } } } else={ ## :local rtTable "via-wg-$worldApProfile" $routingAddRule rtTable=$rtTable srcAddress=$wlanNetwork } } :if ([:len [/file/find name=$cacheFile]] = 0) do={ /file/add name=$cacheFile content=[:serialize value=$stateCache to=json] } else={ /file/set numbers=[find name=$cacheFile] content=[:serialize value=$stateCache to=json] } :put "### END SCRIPT manage4vpn.rsc ###"
rotate4vpn.rsc
:put "### START SCRIPT rotate4vpn.rsc ###" :local lock do={ :while ([:len [/file/find name=$1]] > 0) do={ :put "delay 1s" :delay 1 } :put $lockFile /file/add name=$1 } :local unlock do={ /file/remove $1 } :local lockFile "4vpn.lock" $lock $lockFile /system/script/run functions4vpn :do { /system/script/run rotate_profile4vpn } on-error={ :put "The \"/system/script/run rotateprofile4vpn\" failed" } :do { /system/script/run manage4vpn } on-error={ :put "The \"/system/script/run manage4vpn\" failed" } $unlock $lockFile :put "### END SCRIPT rotate4vpn.rsc ###"
rotate_profile4vpn.rsc
:put "### START SCRIPT rotate_profile4vpn.rsc ###" :local jsonName "flash/4vpn/desired_profiles.json" :local json [/file get $jsonName contents] :local desiredProfiles [:deserialize from=json $json] :local worldApProfile ($desiredProfiles->"worldAP"->"current") :local rotateProfiles ($desiredProfiles->"worldAP"->"rotate") :local count [:len $rotateProfiles] :local nextProfile for i from=0 to=($count-1) do={ if ($worldApProfile = ($rotateProfiles->$i)) do={ :set nextProfile ($rotateProfiles->(($i+1) % count)) } } :set ($desiredProfiles->"worldAP"->"current") $nextProfile /file/set numbers=[find name=$jsonName] content=[:serialize value=$desiredProfiles to=json] :put "### END SCRIPT rotate_profile4vpn.rsc ###"
tgbot4vpn.rsc
:put "### START SCRIPT tgbot4vpn.rsc ###" :global updateId :global arrToStr :global arrKeyToStr :global strToArr :global msgToArr :local chatId IDIDIDIDIDID :local token TOKENTOKENTOKENTOKENTOKEN :local url "https://api.telegram.org/bot" :local requestGet ($url.$token."/"."getUpdates?offset=-1") :local requestPost ($url.$token."/"."sendMessage") :local jsonName "flash/4vpn/desired_profiles.json" :local installedProfFile "flash/4vpn/installed_profiles.json" :local text; :local d; :local r :retry delay=3s max=3 on-error={:put "All retries failed."} command={ :put "[/tool/fetch] Trying to getUpdates from telegram" :set r [/tool/fetch mode=https url=$requestGet as-value output=user] :put " ↳ Done." } :if ($r->"status" = "finished") do={ :set d [:deserialize from=json ($r->"data")] :if (($d->"ok") = true) do={ :if (($d->"result"->0->"message"->"chat"->"id") = $chatId) do={ :if ($updateId != ($d->"result"->0->"update_id")) do={ :set text ($d->"result"->0->"message"->"text") :if (($text ~ "^/get( .+)*") or ($text ~ "^/set .+") or ($text = "/help") or ($text = "/rotate")) do={ :local messageId ($d->"result"->0->"message"->"message_id") :local json [/file get $jsonName contents] :local desiredProfiles [:deserialize from=json $json] :local worldApProfile ($desiredProfiles->"worldAP"->"current") :local rotateStr [$arrToStr ($desiredProfiles->"worldAP"->"rotate") ","] :local countryStr [$arrToStr ($desiredProfiles->"countryAP") ","] :local json [/file get $installedProfFile contents] :local installedProfiles [:deserialize from=json $json] :local instProfStr [$arrKeyToStr $installedProfiles ","] :if ($text ~ "^/get( .+)*") do={ :local data "{\"chat_id\":$chatId,\"parse_mode\":\"MarkdownV2\",\"reply_to_message_id\":\"$messageId\",\"text\":\"" :set data ($data."*Current settings*\n_worldAP_: \n `".$worldApProfile." [".$rotateStr."]`\n") :set data ($data."_countryAP_: \n `".$countryStr."`\n") :set data ($data."_Available profiles_: \n `".$instProfStr."`\"}") :retry delay=3s max=3 on-error={:put "All retries failed."} command={ :put "[/tool/fetch] Trying send Current settings to telegram" /tool/fetch mode=https http-method=post url=$requestPost http-header-field="Content-Type: application/json" http-data=$data as-value output=none :put " ↳ Done." } } :if ($text ~ "^/set .+") do={ :if ([$msgToArr $text "world:"] != {}) do={ :set ($desiredProfiles->"worldAP"->"current") [$msgToArr $text "world:"] } :if ([$msgToArr $text "rotate:"] != {}) do={ :set ($desiredProfiles->"worldAP"->"rotate") [$msgToArr $text "rotate:"] } :if ([$msgToArr $text "country:"] != {}) do={ :set ($desiredProfiles->"countryAP") [$msgToArr $text "country:"] } /file/set numbers=[find name=$jsonName] content=[:serialize value=$desiredProfiles to=json] :set rotateStr [$arrToStr ($desiredProfiles->"worldAP"->"rotate") ","] :set countryStr [$arrToStr ($desiredProfiles->"countryAP") ","] :local data "{\"chat_id\":$chatId,\"parse_mode\":\"markdown\",\"reply_to_message_id\":\"$messageId\",\"text\":\"" :set data ($data."*New settings*\n_worldAP_: \n `".($desiredProfiles->"worldAP"->"current")." [".$rotateStr."]`\n") :set data ($data."_countryAP_: \n `".$countryStr."`\"}") :retry delay=3s max=3 on-error={:put "All retries failed."} command={ :put "[/tool/fetch] Trying send New settings to telegram" /tool/fetch mode=https http-method=post url=$requestPost http-header-field="Content-Type: application/json" http-data=$data as-value output=none :put " ↳ Done." } } :if ($text = "/rotate") do={ /system/script/run rotate_profile4vpn :local json [/file get $jsonName contents] :local desiredProfiles [:deserialize from=json $json] :local data "{\"chat_id\":$chatId,\"parse_mode\":\"markdown\",\"reply_to_message_id\":\"$messageId\",\"text\":\"" :set data ($data."*New settings*\n_worldAP_: \n `".($desiredProfiles->"worldAP"->"current")." [".$rotateStr."]`\n") :set data ($data."_countryAP_: \n `".$countryStr."`\"}") :retry delay=3s max=3 on-error={:put "All retries failed."} command={ :put "[/tool/fetch] Trying send New settings to telegram" /tool/fetch mode=https http-method=post url=$requestPost http-header-field="Content-Type: application/json" http-data=$data as-value output=none :put " ↳ Done." } } :if ($text = "/help") do={ :local data "{\"chat_id\":$chatId,\"parse_mode\":\"MarkdownV2\",\"reply_to_message_id\":\"$messageId\",\"text\":\"" :set data ($data."_Get current settings:_\n```\n/get```\n") :set data ($data."_Rotate WG profile for worldAP:_\n```\n/rotate```\n") :set data ($data."_Set new settings:_\n") :set data ($data."```\n/set world:de-fra```\n") :set data ($data."```\n/set rotate:us-nyc,de-fra```\n") :set data ($data."```\n/set country:ae-dub,de-fra,in-del```\n") :set data ($data."```\n/set world:de-fra rotate:us-nyc,de-fra```\n") :set data ($data."```\n/set rotate:us-nyc,de-fra country:ae-dub,de-fra,in-del```\n") :set data ($data."```\n/set world:de-fra rotate:us-nyc,de-fra country:ae-dub,de-fra,in-del```\n") :set data ($data."_Help:_\n```\n/help```\"}") :retry delay=3s max=3 on-error={:put "All retries failed."} command={ :put "[/tool/fetch] Trying send Help to telegram" /tool/fetch mode=https http-method=post url=$requestPost http-header-field="Content-Type: application/json" http-data=$data as-value output=none :put " ↳ Done." } } :set updateId ($d->"result"->0->"update_id") } } } } } put "### END SCRIPT tgbot4vpn.rsc ###"
scheduler_manage4vpn.rsc
:put "### START SCRIPT scheduler_manage4vpn.rsc ###" :local lock do={ /file/add name=$1 } :local unlock do={ /file/remove $1 } :local lockFile "4vpn.lock" :if ([:len [/file/find name=$lockFile]] = 0) do={ $lock $lockFile /system/script/run functions4vpn :do { /system/script/run tgbot4vpn } on-error={ :put "The \"/system/script/run tgbot4vpn\" failed" } :do { /system/script/run manage4vpn } on-error={ :put "The \"/system/script/run manage4vpn\" failed" } $unlock $lockFile } else={ :put "Found $lockFile file. Nothing to do." } :put "### END SCRIPT scheduler_manage4vpn.rsc ###"
install_scripts4vpn.rsc
:put "### START SCRIPT install_scripts4vpn.rsc ###" :local arr { "functions4vpn"; "setup4vpn"; "manage4vpn"; "rotate_profile4vpn"; "rotate4vpn"; "tgbot4vpn"; "scheduler_manage4vpn" } foreach script in=($arr) do={ :put $script :do { /system/script/remove $script } on-error={} /system/script/add name=$script source=[/file/get ($script . ".rsc") contents] } /system/script/run setup4vpn; /file/remove setup_profiles.json /system/routerboard/reset-button/set hold-time=0s..1s on-event=rotate4vpn enabled=yes :do {/system/scheduler/remove scheduler_manage4vpn} on-error={} /system/scheduler/add name=scheduler_manage4vpn interval=15s on-event=scheduler_manage4vpn :put "### START SCRIPT install_scripts4vpn.rsc ###"
setup_profiles.json
{ "common": { "wlan": { "master-interface": "wlan1", "mode": "dynamic-keys", "authentication-types": "wpa2-psk", "unicast-ciphers": "aes-ccm", "group-ciphers": "aes-ccm", "netmask": "/24" }, "wg": { "allowed-address": "0.0.0.0/0", "persistent-keepalive": "20s", "private-key": "privatekeyprivatekeyprivatekeyprivatekey", "network": "10.14.0.0/16" }, "dns": "8.8.4.4" }, "profiles": [ { "name": "tr-ist", "ssid": "MikroTrIst", "wpa2-pre-shared-key": "tristtrist", "wg": { "public-key": "publickeypublickeypublickeypublickeypublickey", "endpoint-address": "tr-ist.vpnproviderserver_com", "endpoint-port": 51820 }, "netindex": 1 }, { "name": "us-nyc", "ssid": "MikroUsNyc", "wpa2-pre-shared-key": "usnycusnyc", "wg": { "public-key": "publickeypublickeypublickeypublickeypublickey", "endpoint-address": "us-nyc.vpnproviderserver_com", "endpoint-port": 51820 }, "netindex": 2 }, { "name": "ae-dub", "ssid": "MikroAeDub", "wpa2-pre-shared-key": "aedubaedub", "wg": { "public-key": "publickeypublickeypublickeypublickeypublickey", "endpoint-address": "ae-dub.vpnproviderserver_com", "endpoint-port": 51820 }, "netindex": 3 }, { "name": "in-del", "ssid": "MikroInDel", "wpa2-pre-shared-key": "indelindel", "wg": { "public-key": "publickeypublickeypublickeypublickeypublickey", "endpoint-address": "in-del.vpnproviderserver_com", "endpoint-port": 51820 }, "netindex": 4, "enabled": "true" }, { "name": "de-fra", "ssid": "MikroDeFra", "wpa2-pre-shared-key": "defradefra", "wg": { "public-key": "publickeypublickeypublickeypublickeypublickey", "endpoint-address": "de-fra.vpnproviderserver_com", "endpoint-port": 51820 }, "netindex": 77 }, { "name": "world", "ssid": "MikroWorld", "wpa2-pre-shared-key": "worldworldworld", "via": "tr-ist", "netindex": 199 } ] }
desired_profiles.json
{ "worldAP": { "current": "tr-ist", "rotate": [ "tr-ist", "us-nyc", "de-fra" ] }, "countryAP": [ "in-del", "de-fra" ] }
Достаточно
заполнить на свой вкус
setup_profiles.jsonиdesired_profiles.jsonзаменить
chatIdиtokenвtgbot4vpn.rscзалить все
.rscиsetup_profiles.jsonфайлы в корень ФС mikrotik (например так:scp *.rsc setup_profiles.json mikrotik:или через webfig)залить
desired_profiles.jsonвflash/4vpn/директорию mikrotik (например так:scp desired_profiles.json mikrotik:flash/4vpn/)выполнить на роутере команду:
/import functions4vpn.rsc; /import install_scripts4vpn.rsc
Если вы использовали такой же набор профилей как из примера выше (файлы setup_profiles.json и desired_profiles.json), то у вас поднимутся 2 точки доступа (SSID: MikroInDel, MikroDeFra) и 2 WireGuard туннеля до India/Delhi и Germany/Frankfurt, соответственно. Плюс MikroWorld AP с туннелем до Turkey/Istanbul.
Управление
MikroWorld AP
Жму reset-button на корпусе mikrotik, переключаюсь на следующий VPN профиль (WG профиль) из списка ротации.
Или через telegram бот
/rotate
или
/set world:<profile>
/set rotate:us-nyc,de-fra
Было/Стало
Было
[admin@RouterOS] > /routing/rule/print where src-address="10.14.199.0/24" Flags: X - disabled, I - inactive 11 ;;; auto-generated src-address=10.14.199.0/24 action=lookup table=via-wg-us-nyc
Стало
[admin@RouterOS] > /routing/rule/print where src-address="10.14.199.0/24" Flags: X - disabled, I - inactive 13 ;;; auto-generated src-address=10.14.199.0/24 action=lookup table=via-wg-de-fra
Mikro<Country><City> AP
Управление через telegram бот
/set country:ae-dub,de-fra,in-del
Было/Стало
Было
[admin@RouterOS] > /interface/wireless/print proplist=ssid without-paging where !disabled Flags: X - disabled; R - running 5 ;;; auto-generated ssid="MikroDeFra" 6 ;;; auto-generated ssid="MikroInDel" 10 ;;; auto-generated ssid="MikroWorld" 11 R ssid="MikroTik"
Стало
[admin@RouterOS] > /interface/wireless/print proplist=ssid without-paging where !disabled Flags: X - disabled; R - running 0 ;;; auto-generated ssid="MikroAeDub" 5 ;;; auto-generated ssid="MikroDeFra" 6 ;;; auto-generated ssid="MikroInDel" 10 ;;; auto-generated ssid="MikroWorld" 11 R ssid="MikroTik"
Детали
Логика скриптов
install_scripts4vpn.rsc
Устанавливает на роутер следующие скрипты: functions4vpn, setup4vpn, manage4vpn, rotate_profile4vpn, rotate4vpn, tgbot4vpn, scheduler_manage4vpn
Запускает скрипт setup4vpn
Удаляет setup_profiles.json
Вешает запуск скрипта rotate4vpn на нажатие кнопки reset-button
Добавляет в шедулер задание на запуск скрипта scheduler_manage4vpn каждые 15 секунд
functions4vpn.rsc
Тут набор глобальных функций для манипуляции интерфейсами, ip адресами, роутами, файрволом и другими сущностями в RouterOS, плюс пару вспомогательных функций
setup4vpn.rsc
Читает файл
setup_profiles.json, настраивает Access Point, WireGuard, DHCP сервер, ip адреса, маршрутизацию, файрвол и другие необходимые сущностиГенерирует файл
flash/4vpn/installed_profiles.json, работать с которым, далее, будет скриптmanage4vpn
manage4vpn.rsc
В зависимости от содержимого flash/4vpn/desired_profiles.json, либо активирует, либо деактивирует логические сущности AP и/или WG (wireless, wireguard интерфейсы, роуты, файрвол и т.д.).
Чтобы ускорить работу скрипта (за счет уменьшения количества обращений к RouterOS) используется кэш. Содержимое кэша (файл state_cache.json):
{ "ae-dub": { "ap": "enabled", "wg": "enabled" }, "de-fra": { "ap": "enabled", "wg": "enabled" }, "in-del": { "ap": "enabled", "wg": "enabled" }, "tr-ist": { "ap": "disabled", "wg": "disabled" }, "us-nyc": { "ap": "disabled", "wg": "disabled" } }
Кэш-файл можно удалить без риска нарушения работы:
[admin@RouterOS] > /file/remove state_cache.json
rotate_profile4vpn.rsc
Выбирает название следующего WG профиля для MikroWorld AP и записывает его в файл flash/4vpn/desired_profiles.json
tgbot4vpn.rsc
Простой telegram бот. Читает последнее сообщение (команду) из чата с пользователем и исполняет ее. Чтобы не повторяться, ID последнего сообщения из чата запоминается.
Здесь важны 2 переменные: "token" и "chatId". Первая, очевидно, это токен вашего бота (нужно создать собственный бот через BotFather и там же взять токен), вторая - идентификатор пользователя, под которым боту отправляются команды. Нужен, чтобы только вы могли командовать своим ботом, и своим роутером, соответственно.
Узнать свой chatId
Написать сообщение боту и:
curl "https://api.telegram.org/bot<TOKEN>/getUpdates?offset=-1" | jq '.result[0].message.from.id'
rotate4vpn.rsc
Именно этот скрипт ассоциирован с кнопкой reset-button:
[admin@RouterOS] > /system/routerboard/reset-button/print enabled: yes hold-time: 0s..1s on-event: rotate4vpn
Вызывает 2 других скрипта: rotate_profile4vpn и manage4vpn.
Чтобы между событиями "нажатие reset-button" и "запуск таска по шедулеру" не было "гонки", берется lock.
scheduler_manage4vpn.rsc
[admin@RouterOS] > /system/scheduler/print Columns: NAME, START-DATE, START-TIME, INTERVAL, ON-EVENT, RUN-COUNT # NAME START-DATE START-TIME INTERVAL ON-EVENT RUN-COUNT 0 scheduler_manage4vpn 2026-02-27 23:40:21 15s scheduler_manage4vpn 114
Вызывается каждые 15 секунд по событию от шедулера. Логика похожа на rotate4vpn.rsc, тоже под lock'ом.
Вызывает скрипты: tgbot4vpn и manage4vpn.
Дефолт
Захотелось мне завернуть через VPN и трафик из дефолтной сети (192.168.88.0/24). Прописал в качестве шлюза по-умолчанию один из WG интерфейсов, и, конечно же, все сломалось. Проблема "курицы и яйца" - WG клиент пытается достучаться до WG сервера через свой же WG интерфейс.
Возможные решения:
"Понять и забить". Не использовать дефолтную сеть для работы через VPN (я выбрал этот пункт).
Прописать кучу статических маршрутов до WG серверов, которые периодически нужно будет переписывать, так как ip адреса имеют свойство меняться. (Как вариант взять публичные сети VPN провайдера, которые конечно тоже могут меняться). На MikroTik такую машинерию можно поднять, но это дополнительная нагрузка на и так не очень быстрое железо.
Маркировать output трафик прямо в WG клиенте и маршрутизировать напрямую? Не копал, не знаю умеет ли такое MikroTik.
Маркируем трафик дефолтной сети по условию
src-address=192.168.88.0/24 and dst-address=!192.168.88.0/24и маршрутизируем через нужную WG таблицу.
Последние настраивается так:
[admin@RouterOS] > /ip/firewall/mangle/add \ chain=prerouting \ src-address=192.168.88.0/24 \ dst-address=!192.168.88.0/24 \ action=mark-routing \ new-routing-mark="via-wg-de-fra"
[admin@RouterOS] > /ip/route/add \ routing-table="via-wg-de-fra" \ dst-address="0.0.0.0/0" \ gateway="%wg-de-fra"
У меня не взлетело. Плюс маркировка не бесплатна и грузит CPU. Перестал копать.
Masquerade
Если ваш VPN провайдер отдал вам не /16сеть, а, скажем, /24 или просто /32, и/или вам не хочется думать над IP планом сети, а просто взять готовый, например, тот же описанный выше setup_profiles.json, и чтобы все заработало, нужен один дополнительный шаг - включить SNAT/MASQUERADE:
[admin@RouterOS] > /ip/firewall/nat/add chain=srcnat out-interface-list=WG action=masquerade
Производительность
Глубоко в тесты не погружался. Использовал обычный speedtest и ping.
При условии что подняты MikroWorld AP + пару Mikro<Country><City> AP, можно рассчитывать на такие цифры: 20-30 Mbps download и 25-35 Mbps upload для mAP lite.
Или в эквиваленте видео: спокойно можно смотреть 1440p. Можно и 4k, но бывают фризы.
Speedtest запускался многократно, с целевыми серверами в разных странах, с опцией "connection type: multi". Иногда цифры были ниже или выше, но большинство запусков укладывались в выше описанный диапазон. Плюс тесты "на глаз" в youtube.
Из неприятного: так как в mAP lite один физический радиоканал (как почти в любом другом домашнем роутере), то он делится между всеми поднятыми AP. Визуально это проявляется в повышенном latency до шлюза (192.168.88.1) даже без нагрузки (только icmp). И чем больше виртуальных AP поднято, тем хуже. Цифры могут доходить всплесками до сотен миллисекунд.
Вторая неприятность которая еще сильнее растягивает latency это CPU. На mAP lite ядро одно, и когда WireGuard съедает его почти полностью (примерно на 30Mbps), latency до шлюза вырастают в 5-10, а могут и более, раз.
Кстати, включение маскарадинга, визуально, на Mbps и rtt никак не влияет.
Эксперимент 100+
Загрузил 100+ VPN профилей. Ну как загрузил... попытался. Конфигурация на RouterOS и/или mAP lite, в целом, применяется, и скорее всего читается, не быстро. Пришло минут 15 прежде чем дело дошло до VPN профиля номер 90, когда mikrotik решил отдохнуть. (Вообще, кроме того что операции чтение и применение конфигурации долгие по времени, они, оказывается, еще и ресурсоемкие. Во время загрузки профилей, утилизация CPU ни разу не опустилась ниже 100%. WireGuard тихо курит). Походу вайфаю в какой-то момент не хватило CPU и моя сессия с роутером порвалась. Также нужно сказать, когда поднимается/опускается виртуальная точка доступа, физический wlan интерфейс реинициализируется (не всегда), что приводит к отключению клиента от wlan сети. Но мне было лень настраивать ethernet и я страдал на вайфае. Когда стало понятно что 100+ профилей загрузить по воздуху не судьба, я настроил ethernet на mikrotik. За те же 15-20 минут все профили прогрузились в роутер. (Понятно что загрузку профилей можно было бы запустить как фоновую задачу в роутере (через тот же scheduler), и тогда канал связи (wifi, ehternet) не играл бы роли, но хотелось отслеживать процесс в реальном времени в консоли).
После успешной загрузки 100+ профилей, настала очередь manage4vpn скрипта, который в итоге решает какие сущности в RouterOS нужно активировать, а какие деактивировать. Данный этап продлился тоже минут 20. Следующие запуски отрабатывали уже за пару секунд за счет кэша, созданного после первого прогона manage4vpn.
Короткий вывод: загрузить на mAP lite 100+ VPN профилей можно, и это даже будет сносно работать, до первого ребута. Конечно и ребут легко починить, поместив файл с кэшом в энергонезависимый раздел /flash, но на то он и кэш, чтобы, в том числе, его можно было легко сбросить, например, ребутнув роутер.
Есть нехорошее подозрение на то, что чем больше сущностей настроено в RouterOS, тем дольше длится операция поиска нужного ресурса, т.е. время поиска не константно. Но могу ошибаться.
Что не решено
Если по каким-то причинам, интернет-провайдер блокирует доступ к Telegram, бот на mikrotik не сможет читать команды.
Самое очевидное решение - поднять отдельный WG туннель, и завернуть трафик api.telegram.org в него.
Заключение
Получилось реализовать достаточно удобное управление множеством VPN профайлов на MikroTik.
Для mAP lite остановился на таком конфиге: 10 VPN профилей, MikroWorld AP + 2 Mikro<Country><City> AP.
Железо. mAP lite в качестве travel-роутера, можно сказать, смотрится идеально, особенно в соотношении размер/производительность (20-30 Mbps). Для дома, настоятельная рекомендация, выбрать что-нибудь помощнее, скажем на CPU с 2-мя ядрами и ARM архитектуре.
Полагаю, аналогичную конструкцию можно реализовать на любом роутере с поддержкой DD-WRT/OpenWRT, но это не точно.
Я всего лишь хотел поиграть в Xbox.
