🇷🇺 Что нового (RU)
Коротко
v1.10.2 чинит два связанных бага в фейловере тоннеля (upstream.type = "tunnel", WireGuard/AmneziaWG egress): watchdog-таймер health-check мог остановиться навсегда и никогда не перезапуститься сам, а протухший (но формально «up») тоннель мог использоваться бесконечно без попытки самовосстановления.
Ядро MTProto-прокси и сетевой протокол не менялись. Обновление безопасно и рекомендуется всем, кто использует upstream.type = "tunnel".
Таймер mtproto-tunnel-pool.timer останавливался навсегда
Симптом: обнаружено вживую на проде (proxy.sleep3r.ru) — прокси переставал доставать до Telegram, хотя systemctl status mtproto-tunnel-pool.timer показывал active. LastTriggerUSec был заморожен на дате более чем десятидневной давности, NextElapseUSecRealtime — пустой: сам failover-watchdog фактически не работал уже давно.
Причина: таймер стоял на связке OnBootSec=30s + OnUnitInactiveSec=30s. OnUnitInactiveSec перевзводится от перехода связанного .service в inactive; после обычного systemctl restart самого таймера (без ребута хоста) OnBootSec больше не сработает — он привязан к уже прошедшему бут-циклу, — а OnUnitInactiveSec не имеет свежего перехода, от которого оттолкнуться. Таймер замолкает навсегда. Воспроизведено вживую: systemctl restart таймера не помогал, а разовый ручной systemctl start самого .service давал OnUnitInactiveSec нужный «якорь», и таймер снова затикал — это подтвердило причину и легло в основу фикса.
Фикс: таймер переведён на OnCalendar=*-*-* *:*:00/30 — расписание пересчитывается от wall-clock при каждом (ре)старте юнита и не зависит от истории состояния связанного сервиса, так что застрять описанным образом больше не может.
Протухший тоннель использовался вечно вместо самовосстановления
Симптом: health-check пула уже умел определять «тоннель up, но Telegram через него не достаётся» — но при единственном тоннеле в пуле фейловериться было не с кем, и скрипт просто продолжал маршрутизировать через тот же деградировавший тоннель, помечая статус degraded. Тот же класс проблемы, что VPN на телефоне, который выглядит подключённым, но не работает, пока его не выключить и включить руками.
Причина: проверка интерфейса поднимала его, только если он вообще отсутствовал; если интерфейс был на месте, а WG-сессия внутри протухла (например, после сетевого сбоя), её никто и никогда не передёргивал заново.
Фикс: добавлен счётчик подряд идущих «degraded»-проверок на интерфейс. После BOUNCE_AFTER_DEGRADED_CHECKS (2, то есть ~60с при 30-секундном такте) подряд неудачных проб тоннель один раз передёргивается (awg-quick/wg-quick down → up), чтобы форсировать свежий handshake, и перепроверяется — прежде чем соглашаться на деградированный fallback. Порог намеренно выше одного цикла, чтобы разовый всплеск не дёргал здоровый тоннель; счётчик сбрасывается при успехе или после попытки bounce, так что по-настоящему заблокированный маршрут не будет передёргиваться каждый цикл бесконечно.
Проверено
zig build test: 269/269 зелёные; кросс-сборкаx86_64-linux— чистая.- Задеплоено и подтверждено на живом проде (
proxy.sleep3r.ru): после обновления таймер сразу показал корректный пересчитанныйTrigger(не пустойNextElapseUSecRealtime, как раньше), несколько циклов подряд отработали чисто с новым скриптом, тоннель осталсяhealthy.
Changelog
- fix(ctl): repair dead pool-failover timer + auto-heal stale WG sessions (#384)
🇬🇧 Release notes (EN)
TL;DR
v1.10.2 fixes two related bugs in the tunnel failover path (upstream.type = "tunnel", WireGuard/AmneziaWG egress): the health-check watchdog timer could stop for good and never re-arm itself, and a stale-but-nominally-up tunnel could be used forever without ever attempting to self-heal.
The MTProto proxy core and wire protocol are unchanged. Upgrading is safe and recommended for anyone running upstream.type = "tunnel".
mtproto-tunnel-pool.timer stopped re-arming for good
Symptom: found live on prod (proxy.sleep3r.ru) — the proxy stopped reaching Telegram even though systemctl status mtproto-tunnel-pool.timer reported active. LastTriggerUSec was frozen more than ten days in the past, and NextElapseUSecRealtime was empty: the failover watchdog had effectively been dead the whole time.
Cause: the timer used OnBootSec=30s + OnUnitInactiveSec=30s. OnUnitInactiveSec re-arms off the paired .service's own inactive-transition; after a plain systemctl restart of just the timer (no host reboot), OnBootSec won't fire again — it's tied to a boot cycle that already happened — and OnUnitInactiveSec has no fresh transition to key off of. The timer goes silent for good. Reproduced live: restarting the timer alone never helped, while manually running the paired .service once gave OnUnitInactiveSec the anchor it needed and the timer started ticking again — confirming the cause and the fix.
Fix: the timer now uses OnCalendar=*-*-* *:*:00/30, which recomputes from wall-clock on every (re)start instead of chaining off the paired unit's state history, so it can't get stuck this way again.
A stale tunnel was used forever instead of self-healing
Symptom: the pool health-check could already detect "tunnel up, but Telegram unreachable through it" — but with a single tunnel in the pool there was nothing to fail over to, so the script just kept routing through the same degraded tunnel, marking it degraded. The same failure class as a phone VPN that looks connected but passes nothing until manually toggled off and on.
Cause: the interface check only brought the interface up if it was missing entirely; if the interface was present but its WG session had gone stale (e.g. after a network hiccup), nothing ever bounced it.
Fix: added a consecutive-degraded-checks counter per interface. After BOUNCE_AFTER_DEGRADED_CHECKS (2, ~60s at the 30s cadence) failed probes in a row, the tunnel is bounced once (awg-quick/wg-quick down → up) to force a fresh handshake and re-probed, before falling back to the degraded state. The threshold is kept above a single cycle so one transient probe blip doesn't churn a healthy tunnel; the counter resets on success or after an attempted bounce, so a genuinely-blocked route isn't bounced every cycle forever.
Verified
zig build test: 269/269 pass;x86_64-linuxcross-compile is clean.- Deployed and confirmed on live prod (
proxy.sleep3r.ru): after the update the timer immediately showed a correctly recomputedTrigger(no longer an emptyNextElapseUSecRealtime), several cycles ran clean with the new script, and the tunnel stayedhealthy.
Changelog
- fix(ctl): repair dead pool-failover timer + auto-heal stale WG sessions (#384)
Full Changelog: v1.10.1...v1.10.2