14 KiB
DM: доставка сообщений и файлов
Личные беседы принадлежат src/dm/. Общие CHAT-группы используются для знакомства,
проверки ролей и маршрутизации; группа не становится идентичностью беседы.
Реализовано
- История и постоянная исходящая очередь сообщений в общей SQLite ядра.
- Доставка через живой маршрут router, включая BGP-транзит.
- При отсутствии маршрута — подписанный PUT доступному суперузлу общей группы сторон.
- Приём после проверки подписи и расшифровки, подтверждение только после commit.
- Доставка с суперузла после возвращения получателя, независимо от отправителя.
- Точное удаление промежуточного сообщения и сохранение квитанции для поздних повторов.
- Подготовка, шифрование, проверка и durable публикация медиа в рабочих потоках.
- Прямая передача файлов либо custody на отдельном M, доставка после restart и подтверждённое удаление.
- TTL и квота M; вложения и статусы доставки в Qt и Android.
Идентичность и шифрование
conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63]
content_key = SHA256(X25519(my_private, target_public) || "utun_dm_content")
msg_nonce = SHA256(conv_id || author || seq)[..13]
media_nonce = SHA256("utun_dm_media" || author || media_id || part)[..13]
Сообщения и медиа используют один content_key target, без ключей отдельных файлов. AES-256-CCM: nonce 13 байт, tag 16 байт. Максимальная порция plaintext — 65535 байт; порция медиа — 32768 байт. UUID медиа новый для каждой подготовки. Повтор доставки использует неизменяемый ciphertext. Повторная подготовка после ошибки требует нового UUID.
Каноническое тело сообщения:
[conv:8][seq:8][ts:8][author:8][ct_len:1][ct][enc_len:2][ciphertext+tag][author_sig:64]
Автор подписывает все поля до sig. conv связывает автора с получателем.
Для ct=file перед подписью добавляется descriptor [UUID:16][plaintext_size:8][SHA256(ciphertext):32].
В зашифрованном теле находится имя файла; пути хранения строятся только из UUID.
Размер plaintext сообщения ограничен 1024 байтами. seq — положительное int64.
Медиа ciphertext — последовательность порций с отдельным tag каждой порции. Последняя порция короче; пустой файл содержит один tag. Размер plaintext и SHA256 ciphertext должны входить в подписанный manifest. Потоковая расшифровка проверяет все tags, точный размер и хеш; владелец публикует временный файл только после успеха и собственного durable сохранения.
Владение и данные
dm_core: беседы, история, outbox и подтверждение получателя.dm_mailbox: промежуточные сообщения суперузла и квитанции.dm_crypto: вывод ключей, nonce, CCM и файловые примитивы без БД/сети.dm_media: файлы endpoint, загрузки и отдельная custody M.dm_mailbox_media: durable задания S, выбор M и повтор удаления до DELETE_ACK.file_transfer: подписанная передача байтов, backpressure и отдельный CM handle каждой операции.
dm_conversations(conv_id, peer_node_id, peer_x25519, peer_ed25519, peer_name,
group_id, last_out_seq, last_in_seq, created_at, last_ts)
dm_messages(conv_id, dir, seq, ts, author, ct, data, sig)
dm_outbox(conv_id, seq, body, retry_at)
dm_pending_messages(recipient, sender, seq, msg, group_id, retry_at)
dm_mail_receipts(recipient, sender, seq, receipt, group_id)
Запись исходящего сообщения, увеличение seq и запись неизменяемого body в outbox — одна транзакция. При ошибке откатывается всё, сообщение не выходит в сеть. Приём, создание/обновление беседы и история — также одна транзакция.
Dedup — точный ключ (conv,dir,seq) с проверкой сохранённой подписи. last_in_seq — статистика максимального номера, не курсор подтверждения: seq=99 после seq=100 сохраняется. Тело с другой подписью для уже сохранённого seq отклоняется.
У каждой локальной очереди таймер 1с; порция обработки до 32 задач, повтор задачи через 5с. retry_at предотвращает голодание хвоста очереди. Перед отправкой проверяется backpressure router. Потеря пути сохраняет durable задачу; остановка отменяет таймеры, но сохраняет БД. При старте очередь продолжает работу.
Подтверждения и протокол
Сервис ETCP_RT_ID_DM=0x34: MSG=1, RECEIPT=2.
["DMACK001":8][conv:8][seq:8][author:8][recipient:8][SHA256(message):32][recipient_sig:64]
Квитанцию создаёт только получатель после commit. Подпись охватывает первые 72 байта. Отправитель удаляет outbox только после проверки подписи и точного хеша тела. История беседы сохраняется. Подтверждение транспорта router не заменяет квитанцию.
Сервис ETCP_RT_ID_DM_MAILBOX=0x35:
PUT=1 [recipient:8][message]
DELIVER=2 [message]
RECEIPT=3 [receipt:136]
STORED=4 [conv:8][seq:8]
Все пакеты идут с подписью router; unsigned control отклоняется. PUT принимается только от автора, при роли supernode=yes у локального узла в общей группе автора и получателя. Роль берётся из подписанного adm_tags; node_type=4 — производная сетевой классификации, для авторизации не используется. storage=yes само по себе не даёт права хранить сообщения.
STORED подтверждает durable запись суперузлом, outbox до квитанции получателя остаётся у автора. Суперузел повторяет DELIVER при появлении живого маршрута. RECEIPT проверяется по точному сохранённому телу; запись квитанции и удаление копии — одна транзакция. Квитанция пересылается автору. Поздний PUT возвращает её и не возрождает задачу.
dm_mail_receipts содержит только служебные квитанции; доставленное содержимое с промежуточного узла удаляется. Ранее использовавшийся dm_mail/PULL/HELLO и диапазонные ACK не поддерживаются: это devel, совместимость не сохраняется. TTL текстовых сообщений не введён; недельный TTL из pm.txt относится к медиа.
Доставка медиа по pm.txt
- Общая передача байтов: явные peer/group/file/size, backpressure и CM handle
на всю активную операцию. Владение handle сохраняется после DOWN/TIMEOUT.
Используется
ETCP_RT_ID_FILE_TRANSFER=0x37. Один callback читает/отправляет не более 8 КиБ, продолжение черезcall_soon; inflight ограничен 16 пакетами. - Отдельный owner dm_media: подготовка ciphertext, manifest, отправка/скачивание, временные файлы, TTL и лимит хранения. Каталоги dm_media/ и dm_pending/ вне группового обхода очистки orphan-файлов; групповые объявления HAVE_BLOCK не используются.
- Суперузел сначала durable регистрирует сообщение и media job, затем выбирает M. M скачивает ciphertext у A и сообщает S подтверждённое хранение, место и expiry.
- Текстовая квитанция и квитанция медиа независимы. Получатель подтверждает медиа после проверки, публикации файла и commit. S хранит задачу удаления у M до DELETE_ACK; поздний STORED не возрождает завершённую доставку.
- TTL медиа
dm_media_ttl_secпо умолчанию 604800 с; квотаdm_media_storage_mbпо умолчанию 1024 МиБ. Настройки на M. Новая custody отклоняется при недостатке квоты; уже подтверждённые копии не вытесняются молча раньше срока. - Stop снимает новые задачи и закрывает transfer/CM handles, затем ждёт media_async с живыми owner-контекстами. Группы уничтожаются после owners.
Control ETCP_RT_ID_DM_MEDIA=0x38: OFFER, STORE, STORED, RECEIPT, DELETE,
DELETE_ACK, EXPIRED, REJECT; точные форматы заданы в dm_media.h и реализациях owners.
Подпись router обязательна. Media receipt содержит DMFILE01, conv, seq, author,
recipient, UUID и SHA256 полного подписанного сообщения; B подписывает первые 88 байт.
Отдельные таблицы dm_files, dm_custody, dm_custody_done, dm_media_jobs,
dm_media_holders сохраняют состояния и tombstones. Все выданные grants M
учитываются при удалении; повторный STORE не продлевает TTL.
Qt и Android получают историю через события ядра: 41 — conv + JSON сообщений,
45 — JSON бесед. Снимки ограничены 200 записями. Отправка вложения вызывает
dm_send_file в uasync; длительная обработка выполняется в media_async.
Android копирует выбранный URI и сохраняет полученный файл через Dispatchers.IO;
Qt сохраняет файл из контекстного меню в отдельном потоке. Фото и видео пока
отправляются как оригинальные файловые вложения, без канального транскодирования.
Проверки
test_dm: симметрия ключа target, CCM round-trip, пустое тело, пределы длины,
неверный ключ/tag, разделение nonce; потоковые файлы на границах 32 КиБ,
неверные author/UUID/hash, обрезание и лишние байты.
test_dm_e2e: реальные A–C–B соединения и BGP; нет физического A–B линка.
Проверяет out-of-order, dedup/conflict, подписи, SQL rollback при приёме и enqueue,
ACK чужого тела, отказ mailbox без роли, точное удаление и поздний PUT.
Первый сценарий передаёт 40 offline-сообщений (больше порции pump) и перезапускает источник с pending outbox; второй перезапускает
суперузел и доставляет первое сообщение при выключенном источнике.
test_dm_media: реальные A/B/S/M с раздельными ролями supernode и storage;
прямая передача, custody, restart S/M при выключенном A, точный файл на B,
удаление копии и тела S, поздние STORE/STORED, TTL, отказ по квоте, неверная квитанция,
SQL rollback регистрации, состояния JSON и отмена операции с CM handle.
test_dm_worker: повторное шифрование 8 МиБ не менее 0,5 с и проверка расшифровки;
work выполняется вне uasync, done в его потоке, heartbeat продолжает работать.