# FluxDesk relay protocol v1

Публичная точка подключения:

```text
wss://relay.nightlumen.com/v1/tunnel
```

## Назначение

Relay соединяет два исходящих WebSocket-подключения, поэтому host и viewer
работают за NAT без входящего порта. После сопряжения сервер вслепую пересылает
binary frames. Поверх этого byte stream FluxDesk устанавливает существующий
end-to-end `SslStream`.

## Setup

Первое WebSocket-сообщение является JSON text frame размером не более 4 KiB:

```json
{
  "version": 1,
  "accessId": "26-CHAR-CROCKFORD-BASE32-ID",
  "role": "host",
  "channel": "control",
  "attemptId": "00000000-0000-0000-0000-000000000000",
  "hostSecret": "64-HEX-CHARACTER-SECRET"
}
```

- `hostSecret`: случайный 256-битный секрет, известный только host и relay;
- `accessId`: первые 130 бит SHA-256 от `hostSecret`, записанные 26
  Crockford Base32-символами; viewer получает только этот производный ID;
- `role`: `host` или `viewer`;
- `channel`: `control` или `video`;
- host отправляет пустой `attemptId`;
- viewer использует один случайный GUID для обоих каналов.

Relay проверяет доказательство владения ID до регистрации host и не сохраняет
`hostSecret` в состоянии комнаты. Viewer не должен отправлять `hostSecret`.

Relay отвечает одним JSON text frame. При `status = ready` ответ содержит
viewer `attemptId`; далее разрешены только binary frames:

```json
{
  "version": 1,
  "status": "ready",
  "message": "Relay tunnel is ready.",
  "attemptId": "b2c6f234-5082-4f2e-829c-bc58dd084c18"
}
```

Другие статусы: `invalidRequest`, `notFound`, `busy`, `capacityExceeded`,
`timedOut`.

## Pilot admission

Если задан `Relay:AdmissionTokenSha256`, relay до WebSocket Upgrade требует
ровно один HTTP-заголовок `X-FluxDesk-Admission`. SHA-256 от значения заголовка
сравнивается с настроенным verifier digest за постоянное время. Digest должен
состоять ровно из 64 hex-символов. `/healthz` сообщает только
`admissionProtected: true` или `false` и не возвращает digest или токен.

Production Compose требует переменную
`FLUXDESK_RELAY_ADMISSION_TOKEN_SHA256`, передаёт контейнеру только verifier
digest и завершается с ошибкой конфигурации, если переменная не задана.
Plaintext-токен relay-серверу не нужен. Настройка `Relay:AdmissionToken`
сохранена только для локальной разработки и тестов; одновременное задание её и
`Relay:AdmissionTokenSha256` останавливает запуск.

Windows-клиент получает исходный bearer во время сборки через MSBuild property
`FluxDeskRelayAdmissionToken`; значение записывается в `AssemblyMetadata` и
автоматически добавляется к relay-запросу. Digest следует вычислить на доверенной
машине сборки и передать серверу отдельно, не копируя туда plaintext bearer.

Этот общий токен можно извлечь из собранного EXE. Он является только приватным
барьером от случайного и массового злоупотребления пилотным relay, а не
аутентификацией учётной записи, авторизацией пользователя, механизмом отзыва
доступа или биллингом.

## Pairing

1. Host заранее регистрирует `control` и `video`.
2. Viewer подключает `control`; relay закрепляет комнату за `attemptId`.
3. Внутренний TLS и `ClientHello` аутентифицируют control.
4. Viewer подключает `video` с тем же `attemptId`.
5. Host сверяет relay attempt с inner `ClientHello.SessionId`.
6. Закрытие control отменяет связанный video tunnel и освобождает attempt.

Код подключения не входит в relay protocol и передаётся только внутри
end-to-end TLS.

## Limits

- handshake: 4 KiB, 5 секунд;
- WebSocket chunk: 64 KiB;
- 128 одновременных outer connections;
- 4 connections с одного IP;
- 8 активных channel pairs, то есть до четырёх полных сессий;
- video budget: `Relay:VideoBytesPerSecond`, по умолчанию `12_500_000`
  bytes/s (примерно 100 Мбит/с) на направление, допустимый диапазон
  `1_000_000`–`100_000_000`;
- control budget: `Relay:ControlBytesPerSecond`, по умолчанию `524_288`
  bytes/s (примерно 4,2 Мбит/с) на направление, допустимый диапазон
  `65_536`–`4_194_304`;
- host wait: 12 часов с WebSocket keepalive;
- viewer wait: 30 секунд;
- active channel-pair lifetime: 12 часов (`0` в конфигурации отключает предел);
- WebSocket compression отключён.

Backpressure применяется непосредственно в blind pump; видеоданные не
накапливаются в отдельной server-side очереди.

Увеличенный video budget не даёт грубому секундному ограничителю примерно на
20 Мбит/с задерживать всплески high-motion 1080p30 настолько, чтобы они
сталкивались с двухсекундным дедлайном сетевой записи host. Это предохранительный
предел relay, а не целевой bitrate видеокодека.

Запросы WebSocket с непустым HTTP `Origin` отклоняются. Это убирает
drive-by-подключения из обычной веб-страницы, но не заменяет учётные записи и
полноценную admission policy для публичного массового сервиса.

## Trust model

Relay получает регистрационный секрет host и видит Internet ID, IP-адреса, роль,
канал, attempt ID, размеры и тайминги сообщений, а также метаданные рукопожатия
внутреннего TLS. Регистрационный секрет проверяется до создания комнаты и не
сохраняется в её состоянии. В зависимости от согласованной версии TLS к
метаданным может относиться сертификат host. Прикладное содержимое desktop
protocol — изображение, ввод и pairing code — relay расшифровать не может.
Производный Internet ID не позволяет зарегистрировать поддельный host без
скрытого `hostSecret`, однако подлинность самого host для viewer всё равно не
следует из relay-соединения: пользователь должен сравнить SHA-256 fingerprint
host до отправки pairing code.
