/* * chat_join — добавление участника канала через узел подключения C и инвайтера A. * Модуль регистрирует invite-ключи, пересылает запрос к A и сохраняет подписанного * участника. Прямой обмен с новым узлом J выполняет chat_sync; Merkle разносит запись. * * Протокол (A и C могут быть одним узлом): * * инвайтер A connection-узел C джойнер J * │ KEY_REGISTER(key,ch) │ │ * ├──────etcp_router─────────▶│ хранит {key→A, 10 мин} │ * │◀── KEY_REGISTER_ACK ──────┤ │ * │ теперь ссылка готова │ │ * │ │◀─── JOIN_INFO_REQ(key) ──────┤ (J подключился к C по ссылке) * │ │── JOIN_INFO_RESP ───────────▶│ (ch_x25519, ch_ed25519, ch_sig, C-мембер) * │ │◀─── JOIN_REQUEST ────────────┤ (key + полный набор мембера J) * │ │ валиден ключ → только pending, БЕЗ BGP new_conn * │◀── REQUEST_FWD ───────────┤ │ * │ verif join_sig, node_id=derive(x25519) │ * │ member_sync_put(J, signed_by=A, signature=sign(A, J_x25519||"group_join")) * │ ── merkle broadcast ──▶ (рекорд J дошёл по merkle) │ * │ │── JOIN_READY(ch) ────────────▶│ добавление подтверждено у C * * КОНТРАКТ ЧЛЕНСТВА (обязателен для всех путей подключения): * - A и C уже участвуют в канале; A может совпадать с C. * - NCD даёт транспорт J–C для протокола добавления. Наличие транспорта, * invite-ключа или локального TOPO_GROUP не даёт участия J в BGP-группе. * Инициатор удерживает отдельный NCD handle до результата/таймаута добавления; * закрывать его на транспортном UP, рассчитывая на владельца BGP, запрещено. * - После проверки JOIN_REQUEST инвайтер сохраняет мембера через member_sync. * Merkle распространяет запись между существующими участниками. * - Каждый узел разрешает групповую сессию J после принятия и сохранения * записи J в СВОЕЙ таблице участников. Подтверждение всех узлов не требуется. * - Проверка invite-ключа разрешает только обработку запроса добавления. * Временного членства и раннего BGP new_conn до сохранения мембера нет. * - JOIN_READY означает наличие записи J у C. Это не подтверждение доставки * всем узлам и не окончание обмена топологией. BGP JOIN_GROUP / TABLE_COMPLETE * из topo_group.h относятся к отдельной групповой сессии. * * ПОРЯДОК И ПРОВЕРКИ: * 1. A регистрирует (канал, join_key) -> A на C на JOIN_KEY_TTL_SECONDS. * Ссылку можно публиковать только после успешного KEY_REGISTER_ACK. * При A==C регистрация и обработка запроса локальные. * 2. J открывает NCD к C; C проверяет ключ из JOIN_INFO_REQ и возвращает * подписанное описание канала и свою запись участника. J проверяет ответ. * Создание локальной инфраструктуры канала ещё не добавляет J на других узлах. * 3. C проверяет JOIN_REQUEST: ключ и привязку идентичности J к транспорту. * Запрос идёт к A по существующему маршруту канала; маршрут через J не нужен. * 4. A проверяет запись, подписывает приглашение и сохраняет мембера. * После локального commit или принятия записи по Merkle C отправляет JOIN_READY. * 5. JOIN_GROUP, пришедший раньше записи мембера, не создаёт сессию у получателя. * После commit member_sync повторно инициирует групповое согласование * с этим пиром, если транспорт уже установлен. Новый ETCP-коннект не требуется. * * ОШИБКИ, ПОВТОРЫ И ОТМЕНА: * - Неверный/истёкший ключ, несовпадение идентичности или неверная подпись * не создают мембера. Ошибка/таймаут не дают временных прав. * - Повторная запись проходит обычную проверку подписей и версий member_sync; * повтор не создаёт второго мембера или второго владельца групповой сессии. * - Ответ относится к ожидаемым каналу и пиру; ответ другой попытки не должен * завершать текущее ожидание. UP другого пира не меняет адресата приглашения. * - Отмена ожидания не откатывает уже сохранённую/реплицированную запись. * Удаление локального канала отменяет его ожидания и подключения. * - Recovery восстанавливает сессии существующих участников и не использует * invite-ключ как замену локальной таблицы участников. * * J <-> C: ETCP_RT_ID_CHAT_SYNC, заголовок [service:1][group_id:8][type:1], payload * описан в chat_sync.h. A <-> C: ETCP_RT_ID_JOIN через etcp_router с подписью * и шифрованием; subcommands и payload описаны ниже. * * Модуль отвечает только за маршрутизируемую (etcp_router) часть: * - регистрация ключа (инвайтер → connection), * - форвард join-запроса (connection → инвайтер), * - обработка на инвайтере: верификация + добавление мембера + подпись дерева. * Прямой обмен (джойнер ↔ connection, JOIN_INFO_REQ/RESP, JOIN_REQUEST, JOIN_READY) * и pending-трекинг готовности — в chat_sync.c. */ #ifndef CHAT_JOIN_H #define CHAT_JOIN_H #include #include #include "member_sync.h" struct UTUN_INSTANCE; #ifdef __cplusplus extern "C" { #endif /* etcp_router service id (connection ↔ инвайтер): ETCP_RT_ID_JOIN (см. etcp_api.h) */ /* subcommands (первый байт payload etcp_router) */ #define JOIN_SUBCMD_KEY_REGISTER 0x01 /* инвайтер → connection: {join_key:8} */ #define JOIN_SUBCMD_REQUEST_FWD 0x02 /* connection → инвайтер: {join_key:8, member...} */ #define JOIN_SUBCMD_KEY_REGISTER_ACK 0x03 /* connection → инвайтер: {join_key:8, status:1}; 0=stored, 1=rejected */ #define JOIN_KEY_TTL_SECONDS 600 /* 10 минут хранения ключа на connection-узле */ #define JOIN_REGISTER_TIMEOUT_TB 50000 /* 5 секунд на подтверждение */ enum chat_join_result { CHAT_JOIN_OK = 0, CHAT_JOIN_ERROR = -1, CHAT_JOIN_TIMEOUT = -2, CHAT_JOIN_CANCELLED = -3 }; struct chat_join_registration; typedef void (*chat_join_registered_fn)(void* arg, int result); /* Полный набор мембера джойнера (без подписи дерева — её ставит инвайтер). */ struct join_member_data { uint64_t node_id; /* derive(x25519), идентичность нового участника */ const uint8_t* x25519; /* 32 */ const uint8_t* ed25519; /* 32 */ const uint8_t* join_sig; /* 64, может быть NULL */ uint64_t join_ts; /* время подписи вступления, Unix seconds */ const uint8_t* update_sig; /* 64, может быть NULL */ uint64_t update_ts; /* версия подписанного блока участника */ char userinfo[MS_USERINFO_MAX + 1]; /* JSON, null-terminated */ }; /* Сериализовать member-блок (без join_key). Возвращает 0 и *out_len, -1 если не влез. */ int chat_join_serialize_member(const struct join_member_data* m, uint8_t* out, size_t out_sz, size_t* out_len); /* Разобрать member-блок. Указатели ссылаются на data (не копируют). 0=ок, -1=ошибка. */ int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_data* m); /* Инициализация: etcp_router_bind(ETCP_RT_ID_JOIN). */ int chat_join_init(struct UTUN_INSTANCE* inst); /* Снять обработчик, освободить ключи; ожидающие регистрации получают CANCELLED. */ void chat_join_destroy(struct UTUN_INSTANCE* inst); /* Локальное приглашение (инвайтер == connection). 0=stored, -1=ошибка. */ int chat_join_store_local_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key); /* Все вызовы в uasync-потоке. NULL=ошибка запуска, callback не вызывается. * Иначе callback ровно один раз, после возврата register_key (также для self). * Handle действителен до callback. cancel/destroy вызывают callback синхронно с CANCELLED. */ struct chat_join_registration* chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id, uint64_t join_key, chat_join_registered_fn callback, void* arg); void chat_join_cancel_registration(struct chat_join_registration* registration); /* Connection-узел: найти инвайтера по ключу. 0 = нет/истёк. */ uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key); /* Connection-узел: форварднуть join-запрос инвайтеру по etcp_router. 0=ок, -1=ошибка. */ int chat_join_forward_request(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t inviter_id, uint64_t join_key, const struct join_member_data* m); /* Инвайтер (локально, когда connection==инвайтер, или из recv REQUEST_FWD): * верификация join_sig + добавление мембера + подпись дерева. */ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t from_node, uint64_t join_key, const uint8_t* member_data, size_t member_len); #ifdef __cplusplus } #endif #endif /* CHAT_JOIN_H */