Measure owned endpoint mapping and inbound reachability separately

This commit is contained in:
evgeny committed 2026-10-12 00:01:40 +02:00
1 parent b54307a0df
commit ff69552a61
51 files changed
+2620 -2483

No files matched your search

+2 -2
View File
@@ -1,9 +1,9 @@
# Tests Makefile.am for utun - all tests with new ll_queue library
EXTRA_DIST = fixtures/reality_openssl_hello.hex data/silero_speech.pcm data/silero_speech.txt test_net.h \
EXTRA_DIST = fixtures/reality_openssl_hello.hex data/silero_speech.pcm data/silero_speech.txt test_net.h nat_fixture.h \
run_tests.py test_run_tests.py test_build_script.py stream_netns/topology.py stream_netns/network.py \
stream_netns/probe.py stream_netns/run.py stream_netns/scenario.py stream_netns/README.md stream_netns/TESTED.md \
stream_netns/test_isolation.py
stream_netns/test_isolation.py stream_netns/nat_scenario.py
# Порядок по замерам Linux: быстрые проверки раньше длительных; состав зависит от прежних configure-условий.
+82
View File
@@ -0,0 +1,82 @@
#ifndef NAT_FIXTURE_H
#define NAT_FIXTURE_H
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "test_net.h"
#include "utun_instance.h"
#include "config_parser.h"
#include "transport_layer/etcp.h"
#include "transport_layer/node_conn_direct.h"
#include "routing_layer/topo_group.h"
#include "routing_layer/topo_node_sqlite.h"
#include "routing_layer/nat_detection.h"
#include "routing_layer/route_ping.h"
#include "../lib/u_async.h"
#include "../lib/mem.h"
struct nat_fixture {
struct UASYNC *ua;
struct UTUN_INSTANCE *nodes[8];
struct NODE_CONN_DIRECT *handles[24];
unsigned node_count, handle_count;
size_t allocations;
};
static inline void nat_fixture_init(struct nat_fixture *fixture) {
memset(fixture, 0, sizeof(*fixture)); fixture->allocations = u_get_allocated_count();
fixture->ua = uasync_create(); assert(fixture->ua);
}
static inline struct UTUN_INSTANCE *nat_fixture_node(struct nat_fixture *fixture, const char *ip,
const struct SC_MYKEYS *keys, const char *extra) {
struct SC_MYKEYS generated;
if (!keys) { assert(sc_generate_keypair(&generated) == SC_OK); keys = &generated; }
char private_key[65], public_key[65], config[4096];
for (unsigned i = 0; i < 32; i++) {
snprintf(private_key + i * 2, 3, "%02x", keys->private_key[i]);
snprintf(public_key + i * 2, 3, "%02x", keys->public_key[i]);
}
snprintf(config, sizeof(config), "[global]\nmy_private_key=%s\nmy_public_key=%s\nauto_sockets=no\n"
"[ntp]\nenabled=no\n[server:udp]\naddr=%s:0\ntype=public\n[allowed_keys]\nallow_all=1\n%s",
private_key, public_key, ip, extra ? extra : "");
struct UTUN_INSTANCE *node = utun_instance_create_from_str(fixture->ua, config);
assert(node && fixture->node_count < 8 && utun_core_start(node) == 0);
nat_detection_set_allow_local(node->nat_det, 1);
fixture->nodes[fixture->node_count++] = node; return node;
}
/* Без групповой автосвязности: A контактирует только с явно указанным B.
* NCD переносит настоящую подписанную запись, handshake идёт по UDP. */
static inline struct ETCP_CONN *nat_fixture_connect(struct nat_fixture *fixture, struct UTUN_INSTANCE *from,
const char *source, struct UTUN_INSTANCE *to) {
assert(fixture->handle_count < 24);
struct TOPO_NODE *peer = topo_node_registry_find(to->topo_groups, to->node_id); assert(peer);
struct NODE_CONN_DIRECT **handle = &fixture->handles[fixture->handle_count++];
assert(node_conn_direct_open_node(from, to->node_id, NULL, NULL, handle, peer,
test_etcp_bound_socket(from, source)) != NCD_ERR);
uint64_t end = get_time_tb() + 50000;
struct ETCP_CONN *connection = NULL;
while (get_time_tb() < end) {
connection = node_conn_direct_get_conn(*handle);
if (connection && connection->links_up) return connection;
uasync_poll(fixture->ua, 10);
}
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "fixture connection timed out from=%016llx to=%016llx",
(unsigned long long)from->node_id, (unsigned long long)to->node_id);
abort();
}
static inline void nat_fixture_cleanup(struct nat_fixture *fixture) {
for (unsigned i = 0; i < fixture->handle_count; i++) node_conn_direct_force_close(fixture->handles[i]);
for (unsigned i = 0; i < fixture->node_count; i++) utun_instance_destroy(fixture->nodes[i]);
uasync_poll(fixture->ua, 0);
assert(fixture->ua->timer_alloc_count == fixture->ua->timer_free_count);
assert(fixture->ua->socket_alloc_count == fixture->ua->socket_free_count);
uasync_destroy(fixture->ua, 0);
if (u_get_allocated_count() != fixture->allocations) u_report_unfreed_blocks();
assert(u_get_allocated_count() == fixture->allocations);
}
#endif
+26 -1
View File
@@ -16,6 +16,26 @@ python3 tests/stream_netns/run.py --network-only
python3 tests/stream_netns/run.py
```
Проверка детектора на всех 16 настоящих uTun, отдельно от стриминга:
```sh
python3 tests/stream_netns/run.py --nat-only --tcp
```
`--nat-check` выполняет ту же проверку перед обычными JOIN/Merkle/BGP/стримингом.
Контроллер сначала запускает только ядро с ручными NAT-раундами, переносит
подписанные снимки через RPC и создаёт нужные NCD-подключения. Наблюдения,
испытания входа и TCP-handshake идут по настоящей сети. Помощники измерения
не требуют готового собственного NAT-профиля. Измерение входа происходит
до первого исходящего контакта рабочего сокета с помощником.
Проверяются отдельно mapping и inbound каждого UDP endpoint, точный собственный
внешний IP:port, отсутствие ложного STRICT без помощника и корректность TCP listener.
Ожидания: public — UNCHANGED/OPEN, cone — EIM/OPEN, restricted — EIM/CONTACT_REQUIRED,
strict — DEPENDENT без универсального внешнего адреса. TCP за NAT без port forwarding
остаётся UNKNOWN/no_listener. Результаты: `nat-profiles.json`, `nat-result.json`;
обычная телеметрия также включает `address_profiles`, контакты и причину неполного измерения.
Полная baseline-проверка длится 120 секунд после старта доставки, плюс калибровка и JOIN. Она покрывает оборот 16-битного timestamp и несколько циклов проб. Короткая проверка:
```sh
@@ -72,7 +92,12 @@ UDP NAT-профили:
- `g_c1`: EIM, вход только от ранее опрошенного IP/порта (APDF).
- `g_a2`, `g_b2`, `g_c2`: внешний UDP-порт зависит от **IP назначения** (ADM), возврат только по conntrack (APDF). Для известных внешних endpoints порты назначаются явно; это воспроизводимый строгий NAT, а не заявление, что обычный masquerade всегда симметричен.
Cone/restricted mapping разрешается только после исходящего пакета, время активности 60 секунд обновляется на каждом исходящем пакете. TCP использует обычный stateful NAT без проброса входящих портов. Проверка перед uTun измеряет реальный внешний адрес/порт на двух публичных получателях и отдельно пробует вход от третьего, включая отсутствие mapping до исходящего трафика. Для проб до/после открытия используются разные внешние tuples, чтобы не переиспользовать отрицательный conntrack.
Mapping разрешается после исходящего пакета, время активности/контакта 60 секунд обновляется исходящим трафиком.
Запрещённый вход отсекается в `prerouting priority raw` до conntrack. Иначе неуспешная
входящая проба могла закрепить tuple без DNAT и помешать последующему открытию фильтра.
TCP использует обычный stateful NAT без проброса входящих портов. Калибровка проверяет
внешний endpoint на разных назначениях, вход до mapping, незапрошенный вход и разрешённый
вход после исходящего контакта с тем же проверявшим узлом. После калибровки сеть пересоздаётся.
После калибровки ее namespace уничтожаются, затем создается новая сеть с теми же параметрами и пустыми NAT mappings/conntrack. Результаты проб не облегчают подключения реальных процессов. Снимки `network-calibration-initial.json` / `network-calibrated.json` относятся к пробам, `network-initial.json` / `network-final.json` — к uTun; команды содержат inode соответствующего namespace. PID в `namespaces.json` записаны относительно PID namespace контроллера.
+19
View File
@@ -2,6 +2,25 @@
Стенд: 16 процессов uTun, 10 NAT-клиентов, 3 страны. Основной network namespace не изменился во всех перечисленных запусках.
Результаты прежних стриминговых прогонов ниже подтверждали сетевые правила шлюзов
и доставку данных. Они не проверяли правильность классификации NAT внутри uTun.
Новая проверка владельца endpoint: `--nat-only --tcp`, артефакты
`/tmp/nat-owner-netns-02/nat-result.json`. Проверены все 16 UDP-профилей и 16 TCP-профилей:
6 UNCHANGED/OPEN, 4 EIM/OPEN, 2 EIM/CONTACT_REQUIRED, 4 DEPENDENT/UNKNOWN;
TCP: 6 публичных OPEN, 10 UNKNOWN/no_listener. Точные внешние адреса совпали с ожидаемыми.
Полная проверка с новым детектором:
`python3 tests/stream_netns/run.py --nat-check --publisher n05 --tcp --duration 75 --rate 2000000 --faults --fault-duration 15 --output /tmp/nat-owner-stream-integration-02`.
PASS: сначала все 16 UDP/TCP-профилей, затем реальные JOIN/Merkle/BGP, 4041 пачка,
перегрузка WAN, recovery, две отписки и END у оставшихся 13 подписчиков.
Ошибок payload/checksum/порядка нет, GAP полностью учитывается. Главный namespace неизменён.
`make -C tests check -j4`: 144 passed, 0 failed, 0 skipped (`/tmp/nat-owner-regressions-02.log`).
Калибровка дополнена переходом «незапрошенный вход → исходящий контакт → вход».
`/tmp/nat-owner-calibration-before-guard` воспроизвёл блокировку последующего UDP
для restricted NAT даже без uTun. После фильтрации запрещённого входа до conntrack
`/tmp/nat-owner-calibration-after-guard` прошёл. Главный namespace не изменился.
| Сценарий | Команда из корня репозитория | Результат |
| --- | --- | --- |
| Публичный UDP-источник, длинный прогон и перегрузка | `python3 tests/stream_netns/run.py --duration 120 --rate 2000000 --faults --output /tmp/stream-netns-final-01` | PASS: 5996 пачек, 3 оборота timestamp, вытеснение из кольца, WAN recovery, unsubscribe, END |
+107
View File
@@ -0,0 +1,107 @@
"""Проверка детектора uTun на настоящем NAT; контроль не создаёт контакт с помощником."""
import json
from topology import NATS, NODES
def profile(statistics, protocol):
return next(item for item in statistics["address_profiles"] if item["socket"] == protocol)
def check_profiles(scenario):
agents = scenario.agents
public = [node for node in NODES if not node.nat]
transport = "tcp" if scenario.args.tcp_only else "udp"
connected = set()
def connect(source, target):
pair = (source.name, target.name)
if pair in connected:
return
snapshot = agents[target.name].rpc("snapshot")["snapshot"]
agents[source.name].rpc("nat_link", snapshot=snapshot, socket=transport)
target_id = agents[target.name].identity
scenario.wait(f"nat-link-{source.name}-{target.name}",
lambda data: target_id in data[source.name]["direct_up"], 20)
connected.add(pair)
def store_owner(owner, observer):
agents[observer.name].rpc("nat_store", snapshot=agents[owner.name].rpc("snapshot")["snapshot"])
def measure(owner, observer, protocol):
before = profile(agents[owner.name].rpc("stats"), protocol)
agents[owner.name].rpc("nat_check", socket=protocol, observer=agents[observer.name].identity)
data = scenario.wait(f"nat-profile-{owner.name}-{protocol}", lambda statistics:
profile(statistics[owner.name], protocol)["rounds"] > before["rounds"] and
profile(statistics[owner.name], protocol)["phase"] == 0, 40)
return profile(data[owner.name], protocol)
# Отсутствие второго наблюдателя не превращается в STRICT/DIRECT.
first, second = public[:2]
connect(first, second)
if not scenario.args.tcp_only:
incomplete = measure(first, second, "udp")
if (incomplete["mapping"], incomplete["inbound"], incomplete["reason"]) != ("UNKNOWN", "UNKNOWN", "no_helper"):
raise RuntimeError(f"NAT no-helper test: {incomplete}")
verified = {}
for owner in public + [node for node in NODES if node.nat]:
observer = second if owner.name == first.name else first
if not scenario.args.tcp_only:
contacts = set(profile(agents[owner.name].rpc("stats"), "udp")["contacts"])
helper = next((node for node in public if node.name not in {owner.name, observer.name} and node.ip not in contacts), None)
if not helper:
raise RuntimeError(f"{owner.name}: no independent helper remains before cold measurement; contacts={contacts}")
connect(observer, helper)
connect(owner, observer)
store_owner(owner, observer)
protocols = ["tcp"] if scenario.args.tcp_only else ["udp", "tcp"] if scenario.args.tcp else ["udp"]
observed = {}
for protocol in protocols:
if protocol == "tcp" and owner.nat:
# Приватный TCP listener не имеет обнаруженного публичного endpoint.
agents[owner.name].rpc("nat_check", socket="tcp", observer=agents[observer.name].identity)
data = scenario.wait(f"nat-private-tcp-{owner.name}", lambda statistics:
profile(statistics[owner.name], "tcp")["reason"] == "no_listener", 5)
actual = profile(data[owner.name], "tcp")
expected = ("UNKNOWN", "UNKNOWN")
else:
actual = measure(owner, observer, protocol)
if protocol == "tcp" or not owner.nat:
expected = ("UNCHANGED", "OPEN")
else:
gateway = next(item for item in NATS if item.name == owner.nat)
expected = {"cone": ("EIM", "OPEN"), "restricted": ("EIM", "CONTACT_REQUIRED"),
"strict": ("DEPENDENT", "UNKNOWN")}[gateway.mode]
if (actual["mapping"], actual["inbound"]) != expected:
raise RuntimeError(f"{owner.name}/{protocol}: expected {expected}, detector returned {actual}")
if expected[0] == "EIM":
gateway = next(item for item in NATS if item.name == owner.nat)
external = f"{gateway.ip}:{owner.mapped_port}"
elif expected[0] == "UNCHANGED":
external = f"{owner.ip}:32000"
else:
external = ""
if actual["external"] != external:
raise RuntimeError(f"{owner.name}/{protocol}: foreign/invalid external endpoint: {actual}, expected {external}")
observed[protocol] = actual
print(f"NAT {owner.name}/{protocol}: {actual['mapping']} / {actual['inbound']}"
f" external={actual['external'] or '-'} reason={actual['reason']}", flush=True)
verified[owner.name] = observed
(scenario.network.directory / "nat-profiles.json").write_text(json.dumps(verified, indent=2) + "\n")
# Повтор на использовавшемся порту сохраняет mapping, но не обновляет OPEN
# по контактировавшим IP. Проверка запускается после всех холодных измерений.
first_profile = verified[first.name].get("udp")
if first_profile:
again = measure(first, second, "udp")
if again["mapping"] != "UNCHANGED" or again["inbound"] not in {"OPEN", "UNKNOWN"}:
raise RuntimeError(f"{first.name}: warm measurement produced invalid classification: {again}")
if again["inbound"] == "UNKNOWN" and again["reason"] != "contact_history":
raise RuntimeError(f"{first.name}: UNKNOWN without contact-history explanation: {again}")
result = {"nodes_checked": len(verified), "protocols": protocols, "no_helper_checked": not scenario.args.tcp_only,
"warm_recheck_checked": bool(first_profile), "verified": verified}
(scenario.network.directory / "nat-result.json").write_text(json.dumps(result, indent=2) + "\n")
return result
+17
View File
@@ -23,6 +23,23 @@ def nat_rules(gateway):
lines.append(f"{match} update @active {{ {node.mapped_port} timeout 60s }}")
elif gateway.mode == "restricted":
lines.append(f"{match} update @contacts {{ ip daddr . udp dport . {node.mapped_port} timeout 60s }}")
elif gateway.mode == "strict":
for index, destination in enumerate(destinations):
port = 40000 + node.number * 100 + index
lines.append(f"{match} ip daddr {destination} update @contacts {{ ip daddr . udp dport . {port} timeout 60s }}")
# Запрещённый вход отсекается ДО conntrack. Иначе первая неуспешная
# входящая проба закрепляет tuple без DNAT и блокирует последующее открытие.
lines += [" }", " chain ingress_guard { type filter hook prerouting priority raw; policy accept;"]
for node in clients:
ports = ([40000 + node.number * 100 + index for index in range(len(destinations))]
if gateway.mode == "strict" else [node.mapped_port])
for port in ports:
match = f' iifname "wan" udp dport {port}'
if gateway.mode == "cone":
lines.append(f"{match} udp dport @active accept")
else:
lines.append(f"{match} ip saddr . udp sport . udp dport @contacts accept")
lines.append(f"{match} counter drop")
lines += [" }", " chain prerouting { type nat hook prerouting priority dstnat; policy accept;"]
for node in clients:
match = f' iifname "wan" udp dport {node.mapped_port}'
+151 -4
View File
@@ -21,6 +21,11 @@
#include "../../src/chat/invite_link.h"
#include "../../src/stream/stream_priv.h"
#include "../../src/stream/stream_member.h"
#include "../../src/routing_layer/nat_detection.h"
#include "../../src/routing_layer/route_ping.h"
#include "../../src/routing_layer/topo_node_sqlite.h"
#include "../../src/transport_layer/node_conn_direct.h"
#include "../../src/transport_layer/etcp.h"
#include "../../src/utun_instance.h"
#define LAB_STREAMS 4
@@ -40,6 +45,9 @@ struct lab {
struct lab_stream streams[LAB_STREAMS];
struct chat_invite_build *invite;
uint64_t invite_id, group;
struct NODE_CONN_DIRECT *nat_handles[32];
uint64_t nat_records[32];
unsigned nat_handle_count, nat_record_count;
int errors;
};
static volatile sig_atomic_t stopping;
@@ -168,7 +176,47 @@ static void statistics(struct lab *lab, uint64_t id, int loads) {
stream_member_load(lab->inst, lab->group, node->node_id, &state, NULL));
}
}
printf("},\"streams\":["); separator = 0;
printf("},\"address_profiles\":["); separator = 0;
for (struct ETCP_SOCKET *socket = lab->inst->etcp_sockets; socket; socket = socket->next) {
if (socket->probe_only) continue;
struct nat_check_status status = {0}; nat_detection_get_status(lab->inst->nat_det, socket, &status);
printf("%s{\"socket\":", separator++ ? "," : ""); json_string(socket->name);
printf(",\"socket_id\":%u,\"generation\":\"%" PRIu64 "\",\"protocol\":\"%s\",\"local\":",
socket->sock_id, socket->generation, socket->is_tcp ? "tcp" : "udp");
json_string(sockaddr_storage_to_str(socket->is_tcp ? &socket->interface_addr : &socket->local_addr).str);
printf(",\"mapping\":\"%s\",\"inbound\":\"%s\",\"evidence\":\"%" PRIu64 "\",\"checked_at\":\"%" PRIu64 "\",\"external\":",
address_mapping_name(socket->address_profile.mapping), address_inbound_name(socket->address_profile.inbound),
socket->address_profile.evidence_id, socket->address_profile.checked_at);
const struct sockaddr_storage *external = &socket->nat_addr;
if (!external->ss_family && socket->address_profile.mapping == ADDRESS_MAP_UNCHANGED)
external = socket->is_tcp ? &socket->interface_addr : &socket->local_addr;
json_string(external->ss_family ? sockaddr_storage_to_str(external).str : "");
printf(",\"phase\":%u,\"reason\":\"%s\",\"observer\":\"%" PRIu64 "\",\"helper\":\"%" PRIu64 "\","
"\"rounds\":%u,\"helper_reselections\":%u,\"history_complete\":%u,\"contacts\":[", status.phase,
nat_check_reason_name(status.reason), status.observer, status.helper, status.rounds,
status.helper_reselections, status.history_complete);
uint8_t local[ROUTE_ADDR_WIRE_SIZE]; sqlite3_stmt *contacts = NULL; int contact_separator = 0;
if (!socket->is_tcp && !route_ping_addr_encode(local, &socket->local_addr) &&
sqlite3_prepare_v2(lab->inst->topo_sqlite_db, "SELECT remote FROM nat_contacts WHERE local=?",
-1, &contacts, NULL) == SQLITE_OK) {
sqlite3_bind_blob(contacts, 1, local, sizeof(local), SQLITE_TRANSIENT);
while (sqlite3_step(contacts) == SQLITE_ROW) {
const uint8_t *address = sqlite3_column_blob(contacts, 0); char text[64];
if (!address || sqlite3_column_bytes(contacts, 0) != 17 || (address[0] != 4 && address[0] != 6)) continue;
if (!inet_ntop(address[0] == 4 ? AF_INET : AF_INET6, address + 1, text, sizeof(text))) continue;
if (contact_separator++) printf(",");
json_string(text);
}
}
sqlite3_finalize(contacts); printf("]}");
}
printf("],\"direct_up\":["); separator = 0;
for (struct ll_entry *entry = lab->inst->connections->head; entry; entry = entry->next) {
const struct ETCP_CONN *connection = ((const struct conn_queue_entry *)entry->data)->conn;
if (connection && connection->links_up && !connection->close_requested && connection->state != 2)
printf("%s\"%" PRIu64 "\"", separator++ ? "," : "", connection->peer_node_id);
}
printf("],\"streams\":["); separator = 0;
for (unsigned i = 0; i < LAB_STREAMS; i++) {
struct lab_stream *stream = &lab->streams[i];
if (!stream->source) continue;
@@ -242,6 +290,100 @@ static void invite_ready(void *arg, int result, const char *link) {
json_string(link ? link : ""); printf("}\n");
}
static struct TOPO_NODE *parse_snapshot(struct lab *lab, const char *json) {
char hex[4097]; uint8_t wire[2048]; size_t length;
if (json_flat_get(json, "snapshot", hex, sizeof(hex)) || !(length = strlen(hex)) || length % 2 || length / 2 > sizeof(wire)) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "lab: invalid node snapshot size"); return NULL;
}
for (size_t i = 0; i < length / 2; i++) {
unsigned value;
if (sscanf(hex + i * 2, "%2x", &value) != 1) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "lab: invalid snapshot hex"); return NULL; }
wire[i] = (uint8_t)value;
}
struct TOPO_GROUP group = {.instance = lab->inst};
struct TOPO_NODE *node = NULL;
if (topo_node_deserialize(&group, wire, length / 2, &node, NULL, NULL, NULL, NULL) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "lab: snapshot decode failed"); return NULL;
}
if (node->node_id == lab->inst->node_id || topo_node_verify(node) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "lab: snapshot signature/identity rejected");
topo_node_destroy(lab->inst->topo_groups, node); return NULL;
}
return node;
}
/* Стенд переносит подписанные снимки через RPC только до запуска группы.
* NCD handshake, наблюдения и весь трафик всё равно проходят по настоящей сети. */
static int nat_command(struct lab *lab, const char *json, const char *operation, uint64_t id) {
if (!strcmp(operation, "chat_start")) {
int result = chat_service_start(lab->inst);
if (!result) stream_set_event_callback(lab->inst, event, lab);
reply(id, result); return 1;
}
if (!strcmp(operation, "nat_release")) {
for (unsigned i = 0; i < lab->nat_handle_count; i++) node_conn_direct_close(lab->nat_handles[i]);
for (unsigned i = 0; i < lab->nat_record_count; i++) topo_node_registry_unref(lab->inst->topo_groups, lab->nat_records[i]);
lab->nat_handle_count = lab->nat_record_count = 0; reply(id, 0); return 1;
}
if (!strcmp(operation, "nat_auto")) {
uint64_t enabled;
if (number(json, "enabled", &enabled) || enabled > 1) { reply(id, -1); return 1; }
nat_detection_set_automatic(lab->inst->nat_det, enabled); reply(id, 0); return 1;
}
if (!strcmp(operation, "nat_check")) {
char name[32]; uint64_t observer;
if (!json_flat_get(json, "socket", name, sizeof(name)) && !number(json, "observer", &observer)) {
struct ETCP_SOCKET *socket = lab->inst->etcp_sockets;
while (socket && (socket->probe_only || strcmp(socket->name, name))) socket = socket->next;
reply(id, socket ? nat_detection_request_check_socket(lab->inst->nat_det, socket, observer) : -1);
} else { nat_detection_request_check_all(lab->inst->nat_det); reply(id, 0); }
return 1;
}
if (!strcmp(operation, "snapshot")) {
uint8_t wire[2048]; struct TOPO_GROUP_NODE nq = {0};
struct TOPO_NODE *node = topo_node_registry_find(lab->inst->topo_groups, lab->inst->node_id);
int length = topo_node_serialize(node, &nq, 0, 0, wire, sizeof(wire), 0);
if (length < 0) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "lab: snapshot serialize failed"); reply(id, -1); return 1; }
printf("{\"id\":%" PRIu64 ",\"rc\":0,\"snapshot\":\"", id);
for (int i = 0; i < length; i++) printf("%02x", wire[i]);
printf("\"}\n"); return 1;
}
int store = !strcmp(operation, "nat_store"), link = !strcmp(operation, "nat_link");
if (!store && !link) return 0;
struct TOPO_NODE *node = parse_snapshot(lab, json);
if (!node) { reply(id, -1); return 1; }
int result = 0;
if (link) {
char source[32]; struct ETCP_SOCKET *socket = NULL;
if (lab->nat_handle_count < 32 && !json_flat_get(json, "socket", source, sizeof(source)))
for (socket = lab->inst->etcp_sockets; socket; socket = socket->next)
if (!socket->probe_only && !strcmp(socket->name, source)) break;
if (!socket) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "lab: native link source/handle unavailable"); result = -1; }
else {
struct NODE_CONN_DIRECT *handle = NULL;
int opened = node_conn_direct_open_node(lab->inst, node->node_id, NULL, NULL, &handle, node, socket);
if (opened == NCD_ERR || !handle) result = -1;
else lab->nat_handles[lab->nat_handle_count++] = handle;
}
topo_node_destroy(lab->inst->topo_groups, node);
} else {
if (lab->nat_record_count == 32) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "lab: imported snapshot limit reached"); result = -1;
topo_node_destroy(lab->inst->topo_groups, node);
} else {
struct TOPO_NODE *stored = topo_node_registry_store(lab->inst->topo_groups, node);
if (!stored) { topo_node_destroy(lab->inst->topo_groups, node); result = -1; }
else {
unsigned i;
for (i = 0; i < lab->nat_record_count; i++) if (lab->nat_records[i] == stored->node_id) break;
if (i == lab->nat_record_count) lab->nat_records[lab->nat_record_count++] = stored->node_id;
else topo_node_registry_unref(lab->inst->topo_groups, stored->node_id);
}
}
}
reply(id, result); return 1;
}
static void command(struct lab *lab, const char *json) {
uint64_t id = 0, source = 0, value = 0;
char operation[32];
@@ -251,6 +393,7 @@ static void command(struct lab *lab, const char *json) {
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "lab: RPC id=%" PRIu64 " op=%s", id, operation);
if (!strcmp(operation, "stats")) { number(json, "loads", &value); statistics(lab, id, value != 0); return; }
if (!strcmp(operation, "quit")) { stopping = 1; reply(id, 0); return; }
if (nat_command(lab, json, operation, id)) return;
if (!strcmp(operation, "create")) {
if (lab->group) { reply(id, -1); return; }
chat_core_create_channel_auto(lab->inst, "stream-netns");
@@ -354,7 +497,8 @@ int main(int argc, char **argv) {
for (unsigned i = 0; i < 32; i++) printf("%02x", public_key[i]);
printf("\",\"node\":\"%" PRIu64 "\"}\n", sc_derive_node_id_from_pubkey(public_key)); return 0;
}
if (argc != 3) { fprintf(stderr, "Usage: %s CONFIG LOG | --identity SEED\n", argv[0]); return 1; }
int core_only = argc == 4 && !strcmp(argv[3], "--core-only");
if (argc != 3 && !core_only) { fprintf(stderr, "Usage: %s CONFIG LOG [--core-only] | --identity SEED\n", argv[0]); return 1; }
setvbuf(stdout, NULL, _IOLBF, 0);
signal(SIGTERM, stop_signal); signal(SIGINT, stop_signal); signal(SIGPIPE, SIG_IGN);
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_enable_console(0); debug_enable_file_output(argv[2], 1);
@@ -363,11 +507,12 @@ int main(int argc, char **argv) {
struct lab *lab = u_calloc(1, sizeof(*lab));
if (!async || !lab) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: initialization allocation failed"); return 1; }
lab->inst = utun_instance_create(async, argv[1]);
if (!lab->inst || utun_core_start(lab->inst) || chat_service_start(lab->inst)) {
if (!lab->inst || utun_core_start(lab->inst) || (!core_only && chat_service_start(lab->inst))) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: service startup failed"); return 1;
}
if (core_only) nat_detection_set_automatic(lab->inst->nat_det, 0);
debug_enable_console(0);
stream_set_event_callback(lab->inst, event, lab);
if (!core_only) stream_set_event_callback(lab->inst, event, lab);
if (fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: stdin nonblocking failed: %s", strerror(errno)); return 1;
}
@@ -397,6 +542,8 @@ int main(int argc, char **argv) {
if (lab->streams[i].errors) lab->errors++;
}
int errors = lab->errors;
for (unsigned i = 0; i < lab->nat_handle_count; i++) node_conn_direct_force_close(lab->nat_handles[i]);
for (unsigned i = 0; i < lab->nat_record_count; i++) topo_node_registry_unref(lab->inst->topo_groups, lab->nat_records[i]);
utun_instance_destroy(lab->inst); uasync_destroy(async, 1); u_free(lab);
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "lab: stopped errors=%d", errors);
debug_disable_file_output(); return errors ? 1 : 0;
+7 -1
View File
@@ -121,9 +121,15 @@ class Probes:
expected = (gateway.mode != "strict", gateway.mode == "cone")
if (stable, inbound) != expected:
raise RuntimeError(f"NAT {node.name}/{gateway.mode}: stable={stable}, unsolicited={inbound}, expected={expected}")
opened = self.exchange(node.name, c.ip, count=3)
if not opened["observed"]:
raise RuntimeError(f"NAT {node.name}/{gateway.mode}: outgoing to previous unsolicited sender failed: {opened}")
after = self.exchange(c.name, gateway.ip, opened["observed"][1], count=3, timeout=0.4)
if not after["observed"]:
raise RuntimeError(f"NAT {node.name}/{gateway.mode}: incoming failed after contact: {after}")
results["nat"].append({"node": node.name, "gateway": gateway.name, "mode": gateway.mode,
"first": first["observed"], "second": second["observed"],
"stable": stable, "unsolicited": inbound})
"stable": stable, "unsolicited": inbound, "after_contact": bool(after["observed"])})
return results
def close(self):
+6
View File
@@ -92,6 +92,8 @@ def main():
parser.add_argument("--output", help="New directory for logs and results")
parser.add_argument("--seed", type=int, default=20261011)
parser.add_argument("--network-only", action="store_true", help="Only topology, WAN and NAT calibration")
parser.add_argument("--nat-check", action="store_true", help="Verify owned endpoint profiles before real JOIN/streaming")
parser.add_argument("--nat-only", action="store_true", help="Only real uTun endpoint detection after network calibration")
parser.add_argument("--binary", default=str(Path(__file__).parent / "stream_lab_node"))
parser.add_argument("--publisher", choices=[f"n{i:02d}" for i in range(1, 17)], default="n01")
parser.add_argument("--rate", type=int, default=1000000, help="Application bitrate in bits/s, before FEC")
@@ -125,6 +127,10 @@ def main():
"--fault-duration", str(args.fault_duration)]
if args.network_only:
command.append("--network-only")
if args.nat_check:
command.append("--nat-check")
if args.nat_only:
command.append("--nat-only")
if args.faults:
command.append("--faults")
if args.tcp:
+22 -5
View File
@@ -11,7 +11,7 @@ from topology import NODES, WANS
class Agent:
def __init__(self, network, node, binary, seed, tcp, tcp_only):
def __init__(self, network, node, binary, seed, tcp, tcp_only, core_only=False):
self.node = node
directory = network.directory / node.name
directory.mkdir()
@@ -26,12 +26,15 @@ class Agent:
f"db_path={directory / 'db'}\ndebug_level=warn\n"
"[ntp]\nenabled=no\n[allowed_keys]\nallow_all=1\n"
"[chatserver]\ngroup_autoconnect=1\nstorage_autoload=0\nmember_files_enabled=0\n"
"[debug]\nstream=debug\nchat_sync=info\nmember_sync=info\nbgp=info\nconnection=info\n" + sockets)
"[debug]\nstream=debug\nnat=debug\nchat_sync=info\nmember_sync=info\nbgp=info\nconnection=info\n" + sockets)
path = directory / "node.conf"
path.write_text(config)
self.stderr = (directory / "stderr.log").open("w")
self.transcript = (directory / "rpc.jsonl").open("w", buffering=1)
self.process = network.spawn(node.name, [str(binary), str(path), str(directory / "utun.log")],
command = [str(binary), str(path), str(directory / "utun.log")]
if core_only:
command.append("--core-only")
self.process = network.spawn(node.name, command,
stdin=-1, stdout=-1, stderr=self.stderr, text=True, bufsize=1)
self.serial = 0
if self.read(10) != {"ready": True, "node": self.identity}:
@@ -133,8 +136,19 @@ class Scenario:
def run(self):
binary = Path(self.args.binary).resolve()
nat_check = self.args.nat_check or self.args.nat_only
for node in NODES:
self.agents[node.name] = Agent(self.network, node, binary, self.args.seed, self.args.tcp, self.args.tcp_only)
self.agents[node.name] = Agent(self.network, node, binary, self.args.seed, self.args.tcp, self.args.tcp_only, nat_check)
nat_result = None
if nat_check:
from nat_scenario import check_profiles
nat_result = check_profiles(self)
if self.args.nat_only:
return {"pass": True, "nat": nat_result, "transport": "tcp" if self.args.tcp_only else
"udp+tcp" if self.args.tcp else "udp"}
for agent in self.agents.values():
agent.rpc("nat_auto", enabled=1)
agent.rpc("chat_start")
owner = self.agents["n01"]
group = owner.rpc("create")["group"]
print(f"Joining real CHAT group {group}", flush=True)
@@ -146,6 +160,9 @@ class Scenario:
self.wait("join-merkle-bgp", lambda s: all(set(n["online"]) == identities and
set(n["profile_status"]) == identities and all(rc == 0 for rc in n["profile_status"].values())
for n in s.values()), 120)
if nat_check:
for agent in self.agents.values():
agent.rpc("nat_release")
source = self.agents[self.args.publisher]
source_id = source.identity
source.rpc("publish", seed=self.args.seed)
@@ -207,7 +224,7 @@ class Scenario:
return {"pass": True, "group": group, "publisher": self.args.publisher, "generated": generated,
"phases": self.phases, "subscribers_checked": len(names), "source_timestamp_wraps": self.clock_wraps,
"transport": "tcp" if self.args.tcp_only else "udp+tcp" if self.args.tcp else "udp",
"rate_bps": self.args.rate}
"rate_bps": self.args.rate, "nat": nat_result}
def report(self, final, source):
rows = []
+1 -1
View File
@@ -192,7 +192,7 @@ static struct TOPO_GROUP_NODE* mk_srv_node(void) {
struct TOPO_SOCKMETA4* sm = memory_pool_alloc(ctx.client->topo_groups->v4_sock_meta_pool);
if (!sm) { u_free(nq); u_free(ni); return NULL; }
memset(sm, 0, sizeof(*sm));
sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->nat_type = NAT_TYPE_UNKNOWN;
sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->address_profile = (struct address_profile){0};
sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm;
struct TOPO_ADDR4* a_udp = memory_pool_alloc(ctx.client->topo_groups->v4_addr_pool);
+1 -1
View File
@@ -80,7 +80,7 @@ static struct TOPO_GROUP_NODE* mknode(struct UTUN_INSTANCE* inst, uint64_t nid,
nq->ll.size = sizeof(struct TOPO_GROUP_NODE) - sizeof(struct ll_entry);
nq->node_id = nid;
struct TOPO_SOCKMETA4* sm = memory_pool_alloc(inst->topo_groups->v4_sock_meta_pool);
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->nat_type = NAT_TYPE_UNKNOWN; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->address_profile = (struct address_profile){0}; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
struct TOPO_ADDR4* addr = memory_pool_alloc(inst->topo_groups->v4_addr_pool);
if (addr) { addr->addr[0] = a; addr->addr[1] = b; addr->addr[2] = c; addr->addr[3] = d; addr->port = port; addr->type = TOPO_ADDR_INTERFACE; addr->socket_id = 0; addr->protocol = TOPO_PROTO_UDP; addr->next = ni->v4_addrs; ni->v4_addrs = addr; }
topo_node_registry_store(inst->topo_groups, ni);
+12 -12
View File
@@ -161,14 +161,15 @@ static void monitor_connections(void* arg) {
if (link->initialized) {
client_initialized = 1;
// Check NAT fields are populated
const struct sockaddr_in *observed = (const struct sockaddr_in *)&link->local_as_seen_by_peer;
// Наблюдение относится к нашему исходящему endpoint.
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[CLIENT] Checking NAT fields...");
if (link->nat_ip == 0) {
if (link->local_as_seen_by_peer.ss_family != AF_INET || observed->sin_addr.s_addr == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[CLIENT] FAIL: nat_ip is 0 (should be set)");
test_completed = 2; // Failure
return;
}
if (link->nat_port == 0) {
if (ntohs(observed->sin_port) == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[CLIENT] FAIL: nat_port is 0 (should be set)");
test_completed = 2; // Failure
return;
@@ -177,33 +178,32 @@ static void monitor_connections(void* arg) {
// Convert nat_ip to string for display
char nat_ip_str[INET_ADDRSTRLEN];
struct in_addr nat_addr;
nat_addr.s_addr = link->nat_ip; // уже в network byte order
nat_addr.s_addr = observed->sin_addr.s_addr; // уже в network byte order
inet_ntop(AF_INET, &nat_addr, nat_ip_str, sizeof(nat_ip_str));
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[CLIENT] PASS: NAT address is set: %s:%u",
nat_ip_str, link->nat_port);
nat_ip_str, ntohs(observed->sin_port));
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[CLIENT] PASS: nat_changes_count=%u, nat_hits_count=%u",
link->nat_changes_count, link->nat_hits_count);
// Verify NAT IP is 127.0.0.1 (localhost) since client connects from localhost
// В network byte order: 127.0.0.1 = 0x0100007F
if (link->nat_ip != 0x0100007F) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[CLIENT] FAIL: NAT IP is not 127.0.0.1 (expected 0x0100007F, got 0x%08X)",
link->nat_ip);
if (observed->sin_addr.s_addr != htonl(0x7f000001)) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[CLIENT] FAIL: Observed IP is not 127.0.0.1 (got 0x%08X)",
observed->sin_addr.s_addr);
test_completed = 2; // Failure
return;
}
// Verify NAT port matches client's local port exactly (no NAT in test)
if (expected_client_port != 0 && link->nat_port != expected_client_port) {
if (expected_client_port != 0 && ntohs(observed->sin_port) != expected_client_port) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[CLIENT] FAIL: NAT port %u does not match expected port %u",
link->nat_port, expected_client_port);
ntohs(observed->sin_port), expected_client_port);
test_completed = 2; // Failure
return;
}
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[CLIENT] PASS: NAT IP and port match exactly (127.0.0.1:%u)",
link->nat_port);
ntohs(observed->sin_port));
}
}
link = link->next;
+2 -1
View File
@@ -164,7 +164,8 @@ static int verify_ipv6_local_nodeinfo(const char* name, struct UTUN_INSTANCE* in
{ const struct TOPO_SOCKMETA6* m = ni->v6_sock_meta; int i = 0;
while (m) {
if (m->config_type != CFG_SERVER_TYPE_PUBLIC) { printf("FAIL [%s]: v6 socket meta[%d] config_type=%d expected=%d\n", name, i, m->config_type, CFG_SERVER_TYPE_PUBLIC); return 0; }
printf("PASS [%s]: v6 socket meta[%d] id=%d config=%d nat=%d\n", name, i, m->id, m->config_type, m->nat_type);
printf("PASS [%s]: v6 socket meta[%d] id=%d config=%d mapping=%s inbound=%s\n", name, i, m->id, m->config_type,
address_mapping_name(m->address_profile.mapping), address_inbound_name(m->address_profile.inbound));
i++; m = m->next;
}
}
+110 -512
View File
@@ -1,527 +1,125 @@
/**
* @file test_nat_detection.c
* @brief Интеграционный тест детекции типа NAT через три узла
*
* Топология: C1 -> S <- C2
* - S получает NODEINFO от C1 (direct peer)
* - S использует C2 как третий узел для NAT-пинга C1
* - C2 пингует C1, результат возвращается S
* - S отправляет NAT_INFO C1
* - Проверяем корректность заполнения всех NAT-полей
*/
/* Детектор владеет профилем сокета; помощник с IP наблюдателя заменяется.
* Настоящие ETCP/NCD/PING/STCP, без групповой автосвязности и без подмены времени. */
#include "nat_fixture.h"
#include "ntp_time.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "test_utils.h"
#include "test_net.h"
#include "etcp.h"
#include "etcp_connections.h"
#include "../src/config_parser.h"
#include "../src/config_updater.h"
#include "../src/utun_instance.h"
#include "routing.h"
#include "topo_group.h"
#include "route_ping.h"
#include "nat_detection.h"
#include "topo_node.h"
#include "../src/tun_if.h"
#include "secure_channel.h"
#include "../lib/u_async.h"
#include "../lib/debug_config.h"
#define TEST_TIMEOUT_MS 5000
static uint64_t g_node_id_s = 0;
static uint64_t g_node_id_c1 = 0;
static uint64_t g_node_id_c2 = 0;
static struct UTUN_INSTANCE* inst_s = NULL;
static struct UTUN_INSTANCE* inst_c1 = NULL;
static struct UTUN_INSTANCE* inst_c2 = NULL;
static struct UASYNC* ua = NULL;
static int test_timed_out = 0;
static void* test_timeout_id = NULL;
static char temp_dir[] = "/tmp/utun_nat_test_XXXXXX";
static char config_s[256];
static char config_c1[256];
static char config_c2[256];
static struct {
int done;
int success;
uint16_t avg_rtt;
uint8_t count_sent;
uint8_t count_ok;
} nat_ping_result;
static int write_config(const char* path, const char* content) {
FILE* f = fopen(path, "w");
if (!f) return -1;
fprintf(f, "%s", content);
fclose(f);
return 0;
}
static char* get_pubkey_from_config(const char* path) {
struct utun_config* cfg = parse_config(path);
if (!cfg) return NULL;
char* pub = strdup(cfg->global.my_public_key_hex);
free_config(cfg);
return pub;
}
static int create_temp_configs(void) {
if (test_mkdtemp(temp_dir) != 0) {
fprintf(stderr, "Failed to create temp directory\n");
return -1;
static void wait_profile(struct nat_fixture *fixture, struct ETCP_SOCKET *socket, uint8_t mapping, uint8_t inbound) {
uint64_t end = get_time_tb() + 180000;
while (get_time_tb() < end) {
if (socket->address_profile.evidence_id && socket->address_profile.mapping == mapping &&
socket->address_profile.inbound == inbound) return;
uasync_poll(fixture->ua, 10);
}
snprintf(config_s, sizeof(config_s), "%s/s.conf", temp_dir);
snprintf(config_c1, sizeof(config_c1), "%s/c1.conf", temp_dir);
snprintf(config_c2, sizeof(config_c2), "%s/c2.conf", temp_dir);
const char* tpl_s =
"[global]\n"
"tun_ip=10.100.0.1/24\n"
"tun_ifname=tun100\n"
"\n"
"[server:test_s]\n"
"addr=127.0.0.1:0\n"
"type=public\n"
"\n"
"[allowed_keys]\n"
"allow_all=1\n";
if (write_config(config_s, tpl_s) != 0) return -1;
if (config_ensure_keys_and_node_id(config_s) != 0) return -1;
char* pub_s = get_pubkey_from_config(config_s);
if (!pub_s) return -1;
const char* tpl_c2 =
"[global]\n"
"tun_ip=10.100.0.3/24\n"
"tun_ifname=tun103\n"
"\n"
"[server:test_c2]\n"
"addr=127.0.0.1:0\n"
"type=public\n"
"\n"
"[client:to_s]\n"
"keepalive=1\n"
"peer_public_key=%s\n"
"link=test_c2:127.0.0.1:0\n";
char tpl_c2_full[4096];
snprintf(tpl_c2_full, sizeof(tpl_c2_full), tpl_c2, pub_s);
if (write_config(config_c2, tpl_c2_full) != 0) { free(pub_s); return -1; }
if (config_ensure_keys_and_node_id(config_c2) != 0) { free(pub_s); return -1; }
char* pub_c2 = get_pubkey_from_config(config_c2);
if (!pub_c2) { free(pub_s); return -1; }
const char* tpl_c1 =
"[global]\n"
"tun_ip=10.100.0.2/24\n"
"tun_ifname=tun102\n"
"\n"
"[server:test_c1]\n"
"addr=127.0.0.1:0\n"
"type=nat\n"
"\n"
"[client:to_s]\n"
"keepalive=1\n"
"peer_public_key=%s\n"
"link=test_c1:127.0.0.1:0\n";
char tpl_c1_full[4096];
snprintf(tpl_c1_full, sizeof(tpl_c1_full), tpl_c1, pub_s);
free(pub_s);
if (write_config(config_c1, tpl_c1_full) != 0) { free(pub_c2); return -1; }
if (config_ensure_keys_and_node_id(config_c1) != 0) { free(pub_c2); return -1; }
free(pub_c2);
return 0;
struct nat_check_status status = {0};
nat_detection_get_status(socket->instance->nat_det, socket, &status);
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "profile wait failed socket=%s mapping=%s inbound=%s phase=%u reason=%s",
socket->name, address_mapping_name(socket->address_profile.mapping),
address_inbound_name(socket->address_profile.inbound), status.phase, nat_check_reason_name(status.reason));
abort();
}
static void cleanup_temp_configs(void) {
test_unlink(config_s);
test_unlink(config_c1);
test_unlink(config_c2);
test_rmdir(temp_dir);
}
static void check_snapshot(struct UTUN_INSTANCE *owner, struct ETCP_SOCKET *socket) {
struct TOPO_NODE *node = topo_node_registry_find(owner->topo_groups, owner->node_id);
assert(node && topo_node_verify(node) == 0);
struct TOPO_SOCKMETA4 *meta = node->v4_sock_meta;
while (meta && meta->id != socket->sock_id) meta = meta->next;
assert(meta && meta->address_profile.evidence_id == socket->address_profile.evidence_id &&
meta->address_profile.checked_at == socket->address_profile.checked_at &&
meta->address_profile.mapping == socket->address_profile.mapping &&
meta->address_profile.inbound == socket->address_profile.inbound);
struct address_profile saved = meta->address_profile;
meta->address_profile.checked_at++;
assert(topo_node_verify(node) < 0);
meta->address_profile = saved;
meta->address_profile.inbound = ADDRESS_IN_CONTACT;
assert(topo_node_verify(node) < 0);
meta->address_profile = saved; assert(topo_node_verify(node) == 0);
static int has_initialized_link(struct UTUN_INSTANCE* inst) {
if (!inst || !inst->connections) return 0;
struct ll_entry* entry = inst->connections->head;
while (entry) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
struct ETCP_CONN* conn = ce->conn;
struct ETCP_LINK* link = conn->links;
while (link) {
if (link->initialized) return 1;
link = link->next;
}
entry = entry->next;
}
return 0;
}
static struct ETCP_LINK* find_link_to_node(struct UTUN_INSTANCE* inst, uint64_t node_id) {
if (!inst || !inst->connections) return NULL;
struct ll_entry* entry = queue_find_data_by_index(inst->connections, (const uint8_t*)&node_id);
return entry ? ((struct conn_queue_entry*)entry->data)->conn->links : NULL;
}
static void test_timeout_cb(void* arg) {
(void)arg;
test_timed_out = 1;
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "test_nat_detection: overall test timeout");
}
static void nat_ping_resp_cb(int success, uint16_t avg_rtt, uint8_t count_sent, uint8_t count_ok, void* arg) {
(void)arg;
nat_ping_result.done = 1;
nat_ping_result.success = success;
nat_ping_result.avg_rtt = avg_rtt;
nat_ping_result.count_sent = count_sent;
nat_ping_result.count_ok = count_ok;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "nat_ping_resp_cb: success=%d avg_rtt=%u sent=%u ok=%u",
success, (unsigned)avg_rtt, (unsigned)count_sent, (unsigned)count_ok);
sqlite3_stmt *statement = NULL;
assert(sqlite3_prepare_v2(owner->topo_sqlite_db,
"SELECT nat_mapping,nat_inbound,nat_evidence,nat_checked FROM node_addresses WHERE node_id=? AND socket_id=?",
-1, &statement, NULL) == SQLITE_OK);
sqlite3_bind_int64(statement, 1, (sqlite3_int64)owner->node_id); sqlite3_bind_int(statement, 2, socket->sock_id);
assert(sqlite3_step(statement) == SQLITE_ROW);
assert(sqlite3_column_int(statement, 0) == saved.mapping && sqlite3_column_int(statement, 1) == saved.inbound);
assert((uint64_t)sqlite3_column_int64(statement, 2) == saved.evidence_id);
assert((uint64_t)sqlite3_column_int64(statement, 3) == saved.checked_at);
sqlite3_finalize(statement);
}
int main(void) {
int test_result = 1;
debug_config_init();
debug_set_level(DEBUG_LEVEL_INFO);
debug_set_categories(DEBUG_CATEGORY_ETCP | DEBUG_CATEGORY_BGP | DEBUG_CATEGORY_ROUTING);
utun_instance_set_tun_init_enabled(0);
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_NAT, DEBUG_LEVEL_DEBUG);
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,
"[server:second]\naddr=127.0.0.5:0\ntype=public\n"
"[server:tcp]\naddr=127.0.0.2:0\ntype=public\ntransport=tcp\n");
struct UTUN_INSTANCE *observer = nat_fixture_node(&fixture, "127.0.0.3", NULL, NULL);
struct UTUN_INSTANCE *same_ip = nat_fixture_node(&fixture, "127.0.0.3", &first, NULL);
struct UTUN_INSTANCE *helper = nat_fixture_node(&fixture, "127.0.0.4", &second, NULL);
struct ETCP_SOCKET *socket = test_etcp_bound_socket(owner, "udp");
struct ETCP_SOCKET *second_socket = test_etcp_bound_socket(owner, "second");
struct ETCP_SOCKET *tcp = test_etcp_bound_socket(owner, "tcp");
struct ETCP_SOCKET *observer_socket = test_etcp_bound_socket(observer, "udp");
assert(socket->sock_id != observer_socket->sock_id);
if (create_temp_configs() != 0) {
fprintf(stderr, "Failed to create temp configs\n");
return 1;
}
/* B уже знает обоих помощниках, но A не отправлял UDP ни одному из них. */
nat_fixture_connect(&fixture, observer, "udp", same_ip);
nat_fixture_connect(&fixture, observer, "udp", helper);
struct ETCP_CONN *connection = nat_fixture_connect(&fixture, owner, "udp", observer);
assert(connection->links && connection->links->remote_socket_id == observer_socket->sock_id);
assert(connection->links->local_as_seen_by_peer.ss_family == AF_INET);
assert(route_ping_addr_equal(&connection->links->local_as_seen_by_peer, &socket->local_addr, 1));
/* Подписанная запись A нужна независимому испытанию настоящего TCP listener. */
nat_fixture_connect(&fixture, observer, "udp", owner);
owner->ntp.reachable = 0; /* ни измерение, ни публикация не требуют интернет-NTP */
nat_detection_request_check_all(owner->nat_det);
wait_profile(&fixture, socket, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_OPEN);
wait_profile(&fixture, second_socket, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_OPEN);
wait_profile(&fixture, tcp, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_OPEN);
ua = uasync_create();
if (!ua) {
cleanup_temp_configs();
return 1;
}
struct nat_check_status status;
assert(nat_detection_get_status(owner->nat_det, socket, &status) == 0);
assert(status.helper_reselections > 0 && status.helper == helper->node_id && status.observer == observer->node_id);
assert(socket->address_profile.evidence_id != second_socket->address_profile.evidence_id);
check_snapshot(owner, socket);
check_snapshot(owner, second_socket);
check_snapshot(owner, tcp);
inst_s = utun_instance_create(ua, config_s);
inst_c1 = utun_instance_create(ua, config_c1);
inst_c2 = utun_instance_create(ua, config_c2);
if (!inst_s || !inst_c1 || !inst_c2) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to create instances");
goto cleanup;
}
/* Профиль B не заимствует профиль A, даже когда транспорт переиспользуется в обе стороны. */
assert(!route_ping_addr_equal(&socket->local_addr, &observer_socket->local_addr, 1));
assert(!route_ping_addr_equal(&socket->local_addr, &test_etcp_bound_socket(helper, "udp")->local_addr, 1));
test_etcp_set_peer(inst_c1, "to_s", inst_s, "test_s");
test_etcp_set_peer(inst_c2, "to_s", inst_s, "test_s");
// Разрешаем NAT check для localhost (для теста)
/* Повторная проверка не объявляет старый контакт независимым. */
uint64_t old_evidence = socket->address_profile.evidence_id;
nat_detection_request_check_all(owner->nat_det);
uint64_t end = get_time_tb() + 180000;
while (get_time_tb() < end && socket->address_profile.evidence_id == old_evidence) uasync_poll(fixture.ua, 10);
assert(socket->address_profile.evidence_id != old_evidence);
assert(socket->address_profile.mapping == ADDRESS_MAP_UNCHANGED && socket->address_profile.inbound == ADDRESS_IN_UNKNOWN);
if (utun_instance_init(inst_s) != 0 || utun_instance_init(inst_c1) != 0 || utun_instance_init(inst_c2) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to init connections");
goto cleanup;
}
/* Закрытие endpoint посреди запроса и переиспользование ID/порта: старые ответы не применяются. */
nat_detection_request_check_all(owner->nat_det);
end = get_time_tb() + 20000;
do { uasync_poll(fixture.ua, 0); nat_detection_get_status(owner->nat_det, socket, &status); }
while (!status.phase && get_time_tb() < end);
assert(status.phase);
uint8_t old_id = socket->sock_id; uint64_t old_generation = socket->generation;
struct CFG_SERVER recreated; memset(&recreated, 0, sizeof(recreated));
recreated.ip = socket->local_addr; recreated.type = CFG_SERVER_TYPE_PUBLIC; recreated.mtu = 1500; recreated.fib = -1;
snprintf(recreated.name, sizeof(recreated.name), "recreated");
etcp_socket_remove(socket);
owner->next_socket_id = old_id;
socket = etcp_socket_add(owner, &recreated);
assert(socket && socket->sock_id == old_id && socket->generation != old_generation);
end = get_time_tb() + 2000;
while (get_time_tb() < end) uasync_poll(fixture.ua, 10);
assert(socket->address_profile.mapping == ADDRESS_MAP_UNKNOWN && socket->address_profile.inbound == ADDRESS_IN_UNKNOWN);
if (topo_groups_get_default(inst_s->topo_groups)) topo_group_set_nat_check_local(topo_groups_get_default(inst_s->topo_groups), 1);
g_node_id_s = inst_s->node_id; g_node_id_c1 = inst_c1->node_id; g_node_id_c2 = inst_c2->node_id;
test_timeout_id = uasync_set_timeout(ua, TEST_TIMEOUT_MS * 10, NULL, test_timeout_cb, "test_nat");
// 1. Wait for links C1->S and C2->S to initialize
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Waiting for ETCP links to initialize...");
// Wait for links to be initialized instead of fixed 100 iterations
while (!test_timed_out) {
if (has_initialized_link(inst_c1) && has_initialized_link(inst_c2)) {
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Links C1->S and C2->S initialized");
break;
}
uasync_poll(ua, 10);
}
if (test_timed_out) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Timeout waiting for links");
goto cleanup;
}
// 2. Wait for BGP exchange so S learns about C1 and C2
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for BGP exchange (S learns C1 and C2)...");
int bgp_wait_cycles = 0;
while (!test_timed_out && bgp_wait_cycles < 500) {
if (topo_groups_get_default(inst_s->topo_groups) && topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c1) != NULL &&
topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c2) != NULL) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "S learned about C1 and C2");
break;
}
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
if (!topo_groups_get_default(inst_s->topo_groups) || topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c1) == NULL ||
topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c2) == NULL) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "S did not learn about C1/C2 in time");
goto cleanup;
}
// Verify C1 socket has config type before NAT detection
{
struct ETCP_SOCKET* s = inst_c1->etcp_sockets;
if (!s || s->type != CFG_SERVER_TYPE_NAT) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: C1 socket type=%d, expected CFG_SERVER_TYPE_NAT(%d) before detection",
s ? (int)s->type : -1, CFG_SERVER_TYPE_NAT);
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "C1 socket has config NAT type before detection");
}
// 3. Wait for NAT detection to complete for C1 - check link on server
struct ETCP_LINK* link_sc1 = find_link_to_node(inst_s, g_node_id_c1);
if (!link_sc1) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "No link to C1 on server");
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for NAT detection to complete for C1...");
bgp_wait_cycles = 0;
while (!test_timed_out && bgp_wait_cycles < 1500) {
if (link_sc1->nat_check_status == NAT_CHECK_EIM) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "NAT detection completed for C1: EIM");
break;
}
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
if (!link_sc1 || link_sc1->nat_check_status != NAT_CHECK_EIM) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NAT detection did not complete for C1");
goto cleanup;
}
// 4. Wait for C1 to receive NAT_INFO (verified type)
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for C1 to receive NAT_INFO...");
bgp_wait_cycles = 0;
while (!test_timed_out && bgp_wait_cycles < 500) {
struct ETCP_SOCKET* sock = inst_c1->etcp_sockets;
int found = 0;
while (sock) {
if (sock->nat_type == NAT_VERIFIED_EIM) { found = 1; break; }
sock = sock->next;
}
if (found) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "C1 received NAT_INFO");
/* Даем время для получения NODEINFO с обновленным local_node */
for (int i = 0; i < 50 && !test_timed_out; i++) {
uasync_poll(ua, 10);
}
break;
}
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
// 5. Verify all NAT fields on server
struct TOPO_GROUP_NODE* node_c1 = topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c1);
if (!node_c1) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "TOPO_GROUP_NODE for C1 disappeared");
goto cleanup;
}
if (link_sc1->nat_check_status != NAT_CHECK_EIM) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: nat_check_status=%d, expected EIM(%d)",
(int)link_sc1->nat_check_status, NAT_CHECK_EIM);
goto cleanup;
}
if (link_sc1->nat_type != NAT_TYPE_EIM) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: nat_type=%d, expected EIM(%d)",
(int)link_sc1->nat_type, NAT_TYPE_EIM);
goto cleanup;
}
if (link_sc1->nat_ip == 0 || link_sc1->nat_port == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: nat_ip/port not set (ip=%08x port=%u)",
(unsigned)link_sc1->nat_ip, (unsigned)link_sc1->nat_port);
goto cleanup;
}
// Verify link nat_type on server
struct ETCP_CONN* conn_sc1 = NULL;
{ struct ll_entry* e = inst_s->connections->head;
while (e) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data;
if (ce->conn->peer_node_id == g_node_id_c1) { conn_sc1 = ce->conn; break; }
e = e->next;
} }
if (!link_sc1 || link_sc1->nat_type != NAT_TYPE_EIM) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: link nat_type not EIM on server");
goto cleanup;
}
// Verify socket nat_type on C1
struct ETCP_SOCKET* sock_c1 = NULL;
struct ETCP_SOCKET* sock = inst_c1->etcp_sockets;
while (sock) {
if (sock->nat_type == NAT_VERIFIED_EIM) {
sock_c1 = sock;
break;
}
sock = sock->next;
}
if (!sock_c1) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: no socket with NAT_VERIFIED_EIM on C1");
goto cleanup;
}
// Verify local_node socket update on C1 (received NAT_INFO updated nodeinfo)
if (topo_groups_get_default(inst_c1->topo_groups) && topo_groups_get_default(inst_c1->topo_groups)->local_node) {
struct TOPO_NODE* lni = topo_node_registry_find(inst_c1->topo_groups, topo_groups_get_default(inst_c1->topo_groups)->local_node->node_id);
if (lni) {
int found = 0;
for (const struct TOPO_SOCKMETA4* m = lni->v4_sock_meta; m; m = m->next) {
if (m->id == sock_c1->sock_id) {
if (m->nat_type != NAT_VERIFIED_EIM) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: local_node socket meta not updated on C1: nat_type=%u expected=%u", m->nat_type, NAT_VERIFIED_EIM);
goto cleanup;
}
found = 1; break;
}
}
if (!found) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: socket not found in local_node meta on C1"); goto cleanup; }
// Verify NAT addr in flat address list
int nat_found = 0;
for (const struct TOPO_ADDR4* a = lni->v4_addrs; a; a = a->next) {
if (a->type == TOPO_ADDR_NAT && a->socket_id == sock_c1->sock_id) {
uint32_t addr_ip; memcpy(&addr_ip, a->addr, 4);
if (addr_ip != link_sc1->nat_ip || a->port != link_sc1->nat_port) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: NAT addr mismatch in local_node: addr=%08x nat_ip=%08x port=%u nat_port=%u",
addr_ip, link_sc1->nat_ip, a->port, link_sc1->nat_port);
goto cleanup;
}
nat_found = 1; break;
}
}
if (!nat_found) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: NAT addr not found in local_node flat addr list on C1"); goto cleanup; }
DEBUG_INFO(DEBUG_CATEGORY_BGP, "local_node meta+addr check PASSED on C1");
}
}
// Wait for C2 to learn updated C1 nodeinfo (with verified NAT type)
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for C2 to learn updated C1 nodeinfo...");
bgp_wait_cycles = 0;
int c2_verified_nat = 0;
struct TOPO_GROUP_NODE* node_c1_on_c2 = NULL;
struct TOPO_NODE* cni = NULL;
while (!test_timed_out && bgp_wait_cycles < 500) {
node_c1_on_c2 = topo_groups_get_default(inst_c2->topo_groups) ? topo_node_find_by_id(topo_groups_get_default(inst_c2->topo_groups), g_node_id_c1) : NULL;
cni = node_c1_on_c2 ? topo_node_registry_find(inst_c2->topo_groups, node_c1_on_c2->node_id) : NULL;
if (node_c1_on_c2 && cni) {
for (const struct TOPO_SOCKMETA4* m = cni->v4_sock_meta; m; m = m->next) {
if (m->nat_type == NAT_VERIFIED_EIM) { c2_verified_nat = 1; break; }
}
if (c2_verified_nat) break;
}
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
if (!c2_verified_nat) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: C2 did not receive updated C1 nodeinfo with verified NAT");
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "C2 received updated C1 nodeinfo with verified NAT");
// 6. Separate STUN check via explicit route_ping_send_req_addr
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Performing explicit STUN ping from S via C2 to C1...");
struct ETCP_CONN* conn_sc2 = NULL;
struct ll_entry* entry = inst_s->connections->head;
while (entry) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
if (ce->conn->peer_node_id == g_node_id_c2) { conn_sc2 = ce->conn; break; }
entry = entry->next;
}
if (!conn_sc2) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: S has no connection to C2");
goto cleanup;
}
memset(&nat_ping_result, 0, sizeof(nat_ping_result));
/* nat_ip уже в network byte order, API тоже принимает NBO */
uint32_t target_ip_net = link_sc1->nat_ip;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "TEST: sending ping with pubkey, nat_ip_net=0x%08x nat_port=%u",
link_sc1->nat_ip, link_sc1->nat_port);
int ret = route_ping_send_req_addr(inst_s->nat_det, topo_groups_get_default(inst_s->topo_groups), conn_sc2,
target_ip_net, link_sc1->nat_port,
3, 10, 200, 3000, nat_ping_resp_cb, NULL,
node_c1_on_c2 ? topo_node_registry_find(inst_c2->topo_groups, node_c1_on_c2->node_id)->public_key : NULL);
if (ret != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "route_ping_send_req_addr failed: %d", ret);
goto cleanup;
}
bgp_wait_cycles = 0;
while (!test_timed_out && bgp_wait_cycles < 500 && !nat_ping_result.done) {
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
if (!nat_ping_result.done) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: STUN ping timeout");
goto cleanup;
}
if (!nat_ping_result.success || nat_ping_result.count_ok == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: STUN ping failed success=%d ok=%u",
nat_ping_result.success, (unsigned)nat_ping_result.count_ok);
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "STUN check PASSED");
// 7. Test route_ping_send_req_addr WITHOUT embedded pubkey (C2 should resolve pubkey from node_id)
// First wait for C2 to learn C1's nodeinfo via BGP exchange from S
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for C2 to learn C1 nodeinfo...");
bgp_wait_cycles = 0;
while (!test_timed_out && bgp_wait_cycles < 500) {
if (topo_groups_get_default(inst_c2->topo_groups) && topo_node_find_by_id(topo_groups_get_default(inst_c2->topo_groups), g_node_id_c1) != NULL) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "C2 learned C1 nodeinfo");
break;
}
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
if (!topo_groups_get_default(inst_c2->topo_groups) || topo_node_find_by_id(topo_groups_get_default(inst_c2->topo_groups), g_node_id_c1) == NULL) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: C2 did not learn C1 nodeinfo in time");
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Testing route ping without embedded pubkey...");
memset(&nat_ping_result, 0, sizeof(nat_ping_result));
/* nat_ip уже в network byte order, API тоже принимает NBO */
target_ip_net = link_sc1->nat_ip;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "TEST: sending ping NO pubkey, nat_ip_net=0x%08x nat_port=%u",
link_sc1->nat_ip, link_sc1->nat_port);
ret = route_ping_send_req_addr(inst_s->nat_det, topo_groups_get_default(inst_s->topo_groups), conn_sc2,
target_ip_net, link_sc1->nat_port,
3, 10, 200, 3000, nat_ping_resp_cb, NULL,
cni->public_key);
if (ret != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "route_ping_send_req_addr (no pubkey) failed: %d", ret);
goto cleanup;
}
bgp_wait_cycles = 0;
while (!test_timed_out && bgp_wait_cycles < 500 && !nat_ping_result.done) {
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
if (!nat_ping_result.done) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: no-pubkey ping timeout");
goto cleanup;
}
if (!nat_ping_result.success || nat_ping_result.count_ok == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: no-pubkey ping failed success=%d ok=%u",
nat_ping_result.success, (unsigned)nat_ping_result.count_ok);
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "No-pubkey ping check PASSED");
DEBUG_INFO(DEBUG_CATEGORY_BGP, "NAT detection test PASSED");
test_result = 0;
cleanup:
if (test_timeout_id) uasync_cancel_timeout(ua, test_timeout_id);
if (inst_s) utun_instance_destroy(inst_s);
if (inst_c1) utun_instance_destroy(inst_c1);
if (inst_c2) utun_instance_destroy(inst_c2);
if (ua) uasync_destroy(ua, 0);
cleanup_temp_configs();
return test_result;
nat_fixture_cleanup(&fixture);
return 0;
}
+11 -5
View File
@@ -294,7 +294,7 @@ static void t8(void* arg) {
memcpy(ni->public_key, pubkey_b, SC_PUBKEY_SIZE);
struct TOPO_SOCKMETA4* sm = memory_pool_alloc(g_a->topo_groups->v4_sock_meta_pool);
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->nat_type = NAT_TYPE_UNKNOWN; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->address_profile = (struct address_profile){0}; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
struct TOPO_ADDR4* addr = memory_pool_alloc(g_a->topo_groups->v4_addr_pool);
if (addr) { addr->addr[0]=127; addr->addr[1]=0; addr->addr[2]=0; addr->addr[3]=1; addr->port=1; addr->type=TOPO_ADDR_INTERFACE; addr->socket_id=0; addr->protocol=TOPO_PROTO_UDP; addr->next=ni->v4_addrs; ni->v4_addrs=addr; }
@@ -343,7 +343,7 @@ static int inject_node(struct UTUN_INSTANCE* inst, uint64_t nid, const uint8_t p
ni->group_ref_count = 0; ni->node_id = nid; ni->ver = 0;
memcpy(ni->public_key, pk, SC_PUBKEY_SIZE);
struct TOPO_SOCKMETA4* sm = memory_pool_alloc(inst->topo_groups->v4_sock_meta_pool);
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->nat_type = NAT_TYPE_UNKNOWN; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->address_profile = (struct address_profile){0}; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
struct TOPO_ADDR4* addr = memory_pool_alloc(inst->topo_groups->v4_addr_pool);
if (addr) { addr->addr[0]=127; addr->addr[1]=0; addr->addr[2]=0; addr->addr[3]=1; addr->port=port; addr->type=TOPO_ADDR_INTERFACE; addr->socket_id=0; addr->protocol=TOPO_PROTO_UDP; addr->next=ni->v4_addrs; ni->v4_addrs=addr; }
if (!topo_node_registry_store(inst->topo_groups, ni)) { u_free(ni); return -1; }
@@ -377,13 +377,19 @@ static void test_compatibility(void) {
interface->sin_addr.s_addr = htonl(UINT32_C(0x08080808));
uint8_t saved_cfg = peer->v4_sock_meta->config_type, saved_self = self->v4_sock_meta->config_type;
peer->v4_sock_meta->config_type = CFG_SERVER_TYPE_LOCAL;
int reverse = node_conn_direct_compatible(g_a, nid_b);
int local = node_conn_direct_compatible(g_a, nid_b);
self->v4_sock_meta->config_type = CFG_SERVER_TYPE_LOCAL;
int local_both = node_conn_direct_compatible(g_a, nid_b);
uint8_t saved_protocol = peer->v4_addrs->protocol;
peer->v4_addrs->protocol = 0;
int incompatible = node_conn_direct_compatible(g_a, nid_b);
peer->v4_addrs->protocol = saved_protocol;
peer->v4_sock_meta->config_type = saved_cfg; self->v4_sock_meta->config_type = saved_self;
socket->interface_addr = saved_interface;
if (!reverse || incompatible || instance_find_conn(g_a, nid_b)) { fail("reverse/local compatibility or read-only contract"); return; }
fprintf(stderr, "[PASS] compatibility: direct, reverse-only, incompatible local and no transport mutation\n");
if (!local || !local_both || incompatible || instance_find_conn(g_a, nid_b)) {
fail("LAN candidate, unsupported protocol, or read-only compatibility contract"); return;
}
fprintf(stderr, "[PASS] compatibility: direct/LAN candidates, unsupported protocol and no transport mutation\n");
}
static void cleanup(void) { test_unlink(ca); test_unlink(cb); test_rmdir(tdir); }
+82 -325
View File
@@ -1,341 +1,98 @@
/**
* @file test_route_ping.c
* @brief Интеграционный тест route_ping через три инстанса
*
* Топология: A -> B -> C
* - A отправляет запрос route_ping на B для узла C
* - B пингует C через ETCP ping
* - C отвечает PONG
* - B отправляет BGP_PING_RESPONSE на A
* - A получает ответ через route_ping_send_req callback
*/
/* Настоящие UDP-наблюдения и отклонение PONG чужого ключа/поколения. */
#include "nat_fixture.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "test_utils.h"
#include "test_net.h"
#include "etcp.h"
#include "etcp_connections.h"
#include "../src/config_parser.h"
#include "../src/config_updater.h"
#include "../src/utun_instance.h"
#include "routing.h"
#include "topo_group.h"
#include "route_ping.h"
#include "../src/tun_if.h"
#include "secure_channel.h"
#include "../lib/u_async.h"
#include "../lib/debug_config.h"
struct result { int done, success; uint8_t data[128]; size_t length; };
#define TEST_TIMEOUT_MS 10000
static uint64_t g_node_id_a = 0;
static uint64_t g_node_id_b = 0;
static uint64_t g_node_id_c = 0;
static struct UTUN_INSTANCE* inst_a = NULL;
static struct UTUN_INSTANCE* inst_b = NULL;
static struct UTUN_INSTANCE* inst_c = NULL;
static struct UASYNC* ua = NULL;
static int test_timed_out = 0;
static void* test_timeout_id = NULL;
static char temp_dir[] = "/tmp/utun_route_ping_test_XXXXXX";
static char config_a[256];
static char config_b[256];
static char config_c[256];
static struct {
int done;
int success;
uint16_t avg_rtt;
uint8_t count_sent;
uint8_t count_ok;
} ping_result;
static int write_config(const char* path, const char* content) {
FILE* f = fopen(path, "w");
if (!f) return -1;
fprintf(f, "%s", content);
fclose(f);
return 0;
static void ping_done(int success, uint16_t rtt, void *arg, uint64_t nonce, const uint8_t *data, size_t length) {
(void)rtt; (void)nonce;
struct result *result = arg; result->done++; result->success = success; result->length = length;
assert(length <= sizeof(result->data)); if (length) memcpy(result->data, data, length);
}
static char* get_pubkey_from_config(const char* path) {
struct utun_config* cfg = parse_config(path);
if (!cfg) return NULL;
char* pub = strdup(cfg->global.my_public_key_hex);
free_config(cfg);
return pub;
static void wait_result(struct nat_fixture *fixture, struct result *result) {
uint64_t end = get_time_tb() + 30000;
while (!result->done && get_time_tb() < end) uasync_poll(fixture->ua, 10);
assert(result->done == 1);
}
static int create_temp_configs(void) {
if (test_mkdtemp(temp_dir) != 0) {
fprintf(stderr, "Failed to create temp directory\n");
return -1;
/* Аутентичный PONG от другого узла с известным nonce. Его source совпадает с
* target ожидающего запроса, поэтому ключ пира обязан проверяться отдельно. */
static void send_foreign_pong(struct UTUN_INSTANCE *sender, struct ETCP_SOCKET *destination, uint64_t nonce) {
uint8_t plain[23] = {0}, encrypted[256], salt[8], obfuscated[32];
plain[2] = 1; plain[3] = ETCP_PONG;
for (unsigned i = 0; i < 8; i++) {
plain[5 + i] = (uint8_t)(sender->node_id >> (56 - i * 8));
plain[13 + i] = (uint8_t)(nonce >> (56 - i * 8));
}
snprintf(config_a, sizeof(config_a), "%s/a.conf", temp_dir);
snprintf(config_b, sizeof(config_b), "%s/b.conf", temp_dir);
snprintf(config_c, sizeof(config_c), "%s/c.conf", temp_dir);
// 1. Config C (no keys)
const char* tpl_c =
"[global]\n"
"tun_ip=10.100.0.3/24\n"
"tun_ifname=tun102\n"
"\n"
"[server:test_c]\n"
"addr=127.0.0.1:0\n"
"type=public\n"
"\n"
"[allowed_keys]\n"
"allow_all=1\n";
if (write_config(config_c, tpl_c) != 0) return -1;
if (config_ensure_keys_and_node_id(config_c) != 0) return -1;
char* pub_c = get_pubkey_from_config(config_c);
if (!pub_c) return -1;
// 2. Config B (no own keys, but needs peer_public_key of C)
char tpl_b[4096];
snprintf(tpl_b, sizeof(tpl_b),
"[global]\n"
"tun_ip=10.100.0.2/24\n"
"tun_ifname=tun101\n"
"\n"
"[server:test_b]\n"
"addr=127.0.0.1:0\n"
"type=public\n"
"\n"
"[allowed_keys]\n"
"allow_all=1\n"
"\n"
"[client:to_c]\n"
"keepalive=1\n"
"peer_public_key=%s\n"
"link=test_b:127.0.0.1:0\n",
pub_c);
free(pub_c);
if (write_config(config_b, tpl_b) != 0) return -1;
if (config_ensure_keys_and_node_id(config_b) != 0) return -1;
char* pub_b = get_pubkey_from_config(config_b);
if (!pub_b) return -1;
// 3. Config A (no own keys, but needs peer_public_key of B)
char tpl_a[4096];
snprintf(tpl_a, sizeof(tpl_a),
"[global]\n"
"tun_ip=10.100.0.1/24\n"
"tun_ifname=tun100\n"
"\n"
"[server:test_a]\n"
"addr=127.0.0.1:0\n"
"type=public\n"
"\n"
"[allowed_keys]\n"
"allow_all=1\n"
"\n"
"[client:to_b]\n"
"keepalive=1\n"
"peer_public_key=%s\n"
"link=test_a:127.0.0.1:0\n",
pub_b);
free(pub_b);
if (write_config(config_a, tpl_a) != 0) return -1;
if (config_ensure_keys_and_node_id(config_a) != 0) return -1;
return 0;
sc_context_t crypto; assert(sc_init_ctx(&crypto, &sender->my_keys) == SC_OK);
assert(sc_set_peer_public_key(&crypto, destination->instance->my_keys.public_key, SC_PEER_PUBKEY_BIN) == SC_OK);
size_t length = 0; assert(sc_encrypt(&crypto, plain, sizeof(plain), encrypted, &length) == SC_OK);
assert(random_bytes(salt, sizeof(salt)) == 0);
sc_obfuscate_pubkey(salt, destination->instance->my_keys.public_key, sender->my_keys.public_key, obfuscated);
memcpy(encrypted + length, salt, sizeof(salt)); memcpy(encrypted + length + sizeof(salt), obfuscated, sizeof(obfuscated));
length += sizeof(salt) + sizeof(obfuscated);
struct ETCP_SOCKET *socket = test_etcp_bound_socket(sender, "udp");
assert(socket_sendto(socket->fd, encrypted, length, (const struct sockaddr *)&destination->local_addr,
sizeof(struct sockaddr_in)) == (ssize_t)length);
}
static void cleanup_temp_configs(void) {
test_unlink(config_a);
test_unlink(config_b);
test_unlink(config_c);
test_rmdir(temp_dir);
}
static int has_initialized_link(struct UTUN_INSTANCE* inst) {
if (!inst || !inst->connections) return 0;
struct ll_entry* entry = inst->connections->head;
while (entry) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
struct ETCP_CONN* conn = ce->conn;
struct ETCP_LINK* link = conn->links;
while (link) {
if (link->initialized) return 1;
link = link->next;
}
entry = entry->next;
}
return 0;
}
static struct ETCP_CONN* find_conn_to_peer(struct UTUN_INSTANCE* inst, uint64_t peer_node_id) {
if (!inst || !inst->connections) return NULL;
struct ll_entry* entry = queue_find_data_by_index(inst->connections, (const uint8_t*)&peer_node_id);
return entry ? ((struct conn_queue_entry*)entry->data)->conn : NULL;
}
static void test_timeout_cb(void* arg) {
(void)arg;
test_timed_out = 1;
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "test_route_ping: overall test timeout");
}
static void ping_resp_cb(int success, uint16_t avg_rtt, uint8_t count_sent, uint8_t count_ok, void* arg) {
(void)arg;
ping_result.done = 1;
ping_result.success = success;
ping_result.avg_rtt = avg_rtt;
ping_result.count_sent = count_sent;
ping_result.count_ok = count_ok;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "ping_resp_cb: success=%d avg_rtt=%u sent=%u ok=%u",
success, (unsigned)avg_rtt, (unsigned)count_sent, (unsigned)count_ok);
static void address_encoding(void) {
struct sockaddr_storage address = {0}, decoded;
struct sockaddr_in *v4 = (struct sockaddr_in *)&address;
v4->sin_family = AF_INET; assert(inet_pton(AF_INET, "198.51.100.7", &v4->sin_addr) == 1); v4->sin_port = htons(1300);
uint8_t wire[ROUTE_ADDR_WIRE_SIZE];
static const uint8_t expected[19] = {4,198,51,100,7,0,0,0,0,0,0,0,0,0,0,0,0,5,20};
assert(route_ping_addr_encode(wire, &address) == 0 && !memcmp(wire, expected, sizeof(wire)));
assert(route_ping_addr_decode(wire, &decoded) == 0 && route_ping_addr_equal(&address, &decoded, 1));
wire[5] = 1; assert(route_ping_addr_decode(wire, &decoded) < 0); /* скрытые байты IPv4 запрещены */
memset(&address, 0, sizeof(address));
struct sockaddr_in6 *v6 = (struct sockaddr_in6 *)&address;
v6->sin6_family = AF_INET6; v6->sin6_port = htons(0xabcd);
assert(inet_pton(AF_INET6, "2001:db8::1234", &v6->sin6_addr) == 1);
assert(route_ping_addr_encode(wire, &address) == 0 && wire[0] == 6 && wire[17] == 0xab && wire[18] == 0xcd);
assert(route_ping_addr_decode(wire, &decoded) == 0 && route_ping_addr_equal(&address, &decoded, 1));
wire[17] = wire[18] = 0; assert(route_ping_addr_decode(wire, &decoded) < 0);
}
int main(void) {
int test_result = 1;
debug_config_init();
debug_set_level(DEBUG_LEVEL_INFO);
debug_set_categories(DEBUG_CATEGORY_ETCP | DEBUG_CATEGORY_BGP | DEBUG_CATEGORY_ROUTING);
utun_instance_set_tun_init_enabled(0);
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_NAT, DEBUG_LEVEL_DEBUG);
address_encoding();
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 *foreign = nat_fixture_node(&fixture, "127.0.0.4", NULL, NULL);
struct ETCP_SOCKET *socket = test_etcp_bound_socket(owner, "udp");
struct ETCP_SOCKET *peer = test_etcp_bound_socket(observer, "udp");
const uint64_t round = UINT64_C(0x0102030405060708);
struct result result = {0};
assert(route_ping_observe(socket, observer->my_keys.public_key, &peer->local_addr, round, 500, ping_done, &result) == 0);
wait_result(&fixture, &result);
struct sockaddr_storage observed;
assert(result.success && route_ping_observation(result.data, result.length, socket, round, &observed) == 0);
assert(route_ping_addr_equal(&observed, &socket->local_addr, 1));
assert(route_ping_observation(result.data, result.length, socket, round + 1, &observed) < 0);
assert(socket->address_profile.mapping == ADDRESS_MAP_UNKNOWN); /* один ответ не назначает NAT-тип */
if (create_temp_configs() != 0) {
fprintf(stderr, "Failed to create temp configs\n");
return 1;
}
memset(&result, 0, sizeof(result));
struct ETCP_SOCKET *untrusted = test_etcp_bound_socket(foreign, "udp");
assert(route_ping_observe(socket, observer->my_keys.public_key, &untrusted->local_addr,
round + 1, 300, ping_done, &result) == 0);
assert(owner->pending_pings);
send_foreign_pong(foreign, socket, owner->pending_pings->nonce);
wait_result(&fixture, &result);
assert(!result.success && !owner->pending_pings);
ua = uasync_create();
if (!ua) {
cleanup_temp_configs();
return 1;
}
memset(&result, 0, sizeof(result));
assert(route_ping_observe(socket, observer->my_keys.public_key, &peer->local_addr, round + 2, 300, ping_done, &result) == 0);
uint64_t generation = socket->generation; socket->generation = generation == UINT64_MAX ? generation - 1 : generation + 1;
wait_result(&fixture, &result); assert(!result.success); socket->generation = generation;
inst_a = utun_instance_create(ua, config_a);
inst_b = utun_instance_create(ua, config_b);
inst_c = utun_instance_create(ua, config_c);
if (!inst_a || !inst_b || !inst_c) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to create instances");
goto cleanup;
}
test_etcp_set_peer(inst_a, "to_b", inst_b, "test_b");
test_etcp_set_peer(inst_b, "to_c", inst_c, "test_c");
if (utun_instance_init(inst_a) != 0 || utun_instance_init(inst_b) != 0 || utun_instance_init(inst_c) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to init connections");
goto cleanup;
}
g_node_id_a = inst_a->node_id; g_node_id_b = inst_b->node_id; g_node_id_c = inst_c->node_id;
test_timeout_id = uasync_set_timeout(ua, TEST_TIMEOUT_MS * 10, NULL, test_timeout_cb, "test_route_ping");
// 1. Wait for links A->B and B->C to initialize
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Waiting for ETCP links to initialize...");
while (!test_timed_out) {
if (has_initialized_link(inst_a) && has_initialized_link(inst_b)) {
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Links initialized");
break;
}
uasync_poll(ua, 10);
}
if (test_timed_out) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Timeout waiting for links");
goto cleanup;
}
// 2. Wait for BGP exchange so B learns C (registry has C's nodeinfo + addresses)
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for BGP exchange (B learns C)...");
int bgp_wait_cycles = 0;
while (!test_timed_out && bgp_wait_cycles < 500) {
struct TOPO_NODE* ni_c = topo_node_registry_find(inst_b->topo_groups, g_node_id_c);
if (ni_c && ni_c->v4_addrs) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "B learned about C");
break;
}
uasync_poll(ua, 10);
bgp_wait_cycles++;
}
if (!topo_node_registry_find(inst_b->topo_groups, g_node_id_c)) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "B did not learn about C in time");
goto cleanup;
}
// 3. Get target info for C and send route ping request from A to B
struct ETCP_CONN* conn_ab = find_conn_to_peer(inst_a, g_node_id_b);
if (!conn_ab) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "A has no connection to B");
goto cleanup;
}
/* Получаем target IP:port из nodeinfo узла C */
/* addr[] в network byte order, port в host byte order */
uint32_t target_ip = 0;
uint16_t target_port = 0;
struct TOPO_GROUP_NODE* nq = topo_groups_get_default(inst_b->topo_groups) ? topo_node_find_by_id(topo_groups_get_default(inst_b->topo_groups), g_node_id_c) : NULL;
struct TOPO_NODE* ni = nq ? topo_node_registry_find(inst_b->topo_groups, nq->node_id) : NULL;
if (nq && ni) {
const struct TOPO_ADDR4* a = ni->v4_addrs;
if (a) {
memcpy(&target_ip, a->addr, 4);
target_port = a->port;
}
}
if (target_ip == 0 || target_port == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Cannot resolve target for node C");
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Sending route ping request A->B for C (%s:%u)",
ip_to_str(&target_ip, AF_INET).str, (unsigned)target_port);
memset(&ping_result, 0, sizeof(ping_result));
int ret = route_ping_send_req_addr(inst_a->nat_det, topo_groups_get_default(inst_a->topo_groups), conn_ab, target_ip, target_port,
3, // count
10, // interval_ms
200, // timeout_ms
5000,// wait_timeout_ms
ping_resp_cb, NULL,
ni ? ni->public_key : NULL);
if (ret != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "route_ping_send_req_addr failed: %d", ret);
goto cleanup;
}
// 4. Wait for response
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for route ping response...");
while (!test_timed_out && !ping_result.done) {
uasync_poll(ua, 10);
}
if (test_timed_out) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Timeout waiting for ping response");
goto cleanup;
}
// 5. Verify results
if (!ping_result.success) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Ping reported failure (success=0)");
goto cleanup;
}
if (ping_result.count_ok == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Ping count_ok is zero");
goto cleanup;
}
if (ping_result.avg_rtt == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Ping avg_rtt is zero");
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Route ping test PASSED: sent=%u ok=%u avg_rtt=%u",
(unsigned)ping_result.count_sent, (unsigned)ping_result.count_ok, (unsigned)ping_result.avg_rtt);
test_result = 0;
cleanup:
if (test_timeout_id) uasync_cancel_timeout(ua, test_timeout_id);
if (inst_a) utun_instance_destroy(inst_a);
if (inst_b) utun_instance_destroy(inst_b);
if (inst_c) utun_instance_destroy(inst_c);
if (ua) uasync_destroy(ua, 0);
cleanup_temp_configs();
return test_result;
memset(&result, 0, sizeof(result));
assert(route_ping_observe(socket, observer->my_keys.public_key, &peer->local_addr, round + 3, 500, ping_done, &result) == 0);
etcp_cancel_pings(owner, ping_done, &result); assert(!owner->pending_pings);
uint64_t end = get_time_tb() + 6000;
while (get_time_tb() < end) uasync_poll(fixture.ua, 10);
assert(!result.done);
nat_fixture_cleanup(&fixture);
return 0;
}
+40 -82
View File
@@ -1,88 +1,46 @@
/**
* @file test_sock_match.c
* @brief Unit-тесты классификации сокетов и сопоставления адресов (sock_match).
*
* Тестирует только чистую логику sock_role_of / sock_target_class / sock_match
* без сети и без UTUN_INSTANCE — с синтетическими struct sock_view и адресами.
*/
#include <stdio.h>
/* Политика не заменяет измерение; неопределённость разрешает ограниченную пробу. */
#include <assert.h>
#include <string.h>
#include "../src/routing_layer/sock_match.h"
#include "../src/config_parser.h"
#include "../src/transport_layer/etcp_connections.h"
#include "../src/routing_layer/topo_node.h"
#include "routing_layer/sock_match.h"
#include "routing_layer/topo_node.h"
#include "config_parser.h"
#include "../lib/platform_compat.h"
static int fails = 0;
static int checks = 0;
#define CHECK(cond, msg) do { checks++; if (!(cond)) { fails++; printf("FAIL: %s\n", msg); } } while (0)
static struct sock_view mk_view(uint8_t role, uint8_t has_inet, uint8_t has_priv, uint8_t is_eim, uint32_t pub4) {
struct sock_view v; memset(&v, 0, sizeof(v));
v.role = role; v.has_inet = has_inet; v.has_priv = has_priv; v.is_eim = is_eim; v.public_ip4 = pub4;
return v;
}
/* Классификация IPv4-адреса (network byte order): публичный/маршрутизируемый или нет.
* Адрес задаём «логически» (старший байт = первый октет) и переводим в s_addr через htonl. */
static void test_ip_is_public(void) {
CHECK(ip_is_public(htonl(0x08080808u)) == 1, "8.8.8.8 public");
CHECK(ip_is_public(htonl(0x01020304u)) == 1, "1.2.3.4 public");
CHECK(ip_is_public(htonl(0xC0A80101u)) == 0, "192.168.1.1 private");
CHECK(ip_is_public(htonl(0x0A000001u)) == 0, "10.0.0.1 private");
CHECK(ip_is_public(htonl(0xAC100001u)) == 0, "172.16.0.1 private");
CHECK(ip_is_public(htonl(0xAC1F0001u)) == 0, "172.31.0.1 private");
CHECK(ip_is_public(htonl(0x64400001u)) == 0, "100.64.0.1 CGNAT");
CHECK(ip_is_public(htonl(0x7F000001u)) == 0, "127.0.0.1 loopback");
CHECK(ip_is_public(htonl(0xA9FE0101u)) == 0, "169.254.1.1 link-local");
CHECK(ip_is_public(htonl(0x00000000u)) == 0, "0.0.0.0");
CHECK(ip_is_public(htonl(0xE0000001u)) == 0, "224.0.0.1 multicast");
CHECK(ip_is_public(htonl(0xFFFFFFFFu)) == 0, "255.255.255.255 reserved");
}
static void test_target_class(void) {
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PUBLIC, NAT_TYPE_UNKNOWN) == SOCK_TC_PUB, "public INTERFACE -> PUB");
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_UNKNOWN, NAT_TYPE_UNKNOWN) == SOCK_TC_PUB, "unknown -> PUB");
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_LOCAL, NAT_TYPE_UNKNOWN) == SOCK_TC_PRIV, "local -> PRIV");
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PRIVATE, NAT_TYPE_UNKNOWN) == SOCK_TC_PRIV, "private -> PRIV");
CHECK(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_EIM) == SOCK_TC_PUB, "nat+EIM NAT-addr -> PUB");
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_EIM) == SOCK_TC_PRIV, "nat+EIM INTERFACE -> PRIV");
CHECK(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_DIRECT) == SOCK_TC_PUB, "nat+DIRECT NAT-addr -> PUB");
CHECK(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_STRICT) == SOCK_TC_UNREACH, "nat+STRICT NAT-addr -> UNREACH");
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_STRICT) == SOCK_TC_PRIV, "nat+STRICT INTERFACE -> PRIV");
CHECK(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_TYPE_UNKNOWN) == SOCK_TC_UNREACH, "nat+unknown NAT-addr -> UNREACH");
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, NAT_TYPE_UNKNOWN) == SOCK_TC_PRIV, "nat+unknown INTERFACE -> PRIV");
}
static void test_match(void) {
uint32_t pub = 0x01020304u; /* произвольный "публичный" IP */
struct sock_view direct = mk_view(SOCK_ROLE_DIRECT, 1, 0, 0, pub);
struct sock_view natout = mk_view(SOCK_ROLE_NAT, 1, 1, 0, 0);
struct sock_view local = mk_view(SOCK_ROLE_LOCAL, 0, 1, 0, 0);
/* наш DIRECT (публичный) */
CHECK(sock_match(&direct, TOPO_ADDR_NAT, CFG_SERVER_TYPE_PUBLIC, NAT_TYPE_UNKNOWN, pub) == 1, "DIRECT->PUB ok");
CHECK(sock_match(&direct, TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_LOCAL, NAT_TYPE_UNKNOWN, 0) == 0, "DIRECT->PRIV нет приватного интерфейса");
/* наш NAT (outbound-only) */
CHECK(sock_match(&natout, TOPO_ADDR_NAT, CFG_SERVER_TYPE_PUBLIC, NAT_TYPE_UNKNOWN, pub) == 1, "NAT->PUB ok");
CHECK(sock_match(&natout, TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_STRICT, 0) == 1, "NAT->PRIV (есть приватный)");
/* наш LOCAL (без интернета) */
CHECK(sock_match(&local, TOPO_ADDR_NAT, CFG_SERVER_TYPE_PUBLIC, NAT_TYPE_UNKNOWN, pub) == 0, "LOCAL->PUB нет интернета");
CHECK(sock_match(&local, TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_LOCAL, NAT_TYPE_UNKNOWN, 0) == 1, "LOCAL->PRIV ок (LAN)");
/* UNREACH цель */
CHECK(sock_match(&direct, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_STRICT, pub) == 0, "строгий NAT-адрес недостижим");
/* hairpin: оба EIM, одинаковый публичный IP */
struct sock_view eim = mk_view(SOCK_ROLE_DIRECT, 1, 1, 1, pub);
CHECK(sock_match(&eim, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_EIM, pub) == 0, "hairpin (одинаковый публичный IP)");
CHECK(sock_match(&eim, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_EIM, 0x05060708u) == 1, "EIM разные публичные IP -> ok");
}
#include "../lib/debug_config.h"
int main(void) {
test_ip_is_public();
test_target_class();
test_match();
if (fails) { printf("RESULT: %d/%d failed\n", fails, checks); return 1; }
printf("RESULT: all %d checks passed\n", checks);
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
const uint64_t now = UINT64_C(1700000000000000);
const uint32_t public_ip = htonl(UINT32_C(0xc633640b));
const uint32_t private_ip = htonl(UINT32_C(0x0a400102));
struct address_profile unknown = {0};
struct address_profile open = {.mapping = ADDRESS_MAP_EIM, .inbound = ADDRESS_IN_OPEN, .evidence_id = 1, .checked_at = now};
struct address_profile contact = open; contact.inbound = ADDRESS_IN_CONTACT;
struct address_profile dependent = open; dependent.mapping = ADDRESS_MAP_DEPENDENT; dependent.inbound = ADDRESS_IN_UNKNOWN;
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PUBLIC, &unknown, 1, now) == SOCK_TC_PROBE);
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PUBLIC, &open, 0, now) == SOCK_TC_PRIV);
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, 1, now) == SOCK_TC_PUB);
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &contact, 1, now) == SOCK_TC_CONTACT);
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &unknown, 1, now) == SOCK_TC_PROBE);
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &dependent, 1, now) == SOCK_TC_UNREACH);
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, &dependent, 0, now) == SOCK_TC_PRIV);
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_LOCAL, &open, 1, now) == SOCK_TC_PRIV);
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PRIVATE, &open, 1, now) == SOCK_TC_PRIV);
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, 1, now + ADDRESS_PROFILE_TTL_US + 1) == SOCK_TC_PROBE);
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, 1, now - 1) == SOCK_TC_PROBE);
struct sock_view outgoing = {.has_inet = 1, .has_priv = 1, .now_us = now, .public_ip4 = public_ip};
struct sock_view local = {.has_priv = 1, .now_us = now};
assert(sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, public_ip));
assert(sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &contact, public_ip));
/* Совпадение публичного IP разрешает проверку hairpin. */
outgoing.address_profile = open;
assert(sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, public_ip));
assert(!sock_match(&local, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, public_ip));
assert(sock_match(&local, TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, &dependent, private_ip));
assert(!sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &dependent, public_ip));
outgoing.now_us = now + ADDRESS_PROFILE_TTL_US + 1;
assert(sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, public_ip));
/* Смена времени/давности снижает уверенность, сохраняя возможность реального handshake. */
return 0;
}