Files
utun2/tests/test_nat_detection_faults.c
T

291 lines
20 KiB
C

/* Настоящие UDP/router-пакеты; сетевые потери и задержка вводятся на границе sendto.
* Подмена наблюдения отдельно проверяет применение подписанных mapping-отчётов. */
#include "nat_fixture.h"
#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_LOCAL_INGRESS, FAULT_LOCAL_OBSERVER, FAULT_LOCAL_HELPER_MAPPING };
static struct {
struct nat_fixture *fixture;
struct UTUN_INSTANCE *owner, *observer, *helper, *alias;
struct ETCP_SOCKET *socket, *alias_source;
enum fault_mode mode;
unsigned dropped, altered, direct_received, observations;
int contacted, injected;
socket_t delayed_fd;
uint8_t delayed[1600];
size_t delayed_length;
struct sockaddr_storage delayed_target;
socklen_t delayed_target_length;
} fault;
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;
}
/* Расшифровываем только PING/PONG с публичным хвостом, используя настоящие ключи участников стенда. */
static size_t decode_ping(struct ETCP_SOCKET *socket, const uint8_t *packet, size_t length, uint8_t plain[1600]) {
if (length <= SC_PUBKEY_ENC_SIZE || length > 1600) return 0;
const uint8_t *salt = packet + length - SC_PUBKEY_ENC_SIZE;
for (unsigned i = 0; i < fault.fixture->node_count; i++) {
struct UTUN_INSTANCE *peer = fault.fixture->nodes[i];
uint8_t obfuscated[SC_PUBKEY_SIZE];
sc_obfuscate_pubkey(salt, peer->my_keys.public_key, socket->instance->my_keys.public_key, obfuscated);
if (memcmp(obfuscated, salt + SC_PUBKEY_ENC_SALT_SIZE, sizeof(obfuscated))) continue;
sc_context_t crypto; assert(sc_init_ctx(&crypto, &socket->instance->my_keys) == SC_OK);
assert(sc_set_peer_public_key(&crypto, peer->my_keys.public_key, SC_PEER_PUBKEY_BIN) == SC_OK);
size_t size = 0; assert(sc_decrypt(&crypto, packet, length - SC_PUBKEY_ENC_SIZE, plain, &size) == SC_OK);
return size >= 23 && (plain[3] == ETCP_PING || plain[3] == ETCP_PONG) ? size : 0;
}
return 0;
}
ssize_t __wrap_socket_sendto(socket_t fd, const void *packet, size_t length, const struct sockaddr *target, socklen_t target_length) {
if (!fault.fixture) return __real_socket_sendto(fd, packet, length, target, target_length);
struct ETCP_SOCKET *source = NULL;
for (unsigned i = 0; i < fault.fixture->node_count && !source; i++)
for (struct ETCP_SOCKET *socket = fault.fixture->nodes[i]->etcp_sockets; socket; socket = socket->next)
if (socket->fd == fd) { source = socket; break; }
uint8_t plain[1600];
size_t size = source ? decode_ping(source, packet, length, plain) : 0;
if (!size) return __real_socket_sendto(fd, packet, length, target, target_length);
struct sockaddr_storage destination = {0}; assert(target_length <= sizeof(destination));
memcpy(&destination, target, target_length);
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);
}
if ((fault.mode == FAULT_NO_PONG || fault.mode == FAULT_NO_ANCHOR) && to_owner && plain[3] == ETCP_PONG) {
fault.dropped++; return length;
}
if (source == fault.socket && plain[3] == ETCP_PING && (plain[4] & ETCP_PING_FLAG_OBSERVE) &&
!route_ping_addr_equal(&destination, &test_etcp_bound_socket(fault.observer, "udp")->local_addr, 0)) {
fault.contacted = 1;
ssize_t result = __real_socket_sendto(fd, packet, length, target, target_length);
if (fault.delayed_length) {
assert(__real_socket_sendto(fault.delayed_fd, fault.delayed, fault.delayed_length,
(const struct sockaddr *)&fault.delayed_target, fault.delayed_target_length) == (ssize_t)fault.delayed_length);
DEBUG_INFO(DEBUG_CATEGORY_NAT, "fault fixture: delayed PRE delivered after outgoing contact");
fault.delayed_length = 0; fault.injected = 1;
}
return result;
}
if (to_owner && plain[3] == ETCP_PING && (plain[4] & ETCP_PING_FLAG_ADDRESS_CHECK)) {
if (fault.mode == FAULT_CONTACT && !fault.injected) {
fault.injected = 1;
assert(!etcp_send_ping_to_socket(fault.owner, fault.socket, source->instance->my_keys.public_key,
&source->local_addr, 500, ignored_ping, NULL, NULL, 0, 0));
DEBUG_INFO(DEBUG_CATEGORY_NAT, "fault fixture: real outgoing PING contaminates PRE source IP");
}
if (fault.mode == FAULT_INGRESS_BLOCKED ||
((fault.mode == FAULT_FILTERED || fault.mode == FAULT_LATE_PRE || fault.mode == FAULT_NO_ANCHOR) && !fault.contacted)) {
if (fault.mode == FAULT_LATE_PRE && !fault.delayed_length && !fault.injected) {
memcpy(fault.delayed, packet, length); fault.delayed_length = length; fault.delayed_fd = fd;
fault.delayed_target = destination; fault.delayed_target_length = target_length;
}
fault.dropped++; return length;
}
fault.direct_received++;
}
return __real_socket_sendto(fd, packet, length, target, target_length);
}
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;
}
fault.observations++;
if (fault.mode == FAULT_NO_REPORT) { fault.dropped++; return; }
struct sockaddr_storage observed = *source;
if (fault.mode == FAULT_DEPENDENT || (fault.mode == FAULT_UNSTABLE && fault.observations == 1)) {
struct sockaddr_in *address = (struct sockaddr_in *)&observed;
address->sin_port = htons(ntohs(address->sin_port) == 65535 ? 65534 : ntohs(address->sin_port) + 1);
fault.altered++;
DEBUG_INFO(DEBUG_CATEGORY_NAT, "fault fixture: observed mapping changed from=%s to=%s",
sockaddr_storage_to_str(source).str, sockaddr_storage_to_str(&observed).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 && before.helper == fault.alias->node_id) {
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);
struct UTUN_INSTANCE *observer = nat_fixture_node(&fixture, "127.0.0.3", NULL, NULL);
struct UTUN_INSTANCE *helper = nat_fixture_node(&fixture, "127.0.0.4", NULL, NULL);
nat_detection_set_automatic(observer->nat_det, 0); nat_detection_set_automatic(helper->nat_det, 0);
nat_fixture_connect(&fixture, observer, "udp", helper);
nat_fixture_connect(&fixture, owner, "udp", observer);
nat_detection_set_automatic(owner->nat_det, 0);
memset(&fault, 0, sizeof(fault)); fault.fixture = &fixture; fault.owner = owner;
fault.observer = observer; fault.helper = helper; fault.socket = test_etcp_bound_socket(owner, "udp"); fault.mode = mode;
struct nat_check_status status = {0};
nat_detection_get_status(owner->nat_det, fault.socket, &status); uint32_t rounds = status.rounds;
assert(!nat_detection_request_check_socket(owner->nat_det, fault.socket, observer->node_id));
uint64_t end = get_time_tb() + 400000;
do { test_clock_poll(fixture.ua, 1000); assert(!nat_detection_get_status(owner->nat_det, fault.socket, &status)); }
while (get_time_tb() < end && (status.rounds == rounds || status.phase || status.cycling));
DEBUG_INFO(DEBUG_CATEGORY_NAT, "fault fixture result mode=%d mapping=%s inbound=%s phase=%u cycling=%u reason=%s dropped=%u observations=%u",
mode, address_mapping_name(fault.socket->address_profile.mapping), address_inbound_name(fault.socket->address_profile.inbound),
status.phase, status.cycling, nat_check_reason_name(status.reason), fault.dropped, fault.observations);
assert(!status.phase && !status.cycling);
assert(fault.socket->address_profile.mapping == mapping && fault.socket->address_profile.inbound == 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, 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)) {
struct SC_MYKEYS swap = first; first = second; second = swap;
}
struct UTUN_INSTANCE *owner = nat_fixture_node(&fixture, "127.0.0.2", NULL, NULL);
struct UTUN_INSTANCE *observer = nat_fixture_node(&fixture, "127.0.0.3", NULL, NULL);
char extra[160]; snprintf(extra, sizeof(extra), "[server:alias]\naddr=%s:0\ntype=private\n", alias_ip);
struct UTUN_INSTANCE *alias = nat_fixture_node(&fixture, "127.0.0.8", &first, extra);
struct UTUN_INSTANCE *helper = nat_fixture_node(&fixture, "127.0.0.4", &second, NULL);
nat_detection_set_automatic(observer->nat_det, 0); nat_detection_set_automatic(alias->nat_det, 0);
nat_detection_set_automatic(helper->nat_det, 0);
nat_fixture_connect(&fixture, observer, "udp", alias); nat_fixture_connect(&fixture, observer, "udp", helper);
nat_fixture_connect(&fixture, owner, "udp", observer); nat_detection_set_automatic(owner->nat_det, 0);
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));
do { test_clock_poll(fixture.ua, 1000); assert(!nat_detection_get_status(owner->nat_det, socket, &status)); }
while (get_time_tb() < end && (status.rounds == rounds || status.phase || status.cycling));
assert(socket->address_profile.mapping == ADDRESS_MAP_UNCHANGED && socket->address_profile.inbound == ADDRESS_IN_OPEN);
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);
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);
struct UTUN_INSTANCE *owner = nat_fixture_node(&fixture, "127.0.0.2", NULL,
"[server:tcp]\naddr=127.0.0.2:0\ntransport=tcp\ntype=public\n");
struct UTUN_INSTANCE *observer = nat_fixture_node(&fixture, "127.0.0.3", NULL,
"[server:other]\naddr=127.0.0.8:0\ntype=public\n[server:tcp]\naddr=127.0.0.3:0\ntransport=tcp\ntype=public\n");
struct UTUN_INSTANCE *helper = nat_fixture_node(&fixture, "127.0.0.4", NULL,
"[server:tcp]\naddr=127.0.0.4:0\ntransport=tcp\ntype=public\n");
nat_detection_set_automatic(observer->nat_det, 0); nat_detection_set_automatic(helper->nat_det, 0);
struct ETCP_SOCKET *bad = test_etcp_bound_socket(observer, "udp");
assert(!uasync_remove_socket_t(fixture.ua, bad->fd)); bad->socket_id = NULL;
nat_fixture_connect(&fixture, observer, "tcp", helper); nat_fixture_connect(&fixture, owner, "tcp", observer);
nat_detection_set_automatic(owner->nat_det, 0);
struct ETCP_SOCKET *socket = test_etcp_bound_socket(owner, "udp");
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));
do { test_clock_poll(fixture.ua, 1000); assert(!nat_detection_get_status(owner->nat_det, socket, &status)); }
while (get_time_tb() < end && (status.rounds == rounds || status.phase || status.cycling));
DEBUG_INFO(DEBUG_CATEGORY_NAT, "endpoint fixture result mapping=%s inbound=%s rounds=%u previous=%u reason=%s",
address_mapping_name(socket->address_profile.mapping), address_inbound_name(socket->address_profile.inbound),
status.rounds, rounds, nat_check_reason_name(status.reason));
assert(socket->address_profile.mapping == ADDRESS_MAP_UNCHANGED && socket->address_profile.inbound == ADDRESS_IN_OPEN);
assert(status.helper_reselections && status.observer == observer->node_id);
nat_fixture_cleanup(&fixture);
DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS NAT retries another observer IP after timeout");
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_NAT, DEBUG_LEVEL_DEBUG);
test_clock_init(1000);
run_case(FAULT_NO_PONG, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_OPEN);
run_case(FAULT_FILTERED, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_CONTACT);
run_case(FAULT_LATE_PRE, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_CONTACT);
run_case(FAULT_CONTACT, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_UNKNOWN);
run_case(FAULT_DEPENDENT, ADDRESS_MAP_DEPENDENT, ADDRESS_IN_OPEN);
run_case(FAULT_UNSTABLE, ADDRESS_MAP_UNKNOWN, ADDRESS_IN_UNKNOWN);
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_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;
}