Исследование переменных Mikrotik. Скрипт обновления Dynamic DNS записей FreeDNS.afraid.org

в 13:58, , рубрики: mikrotik, Сетевые технологии, системное администрирование, скприпты

Я использую Mikrotik в качестве домашнего и офисного маршрутизатора, и в целом система очень нравится. RouterOS имеет широкие возможности, которые покрывают 90% моих задач, если чего-то недостает, то можно «дописать» функционал с помощью внутренних скриптов. Но когда начинаешь писать более-менее вменяемый скрипт или пытаешься понять и применить чужой рецепт, становятся заметны очертания подводной части айсберга, всплывают странные особенности языка.

Я провел небольшое исследование переменных в скриптах Mikrotik, рассмотрел под лупой объявление и инициализацию.
Получилась, на мой взгляд, достойная тема для написания статьи. Итак, приступим.

Что же нам говорит Manual:Scripting о переменных в скриптах? А говорит он нам, что переменные бывают двух областей видимости: локальные и глобальные, что объявляются они командами —

:local <name> <value>
и
:global <name> <value>

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

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

Manual:Scripting говорит нам, что есть около десятка типов, из которых мы рассмотрим только некоторые, важные для понимания переменных. Итак, явные и логичные типы: числовой num, строковый str и массив array. Далее упоминается тип (оно же — значение переменной) nil, про который написано, что он будет у переменной по умолчанию, если ей ничего не присвоено. Поверим.

Если поработать с командной консолью в WinBox, можно заметить, что есть еще одно странное ключевое слово nothing, которое непонятно что означает, какое-то «ничего».

[admin@MikroTik] > :global var0        
[admin@MikroTik] > :put [:typeof $var0]
nothing
[admin@MikroTik] > /environment print  
var0=[:nothing]

Ну и где тут nil? Зато полно nothing.

Исследование nil

В общем, чтобы разобраться с этими особыми типами и значениями, применим системный подход, проанализируем выдачу однотипных запросов, но с разными наборами исходных данных. А исходными данными у нас будут в начале разные «странные» константы и выражения. В результате получается такая таблица:

Что это? <value> <value2> :if (<value> = <value2>) do={:put TRUE} else={:put FALSE} :put [:typeof <value>] :put [:typeof [<value>]] :put [:len <value>] :put [:len [<value>]]
1 Nothing в командной обертке [] / [:nothing] / или даже так [:] [] TRUE nil nil 0 0
2 Пустая строка "" "" / {} TRUE str expected command name 0 0
3 Nothing внутри выражения (:nothing) / или даже так (:) [] / "" / {} FALSE nothing nil 0 0
4 Массив из nothing {:nothing} / {:} {} / "" TRUE array nil 1 1
5 Элемент массива nothing ({:nothing}->0) [] / "" / {} FALSE nothing nil 0 0

Тут даны результаты сравнения различных констант выражений и команд над константами между собой. Понимание свойств констант поможет при написании правых частей операторов сравнения.

На счет полей таблицы. В языке скриптов Mikrotik квадратные скобки обозначают встраивание результата команды в общее выражение, поэтому в общем смысле value != [value], это важно.

Прокомментирую построчно:

Строка 1: все варианты записи поля value синонимичны! В будущем я предлагаю использовать лаконичное []. Повторюсь, что квадратные скобки обозначают встраивание результата команды в общее выражение. Как видите, это один из способов «генерации» nil не в чистом виде, а как результат пустой команды в квадратных скобках, который как раз равен nil.

Строка 2:
Тут все просто. Пустая строка вполне очевидная вещь. Единственные момент, что она оказывается равна массиву из ничего, но предлагаю не обращать на это особого внимание. И нельзя использовать пустую строку как команду в квадратных скобках, это единственное место, где терминал не проглотил синтаксис по исходным данным таблицы, логично.

Строка 3:
Круглые скобки несут в себе выражение, внутри можно тоже поместить nothing, и результат от такого выражения тоже будет nothing, по сути это почти чистый nothing. Видно, что тип результата выражения (:nothing) дает nothing, а тип от обертки из командных скобок [] дает nil, по аналогии со строкой 1. Вообще после повторного осмысления написанного, я понял что это спекуляция, т.к. внутрь () можно поместить что угодно, любой бессмысленный набор символов, главное, что результат (...) дает nothing.

Строка 4:
Массив, который содержит ничего, на самом деле содержит 1 элемент. Тип, как и ожидалось, array, тип результата командной обертки также дает nil

Строка 5:
Добираемся до элемента массива из ничего, прослеживается полная аналогия выдачи со строкой 3. В общем такой элемент — это тоже чистое nothing.

Какие общие выводы можно сделать из этого странного brainfuck-а. nothing действительно существует, им можно оперировать, редкие выражения могут его возвращать, но команды в [] никогда не возвращают nothing, а только nil. Другой более важный вывод, что оператор длины значения или кол-ва элементов массива :len ведет себя очень стабильно и генерирует предсказуемый результат, поэтому его я могу однозначно рекомендовать для использования в скриптах, когда требуется проверка возвращаемых выражениями и командами значений. И что [] = [:nothing] = nil.

Таблица дает представление о том, что могут возвращать различные команды и выражения языка Mikrotik.

Исследование объявлений переменных

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

Что это? Объявление <val> <val2> :put <val> :if (<val> = <val2>) do={:put TRUE} else={:put FALSE} :put [:typeof <val>] :put [:typeof [<val>]] :put [:len <val>] оно же :put [:len [<val>]] /environment print
1 Без присвоения :global var1 $var1 [] / (:) FALSE / TRUE nothing nil 0 var1=[:nothing]
2 Присвоение, удаляющее переменную :global var2 (:nothing) $var2 (:) TRUE nothing nil 0 -
3 Присвоение nil :global var3 [] $var3 [] TRUE nil nil 0 var3=[]
4 Присвоение пустой строки :global var4 "" $var4 "" / {} TRUE / TRUE str str 0 var4=""
5 (3) Странный nil, аналог 3 :global var5 [{}] $var5 [{}] / [] TRUE / TRUE nil nil 0 var5=[]
6 Массив из nothing :global var6 {:} $var6 "" / {""} / {} TRUE / TRUE / TRUE array array 1 var6={[:nothing]}
7 (6, 8) Массив из пустой строки :global var7 {""} $var7 "" / {""} / {} TRUE / TRUE / TRUE array array 1 var7={""}
8 (6, 7) Массив из nil :global var8 {[]} $var8 "" / {""} / {} TRUE / TRUE / TRUE array array 1 var8={[]}
Что это? Объявление <val> <val2> :put <val> :if (<val> = <val2>) do={:put TRUE} else={:put FALSE} :put [:typeof <val>] :put [:typeof [<val>]] :put [:len <val>] оно же :put [:len [<val>]] /environment print
9 Присвоение числа :global var9 123 $var9 н./и. 123 н./п. num num 3 var9=123
10 Присвоение строки :global var10 "987" $var10 н./и. 987 н./п. str str 3 var10="987"
11 Массив из одного числа :global var11 {555} / :global var11 {555;} $var11 н./и. 555 н./п. array array 1 var11={555}
12 Массив разнородных элементов :global var12 {33;"test123"} $var12 н./и. 33;test123 н./п. array array 2 var12={33; "test123"}
13 Элемент массива -//- ($var12->0) н./и. 33 н./п. num num 2 -//-
14 Элемент массива -//- ($var12->1) н./и. test123 н./п. str str 7 -//-
15 Массив c nothing элементом :global var13 {33;(:)} $var13 н./и. 33; н./п. array array 2 var13={33; [:nothing]}
16 Элемент массива -//- ($var13->0) н./и. 33 н./п. num num 2 -//-
17 Элемент массива -//- ($var13->1) н./и. н./п. nothing nil 0 -//-
18 Массив c nil элементом :global var14 {1012;[]} $var14 н./и. 1012; н./п. array array 2 var14={1012; []}
19 Элемент массива -//- ($var14->1) н./и. н./п. nil nil 0 -//-

н./и. — не используется; н./п. — не применимо

Построчные комментарии к таблице:

1. Переменная создана, но ей ничего не присвоено. Переменная как бы содержит nothing.
2. Если переменной присвоить такое выражение, то это приведет к удалению глобальной переменной из переменных окружения.
3. Стандартный способ создания пустых переменных. Переменная содержит nil.
4. Присвоение пустой строки. Тут все очевидно.
5. Получается, что такое выражение аналогично [] простому присвоению nil, как в 3. Думаю, это потому, что внутри [] несуществующая команда и результат этой команды дает nil
6, 7, 8. Присвоение фигурных скобок делает из переменной массив, хоть и пустой. Обратите внимание, что записи имеют одинаковые результаты в таблице, но это не касается свойств элементов этих массивов. Свойства элементов массивов рассмотрены ниже.
9, 10. Простые типы данных. Все довольно очевидно.
11. Массив из одного элемента, обратите внимание, что {555;} по результатам равно {555}
12, 13, 14. В массив могут входить элементы разных типов данных. Исследование свойств элементов массива дает предсказуемые результаты.
15, 16, 17. Один из элементов массива nothing. Элементы массива обладают теми же свойствами, что и просто переменные и константы данных типов и значений. Прослеживается аналогия с пунктом 2.
18, 19. Прослеживается аналогия с пунктом 3.

Исследование, на мой взгляд, получилось немного спорным, но я очень надеюсь, оно внесет больше порядка в ваше понимание Mikrotik, чем хаоса. В качестве дополнительной компенсации публикую скрипт для работы с динамическим DNS замечательного сервиса FreeDNS.afraid.org.

Скрипт для FreeDNS.afraid.org

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

За основу я взял скрипт от LESHIYODESSA. Мне не очень понравился его алгоритм, в котором использовался файл для хранения текущих адресов записей Dynamic DNS и производился его периодический парсинг, кроме того, скрипт не поддерживает обновление разных записей, для этого предлагается размножить скрипт, но это не снимает проблему обновления записи по заданному IP-адресу. Поэтому фактически я написал свой собственный скрипт, в котором заменил работу с файлами на более надежный механизм периодического обновления (с часовым интервалом) и форсированное обновление по изменению отслеживаемых IP-адресов интерфейсов, полученных по DHCP, независимое для нескольких записей.

Объявляем массивы имен субдоменов FreeDNS.afraid.org и их хешей, имен WAN-инетфейсов, у которых мы будем отслеживать IP-адреса. А также задаем кол-во записей (Quant) по размеру массива либо вручную:

:local SubdomainHashes {"U3dWVE5V01TWxPcjluEo0bEtJQWjg5DUz=";"U3pWV5VFTWxPcjlOEo0EtJpOE1MAyDc="}
:global DNSDomains {"aaa.xyz.pu";"bbb.xyz.pu"}
:global WANInterfaces {"ether4-WAN-Inet";"ether3-WAN-Beeline"}
:global Quant [:len $DNSDomains]

Объявляем вспомогательные переменные:

SkipCounters — массив счетчиков проверок отслеживаемых интерфейсов
LastIPs — массив IP-адресов, которые уже были отправлены в FreeDNS.

Массив счетчиков позволит обновлять Dynamic DNS записи независимо друг от друга.

Сначала я делаю пустое объявление переменной :global SkipCounters, такое объявление позволяет либо создать новую глобальную переменную, либо использовать уже существующую в переменных окружения и ее значение без перезаписи.

Следующие конструкции работают для только что созданных переменных, проверяется тип данных, если он не массив, то тип меняется на массив, и присваиваются значения переменных. Таким образом на выходе мы имеем проинициализированные нужными значениями переменные типа массив.

:global SkipCounters
:if ([:typeof $SkipCounters] != "array") do={
:set SkipCounters {""}
:for i from=0 to=($Quant-1) do={:set ($SkipCounters->$i) 1}
}

:global LastIPs
:if ([:typeof $LastIPs] != "array") do={
:set LastIPs {""}
:for i from=0 to=($Quant-1) do={:set ($LastIPs->$i) ""}
}

Ни и собственно сам алгоритм отслеживания-обновления.

Получаем текущий IP-адрес из dhcp-client. Дальше самое интересное.

Команда [/ip dhcp-client get [find where interface=($WANInterfaces->$i)] address] в общем случае может вернуть что угодно кроме строки, содержащей IP-адрес, поэтому она в норме обновит значение переменной CurrentIP. Возвращаемым значением может быть либо строка с IP, либо nil, или будет ошибка выполнения и команда не обновит CurrentIP. Поэтому я строкой выше ввожу явное объявление :local CurrentIP "". И после выполнения команды в CurrentIP будет либо "", либо nil, либо IP-адрес.

Как я писал выше, наибольшей устойчивостью обладает оператор :len, поэтому используем его дальше для проверки адекватности полученных данных [:len $CurrentIP] > 0. Еще отслеживаем значение счетчика, и если он >=60, принудительно отсылаем запрос в FreeDNS. Таким образом повышается устойчивость алгоритма к проблемам связи. Скрипт в шедулере у меня выполняется раз в минуту, поэтому период обязательного обновления около 1 часа, что не сильно обременяет сервис FreeDNS.

На что еще стоит обратить внимание. В URL запроса на обновление присутствует параметр "&address=".$CurrentIP, этот параметр позволяет явно указать IP-адрес для субдомена вместо автоматического (по интерфейсу с которого ушел запрос).

:for i from=0 to=($Quant-1) do={
:local CurrentIP ""
:set CurrentIP [/ip dhcp-client get [find where interface=($WANInterfaces->$i)] address]
:set CurrentIP [:pick $CurrentIP 0 ([:len $CurrentIP]-3)]
# :log info ("Current SkipCounter$i: ".($SkipCounters->$i))
:if ([:len $CurrentIP] > 0 and ($CurrentIP != ($LastIPs->$i) or ($SkipCounters->$i) > 59)) do={
:if ($CurrentIP != ($LastIPs->$i)) do={
:log info ("Service Dynamic DNS: Renew IP: ".($LastIPs->$i)." for ".($DNSDomains->$i)." to $CurrentIP")
}
/tool fetch url=("http://freedns.afraid.org/dynamic/update.php?".($SubdomainHashes->$i)."&address=".$CurrentIP) keep-result=no
:set ($LastIPs->$i) $CurrentIP
:set ($SkipCounters->$i) 1
} else={
:set ($SkipCounters->$i) (($SkipCounters->$i) + 1)
}
}

Скрипт MultiFreeDNS целиком

# MultiFreeDNS
:local SubdomainHashes {"U3dWVE5V01TWxPcjluEo0bEtJQWjg5DUz=";"U3pWV5VFTWxPcjlOEo0EtJpOE1MAyDc="}
:global DNSDomains {"aaa.xyz.pu";"bbb.xyz.pu"}
:global WANInterfaces {"ether4-WAN-Inet";"ether3-WAN-Beeline"}
:global Quant [:len $DNSDomains]

:global SkipCounters
:if ([:typeof $SkipCounters] != "array") do={
:set SkipCounters {""}
:for i from=0 to=($Quant-1) do={:set ($SkipCounters->$i) 1}
}

:global LastIPs
:if ([:typeof $LastIPs] != "array") do={
:set LastIPs {""}
:for i from=0 to=($Quant-1) do={:set ($LastIPs->$i) ""}
}

:for i from=0 to=($Quant-1) do={
:local CurrentIP ""
:set CurrentIP [/ip dhcp-client get [find where interface=($WANInterfaces->$i)] address]
:set CurrentIP [:pick $CurrentIP 0 ([:len $CurrentIP]-3)]
# :log info ("Current SkipCounter$i: ".($SkipCounters->$i))
:if ([:len $CurrentIP] > 0 and ($CurrentIP != ($LastIPs->$i) or ($SkipCounters->$i) > 59)) do={
:if ($CurrentIP != ($LastIPs->$i)) do={
:log info ("Service Dynamic DNS: Renew IP: ".($LastIPs->$i)." for ".($DNSDomains->$i)." to $CurrentIP")
}
/tool fetch url=("http://freedns.afraid.org/dynamic/update.php?".($SubdomainHashes->$i)."&address=".$CurrentIP) keep-result=no
:set ($LastIPs->$i) $CurrentIP
:set ($SkipCounters->$i) 1
} else={
:set ($SkipCounters->$i) (($SkipCounters->$i) + 1)
}
}

Автор: Chupakabra303

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js