Как стать автором
Обновить

Удаленное управление и обмен данными между роутерами Микротик через SSH-exec

Время на прочтение12 мин
Количество просмотров9K
До известного времени разработчики Роутер ОС Микротик были непреклонны в своей политике — никаких ssh-соединений в скриптах. Лишь, кажется, году в 2019 (а если точно то 27 июня 2019 г. с выходом версии 6.45.1) в Роутер ОС появился не слишком очевидный, но всё же вариант – это инструмент /system ssh-exec. Главное условие — вход ssh должен быть настроен на роутерах по ключу. Нам сейчас не нужен SSH между пользователем и роутером (мы его не рассматриваем), а исходя из названия статьи, нужна настройка SSH именно между роутерами.

Вспомним, как настроить SSH ROS-ROS (пример привожу с этого топика официального форума Микротик). Предположим, у нас есть два роутера Микротик между которыми мы хотим настроить SSH соединение. Роутер-источник, с которого будет осуществляться подключение мы называем SSH-клиентом, роутер к которому подключаемся будет, естественно, SSH-сервером (схема).
image

Можно, разумеется, все настройки делать через утилиту Микротик Winbox, но мы будем это делать из командной строки Терминала, так надежнее:

Сначала на Микротике, будущим SHH-клиенте, мы генерируем открытый и закрытый ключи RSA (можно, наверное, такое проделать и с DSA-ключами).

/ip ssh export-host-key key-file-prefix=admin

Под «admin» должно быть имя пользователя с полными правами. После такой команды в /files появляются два файла: admin_rsa (приватный ключ) и admin_rsa.pub (публичный ключ).

Файл admin_rsa.pub должен быть импортирован на Микротик SSH-сервер.

Файл admin_rsa (приватный ключ) должен быть импортирован в Mикротик-клиент.

При импорте важно выбрать пользователя с полными правами. По крайней мере, у меня (может я где-то ошибся) всё получилось только, когда я использовал пользователя с полными правами и никак не хотело работать так, как описано в официальном руководстве РоутерОС Микротик (достаточно пользователя с правами ssh,read,write,test).

/user ssh-keys private import user=admin private-key-file=admin_rsa

Файл, который мы создали в Микротике-клиенте admin_rsa.pub, необходимо импортировать на Микротик-сервер. Копируем его в /files и импортируем в /user

/user ssh-keys import user=admin public-key-file=admin_rsa.pub

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

Чтобы всё заработало нужно также проверить что у нас в /ip service. В /ip service должен быть разрешен сервис SSH и соответствующим образом настроен порт (если Вы не хотите использовать стандартный 22), а также ограничения по адресам сетей для доступа. Я использую SSH-соединения только между локальными роутерами в пределах своих локальных подсетей либо через сети VPN, но никогда не открываю порт SSH наружу, чтобы избежать снижения безопасности. Если это делать, то, по крайней мере, нужно использовать нестандартный порт в /ip serviсe SSH.

После того, как открытый ключ установлен и доверен на Микротик-сервере, сеанс SSH может быть создан из Микротик SSH-клиента. Протестируем соединение. Это можно сделать с помощью клманды /system ssh. Шаблон вызова этой возможности выглядит так:

/system ssh address= <ip-адрес>  port=<SSH-port> user=<имя пользователя > command="<команда>"

Подставляем в шаблон свои данные, порт опускаем, так как используем стандартный (22) и его не меняли, command также пока опускаем, проверяя соединение. Получаем:

/system ssh address=192.168.1.2 user=admin

В команде указываем IP-адрес Микротика SSH-сервера и видим что он правильно подключается, не запрашивая у нас имя пользователя и пароль (соединение настроено по ключу). Если происходит запрос пароля, значит ключи сгенерированы неверно. Чаще всего, по крайней мере моему опыту, это происходит, когда при генерации или импорте ключей использовался пользователь не с полными правами.

А так можно подключиться и сразу выполнить команду на SSH-сервере. Получаем адресацию со стороны Микротика-сервера.

/system ssh 192.168.1.2 user admin "/ip address print"


Если Вы хотите в будущем осуществлять SSH-соединение в обе стороны, то нужно импортировать оба ключа, и публичный и приватный на оба Микротика.

Всё это хорошо, но работает только в командной строке Терминала. А если мы хотим из скрипта? Увы из скрипта /system ssh не работает. Но теперь есть другой вариант:

Перейдем к /ssh-exec


Согласно официальной документации инструмент /system ssh-exec, в отличии от /system ssh, может использоваться не только в командной строке, но и в скриптах, чего мы давно ждали.

Вот два примера, которые я взял из сетевой записной книжки моего друга и модератора русскоязычного форума Микротик podarok66.

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

:global i ([/system ssh-exec address=192.168.100.10 user=podarok66 command=":put [/system routerboard get model]" as-value ] -> "output")

Второй — создать на удалённом устройстве переменную, присвоив ей значение, взятое из локальной переменной у себя на устройстве:

{:local y "123"; /system ssh-exec address=192.168.100.10 user=podarok66 command=":global cif $y"}

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

Можно также запустить скрипт на удалённом роутере через SSH, если имя этого скрипта нам заранее известно:

/system ssh-exec  address=192.168.1.2 user=admin command="/system script run scriptName"

Можно также выполнить и активную функцию на удалённом Микротике. Если она уже установлена в окружение под глобальной переменной для этого достаточно команды:

/system ssh-exec  address=192.168.1.2 user=admin command=":global myFunc; [\$myFunc]"

Здесь не забываем, что перед вызовом функции её, как и в обычном скрипте, нужно декларировать (объявить):

:global myFunc; [$myFunc]

При передаче символа $ он должен быть экранирован – по этому используем \$.
Исполняя команду в Терминале получаем вот такой вывод:

Status: connecting

Status: finished
downloaded: 0Kib
total: 0kiB
duration: 1s

Welcome back !

Mы видим, что соединение произошло и команда запуска функции была выполнена. Также можно выполнять по SSH функции на удалённом Микротике с параметрами, размещая их при вызове за именем функции так:

/system ssh-exec  address=192.168.1.2 user=admin command=":global myFunc; [\$myFunc Var1 Var2 … VarN]"

Как и во всех функциях параметры могут быть именованными и позиционными.

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

Для чего вообще всё это может быть нужно?

Да для очень многих задач, которые все сразу и не опишешь. Ну, например, для использования каких-то возможностей Микротика SSH-сервера, которых не имеет SSH-клиент. Предположим нам нужно отправить SMS-сообщение, а модем имеется только у Микротика SSH-сервера. Можно ли это сделать через SSH? Конечно. Тестируем эту возможность из командной строки:

/system ssh 192.168.1.2 "/tool sms send lte1 +7XXXYYYYYYY message="you message"

Приведенная выше команда успешно отправляет нам SMS (текст правда может быть только на латиннице, но это уже ограничение ROS). Раз из терминала работает, значит можно и из скрипта! Я тут же написал маленькую функцию (я назвал её SSHsend), которая избавляет программиста и пользователя от необходимости помнить лишнее:

# Function SSH SMS send 
# by Sertik 11/01/2023 
# https://forum.mikrotik.com/viewtopic.php?t=174803#p976624 

# $1 - router IP
# $2 - username 
# $3 - (usbX/lteY)
# $4 - phonenumber
# $5 - SMS-message text

:global SSHsend do={
    do {:set $1 [:toip $1]} on-error={:return "Error syntax address IP SSH-Server"}
    :if (!any $2) do={:return "Error username"}
    :if (!any $3) do={:return "Error SMS interface number or type"}
    :if (!any $4) do={:global ADMINPHONE; :set $4 $ADMINPHONE}
    :if (!any $5) do={:set $5 "test"}

  do {
            [/system ssh-exec address=$1 user=$2 command="/tool sms send port=\"$3\" phone-number=\"$4\" message=\"$5\""]
    } on-error={:return "Error send message from SSH-server"}

 :log warning "SMS $5 to a subscriber $4 via modem $3 router ssh-server IP $1 is sent"; :return "Done"
}

:local sshAns [$SSHsend 192.168.1.2 admin lte2 89107777777 "Hello from router!"]
:if ($sshAns!="Done") do={:log error $sshAns}

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

Тогда мне в голову пришла такая мысль:

А можно ли передать саму функцию с Микротика SSH-клиента на Микротик SSH-сервер?

Думая над решением этой задачи и вспоминая свои и чужие опыты по сохранению кода функции, например, в /ip firewall layer7, я понял, что код функции передать нельзя, так как команда /system ssh-exec передаёт параметр command= в виде строки, которая не получит статус кода на удалённом устройстве. А вот текст функции из скрипта оказалось можно передать и соответственно выполнить, чтобы функция определилась (установилась в окружении) на удаленном Микротике, но с помощью некоторых ухищрений.

Признаюсь, не до всего я дошёл самостоятельно. В данной работе мне очень помог гуру официального форума Микротик Rextended (посмотреть профиль пользователя могут только зарегистрированные пользователи форума Микротик). Он предложил наглядный путь решения, но, оказалось, что я уже решил прежде задачу самостоятельно, не учтя, только, какие именно символы текста передаваемой функции нужно экранировать (эту часть дополнил Rextended).

Итак, для подхода к задаче и тестирования (алгоритм подхода Rextended):

1. Пишем функцию для примера со сложным синтаксисом. Для тестирования была взята функция самого Rextended ip2bin, преобразующая ip-адрес в двоичное число:

# https://forum.mikrotik.com/viewtopic.php?p=973554#p973554 
:global ip2bin do={
    :local number [:tonum [:toip $1]]
    :local ret    ""
    :for i from=0 to=31 step=1 do={
        :set ret "$(($number >> $i) & 1)$ret"
        :if ([:tostr $i]~"^(7|15|23)\$") do={:set ret "$2$ret"}
    }
    :return $ret
}

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

2. Делаем экспорт скриптов репозитория и достаём оттуда нашу функцию:

/system script> export 
# RouterOS 6.48.6
#
/system script
add name=script1 source=":global ip2bin do={\r\
    \n    :local number [:tonum [:toip \$1]]\r\
    \n    :local ret    \"\"\r\
    \n    :for i from=0 to=31 step=1 do={\r\
    \n        :set ret \"\$((\$number >> \$i) & 1)\$ret\"\r\
    \n        :if ([:tostr \$i]~\"^(7|15|23)\\\$\") do={:set ret \"\$2\$ret\"}\r\
    \n    }\r\
    \n    :return \$ret\r\
    \n}"

Мы видим, что текст содержит дополнительные управляющие символы возврата каретки \r (CR) и перевода строки \n (LF).

3. Создаём конструкцию для передачи функции на другой Микротик и выполнения:

В полученном выше экспортном листинге нужно заменить все «\r» на «;» и убрать все «\n» — тогда мы получим исполняемый рабочий код:

[/system ssh-exec address=192.168.1.2 user=admin command=":global ip2bin do={;\
        :local number [:tonum [:toip \$1]];\
        :local ret    \"\";\
        :for i from=0 to=31 step=1 do={;\
            :set ret \"\$((\$number >> \$i) & 1)\$ret\";\
            :if ([:tostr \$i]~\"^(7|15|23)\\\$\") do={:set ret \"\$2\$ret\"};\
        };\
        :return \$ret;\
    }; :global result [\$ip2bin 127.0.0.1]"]

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

# FuncSSHfunc by Sertik 12/01/2023

# $1 - адрес удаленного SSH-сервера
# $2 - username удаленного SSH-сервера для SSH соединения
# $3 - имя скрипта-источника функции на SSH-клиенте 

:global SSHfunc do={

:local ReplaceChars do={
    :local cmd
    :for idx from=0 to=([:len $1] - 1) do={
        :local char [:pick $1 $idx]
        :if ($char~"(\a|\b|\f|\n|\r|\v)") do={:set char ";"}
        :if ($char = "\t") do={:set char " "}
        :set cmd "$cmd$char"
    }
:return $cmd
}

:if ([:len $1]=0) do={:return "Error IP SSH-Server"}
do {:set $1 [:toip $1]} on-error={:return "Error syntax address IP SSH-Server"}
:if (!any $2) do={:return "Error username"}
:if ([:len $3]=0) do={:return "Error Script-sourse name not set"}

:if ([:len [/system script find name=$3]]=0) do={:return "Error Script $3 no find in repository"}
  :local F
    :do { :set F [/system script get $3 source]
       } on-error={:return "Error reading a script into a variable script larger than 4096 bytes"}
    :set F [$ReplaceChars $F]
     :do { [/system ssh-exec address=$1 user=$2 command="$F"]
       } on-error={:return "Error transmitting or creating a script on the SSH server"}
   :log warning "Script $3 was transferred to the SSH server $1 and set function"; :return "Done"
}

:local sshAns [$SSHfunc 192.168.1.2 admin ip2bin]
:if ($sshAns!="Done") do={:log error $sshAns}

Функция SSHfunc берет в качестве источника кода функции скрипт, имя которого указывается в параметре $3 – в данном случае, для примера, всё ту же ip2bin, что мы исполняли в примере выше. Перед передачей текст функции проходит «обработку» — замену управляющих символов в тексте (подфункция ReplaceChars). Без неё моя функция не могла правильно работать. Дальше строка с текстом функции передается на SSH-сервер и исполняется там, устанавливаясь в окружение. Зная её имя, мы можем сразу или по необходимости её выполнить на SSH-сервере, как мы это также уже делали выше. При этом следует учитывать, что имя глобальной переменной, под которым передаваемая функция устанавливается в окружение на SSH-сервере, вовсе не должна соответствовать имени скрипта-источника её кода на SSH-клиенте, указываемого в параметре $3. Это имя мы должны знать заранее.

Функция SSHfunc возвращает различные виды ошибок при ошибочном завершении своей работы (например, при неверно переданных параметрах) и «Done» при успешном.

Rextended предложил свой вариант аналогично работающей функции:

# scr2sshcmd by Rextended 
# https://forum.mikrotik.com/viewtopic.php?t=174803#p977528 
# put [$scr2sshcmd address user scriptFuncSourceName ("<escaped instructions>")]

# $1: IP (as string, but must be a valid IP)
# $2: username (as string with length > 0)
# $3: script name (as string with length > 0)
# $4: extra instructions on command line (as quoted escaped string between parenthesys)
# what return: "DONE" if is done, else nothing.

:global scr2sshcmd do={
    :local remote [:toip  $1] ; :local usr [:tostr $2]
    :local scr    [:tostr $3] ; :local ext [:tostr $4]
    /system script
    :if ([:typeof $remote] = "nil") do={:return "IP wrong or not defined"}
    :if ([:len $usr] < 1)  do={:return "user not defined"}
    :if ([:len [find where name=$scr]]!=1) do={:return "script not found or not defined"}
    :if ($ext~"(\a|\b|\f|\n|\r|\t|\v)")      do={:return "invalid characters on command extension"}
    :local source [get $scr source]
    :local cmd    ""
    :for idx from=0 to=([:len $source] - 1) do={
        :local char [:pick $source $idx]
        :if ($char~"(\a|\b|\f|\n|\r|\v)") do={:set char ";"}
        :if ($char = "\t") do={:set char " "}
        :set cmd "$cmd$char"
    }
    :return [/system ssh-exec address=$remote user=$usr command="$cmd;$ext;:return \"Done\""]
}

Он написал функцию так, что в качестве четвертого параметра ($4) можно сразу использовать инструкции РоутерОС, например, сразу позволяющие исполнить переданную функцию и записать результат исполнения в глобальную переменную удаленного SSH-Микротика, при этом сама функция возвращает результат успешного завершения «DONE» или соответствующие ошибки.

:put [$scr2sshcmd 192.168.1.2 admin ip2bin (":global test [\$ip2bin 127.0.0.1]")]

В данном примере вызова текст функции берется из скрипта-источника (ip2bin). На Микротике SSH-сервере функция декларируется и выполняется, используя в качестве параметра переданный нами адрес 127.0.0.1, получает результат в виде двоичного числа и записывает его в создаваемую переменную test, а нам возвращает «DONE».

Можно получить и сам результат исполненной функции на SSH-клиенте (вернуть результат функции ip2bin), как и любой другой функции, имеющей свой :return. Делается это так:

:global SSHanswer ([/system ssh-exec address=192.168.1.2 user=admin command=":put [\$ip2bin 127.0.0.1]" as-value ] -> "output")
:log warning $SSHanswer

Вывод в лог на Микротике-источнике (SSH-клиенте) (в данном примере представление адреса 127.0.0.1 в виде двоичного числа):

01111111000000000000000000000001

Тот же результат можно получить и используя мою первоначальную функцию SSHfunc, но для этого придётся открывать SSH-соединение ещё раз для получения результата, функция Rextended делает всё за один приём, что, вероятно, рациональнее.

Передачу функций с роутера на роутер можно использовать, например, для защиты скриптов (в смысле защиты от копирования и не санкционированного распространения авторских программ) что по-сути всегда отсутствовало в Роутер ОС). Это возможно, когда на роутере SSH-сервере работают какие-то важные скрипты, нуждающиеся в своей работе в функции, расположенной на SSH-клиенте, а SSH-соединение возможно только в одну сторону (расположение сертификатов исключает обратное). Текст передаваемой по SSH функции на роутере-сервере при этом для прочтения остаётся всегда не доступным, так как на сервер сразу попадает только её нечитабельный код. Кстати, после исполнения функцию можно сразу уничтожить (стереть) из одного SSH-сеанса, что вообще создаёт на SSH-сервере иллюзию какой-то инопланетной инвазии (неизвестный пришёл, что-то сделал и бесследно исчез). Разумеется, это возможно только на роутерах, имеющих сертификаты на SSH (то есть на «доверенных» роутерах). При перезагрузке SSH-сервера функция, переданная SSH-клиентом пропадает из оперативной памяти, её снова нужно получать с SSH-сервера.

В моих планах написать целую библиотеку функций для обслуживания различных вариантов использования SSH-exec. Вот как это может выглядеть на бумаге:

Создание библиотеки для SSH-exec (план):
  1. Получение и передача данных OID (с подключением SNMP)
  2. СЧИТАТЬ СПИСОК СКРИПТОВ, ФУНКЦИЙ, ЗАДАНИЙ ПЛАНИРОВЩИКА удаленного SSH-сервера (чтобы знать их названия на клиенте для последующих вызовов)
  3. Выполнить скрипт удалённого Микротик (SSH-сервера)
  4. Выполнить функцию удалённого Микротик (МТ)
  5. Послать команду Роутер ОС на удалённый роутер
  6. Считать значение переменной на удалённом МТ
  7. Создать переменную исходного типа на удалённом МТ и передать ей значение
  8. Передать функцию/выполнить на удалённом МТ
  9. Передать скрипт/выполнить на удалённом МТ
  10. Создать задание Планировщика на удаленном МТ (с запуском скрипта удалённого МТ, из текста скрипта с переданным именем МТ-клиента)

При создании скриптов и заданий Планировщика на удалённом МТ создавать комментарии к ним (если нужно)

+ передача функции на выполнение с параметрами
+ передача скрипта на выполнение с параметрами

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

17/01/2023 Серков Сергей Владимирович (Sertik)
Теги:
Хабы:
Всего голосов 7: ↑7 и ↓0+7
Комментарии0

Публикации