From 83efa229ed34d3ec529e19163d40a47429d323f2 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 17 Oct 2026 16:54:07 +0200 Subject: [PATCH] nat: reject local probe sources and preserve internet profiles on LAN --- src/routing_layer/nat_detection.c | 53 ++++++++++++++++------ tests/Makefile.am | 3 +- tests/test_nat_detection_faults.c | 74 +++++++++++++++++++++++++++++-- 3 files changed, 112 insertions(+), 18 deletions(-) diff --git a/src/routing_layer/nat_detection.c b/src/routing_layer/nat_detection.c index a5d6e7f8..d3d6a82e 100644 --- a/src/routing_layer/nat_detection.c +++ b/src/routing_layer/nat_detection.c @@ -517,6 +517,21 @@ void nat_detection_record_observation(struct NAT_DETECTION *detector, struct ETC DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT observation has invalid group/identity group=%016llx owner=%016llx", (unsigned long long)group_id, (unsigned long long)owner); return; } + int eligible = public_address(detector, source); + if (!eligible) DEBUG_WARN(DEBUG_CATEGORY_NAT, + "NAT observation rejected by receiver group=%016llx owner=%016llx round=%016llx source=%s reason=non_public_source", + (unsigned long long)group_id, (unsigned long long)owner, (unsigned long long)round, sockaddr_storage_to_str(source).str); + if (length == NAT_QUERY_TAG_SIZE) { + struct nat_wire report = {.command = NAT_OBSERVATION, .owner_socket = tag[8], .step = tag[ROUTE_OBSERVE_TAG_SIZE], + .reason = eligible ? NAT_REASON_NONE : NAT_REASON_INVALID_OBSERVATION}; + write64(report.owner, owner); write64(report.generation, generation); write64(report.round, round); + write64(report.token, read64(tag + ROUTE_OBSERVE_TAG_SIZE + 1)); + write64(report.observer, detector->instance->node_id); write64(report.helper, detector->instance->node_id); + if (route_ping_addr_encode(report.target, source) || send_control(detector, group_id, owner, &report)) + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT routed observation delivery failed group=%016llx owner=%016llx round=%016llx", + (unsigned long long)group_id, (unsigned long long)owner, (unsigned long long)round); + } + if (!eligible) return; struct nat_binding *binding = binding_find(detector, group_id, owner, generation, round); if (!binding) { unsigned count = 0; @@ -532,15 +547,6 @@ void nat_detection_record_observation(struct NAT_DETECTION *detector, struct ETC DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT observation group=%016llx owner=%016llx socket=%u generation=%016llx round=%016llx observed=%s via=%s", (unsigned long long)group_id, (unsigned long long)owner, tag[8], (unsigned long long)generation, (unsigned long long)round, sockaddr_storage_to_str(source).str, socket->name); - if (length == NAT_QUERY_TAG_SIZE) { - struct nat_wire report = {.command = NAT_OBSERVATION, .owner_socket = tag[8], .step = tag[ROUTE_OBSERVE_TAG_SIZE]}; - write64(report.owner, owner); write64(report.generation, generation); write64(report.round, round); - write64(report.token, read64(tag + ROUTE_OBSERVE_TAG_SIZE + 1)); - write64(report.observer, detector->instance->node_id); write64(report.helper, detector->instance->node_id); - if (route_ping_addr_encode(report.target, source) || send_control(detector, group_id, owner, &report)) - DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT routed observation delivery failed group=%016llx owner=%016llx round=%016llx", - (unsigned long long)group_id, (unsigned long long)owner, (unsigned long long)round); - } } static void clear_profile(struct nat_socket *state, uint8_t reason) { @@ -669,7 +675,8 @@ static void owner_heartbeat_cb(int success, uint16_t rtt, void *arg, uint64_t no struct nat_socket *state = arg; state->heartbeat_active = 0; if (!state->phase) return; struct sockaddr_storage observed; - if (success && !route_ping_observation(data, length, state->socket, state->group_id, state->round, &observed)) { + if (success && !route_ping_observation(data, length, state->socket, state->group_id, state->round, &observed) && + public_address(state->detector, &observed)) { if (state->baseline.ss_family && !route_ping_addr_equal(&state->baseline, &observed, 1)) { DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT anchor mapping changed socket=%s round=%016llx previous=%s current=%s", state->socket->name, (unsigned long long)state->round, @@ -697,6 +704,11 @@ static void owner_ping_cb(int success, uint16_t rtt, void *arg, uint64_t nonce, if (++state->retries < NAT_PROBE_COUNT) { state->due = get_time_tb() + 1000; return; } owner_finish(state, success ? NAT_REASON_INVALID_OBSERVATION : NAT_REASON_TIMEOUT); return; } + if (!public_address(state->detector, &observed)) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT observation unusable socket=%s phase=%u round=%016llx observed=%s reason=non_public_source", + state->socket->name, state->phase, (unsigned long long)state->round, sockaddr_storage_to_str(&observed).str); + owner_finish(state, NAT_REASON_INVALID_OBSERVATION); return; + } state->retries = 0; DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT own observation socket=%s phase=%u round=%016llx observed=%s", state->socket->name, state->phase, (unsigned long long)state->round, sockaddr_storage_to_str(&observed).str); @@ -757,11 +769,20 @@ void nat_detection_receive_ingress(struct NAT_DETECTION *detector, struct ETCP_S if (!step || memcmp(tag, expected, sizeof(expected)) || tag[ROUTE_OBSERVE_TAG_SIZE] != step || read64(tag + ROUTE_OBSERVE_TAG_SIZE + 1) != state->token || sc_derive_node_id_from_pubkey(peer_key) != state->helper || memcmp(peer_key, state->helper_key, 32) || - source->ss_family != state->local.ss_family || !public_address(detector, source) || - ((step == NAT_STEP_POST || state->pre_received) && !route_ping_addr_equal(source, &state->helper_address, 1))) { + source->ss_family != state->local.ss_family) { 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 (!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); + owner_finish(state, NAT_REASON_NO_HELPER); return; + } + if ((step == NAT_STEP_POST || state->pre_received) && !route_ping_addr_equal(source, &state->helper_address, 1)) { + DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT ingress endpoint mismatch socket=%s phase=%u round=%016llx source=%s expected=%s", + socket->name, state->phase, (unsigned long long)state->round, + sockaddr_storage_to_str(source).str, sockaddr_storage_to_str(&state->helper_address).str); return; + } if (route_ping_addr_equal(source, &state->observer_address, 0) || route_ping_addr_equal(source, &state->baseline, 0)) { DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT helper rejected by receiver socket=%s round=%016llx helper=%016llx source=%s reason=%s", socket->name, (unsigned long long)state->round, (unsigned long long)state->helper, @@ -859,7 +880,7 @@ static void helper_ping_cb(int success, uint16_t rtt, void *arg, uint64_t nonce, struct ETCP_SOCKET *socket = find_socket(probe->detector, probe->io_socket, probe->io_generation); struct sockaddr_storage mapped; if (success && socket && !route_ping_observation(data, length, socket, probe->group_id, - read64(probe->message.round), &mapped)) { + read64(probe->message.round), &mapped) && public_address(probe->detector, &mapped)) { if (route_ping_addr_encode(probe->message.primary, &mapped)) { helper_finish(probe, NAT_REASON_INVALID_OBSERVATION); return; } probe->message.received++; DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT helper endpoint measured group=%016llx owner=%016llx step=%u socket=%s mapped=%s fallback=%u", @@ -1089,6 +1110,7 @@ static void receive_observation(struct NAT_DETECTION *detector, uint64_t group, probe->io_generation != socket->generation || read64(probe->message.round) != read64(message->round) || read64(probe->message.token) != read64(message->token) || read64(probe->message.observer) != sender) continue; etcp_cancel_pings(detector->instance, helper_ping_cb, probe); + if (message->reason) { helper_finish(probe, message->reason); return; } helper_ping_cb(1, 0, probe, 0, data, sizeof(data)); return; } DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT stale helper observation report ignored"); return; @@ -1101,6 +1123,7 @@ static void receive_observation(struct NAT_DETECTION *detector, uint64_t group, (unsigned long long)group, (unsigned long long)sender, message->step); 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)); } @@ -1514,6 +1537,10 @@ void nat_detection_link_ready(struct NAT_DETECTION *detector, struct ETCP_LINK * void nat_detection_observed_by_peer(struct NAT_DETECTION *detector, struct ETCP_LINK *link, const struct sockaddr_storage *observed) { if (!detector || !link || !link->conn || link->is_tcp || link->conn->socks_udp || !observed->ss_family) return; + if (!public_address(detector, observed)) { + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT ignores LAN observation socket=%s peer=%016llx observed=%s", + link->conn->name, (unsigned long long)link->etcp->peer_node_id, sockaddr_storage_to_str(observed).str); return; + } struct nat_socket *state = socket_state(detector, link->conn, 1); if (!state) return; if ((state->measured_profile.mapping == ADDRESS_MAP_UNCHANGED && diff --git a/tests/Makefile.am b/tests/Makefile.am index b3c53d9e..45baf7be 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -450,7 +450,8 @@ check_PROGRAMS += test_nat_detection_faults test_nat_detection_faults_SOURCES = test_nat_detection_faults.c $(test_clock_sources) test_nat_detection_faults_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_nat_detection_faults_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) -test_nat_detection_faults_LDFLAGS = $(test_clock_ldflags) -Wl,--wrap=socket_sendto -Wl,--wrap=nat_detection_record_observation +test_nat_detection_faults_LDFLAGS = $(test_clock_ldflags) -Wl,--wrap=socket_sendto \ + -Wl,--wrap=nat_detection_record_observation -Wl,--wrap=nat_detection_receive_ingress endif test_stun_nat_SOURCES = test_stun_nat.c $(test_clock_sources) diff --git a/tests/test_nat_detection_faults.c b/tests/test_nat_detection_faults.c index 8bbae4a9..c487cb18 100644 --- a/tests/test_nat_detection_faults.c +++ b/tests/test_nat_detection_faults.c @@ -4,7 +4,8 @@ #include "test_clock.h" enum fault_mode { FAULT_NONE, FAULT_NO_PONG, FAULT_FILTERED, FAULT_LATE_PRE, FAULT_CONTACT, - FAULT_DEPENDENT, FAULT_UNSTABLE, FAULT_NO_REPORT, FAULT_INGRESS_BLOCKED, FAULT_NO_ANCHOR }; + FAULT_DEPENDENT, FAULT_UNSTABLE, FAULT_NO_REPORT, FAULT_INGRESS_BLOCKED, FAULT_NO_ANCHOR, + FAULT_LOCAL_INGRESS, FAULT_LOCAL_OBSERVER, FAULT_LOCAL_HELPER_MAPPING }; static struct { struct nat_fixture *fixture; @@ -23,6 +24,8 @@ static struct { ssize_t __real_socket_sendto(socket_t, const void *, size_t, const struct sockaddr *, socklen_t); void __real_nat_detection_record_observation(struct NAT_DETECTION *, struct ETCP_SOCKET *, const uint8_t *, const struct sockaddr_storage *, const uint8_t *, size_t); +void __real_nat_detection_receive_ingress(struct NAT_DETECTION *, struct ETCP_SOCKET *, const uint8_t *, + const struct sockaddr_storage *, const uint8_t *, size_t); static void ignored_ping(int success, uint16_t rtt, void *arg, uint64_t nonce, const uint8_t *data, size_t length) { (void)success; (void)rtt; (void)arg; (void)nonce; (void)data; (void)length; @@ -59,6 +62,7 @@ ssize_t __wrap_socket_sendto(socket_t fd, const void *packet, size_t length, con int to_owner = route_ping_addr_equal(&destination, &fault.socket->local_addr, 1); if (to_owner && source->instance == fault.alias && plain[3] == ETCP_PING && (plain[4] & ETCP_PING_FLAG_ADDRESS_CHECK)) { + if (fault.mode == FAULT_LOCAL_HELPER_MAPPING) { fault.dropped++; return length; } fault.altered++; return __real_socket_sendto(fault.alias_source->fd, packet, length, target, target_length); } @@ -99,6 +103,15 @@ ssize_t __wrap_socket_sendto(socket_t fd, const void *packet, size_t length, con void __wrap_nat_detection_record_observation(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket, const uint8_t *key, const struct sockaddr_storage *source, const uint8_t *tag, size_t length) { + if (fault.fixture && socket->instance == fault.observer && length == ROUTE_OBSERVE_TAG_SIZE + 9 && + ((fault.mode == FAULT_LOCAL_OBSERVER && !memcmp(key, fault.owner->my_keys.public_key, 32)) || + (fault.mode == FAULT_LOCAL_HELPER_MAPPING && !memcmp(key, fault.alias->my_keys.public_key, 32)))) { + /* Настоящий source — loopback; в этом callback включаем промышленную проверку публичности. */ + fault.injected++; + nat_detection_set_allow_local(detector, 0); + __real_nat_detection_record_observation(detector, socket, key, source, tag, length); + nat_detection_set_allow_local(detector, 1); return; + } if (!fault.fixture || socket->instance != fault.helper || memcmp(key, fault.owner->my_keys.public_key, 32) || length != ROUTE_OBSERVE_TAG_SIZE + 9) { __real_nat_detection_record_observation(detector, socket, key, source, tag, length); return; @@ -116,6 +129,31 @@ void __wrap_nat_detection_record_observation(struct NAT_DETECTION *detector, str __real_nat_detection_record_observation(detector, socket, key, &observed, tag, length); } +/* Подлинная проба приходит с локального IP, несмотря на пригодный предварительный адрес помощника. */ +void __wrap_nat_detection_receive_ingress(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket, + const uint8_t *key, const struct sockaddr_storage *source, const uint8_t *tag, size_t length) { + if (fault.fixture && fault.mode == FAULT_LOCAL_INGRESS && detector == fault.owner->nat_det && + !memcmp(key, fault.alias->my_keys.public_key, 32)) { + struct nat_check_status before, after; + assert(!nat_detection_get_status(detector, socket, &before)); + nat_detection_set_allow_local(detector, 0); + /* Неверный token не должен позволять постороннему пакету сменить кандидата. */ + uint8_t stale[ROUTE_OBSERVE_TAG_SIZE + 9]; assert(length == sizeof(stale)); + memcpy(stale, tag, length); stale[length - 1] ^= 1; + __real_nat_detection_receive_ingress(detector, socket, key, source, stale, length); + assert(!nat_detection_get_status(detector, socket, &after)); + assert(after.phase == before.phase && after.helper_reselections == before.helper_reselections); + __real_nat_detection_receive_ingress(detector, socket, key, source, tag, length); + assert(!nat_detection_get_status(detector, socket, &after)); + if (before.phase) { + assert(!after.phase && after.helper_reselections == before.helper_reselections + 1); + assert(after.reason == NAT_REASON_NO_HELPER); fault.injected++; + } + nat_detection_set_allow_local(detector, 1); return; + } + __real_nat_detection_receive_ingress(detector, socket, key, source, tag, length); +} + static void run_case(enum fault_mode mode, uint8_t mapping, uint8_t inbound) { struct nat_fixture fixture; nat_fixture_init(&fixture); struct UTUN_INSTANCE *owner = nat_fixture_node(&fixture, "127.0.0.2", NULL, NULL); @@ -141,12 +179,13 @@ static void run_case(enum fault_mode mode, uint8_t mapping, uint8_t inbound) { if (mode == FAULT_NO_PONG || mode == FAULT_FILTERED || mode == FAULT_LATE_PRE || mode == FAULT_NO_REPORT) assert(fault.dropped); if (mode == FAULT_LATE_PRE || mode == FAULT_CONTACT) assert(fault.injected); if (mode == FAULT_DEPENDENT || mode == FAULT_UNSTABLE) assert(fault.altered); + if (mode == FAULT_LOCAL_OBSERVER) assert(fault.injected && status.helper_reselections && !fault.observations); fault.fixture = NULL; nat_fixture_cleanup(&fixture); DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS NAT fault mode=%d", mode); } /* Разные ключи не означают разные IP: неподходящего помощника отклоняет получатель. */ -static void run_alias(const char *alias_ip) { +static void run_alias(const char *alias_ip, enum fault_mode mode) { struct nat_fixture fixture; nat_fixture_init(&fixture); struct SC_MYKEYS first, second; assert(sc_generate_keypair(&first) == SC_OK && sc_generate_keypair(&second) == SC_OK); if (sc_derive_node_id_from_pubkey(first.public_key) > sc_derive_node_id_from_pubkey(second.public_key)) { @@ -164,6 +203,7 @@ static void run_alias(const char *alias_ip) { struct ETCP_SOCKET *socket = test_etcp_bound_socket(owner, "udp"); memset(&fault, 0, sizeof(fault)); fault.fixture = &fixture; fault.owner = owner; fault.socket = socket; fault.observer = observer; fault.helper = helper; fault.alias = alias; fault.alias_source = test_etcp_bound_socket(alias, "alias"); + fault.mode = mode; struct nat_check_status status = {0}; uint64_t end = get_time_tb() + 400000; nat_detection_get_status(owner->nat_det, socket, &status); uint32_t rounds = status.rounds; assert(!nat_detection_request_check_socket(owner->nat_det, socket, observer->node_id)); @@ -173,11 +213,34 @@ static void run_alias(const char *alias_ip) { DEBUG_INFO(DEBUG_CATEGORY_NAT, "alias fixture result source=%s helper=%016llx expected=%016llx reselections=%u remapped=%u", alias_ip, (unsigned long long)status.helper, (unsigned long long)helper->node_id, status.helper_reselections, fault.altered); - assert(status.helper == helper->node_id && status.helper_reselections && fault.altered); + assert(status.helper == helper->node_id && status.helper_reselections); + if (mode == FAULT_LOCAL_HELPER_MAPPING) assert(fault.injected && fault.dropped); + else assert(fault.altered); + if (mode == FAULT_LOCAL_INGRESS) assert(fault.injected); fault.fixture = NULL; nat_fixture_cleanup(&fixture); DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS NAT receiver rejects helper source=%s", alias_ip); } +/* LAN INIT_RESPONSE описывает свой путь и не инвалидирует уже известный интернет-профиль. */ +static void run_lan_observation(void) { + struct nat_fixture fixture; nat_fixture_init(&fixture); + struct UTUN_INSTANCE *owner = nat_fixture_node(&fixture, "127.0.0.2", NULL, NULL); + struct UTUN_INSTANCE *peer = nat_fixture_node(&fixture, "127.0.0.3", NULL, NULL); + nat_detection_set_automatic(owner->nat_det, 0); nat_detection_set_automatic(peer->nat_det, 0); + struct ETCP_CONN *connection = nat_fixture_connect(&fixture, owner, "udp", peer); + struct ETCP_SOCKET *socket = test_etcp_bound_socket(owner, "udp"); + struct address_profile profile = {ADDRESS_MAP_EIM, ADDRESS_IN_OPEN, ADDRESS_SOURCE_ETCP}; + struct sockaddr_storage external = socket->local_addr; + assert(inet_pton(AF_INET, "8.8.8.8", &((struct sockaddr_in *)&external)->sin_addr) == 1); + socket->address_profile = profile; socket->nat_addr = external; + nat_detection_set_allow_local(owner->nat_det, 0); + nat_detection_observed_by_peer(owner->nat_det, connection->links, &socket->local_addr); + assert(!memcmp(&socket->address_profile, &profile, sizeof(profile))); + assert(route_ping_addr_equal(&socket->nat_addr, &external, 1)); + nat_fixture_cleanup(&fixture); + DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS LAN observation preserves internet mapping and inbound profile"); +} + /* У наблюдателя первый UDP listener недоступен, но управление по TCP и второй IP работают. */ static void run_endpoint_retry(void) { struct nat_fixture fixture; nat_fixture_init(&fixture); @@ -219,6 +282,9 @@ int main(void) { run_case(FAULT_NO_REPORT, ADDRESS_MAP_UNKNOWN, ADDRESS_IN_UNKNOWN); run_case(FAULT_INGRESS_BLOCKED, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_UNKNOWN); run_case(FAULT_NO_ANCHOR, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_UNKNOWN); - run_alias("127.0.0.3"); run_alias("127.0.0.2"); run_endpoint_retry(); + run_case(FAULT_LOCAL_OBSERVER, ADDRESS_MAP_UNKNOWN, ADDRESS_IN_UNKNOWN); + run_alias("127.0.0.3", FAULT_NONE); run_alias("127.0.0.2", FAULT_NONE); + run_alias("127.0.0.9", FAULT_LOCAL_INGRESS); run_alias("127.0.0.9", FAULT_LOCAL_HELPER_MAPPING); + run_lan_observation(); run_endpoint_retry(); test_clock_finish(); return 0; }