Files
harbor-net/context.md
T
dokril a84cca0668
Build and Deploy Gateway / build-and-push (push) Failing after 1s
Build and Deploy Gateway / deploy (push) Has been skipped
Improve failover controls and preserve manual switching state
2026-08-28 19:40:53 +03:00

209 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 аудита не запускались.