Контейнер с VPN для Mikrotik, претендующий на универсальность

Собирал для себя, но выглядит так, что получилось достаточно хорошо для публикации.
Основан на Xray-core, туда же добавлены бинарники wireproxy awg и byedpi.
Также geoip.dat и geosite.dat были заменены на варианты, адаптированные для РФ
Конкретно geosite.dat был взят в варианте geosite:ru-blocked, т.е. заблокированные домены + домены, доступные только из РФ.
Есть возможность собрать обрезанный geoip.dat, содержащий только списки для РФ(об этом ниже). Также оставлена возможность прямого подключения к wireproxy awg и byedpi, например через браузерный плагин(foxy proxy или аналоги) - оба бинарника биндятся на ip контейнера(по умолчанию byedpi на порт 9999, а wireproxy на 9998)

Для кого это нужно?

Для тех, кому нужен только byedpi или только амнезия, весь этот велосипед затаскивать нет никакого смысла. Проще воспользоваться решениями от тов. wiktorbgu
А вот если нужен Vless, shadowsocks и тому подобные инструменты, которые умеет xray - тогда да, тогда вам сюда

Почему был использован xray-core, а не sing-box?

Потому что именно xray-core поддерживает wireguard-inbound в качестве точек входа, что очень удобно для маршрутизации траффика. Фактически, внутри контейнера нет вообще никаких настроек маршрутизации, просто наружу торчат wireguard интерфейсы. Настройка роутера так же в связи с этим становится достаточно тривиальной и сводится к обычной конфигурации wireguard подключения. Сам контейнер при этом вообще не обязан размещаться на роутере, его вполне можно выселить на малинку, если роутеру не хватает ресурсов. Алярма! Attention! Achtung! В последней версии xray-core сломали маршрутизацию по тегам для wireguard инбаундов. Уже как будто чинят обратно, но пока самая рабочая версия - v24.10.31(которая и была использована)

Почему был использован wireproxy awg, а не нормальный amnesia wireguard в tun режиме?

Потому что лично у меня обычная сборка amnesia-wg работала криво, непредсказуемо отваливалась и по логам было совершенно непонятно, что именно с ней происходит. Wireproxy в свою очередь продемонстрировал высокую стабильность, простоту, а логов там даже слишком много. Из дополнительных плюсов - можно настроить ливнес пробы, можно вывесить наружу веб ручку с текущим статусом подключения и состоянием пробы, всё это при необходимости можно собирать wireproxy экспортером, собирать прометеем и выводить в графане. Т.е. весьма широкие возможности по настройке автоматизаций и мониторинга. В рамках данного контейнера не реализовано, кому такое надо - легко сами справятся с докой, но сам факт очень греет. Конфигурация так же проста как три рубля. Лично мне решение очень понравилось, единственный его минус - прожорливость до ОЗУ.

Зачем нужно было запихивать всё в один контейнер?

В целом - особо незачем. Обе прокси(что wireproxy, что ciadpi) спокойно могут жить в отдельных контейнерах, просто вывесив порт наружу. Работать будет точно так же. Но при запихивании в один контейнер добавление общего бизибокса стоит очень дешёво, зато предоставлят более-менее адекватный шелл для отладки и позволяет сделать человеческий вывод логов и хоть какую-то автоматизацию в генерации конфигов. Опять же, решение под ключ - это удобно.

Как это настраивать.
Для начала стоит учесть, что контейнеру для нормальной работы требуется хотя бы 80-100мб свободной оперативной памяти, а по настоящему хорошо он начинает чувствовать себя при 200-250Мб. Соотв hap AC2 - в пролёте, не нужно мучить зверушку, лучше докупить к нему малинку в помощь. Пробовать запихивать на сам роутер можно начиная с hap AC3, а лучше всего на hap AX2 или AX3. Вроде бы в тестовой ветке RouterOS прикрутили возможность добавить своп, но как именно это работает - лично я не проверял. Если кто хочет проэксперементировать - велком.
Также инструкция предполагает, что на текущий момент, читателем уже прочитаны материалы по контейнерам на ROS, например статья на хабре или офф. дока, а также есть понимание, как конфигурить wireguard интерфейсы на микротах(например можно ознакомиться со статьёй про соединение двух микротов через WG), т.е. не возникает слишком удивляющих вопросов на тему “чем это таким мы тут будем заниматься?”

Подготовка роутера

На микроте ставим пакет с контейнерами, ребутаемся, включаем контейнеры
/system/device-mode/update container=yes
На железке после этого нужно будет нажать кнопку mode за ограниченное время, удалённо включить контейнеры не выйдет, т.к. потанцевальная дыра в безопасности. Дальше создаём VETH для нашего контейнера, создаём отдельный бридж для контейнеров, навешиваем туда ip адреса(я тут добавляю и ipv6 и ipv4 адрес. На самом деле для схемы с роутингом через WG интерфейс достаточно только v4, но у меня все контейнеры настроены единообразно, так что для общего понимания оставлю)

/interface/veth/add address=172.20.0.2/24,fd08:172:20::2/64 gateway=172.20.0.1 gateway6=fd08:172:20::1 name=xray_eth
/interface/bridge/add name=containers
/interface/bridge/port/add bridge=containers interface=xray_eth
/ip/address/add address=172.20.0.1/24 interface=containers
/ip/firewall/nat/
add action=masquerade chain=srcnat comment="from containers masq" out-interface=containers src-address=192.168.88.0/24
add action=masquerade chain=srcnat comment="from containers masq" out-interface-list=WAN src-address=172.20.0.0/24
/ipv6/address/add address=fd08:172:20::1 interface=containers
/ipv6/firewall/nat/
add action=masquerade chain=srcnat comment="from containers masq" out-interface-list=WAN src-address=fd08:172:20::/64
add action=masquerade chain=srcnat comment="to containers masq" out-interface=containers
/ipv6/firewall/filter/add action=accept chain=forward comment="Allow Containers" out-interface-list=WAN src-address=fd08:172:20::/64

Опять же, конкретно в варианте с wireguard инбаундами никакой дополнительный маскарадинг для ipv4 не нужен, с головой хватит тех, что мы потом добавим отдельно, а также дефолтного правила маскарадинга всего выходного траффика. Но для единообразия пусть будут.
Дальше втыкаем в микрот флешку и из интерфейса форматируем её в EXT4(Обязательно! Если будет FAT или exFAT - при распаковке контейнера порушатся разрешения фалов и ничего не взлетит)
Также включаем на роутере ftp(если выключен) - пригодится для подкидывания конфигов и имеджей
/ip/service/set ftp disabled=no port=21

Закатываем контейнер

Конфигурим местный докер.
/container/config/set ram-high=384.0MiB tmpdir=usb1/pull
Аттеншн! Тут ram-high=384.0MiB - это конфиг для AX3 с гигом оперативы. У AС3 её всего 256Мб и с пустым конфигом и установленным пакетом wifi-qcom доступно примерно 145Мб(а с подробным конфигом и под нагрузкой - что-то около 70Мб).
При превышении лимита ram-high процессы в контейнере начнут нещадно троттлиться, т.е. работать будет, но с дикими тормозами. А вот если память у роутера закончится - скорее всего он тупо крашнется. Не доводим и не включаем старт контейнера при загрузке пока не отладим всё.
В данном случае я не выкладывал образ на докерхаб, поэтому подкидываем его локально. Для этого подключаемся по ftp на микрот и подкладываем xray_full_mikro.tar в корень диска usb1. Можно воспользоваться WinSCP, очень удобно. Ну или драг-н-дроп-ом через winbox, но там удобство уже так себе.
Также создаём папочки по пути /usb1/xray_full/config (тоже через ftp или через winbox)
Создаём маунт для папочки с конфигами
/container/mounts/add dst=/opt/config name=xray_full_config src=/usb1/xray_full/config
И закатываем контейнер
/container/add comment="Xray-core PLUS" file=usb1/xray_full_mikro.tar interface=veth1 envlist=xray_full hostname=XrayPLUS interface=xray_eth logging=yes mounts=xray_full_config root-dir=/usb1/xray_full/store logging=yes start-on-boot=no

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

Создаём WG интерфейсы и настраиваем маршрутизацию
/interface/wireguard/add listen-port=13234 mtu=1420 name=BDPI_WG
/interface/wireguard/peers/add name=BDPI_WG_peer allowed-address=0.0.0.0/0,::/0 endpoint-address=172.20.0.2 endpoint-port=3124 interface=BDPI_WG 
/interface/wireguard/add listen-port=13235 mtu=1420 name=AWG_WG
/interface/wireguard/peers/add name=AWG_WG_peer allowed-address=0.0.0.0/0,::/0 endpoint-address=172.20.0.2 endpoint-port=3125 interface=AWG_WG 

Далее набираем
/interface/wireguard/print
И видим примерно такую картинку

Flags: X - disabled; R - running 
 0  R name="BDPI_WG" mtu=1420 listen-port=13234 private-key="wO1qi2jL68IkBEcwZ9IT9BaTH16dONd7Ntj+Lbft0kg=" public-key="NN0BnydbxXQMl9n/hOqow5z6SF/b8h6lMoaa5uBeYGE=" 

 1  R name="AWG_WG" mtu=1420 listen-port=13235 private-key="SHp5JzLo3YvvyswGDEudwukltBCEBmvs59epANgxeWE=" public-key="hbK38jNBghVeNB7/D+YDqUZLVqn5qSGcZvGogQ63JXQ=" 

Здесь нас интересуют публичные ключи. Заносим их в переменные контейнера

/container/envs
add key=WG_BDPIIN_PUBKEY name=xray_full value="NN0BnydbxXQMl9n/hOqow5z6SF/b8h6lMoaa5uBeYGE="
add key=WG_WPIN_PUBKEY name=xray_full value="hbK38jNBghVeNB7/D+YDqUZLVqn5qSGcZvGogQ63JXQ="

Далее добываем ключи пиров

/interface/wireguard/peers/export show-sensitive 
# 2024-12-09 16:55:52 by RouterOS 7.16.1
# software id = 22UI-VTRD
#
# model = C53UiG+5HPaxD2HPaxD
# serial number = XXXXXXXXXXX
/interface wireguard peers
add allowed-address=0.0.0.0/0,::/0 endpoint-address=172.20.0.2 endpoint-port=3124 interface=BDPI_WG name=BDPI_WG_peer private-key="aPfr+HjXhgrzejN3YawS9SPddIyWidDgPYI3nHezlGw=" public-key=\
    "I3GJ75Wz0BgfBw1KwiEkmNsCmg8CTHnyqDDKsStBdGw="
add allowed-address=0.0.0.0/0,::/0 endpoint-address=172.20.0.2 endpoint-port=3125 interface=AWG_WG name=AWG_WG_peer private-key="OJrDUVmmM6LbSmEfMddKhx15LoKNh4l2P/XdlO8An1M=" public-key=\
    "t4F3NASnN32rpY6OgbrXc8S2xHdrXsuIuyQHwCvDGXY="

Здесь нас интересуют приватные ключи. Аналогично добавляем их в переменные

/container/envs
add key=WG_BDPIIN_PRIVKEY name=xray_full value="aPfr+HjXhgrzejN3YawS9SPddIyWidDgPYI3nHezlGw="
add key=WG_WPIN_PRIVKEY name=xray_full value="OJrDUVmmM6LbSmEfMddKhx15LoKNh4l2P/XdlO8An1M="

И наконец настраиваем правила и таблица маршрутизации

/routing table 
add disabled=no fib name=bdpi
add disabled=no fib name=awg
/routing rule
add action=lookup disabled=no interface=BDPI_WG routing-mark=warp table=bdpi
add action=lookup disabled=no interface=AWG_WG routing-mark=vless table=awg

Добавляем маскарадинг

/ip firewall nat
add action=masquerade chain=srcnat comment="BDPI_WG masq" out-interface=BDPI_WG 
add action=masquerade chain=srcnat comment="AWG_WG masq" out-interface=AWG_WG
/ipv6 firewall nat
add action=masquerade chain=srcnat comment="BDPI_WG masq" out-interface=BDPI_WG
add action=masquerade chain=srcnat comment="AWG_WG masq" out-interface=AWG_WG

Дальше нам нужно повесить на интерфейсы WG какие-нибудь IP адреса, любые, из приватного диапазона(по ним будет идти маршрутизация только до приложения в контейнере). Например так. Опять же, в целом можно без этого, т.к. маршрутизировать мы будет тупо по интерфейсу, но некрасиво.

/ip address 
add address=172.19.0.2 interface=BDPI_WG network=172.19.0.2
add address=172.19.0.3 interface=AWG_WG network=172.19.0.3
/ipv6 address
add address=fd08:172:19::3/128 advertise=no interface=BDPI_WG no-dad=yes
add address=fd08:172:19::2/128 advertise=no interface=AWG_WG no-dad=yes

И создаём маршруты в дикий интернет для наших новых таблиц маршрутизации

/ip route
add comment=BDPI disabled=no distance=1 dst-address=0.0.0.0/0 gateway=BDPI_WG routing-table=bdpi
add comment=AWG disabled=no distance=1 dst-address=0.0.0.0/0 gateway=AWG_WG routing-table=awg
/ipv6 route
add comment=BDPI disabled=no distance=1 dst-address=2000::/3 gateway=BDPI_WG routing-table=bdpi
add comment=AWG disabled=no distance=1 dst-address=2000::/3 gateway=AWG_WG routing-table=awg

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

Исправим это

Сначала добавим переменную, передающую параметры в byedpi. В неё нужно прописать ваши параметры, которые работают у вас
/container/envs/add key=BDPI_ARGS name=xray_full value="--proto=http,tls --oob 2"
Дальше добавляем конфиг амнезии. Если амнезия нужна для подключения к WARP - можно воспользоваться генератором
Получится файл вида

[Interface]
PrivateKey = СЕЕРКТНЫЙ_КЛЮЧ
S1 = 0
S2 = 0
Jc = 50
Jmin = 10
Jmax = 120
H1 = 1
H2 = 2
H3 = 3
H4 = 4

MTU = 1280
Address = 172.16.0.2, IPV6_адрес

[Peer]
PublicKey = ПУБЛИЧНЫЙ_КЛЮЧ
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
Endpoint = engage.cloudflareclient.com:2408

В него, в раздел [Interface] по желанию можно добавить строки

CheckAlive = 1.1.1.1, 8.8.8.8
CheckAliveInterval = 10

Тогда раз в CheckAliveInterval хосты, перечисленные в CheckAlive, будут пинговаться, и если не ответят - ливнес проба на /readyz вернёт 503. А если всё ОК - 200.
Также Endpoint можно задать через перменную окружения
Endpoint = $AWG_ENDPOINT
И задать её через переменные контейнера
/container/envs/add key=AWG_ENDPOINT name=xray_full value=162.159.192.1:2408
wireproxy так умеет и понимает.
Конфиг подкидываем по пути /usb1/xray_full/config/чтоугодно.conf
Напр. /usb1/xray_full/config/awg.conf
Если стартовали контейнер - рестартим

После этого контейнер с базовым конфигом почти готов к работе. Любой траффик, который пойдёт на интерфейс BDPI_WG, через правила маршрутизации, прописанном в конфие xray, зарулится на byedpi, а тот что пойдёт на AWG_WG - попадёт в wireproxy. Остаётся настроить правила, которые будут заворачивать маршруты для интересных нам хостов.

Настраиваем заворот траффика

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

/ip firewall mangle
add action=mark-routing chain=prerouting comment=BDPI dst-address-list=bdpi new-routing-mark=bdpi passthrough=yes routing-mark=!bdpi src-address=!172.20.0.0/24
add action=mark-routing chain=prerouting comment=AWG dst-address-list=awg new-routing-mark=awg passthrough=yes routing-mark=!awg src-address=!172.20.0.0/24
/ipv6 firewall mangle
add action=mark-routing chain=prerouting comment=BDPI dst-address-list=bdpi new-routing-mark=bdpi passthrough=yes routing-mark=!bdpi src-address=!fd08:172:20::/64
add action=mark-routing chain=prerouting comment=AWG dst-address-list=awg new-routing-mark=awg passthrough=yes routing-mark=!awg src-address=!fd08:172:20::/64

Теперь эти списки нужно как-то наполнять. Проще всего - через статические DNS записи(клиенты должны использовать микротовски DNS сервер, иначе фокус не получится)
Предположим, мы хотим обойти блокировки для домена ntc.party. Предположим, мы хотим сделать это через byedpi.
Тогда добавляем статическую запись такого вида

/ip/dns/static/add address-list=bdpi comment="DPI ntc.party" forward-to=localhost match-subdomain=yes name=ntc.party type=FWD

И настраиваем время жизни записи в листе на подольше(я себе ставлю сутки, нужно чтоб она жила дольше, чем DNS кеш у клиентов
/ip/dns/set address-list-extra-time=1d
После этого, когда клиент попытается сходит на ntc.party, он стукнется на микротиковский DNS, чтобы разрезолвить хост. Микротиковский DNS найдёт у себя статическую FWD запись, форварднёт себе же, добавит в лист bdpi и потом разрезолвит об вышестоящий DNS сервер и вернёт клиенту ip. И на тот момент, когда клиент попытается установить соединение с этм ip - он уже находится в листе bdpi, а значит траффик попадёт в таблицу маршрутизации bdpi и завернётся в интерфейс BDPI_WG. Т.о. можно гибко настраивает списки для разных хостов.

Касаемо логов контейнера. Сделано так, что для каждого из бинарников его сообщения имеют соотв. префикс в общем логфайле микротика.
Будет так - hostname_контейнера: Имя_приложения: сообщение. Если хостнейм контейнера не настроен - вместо него будет выведен его ip адрес. Также есть возможность отфильтровать сообщения от wireproxy, т.к. он генерит их ну прям очень много. Делается через задание энвы контейнера LOGFILTER
Например как-то так:
add key=LOGFILTER name=xray_full value="Transport packet|Sending keepalive|Sending handshake|Received handshake"

После этого контейнер базово работает и заворачивает траффик в амнезию или byedpi. Но ведь он умеет не только это) Начинается самое вкусное.
С некоторых пор xray-proxy поддерживает многофайловые конфиги, т.е. больше нет никакой необходимости ковыряться в километровой жсонине, можно порубить её на жсоны помельче и точечно добавлять или менять куски конфигурации.
Полная дока доступна здесь, но если вкратце - суть такая.
В конфиге xray есть примерно такой набор блоков:

{
  "log": {},
  "api": {},
  "dns": {},
  "stats": {},
  "policy": {},
  "transport": {},
  "routing": {},
  "inbounds": [],
  "outbounds": []
}

Для всех них валидно следующее правило - каждый следующий конфиг заменяет собой те блоки в предыдущем конфиге, которые он содержит. Т.е. если следующим конфигом добавить json с содержимым вида
{
“log”: {
Что-то
}
}
То весь блок log в конфиге заменится на новый и общий конфиг примет вид

{
  "log": {
     Что-то
   },
  "api": {},
  "dns": {},
  "stats": {},
  "policy": {},
  "transport": {},
  "routing": {},
  "inbounds": [],
  "outbounds": []
}

Правило валидно для всех блоков, кроме inbounds и outbounds, для них всё хитрее. Если в следующем конфиге прописано два или больше инбаунда или аутбаунда - то всё тоже самое, заменяется весь блок. А вот если их там только по одной штучке - то инбаунды из следующего конфига допишутся в начало общего. Аналогично и аутбаунды, только для них есть ещё одно правило - если в имени конфига есть слово tail, то аутбаунды допишутся в конец.
Конфиги намапливаются друг на друга в алфавитном порядке, т.е. сначала применится конфиг 00-что-то.json, потом 01-что-то.json и т.п. Все расширения кроме json также игнорируются, т.о. если изменить расширение файла - ненужный кусок конфига можно просто выключить. Удобно например для временного включения расширенного логирования.
Т.о. можно без особых усилий добавлять к базовой конфиге xray в контейнере свои собственные конфигурации для VPN

Базовый конфиг

Лежит в контейнере по пути
/usb1/xray_full/store/opt/xray-core/00_base.json
И имеет такой вид:

{
  "log": 
  {
    "access": "none",
    "dnsLog": false,
    "loglevel": "warning"
  },
  "routing": 
  {
    "domainStrategy": "IPIfNonMatch",
    "rules": [
      {
        "type": "field",
        "ip": [
          "geoip:private",
          "192.168.88.0/24"
        ],
        "outboundTag": "direct"
      },
      {
        "type": "field",
        "domain": "geosite:ru-available-only-inside",
        "outboundTag": "direct"
      },
      {
        "type": "field",
        "protocol": "bittorrent",
        "outboundTag": "direct"
      },
      {
        "type": "field",
        "ip": "geoip:ru",
        "outboundTag": "bdpi"
      },
      {
        "type": "field",
        "inboundTag": "inbound-wg-bdpi:3124",
        "outboundTag": "bdpi"
      },
      {
        "type": "field",
        "inboundTag": "inbound-wg-awg:3125",
        "outboundTag": "awg"
      }
    ]
  },
  "inbounds": 
  [
    {
      "listen": "$MYIP",
      "port": 3124,
      "protocol": "wireguard",
      "settings": {
        "mtu": 1420,
        "secretKey": "$WG_BDPIIN_PRIVKEY",
        "peers": [
          {
            "publicKey": "$WG_BDPIIN_PUBKEY",
            "allowedIPs": [
              "0.0.0.0/0",
              "::/0"
            ],
            "keepAlive": 0
          }
        ],
        "noKernelTun": true
      },
      "streamSettings": null,
      "tag": "inbound-wg-bdpi:3124",
      "sniffing": {
        "enabled": true,
        "destOverride": "fakedns+others",
        "metadataOnly": false
      }
    },
    {
      "listen": "$MYIP",
      "port": 3125,
      "protocol": "wireguard",
      "settings": {
        "mtu": 1420,
        "secretKey": "$WG_WPIN_PRIVKEY",
        "peers": [
          {
            "publicKey": "$WG_WPIN_PUBKEY",
            "allowedIPs": [
              "0.0.0.0/0",
              "::/0"
            ],
            "keepAlive": 0
          }
        ],
        "noKernelTun": true
      },
      "streamSettings": null,
      "tag": "inbound-wg-awg:3125",
      "sniffing": {
        "enabled": true,
        "destOverride": "fakedns+others",
        "metadataOnly": false
      }
    }
  ],
  "outbounds": 
  [
    {
      "tag": "direct",
      "protocol": "freedom",
      "settings": {
        "domainStrategy": "AsIs"
      }
    },
    {
      "tag": "blocked",
      "protocol": "blackhole",
      "settings": {}
    },
    {
      "tag": "awg",
      "protocol": "socks",
      "settings": {
        "servers": [
          {
            "address": "$MYIP",
            "port": $WP_PORT
          }
        ]
      }
    },
    {
      "tag": "bdpi",
      "protocol": "socks",
      "settings": {
        "servers": [
          {
            "address": "$MYIP",
            "port": $BDPI_PORT
          }
        ]
      }
    }
  ]
}

Это фактически шаблон, в котором при старте контейнера переменные подменяются на их значения, после чего содержимое перенаправляется через named pipe по пути
/usb1/xray_full/store/opt/config/00_base_dyn.json
Т.е. после старта контейнера в маунте с конфигурациями появляется пустой файл с таким именем. Его можно удалить, он пересоздастся после старта, а вот пробовать открывать его не надо, это на самом деле не файл, а ссылка на пайп, другой конец которого в этот момент уже оторвана. Не откроется.
Также в нём изначально прописан ряд правил маршрутизации.

  1. Для адресов из приватного диапазона аутбаунд всегда direct
  2. Для хостов, доступных только из РФ аутбаунд тоже директ(правило работает только в том случае, если xray используется как прокси. Если используется wireguard inbound - xray ничего не знает о доменном имени и база geosite для него бесполезна. Блок оставлен по сути на всякий случай
  3. Для битторент траффика аутбаунд всегда direct. На случай случайно залетевшего маршрута.
  4. Для российских ip адресов аутбаунд по умолчанию byedpi. Опять же, оставлено скорее как страховка, чтоб не палить свои VPS-ки
  5. Для входящего интерфейса с тегом inbound-wg-bdpi аутбаунд - byedpi
  6. Для входящего интерфейса с тегом inbound-wg-awg аутбаунд - awg
    В общем случае, правила работают так. Любому соединению нужно соотвествовать всем условиям, описанным в правиле. Если хоть одно не соответствует - мимо. Если соответсвуют несколько - применится то, которое выше в списке. Если не соотвествует ни одно - траффик выйдет через самый последний аутбаунд в списке(поэтому в списке последним прописан тот же byedpi - чтоб случайно не спалить свою VPSку)

Ну и дальше, представим себе что нам хочется добавить свой собственный VPN в качестве ещё одного в списке вариантов обхода. Допустим, это VLess протокол
Подкидываем по пути /usb1/xray_full/store/opt/config/ конфиг 01_vless_out.json вида

01_vless_out.json
{
  "inbounds": 
  [
    {
      "listen": "172.20.0.2",
      "port": 3126,
      "protocol": "wireguard",
      "settings": {
        "mtu": 1420,
        "secretKey": "ПРИВАТНЫЙ_КЛЮЧ",
        "peers": [
          {
            "publicKey": "ПУБЛИЧНЫЙ_КЛЮЧ",
            "allowedIPs": [
              "0.0.0.0/0",
              "::/0"
            ],
            "keepAlive": 0
          }
        ],
        "noKernelTun": true
      },
      "streamSettings": null,
      "tag": "inbound-wg-vless:3126",
      "sniffing": {
        "enabled": false,
        "metadataOnly": false,
        "routeOnly": false
      }
    }
  ],
  "outbounds": [
    {
      "tag": "vless_out",
      "protocol": "vless",
      "settings": {
        "vnext": [
          {
            "address": "АДРЕС",
            "port": 443,
            "users": [
              {
                "id": "АЙДИ",
                "flow": "xtls-rprx-vision",
                "encryption": "none"
              }
            ]
          }
        ]
      },
      "streamSettings": {
        "network": "tcp",
        "security": "reality",
        "realitySettings": {
          "publicKey": "ПУБЛИЧНЫЙ_КЛЮЧ_VLESS",
          "fingerprint": "chrome",
          "serverName": "что-то.com",
          "shortId": "айди",
          "spiderX": "/"
        },
        "tcpSettings": {
          "header": {
            "type": "none"
          }
        }
      }
    }
  ]
}

Т.к. в этом конфиге только один инбаунд и только один аутбаунд - они допишутся к общему, а не заменят список целиком. Если нужно больше инбаундов(напр. мы хотим добавить dokodemo-door, который будет проксировать траффик в наш vless-out, то нужно класть его в ещё один конфиг, следующий в списке)
Подкидываем туда же 02_routing.json, в который копируем правила из дефолтного+новое

02_routing.json
{
"routing": 
  {
    "domainStrategy": "AsIs",
    "rules": 
    [
      {
        "type": "field",
        "ip": [
          "geoip:private",
          "192.168.88.0/24"
        ],
        "outboundTag": "direct"
      },
      {
        "type": "field",
        "domain": "geosite:ru-available-only-inside",
        "outboundTag": "direct"
      },
      {
        "type": "field",
        "ip": "geoip:ru",
        "outboundTag": "bdpi"
      },
      {
        "type": "field",
        "protocol": "bittorrent",
        "outboundTag": "direct"
      },
      {
        "type": "field",
        "inboundTag": "inbound-wg-vless:3126",
        "outboundTag": "vless_out"
      }
      {
        "type": "field",
        "inboundTag": "inbound-wg-awg:3125",
        "outboundTag": "awg"
      },
      {
        "type": "field",
        "inboundTag": "inbound-wg-bdpi:3124",
        "outboundTag": "bdpi"
      }
    ]
  }
}

Настраиваем ещё один Wireguard интерфейс на роутере по аналогии, рестартим контейнер и вуаля - новый впн настроен. Можно прицеливать нужные хосты туда.

Про обрезку geoip.dat

Можно убрать ненужные списки из geoip.dat, например оставив только списки ru, private и ru-blocked
Для этого качаем полный, с добавленными для РФ списками
Клонируем себе куда-нибудь гит с интсрументарием, заходим и собираем (нужен golang-go)

git clone https://github.com/v2fly/geoip.git
cd geoip
go build

После этого правим config.json, оставляя от него что-то такое

{
  "input": [
    {
      "type": "v2rayGeoIPDat",
      "action": "add",
      "args": {
        "uri": "../geoip.dat",
        "wantedList": ["ru", "ru-blocked"]
      }
    },
    {
      "type": "private",
      "action": "add"
    }
  ],
  "output": [
    {
      "type": "v2rayGeoIPDat",
      "action": "output",
      "args": {
        "outputDir": "./output",
        "outputName": "geoip-ru.dat"
      }
    }
  ]
}

где …/geoip.dat - путь до ранее скаченного geoip.dat
Всё, сохраняем, запускаем бинарь geoip, и он выплюнет в ./output geoip-ru.dat, содержащий интересующие нас списки. Подкидывать по пути /usb1/xray_full/store/opt/xray-core/data/geoip.dat
Зачем нужно? В теории можно повысить производительность, если используется сравнение только с русскими списками - зачем таскать все остальные? На практике если честно особой разницы не заметил, но кому-то возможно может помочь съэкономить несколько мегабайт памяти

Собранный контейнер(для arm64) выложил на ядиск, также в следующем сообщении(в это не помещается) выкладываю скрипт для самостоятельной сборки(потому что не надо доверять вещам, собранным рандомным челом из интернета). Ну и ещё можно подрихтовать под себя базовый конфиг, убрать, если не нужны, бинарники wireproxy и ciadpi, обновить контейнер когда выйдут свежие версии и т.п.

1 Like
build.sh
#!/bin/bash
DISTR_XRAY="https://github.com/XTLS/Xray-core/releases/download/v24.10.31/Xray-linux-arm64-v8a.zip"
DISTR_BDPI="https://github.com/hufrea/byedpi/releases/download/v0.15/byedpi-15-aarch64.tar.gz"
DISTR_WIREPROXY_AWG="https://github.com/artem-russkikh/wireproxy-awg/releases/latest/download/wireproxy_linux_arm64.tar.gz"
GEOSITE_RU_BLOCKED="https://github.com/runetfreedom/russia-blocked-geosite/releases/latest/download/geosite-ru-only.dat"
GEOIP_RU="https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release/geoip.dat"
#-------------------------------------------------
download() {
	if [ $download_enabled -eq 1 ]; then
		rm -rf temp/*
		wget ${1} -O temp/${2}
		case `echo ${2} | awk -F. '{print $NF}'` in 
			"zip")
				unzip temp/${2} -d temp/ ;;
			*)
				tar -xf temp/${2} -C temp/ ;;
		esac
	fi
}
precheck() {
	echo "1. Checking installation..."
	if [ ! `which docker` ]; then
		echo "Install docker first"
		exit 0;
	fi
	if [ ! `which wget` ]; then
		echo "Install wget first"
		exit 0;
	fi
	if [ ! `which unzip` ]; then
		echo "Install unzip first"
		exit 0;
	fi
	
	rm -rf temp prepare
	mkdir -p temp
	mkdir -p prepare/opt/xray-core/data
	mkdir -p prepare/opt/config/
	mkdir -p prepare/usr/share
	echo "Installation OK"
	download_enabled=1
}
#--------------------
xray_baseconf_gen() {
cat > prepare/opt/xray-core/00_base.json << EOF
{
  "log": 
  {
    "access": "none",
    "dnsLog": false,
    "loglevel": "warning"
  },
  "routing": 
  {
    "domainStrategy": "IPIfNonMatch",
    "rules": [
      {
        "type": "field",
        "ip": [
          "geoip:private",
          "192.168.88.0/24"
        ],
        "outboundTag": "direct"
      },
      {
        "type": "field",
        "domain": "geosite:ru-available-only-inside",
        "outboundTag": "direct"
      },
      {
        "type": "field",
        "protocol": "bittorrent",
        "outboundTag": "direct"
      },
      {
        "type": "field",
        "ip": "geoip:ru",
        "outboundTag": "bdpi"
      },
      {
        "type": "field",
        "inboundTag": "inbound-wg-bdpi:3124",
        "outboundTag": "bdpi"
      },
      {
        "type": "field",
        "inboundTag": "inbound-wg-awg:3125",
        "outboundTag": "awg"
      }
    ]
  },
  "inbounds": 
  [
    {
      "listen": "\$MYIP",
      "port": 3124,
      "protocol": "wireguard",
      "settings": {
        "mtu": 1420,
        "secretKey": "\$WG_BDPIIN_PRIVKEY",
        "peers": [
          {
            "publicKey": "\$WG_BDPIIN_PUBKEY",
            "allowedIPs": [
              "0.0.0.0/0",
              "::/0"
            ],
            "keepAlive": 0
          }
        ],
        "noKernelTun": true
      },
      "streamSettings": null,
      "tag": "inbound-wg-bdpi:3124",
      "sniffing": {
        "enabled": true,
        "destOverride": "fakedns+others",
        "metadataOnly": false
      }
    },
    {
      "listen": "\$MYIP",
      "port": 3125,
      "protocol": "wireguard",
      "settings": {
        "mtu": 1420,
        "secretKey": "\$WG_WPIN_PRIVKEY",
        "peers": [
          {
            "publicKey": "\$WG_WPIN_PUBKEY",
            "allowedIPs": [
              "0.0.0.0/0",
              "::/0"
            ],
            "keepAlive": 0
          }
        ],
        "noKernelTun": true
      },
      "streamSettings": null,
      "tag": "inbound-wg-awg:3125",
      "sniffing": {
        "enabled": true,
        "destOverride": "fakedns+others",
        "metadataOnly": false
      }
    }
  ],
  "outbounds": 
  [
    {
      "tag": "direct",
      "protocol": "freedom",
      "settings": {
        "domainStrategy": "AsIs"
      }
    },
    {
      "tag": "blocked",
      "protocol": "blackhole",
      "settings": {}
    },
    {
      "tag": "awg",
      "protocol": "socks",
      "settings": {
        "servers": [
          {
            "address": "\$MYIP",
            "port": \$WP_PORT
          }
        ]
      }
    },
    {
      "tag": "bdpi",
      "protocol": "socks",
      "settings": {
        "servers": [
          {
            "address": "\$MYIP",
            "port": \$BDPI_PORT
          }
        ]
      }
    }
  ]
}
EOF
}
xray_prepare() {
	echo "2. Preparing xray-core"
	download $DISTR_XRAY xray.zip
	mv temp/xray prepare/opt/xray-core/
	chmod +x prepare/opt/xray-core/xray
	wget $GEOSITE_RU_BLOCKED -O prepare/opt/xray-core/data/geosite.dat
	wget $GEOIP_RU -O prepare/opt/xray-core/data/geoip.dat
	
	wget 
	ln -s /opt/xray-core/data prepare/usr/share/xray
	if [ -e prepare/opt/xray-core/xray ]; then
		echo "xray-core OK"
	else
		echo "Unable to prepare xray-core. Exiting"
		exit 0
	fi
}
#--------------------
bdpi_prepare(){
	echo "3. Preparing byedpi"
	download $DISTR_BDPI bdpi.tar.gz
	tar -xf temp/bdpi.tar.gz -C temp/
	mv temp/ciadpi-aarch64 prepare/opt/ciadpi
	chmod +x prepare/opt/ciadpi
	if [ -e prepare/opt/ciadpi ]; then
		echo "byedpi OK"
	else
		echo "Unable to prepare byedpi. Exiting"
		exit 0
	fi
}
#--------------------
awg_prepare(){
	echo "4. Preparing AWG"
	download $DISTR_WIREPROXY_AWG awg.tar.gz
	mv temp/wireproxy prepare/opt/
	chmod +x prepare/opt/wireproxy
	if [ -e prepare/opt/wireproxy ]; then
		echo "AWG OK"
	else
		echo "Unable to prepare AWG. Exiting"
		exit 0
	fi
}
#--------------------
entrypoint_gen() {
cat > prepare/opt/entrypoint.sh << EOF2
#!/bin/busybox sh
#setting envs
export MYIP=\$(ip -4 addr show dev eth0 | grep inet | awk -F' ' '{split(\$2, a, "/");print a[1]}') #because busybox needs some hacks
if hostname | grep -iq mikrotik; then MYHOST=\$MYIP; else MYHOST=\$(hostname); fi #if container hostname is not set, it will be router name
if [ ! "\$WP_PORT" ]; then export WP_PORT=9998; fi #setting wireproxy port
if [ ! "\$BDPI_PORT" ]; then export BDPI_PORT=9999; fi #setting byedpi port
if [ ! "\$WG_WPIN_PRIVKEY" ]; then echo "\$MYHOST: WG_WPIN_PRIVKEY is not set. Exiting"; exit 0 ; fi
if [ ! "\$WG_WPIN_PUBKEY" ]; then echo "\$MYHOST: WG_WPIN_PUBKEY is not set. Exiting"; exit 0 ; fi
if [ ! "\$WG_BDPIIN_PRIVKEY" ]; then echo "\$MYHOST: WG_BDPIIN_PRIVKEY is not set. Exiting"; exit 0 ; fi
if [ ! "\$WG_BDPIIN_PUBKEY" ]; then echo "\$MYHOST: WG_BDPIIN_PUBKEY is not set. Exiting"; exit 0 ; fi

#generating xray base conf from template
mkfifo /opt/config/00_base_dyn.json
eval "cat <<EOF
\$(cat /opt/xray-core/00_base.json)
EOF
" > /opt/config/00_base_dyn.json &
#generating config for wireproxy with socks block
mkfifo /opt/awg.conf
eval "cat <<EOF
\$(find /opt/config/ -maxdepth 1 -type f -name "*.conf" -exec cat {} \; -quit)
[Socks5]
BindAddress = \$MYIP:\$WP_PORT
EOF
" > /opt/awg.conf &
#starting all processes
log() { while read line; do echo "\$MYHOST: \${1}: \${line}"; done; }
if [ ! "\$LOGFILTER" ]; then #wireproxy produce too much logs. We can filter them
	echo "Filеring logs for wireproxy disabled"
	/opt/wireproxy -c /opt/awg.conf 2>&1 | log "WireproxyAWG" &
else
	/opt/wireproxy -c /opt/awg.conf 2>&1 | grep -Eiv "\$LOGFILTER" | log "WireproxyAWG" &
fi
/opt/ciadpi --ip \$MYIP --port \$BDPI_PORT \$BDPI_ARGS 2>&1 | log "ByeDpi" &
/opt/xray-core/xray run -confdir /opt/config 2>&1 | log "Xray-core"
EOF2
}
dockerbuild () {
	echo "5. Building docker image"
	echo 'FROM --platform=linux/arm64 busybox:stable
WORKDIR /opt/xray-core/data
COPY ./prepare/ /
ENTRYPOINT ["/opt/entrypoint.sh"]
' > Dockerfile
	chmod +x prepare/opt/entrypoint.sh
	docker build --rm -t xray_full_mikro .
	docker save xray_full_mikro:latest -o xray_full_mikro.tar
	docker image rm xray_full_mikro:latest
}
precheck
xray_prepare
xray_baseconf_gen
bdpi_prepare
awg_prepare
entrypoint_gen
dockerbuild
1 Like

PSA:

  1. копия исходных двух постов на webarchive
  2. копия архива с контейнером на webarchive и pixeldrain

SHA512: 7f02fa6fe6949441d5e52267453809b5704e6394824d09dc59ee45e5874477cd22569b5bab41272b77edec618f887240a8827deaea8f207ea849ddd55137e1bf, сверяйте с оригиналом самостоятельно.

Из моего опыта тестов данный файл слишком урезан, встретились заблокированные сайты, которые там отсутствуют. geosite:ru-blocked-all в этом плане лучше, там я не обнаружил упущений.

Ну, тут уже по желанию. Нужную версию можно подкинуть по FTP по пути /usb1/xray_full/store/opt/xray-core/data
Я просто geosite вообще не использую, у меня маршрутизация на третьем слое, в таком варианте xray ни про какие хосты не знает(по крайней мере у меня это не работает, видимо sni сниффать он не умеет)