Небольшой рассказ - туториал о том, как на 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).

Mikrotik RBmAPL-2nD (mAP lite)
Mikrotik RBmAPL-2nD (mAP lite)

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

Mikrotik RBmAP2nD (mAP)
Mikrotik RBmAP2nD (mAP)

Позже наткнулся на старшего брата 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 грамм. По ощущениям даже меньше. Можно положить в карман и забыть.

Сравнение mAP lite c часами G-Shock GA-2100RW и кейсом от AirPods Pro 2
Сравнение mAP lite c часами G-Shock GA-2100RW и кейсом от AirPods Pro 2

Включаю роутер, подключаюсь к 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 для виртуальной точки доступа
Wireless -> Security Profiles -> Add
Wireless -> Security Profiles -> Add

или

[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. Интерфейс виртуальной точки доступа
Wireless -> New
Wireless -> New

или

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

или

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

или

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

или

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

или

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

или

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

или

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

или

[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 интерфейс
WIreGuard -> New
WIreGuard -> New

или

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

или

[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
Routing -> Tables -> New
Routing -> Tables -> New

или

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

или

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

или

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

или

16. TCP MSS
IP -> Firewall -> Mangle -> New
IP -> Firewall -> Mangle -> New
продолжение
продолжение

или

[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 бот
Выбор следующего VPN профиля
Выбор следующего VPN профиля

/rotate

Выбор конкретного VPN профиля из списка ротации
Выбор конкретного VPN профиля из списка ротации

или

/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
  1. Устанавливает на роутер следующие скрипты: functions4vpn, setup4vpn, manage4vpn, rotate_profile4vpn, rotate4vpn, tgbot4vpn, scheduler_manage4vpn

  2. Запускает скрипт setup4vpn

  3. Удаляет setup_profiles.json

  4. Вешает запуск скрипта rotate4vpn на нажатие кнопки reset-button

  5. Добавляет в шедулер задание на запуск скрипта scheduler_manage4vpn каждые 15 секунд

functions4vpn.rsc

Тут набор глобальных функций для манипуляции интерфейсами, ip адресами, роутами, файрволом и другими сущностями в RouterOS, плюс пару вспомогательных функций

setup4vpn.rsc
  1. Читает файл setup_profiles.json, настраивает Access Point, WireGuard, DHCP сервер, ip адреса, маршрутизацию, файрвол и другие необходимые сущности

  2. Генерирует файл 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 интерфейс.

Возможные решения:

  1. "Понять и забить". Не использовать дефолтную сеть для работы через VPN (я выбрал этот пункт).

  2. Прописать кучу статических маршрутов до WG серверов, которые периодически нужно будет переписывать, так как ip адреса имеют свойство меняться. (Как вариант взять публичные сети VPN провайдера, которые конечно тоже могут меняться). На MikroTik такую машинерию можно поднять, но это дополнительная нагрузка на и так не очень быстрое железо.

  3. Маркировать output трафик прямо в WG клиенте и маршрутизировать напрямую? Не копал, не знаю умеет ли такое MikroTik.

  4. Маркируем трафик дефолтной сети по условию 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.