diff --git a/src/radio/radio_call.c b/src/radio/radio_call.c index b77c61c6..3a4c5324 100644 --- a/src/radio/radio_call.c +++ b/src/radio/radio_call.c @@ -1,6 +1,6 @@ /* Короткоживущие приглашения в рацию; один pending, ограниченный кэш результатов. - * Повторы нужны также после смены router epoch. Один таймер владеет ближайшим - * всеми deadline; callback не хранит указателей на освобождаемые сессии. */ + * Повторы нужны также после смены router epoch. Один таймер обслуживает ближайший + * deadline; callback не хранит указателей на освобождаемые сессии. */ #include #include "radio_call.h" #include "radio.h" @@ -205,10 +205,10 @@ static void rc_receive(struct ETCP_CONN* conn, struct ll_entry* entry) { struct radio_call_session* s = rc_slot(ctx, id, group, peer, 0); if (command == RC_INVITE && !s) { int64_t expires; memcpy(&expires, b + 9, 8); - int64_t remaining = expires - ntp_time_get_us(ctx->inst); - if (remaining <= 0 || remaining > 4000000 || !rc_group(ctx->inst, group, peer)) { - DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio call stale/invalid invite id=%016llx remaining_us=%lld", - (unsigned long long)id, (long long)remaining); goto done; + int64_t now_us = ntp_time_get_us(ctx->inst); + if (expires <= now_us || expires > now_us + 4000000 || !rc_group(ctx->inst, group, peer)) { + DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio call stale/invalid invite id=%016llx expires_us=%lld now_us=%lld", + (unsigned long long)id, (long long)expires, (long long)now_us); goto done; } int busy = radio_call_busy(ctx->inst) || call_busy(ctx->inst); s = rc_slot(ctx, id, group, peer, 1); diff --git a/tests/test_radio.c b/tests/test_radio.c index 49a5fbad..93240fae 100644 --- a/tests/test_radio.c +++ b/tests/test_radio.c @@ -10,6 +10,8 @@ // кадры в порядке seq 0..4 и FIN. // 3. C radio_off (обрезание) → A снова говорит → B получает, C не получает. // 4. Без FIN: C снимает индикацию при radio_off, B — по таймауту источника. +// До передачи: вызовы через B, deadline 2с/60с, потери RESULT/ACK, отказ/отмена/занятость, +// отсутствие живого BGP-пути, некорректный срок INVITE и отсутствие таймеров рации в покое. #include "chat_core.h" #include "chat_sync.h" @@ -132,6 +134,16 @@ static void radio_action(struct UTUN_INSTANCE* inst, uint64_t id, int action, ui radio_call_execute(&req); } +static int radio_invitation_timers(struct UASYNC* ua) { + int count = 0; + for (size_t i = 0; i < ua->timeout_heap->size; ++i) { + TimeoutEntry* entry = &ua->timeout_heap->heap[i]; + struct timeout_node* node = entry->data; + if (!entry->deleted && node && node->name && !strcmp(node->name, "radio_call")) ++count; + } + return count; +} + /* Проверяем именно UI-события: self, прямой пир и пир за посредником. */ static void member_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { if (type == CHAT_EVT_RADIO_CALL) { @@ -456,6 +468,27 @@ static void rad_tick(void* arg) { case 0: { uint8_t key[32]; if (member_sync_verified_ed25519(A, CH_ID, nc, key) || member_sync_verified_ed25519(C, CH_ID, na, key)) break; + if (radio_invitation_timers(t->ua)) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test idle radio invitation service has timers"); + t->result = 2; uasync_stop(t->ua); return; + } + /* Уже расшифрованный пакет от проверенного пира: даже INT64_MIN обязан безопасно отбрасываться. */ + struct ll_entry* expired = ll_alloc_lldgram(ROUTER_SVC_HDR_SIZE + 18); + if (!expired) abort(); + memset(expired->dgram, 0, expired->len); + expired->dgram[0] = ETCP_RT_ID_RADIO_CALL; + memcpy(expired->dgram + ROUTER_SVC_SRC_OFF, &nc, 8); memcpy(expired->dgram + ROUTER_SVC_DST_OFF, &na, 8); + memcpy(expired->dgram + ROUTER_SVC_GROUP_OFF, &gid, 8); + expired->dgram[ROUTER_SVC_FLAGS_OFF] = ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED; + expired->dgram[ROUTER_SVC_PAYLOAD_OFF] = 1; + uint64_t expired_id = 108; int64_t expired_at = INT64_MIN; + memcpy(expired->dgram + ROUTER_SVC_PAYLOAD_OFF + 1, &expired_id, 8); + memcpy(expired->dgram + ROUTER_SVC_PAYLOAD_OFF + 9, &expired_at, 8); + struct ETCP_CONN source = {.instance = A}; + A->router_bindings.callbacks[ETCP_RT_ID_RADIO_CALL](&source, expired); + if (radio_call_busy(A) || g_member_event_bad) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test malformed invite created a call"); t->result = 2; uasync_stop(t->ua); return; + } struct TOPO_GROUP* cg = topo_groups_find(C->topo_groups, gid); struct ETCP_CONN* path = topo_group_find_conn_for_node(cg, na); if (!path) break; @@ -544,7 +577,16 @@ static void rad_tick(void* arg) { t->result = 2; uasync_stop(t->ua); return; } } - DEBUG_INFO(DEBUG_CATEGORY_RADIO, "test radio calls passed: 2s/60s deadlines, accept, decline, cancel, busy, lost result/ACK"); + t->call_started = now; ++t->call_step; + } + break; + case 9: + if (now - t->call_started >= 5000) { + if (radio_invitation_timers(t->ua) || radio_call_busy(A) || radio_call_busy(B) || radio_call_busy(C)) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test finished invitations retain timers or busy state"); + t->result = 2; uasync_stop(t->ua); return; + } + DEBUG_INFO(DEBUG_CATEGORY_RADIO, "test radio calls passed: 2s/60s, accept/decline/cancel/busy, lost result/ACK, idle has no timer"); t->phase = P_RADIO_ON; } break;