Безопасные shell-скрипты для OpenWrt

TL;DR: на слабом роутере shell-скрипт должен сначала собрать новый конфиг, потом провалидировать его, и только после этого аккуратно применить изменения и проверить сервис.

Главная идея

На обычном Linux неудачный shell-скрипт часто просто ломает один сервис.

На домашнем роутере под OpenWrt цена ошибки выше:

Поэтому хороший helper script для роутера должен работать не в стиле "поправил файл и рестартанул", а в стиле:

  1. собрать candidate-конфиг
  2. сравнить его с текущим
  3. провалидировать
  4. применить атомарно
  5. сделать health check
  6. откатиться, если что-то пошло не так

Ключевые моменты

Детали

Почему staged apply важнее, чем "правка по месту"

Самая частая ошибка в таких скриптах: взять боевой конфиг, переписать его на месте и сразу сделать restart.

Проблема в том, что при любой синтаксической ошибке ты получаешь сразу две проблемы:

Надёжнее делать так:

  1. собрать новый конфиг во временный файл
  2. если он совпадает с текущим, выйти без изменений
  3. прогнать проверку syntax/config test
  4. сохранить backup текущей версии
  5. атомарно заменить через mv
  6. сделать restart только после успешной валидации

Этот подход особенно хорошо работает для dnsmasq, nftables, sing-box и любых generated snippets.

Минимальный безопасный контракт для helper script

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

Если чего-то из этого нет, скрипт лучше не ставить в cron.

Validate before reload

Для типовых сервисов OpenWrt можно использовать такие проверки:

Важно: сначала validate, потом apply, потом restart.

Не наоборот.

Restart только при реальном diff

Один из лучших паттернов для роутера: не трогать сервис, если состояние по сути не изменилось.

Это даёт сразу несколько плюсов:

Практическое правило простое: если candidate == current, скрипт пишет в лог no changes и завершает работу.

Health check после restart

Проверка только через pidof недостаточна.

Процесс может существовать, но сервис уже быть функционально сломанным. После restart нужен короткий smoke test:

Если smoke test не прошёл:

  1. вернуть backup
  2. повторно поднять сервис
  3. если сервис всё равно не поднялся, остановиться и не делать дальнейших изменений

Никакого тихого fallback в /tmp

Это особенно важно для OpenWrt на железе с 256 МБ RAM.

Если план был такой:

то при пропавшем /mnt/usb скрипт не должен молча писать в /tmp. Иначе ты получаешь:

Правильное поведение в таких случаях: log critical, skip apply, leave direct connectivity alive.

Locking против параллельного запуска

На роутере очень легко случайно столкнуть:

Если один и тот же скрипт умеет менять конфиг и рестартить сервис, параллельный запуск опасен. Самая простая защита - lockdir:

LOCKDIR="/var/run/my-script.lock"
if ! mkdir "$LOCKDIR" 2>/dev/null; then
    logger -t my-script "another instance is already running; skipping"
    exit 0
fi
trap 'rmdir "$LOCKDIR" 2>/dev/null || true' EXIT INT TERM

Для OpenWrt этого обычно достаточно.

Что считать хорошим эталоном

Хороший образец - скрипт, который:

Именно такой паттерн стоит использовать как базу для всех остальных helper scripts.

Практический чеклист

Перед тем как поставить shell-скрипт в cron, стоит пройтись по такому списку:

Если на любой из этих вопросов ответ "нет", скрипт ещё сырой.

Связанные заметки

Дисклеймер / Disclaimer: material is published for informational and research purposes. Полный отказ от ответственности / Full disclaimer.