Симптом
Возможны два демона tunnelvault одновременно + два openvpn на один конфиг → бесконечный churn реконнектов (инцидент уже наблюдался в этой сессии).
Причина
Синглтон-гард _refuse_if_daemon_running (tunnelvault.py) проверяет живой демон через IPC-сокет. Но сокет поднимается только ПОСЛЕ connect_all, а настоящий атомарный flock (tv/daemon.py:write_pid → _recover_stale_lock, kill-and-replace) берётся тоже после connect_all (в grandchild daemonize). Проверка и замок разнесены вокруг connect_all → окно дубля = вся длительность подъёма (openvpn init — десятки секунд).
Необойдённые дыры:
- Гонка/окно подъёма: второй запуск (или два одновременных
sudo ./tvpn), стартовавший пока первый ещё поднимает тоннели, видит «сокета нет» → гард пропускает → оба идут в connect_all → два openvpn на один конфиг. flock/kill-and-replace арбитрируют уже ПОСЛЕ дубля.
--foreground (launchd/systemd): исключён из гарда намеренно (политика kill-and-replace), но write_pid вызывается ПОСЛЕ connect_all → при рестарте поверх живого сначала дубль-connect, потом SIGTERM старому. Окно дубля = длительность connect_all.
- IPC-сокет перетирается без проверки живости:
tv/ipc_server.py:126-133 (_cleanup_stale_socket) безусловно unlinkает существующий сокет → второй демон замещает сокет первого, первый осиротел и невидим для --status/--disconnect.
Направление фикса
Взять flock (или отдельный стартовый замок) ДО connect_all на ВСЕХ путях подъёма, включая --foreground — тогда refuse/kill-and-replace решается до поднятия хоть одного тоннеля. В _cleanup_stale_socket перед unlink пробовать connect: успех = «жив, другой демон» → не биндить.
Требует аккуратной работы с lifecycle демона (flock через double-fork) + живой проверки.
Контекст: частичный фикс уже влит (44776bc синглтон-гард закрыл узкий интерактивный кейс; e28f0d1 починил регресс --reconnect). Это issue — про оставшуюся гонку/полноту.
Найдено адверсариальным ревью (singleton ось), 2026-07-02.
Симптом
Возможны два демона tunnelvault одновременно + два openvpn на один конфиг → бесконечный churn реконнектов (инцидент уже наблюдался в этой сессии).
Причина
Синглтон-гард
_refuse_if_daemon_running(tunnelvault.py) проверяет живой демон через IPC-сокет. Но сокет поднимается только ПОСЛЕconnect_all, а настоящий атомарныйflock(tv/daemon.py:write_pid→_recover_stale_lock, kill-and-replace) берётся тоже послеconnect_all(в grandchilddaemonize). Проверка и замок разнесены вокругconnect_all→ окно дубля = вся длительность подъёма (openvpn init — десятки секунд).Необойдённые дыры:
sudo ./tvpn), стартовавший пока первый ещё поднимает тоннели, видит «сокета нет» → гард пропускает → оба идут вconnect_all→ два openvpn на один конфиг. flock/kill-and-replace арбитрируют уже ПОСЛЕ дубля.--foreground(launchd/systemd): исключён из гарда намеренно (политика kill-and-replace), ноwrite_pidвызывается ПОСЛЕconnect_all→ при рестарте поверх живого сначала дубль-connect, потом SIGTERM старому. Окно дубля = длительностьconnect_all.tv/ipc_server.py:126-133(_cleanup_stale_socket) безусловноunlinkает существующий сокет → второй демон замещает сокет первого, первый осиротел и невидим для--status/--disconnect.Направление фикса
Взять
flock(или отдельный стартовый замок) ДОconnect_allна ВСЕХ путях подъёма, включая--foreground— тогда refuse/kill-and-replace решается до поднятия хоть одного тоннеля. В_cleanup_stale_socketпередunlinkпробовать connect: успех = «жив, другой демон» → не биндить.Требует аккуратной работы с lifecycle демона (flock через double-fork) + живой проверки.
Контекст: частичный фикс уже влит (
44776bcсинглтон-гард закрыл узкий интерактивный кейс;e28f0d1починил регресс--reconnect). Это issue — про оставшуюся гонку/полноту.Найдено адверсариальным ревью (singleton ось), 2026-07-02.