Скрипты на MikroTik — /system/script
Скрипты на MikroTik — /system/script от основ до автоматизации
Скрипты MikroTik (/system/script) — встроенная система хранения и выполнения программного кода на RouterOS scripting language. Скрипты позволяют автоматизировать любую задачу: от backup конфигурации и мониторинга каналов до интеграции с Telegram и автоматического обновления RouterOS. В отличие от одноразовых команд в terminal, скрипты сохраняются на роутере и могут вызываться из scheduler, netwatch, hotspot on-login, PPP on-up/on-down и других событийных механизмов RouterOS. В этой статье подробно разберём синтаксис RouterOS scripting language, управление скриптами, 10 готовых скриптов для copy-paste, отладку, безопасность и типичные ошибки.
Описание
Что такое /system/script
/system/script — хранилище скриптов на роутере MikroTik. Каждый скрипт имеет имя, исходный код (source), набор прав (policy) и может быть запущен вручную или автоматически. Скрипты хранятся в NVRAM и сохраняются после перезагрузки.
Где можно вызывать скрипты:
| Источник вызова | Описание | Пример |
|---|---|---|
| Terminal | Ручной запуск из CLI | /system/script/run my-script |
| Scheduler | По расписанию | /system/scheduler add on-event=my-script |
| Netwatch | При изменении доступности хоста | Переключение канала при down |
| Hotspot on-login | При подключении клиента hotspot | Логирование, уведомление |
| PPP on-up / on-down | При подключении/отключении VPN-клиента | Динамические маршруты |
| DHCP lease-script | При выдаче/освобождении DHCP lease | Учёт устройств |
| Tool/Netwatch | При изменении маршрута | Failover-сценарии |
Архитектура скриптовой системы
code[/system/script] │ ├── source — исходный код скрипта ├── policy — права выполнения ├── last-started — время последнего запуска ├── run-count — количество запусков └── owner — создатель скрипта │ ▼ [/system/script/environment] │ └── Глобальные переменные — доступны всем скриптам │ ▼ [/system/script/job] │ └── Текущие выполняющиеся скрипты (running jobs)
Script vs Scheduler on-event vs Terminal
| Критерий | /system/script | Scheduler on-event | Terminal |
|---|---|---|---|
| Хранение | Постоянное (NVRAM) | В конфигурации scheduler | Не сохраняется |
| Повторное использование | Да (из scheduler, netwatch и т.д.) | Только в этой задаче | Нет |
| Максимальная длина | Практически неограничена | Ограничена полем on-event | Неограничена |
| Отладка | /system/script/run + :put/:log | Только через логи | Вывод в terminal |
| Вызов из других мест | Да | Нет | Нет |
| Версионирование | Нет (ручное через /export) | Нет | Нет |
Синтаксис RouterOS scripting language
Переменные
RouterOS поддерживает два типа переменных: локальные (:local) и глобальные (:global).
[admin@MikroTik] ># Локальная переменная — видна только в текущем скрипте :local myVar "Hello World" :local myNum 42 :local myBool true :local myIP 192.168.1.1 :local myTime 00:30:00 # Глобальная переменная — доступна всем скриптам :global sharedVar "shared value" # Использование переменных :put $myVar :log info "Number is: $myNum" # Переопределение :set myVar "New value"
Правила области видимости (scope):
| Тип | Область видимости | Время жизни | Доступ из другого скрипта |
|---|---|---|---|
:local | Текущий блок { } | До конца блока | Нет |
:global | Весь роутер | До перезагрузки или :set | Да |
| Environment | Постоянное хранилище | Переживает перезагрузку | Через /system/script/environment |
Типы данных
[admin@MikroTik] ># Строка :local str "hello" # Число :local num 100 # Boolean :local flag true # IP-адрес :local ip 10.0.0.1 # IP-префикс :local net 192.168.1.0/24 # Время :local t 01:30:00 # Массив (array) :local arr {"apple";"banana";"cherry"} # Ничего (nil) :local empty
Условные конструкции (if/else)
[admin@MikroTik] ># Простое условие :local cpu [/system/resource/get cpu-load] :if ($cpu > 80) do={ :log warning "High CPU: $cpu%" } # if-else :local mem [/system/resource/get free-memory] :if ($mem < 16000000) do={ :log error "Low memory: $mem bytes" } else={ :log info "Memory OK: $mem bytes" } # Вложенные условия :local status "warning" :if ($status = "critical") do={ :log error "CRITICAL!" } else={ :if ($status = "warning") do={ :log warning "Warning state" } else={ :log info "Normal" } }
Операторы сравнения:
| Оператор | Описание | Пример |
|---|---|---|
= | Равно | $a = "yes" |
!= | Не равно | $a != "" |
< | Меньше | $num < 100 |
> | Больше | $num > 0 |
<= | Меньше или равно | $num <= 50 |
>= | Больше или равно | $num >= 10 |
~ | Содержит (regex-like) | $str ~ "error" |
in | Входит в список | $item in $array |
Логические операторы:
| Оператор | Описание | Пример |
|---|---|---|
&& или and | Логическое И | ($a > 0 && $b > 0) |
|| или or | Логическое ИЛИ | ($a = "" || $b = "") |
! или not | Логическое НЕ | (!$flag) |
Циклы
[admin@MikroTik] ># foreach — перебор элементов :foreach iface in=[/interface/find where type="ether"] do={ :local name [/interface/get $iface name] :local status [/interface/get $iface running] :log info "Interface $name running=$status" } # foreach — перебор массива :local servers {"8.8.8.8";"1.1.1.1";"77.88.8.8"} :foreach srv in=$servers do={ :put "Server: $srv" } # while — цикл с условием :local count 0 :while ($count < 10) do={ :set count ($count + 1) :put "Iteration: $count" } # for — цикл со счётчиком :for i from=1 to=5 do={ :put "Step $i" }
Работа со строками
[admin@MikroTik] ># Длина строки :local str "Hello MikroTik" :local len [:len $str] :put "Length: $len" # Подстрока (pick) — извлечение части строки :local sub [:pick $str 0 5] :put $sub # Результат: Hello # Поиск подстроки (find) :local pos [:find $str "MikroTik"] :put "Found at: $pos" # Результат: Found at: 6 # Конкатенация строк :local first "Hello" :local second "World" :local result ("$first $second!") :put $result # Преобразование типов :local num 42 :local str [:tostr $num] :local back [:tonum $str] # Замена символов через pick + конкатенацию :local original "192.168.1.1/24" :local slashPos [:find $original "/"] :local ipOnly [:pick $original 0 $slashPos] :put "IP without mask: $ipOnly"
Массивы (arrays)
[admin@MikroTik] ># Создание массива :local fruits {"apple";"banana";"cherry"} # Доступ по индексу :put ($fruits->0) # Результат: apple # Длина массива :put [:len $fruits] # Результат: 3 # Перебор массива :foreach f in=$fruits do={ :put "Fruit: $f" } # Ассоциативный массив (key-value) :local config { "server"="10.0.0.1"; "port"="8080"; "timeout"="30" } :put ($config->"server") # Результат: 10.0.0.1
Функции
RouterOS поддерживает определение функций через :local с do=:
[admin@MikroTik] ># Определение функции :local sendLog do={ :local level $1 :local message $2 :if ($level = "error") do={ :log error $message } else={ :log info $message } } # Вызов функции [$sendLog "error" "Something went wrong"] [$sendLog "info" "All systems normal"] # Функция с возвратом значения :local getWanIP do={ :local iface $1 :local addr [/ip/address/get [find interface=$iface] address] :return [:pick $addr 0 [:find $addr "/"]] } :local myIP [$getWanIP "ether1"] :put "WAN IP: $myIP"
Обработка ошибок
[admin@MikroTik] ># do ... on-error — try/catch аналог :do { /tool/fetch url="https://example.com/data.txt" dst-path=data.txt :log info "Fetch successful" } on-error={ :log error "Fetch failed — host unreachable or timeout" } # Проверка существования перед обращением :local ifaceId [/interface/find where name="ether1"] :if ([:len $ifaceId] = 0) do={ :log error "Interface ether1 not found" } else={ :local status [/interface/get $ifaceId running] :log info "ether1 running: $status" }
Delay и Logging
[admin@MikroTik] ># Задержка выполнения :delay 5s :delay 500ms :delay 1m # Логирование с разными уровнями :log info "Information message" :log warning "Warning message" :log error "Error message" :log debug "Debug message" # Вывод в terminal (только при ручном запуске) :put "This appears in terminal only"
Примеры готовых скриптов
Скрипт 1: Backup конфигурации на email
Полный скрипт для экспорта конфигурации и отправки на email. Предварительно настройте /tool/e-mail.
[admin@MikroTik] >/system/script add name=backup-email \ policy=read,write,ftp,test,sensitive \ source={ # Настройки :local emailTo "admin@example.com" :local identity [/system/identity/get name] :local date [/system/clock/get date] :local time [/system/clock/get time] :local fname ("backup-$identity-$date") # Создание backup /export file=$fname :delay 3s /system/backup/save name=$fname dont-encrypt=yes :delay 3s # Отправка email :local body ("Backup from router: $identity\n" . \ "Date: $date $time\n" . \ "RouterOS: " . [/system/resource/get version]) /tool/e-mail/send to=$emailTo \ subject="[MikroTik] Backup - $identity - $date" \ body=$body \ file=("$fname.rsc,$fname.backup") :delay 5s # Очистка файлов /file/remove [find name~"$fname"] :log info "Backup sent to $emailTo" }
Настройка email-сервера:
[admin@MikroTik] >/tool/e-mail set server=smtp.gmail.com port=587 \ start-tls=yes \ from="router@example.com" \ user="router@example.com" \ password="app-password-here"
Скрипт 2: Мониторинг канала — переключение при падении (failover)
Автоматическое переключение на резервный канал при потере связи через основной. Используется совместно с netwatch.
[admin@MikroTik] >/system/script add name=failover-switch \ policy=read,write,test \ source={ :local primaryGw "192.168.1.1" :local backupGw "192.168.2.1" :local checkHost "8.8.8.8" :local primaryIface "ether1" # Ping проверка :local pingResult [/ping $checkHost count=3 interface=$primaryIface] :if ($pingResult = 0) do={ # Основной канал недоступен :log warning "Failover: primary link DOWN, switching to backup" /ip/route/set [find where dst-address="0.0.0.0/0" and gateway=$primaryGw] \ disabled=yes /ip/route/set [find where dst-address="0.0.0.0/0" and gateway=$backupGw] \ disabled=no } else={ # Основной канал работает :local primaryDisabled [/ip/route/get \ [find where dst-address="0.0.0.0/0" and gateway=$primaryGw] disabled] :if ($primaryDisabled = true) do={ :log info "Failover: primary link UP, switching back" /ip/route/set [find where dst-address="0.0.0.0/0" and gateway=$primaryGw] \ disabled=no /ip/route/set [find where dst-address="0.0.0.0/0" and gateway=$backupGw] \ disabled=yes } } }
Привязка к scheduler для проверки каждые 30 секунд:
[admin@MikroTik] >/system/scheduler add name=failover-check interval=30s \ on-event="/system/script/run failover-switch" \ policy=read,write,test
Скрипт 3: Обновление DNS blacklist через /tool/fetch
Загрузка списка вредоносных доменов и добавление их в static DNS с адресом 0.0.0.0 (sinkhole).
[admin@MikroTik] >/system/script add name=dns-blacklist-update \ policy=read,write,ftp,test \ source={ :local url "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" :local tmpFile "hosts.txt" :local count 0 :log info "DNS Blacklist: downloading..." :do { /tool/fetch url=$url dst-path=$tmpFile mode=https :delay 5s } on-error={ :log error "DNS Blacklist: download failed" :error "Download failed" } # Удаление старых записей с комментарием blacklist /ip/dns/static/remove [find where comment="blacklist"] # Парсинг файла и добавление записей :local content [/file/get [find name=$tmpFile] contents] :local lineStart 0 :local contentLen [:len $content] :while ($lineStart < $contentLen && $count < 1000) do={ :local lineEnd [:find $content "\n" $lineStart] :if ([:typeof $lineEnd] = "nil") do={ :set lineEnd $contentLen } :local line [:pick $content $lineStart $lineEnd] :set lineStart ($lineEnd + 1) # Пропуск комментариев и localhost :if ([:pick $line 0 1] != "#" && [:len $line] > 10) do={ :local spacePos [:find $line " " 4] :if ([:typeof $spacePos] != "nil") do={ :local domain [:pick $line ($spacePos + 1) [:len $line]] :set domain [:pick $domain 0 ([:len $domain])] :if ($domain != "localhost" && [:len $domain] > 3) do={ :do { /ip/dns/static/add name=$domain address=0.0.0.0 \ comment="blacklist" ttl=1d :set count ($count + 1) } on-error={} } } } } # Очистка временного файла /file/remove [find name=$tmpFile] :log info "DNS Blacklist: added $count domains" }
Скрипт 4: Уведомление в Telegram при входе в WinBox
Мониторинг подключений к WinBox и SSH с отправкой уведомлений в Telegram.
[admin@MikroTik] >/system/script add name=telegram-login-alert \ policy=read,write,ftp,test,sensitive \ source={ :local botToken [/system/script/environment get [find name=telegramBot] value] :local chatId [/system/script/environment get [find name=telegramChat] value] :local identity [/system/identity/get name] # Получаем последние записи лога о входе :local logEntries [/log/find where message~"logged in" and topics~"system"] :local lastEntry ($logEntries->([:len $logEntries] - 1)) :local logMsg [/log/get $lastEntry message] :local logTime [/log/get $lastEntry time] :local msg ("Security Alert!\n" . \ "Router: $identity\n" . \ "Event: $logMsg\n" . \ "Time: $logTime") # URL encode пробелы :local encodedMsg "" :for i from=0 to=([:len $msg] - 1) do={ :local char [:pick $msg $i ($i + 1)] :if ($char = " ") do={ :set encodedMsg ($encodedMsg . "%20") } else={ :if ($char = "\n") do={ :set encodedMsg ($encodedMsg . "%0A") } else={ :set encodedMsg ($encodedMsg . $char) } } } :local url ("https://api.telegram.org/bot$botToken/sendMessage" . \ "\?chat_id=$chatId&text=$encodedMsg") :do { /tool/fetch url=$url keep-result=no :log info "Telegram alert sent: login notification" } on-error={ :log error "Telegram alert failed" } }
Предварительная настройка переменных:
[admin@MikroTik] >/system/script/environment add name=telegramBot value="123456789:AAF-your-bot-token" /system/script/environment add name=telegramChat value="-1001234567890"
Скрипт 5: Автоматическое обновление RouterOS
Проверка обновлений и автоматическая установка с перезагрузкой (использовать с осторожностью).
[admin@MikroTik] >/system/script add name=auto-update-routeros \ policy=read,write,ftp,reboot,policy \ source={ :local channel [/system/package/update/get channel] :local identity [/system/identity/get name] :log info "Auto-update: checking for updates (channel: $channel)" /system/package/update/check-for-updates :delay 15s :local installed [/system/package/update/get installed-version] :local latest [/system/package/update/get latest-version] :if ($installed = $latest) do={ :log info "Auto-update: already on latest version $installed" :return [] } :log warning "Auto-update: upgrade available $installed -> $latest" # Создаём backup перед обновлением /export file=("pre-update-$installed") /system/backup/save name=("pre-update-$installed") dont-encrypt=yes :delay 5s # Скачиваем обновление :log warning "Auto-update: downloading $latest..." /system/package/update/install # RouterOS перезагрузится автоматически после скачивания }
Важно: автоматическое обновление рекомендуется только для некритичных устройств. Для production используйте ручное обновление после тестирования.
Скрипт 6: Watchdog — перезагрузка если ping не проходит
Если определённый хост недоступен в течение длительного времени — перезагружаем роутер (последнее средство).
[admin@MikroTik] >/system/script add name=watchdog-reboot \ policy=read,write,test,reboot \ source={ :local checkHost "8.8.8.8" :local failThreshold 5 :global watchdogFails # Инициализация счётчика при первом запуске :if ([:typeof $watchdogFails] = "nothing") do={ :set watchdogFails 0 } :local pingResult [/ping $checkHost count=3 interval=1] :if ($pingResult = 0) do={ :set watchdogFails ($watchdogFails + 1) :log warning "Watchdog: ping failed ($watchdogFails/$failThreshold)" :if ($watchdogFails >= $failThreshold) do={ :log error "Watchdog: $failThreshold failures reached, REBOOTING" :set watchdogFails 0 /system/reboot } } else={ :if ($watchdogFails > 0) do={ :log info "Watchdog: ping restored after $watchdogFails failures" :set watchdogFails 0 } } }
Привязка к scheduler:
[admin@MikroTik] >/system/scheduler add name=watchdog interval=1m \ on-event="/system/script/run watchdog-reboot" \ policy=read,write,test,reboot
Скрипт 7: Динамический DNS (обновление IP)
Универсальный скрипт обновления DDNS с проверкой изменения IP.
[admin@MikroTik] >/system/script add name=ddns-updater \ policy=read,write,ftp,test \ source={ :local ddnsService "freedns" :local ddnsToken "your-update-token-here" :local wanIface "ether1" :global ddnsLastIP # Получаем текущий WAN IP :local currentIP :do { :set currentIP [/ip/address/get [find interface=$wanIface] address] :set currentIP [:pick $currentIP 0 [:find $currentIP "/"]] } on-error={ :log error "DDNS: cannot get IP from $wanIface" :return [] } # Проверяем, изменился ли IP :if ($currentIP = $ddnsLastIP) do={ :return [] } :log info "DDNS: IP changed $ddnsLastIP -> $currentIP, updating..." # Обновление на DDNS-сервисе :local updateUrl ("https://freedns.afraid.org/dynamic/update.php\?" . \ "$ddnsToken&address=$currentIP") :do { /tool/fetch url=$updateUrl keep-result=no :set ddnsLastIP $currentIP :log info "DDNS: updated successfully to $currentIP" } on-error={ :log error "DDNS: update failed" } }
Скрипт 8: Сбор статистики трафика по интерфейсам
Запись статистики RX/TX по всем ethernet-интерфейсам в лог.
[admin@MikroTik] >/system/script add name=traffic-stats \ policy=read,write,test \ source={ :local interfaces [/interface/find where type="ether"] :local report "" :foreach iface in=$interfaces do={ :local name [/interface/get $iface name] :local rxBytes [/interface/get $iface rx-byte] :local txBytes [/interface/get $iface tx-byte] :local running [/interface/get $iface running] # Конвертация в MB :local rxMB ($rxBytes / 1048576) :local txMB ($txBytes / 1048576) :set report ($report . "$name: RX=$rxMB MB TX=$txMB MB " . \ "running=$running\n") } :log info ("Traffic report:\n$report") # Опционально: сброс счётчиков # /interface/reset-counters [find where type="ether"] }
Скрипт 9: Автоблокировка IP после N неудачных попыток
Анализ лога на предмет brute-force атак и автоматическое добавление атакующих IP в blacklist.
[admin@MikroTik] >/system/script add name=auto-block-bruteforce \ policy=read,write \ source={ :local maxAttempts 5 :local blockTimeout "1d" :local blockList "bruteforce-blocked" # Поиск записей о неудачных попытках входа :local logEntries [/log/find where message~"login failure" and topics~"system"] # Подсчёт попыток по IP :local ipCounts :set ipCounts [:toarray ""] :foreach entry in=$logEntries do={ :local msg [/log/get $entry message] # Извлечение IP из сообщения :local fromPos [:find $msg "from "] :if ([:typeof $fromPos] != "nil") do={ :set fromPos ($fromPos + 5) :local endPos [:find $msg " " $fromPos] :if ([:typeof $endPos] = "nil") do={ :set endPos [:len $msg] } :local attackIP [:pick $msg $fromPos $endPos] # Проверяем, не заблокирован ли уже :local alreadyBlocked [/ip/firewall/address-list/find \ where list=$blockList and address=$attackIP] :if ([:len $alreadyBlocked] = 0) do={ # Увеличиваем счётчик :if ([:typeof ($ipCounts->$attackIP)] = "nothing") do={ :set ($ipCounts->$attackIP) 1 } else={ :set ($ipCounts->$attackIP) (($ipCounts->$attackIP) + 1) } # Блокировка при превышении порога :if (($ipCounts->$attackIP) >= $maxAttempts) do={ /ip/firewall/address-list/add list=$blockList \ address=$attackIP timeout=$blockTimeout \ comment="Auto-blocked: brute force" :log warning "Auto-blocked IP: $attackIP ($maxAttempts+ failures)" } } } } }
Создание правила firewall для блокировки:
[admin@MikroTik] >/ip/firewall/filter add chain=input \ src-address-list=bruteforce-blocked \ action=drop \ comment="Drop brute-force blocked IPs" \ place-before=0
Скрипт 10: Генерация отчёта по Wi-Fi клиентам
Сбор информации обо всех подключённых Wi-Fi клиентах с уровнем сигнала.
[admin@MikroTik] >/system/script add name=wifi-clients-report \ policy=read,write,ftp,test,sensitive \ source={ :local botToken [/system/script/environment get [find name=telegramBot] value] :local chatId [/system/script/environment get [find name=telegramChat] value] :local identity [/system/identity/get name] :local clients [/interface/wifi/registration-table/find] :local count [:len $clients] :local report ("Wi-Fi Clients Report: $identity\n" . \ "Total clients: $count\n\n") :foreach c in=$clients do={ :local mac [/interface/wifi/registration-table/get $c mac-address] :local iface [/interface/wifi/registration-table/get $c interface] :local signal [/interface/wifi/registration-table/get $c signal] :local uptime [/interface/wifi/registration-table/get $c uptime] :local txRate [/interface/wifi/registration-table/get $c tx-rate] :local rxRate [/interface/wifi/registration-table/get $c rx-rate] # Поиск hostname из DHCP lease :local hostname "unknown" :do { :set hostname [/ip/dhcp-server/lease/get \ [find where mac-address=$mac] host-name] } on-error={} :set report ($report . "MAC: $mac\n" . \ " Host: $hostname\n" . \ " Interface: $iface\n" . \ " Signal: $signal dBm\n" . \ " TX/RX: $txRate / $rxRate\n" . \ " Uptime: $uptime\n\n") } :log info $report # Отправка в Telegram (если настроен) :if ([:typeof $botToken] != "nothing" && $botToken != "") do={ :local url ("https://api.telegram.org/bot$botToken/sendMessage") /tool/fetch url=$url http-method=post \ http-data="chat_id=$chatId&text=$report" \ keep-result=no } }
Управление скриптами
Основные операции
[admin@MikroTik] ># Список всех скриптов /system/script/print # Детальная информация /system/script/print detail # Просмотр исходного кода конкретного скрипта /system/script/print where name=backup-email proplist=name,source # Запуск скрипта /system/script/run backup-email # Редактирование скрипта (открывает inline-редактор в terminal) /system/script/set [find name=backup-email] source="new code here" # Удаление скрипта /system/script/remove [find name=backup-email] # Отключение/включение /system/script/set [find name=backup-email] disabled=yes /system/script/set [find name=backup-email] disabled=no
Вызов скрипта из разных контекстов
[admin@MikroTik] ># Из terminal /system/script/run my-script # Из scheduler /system/scheduler add name=run-my-script interval=1h \ on-event="/system/script/run my-script" \ policy=read,write # Из netwatch (при падении хоста) /tool/netwatch add host=8.8.8.8 interval=30s \ down-script="/system/script/run failover-switch" \ up-script="/system/script/run failover-restore" # Из DHCP lease-script /ip/dhcp-server set [find name=dhcp1] \ lease-script="/system/script/run dhcp-event" # Из PPP on-up /ppp/profile set [find name=default] \ on-up="/system/script/run vpn-connected" \ on-down="/system/script/run vpn-disconnected"
Environment — глобальные переменные
Постоянное хранилище переменных
/system/script/environment — хранилище глобальных переменных, которые переживают перезагрузку роутера (в отличие от :global, которые теряются).
[admin@MikroTik] ># Создание переменной /system/script/environment add name=telegramBot value="123456:token" /system/script/environment add name=emailAdmin value="admin@example.com" # Просмотр /system/script/environment print # Чтение в скрипте :local token [/system/script/environment get [find name=telegramBot] value] # Обновление /system/script/environment set [find name=telegramBot] value="new-token" # Удаление /system/script/environment remove [find name=telegramBot]
Обмен данными между скриптами
Два скрипта могут обмениваться данными через глобальные переменные:
[admin@MikroTik] ># Скрипт 1: записывает данные /system/script add name=data-producer policy=read,write source={ :global lastCheckTime [/system/clock/get time] :global lastCheckResult "OK" :log info "Data producer: stored result" } # Скрипт 2: читает данные /system/script add name=data-consumer policy=read,write source={ :global lastCheckTime :global lastCheckResult :if ([:typeof $lastCheckTime] != "nothing") do={ :log info "Last check at $lastCheckTime: $lastCheckResult" } else={ :log warning "No data from producer yet" } }
Важно: при использовании :global переменную нужно объявить через :global varName в читающем скрипте (без присвоения значения), чтобы получить доступ к значению, установленному другим скриптом.
Безопасность
Policy — права скрипта
Каждый скрипт имеет набор policy, определяющих его возможности:
[admin@MikroTik] ># Скрипт только для чтения — не может менять конфигурацию /system/script add name=read-only-stats \ policy=read,test \ source={ :put [/system/resource/get uptime] :put [/system/resource/get cpu-load] } # Скрипт для backup — нужен ftp для записи файлов /system/script add name=backup \ policy=read,write,ftp,sensitive \ source={ /export file=backup } # Скрипт для перезагрузки — нужен reboot /system/script add name=reboot-safe \ policy=read,reboot \ source={ /system/reboot }
Не хранить пароли в скриптах
[admin@MikroTik] ># ПЛОХО — пароль в исходном коде /system/script add name=bad-example source={ :local password "SuperSecret123" /tool/fetch url="https://user:$password@api.example.com" } # ХОРОШО — пароль в environment /system/script/environment add name=apiPassword value="SuperSecret123" /system/script add name=good-example \ policy=read,write,ftp,test,sensitive \ source={ :local password [/system/script/environment get [find name=apiPassword] value] /tool/fetch url="https://user:$password@api.example.com" }
При /export исходный код скриптов экспортируется полностью. Все пароли, записанные в source, будут видны в экспортированном файле.
Аудит скриптов
Регулярно проверяйте, какие скрипты есть на роутере и что они делают:
[admin@MikroTik] ># Все скрипты с policy /system/script/print proplist=name,policy,owner,last-started,run-count # Скрипты с опасными правами /system/script/print where policy~"reboot" /system/script/print where policy~"policy" # Скрипты, использующие fetch (потенциальная утечка данных) /system/script/print where source~"fetch" # Текущие запущенные скрипты /system/script/job/print
Отладка скриптов
Способы отладки
| Метод | Где работает | Описание |
|---|---|---|
:put | Только terminal | Вывод значения в консоль при ручном запуске |
:log info | Везде | Запись в системный лог (видно в /log) |
:log debug | Везде (при включённом debug) | Детальная отладочная информация |
/system/script/job/print | Везде | Просмотр текущих запущенных скриптов |
:error "msg" | Везде | Принудительная остановка с сообщением об ошибке |
Пошаговая отладка
[admin@MikroTik] ># Добавляйте :log после каждого блока для отслеживания прогресса /system/script add name=debug-example \ policy=read,write,test \ source={ :log info "DEBUG: script started" :local cpu [/system/resource/get cpu-load] :log info "DEBUG: cpu=$cpu" :if ($cpu > 80) do={ :log info "DEBUG: entering high-cpu branch" # ... действия } else={ :log info "DEBUG: entering normal branch" # ... действия } :log info "DEBUG: script finished" }
Просмотр ошибок
[admin@MikroTik] ># Ошибки скриптов /log/print where topics~"script" and message~"error" # Все сообщения скриптов /log/print where topics~"script" # Текущие запущенные скрипты (зависшие?) /system/script/job/print # Принудительное завершение зависшего скрипта /system/script/job/remove [find where script=my-script]
Ограничения RouterOS scripting
| Ограничение | Описание | Обходное решение |
|---|---|---|
| Нет regex | Полноценные регулярные выражения недоступны | Используйте :find, :pick, оператор ~ |
| Нет HTTP POST body | /tool/fetch не поддерживает произвольный POST body в старых версиях | В RouterOS 7.x используйте http-data= |
| Однопоточность | Скрипт выполняется в одном потоке | Разделите на несколько скриптов |
| Нет файлового ввода по строкам | Нельзя читать файл построчно | Загрузите весь файл в переменную через /file/get contents |
| Ограничение размера переменной | Строковые переменные ограничены ~64KB | Обрабатывайте данные частями |
| Нет модулей/импортов | Нельзя подключить библиотеку | Используйте :global функции |
| Нет try/catch с типом ошибки | :do on-error ловит все ошибки одинаково | Проверяйте условия заранее |
Оператор ~ (содержит)
Замена regex для простого поиска:
[admin@MikroTik] ># Проверка: строка содержит подстроку :local msg "login failure from 192.168.1.100" :if ($msg ~ "failure") do={ :put "Found failure keyword" } # Использование в find /log/find where message~"error" /interface/find where name~"ether"
Типичные ошибки
Ошибка 1: Кавычки — двойные vs одинарные
RouterOS различает двойные и одинарные кавычки. Двойные — строка с подстановкой переменных. Одинарные — нет подстановки (не поддерживаются в RouterOS, используйте escape).
[admin@MikroTik] ># Правильно — переменная подставляется :local name "World" :put "Hello $name" # Результат: Hello World # Правильно — экранирование доллара :put "Price: \$100" # Результат: Price: $100 # Ошибка — забыли экранировать спецсимволы :local cmd "/ip/firewall/filter add chain=forward action=accept" # Правильно: :local cmd "/ip/firewall/filter add chain=forward action=accept"
Ошибка 2: Scope переменных
[admin@MikroTik] ># Ошибка: переменная недоступна вне блока :if (true) do={ :local innerVar "inside" } :put $innerVar # ОШИБКА: переменная innerVar не определена # Исправление: объявить переменную до блока :local innerVar :if (true) do={ :set innerVar "inside" } :put $innerVar # Результат: inside
Ошибка 3: Global-переменные между скриптами
[admin@MikroTik] ># Скрипт A устанавливает переменную :global myData "test" # Скрипт B НЕ может прочитать без объявления :put $myData # ОШИБКА: переменная не определена # Правильно в скрипте B: :global myData :put $myData # Результат: test
Ошибка 4: Escape-символы
[admin@MikroTik] ># Спецсимволы, требующие escape в строках: # \" — кавычка # \\ — обратный слэш # \n — перевод строки # \t — табуляция # \$ — знак доллара (без подстановки) # \? — знак вопроса # \r — возврат каретки # Пример: URL с параметрами :local url "https://api.example.com/update\?key=value&ip=1.2.3.4" # Пример: многострочная строка :local msg "Line 1\nLine 2\nLine 3"
Ошибка 5: Пустой результат find
[admin@MikroTik] ># Ошибка: find ничего не нашёл, get падает :local ip [/ip/address/get [find interface="ether99"] address] # ОШИБКА: no such item # Исправление: проверка существования :local ifaceId [/ip/address/find interface="ether99"] :if ([:len $ifaceId] > 0) do={ :local ip [/ip/address/get $ifaceId address] :put $ip } else={ :put "Interface not found" }
Ошибка 6: Переполнение при конкатенации в цикле
[admin@MikroTik] ># Осторожно: конкатенация строк в большом цикле может привести к # переполнению памяти или зависанию скрипта # Плохо: 10000 итераций конкатенации :local result "" :for i from=1 to=10000 do={ :set result ($result . "line $i\n") } # Лучше: ограничивать количество итераций :local result "" :local maxLines 100 :for i from=1 to=$maxLines do={ :set result ($result . "line $i\n") }
Итоги
| Задача | Рекомендуемый подход |
|---|---|
| Простая команда по расписанию | Inline в scheduler on-event |
| Сложная логика (>5 строк) | Отдельный скрипт в /system/script |
| Хранение credentials | /system/script/environment |
| Обмен данными между скриптами | :global переменные |
| Failover при падении канала | Script + netwatch |
| Периодический мониторинг | Script + scheduler |
| Реакция на событие (login, reboot) | Script + log parsing или startup |
| Отладка | :log info + /log/print |
RouterOS scripting — мощный инструмент автоматизации, который позволяет реализовать большинство сетевых сценариев без внешних систем. Ключевые принципы: минимальные policy, credentials в environment, обработка ошибок через :do on-error, тестирование в terminal перед развёртыванием и регулярный аудит скриптов для безопасности.
[/system/script]
│
├── source — исходный код скрипта
├── policy — права выполнения
├── last-started — время последнего запуска
├── run-count — количество запусков
└── owner — создатель скрипта
│
▼
[/system/script/environment]
│
└── Глобальные переменные — доступны всем скриптам
│
▼
[/system/script/job]
│
└── Текущие выполняющиеся скрипты (running jobs)
# Локальная переменная — видна только в текущем скрипте
:local myVar "Hello World"
:local myNum 42
:local myBool true
:local myIP 192.168.1.1
:local myTime 00:30:00
# Глобальная переменная — доступна всем скриптам
:global sharedVar "shared value"
# Использование переменных
:put $myVar
:log info "Number is: $myNum"
# Переопределение
:set myVar "New value"
# Строка
:local str "hello"
# Число
:local num 100
# Boolean
:local flag true
# IP-адрес
:local ip 10.0.0.1
# IP-префикс
:local net 192.168.1.0/24
# Время
:local t 01:30:00
# Массив (array)
:local arr {"apple";"banana";"cherry"}
# Ничего (nil)
:local empty
# Простое условие
:local cpu [/system/resource/get cpu-load]
:if ($cpu > 80) do={
:log warning "High CPU: $cpu%"
}
# if-else
:local mem [/system/resource/get free-memory]
:if ($mem < 16000000) do={
:log error "Low memory: $mem bytes"
} else={
:log info "Memory OK: $mem bytes"
}
# Вложенные условия
:local status "warning"
:if ($status = "critical") do={
:log error "CRITICAL!"
} else={
:if ($status = "warning") do={
:log warning "Warning state"
} else={
:log info "Normal"
}
}
# foreach — перебор элементов
:foreach iface in=[/interface/find where type="ether"] do={
:local name [/interface/get $iface name]
:local status [/interface/get $iface running]
:log info "Interface $name running=$status"
}
# foreach — перебор массива
:local servers {"8.8.8.8";"1.1.1.1";"77.88.8.8"}
:foreach srv in=$servers do={
:put "Server: $srv"
}
# while — цикл с условием
:local count 0
:while ($count < 10) do={
:set count ($count + 1)
:put "Iteration: $count"
}
# for — цикл со счётчиком
:for i from=1 to=5 do={
:put "Step $i"
}
# Длина строки
:local str "Hello MikroTik"
:local len [:len $str]
:put "Length: $len"
# Подстрока (pick) — извлечение части строки
:local sub [:pick $str 0 5]
:put $sub
# Результат: Hello
# Поиск подстроки (find)
:local pos [:find $str "MikroTik"]
:put "Found at: $pos"
# Результат: Found at: 6
# Конкатенация строк
:local first "Hello"
:local second "World"
:local result ("$first $second!")
:put $result
# Преобразование типов
:local num 42
:local str [:tostr $num]
:local back [:tonum $str]
# Замена символов через pick + конкатенацию
:local original "192.168.1.1/24"
:local slashPos [:find $original "/"]
:local ipOnly [:pick $original 0 $slashPos]
:put "IP without mask: $ipOnly"
# Создание массива
:local fruits {"apple";"banana";"cherry"}
# Доступ по индексу
:put ($fruits->0)
# Результат: apple
# Длина массива
:put [:len $fruits]
# Результат: 3
# Перебор массива
:foreach f in=$fruits do={
:put "Fruit: $f"
}
# Ассоциативный массив (key-value)
:local config {
"server"="10.0.0.1";
"port"="8080";
"timeout"="30"
}
:put ($config->"server")
# Результат: 10.0.0.1
# Определение функции
:local sendLog do={
:local level $1
:local message $2
:if ($level = "error") do={
:log error $message
} else={
:log info $message
}
}
# Вызов функции
[$sendLog "error" "Something went wrong"]
[$sendLog "info" "All systems normal"]
# Функция с возвратом значения
:local getWanIP do={
:local iface $1
:local addr [/ip/address/get [find interface=$iface] address]
:return [:pick $addr 0 [:find $addr "/"]]
}
:local myIP [$getWanIP "ether1"]
:put "WAN IP: $myIP"
# do ... on-error — try/catch аналог
:do {
/tool/fetch url="https://example.com/data.txt" dst-path=data.txt
:log info "Fetch successful"
} on-error={
:log error "Fetch failed — host unreachable or timeout"
}
# Проверка существования перед обращением
:local ifaceId [/interface/find where name="ether1"]
:if ([:len $ifaceId] = 0) do={
:log error "Interface ether1 not found"
} else={
:local status [/interface/get $ifaceId running]
:log info "ether1 running: $status"
}
# Задержка выполнения
:delay 5s
:delay 500ms
:delay 1m
# Логирование с разными уровнями
:log info "Information message"
:log warning "Warning message"
:log error "Error message"
:log debug "Debug message"
# Вывод в terminal (только при ручном запуске)
:put "This appears in terminal only"
/system/script add name=backup-email \
policy=read,write,ftp,test,sensitive \
source={
# Настройки
:local emailTo "admin@example.com"
:local identity [/system/identity/get name]
:local date [/system/clock/get date]
:local time [/system/clock/get time]
:local fname ("backup-$identity-$date")
# Создание backup
/export file=$fname
:delay 3s
/system/backup/save name=$fname dont-encrypt=yes
:delay 3s
# Отправка email
:local body ("Backup from router: $identity\n" . \
"Date: $date $time\n" . \
"RouterOS: " . [/system/resource/get version])
/tool/e-mail/send to=$emailTo \
subject="[MikroTik] Backup - $identity - $date" \
body=$body \
file=("$fname.rsc,$fname.backup")
:delay 5s
# Очистка файлов
/file/remove [find name~"$fname"]
:log info "Backup sent to $emailTo"
}
/tool/e-mail set server=smtp.gmail.com port=587 \
start-tls=yes \
from="router@example.com" \
user="router@example.com" \
password="app-password-here"
/system/script add name=failover-switch \
policy=read,write,test \
source={
:local primaryGw "192.168.1.1"
:local backupGw "192.168.2.1"
:local checkHost "8.8.8.8"
:local primaryIface "ether1"
# Ping проверка
:local pingResult [/ping $checkHost count=3 interface=$primaryIface]
:if ($pingResult = 0) do={
# Основной канал недоступен
:log warning "Failover: primary link DOWN, switching to backup"
/ip/route/set [find where dst-address="0.0.0.0/0" and gateway=$primaryGw] \
disabled=yes
/ip/route/set [find where dst-address="0.0.0.0/0" and gateway=$backupGw] \
disabled=no
} else={
# Основной канал работает
:local primaryDisabled [/ip/route/get \
[find where dst-address="0.0.0.0/0" and gateway=$primaryGw] disabled]
:if ($primaryDisabled = true) do={
:log info "Failover: primary link UP, switching back"
/ip/route/set [find where dst-address="0.0.0.0/0" and gateway=$primaryGw] \
disabled=no
/ip/route/set [find where dst-address="0.0.0.0/0" and gateway=$backupGw] \
disabled=yes
}
}
}
/system/scheduler add name=failover-check interval=30s \
on-event="/system/script/run failover-switch" \
policy=read,write,test
/system/script add name=dns-blacklist-update \
policy=read,write,ftp,test \
source={
:local url "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
:local tmpFile "hosts.txt"
:local count 0
:log info "DNS Blacklist: downloading..."
:do {
/tool/fetch url=$url dst-path=$tmpFile mode=https
:delay 5s
} on-error={
:log error "DNS Blacklist: download failed"
:error "Download failed"
}
# Удаление старых записей с комментарием blacklist
/ip/dns/static/remove [find where comment="blacklist"]
# Парсинг файла и добавление записей
:local content [/file/get [find name=$tmpFile] contents]
:local lineStart 0
:local contentLen [:len $content]
:while ($lineStart < $contentLen && $count < 1000) do={
:local lineEnd [:find $content "\n" $lineStart]
:if ([:typeof $lineEnd] = "nil") do={
:set lineEnd $contentLen
}
:local line [:pick $content $lineStart $lineEnd]
:set lineStart ($lineEnd + 1)
# Пропуск комментариев и localhost
:if ([:pick $line 0 1] != "#" && [:len $line] > 10) do={
:local spacePos [:find $line " " 4]
:if ([:typeof $spacePos] != "nil") do={
:local domain [:pick $line ($spacePos + 1) [:len $line]]
:set domain [:pick $domain 0 ([:len $domain])]
:if ($domain != "localhost" && [:len $domain] > 3) do={
:do {
/ip/dns/static/add name=$domain address=0.0.0.0 \
comment="blacklist" ttl=1d
:set count ($count + 1)
} on-error={}
}
}
}
}
# Очистка временного файла
/file/remove [find name=$tmpFile]
:log info "DNS Blacklist: added $count domains"
}
/system/script add name=telegram-login-alert \
policy=read,write,ftp,test,sensitive \
source={
:local botToken [/system/script/environment get [find name=telegramBot] value]
:local chatId [/system/script/environment get [find name=telegramChat] value]
:local identity [/system/identity/get name]
# Получаем последние записи лога о входе
:local logEntries [/log/find where message~"logged in" and topics~"system"]
:local lastEntry ($logEntries->([:len $logEntries] - 1))
:local logMsg [/log/get $lastEntry message]
:local logTime [/log/get $lastEntry time]
:local msg ("Security Alert!\n" . \
"Router: $identity\n" . \
"Event: $logMsg\n" . \
"Time: $logTime")
# URL encode пробелы
:local encodedMsg ""
:for i from=0 to=([:len $msg] - 1) do={
:local char [:pick $msg $i ($i + 1)]
:if ($char = " ") do={
:set encodedMsg ($encodedMsg . "%20")
} else={
:if ($char = "\n") do={
:set encodedMsg ($encodedMsg . "%0A")
} else={
:set encodedMsg ($encodedMsg . $char)
}
}
}
:local url ("https://api.telegram.org/bot$botToken/sendMessage" . \
"\?chat_id=$chatId&text=$encodedMsg")
:do {
/tool/fetch url=$url keep-result=no
:log info "Telegram alert sent: login notification"
} on-error={
:log error "Telegram alert failed"
}
}
/system/script/environment add name=telegramBot value="123456789:AAF-your-bot-token"
/system/script/environment add name=telegramChat value="-1001234567890"
/system/script add name=auto-update-routeros \
policy=read,write,ftp,reboot,policy \
source={
:local channel [/system/package/update/get channel]
:local identity [/system/identity/get name]
:log info "Auto-update: checking for updates (channel: $channel)"
/system/package/update/check-for-updates
:delay 15s
:local installed [/system/package/update/get installed-version]
:local latest [/system/package/update/get latest-version]
:if ($installed = $latest) do={
:log info "Auto-update: already on latest version $installed"
:return []
}
:log warning "Auto-update: upgrade available $installed -> $latest"
# Создаём backup перед обновлением
/export file=("pre-update-$installed")
/system/backup/save name=("pre-update-$installed") dont-encrypt=yes
:delay 5s
# Скачиваем обновление
:log warning "Auto-update: downloading $latest..."
/system/package/update/install
# RouterOS перезагрузится автоматически после скачивания
}
/system/script add name=watchdog-reboot \
policy=read,write,test,reboot \
source={
:local checkHost "8.8.8.8"
:local failThreshold 5
:global watchdogFails
# Инициализация счётчика при первом запуске
:if ([:typeof $watchdogFails] = "nothing") do={
:set watchdogFails 0
}
:local pingResult [/ping $checkHost count=3 interval=1]
:if ($pingResult = 0) do={
:set watchdogFails ($watchdogFails + 1)
:log warning "Watchdog: ping failed ($watchdogFails/$failThreshold)"
:if ($watchdogFails >= $failThreshold) do={
:log error "Watchdog: $failThreshold failures reached, REBOOTING"
:set watchdogFails 0
/system/reboot
}
} else={
:if ($watchdogFails > 0) do={
:log info "Watchdog: ping restored after $watchdogFails failures"
:set watchdogFails 0
}
}
}
/system/scheduler add name=watchdog interval=1m \
on-event="/system/script/run watchdog-reboot" \
policy=read,write,test,reboot
/system/script add name=ddns-updater \
policy=read,write,ftp,test \
source={
:local ddnsService "freedns"
:local ddnsToken "your-update-token-here"
:local wanIface "ether1"
:global ddnsLastIP
# Получаем текущий WAN IP
:local currentIP
:do {
:set currentIP [/ip/address/get [find interface=$wanIface] address]
:set currentIP [:pick $currentIP 0 [:find $currentIP "/"]]
} on-error={
:log error "DDNS: cannot get IP from $wanIface"
:return []
}
# Проверяем, изменился ли IP
:if ($currentIP = $ddnsLastIP) do={
:return []
}
:log info "DDNS: IP changed $ddnsLastIP -> $currentIP, updating..."
# Обновление на DDNS-сервисе
:local updateUrl ("https://freedns.afraid.org/dynamic/update.php\?" . \
"$ddnsToken&address=$currentIP")
:do {
/tool/fetch url=$updateUrl keep-result=no
:set ddnsLastIP $currentIP
:log info "DDNS: updated successfully to $currentIP"
} on-error={
:log error "DDNS: update failed"
}
}
/system/script add name=traffic-stats \
policy=read,write,test \
source={
:local interfaces [/interface/find where type="ether"]
:local report ""
:foreach iface in=$interfaces do={
:local name [/interface/get $iface name]
:local rxBytes [/interface/get $iface rx-byte]
:local txBytes [/interface/get $iface tx-byte]
:local running [/interface/get $iface running]
# Конвертация в MB
:local rxMB ($rxBytes / 1048576)
:local txMB ($txBytes / 1048576)
:set report ($report . "$name: RX=$rxMB MB TX=$txMB MB " . \
"running=$running\n")
}
:log info ("Traffic report:\n$report")
# Опционально: сброс счётчиков
# /interface/reset-counters [find where type="ether"]
}
/system/script add name=auto-block-bruteforce \
policy=read,write \
source={
:local maxAttempts 5
:local blockTimeout "1d"
:local blockList "bruteforce-blocked"
# Поиск записей о неудачных попытках входа
:local logEntries [/log/find where message~"login failure" and topics~"system"]
# Подсчёт попыток по IP
:local ipCounts
:set ipCounts [:toarray ""]
:foreach entry in=$logEntries do={
:local msg [/log/get $entry message]
# Извлечение IP из сообщения
:local fromPos [:find $msg "from "]
:if ([:typeof $fromPos] != "nil") do={
:set fromPos ($fromPos + 5)
:local endPos [:find $msg " " $fromPos]
:if ([:typeof $endPos] = "nil") do={
:set endPos [:len $msg]
}
:local attackIP [:pick $msg $fromPos $endPos]
# Проверяем, не заблокирован ли уже
:local alreadyBlocked [/ip/firewall/address-list/find \
where list=$blockList and address=$attackIP]
:if ([:len $alreadyBlocked] = 0) do={
# Увеличиваем счётчик
:if ([:typeof ($ipCounts->$attackIP)] = "nothing") do={
:set ($ipCounts->$attackIP) 1
} else={
:set ($ipCounts->$attackIP) (($ipCounts->$attackIP) + 1)
}
# Блокировка при превышении порога
:if (($ipCounts->$attackIP) >= $maxAttempts) do={
/ip/firewall/address-list/add list=$blockList \
address=$attackIP timeout=$blockTimeout \
comment="Auto-blocked: brute force"
:log warning "Auto-blocked IP: $attackIP ($maxAttempts+ failures)"
}
}
}
}
}
/ip/firewall/filter add chain=input \
src-address-list=bruteforce-blocked \
action=drop \
comment="Drop brute-force blocked IPs" \
place-before=0
/system/script add name=wifi-clients-report \
policy=read,write,ftp,test,sensitive \
source={
:local botToken [/system/script/environment get [find name=telegramBot] value]
:local chatId [/system/script/environment get [find name=telegramChat] value]
:local identity [/system/identity/get name]
:local clients [/interface/wifi/registration-table/find]
:local count [:len $clients]
:local report ("Wi-Fi Clients Report: $identity\n" . \
"Total clients: $count\n\n")
:foreach c in=$clients do={
:local mac [/interface/wifi/registration-table/get $c mac-address]
:local iface [/interface/wifi/registration-table/get $c interface]
:local signal [/interface/wifi/registration-table/get $c signal]
:local uptime [/interface/wifi/registration-table/get $c uptime]
:local txRate [/interface/wifi/registration-table/get $c tx-rate]
:local rxRate [/interface/wifi/registration-table/get $c rx-rate]
# Поиск hostname из DHCP lease
:local hostname "unknown"
:do {
:set hostname [/ip/dhcp-server/lease/get \
[find where mac-address=$mac] host-name]
} on-error={}
:set report ($report . "MAC: $mac\n" . \
" Host: $hostname\n" . \
" Interface: $iface\n" . \
" Signal: $signal dBm\n" . \
" TX/RX: $txRate / $rxRate\n" . \
" Uptime: $uptime\n\n")
}
:log info $report
# Отправка в Telegram (если настроен)
:if ([:typeof $botToken] != "nothing" && $botToken != "") do={
:local url ("https://api.telegram.org/bot$botToken/sendMessage")
/tool/fetch url=$url http-method=post \
http-data="chat_id=$chatId&text=$report" \
keep-result=no
}
}
# Список всех скриптов
/system/script/print
# Детальная информация
/system/script/print detail
# Просмотр исходного кода конкретного скрипта
/system/script/print where name=backup-email proplist=name,source
# Запуск скрипта
/system/script/run backup-email
# Редактирование скрипта (открывает inline-редактор в terminal)
/system/script/set [find name=backup-email] source="new code here"
# Удаление скрипта
/system/script/remove [find name=backup-email]
# Отключение/включение
/system/script/set [find name=backup-email] disabled=yes
/system/script/set [find name=backup-email] disabled=no
# Из terminal
/system/script/run my-script
# Из scheduler
/system/scheduler add name=run-my-script interval=1h \
on-event="/system/script/run my-script" \
policy=read,write
# Из netwatch (при падении хоста)
/tool/netwatch add host=8.8.8.8 interval=30s \
down-script="/system/script/run failover-switch" \
up-script="/system/script/run failover-restore"
# Из DHCP lease-script
/ip/dhcp-server set [find name=dhcp1] \
lease-script="/system/script/run dhcp-event"
# Из PPP on-up
/ppp/profile set [find name=default] \
on-up="/system/script/run vpn-connected" \
on-down="/system/script/run vpn-disconnected"
# Создание переменной
/system/script/environment add name=telegramBot value="123456:token"
/system/script/environment add name=emailAdmin value="admin@example.com"
# Просмотр
/system/script/environment print
# Чтение в скрипте
:local token [/system/script/environment get [find name=telegramBot] value]
# Обновление
/system/script/environment set [find name=telegramBot] value="new-token"
# Удаление
/system/script/environment remove [find name=telegramBot]
# Скрипт 1: записывает данные
/system/script add name=data-producer policy=read,write source={
:global lastCheckTime [/system/clock/get time]
:global lastCheckResult "OK"
:log info "Data producer: stored result"
}
# Скрипт 2: читает данные
/system/script add name=data-consumer policy=read,write source={
:global lastCheckTime
:global lastCheckResult
:if ([:typeof $lastCheckTime] != "nothing") do={
:log info "Last check at $lastCheckTime: $lastCheckResult"
} else={
:log warning "No data from producer yet"
}
}
# Скрипт только для чтения — не может менять конфигурацию
/system/script add name=read-only-stats \
policy=read,test \
source={
:put [/system/resource/get uptime]
:put [/system/resource/get cpu-load]
}
# Скрипт для backup — нужен ftp для записи файлов
/system/script add name=backup \
policy=read,write,ftp,sensitive \
source={ /export file=backup }
# Скрипт для перезагрузки — нужен reboot
/system/script add name=reboot-safe \
policy=read,reboot \
source={ /system/reboot }
# ПЛОХО — пароль в исходном коде
/system/script add name=bad-example source={
:local password "SuperSecret123"
/tool/fetch url="https://user:$password@api.example.com"
}
# ХОРОШО — пароль в environment
/system/script/environment add name=apiPassword value="SuperSecret123"
/system/script add name=good-example \
policy=read,write,ftp,test,sensitive \
source={
:local password [/system/script/environment get [find name=apiPassword] value]
/tool/fetch url="https://user:$password@api.example.com"
}
# Все скрипты с policy
/system/script/print proplist=name,policy,owner,last-started,run-count
# Скрипты с опасными правами
/system/script/print where policy~"reboot"
/system/script/print where policy~"policy"
# Скрипты, использующие fetch (потенциальная утечка данных)
/system/script/print where source~"fetch"
# Текущие запущенные скрипты
/system/script/job/print
# Добавляйте :log после каждого блока для отслеживания прогресса
/system/script add name=debug-example \
policy=read,write,test \
source={
:log info "DEBUG: script started"
:local cpu [/system/resource/get cpu-load]
:log info "DEBUG: cpu=$cpu"
:if ($cpu > 80) do={
:log info "DEBUG: entering high-cpu branch"
# ... действия
} else={
:log info "DEBUG: entering normal branch"
# ... действия
}
:log info "DEBUG: script finished"
}
# Ошибки скриптов
/log/print where topics~"script" and message~"error"
# Все сообщения скриптов
/log/print where topics~"script"
# Текущие запущенные скрипты (зависшие?)
/system/script/job/print
# Принудительное завершение зависшего скрипта
/system/script/job/remove [find where script=my-script]
# Проверка: строка содержит подстроку
:local msg "login failure from 192.168.1.100"
:if ($msg ~ "failure") do={
:put "Found failure keyword"
}
# Использование в find
/log/find where message~"error"
/interface/find where name~"ether"
# Правильно — переменная подставляется
:local name "World"
:put "Hello $name"
# Результат: Hello World
# Правильно — экранирование доллара
:put "Price: \$100"
# Результат: Price: $100
# Ошибка — забыли экранировать спецсимволы
:local cmd "/ip/firewall/filter add chain=forward action=accept"
# Правильно:
:local cmd "/ip/firewall/filter add chain=forward action=accept"
# Ошибка: переменная недоступна вне блока
:if (true) do={
:local innerVar "inside"
}
:put $innerVar
# ОШИБКА: переменная innerVar не определена
# Исправление: объявить переменную до блока
:local innerVar
:if (true) do={
:set innerVar "inside"
}
:put $innerVar
# Результат: inside
# Скрипт A устанавливает переменную
:global myData "test"
# Скрипт B НЕ может прочитать без объявления
:put $myData
# ОШИБКА: переменная не определена
# Правильно в скрипте B:
:global myData
:put $myData
# Результат: test
# Спецсимволы, требующие escape в строках:
# \" — кавычка
# \\ — обратный слэш
# \n — перевод строки
# \t — табуляция
# \$ — знак доллара (без подстановки)
# \? — знак вопроса
# \r — возврат каретки
# Пример: URL с параметрами
:local url "https://api.example.com/update\?key=value&ip=1.2.3.4"
# Пример: многострочная строка
:local msg "Line 1\nLine 2\nLine 3"
# Ошибка: find ничего не нашёл, get падает
:local ip [/ip/address/get [find interface="ether99"] address]
# ОШИБКА: no such item
# Исправление: проверка существования
:local ifaceId [/ip/address/find interface="ether99"]
:if ([:len $ifaceId] > 0) do={
:local ip [/ip/address/get $ifaceId address]
:put $ip
} else={
:put "Interface not found"
}
# Осторожно: конкатенация строк в большом цикле может привести к
# переполнению памяти или зависанию скрипта
# Плохо: 10000 итераций конкатенации
:local result ""
:for i from=1 to=10000 do={
:set result ($result . "line $i\n")
}
# Лучше: ограничивать количество итераций
:local result ""
:local maxLines 100
:for i from=1 to=$maxLines do={
:set result ($result . "line $i\n")
}