mikrotik-wiki.ru
Главная
Загрузка...

Скрипты на MikroTik — /system/script

RouterOS 7.xSystem21 мин230 мар. 2026 г.
TelegramVK

Скрипты на 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/scriptScheduler on-eventTerminal
ХранениеПостоянное (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 перед развёртыванием и регулярный аудит скриптов для безопасности.

[admin@MikroTik] >
[/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")
}
System / Скрипты на MikroTik — /system/script