From 305b61ccab8791b39f76e5966464a8409db203df Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 17 Oct 2026 23:43:21 +0200 Subject: [PATCH] fix(nat): advance after an unfinished pair uses its full retry budget --- doc/nat_detection.txt | 2 ++ src/routing_layer/nat_detection.c | 9 ++++++++- tests/test_nat_detection_faults.c | 7 +++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/doc/nat_detection.txt b/doc/nat_detection.txt index cba89420..8e2316d9 100644 --- a/doc/nat_detection.txt +++ b/doc/nat_detection.txt @@ -81,6 +81,8 @@ IP:порт тестового сокета на том же локальном через остальных помощников. Порядок обхода фиксирован на его начало; фактический source всегда проверяется по актуальной истории. При паузе по общему бюджету незавершённая попытка повторяется с новыми идентификаторами, без сброса обхода. +Если попытка уже получила полный бюджет, его исчерпание считается таймаутом: +обход переходит дальше, а не повторяет одного помощника бесконечно. Старый сохранённый результат входящих не считается успехом новой проверки. Соединения по LAN работают независимо и не сбрасывают интернет-профиль. diff --git a/src/routing_layer/nat_detection.c b/src/routing_layer/nat_detection.c index bdad12f6..047252fb 100644 --- a/src/routing_layer/nat_detection.c +++ b/src/routing_layer/nat_detection.c @@ -70,7 +70,7 @@ struct nat_socket { uint8_t contacts[NAT_MAX_CONTACTS][17]; unsigned contact_count, contact_cutoff, cycle_contact_cutoff, phase, reason, retries; uint8_t history_loaded, history_complete, history_incomplete, filter_clean, pre_received, post_received, pre_healthy; - uint8_t cycle_history_complete, cycle_mapping_seen; + uint8_t cycle_history_complete, cycle_mapping_seen, pair_full_budget; uint8_t helper_socket, active_ping, heartbeat_active, forced, cycling; uint32_t rounds, helper_reselections; }; @@ -641,6 +641,11 @@ static void remember_address(struct nat_socket *state, const struct sockaddr_sto } static void owner_finish(struct nat_socket *state, uint8_t reason) { + if (reason == NAT_REASON_BUDGET && state->pair_full_budget) { + reason = NAT_REASON_TIMEOUT; + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT pair full budget exhausted socket=%s round=%016llx; advancing candidate", + state->socket->name, (unsigned long long)state->round); + } /* Неудача входящего испытания не мешает сравнить mapping через уже известный публичный endpoint C. */ if ((reason == NAT_REASON_TIMEOUT || reason == NAT_REASON_CONTROL_LOST || reason == NAT_REASON_SEND_ERROR || reason == NAT_REASON_CONTACT_HISTORY) && @@ -1279,6 +1284,7 @@ static void owner_start(struct nat_socket *state) { } uint64_t now = get_time_tb(); history_load(state); + int full_budget = !state->cycling || now >= state->cycle_deadline; if (!state->cycling) { state->cycling = 1; state->cursor = (struct nat_pair){0}; state->cycle_contact_cutoff = state->contact_count; state->cycle_history_complete = state->history_complete; @@ -1290,6 +1296,7 @@ static void owner_start(struct nat_socket *state) { state->socket->name, state->cycle_contact_cutoff, state->contact_count); } if (select_pair(state)) { owner_finish(state, state->reason); return; } + state->pair_full_budget = full_budget; if (random_id(&state->round)) { owner_finish(state, NAT_REASON_SEND_ERROR); return; } state->rounds++; memset(&state->helper_seen, 0, sizeof(state->helper_seen)); memset(&state->baseline, 0, sizeof(state->baseline)); diff --git a/tests/test_nat_detection_faults.c b/tests/test_nat_detection_faults.c index 4f113fc8..2051f0d1 100644 --- a/tests/test_nat_detection_faults.c +++ b/tests/test_nat_detection_faults.c @@ -425,15 +425,18 @@ static void run_budget_resume(void) { assert(!nat_detection_get_status(owner->nat_det, fault.socket, &status)); assert(!status.phase && status.cycling && status.round == paused_round && status.helper_reselections == completed_pairs); assert(fault.socket->address_profile.inbound == ADDRESS_IN_UNKNOWN); - fault.mode = FAULT_NONE; fault.delayed_length = 0; end = get_time_tb() + 700000; + fault.delayed_length = 0; end = get_time_tb() + 1300000; do { test_clock_poll(fixture.ua, 1000); assert(!nat_detection_get_status(owner->nat_det, fault.socket, &status)); } while (get_time_tb() < end && status.round == paused_round); assert(status.round != paused_round && status.helper == paused_helper && status.helper_reselections == completed_pairs); + uint32_t resumed_rounds = status.rounds; + /* Полный бюджет уже дан этой паре: следующий предел завершает её, а не запускает бесконечные повторы. */ do { test_clock_poll(fixture.ua, 1000); assert(!nat_detection_get_status(owner->nat_det, fault.socket, &status)); } while (get_time_tb() < end && status.cycling); assert(!status.phase && !status.cycling && status.round != paused_round && status.helper == paused_helper); + assert(status.rounds == resumed_rounds); assert(status.helper_reselections == completed_pairs + 1 && fault.socket->address_profile.mapping == ADDRESS_MAP_UNCHANGED && - fault.socket->address_profile.inbound == ADDRESS_IN_UNKNOWN && status.reason == NAT_REASON_CONTACT_HISTORY); + fault.socket->address_profile.inbound == ADDRESS_IN_UNKNOWN && status.reason == NAT_REASON_TIMEOUT); fault.fixture = NULL; nat_fixture_cleanup(&fixture); DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS NAT budget pause retains mapping, rejects late ingress and retries the unfinished pair"); }