diff --git a/src/routing_layer/nat_detection.c b/src/routing_layer/nat_detection.c index e00f4c72..48bfc789 100644 --- a/src/routing_layer/nat_detection.c +++ b/src/routing_layer/nat_detection.c @@ -66,10 +66,11 @@ struct nat_socket { uint8_t observer_key[32], helper_key[32]; uint64_t group_id, round, observer, helper, token, observation_token, deadline, due, completed_tb, preferred_observer; uint64_t cycle_deadline, heartbeat_due, heartbeat_received, pre_started; - struct nat_pair cursor; + struct nat_pair cursor, current_pair; /* Курсор продвигается только после завершённой попытки. */ uint8_t contacts[NAT_MAX_CONTACTS][17]; - unsigned contact_count, contact_cutoff, phase, reason, retries; + 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 helper_socket, active_ping, heartbeat_active, forced, cycling; uint32_t rounds, helper_reselections; }; @@ -143,7 +144,8 @@ static int random_id(uint64_t *value) { const char *nat_check_reason_name(uint8_t reason) { static const char *const names[] = {"none", "no_observer", "no_helper", "control_lost", "send_error", "timeout", - "address_changed", "contact_history", "proxy", "no_listener", "cancelled", "invalid_observation", "dependent_mapping"}; + "address_changed", "contact_history", "proxy", "no_listener", "cancelled", "invalid_observation", "dependent_mapping", + "cycle_budget"}; return reason < sizeof(names) / sizeof(names[0]) ? names[reason] : "invalid_reason"; } @@ -274,8 +276,11 @@ static int select_pair(struct nat_socket *state) { for (; more_c; more_c = node_udp_endpoint(detector, cn, state->local.ss_family, 1, previous_c, &ca, &cs)) { if (route_ping_addr_encode(previous_c, &ca)) break; + int old_contact = !state->cycle_history_complete; + for (unsigned i = 0; i < state->cycle_contact_cutoff; i++) + if (!memcmp(state->contacts[i], previous_c, 17)) { old_contact = 1; break; } struct nat_pair candidate = {.group = group->group_id, .observer = observer, .helper = helper, - .helper_priority = 2 * !public_address(detector, &ca) + contacted(state, &ca)}; + .helper_priority = 2 * !public_address(detector, &ca) + old_contact}; memcpy(candidate.observer_endpoint, previous_b, sizeof(previous_b)); memcpy(candidate.helper_endpoint, previous_c, sizeof(previous_c)); if (!pair_after(candidate, state->cursor)) continue; @@ -287,16 +292,17 @@ static int select_pair(struct nat_socket *state) { } } } - if (!best.group && single.group) { + if (!best.group && single.group && !state->cycle_mapping_seen) { best = single; observer_address = single_address; memcpy(observer_key, single_key, 32); } if (!best.group) { - state->reason = observers ? NAT_REASON_NO_HELPER : NAT_REASON_NO_OBSERVER; + if (!observers || !state->cursor.group || !state->reason) + state->reason = observers ? NAT_REASON_NO_HELPER : NAT_REASON_NO_OBSERVER; DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT candidate cycle exhausted socket=%s groups=%u observers=%u reason=%s", state->socket->name, groups, observers, nat_check_reason_name(state->reason)); return -1; } - state->cursor = best; + state->current_pair = best; state->group_id = best.group; state->observer = best.observer; state->helper = best.helper; state->observer_address = observer_address; state->helper_address = helper_address; state->helper_socket = helper_socket; memcpy(state->observer_key, observer_key, 32); memcpy(state->helper_key, helper_key, 32); @@ -650,20 +656,25 @@ static void owner_finish(struct nat_socket *state, uint8_t reason) { if (owner_observe(state)) owner_finish(state, NAT_REASON_SEND_ERROR); return; } + uint64_t now = get_time_tb(); int next_pair = reason && state->phase && state->cycling && reason != NAT_REASON_CANCELLED && reason != NAT_REASON_ADDRESS_CHANGED; etcp_cancel_pings(state->detector->instance, owner_ping_cb, state); etcp_cancel_pings(state->detector->instance, owner_heartbeat_cb, state); - if (next_pair && get_time_tb() >= state->cycle_deadline) { - next_pair = 0; - DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT candidate cycle budget exhausted socket=%s round=%016llx", - state->socket->name, (unsigned long long)state->round); - } + int pause = next_pair && now >= state->cycle_deadline; + if (next_pair && reason != NAT_REASON_BUDGET) state->cursor = state->current_pair; state->active_ping = 0; state->heartbeat_active = 0; state->phase = NAT_IDLE; state->reason = reason; if (next_pair) { - state->forced = 1; state->due = get_time_tb() + 1000; state->helper_reselections++; + state->forced = 1; state->due = now + (pause ? NAT_RETRY_TB : 1000); + if (reason != NAT_REASON_BUDGET) state->helper_reselections++; + if (pause) DEBUG_INFO(DEBUG_CATEGORY_NAT, + "NAT candidate cycle paused socket=%s round=%016llx retry_current=%d resume_in_tb=%llu mapping=%s inbound=%s", + state->socket->name, (unsigned long long)state->round, reason == NAT_REASON_BUDGET, + (unsigned long long)NAT_RETRY_TB, address_mapping_name(state->measured_profile.mapping), + address_inbound_name(state->measured_profile.inbound)); } else { state->cycling = 0; state->preferred_observer = 0; state->cursor = (struct nat_pair){0}; + state->current_pair = (struct nat_pair){0}; int complete = state->measured_profile.mapping == ADDRESS_MAP_DEPENDENT || (state->measured_profile.mapping && state->measured_profile.inbound); state->due = get_time_tb() + (reason == NAT_REASON_ADDRESS_CHANGED ? 2500 : complete ? NAT_REFRESH_TB : NAT_RETRY_TB); @@ -688,6 +699,7 @@ static void owner_apply(struct nat_socket *state, uint8_t mapping, uint8_t inbou nat_check_reason_name(partial_reason)); } state->measured_profile = profile; state->measured_address = state->baseline; + state->cycle_mapping_seen = 1; state->completed_tb = get_time_tb(); DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT profile socket=%s id=%u generation=%016llx round=%016llx mapping=%s inbound=%s effective_inbound=%s" " local=%s external=%s observer=%016llx helper=%016llx clean=%u partial_reason=%s", @@ -697,8 +709,9 @@ static void owner_apply(struct nat_socket *state, uint8_t mapping, uint8_t inbou sockaddr_storage_to_str(&state->local).str, sockaddr_storage_to_str(&state->baseline).str, (unsigned long long)state->observer, (unsigned long long)state->helper, state->filter_clean, nat_check_reason_name(partial_reason)); - owner_finish(state, NAT_REASON_NONE); state->reason = partial_reason; - if (!profile.inbound && mapping != ADDRESS_MAP_DEPENDENT) state->due = get_time_tb() + NAT_RETRY_TB; + int complete = inbound != ADDRESS_IN_UNKNOWN || mapping == ADDRESS_MAP_DEPENDENT; + owner_finish(state, complete ? NAT_REASON_NONE : partial_reason); + if (complete) state->reason = partial_reason; } static int owner_run(struct nat_socket *state, uint8_t step) { @@ -740,6 +753,7 @@ static void owner_heartbeat_cb(int success, uint16_t rtt, void *arg, uint64_t no (void)rtt; (void)nonce; struct nat_socket *state = arg; state->heartbeat_active = 0; if (!state->phase) return; + if (get_time_tb() >= state->cycle_deadline) { owner_finish(state, NAT_REASON_BUDGET); return; } struct sockaddr_storage observed; if (success && !route_ping_observation(data, length, state->socket, state->group_id, state->round, &observed) && public_address(state->detector, &observed)) { @@ -762,6 +776,7 @@ static void owner_ping_cb(int success, uint16_t rtt, void *arg, uint64_t nonce, struct nat_socket *state = arg; state->active_ping = 0; if (state->phase == NAT_IDLE) return; + if (get_time_tb() >= state->cycle_deadline) { owner_finish(state, NAT_REASON_BUDGET); return; } struct sockaddr_storage observed; if (!success || route_ping_observation(data, length, state->socket, state->group_id, state->round, &observed)) { DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT observation incomplete socket=%s phase=%u round=%016llx attempt=%u reason=%s", @@ -842,6 +857,7 @@ void nat_detection_receive_ingress(struct NAT_DETECTION *detector, struct ETCP_S DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT ingress correlation/source mismatch socket=%s phase=%u round=%016llx source=%s", socket->name, state->phase, (unsigned long long)state->round, sockaddr_storage_to_str(source).str); return; } + if (get_time_tb() >= state->cycle_deadline) { owner_finish(state, NAT_REASON_BUDGET); return; } if (!public_address(detector, source)) { DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT helper rejected by receiver socket=%s round=%016llx helper=%016llx source=%s reason=non_public_source", socket->name, (unsigned long long)state->round, (unsigned long long)state->helper, sockaddr_storage_to_str(source).str); @@ -1081,6 +1097,7 @@ static void handle_result(struct nat_socket *state, const struct nat_wire *messa if (!step || message->step != step || read64(message->token) != state->token || read64(message->helper) != state->helper) { DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT stale helper result ignored socket=%s phase=%u", state->socket->name, state->phase); return; } + if (get_time_tb() >= state->cycle_deadline) { owner_finish(state, NAT_REASON_BUDGET); return; } if (message->reason || message->errors || !message->sent || message->received > message->sent) { owner_finish(state, message->reason ? message->reason : NAT_REASON_SEND_ERROR); return; } @@ -1150,6 +1167,7 @@ static void receive_observation(struct NAT_DETECTION *detector, uint64_t group, DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT stale/foreign observation report ignored group=%016llx sender=%016llx step=%u", (unsigned long long)group, (unsigned long long)sender, message->step); return; } + if (get_time_tb() >= state->cycle_deadline) { owner_finish(state, NAT_REASON_BUDGET); return; } etcp_cancel_pings(detector->instance, owner_ping_cb, state); if (message->reason) { owner_finish(state, message->reason); return; } owner_ping_cb(1, 0, state, 0, data, sizeof(data)); @@ -1259,14 +1277,21 @@ static void owner_start(struct nat_socket *state) { if (state->socket->type == CFG_SERVER_TYPE_LOCAL || state->socket->type == CFG_SERVER_TYPE_PRIVATE) { state->due = UINT64_MAX; return; } + uint64_t now = get_time_tb(); + history_load(state); if (!state->cycling) { state->cycling = 1; state->cursor = (struct nat_pair){0}; - state->cycle_deadline = get_time_tb() + NAT_ROUND_TB; + state->cycle_contact_cutoff = state->contact_count; state->cycle_history_complete = state->history_complete; + state->cycle_mapping_seen = 0; state->reason = NAT_REASON_NONE; + state->cycle_deadline = now + NAT_ROUND_TB; + } else if (now >= state->cycle_deadline) { + state->cycle_deadline = now + NAT_ROUND_TB; + DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT candidate cycle resumed socket=%s contacts_at_start=%u current_contacts=%u", + state->socket->name, state->cycle_contact_cutoff, state->contact_count); } - history_load(state); + if (select_pair(state)) { owner_finish(state, state->reason); return; } if (random_id(&state->round)) { owner_finish(state, NAT_REASON_SEND_ERROR); return; } state->rounds++; - if (select_pair(state)) { owner_finish(state, state->reason); return; } memset(&state->helper_seen, 0, sizeof(state->helper_seen)); memset(&state->baseline, 0, sizeof(state->baseline)); state->retries = 0; state->pre_received = 0; state->post_received = 0; state->filter_clean = 1; state->pre_healthy = 0; state->heartbeat_due = get_time_tb(); state->heartbeat_received = 0; state->pre_started = 0; @@ -1291,7 +1316,7 @@ static void detector_tick(void *arg) { DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT phase deadline socket=%s round=%016llx phase=%u cycle_remaining_tb=%llu", state->socket->name, (unsigned long long)state->round, state->phase, (unsigned long long)(state->cycle_deadline > now ? state->cycle_deadline - now : 0)); - owner_finish(state, NAT_REASON_TIMEOUT); continue; + owner_finish(state, now >= state->cycle_deadline ? NAT_REASON_BUDGET : NAT_REASON_TIMEOUT); continue; } if (!state->phase && now >= state->due && (detector->automatic || state->forced)) { state->forced = 0; owner_start(state); diff --git a/src/routing_layer/nat_detection.h b/src/routing_layer/nat_detection.h index 9e7b78b7..0f6e89d7 100644 --- a/src/routing_layer/nat_detection.h +++ b/src/routing_layer/nat_detection.h @@ -50,7 +50,8 @@ enum nat_check_reason { NAT_REASON_NO_LISTENER, /* Нет публичного TCP-listener для проверки входящих. */ NAT_REASON_CANCELLED, /* Проверка отменена. */ NAT_REASON_INVALID_OBSERVATION, /* Наблюдение непригодно, в том числе из-за непубличного source IP. */ - NAT_REASON_DEPENDENT_MAPPING /* Определён strict, входящая доступность осталась неизвестной. */ + NAT_REASON_DEPENDENT_MAPPING, /* Определён strict, входящая доступность осталась неизвестной. */ + NAT_REASON_BUDGET /* Локальная пауза по общему бюджету: текущая попытка будет повторена. */ }; /* Снимок состояния одного сокета для диагностики; получение снимка не запускает проверку. */ @@ -67,7 +68,7 @@ struct nat_check_status { uint8_t phase; /* 0 — ожидание; иначе внутренний номер текущего этапа. */ uint8_t reason; /* Причина завершения или неполного результата: enum nat_check_reason. */ uint8_t history_complete; /* 1 — история исходящих IP загружена без известных потерь. */ - uint8_t cycling; /* 1 — идёт цикл проверки с перебором кандидатов. */ + uint8_t cycling; /* 1 — идёт обход кандидатов, включая паузу по бюджету; phase=0 во время паузы. */ }; /* Создать детектор для instance с готовым маршрутизатором и группами; автоматические проверки включены.