feat(node): pair onboard computers with the Core fleet through UI
This commit is contained in:
@@ -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 браузера.
|
||||
Reference in New Issue
Block a user