Commit Graph
2034 Commits
Author SHA1 Message Date
Dushnilin 0b7494a8e9 fix: Makefile postupgrade BusyBox awk alternation
Same BusyBox awk bug as in service/package.uc: | alternation and \/
inside awk regex literals are not supported. Split into grep -E for
alternation and grep -F for the path with slashes.
2026-08-09 12:36:57 +03:00
Dushnilin 676ee2041b fix: prerm stop-before-rt_tables, BusyBox awk, js-yaml vuln
- service/package.uc: call INIT_PATH stop before process-killing in
  prerm_cleanup so the routing table entry is still visible when the
  service shuts down (fixes test: 'prerm must stop Tachyon before
  removing its routing table name')
- service/package.uc: replace awk regex with | alternation and \/
  escaping with grep -E / grep -F; BusyBox awk does not support |
  in regex literals and silently mishandles \/ — fixes awk syntax
  errors in CI (both prerm_cleanup and postinst_restore)
- fe-app-tachyon: pin js-yaml to ^5.2.1 via overrides to fix the
  quadratic O(n^2) !!omap resolution DoS (same root cause as
  CVE-2026-59870 / GHSA-724g-mxrg-4qvm, never backported to 3.x/4.x)
- fe-app-tachyon: npm audit fix for brace-expansion and nanoid vulns
2026-08-09 12:34:58 +03:00
Dushnilin 95cfe7914f fix(postinst): use BIN_PATH start in postinst_restore + Makefile postupgrade calls tachyon start directly (bypasses rc.common flock) 2026-08-09 12:03:31 +03:00
Dushnilin 0f43680b66 fix(pkg): remove init.d/tachyon status from remember_upgrade_state (root cause of hang); add postupgrade Makefile section; kill hotplug spawners in prerm 2026-08-09 11:51:12 +03:00
Dushnilin e47704ff14 fix(prerm/postinst): use BIN_PATH stop, clear flock in postinst, always return true; fix(install): verify package VERSION not just existence on APK error 2026-08-09 11:44:33 +03:00
Dushnilin 3a0f31bfb5 fix(prerm): kill init.d/tachyon background processes (root cause of flock storms) before stop 2026-08-09 10:07:46 +03:00
Dushnilin c0d0850b1b fix(prerm): kill flock twice + use ubus to stop service, avoiding init.d rc.common flock creation 2026-08-09 10:06:57 +03:00
Dushnilin 5b118c2abb fix(prerm): always return true from prerm_cleanup so APK never rolls back upgrade on cleanup failure 2026-08-09 10:04:27 +03:00
Dushnilin 929c936154 fix(prerm): kill flock 1000 waiters before init.d stop to prevent procd_tachyon.lock deadlock on upgrade 2026-08-09 09:55:30 +03:00
Dushnilin 9b3066c846 fix(install): retry APK commands on database lock (exit 227) in install.sh 2026-08-09 09:43:37 +03:00
Dushnilin 2a43ea2b6b fix(prerm): use timeout 15 for init.d/tachyon stop in prerm_cleanup to prevent deadlock on procd_tachyon.lock 2026-08-09 09:36:11 +03:00
Dushnilin f351164ebc fix(updates): retry apk add up to 10 times on database lock error (exit 227) before failing 2026-08-09 09:22:28 +03:00
Dushnilin 0e16860675 fix(updates): install all tachyon packages in a single apk call to avoid APK database lock (exit 227) 2026-08-09 09:10:05 +03:00
Dushnilin 2896daea51 fix(ci): update release step configuration in build.yml 2026-08-09 09:02:03 +03:00
Dushnilin d91eba3fa8 fix(ci): remove explicit target_commitish on tag push release step in build.yml 2026-08-09 09:00:12 +03:00
Dushnilin 3eeee56f2e fix(ci): separate release action steps for push vs workflow_dispatch in build.yml 2026-08-09 08:59:18 +03:00
Dushnilin 54f7b5b351 fix(ci): fix release step execution condition for tag push events in build.yml 2026-08-09 08:58:36 +03:00
Dushnilin 071f72fe43 fix(ci): replace non-existent actions/checkout@v5.0.0 with v4 in build.yml 2026-08-09 08:57:53 +03:00
Dushnilin 3d84d96e49 feat(ui): display short commit SHA alongside release version across LuCI UI 2026-08-09 08:50:38 +03:00
Dushnilin c850633b01 feat(updates): compare commit SHA for same-release build updates 2026-08-09 08:47:11 +03:00
Dushnilin 6f98bae53d fix(updates): clear update cache in postinst and update version in post-install check 2026-08-09 04:57:37 +03:00
Dushnilin 48e2492c0c fix(updates): clean stale lock directories with rm -rf to unblock update checks 2026-08-09 04:38:08 +03:00
Dushnilin 106ce9352a fix(memory): ограничить чтения неограниченного вывода в память
На роутере со 128 МБ несколько мест читали в память вывод, размер которого
ничем не ограничен. Распаковка gzip подписки шла через пайп с чтением целиком:
скачивание curl ограничено 15 МБ, но коэффициент сжатия base64-текста высокий,
так что пик доходил до десятков мегабайт. Оба вызывающих места передают файл и
ждут файл, поэтому распакованный поток теперь уходит редиректом сразу в
выходной файл, а успех определяется по коду возврата и ненулевому размеру.

Таблица nftables содержит сеты с десятками тысяч элементов из списков, и
`nft list table` печатает их все. Проба контроллера делала это каждый тик,
хотя ищет только имена цепочек и правил; четыре проверки доктора и вывод
чужих меток из полного рулесета — то же самое. Везде добавлен grep на
источнике, отбирающий только те строки, которые код потом и разбирает.

Два просмотра логов читали кольцевой буфер syslog целиком, чтобы показать
несколько сотен строк; теперь количество строк ограничено на стороне logread.
2026-08-08 23:02:18 +03:00
Dushnilin e6e3853f05 fix(errors): перестать глотать сбои записи состояния в пустых catch
Пустых catch в кодовой базе около полусотни, и они не одинаковые. Большинство
выражает намерение: fs.unlink бросает на ENOENT, а вызывающий говорит «пусть
этого файла не будет», и отсутствующий файл это условие уже выполняет; разбор
json в цикле пропускает мусорную строку и берёт следующую. Такие места теперь
несут пояснение, почему пустое тело здесь и есть правильный ответ.

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

Watchdog писал через пустой catch файл лока перезапуска прокси, шкалу
GOMEMLIMIT после OOM, бэкап конфига и метку keepalive. Не лёгший лок означает,
что следующий тик перезапустит прокси заново, а счётчик rate-limit об этом не
узнает; не записанная метка keepalive заставляет init.d считать watchdog мёртвым
и перезапускать его. Все четыре теперь идут через write_state_file, который
логирует отказ. Метка пишется на каждом тике uloop, поэтому безусловная строка
залила бы syslog именно при переполненной файловой системе — там, где лог нужнее
всего; вместо этого каждый путь сообщает о первом отказе и молчит до следующего
успеха, что заодно делает видимым восстановление.

Контроллер разбирал /etc/sing-box/config.json ради портов http и tproxy и при
неудаче молча брал 4534 и 4530. Watchdog после этого опрашивает не тот порт,
вечно считает прокси нерабочим и лечит его по кругу — при том, что в логе нет
ничего, что связало бы это с конфигом. Разбор вынесен в общую функцию, которая
сообщает о непарсящемся конфиге один раз на процесс.

Telegram молча терял карту callback-токенов: токен уже вшит в клавиатуру к
моменту записи, поэтому провал отдаёт пользователю кнопку, которая на нажатие
ответит «кнопка устарела». Обновление списков молча теряло маркер отложенного
применения nft — списки обновлены на диске и до nftables не доехали никогда,
а строкой выше в лог написано, что маркер сохраняется. urltest молча выкидывал
битое регулярное выражение из конфига пользователя, отчего группа тихо
подхватывала меньше outbound-ов, чем сказано в конфиге.

tests/heal_escalation.sh проверял порядок строк по литералу fs.writefile для
лока и обновлён под новый вызов.
2026-08-08 22:32:09 +03:00
Dushnilin feec8c6d35 fix(spawn): закрывать дескрипторы фоновых процессов вместо фиктивного 1000<&-
ucode держит по одному открытому дескриптору на каждый require()-модуль всё
время жизни интерпретатора и не даёт ни закрыть их, ни пометить close-on-exec,
поэтому каждый фоновый процесс наследует весь набор. На роутере watchdog несёт
около 90 таких дескрипторов и передаёт их детям: logread, sh и nfqws2 найдены
держащими дескрипторы на собственных .uc-файлах Tachyon, часть уже удалена
обновлением пакета — старый инод прибит к диску процессом, которому он не нужен.

Против этого код писал литерал 1000<&- в 22 местах 13 модулей. Он не закрывал
ничего: дескриптор 1000 никогда не открывается, а все настоящие лежат в 6..89.
Хуже того, этого литерала требовал tests/service_start_trap.sh — именно поэтому
он и разошёлся по кодовой базе. Проверка пинила видимость защиты и продолжала бы
проходить вечно при живой утечке.

Замена оказалась не такой прямой, как выглядит. Счёт от 3 до потолка не годится:
в dash закрытие дескриптора 10 и выше фатально даже для номера, который никогда
не открывался — dash перемещает туда собственную служебную бухгалтерию, и
перенаправление убивает шелл целиком, а 2>/dev/null прячет сообщение, но не
смерть. Перечисление /proc/self/fd трогает только реально открытое, но сами
высокие дескрипторы никуда не деваются: набор модулей любого размера уводит их
за 10. Поэтому пролог сначала спрашивает шелл в подоболочке, которая поглощает
смерть, и опускает собственный потолок, если ответ отрицательный. busybox ash,
на котором работает OpenWrt, отвечает утвердительно и закрывает весь набор.

Три хелпера в core/common.uc задают форму фонового запуска — раньше каждое место
собирало перенаправления вручную, и они разошлись. Форма с пидом ставит exec
внутрь подоболочки, чтобы $! называл сам демон, а не обёртку, которая тут же
завершается; ведущие присваивания VAR=x при этом выносятся перед exec, иначе
шелл ищет программу с именем VAR=x. Для конвейеров, которые exec заменить не
может, есть отдельный хелпер. initd.uc и state.uc лежат на пути раннего старта и
несут собственные копии, как уже несут shell_quote и command_from_args.

Попутно вскрылись два настоящих дефекта. В лестнице эскалации watchdog строка
читалась как restart & rm -f lock &, то есть лок снимался параллельно с
рестартом, который он охраняет, и вторая эскалация могла начаться посреди
первой. В пакетном прогреве Zero-RTT знак & стоял внутри строки, поэтому
перенаправления после него цеплялись к пустой команде в родительском шелле,
а не к фоновому циклу.

Ещё четыре места запускали фон вообще мимо механизма; одно из них аудит
пропустил, его нашёл новый тест.

tests/background_spawn_fds.sh считает дескрипторы у ребёнка при обоих способах
запуска: структурная проверка такое показать не может, строку надо выполнить.
На роутере старый способ даёт ребёнку 7 дескрипторов, новый — 3.
2026-08-08 21:51:51 +03:00
Dushnilin b9317a553b fix(updates): validate ruleset file size and prevent 0-byte corruptions 2026-08-08 20:21:16 +03:00
Dushnilin d6860d8a15 fix(fe): исправить типизацию fetchHostnames и обновить бандл LuCI 2026-08-08 13:00:17 +03:00
Dushnilin ae64be7625 fix(watchdog): прекратить перезагрузку dnsmasq ради уже выставленного значения
heal_dns_continuous выставляет noresolv в единственное фиксированное значение, поэтому начиная со второго подряд сбоя DNS он переписывал настройку, которая уже содержала это значение, — и всё равно выполнял commit и reload dnsmasq. Перезагрузка сбрасывает кеш резолвера и заново перечитывает все конфигурационные файлы, чтобы вернуться туда же, откуда начали. Три сбоя DNS подряд — это ровно тот момент, когда вероятен четвёртый, так что бесполезная перезагрузка приходила именно тогда, когда резолвер меньше всего мог себе её позволить.

Теперь восстановитель сначала читает dhcp.@dnsmasq[0].noresolv через uci_core. Расхождение устраняется как прежде; совпадение не трогается, а исход честно сообщается как `skipped`, а не как несостоявшийся ремонт.

Счётчик подряд идущих сбоев сбрасывается и на этом пути. Без сброса факт DNS_DOWN приходит на каждом быстром тике, и восстановитель входит в работу бесконечно — ничего не делая, но каждый раз рапортуя. Сброс фиксирует, что факт обработан: dnsmasq уже содержит нужную конфигурацию, значит у сбоя другая причина и она принадлежит другому подписчику.

Тест `tests/heal_dns_idempotent.sh` прогоняет извлечённую функцию на отсутствующем значении, неверном значении, верном значении и событии ниже порога, подсчитывая команды commit и reload в каждом случае.
2026-08-08 12:55:54 +03:00
Dushnilin 5a10cc158b feat(watchdog): приостановка вторичных ремонтов во время устранения первопричины
Падение WAN-интерфейса влечет за собой сбой прокси, DNS и проверок доступности. У каждого из них был свой подписчик, и каждый независимо решал перезапустить службу — три ремонта на один сбой, ни один из которых не мог сработать до восстановления WAN. Ограничение частоты в safe_proxy_restart() сдерживало этот каскад только постфактум, но в коде не было явной связи, что сбои прокси и DNS — последствия сбоя WAN.

Шина уже ранжирует восстановители: WAN выполняется первым с приоритетом 10, sing-box с 20, прокси и DNS — с 40 и 50. Чтобы не вводить вторую иерархию, эти ранги вынесены в именованные константы (PRIORITY_WAN, PRIORITY_SINGBOX, PRIORITY_PROXY, PRIORITY_DNS), используемые как при подписке, так и при проверке подавления.

Восстановитель, устраняющий первопричину, удерживает свой ранг на время ремонта. Вторичный восстановитель приостанавливает работу и выводит сообщение в лог. Три свойства гарантируют надежность: причина никогда не подавляет сама себя; следствие не может подавить причину; срок удержания ограничен, поэтому зависший ремонт не заблокирует вочдог навсегда.

Удержание снимается по событию восстановления, не дожидаясь таймаута. Для этого добавлено новое событие EV.WAN_UP — ранее probe_wan публиковал только сбои.

Удержание WAN захватывается до `ifdown wan`, так как сам ifdown вызовет ошибки прокси и DNS ещё до завершения работы ifup.

Тест `tests/heal_suppression.sh` проверяет ранги, точки захвата/освобождения, защиту восстановителей и логирование пропусков.
2026-08-08 12:47:23 +03:00
Dushnilin b4ca111d34 feat(watchdog): перезапуск только sing-box перед эскалацией до полного стека
При падении процесса sing-box или отсутствии ответа вочдог выполнял полный перезапуск стека, включая правила файрвола и прокси обхода. Это вызывало избыточный простой сети при локальных сбоях sing-box.

Теперь вочдог сначала пробует точечный перезапуск службы sing-box. Эскалация до полного перезапуска стека происходит только если sing-box не восстановился.
2026-08-08 12:26:43 +03:00
Dushnilin ad8087ba76 feat(watchdog): логирование фактических результатов ремонта вместо намерений
Ранее вочдог логировал попытки ремонта как успешные ещё до их выполнения. Если переключение сервиса или перезапуск завершались ошибкой, в логе всё равно отображался успех.

Теперь восстановители проверяют результат действий и логируют фактический исход (успех или ошибка) вместе с подробной контекстной информацией.
2026-08-08 11:57:56 +03:00
Dushnilin 2647061646 refactor(watchdog): разделение наблюдения и восстановления через шину событий
Ранее вочдог опрашивал состояние системы и устранял проблемы в едином смешанном цикле. Добавление новой проверки или действия по восстановлению требовало изменения монолитной функции, где мониторинг и ремонт были переплетены, что затрудняло аудит порядка выполнения и изолированное тестирование.

Вводится `usr/lib/service/event_controller.uc` — шина событий pub/sub для вочдога. Теперь зонды публикуют факты (`EV.PROXY_DOWN`, `EV.DNS_DOWN`, `EV.SINGBOX_STOPPED`, `EV.OOM_DETECTED`, `EV.WAN_DOWN` и т.д.), не зная, кто их обрабатывает. Восстановители (healers) подписываются на эти факты и независимо выполняют ремонт.

Ключевые изменения:
- `event_controller.uc` управляет словарём событий, регистрацией подписчиков, сортировкой по приоритетам и синхронной отправкой.
- Зонды (`probe_proxy`, `probe_dns`, `probe_singbox`, `probe_oom`, `probe_wan`) публикуют события вместо прямого вызова функций ремонта.
- Восстановители регистрируются как подписчики с явными приоритетами, чтобы первопричины устранялись раньше вторичных сбоев.
- `tests/event_bus.sh` добавляет интеграционные тесты для отправки событий, сортировки приоритетов и обработки подписчиков.
2026-08-08 11:01:09 +03:00
Dushnilin 810f6dbfc8 fix: убрать rpcd из shell uci-defaults (нарушал тест shell_inventory) 2026-08-07 20:51:38 +03:00
Dushnilin 55e0f9b303 fix: исправить список серверов в URLTest, ACL для rpcd, Telegram-бот
UI (section.js):
- showUrlTestSettingsModal и showPriorityLevelSettingsModal: предзагрузка
  кэша outbound-имён до открытия модала — CBINamedSection.render() не
  вызывает section.load(), из-за чего DynamicList рендерился без данных
- renderStackedJsonSettingsModal: вызов map.load() перед map.render()

LuCI ACL (luci-app-tachyon.json):
- Добавлены пути /tmp/run/tachyon/... наряду с /var/run/tachyon/...
  На OpenWrt /var — симлинк на /tmp; rpcd резолвит симлинки перед
  проверкой ACL, поэтому /var/run/... никогда не давал прав чтения
  и fs.read() возвращал PermissionError

Postinstall (50_luci-tachyon):
- Добавлен перезапуск rpcd после установки пакета, чтобы новые ACL
  вступали в силу без ручного logout/login в LuCI

Telegram-бот (telegram.uc):
- handle_toggle_mac(): блокировка/разблокировка устройств теперь
  через правила firewall (fw4), а не через UCI blocked_macs
- /cb команда: разрешение токенизированных коллбэков для oversized
  inline-клавиатур (лимит Telegram 64 байта на callback_data)
- /qh_start, /qh_end: интерактивная настройка часов тихого режима
- in_quiet_hours() и daily report: исправлен localtime(time())
  вместо clock() — clock() возвращает [сек, мксек], а не tm-структуру
- Уведомления об обновлениях: current_version вместо installed_version
- Описание команды /restart исправлено

Backend (api.uc, state.uc):
- http_probe(): выделена функция с проверкой HTTP-статуса
  (curl выходит 0 даже при 4xx/5xx — теперь проверяем код явно)
- run_speedtest(): учёт curl exit status
- Math.random() заменён на clock()[1] (Math недоступен без import)
- current_year(): убрано +1900 — localtime().year уже полный год
2026-08-07 20:45:06 +03:00
Dushnilin c8052ee682 fix(watchdog): eliminate aggressive DNS restarts breaking active TCP/RDP connections 2026-08-07 18:22:19 +03:00
Dushnilin 43853f5e3f fix(installer): make package removal warnings non-fatal during upgrade 2026-08-07 18:13:00 +03:00
Dushnilin 1ce9c4a3ae fix(build): replace JS-style method call with ucode global split() in apk postinst/post-upgrade scripts 2026-08-07 18:04:04 +03:00
Dushnilin 138a3831b9 fix(tests): restore exact dashboardFilterChoiceRefreshers registration signature for tests 2026-08-07 17:58:24 +03:00
Dushnilin f5fe6a7471 fix(updates): eliminate false same-release update warnings when current version matches latest release 2026-08-07 17:54:29 +03:00
Dushnilin 7c0959a969 fix(ui): dynamic refresh of outbound choices when focusing or loading section cache 2026-08-07 17:34:42 +03:00
Dushnilin ff92e79c5b fix(ui): resolve empty server list in subscription and urltest filter dropdowns 2026-08-07 17:30:50 +03:00
Dushnilin 37c56a3652 fix(updates): validate hex format for remote_sha in same-release update check 2026-08-07 17:07:14 +03:00
Dushnilin b7fa83dfd2 fix: resolve export_metrics crash and MTProto SIGHUP FATAL (issues #14, #15)
Issue #14: move average_latency() before export_metrics() in watchdog.uc
  - average_latency was declared AFTER export_metrics (line 1213 vs 1178)
  - in ucode timer/closure contexts the forward reference was not reliably
    resolved, causing 'left-hand side is not a function' every 2 min
  - fix: move average_latency to before export_metrics so it is always
    defined before it is referenced

Issue #15: add close_timeout to MTProxy inbound in singbox/servers.uc
  - MTProto clients maintain persistent connections that never close naturally
  - without close_timeout, sing-box waits indefinitely for them during SIGHUP
    hot-reload and exits with FATAL: 'sing-box did not close!'
  - fix: add close_timeout=10s (configurable via mtproto_close_timeout UCI)
    so sing-box forcefully closes stale MTProto connections after 10 s and
    completes the hot-reload cleanly
2026-08-07 16:20:56 +03:00
Dushnilin 59e0dc4194 fix(subscription): add bootstrap DNS 8.8.8.8,1.1.1.1 to curl for downloads without proxy 2026-08-07 14:15:28 +03:00
Dushnilin 37569768d8 fix(tracer): fix rpcd ACL permissions for ruleset files and add source URL display for external .lst lists 2026-08-07 13:35:01 +03:00
Dushnilin 1599a7239c fix(tracer): implement resilient 3-way ruleset reading for domain/IP lists in routing tracer 2026-08-07 13:21:49 +03:00
Dushnilin f2a22a6730 fix(tracer): fix fs.readfile call for domain/IP lists and implement sing-box SIGHUP graceful recovery 2026-08-07 13:14:06 +03:00
Dushnilin 4e419d5aea fix: OpenWrt 25.12 compatibility fixes, startup speedup, and Telegram polling fix
- wget: remove -t flag incompatible with BusyBox 25.12+, add curl-first fallback
  in download_to_file() with wget as fallback

- startup/reload speedup (5 improvements):
  - cut sing-box verification timeout from 45 iterations to 15
  - remove redundant second stability wait for sing-box process
  - batch requirement check + runtime validation into single ucode fork
  - replace dnsmasq full restart with procd reload (config-conditional bounce)
  - move telegram and watchdog runtime start to background

- telegram: fix "API failure" spam - empty result[] from getUpdates is now
  treated as a successful idle poll, not an error that triggers backoff

- watchdog SmartDetect: extract domain from proxy and VPN URLs for DNS
  health check, use real domains (google.com/example.com) instead of
  provider-specific endpoints that trigger false alerts

- tests: new invariants for wget -t prohibition, telegram empty-result
  handling, DNS apply reload-vs-restart, and combined validation mode ownership
2026-08-07 11:46:52 +03:00
Dushnilin 33387ace3d fix(watchdog): use real domains (google.com/example.com) for DNS health check to fix Telegram notification spam 2026-08-07 08:11:53 +03:00
Dushnilin 769598390d fix(ui): dynamic Sing-box repository globe link based on installed variant (shtorm-7/Leadaxe/SagerNet) 2026-08-06 22:07:38 +03:00