# Mirror Tool Асинхронный TCP mirror и панель управления маршрутами для A/D CTF. Один Rust-бинарник включает сетевое ядро, веб-интерфейс и клиент ForcAD. Python, Node и внешние CDN для работы не нужны. Клиент общается с одним **основным upstream**. Входящий поток можно одновременно отправлять выбранным **репликам**; их ответы вычитываются и игнорируются. Смена маршрута применяется к новым соединениям. Открытая сессия сохраняет свой upstream до завершения. ## Запуск Нужен актуальный stable Rust (проверенные версии записаны в `docs/verification.md`). ```sh cargo build --release --locked cp mirror.example.toml mirror.toml # Отредактируйте команды, адреса и порты. Пример использует тестовую сеть RFC 5737. read -s MIRROR_PASSWORD export MIRROR_PASSWORD ./target/release/mirror-tool --config mirror.toml check ./target/release/mirror-tool --config mirror.toml ``` Пароль должен содержать минимум 12 символов. Панель по умолчанию доступна на `http://127.0.0.1:6700`. Для удалённого доступа используйте VPN/SSH-туннель либо HTTPS reverse proxy; при HTTPS установите `secure_cookie = true`. TLS для управляющей панели внутри бинарника не реализован. Переменные окружения с паролем и конфигурацию игры не коммитьте. Панель сохраняет оформление старого `mirror_tool`: горизонтальные колонки сервисов, список команд, кружок primary, выбор реплики нажатием на строку, робот авто режима, открытие сервиса и переключатель запуска. При выборе нового primary прежний становится репликой. Кнопка «+» открывает прежнюю форму: название, порт и необязательный IPv4/IPv6. Предварительно создавать команду для произвольного IP не нужно. У custom-прокси остаются поле IP и удаление в самой колонке. Настройки сохраняются после успешного ответа сервера и переживают рестарт. Дополнительные настройки открываются нажатием на название сервиса: имя, адрес слушателя, upstream-порт и поведение при медленной реплике. Основная панель сохраняет прежние четыре счётчика соединений; байты и диагностика реплик доступны в подсказке над ними и API. Авто режим выбирает только из назначенных реплик. Исходный CSS перенесён с нормализацией пробелов в концах строк; Bootstrap 5.1.3 и Font Awesome Free 6.0.0 той же версии включены в бинарник. Для работы панели Интернет не нужен. Небольшие изменения: клавиатурное управление, индикаторы устаревших данных, сохранение формы во время обновлений и адаптация узкого экрана. Лицензии встроенных зависимостей доступны на `/licenses.txt` и в `web/vendor/`. ## Доставка и перегрузка | Режим | При медленной реплике | При недоступной/сломавшейся реплике | |---|---|---| | `isolate` (по умолчанию) | Ограниченная очередь; при заполнении реплика отключается для этой сессии. Primary продолжает работать. | Ошибка и отключение учитываются в статистике; primary независим. | | `strict` | Чтение клиента замедляется до скорости всех реплик (backpressure). | Сессия завершается с ошибкой; продолжать без обязательной реплики нельзя. | Произвольные фрагменты потока не выбрасываются с продолжением передачи, как в старой версии. Но TCP mirror **не является репликацией БД**: серверы могут генерировать разные идентификаторы, challenge/nonce, ответы и побочные эффекты. TLS-поток нельзя содержательно зеркалировать в независимые TLS-сессии. Проверяйте совместимость конкретного протокола и checker перед игрой. Абсолютные «0 потерь / 0 накладных расходов» недостижимы при сбоях сети и медленных репликах. Strict сохраняет порядок и полную передачу при здоровых участниках ценой скорости/доступности; isolate может доставить отключённой реплике только префикс. Счётчики байтов показывают успешные локальные socket writes, а не фиксацию данных приложением или БД. Незавершённый запрос никогда не переигрывается на другом upstream. ## ForcAD и автоматический выбор ```toml [scoreboard] kind = "forcad" url = "http://scoreboard.example:8080" poll_interval_ms = 3000 stale_after_ms = 15000 ``` ForcAD: постоянная сессия Engine.IO v4 / Socket.IO polling, namespace `/game_events`, ping/pong и переподключение с ограниченным backoff. `init_scoreboard` задаёт метаданные и состояние; `update_scoreboard` обновляет состояние без потери метаданных. Поддерживается формат upstream [pomo-mondreganto/ForcAD](https://github.com/pomo-mondreganto/ForcAD), проверенный на `6ce3407`. События сопоставляются по точному имени сервиса и имени/IP команды. Настройте имена как на scoreboard. Произвольные несовместимые форки автоматически не угадываются. Для adplatf укажите `kind = "adplatf"`: используется HTTP `/state`. URL не должен содержать пароль, query или fragment. Доступный endpoint должен отдавать публичную таблицу без отдельной аутентификации. Для прежней цветной полосы состояния **своей команды** задайте `main_team_ip = "192.0.2.10"` в начале TOML, до секций. Это аналог `MAIN_TEAM_IP`; полоса независима от выбранного primary. Без этого параметра она остаётся нейтральной. Имена команд окрашиваются по checker, чипы показывают потерянные флаги; сортировка сохраняет прежний порядок status → lost → имя. Автопереключение: 1. TCP-пробы выполняются параллельно с ограничением; отдельно считаются последовательные сбои и успешные проверки. 2. Причина переключения — устойчивый TCP-сбой primary либо свежий checker CORRUPT/MUMBLE/DOWN. CHECK_FAILED и неизвестный статус не принимаются за неисправность сервиса. 3. Кандидат должен быть назначенной репликой с недавней успешной TCP-проверкой и пройденным порогом восстановления. Свежий плохой checker исключает кандидата; UP получает приоритет. При равенстве используются `stolen`, затем ID. 4. Работает cooldown, включая ручную смену primary. Самопроизвольного возврата на прежний сервер нет. Старый primary становится репликой. 5. Устаревший или отключённый scoreboard не влияет на выбор; при отсутствии пригодного кандидата маршрут сохраняется. Активные сессии не мигрируют. Встроенный proxy выбирает upstream; системные маршруты, nftables и iptables не меняются. Исходный IP клиента на upstream не сохраняется. UDP и PROXY protocol не реализованы. ## Ресурсы и остановка Tokio multiplexes соединения без subprocess/thread на сессию. Общий `max_connections` ограничивает активные сессии всех сервисов; лишние подключения закрываются и учитываются как `rejected`. Очереди реплик ограничены `replica_queue_chunks`, общий бюджет их разделяемых payload — `max_buffered_bytes` (256 MiB по умолчанию). В этот бюджет не входят рабочие буферы чтения и TCP-буферы ОС. Рабочие буферы ограничены числом сессий/реплик; не выставляйте лимиты выше доступной памяти и fd. Путь без реплик переиспользует буфер; зеркальный путь передаёт разделяемые `Bytes` без отдельной копии на реплику. Счётчики используют атомарные операции. TCP_NODELAY и TCP keepalive включены; connect timeout не превращается в короткий idle timeout. Stop отключает приём новых соединений, сохраняя открытые. Удаление остановленного сервиса ждёт `drain_timeout_ms`, затем отменяет остаток. SIGINT/SIGTERM запрещает новые управляющие изменения, закрывает слушатели и ограничивает завершение процесса общим бюджетом `drain_timeout_ms + 1100 ms` с учётом планирования ОС. Недочитанный HTTP-запрос не задерживает процесс бесконечно. При исчерпании бюджета остаток прерывается; сохранение могло завершиться до обрыва HTTP-ответа. Isolate также ограничивает время отправки остатка репликам после завершения основного обмена. Strict в нормальном режиме может ожидать медленную реплику неограниченно; общая остановка остаётся ограниченной. ## Конфигурация и состояние `mirror.toml` задаёт настройки процесса и начальную топологию. После первого изменения сохранённый `state_file` содержит авторитетный список команд/сервисов; TOML продолжает задавать сеть, мониторинг и scoreboard. Путь состояния отсчитывается от каталога TOML. Чтобы применить начальную топологию заново, остановите процесс и **переместите** state-файл в резервную копию. Файл не исполняется, версия и содержимое валидируются. Запись состояния: временный файл на той же ФС → fsync → atomic rename; права `0600`. Сбой сохранения вызывает попытку отката runtime. Публикация topology сериализована с ручными и автоматическими изменениями. Runtime и ФС не образуют распределённую транзакцию: клиент, принятый в коротком окне до неудачного сохранения, может сохранить временно применённый маршрут. Необходимость отката выводится как ошибка. После допуска к транзакции она завершается независимо от HTTP-соединения: закрытие вкладки не оставляет сохранение/очистку наполовину. Если ответ потерян, обновите snapshot перед повтором — изменение могло сохраниться. Старый `config.py` не выполняется и не импортируется. Перенесите `TEAMS` в `[[teams]]`, `TASKS` в `[[services]]`, ForcAD URL в `[scoreboard]`, `MAIN_TEAM_IP` в верхнеуровневый `main_team_ip`. `PASSWORD` заменён переменной окружения, `SECRET_KEY` не нужен: сервер хранит случайные сессии в памяти. Старые cookie после перезапуска недействительны. API описан в [docs/api.md](docs/api.md). Для Linux есть [systemd unit](deploy/mirror-tool.service): создайте отдельного пользователя `mirror-tool`, разместите бинарник в `/usr/local/bin/`, конфиг и закрытый `mirror.env` в `/etc/mirror-tool/`; в TOML задайте `state_file = "/var/lib/mirror-tool/state.json"`. Unit даёт только CAP_NET_BIND_SERVICE для портов ниже 1024; при ненужности удалите capability. При увеличении drain timeout увеличьте `TimeoutStopSec`. Unit поставляется как шаблон, автоматически не устанавливается. ## Разработка и проверка ```sh cargo fmt --check cargo clippy --all-targets --locked -- -D warnings cargo test --all-targets --locked node --check web/app.js cargo run --release --example benchmark -- 64 none cargo run --release --example benchmark -- 64 strict cargo run --release --example benchmark -- 64 isolate ``` Benchmark сравнивает direct loopback с proxy в том же процессе, проверяет каждый байт, выполняет три чередующихся прогона и 500 latency-проб. Это воспроизводимый локальный baseline, не прогноз скорости реальной сети. Результаты и практические ограничения: [docs/verification.md](docs/verification.md). Для ручного теста UI на изолированном loopback: `cargo build`, затем `python3 scripts/ui_fixture.py target/debug/mirror-tool`. Это необязательная тестовая утилита; её публичный тестовый пароль указан в начале файла, конфиг/состояние временные. Не используйте fixture для реального доступа. Сравнение с прежними шаблонами: установите `jinja2` в отдельный dev-venv и запустите `python scripts/legacy_ui_reference.py ../ad_scripts/mirror_tool`. Утилита проверяет сохранность исходных CSS-правил и рендерит старые страницы на синтетических loopback-данных, не читая `config.py`. Для дополнительной ForcAD-проверки есть `scripts/verify_forcad_library.py`, использующий настоящие версии Socket.IO/Engine.IO из upstream. Структура: `config` — валидация, `engine` — TCP, `scoreboard` — протоколы таблиц, `control` — health/выбор/изменения, `store` — сохранение, `auth`/`web` — управление, `web/` — встроенный интерфейс. `agent.md` и журналы фиксируют ход работы.