Mikrotik RouterOS в Docker с помощью Qemu

Зачем?

И это правильный вопрос, пожалуй с него и стоит начать, в общем пару лет назад я начал проект одной PHP библиотеки для работы с RouterOS через API (если данный пост понравится публике, то статья про PHP библиотеку не заставит себя долго ждать), как показала практика, для достижения стабильной работы библиотеки на разных версиях RouterOS очень полезно иметь тесты.

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

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

И в один прекрасный день мне это надоело, и я принялся искать решение проблемы.

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

После непродолжительного поиска наткнулся на Cloud Hosted Router страницы загрузок, официального сайта Mikrotik. До этого я несколько раз слышал о CHR, но предполагал, что это какие-то хитрые "железные" роутеры с функционалом по запуску виртуальных роутеров. Однако оказалось, что CHR это просто VDI/VMDK/OVA/etc. образы, содержащие в себе полноценную RouterOS, готовую к использованию.

Для тестов требовалось минимум две версии ОС, работающие одновременно, а именно 6.42 и самая свежая (на тот момент это была 6.44).

Дело в том, что в 6.43 разработчики RouterOS поменяли алгоритм авторизации через API, поэтому старая логика авторизации перестала работать на новой версии операционной системы, ну и как следствие, новая логика не работала на старых версия ОС.

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

Но как сделать чтобы любой человек мог выполнить тесты?

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

Но как организовать автоматическую проверку кода публикуемого контрибьюторами?

На этот вопрос тоже есть несколько очевидных ответов и один из них Travis-CI, однако, от идеи запуска VirtualBox в Travis-CI пришлось отказаться сразу, так как Travis не рассчитан на такое "нестандартное" его использование.

И тут меня осенило… есть же Docker!

Как?

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

Первый вопрос, который скорее всего возник у внимательного читателя "а как запустить образ RouterOS в Docker контейнере?", для этого есть один замечательный проект под названием QEMU, благодаря данной программе можно сэмулировать практический любой PC и заставить работать в нём большинство современных операционных систем.

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

К моей великой радости RouterOS начал работать в QEMU без каких-либо проблем, но пришлось немного повозиться с правильными настройками, чтобы сеть работала корректно и у меня был доступ к системе через API.

Так же было желание создать минимальный по размеру image, в качестве базы была выбрана Alpine Linux.

Version 0

Прототип проекта состоял всего из двух файлов:

Dockerfile

FROM alpine:3.8

RUN mkdir /routeros
WORKDIR /routeros
ADD [".", "/routeros"]

RUN apk add --no-cache --update netcat-openbsd qemu-x86_64 qemu-system-x86_64 \
 && wget "https://download.mikrotik.com/routeros/6.42.7/chr-6.42.7.vdi"

ENTRYPOINT ["/routeros/entrypoint.sh"]

На тот момент мне было неизвестно что WORKDIR умеет создавать директории, поэтому добавил mkdir перед ним.

Как видно по коду Dockerfile ничего экстраординарного не происходит, берётся базовый alpine версии 3.8, далее в контейнер добавляются файлы проекта и выполняется установка необходимых пакетов, а так же загрузка образа с сайта Mikrotik.

entrypoint.sh

#!/bin/sh

qemu-system-x86_64 \
    -vnc 0.0.0.0:0 \
    -m 256 \
    -hda /routeros/chr-6.42.7.vdi \
    -device e1000,netdev=net0 \
    -netdev user,id=net0,hostfwd=tcp::22-:22,hostfwd=tcp::23-:23,hostfwd=tcp::8728-:8728,hostfwd=tcp::8729-:8729

Entrypoint скрипт содержит в себе команду по запуску виртуальной машины, а так же ряд опций, в частности виртуальной машине выдаётся всего 256Мбайт ОЗУ, включается VNC сервер на 5900 порту, указывается путь до образа с виртуальной машиной, ну и сетевые настройки конечно же.

Сетевой адаптер всего один, связанно это с особенностями работы Docker, для того чтобы настроить большее количество сетевых интерфейсов надо потратить некоторое время, к тому же для решения моей задачи и одного интерфейса было достаточно. Если интересно, в скриптах есть пример конфигурации, который активирует четыре сетевых интерфейса через настройки QEMU, ну а настройки Docker это уже совсем другая история.

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

Version 1

Однако мне не нравилось что, для того чтобы обновить версию надо редактировать URL загрузки, а это немного неудобно.

Dockerfile

FROM alpine:3.8

ENV ROUTEROS_VERSON="6.42.7"
ENV ROUTEROS_IMAGE="chr-$ROUTEROS_VERSON.vdi"
ENV ROUTEROS_PATH="https://download.mikrotik.com/routeros/$ROUTEROS_VERSON/$ROUTEROS_IMAGE"

RUN mkdir /routeros
WORKDIR /routeros
ADD [".", "/routeros"]

RUN apk add --no-cache --update netcat-openbsd qemu-x86_64 qemu-system-x86_64 \
 && echo ">>> $ROUTEROS_PATH" \
 && if [ ! -e "$ROUTEROS_IMAGE" ]; then wget "$ROUTEROS_PATH"; fi

# For access via VNC
EXPOSE 5900

# Default ports of RouterOS
EXPOSE 21
EXPOSE 22
EXPOSE 23
EXPOSE 80
EXPOSE 443
EXPOSE 8291
EXPOSE 8728
EXPOSE 8729

ENTRYPOINT ["/routeros/entrypoint.sh"]

Вот тут с EXPOSЕ я конечно слегка переборщил, да с best practice не был знаком на тот момент.

Теперь можно было менять версию всего в одном параметре Dockerfile, на тот момент мне это казалось неплохим решением поставленной задачи, после чего был создан репозиторий на GitHub и настроена автоматическая сборка на Docker Hub.

Дальше возник вопрос с автоматизацией смены версии, вручную править Dockerfile было не лучшей идеей, поэтому был создан ещё один скрипт и добавлен в crontab.

cron.sh

#!/bin/bash

# Cron fix
cd "$(dirname $0)"

function getTarballs
{
    curl https://mikrotik.com/download/archive -o - 2>/dev/null | \
        grep -i vdi | \
        awk -F\" '{print $2}' | \
        sed 's:.*/::' | \
        sort -V
}

function getTag
{
    echo "$1" | sed -r 's/chr\-(.*)\.vdi/\1/gi'
}

function checkTag
{
    git rev-list "$1" 2>/dev/null
}

getTarballs | while read line; do
    tag=`getTag "$line"`
    echo ">>> $line >>> $tag"

    if [ "x$(checkTag "$tag")" == "x" ]
        then

            url="https://download.mikrotik.com/routeros/$tag/chr-$tag.vdi"
            if curl --output /dev/null --silent --head --fail "$url"; then
                echo ">>> URL exists: $url"
                sed -r "s/(ROUTEROS_VERSON=\")(.*)(\")/\1$tag\3/g" -i Dockerfile
                git commit -m "Release of RouterOS changed to $tag" -a
                git push
                git tag "$tag"
                git push --tags
            else
                echo ">>> URL don't exist: $url"
            fi

        else
            echo ">>> Tag $tag has been already created"
    fi

done

Хочу заметить, что это самая первая версия скрипта, в данный момент он немного отличается от изначального варианта.

Получившиеся контейнеры удалось задействовать на этапе прогонки тестов через Travis-CI, однако, решение оказалось полезным не только для меня и пользователи начали создавать задачки на GitHub.

Version 2

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

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

Поразмышляв над данной проблемой ещё некоторое время наткнулся на проект Qemu Docker, изучив исходники обнаружил изящное решение задачи с настройкой сети используя TAP интерфейс и мост до eth0.

Но поскольку автор использует в качестве базы полноценный Debian, а мне хотелось оставить всё на Alpine, пришлось скопировать часть скриптов в свой проект, вместо multistage контейнера. Помимо этого автор задействует KVM, что быстрее классического варианта QEMU, но с этим есть одна маленькая проблема.

Некоторые пользователи арендуют VPS работающие на технологии KVM, но хостеры по какой-то причине не включают поддержку KVM in KVM, поэтому запустить Docker контейнер с RouterOS в режиме KVM через QEMU не представляется возможным.

После незначительных правок Dockerfile стал выглядеть так:

Dockerfile

FROM alpine:3.11

# For access via VNC
EXPOSE 5900

# Default ports of RouterOS
EXPOSE 21 22 23 80 443 8291 8728 8729

# Different VPN services
EXPOSE 50 51 500/udp 4500/udp 1194/tcp 1194/udp 1701 1723

# Change work dir (it will also create this folder if is not exist)
WORKDIR /routeros

# Install dependencies
RUN set -xe \
 && apk add --no-cache --update \
    netcat-openbsd qemu-x86_64 qemu-system-x86_64 \
    busybox-extras iproute2 iputils \
    bridge-utils iptables jq bash python3

# Environments which may be change
ENV ROUTEROS_VERSON="6.46.5"
ENV ROUTEROS_IMAGE="chr-$ROUTEROS_VERSON.vdi"
ENV ROUTEROS_PATH="https://download.mikrotik.com/routeros/$ROUTEROS_VERSON/$ROUTEROS_IMAGE"

# Download VDI image from remote site
RUN wget "$ROUTEROS_PATH" -O "/routeros/$ROUTEROS_IMAGE"

# Copy script to routeros folder
ADD ["./scripts", "/routeros"]

ENTRYPOINT ["/routeros/entrypoint.sh"]

Как видно в финальной на данный момент версии уже задействованы best practice.

В данном конфиге многое учтено, однако, есть несколько пакетов, которые стоит удалить в дальнейшем, а именно:

  • python3 — от которого зависит скрипт, генерирующий конфиг udhcpd, сам генератор тоже придётся переписать
  • jq — он нужен для парсинга вывода из ip addr -json
  • netcat-openbsd — отголоски самого первого варианта, когда я ещё не знал как прокинуть порты из гостевой машины на хост

Ну и entrypoint скрипт тоже немного изменился:

entrypoint.sh

#!/bin/sh

###
### Не буду приводить весь код из верхней части скрипта,
### потому как он есть в репозитории, заострю внимание
### только на самом главном.
###

# Generate udhcpd configuration
/routeros/generate-dhcpd-conf.py $QEMU_BRIDGE > $DHCPD_CONF_FILE

# Get name of default interface
default=`ip -json route show | jq -r '.[] | select(.dst == "default") | .dev'`

# Finally, start udhcpd server
udhcpd -I $DUMMY_DHCPD_IP -f $DHCPD_CONF_FILE &

# And run the VM!
exec qemu-system-x86_64 \
    -nographic -serial mon:stdio \
    -vnc 0.0.0.0:0 \
    -m 256 \
    -nic tap,id=qemu0,script=$QEMU_IFUP,downscript=$QEMU_IFDOWN \
    "$@" \
    -hda $ROUTEROS_IMAGE

Как видно по коду, текущая версия контейнера уже работает с сетью при помощи моста, сам по себе интерфейс гостевой машины представляет собой TAP туннель. Данное решение позволило прокидывать из Docker контейнера абсолютно любой порт прямиком на RouterOS, и как побочный эффект стал ходить трафик из RouterOS в интернет.

Однако есть и проблемы, конфигурация udhcpd на данный момент ещё не адаптирована под встроенный докеровский DNS сервер (имею ввиду 127.0.0.11) и как следствие не работает резолв адресов по хостнейму в пределах например композиции Docker Compose.

Какие цели достигнуты?

  • Запустить RouterOS в Docker
  • Наладить работу сети, чтобы тесты API могли выполняться самостоятельно через Travis-CI
  • Создать публичный зоопарк images с разнообразными версиями сборок RouterOS
  • Автоматическая публикация новых версий контейнеров

Иными словами все поставленные задачи были решены успешно, возможно некоторые не совсем изящно как хотелось бы, но решены. Помимо основных целей была достигнута ещё одна побочная цель в реализации проброса любого порта из Docker Host на RouterOS.

Надеюсь данный проект будет полезен аудитории Хабрахабра и у кого-нибудь возникнет желание помочь в улучшении проекта (исходники открыты).

Что дальше?

Минимум

Исправить недоработки описанные в статье, удалить лишние пакеты и переписать генератор конфигурации udhcpd с python на что-то более классическое, например bash.

Медиум

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

Максимум

Распаковать VDI образ и вытащить из него файловую систему RouterOS, попытаться заставить её работать в Docker контейнере без виртуализации. Но скажу сразу, с большой долей вероятности это сделать не получится, так как RouterOS имеет очень кастомный Linux Kernel на борту, с кучей патчей меняющих работу ядра и позволяющих RouterOS вытворять все те чудеса на которые он способен.

И как следствие оболочка системы будет несовместима с ядром Docker Host системы и не будет работать, но если получится, то не придётся задействовать виртуальную машину.

Эпилог

Так уж получилось, что Хабрахабр я начал читать на постоянной основе ещё в далёком 2010 году и у меня возникало много интересных идей, о которых стоило было бы рассказать, но наученный теплым и дружеским общением на LOR и ON^W^W^W других площадках от мыслей о публикации статей обычно отказывался. Однако проект Docker RouterOS настолько любопытен, что мне захотелось пересилить свой read-only mode и рассказать о нём поподробнее.

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

Большое спасибо за то что дочитали статью до конца, если у вас возникли вопросы, прошу в комментарии.

Полезные ссылки