feat(node): pair onboard computers with the Core fleet through UI

This commit is contained in:
DCCONSTRUCTIONS
2026-09-05 21:16:13 +03:00
parent fc545f8440
commit e82d012907
29 changed files with 2442 additions and 19 deletions
+155
View File
@@ -0,0 +1,155 @@
# Привязка БК к Mission Core — протокол v1
Дата: 05.09.2026. Инкремент Node 0.5.0. Основание: MISSIONCOR-76 и согласованная
поверхность `03_SYSTEM_AND_VEHICLE_PAIRING_SURFACE.md`. Чистая установка ОС
отложена владельцем; она не закрыта текущими проверками и не блокирует этот этап.
## Работа оператора
1. В установленном Node: **Система → Mission Core**. Выбрать собственный адрес
БК, доступный Core через общую LAN или Tailscale; создать приглашение.
2. В Core: **Парк → Аппараты → +**. Способ подключения «С бортовым компьютером»,
отдельно класс платформы (UGV, UAV, стационарная, другая), название и код.
3. «Проверить БК» подтверждает криптографическую идентичность и показывает
сведения машины. «Добавить аппарат» сохраняет ожидающую привязку.
4. После подтверждения Node сам соединяется с Core. «В сети» появляется только
по полученному через mTLS сообщению этого БК. Глаз открывает конфигурацию.
5. Отзыв доступен с обеих сторон с подтверждением. Повторное подключение той же
идентичности БК после отзыва сохраняет идентификатор аппарата в этом Core.
UGV/UAV описывают платформу, а не способность автономного движения. Наличие Node
не означает реализованный автопилот. Подключение аппаратов без БК в этой версии
не реализовано. USB-инвентаризация системы не создаёт устройства аппарата.
## Границы компонентов
- Node: Go-служба без root/capabilities, постоянная Ed25519-идентичность;
GTK/WebKit desktop с общей React/DG-поверхностью. Авторизация местного оператора
остаётся через OS/Polkit → закрытый Unix socket → одноразовый вход в loopback UI.
- UI Node `CoreConnectionView.tsx` использует только локальные `/api/core*`.
Она не выдаёт браузеру закрытые ключи, CA-ключ Core или клиентский сертификат.
- Core: `k1link.fleet.trust` — PKI и проверка приглашения;
`registry` — реестр аппаратов и переходы; `transport` — входящий канал Node;
`web.fleet_api` — API локального оператора. `core/fleet/useFleet` и
`workspaces/fleet/VehiclesWorkspace` — отдельный UI-модуль, а не логика в App.
- Control Station остаётся на `127.0.0.1:8000`; новые API отвергают удалённого
клиента, неверный Host и чужой Origin. Авторитет здесь — локальный сеанс Core;
многопользовательская удалённая авторизация Core этим инкрементом не вводится.
- SDK v0alpha2 `ExecutionBinding` связывает постоянный node_id с новым
agent_instance_id процесса и платформой linux. `devices: []` явно означает,
что драйверные устройства этим этапом ещё не опубликованы.
## Частные каналы
- БК временно слушает выбранный собственный IPv4 на TCP 8781 во время приглашения
и ожидающего подтверждения. Wildcard, loopback, публичный адрес, DNS-имя,
произвольный порт, URL с credentials/path/query/fragment запрещены.
- Допущены RFC1918 и CGNAT 100.64/10 для Tailscale. IPv6 не реализован.
- Core слушает TCP 8782 только на конкретном частном обратном адресе, который
реально использовало его соединение с БК. Этот сервис не отдаёт UI и не
принимает приглашения; только mTLS heartbeat/unpair.
- Tailscale — заменяемый IP-транспорт. В протоколе нет его API, идентичности,
публичного relay, cloud rendezvous или обязательного внешнего сервера.
- Адреса фиксируются при привязке. При смене подсети/IP нужна новая привязка.
Автоматический поиск нового адреса Core и маршрутизация между сетями отсутствуют.
- Подключение K1 остаётся отдельной задачей: только Wi-Fi Bridge к общей LAN.
Старый локальный Quick Connect остаётся лабораторным legacy-сценарием.
## Приглашение и криптография
`MCN1.` + base64url JSON со строго заданными полями:
`schema`, `node_id`, `id`, `endpoint`, `expires_at`, `secret`.
Schema: `missioncore.node-pairing/v1`. Срок — 10 минут; id/secret — случайные
256 бит. Код является полномочием доверия: он показывается только при создании,
не пишется в URL, localStorage, логи или Ops. В Node сохраняется только SHA-256
секрета. Создание нового приглашения отменяет старое.
Bootstrap использует TLS 1.3 с самоподписанным сертификатом Node. Core сначала
проверяет тип Ed25519, SHA-256 публичного ключа из node_id, подпись сертификата и
срок действия; только затем отправляет секрет. Системная CA, DNS, прокси и
перенаправления не могут заменить этот pin. Это явно реализованная проверка
peer certificate, а не отключённая проверка идентичности.
Core имеет постоянный Ed25519 CA (10 лет) в закрытом локальном хранилище. Node
проверяет CA fingerprint core_id, подпись CA и выпущенный Core клиентский
сертификат на собственный публичный ключ. Дальше работает обычный TLS 1.3 с
RootCAs, проверкой IP SAN сервера и обязательным клиентским сертификатом.
Закрытый ключ Node никогда не передаётся Core. Common Name не используется как
идентичность: идентичность вычисляется из публичного ключа.
Клиентский сертификат — 30 дней, продление при остатке менее 7 дней через
действующий mTLS-сеанс; прежний serial допускается 1 час для доставки обновления.
Серверный leaf — 30 дней, контекст обновляется каждые 7 дней и при запуске.
Корневой ключ автоматически не заменяется. Долгий offline сверх срока client
certificate требует отзыва и новой привязки через UI. Автоматической ротации
постоянных идентичностей/корня в этой версии нет. Часы обоих компьютеров должны
быть корректны; ошибки доверия не обходятся отключением TLS.
## Переходы и восстановление
Node: `unpaired → inviting → pending → paired`, отдельно `revoked`.
1. `/v1/pair/inspect`: действующий секрет, проверка состояния inviting,
возвращает подтверждённую идентичность и системный инвентарь.
2. Core хранит проверку в памяти до 5 минут (максимум 16 проверок). После кнопки
добавления он транзакционно создаёт запись pending и сохраняет предложение,
приглашение и будущую идентичность привязки для восстановления после сбоя.
3. `/v1/pair/offer`: Node проверяет секрет/CA/client certificate и атомарно
сохраняет pending с hash предложения и случайной квитанцией. Один Node — один
Core. Точный повтор получает ту же квитанцию; иное предложение получает 409.
4. Core сохраняет квитанцию ДО `/v1/pair/commit`. Node атомарно сохраняет paired.
После этого bootstrap закрывается, начинается исходящее соединение.
5. Потерянный ответ commit восстанавливается настоящим mTLS heartbeat. Core
перепроверяет binding_id и состояние после каждого сетевого вызова, поэтому
завершение старого запроса не восстанавливает уже отозванную привязку.
6. Pending повторяется до истечения приглашения. Истёкшее подтверждение остаётся
в реестре как failed с причиной; оператор создаёт новый код. На Node pending
также истекает. Повторное добавление по той же проверке идемпотентно.
Core хранит запись аппарата отдельно от его вычислительного корня: vehicle_id,
класс, название, node_id, revision, enrollment, connectivity, last_seen, host,
execution_binding. UNIQUE(node_id) исключает два аппарата на одном БК в этом Core.
Перезапуск Core сбрасывает доказательство текущей доступности до нового heartbeat.
Отсутствие сообщений более 20 секунд означает offline, а не удаление записи.
## Сохранность и отзыв
- Node: `core-binding.json` в StateDirectory 0700, файл 0600,
temp → fsync → rename → fsync каталога; изменение памяти только после записи.
- Core: закрытый каталог `MISSIONCORE_DATA_DIR/fleet`, SQLite WAL/FULL, файл 0600,
транзакции под единственным writer lock. CA private key — отдельный файл 0600.
- Пока подтверждение не завершено, секрет приглашения нужен Core для повтора и
хранится в закрытой pending-записи. После подтверждения удаляется из текущей
записи; secure_delete включён. Это не обещание физического стирания блоков SSD,
WAL, снимков или резервных копий; закрытое хранилище целиком считается секретным.
- Отзыв в Core немедленно запрещает следующий запрос старого binding_id, включая
существующее TLS-соединение. Node получает 410 и прекращает канал.
- Отзыв в Node прекращает локальную привязку сразу и сохраняет durable outbox
уведомления Core. При недоступном Core аппарат там может оставаться offline до
доставки отзыва. Очередь ограничена 8 элементами; истёкший сертификат больше не
даёт полномочий и удаляется из очереди. Отзыв не зависит от доставки для
прекращения работы самого Node. Старый binding_id не принимает новый реестр.
- Повреждённая идентичность/привязка не заменяется молча. Node отказывает в старте
с повреждённым файлом доверия; восстановление такого хранилища из резервной
копии пока инженерная операция. Недоступное хранилище Fleet отключает pairing,
сохраняя другие разделы Core. GUI-восстановление повреждённой идентичности не
входит в текущую приёмку и остаётся отдельной задачей.
Bootstrap ограничен 16 одновременными соединениями, временем чтения/записи,
размером заголовков/JSON и 32 неверными попытками секрета за минуту. Core mTLS
ограничен 16 соединениями и телом 64 КиБ. Используется исходящий keepalive HTTP/1.1
с heartbeat каждые 5 секунд, таймаутами и автоматическим переподключением.
## Проверка и следующий этап
Покрыты тестами сохранение и повтор commit, конфликт владельцев, TTL, отмена,
отзыв во время сетевого ответа, потерянный ack, повторная регистрация без дубля,
проверка pin до отправки секрета, срок прежнего сертификата, закрытость API и
запрет публичных/DNS/посторонних портов. GUI-приёмка реальной пары и точные хеши
сборок фиксируются отдельным отчётом в MISSIONCOR-76.
Этот этап передаёт только сведения БК. Он не объявляет готовыми команды,
медиа/WebRTC, запись, управление K1, SDK-драйвер D455, телеметрию движения или
автономные миссии. Следующий отдельный инкремент: реальный RealSense D455 за Node;
после него K1 через Wi-Fi Bridge. Их полномочия и API добавляются к существующим
границам Node/Core, а не к SSH или UI браузера.