# Read-only аудит failover/failback ## Короткий вывод Штатное переключение между `primary` и `reserve` меняет маршрут **только для новых соединений**. Уже установленные TCP/UDP-соединения не переносятся и не закрываются самим Harbor. Поэтому: - здоровая загрузка, игра или поток продолжаются через старый канал; - если старый канал действительно умер, существующая сессия может оборваться независимо от переключения; - после переключения новые соединения идут через новый канал; - бесшовной миграции уже открытого TCP/UDP-сеанса на другой внешний адрес нет. ## 1. Точный механизм ### Конфигурация Gateway собирает один dual-channel `sing-box` config: - `channel-primary`; - `channel-reserve`; - selector `channel-selector`; - стабильные `tproxy-in` и `mixed-in`; - отдельные diagnostic inbounds для проверки каждого канала. Пользовательский трафик направляется в selector, а selector настроен с: ```text interrupt_exist_connections: false ``` См. `src/server/singbox.ts:204-247`. Роль меняется через localhost Clash API: - `PUT /proxies/channel-selector`; - затем Harbor читает selector обратно и подтверждает выбранную роль. См. `src/server/services/singboxSelectorService.ts:29-81`. Failover API доступен только в Gateway: - `PUT /api/failover`; - `POST /api/failover/pause`; - `POST /api/failover/switch`; - `POST /api/failover/check`. См. `src/server/http/routes/failoverRoute.ts:18-39`. Переключение selector не вызывает `sing-box` restart/apply/stop. ### Автоматический failover `FailoverService`: 1. Проверяет оба канала через отдельные diagnostic inbound и выбранные HTTPS-сервисы. 2. Считает канал healthy только если все проверки успешны; неизвестный результат даёт `unknown`. 3. При сбое primary ждёт `failureWindowMs`. 4. Переключается на reserve только если reserve healthy. 5. При включённом traffic guard ждёт свежий quiet-window. 6. Перед самой сменой повторно проверяет здоровье и активность. 7. Выполняет selector PUT, read-back и только после этого обновляет canonical applied state. См. `src/server/features/failover/failoverService.ts:225-245`, `:320-514`; state machine — `src/shared/failover.ts:278-354`. Дефолты: - проверка каждые 60 секунд; - сбой primary — 120 секунд; - восстановление primary — 15 минут; - quiet-window — 30 секунд; - активный трафик — выше 32 КБ/с; - минимум на reserve — 10 минут; - после 3 failover за 24 часа — карантин primary на 6 часов. См. `src/shared/failover.ts:137-148`. ### Автоматический failback Отдельной реализации нет: это обратная ветка той же state machine. Из reserve Harbor возвращается на primary только после: - полного `recoveryWindowMs`; - `minimumReserveMs`; - окончания quarantine, если он действует; - quiet-window при включённом traffic guard; - подтверждения, что primary healthy. Причина переключения публикуется как `primary-recovered`. См. `src/shared/failover.ts:311-354`. ## 2. Судьба существующих соединений | Событие | Уже открытый TCP/UDP flow | Новые соединения | |---|---|---| | Автоматический failover primary → reserve | Остаётся на прежнем outbound; Harbor его не закрывает и не мигрирует | Идут через reserve | | Автоматический failback reserve → primary | Остаётся на reserve | Идут через primary | | Ручной selector switch | Не закрывается, если старый outbound ещё работает | Сразу идёт через выбранную роль | | Pause | Ничего не меняет | Идут через текущую роль | | Disable failover | Selector и текущий маршрут не меняются; dual config временно остаётся загруженным | Идут через текущую роль | | Обычный stop/restart/config replacement | Процесс `sing-box` останавливается, поэтому TCP/UDP-сессии прерываются | После запуска — по новой конфигурации | | Реальная авария primary | Уже существующий flow может оборваться сам; Harbor не может перенести его на другой внешний IP | После selector switch новые flow идут через reserve | Проверка `interrupt_exist_connections: false` непосредственно подтверждена TCP- и UDP-fixture-тестом: существующие TCP socket и UDP association продолжают обмен после switch, а новые идут через reserve. См. `test/server/singbox-selector-capability.test.js:141-142`, `:237-356`. ## 3. Отличия режимов ### Ручное переключение `POST /api/failover/switch` вызывает selector напрямую: - traffic guard не проверяется; - состояние здоровья целевого канала backend не проверяет; - после успешного ручного переключения `failoverPolicy.paused` становится `true`; - автоматический failback не произойдёт, пока пользователь не возобновит автоматическое переключение. См. `src/server/features/failover/failoverService.ts:248-317`, `:614-631`. Это означает, что ручной switch может быть выполнен даже на канал, который сейчас не подтверждён healthy. Это важная оговорка. ### Автоматический failover Автоматическое переключение: - ждёт failure window; - требует healthy reserve; - при включённом guard блокируется активным или неизвестным трафиком; - повторно валидирует условия непосредственно перед selector mutation; - не перезапускает `sing-box`. ### Восстановление primary Восстановившийся primary не получает новые соединения сразу. Сначала выдерживаются recovery/hold/quarantine условия и quiet-window. Пока они не выполнены, новые подключения остаются на reserve. ### Pause и Disable `pause` приостанавливает решения, но dual config и наблюдение остаются активными. `disable` останавливает scheduler, probes и failover activity collector, но не переключает selector и не перезапускает процесс. После обычного stop/следующего запуска собирается single-channel config. См. `README.md:78-84`, `docs/product/application-state.md:102-110`. ## 4. Практические сценарии - **Загрузка файла:** при обычном автоматическом failover активная передача по умолчанию задерживает switch. Если primary всё же упал, текущая TCP-загрузка не переносится на reserve; она может завершиться ошибкой. Возобновление или новый HTTP-запрос после switch пойдёт через reserve. - **Игровая сессия:** существующий TCP-сеанс остаётся на старом канале. TCP-сессия оборвётся, если primary реально недоступен. UDP-flow также не мигрирует и может начать терять пакеты или истечь по timeout; новая сессия после switch пойдёт через reserve. - **Стрим:** активный поток обычно блокирует автоматический switch при включённом guard. Ручной switch может быть выполнен сразу, но существующий TCP/QUIC-поток остаётся на старом outbound. При аварии старого канала плеер должен переподключиться. - **WebSocket/долгий polling:** действующий flow не переносится; новые подключения после switch используют новую роль. - **Молчащее соединение:** наличие открытого socket само по себе не считается активностью. Guard смотрит на дельты переданных байтов. ## 5. Условия и оговорки - Failover реализован только для **Gateway**, не для локального Connect или `gateway-direct`. - Активность собирается существующим `/connections` observer каждые 2 секунды, с bounded окном до 10 секунд. См. `src/server/index.ts:311-333`, `src/server/services/domainTrafficService.ts:323-416`, `:437-495`. - Короткое соединение, полностью завершившееся между двумя снимками, может не попасть в activity guard. - Неизвестная или устаревшая activity-информация блокирует автоматический switch; ручной switch остаётся доступен. - Отключение traffic guard разрешает автоматический switch без ожидания тишины, но `interrupt_exist_connections: false` всё равно защищает уже открытые connections от закрытия самим selector. - При изменении policy во время работающего single-channel VPN dual config становится `pending`; скрытого restart нет. См. `src/server/features/connection/connectionService.ts:140-201`, `:288-361`. - Явный stop/restart или обычная смена сервера вне активного failover уже является disruptive operation: `sing-box` получает SIGTERM и текущие сессии прекращаются. См. `src/server/singboxRuntime.ts:39-62`, `:65-122`. - Оба канала находятся в одном процессе `sing-box`; process-wide crash не защищён selector-механизмом. ## 6. Основные файлы - `src/server/singbox.ts:204-247` — dual config, selector, inbound routing. - `src/server/services/singboxSelectorService.ts:29-81` — selector PUT/read-back. - `src/server/features/failover/failoverService.ts:225-245` — проверки каналов. - `src/server/features/failover/failoverService.ts:248-317` — selector switch, commit и rollback. - `src/server/features/failover/failoverService.ts:320-514` — автоматический раунд и traffic guard. - `src/server/features/failover/failoverService.ts:614-631` — ручное переключение. - `src/shared/failover.ts:137-148`, `:278-354` — дефолты и state machine. - `src/server/services/domainTrafficService.ts:129-160`, `:323-416`, `:437-495` — классификация и activity. - `src/server/features/connection/connectionService.ts:140-201`, `:243-361` — обычный apply/stop/restart. - `src/server/singboxRuntime.ts:39-122` — фактическая остановка и перезапуск процесса. - `README.md:78-84` — пользовательская документация. - `docs/product/application-state.md:102-110` — контракт состояния. - `workpack/tasks/TASK-021-auto-server-selection-failover.md:93-106`, `:187-204` — относящийся план и ограничения; задача не выбиралась и не изменялась. ## 7. Тесты, подтверждающие выводы - `test/server/singbox-selector-capability.test.js:141-356` Интеграционная TCP/UDP-проверка: активный трафик задерживает failover, существующие TCP/UDP продолжают работать после switch, новые соединения идут через reserve, PID процесса не меняется. Тест opt-in и пропускается без `HARBOR_SINGBOX_IMAGE`. - `test/server/singbox-gateway-mode.test.js:69-110` Проверяет selector, `interrupt_exist_connections: false`, отдельные diagnostic routes и primary/reserve outbounds. - `test/server/failover-service.test.js:83-200` Failure window, traffic guard, both-unhealthy и failback recovery/hold/quarantine. - `test/server/failover-service.test.js:255-420` Disabled zero-work, отмена устаревших наблюдений и непосредственная revalidation активности. - `test/server/failover-service.test.js:423-520` Selector rollback, commit ordering и ручной switch с pause. - `test/server/domain-traffic.test.js:209-239` Activity считается по пользовательскому VPN traffic, diagnostic connection не блокирует switch. - `test/server/connection-service.test.js:265-310`, `:340-390` Restart dual config и rollback; pending edits для работающего single-channel. - `test/server/singbox-runtime.test.js:16-50` При изменении config runtime запускает новый процесс. - `test/server/failover-route.test.js:10-53` Gateway-only API и маршруты ручного switch/pause/check. - `test/web/failover-feature-contract.test.js:19-76` UI явно сообщает: новые подключения переключаются, открытые остаются на прежнем канале. ## Review findings и residual risks - **medium — `src/server/features/failover/failoverService.ts:614-620`:** ручной switch не проверяет health целевого канала и не применяет traffic guard; он сразу меняет selector и ставит automation на pause. - **medium — `test/server/singbox-selector-capability.test.js:141-356`:** capability test opt-in и использует deterministic `direct` outbounds, а не реальный VLESS/Trojan outage. - **medium — `src/server/services/domainTrafficService.ts:414-416`, `:451-479`:** activity основана на polling и byte deltas; короткие или очень малые потоки могут не блокировать автоматическое решение. - **info — `src/server/singboxRuntime.ts:39-122`:** явный stop/restart отличается от selector switch и прерывает существующие соединения. - **info — архитектура одного процесса:** падение всего `sing-box` не компенсируется selector failover. Файлы не изменялись. Тесты и live Gateway в рамках read-only аудита не запускались.