diff --git a/src/Makefile.am b/src/Makefile.am index 95be5b92..b0751274 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -15,6 +15,7 @@ utun_CORE_SOURCES = \ routing_layer/topo_node.c \ routing_layer/topo_node_sqlite.c \ routing_layer/route_connectivity.c \ + routing_layer/sock_match.c \ routing_layer/conn_mgr_core.c \ routing_layer/conn_mgr_indirect.c \ routing_layer/conn_mgr_monitor.c \ @@ -104,6 +105,7 @@ libutun_a_SOURCES = \ routing_layer/topo_node.c \ routing_layer/topo_node_sqlite.c \ routing_layer/route_connectivity.c \ + routing_layer/sock_match.c \ routing_layer/conn_mgr_core.c \ routing_layer/conn_mgr_indirect.c \ routing_layer/conn_mgr_monitor.c \ diff --git a/src/ntp_time.c b/src/ntp_time.c index 94b18b52..f014c988 100644 --- a/src/ntp_time.c +++ b/src/ntp_time.c @@ -1,6 +1,7 @@ #include "ntp_time.h" #include "ntp_node_time.h" #include "utun_instance.h" +#include "../routing_layer/topo_node.h" #include "../lib/debug_config.h" #include "../lib/socket_compat.h" #include "../lib/u_async.h" @@ -294,6 +295,15 @@ static void ntp_finish_cycle(struct ntp_sync* ctx, int synced) { ntp_node_sync_peers(inst); } + /* Доступность интернет-NTP меняет классификацию сокетов (LOCAL при оффлайне) */ + { int prev = ntp->reachable; + ntp->reachable = synced ? 1 : 0; + if (prev != ntp->reachable) { + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP: reachability changed %d -> %d, updating nodeinfo", prev, ntp->reachable); + topo_node_update_my_addresses(inst); + } + } + ntp->timer = uasync_set_timeout(inst->ua, ntp->resync_interval_sec * 10000, inst, ntp_time_sync_cb, "ntp_sync"); } @@ -343,6 +353,7 @@ int ntp_time_init(struct UTUN_INSTANCE* instance) { struct global_config* g = &instance->config->global; ntp->enabled = g->ntp_enabled; ntp->synced = 0; + ntp->reachable = 1; /* оптимистично: до первого результата считаем, что интернет есть */ ntp->offset_us = 0; ntp->last_sync_tb = 0; ntp->timer = NULL; diff --git a/src/ntp_time.h b/src/ntp_time.h index 9deb4356..ad3d2aa4 100644 --- a/src/ntp_time.h +++ b/src/ntp_time.h @@ -17,6 +17,7 @@ struct UTUN_INSTANCE; struct NTP_TIME { int enabled; int synced; + int reachable; // доступность интернет-NTP по циклам (1=есть интернет, 0=нет); для классификации сокетов int64_t offset_us; // local_time - ntp_time (positive = local clock ahead) uint64_t last_sync_tb; // monotonic time of last successful sync (0.1ms units) void* timer; // uasync timer handle diff --git a/src/routing_layer/conn_mgr_core.c b/src/routing_layer/conn_mgr_core.c index 82f6b5c6..fbae4570 100644 --- a/src/routing_layer/conn_mgr_core.c +++ b/src/routing_layer/conn_mgr_core.c @@ -20,6 +20,7 @@ #include "config_parser.h" #include "topo_node.h" #include "topo_node_sqlite.h" +#include "sock_match.h" #include "topo_group.h" #include "route_ping.h" #ifdef UTUN_HAVE_STANDBY @@ -70,19 +71,6 @@ void cm_deliver_event(struct CONN_MGR_ENTRY* entry, enum conn_mgr_event event) { /* ═══════ NAT / адресные утилиты ═══════ */ -/* Проверяет NAT-совместимость нашего сокета с целевым: возвращает 1 (публичный/EIM — - * можно пробить), 2 (локальная сеть — оба PRIVATE), 0 (несовместим). Используется - * в DIRECT фазе чтобы не создавать заведомо непробиваемые линки. */ -int cm_nat_compatible(struct ETCP_SOCKET* our, uint8_t tcfg, uint8_t tnat) { - uint8_t tt = tnat >= NAT_VERIFIED_UNKNOWN ? CFG_SERVER_TYPE_PUBLIC : tcfg; - uint8_t ot = our->type; - if (ot == CFG_SERVER_TYPE_PUBLIC || ot == CFG_SERVER_TYPE_UNKNOWN) return 1; - if (tt == CFG_SERVER_TYPE_PUBLIC) return 1; /* цель публичная/проверенная — исходящее работает из-за любого NAT */ - if (ot == CFG_SERVER_TYPE_NAT && our->nat_type == NAT_VERIFIED_EIM && tt == CFG_SERVER_TYPE_NAT) return 1; - if ((ot == CFG_SERVER_TYPE_PRIVATE || ot == CFG_SERVER_TYPE_LOCAL) && tt == CFG_SERVER_TYPE_PRIVATE) return 2; - return 0; -} - /* Классификация IPv6-адреса: link-local (fe80), локальный ULA (fc/fd), * глобальный/прямой, другое (loopback, multicast, ::). */ uint8_t cm_classify_v6_addr(const uint8_t addr[16]) { @@ -158,7 +146,7 @@ int cm_has_local_addr(struct CONN_MGR* mgr, struct TOPO_GROUP_NODE* nq) { for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) { if (a->type != TOPO_ADDR_INTERFACE) continue; for (const struct TOPO_SOCKMETA4* m = ni->v4_sock_meta; m; m = m->next) - if (m->id == a->socket_id && (m->config_type == CFG_SERVER_TYPE_PRIVATE || m->config_type == CFG_SERVER_TYPE_LOCAL)) return 1; + if (m->id == a->socket_id && sock_target_class(a->type, m->config_type, m->nat_type) == SOCK_TC_PRIV) return 1; } for (const struct TOPO_ADDR6* a6 = ni->v6_addrs; a6; a6 = a6->next) { if (a6->type != TOPO_ADDR_INTERFACE) continue; @@ -540,14 +528,16 @@ static void cm_ping_cb_impl(int success, uint16_t rtt, void* arg, uint64_t nonce /* ═══════ DIRECT фаза (NCD + ручная NAT-фильтрация линков) ═══════ */ /* Добавляет IPv4/IPv6 линки к conn. Единый цикл для адресов из BGP и из базы: - * протокол из a->protocol (TCP — без NAT-фильтра, UDP — NAT-фильтрация), - * приоритет NAT > INTERFACE. Если ни одного линка не создалось — закрывает NCD + * протокол из a->protocol, приоритет NAT > INTERFACE, сопоставление сокетов + * через sock_match (UDP и TCP). Если ни одного линка не создалось — закрывает NCD * и переходит к REVERSE/INDIRECT (для db_loaded — cleanup + TIMEOUT). */ +#define CM_MAX_SOCKET_VIEWS 32 static void cm_direct_add_links(struct ETCP_CONN* conn, struct CONN_MGR_ENTRY* entry, struct TOPO_NODE* ni, int db_loaded) { int any = 0, v4_cnt = 0, v6_cnt = 0, v4_tcp = 0, v6_tcp = 0; - /* v4 — единый цикл для адресов из BGP и из базы: протокол из a->protocol, - * приоритет NAT > INTERFACE, NAT-фильтрация только для UDP */ + /* Плоский список наших сокетов: роль + локальный/публичный IP + тип */ + struct sock_view views[CM_MAX_SOCKET_VIEWS]; + int nviews = sock_collect_views(entry->mgr->instance, views, CM_MAX_SOCKET_VIEWS); uint8_t prio[] = {TOPO_ADDR_NAT, TOPO_ADDR_INTERFACE}; for (int pr = 0; pr < 2; pr++) { for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) { @@ -555,21 +545,26 @@ static void cm_direct_add_links(struct ETCP_CONN* conn, struct CONN_MGR_ENTRY* e struct sockaddr_in sin; memset(&sin,0,sizeof(sin)); sin.sin_family=AF_INET; memcpy(&sin.sin_addr.s_addr,a->addr,4); sin.sin_port=htons(a->port); struct sockaddr_storage sa; memcpy(&sa,&sin,sizeof(sin)); + uint32_t tip; memcpy(&tip, a->addr, 4); + const struct TOPO_SOCKMETA4* m = NULL; + for (const struct TOPO_SOCKMETA4* mm = ni->v4_sock_meta; mm; mm = mm->next) + if (mm->id == a->socket_id) { m = mm; break; } + uint8_t tcfg = m ? m->config_type : CFG_SERVER_TYPE_UNKNOWN; + uint8_t tnat = m ? m->nat_type : NAT_TYPE_UNKNOWN; if (a->protocol & TOPO_PROTO_TCP) { - struct ETCP_SOCKET* s = entry->mgr->instance->etcp_sockets; - while (s) { if (s->is_tcp && s->local_addr.ss_family == AF_INET && s->type != CFG_SERVER_TYPE_PRIVATE) { - struct ETCP_LINK *tlink = etcp_link_new(conn, s, &sa, 0); + for (int vi = 0; vi < nviews; vi++) { + const struct sock_view* v = &views[vi]; + if (!v->is_tcp || !sock_match(v, a->type, tcfg, tnat, tip)) continue; + struct ETCP_LINK *tlink = etcp_link_new(conn, v->sock, &sa, 0); if (tlink) { tlink->is_tcp = 1; topo_node_apply_reality(tlink, topo_node_find_reality_sock(ni, a->socket_id)); etcp_tcp_link_start_connect(tlink, &sa, a->port); any = 1; v4_tcp++; v4_cnt++; } - } s = s->next; } + } } if (a->protocol & TOPO_PROTO_UDP) { - for (const struct TOPO_SOCKMETA4* m = ni->v4_sock_meta; m; m = m->next) { - if (m->id != a->socket_id) continue; - struct ETCP_SOCKET* s = entry->mgr->instance->etcp_sockets; - while (s) { if (cm_nat_compatible(s, m->config_type, m->nat_type) && s->local_addr.ss_family == AF_INET) { - cm_warn_addr_busy(conn, s, &sa); - if (etcp_link_new(conn, s, &sa, 0)) { any = 1; v4_cnt++; } } s = s->next; } - break; + for (int vi = 0; vi < nviews; vi++) { + const struct sock_view* v = &views[vi]; + if (v->is_tcp || !sock_match(v, a->type, tcfg, tnat, tip)) continue; + cm_warn_addr_busy(conn, v->sock, &sa); + if (etcp_link_new(conn, v->sock, &sa, 0)) { any = 1; v4_cnt++; } } } } diff --git a/src/routing_layer/conn_mgr_priv.h b/src/routing_layer/conn_mgr_priv.h index 9d195860..8705879a 100644 --- a/src/routing_layer/conn_mgr_priv.h +++ b/src/routing_layer/conn_mgr_priv.h @@ -146,7 +146,6 @@ void cm_handle_direct_req(struct ETCP_CONN* conn, struct CONN_MGR* mgr, const ui void cm_handle_disconnect(struct CONN_MGR* mgr, uint64_t node_id); void cm_reverse_init_cb(struct ETCP_CONN* conn, int event, void* arg); void cm_reverse_timeout_cb(void* arg); -int cm_nat_compatible(struct ETCP_SOCKET* our, uint8_t tcfg, uint8_t tnat); void cm_add_v4_link(struct ETCP_CONN* conn, const uint8_t* addr, uint16_t port, struct ETCP_SOCKET* s); void cm_add_v6_link(struct ETCP_CONN* conn, const uint8_t addr[16], uint16_t port, struct ETCP_SOCKET* s); int cm_has_direct_ip(struct CONN_MGR* mgr, struct TOPO_GROUP_NODE* nq); diff --git a/src/routing_layer/route_connectivity.c b/src/routing_layer/route_connectivity.c index 1dbee45f..72622dbf 100644 --- a/src/routing_layer/route_connectivity.c +++ b/src/routing_layer/route_connectivity.c @@ -17,6 +17,7 @@ #include "topo_group.h" #include "route_connectivity.h" #include "topo_node_sqlite.h" +#include "sock_match.h" #ifdef UTUN_HAVE_STANDBY #include "standby.h" #endif @@ -80,9 +81,9 @@ static int addr_eq(const struct TOPO_ADDR4* a, uint32_t ip, uint16_t port) { } // собирает список локальных сокетов-кандидатов для probing заданного адреса -// сортирует: лучшие (по совпадению подсети / типу NAT) первые +// (сопоставление через sock_match — роли сокетов и классы адресов) static int conn_match_candidate_sockets(struct UTUN_INSTANCE* instance, - uint8_t addr_type, + uint8_t addr_type, uint8_t tcfg, uint8_t tnat, const struct sockaddr_storage* target_addr, struct ETCP_SOCKET** out_sockets, uint8_t max_count) { if (!instance || !target_addr || !out_sockets || max_count == 0) return 0; @@ -120,69 +121,14 @@ static int conn_match_candidate_sockets(struct UTUN_INSTANCE* instance, return found; } - // IPv4 target — existing logic + // IPv4: фильтруем наши сокеты через sock_match uint32_t target_ip = ((const struct sockaddr_in*)target_addr)->sin_addr.s_addr; - e_sock = instance->etcp_sockets; - - // Проход 1: точное совпадение подсети (для INTERFACE) или PUBLIC/NAT_VERIFIED (для NAT) - while (e_sock && found < (int)max_count) { - if (e_sock->is_tcp) { e_sock = e_sock->next; continue; } - if (e_sock->local_addr.ss_family != AF_INET) { e_sock = e_sock->next; continue; } - int match = 0; - struct sockaddr_in* if_sin = (struct sockaddr_in*)&e_sock->interface_addr; - uint32_t if_ip = if_sin->sin_addr.s_addr; - - if (addr_type == TOPO_ADDR_INTERFACE) { - if ((e_sock->type == CFG_SERVER_TYPE_PRIVATE || e_sock->type == CFG_SERVER_TYPE_LOCAL) - && (if_ip & 0x00FFFFFF) == (target_ip & 0x00FFFFFF)) - match = 1; - else if (e_sock->nat_type == NAT_VERIFIED_DIRECT || e_sock->nat_type == NAT_VERIFIED_EIM - || e_sock->type == CFG_SERVER_TYPE_PUBLIC || e_sock->type == CFG_SERVER_TYPE_UNKNOWN) - match = 1; - } else { - if (e_sock->nat_type == NAT_VERIFIED_DIRECT) match = 1; - else if (e_sock->nat_type == NAT_VERIFIED_EIM) match = 1; - else if (e_sock->type == CFG_SERVER_TYPE_PUBLIC || e_sock->type == CFG_SERVER_TYPE_UNKNOWN) match = 1; - } - - if (match) { - int dup = 0; - for (int i = 0; i < found; i++) if (out_sockets[i] == e_sock) { dup = 1; break; } - if (!dup) out_sockets[found++] = e_sock; - } - e_sock = e_sock->next; - } - - // Проход 2: все остальные подходящие - e_sock = instance->etcp_sockets; - while (e_sock && found < (int)max_count) { - if (e_sock->is_tcp) { e_sock = e_sock->next; continue; } - if (e_sock->local_addr.ss_family != AF_INET) { e_sock = e_sock->next; continue; } - int dup = 0; - for (int i = 0; i < found; i++) if (out_sockets[i] == e_sock) { dup = 1; break; } - if (!dup) { - if (addr_type == TOPO_ADDR_INTERFACE) { - if (e_sock->type == CFG_SERVER_TYPE_PRIVATE || e_sock->type == CFG_SERVER_TYPE_LOCAL - || e_sock->nat_type == NAT_VERIFIED_DIRECT || e_sock->nat_type == NAT_VERIFIED_EIM - || e_sock->type == CFG_SERVER_TYPE_PUBLIC || e_sock->type == CFG_SERVER_TYPE_UNKNOWN) - out_sockets[found++] = e_sock; - } else { - if (e_sock->type != CFG_SERVER_TYPE_PRIVATE && e_sock->type != CFG_SERVER_TYPE_LOCAL) - out_sockets[found++] = e_sock; - } - } - e_sock = e_sock->next; - } - - // Проход 3: остальные IPv4 (fallback) - e_sock = instance->etcp_sockets; - while (e_sock && found < (int)max_count) { - if (e_sock->is_tcp) { e_sock = e_sock->next; continue; } - if (e_sock->local_addr.ss_family != AF_INET) { e_sock = e_sock->next; continue; } - int dup = 0; - for (int i = 0; i < found; i++) if (out_sockets[i] == e_sock) { dup = 1; break; } - if (!dup) out_sockets[found++] = e_sock; - e_sock = e_sock->next; + struct sock_view views[CONN_MAX_SOCKET_CANDIDATES]; + int nviews = sock_collect_views(instance, views, CONN_MAX_SOCKET_CANDIDATES); + for (int i = 0; i < nviews && found < (int)max_count; i++) { + if (views[i].is_tcp) continue; + if (!sock_match(&views[i], addr_type, tcfg, tnat, target_ip)) continue; + out_sockets[found++] = views[i].sock; } return found; @@ -377,8 +323,11 @@ void route_connectivity_probe_node(struct UTUN_INSTANCE* instance, struct TOPO_G ctx->is_tcp = 1; ctx->candidate_count = 1; ctx->candidate_index = 0; ctx->count_total = 1; } else { + uint8_t tcfg = CFG_SERVER_TYPE_UNKNOWN, tnat = NAT_TYPE_UNKNOWN; + for (const struct TOPO_SOCKMETA4* mm = ni->v4_sock_meta; mm; mm = mm->next) + if (mm->id == a->socket_id) { tcfg = mm->config_type; tnat = mm->nat_type; break; } struct ETCP_SOCKET* candidates[CONN_MAX_SOCKET_CANDIDATES]; - int cand_count = conn_match_candidate_sockets(instance, a->type, &target, candidates, CONN_MAX_SOCKET_CANDIDATES); + int cand_count = conn_match_candidate_sockets(instance, a->type, tcfg, tnat, &target, candidates, CONN_MAX_SOCKET_CANDIDATES); if (cand_count == 0) { DEBUG_INFO(DEBUG_CATEGORY_BGP, "probe: no local sockets for target %s:%u type=%d", ip_to_str(&ip, AF_INET).str, port, a->type); u_free(ctx); continue; } memcpy(ctx->candidate_sockets, candidates, cand_count * sizeof(struct ETCP_SOCKET*)); ctx->candidate_count = cand_count; ctx->candidate_index = 0; ctx->count_total = CONN_PROBE_COUNT; @@ -413,7 +362,7 @@ void route_connectivity_probe_node(struct UTUN_INSTANCE* instance, struct TOPO_G ctx->candidate_count = 1; ctx->candidate_index = 0; ctx->count_total = 1; } else { struct ETCP_SOCKET* candidates[CONN_MAX_SOCKET_CANDIDATES]; - int cand_count = conn_match_candidate_sockets(instance, a6->type, &target, candidates, CONN_MAX_SOCKET_CANDIDATES); + int cand_count = conn_match_candidate_sockets(instance, a6->type, CFG_SERVER_TYPE_UNKNOWN, NAT_TYPE_UNKNOWN, &target, candidates, CONN_MAX_SOCKET_CANDIDATES); if (cand_count == 0) { DEBUG_INFO(DEBUG_CATEGORY_BGP, "probe: no local sockets for IPv6 target type=%d", a6->type); u_free(ctx); continue; } memcpy(ctx->candidate_sockets, candidates, cand_count * sizeof(struct ETCP_SOCKET*)); ctx->candidate_count = cand_count; ctx->candidate_index = 0; ctx->count_total = CONN_PROBE_COUNT; diff --git a/src/routing_layer/sock_match.c b/src/routing_layer/sock_match.c new file mode 100644 index 00000000..8ac1582b --- /dev/null +++ b/src/routing_layer/sock_match.c @@ -0,0 +1,82 @@ +/* sock_match.c — классификация сокетов и сопоставление адресов при подключении */ +#include "sock_match.h" + +#include +#include "../lib/platform_compat.h" +#include "../lib/debug_config.h" +#include "utun_instance.h" +#include "etcp_connections.h" +#include "config_parser.h" +#include "topo_node.h" +#include "ntp_time.h" + +/* Нормализация NAT-типа: сырые (NAT_TYPE_*) или проверенные (NAT_VERIFIED_*) значения. */ +static int nat_is_eim(uint8_t n) { return n == NAT_TYPE_EIM || n == NAT_VERIFIED_EIM; } +static int nat_is_direct(uint8_t n) { return n == NAT_TYPE_DIRECT || n == NAT_VERIFIED_DIRECT; } + +uint8_t sock_role_of(uint8_t type, uint8_t nat_type, int ntp_reachable) { + uint8_t role; + if (type == CFG_SERVER_TYPE_LOCAL) + role = SOCK_ROLE_LOCAL; /* без интернета (из конфига) */ + else if (type == CFG_SERVER_TYPE_PUBLIC || type == CFG_SERVER_TYPE_UNKNOWN) + role = SOCK_ROLE_DIRECT; + else if (type == CFG_SERVER_TYPE_NAT) + role = (nat_is_eim(nat_type) || nat_is_direct(nat_type)) ? SOCK_ROLE_DIRECT : SOCK_ROLE_NAT; + else + role = SOCK_ROLE_NAT; /* PRIVATE — скрытый, только исходящие */ + if (!ntp_reachable && role != SOCK_ROLE_LOCAL) + role = SOCK_ROLE_LOCAL; /* NTP недоступен — интернета нет */ + return role; +} + +uint8_t sock_target_class(uint8_t a_type, uint8_t tcfg, uint8_t tnat) { + if (tcfg == CFG_SERVER_TYPE_PUBLIC || tcfg == CFG_SERVER_TYPE_UNKNOWN) return SOCK_TC_PUB; + if (tcfg == CFG_SERVER_TYPE_PRIVATE || tcfg == CFG_SERVER_TYPE_LOCAL) return SOCK_TC_PRIV; + /* CFG_SERVER_TYPE_NAT */ + if (nat_is_eim(tnat) || nat_is_direct(tnat)) + return (a_type == TOPO_ADDR_INTERFACE) ? SOCK_TC_PRIV : SOCK_TC_PUB; /* EIM=direct: NAT-адрес публичный, INTERFACE — приватный */ + return (a_type == TOPO_ADDR_INTERFACE) ? SOCK_TC_PRIV : SOCK_TC_UNREACH; /* strict/unknown: публичный адрес не пробиваем */ +} + +int sock_match(const struct sock_view* v, uint8_t a_type, uint8_t tcfg, uint8_t tnat, uint32_t target_ip4) { + uint8_t tc = sock_target_class(a_type, tcfg, tnat); + if (tc == SOCK_TC_UNREACH) return 0; /* strict/unknown NAT-адрес — напрямую не пробьём */ + if (tc == SOCK_TC_PUB) { + if (v->role == SOCK_ROLE_LOCAL) return 0; /* у нашего сокета нет интернета */ + if (a_type == TOPO_ADDR_NAT && v->is_eim && nat_is_eim(tnat) + && v->public_ip4 && v->public_ip4 == target_ip4) return 0; /* оба EIM за одним NAT — hairpin */ + return 1; + } + return v->has_priv; /* приватный адрес — только с нашего приватного интерфейса */ +} + +int sock_collect_views(struct UTUN_INSTANCE* inst, struct sock_view* out, int max) { + if (!inst || !out || max <= 0) return 0; + int ntp_reachable = inst->ntp.reachable; + int n = 0; + struct ETCP_SOCKET* s = inst->etcp_sockets; + while (s && n < max) { + if (s->local_addr.ss_family != AF_INET) { s = s->next; continue; } + struct sock_view* v = &out[n++]; + v->sock = s; + v->role = sock_role_of(s->type, s->nat_type, ntp_reachable); + v->has_inet = (uint8_t)(v->role != SOCK_ROLE_LOCAL); + v->has_priv = (uint8_t)(s->type != CFG_SERVER_TYPE_PUBLIC && s->type != CFG_SERVER_TYPE_UNKNOWN); + v->is_eim = (uint8_t)(s->type == CFG_SERVER_TYPE_NAT && nat_is_eim(s->nat_type)); + v->family = AF_INET; + v->is_tcp = s->is_tcp; + v->local_ip4 = 0; v->public_ip4 = 0; + { const struct sockaddr_storage* ia = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr; + if (ia && ia->ss_family == AF_INET) v->local_ip4 = ((const struct sockaddr_in*)ia)->sin_addr.s_addr; } + { const struct sockaddr_in* na = (const struct sockaddr_in*)&s->nat_addr; + if (s->nat_addr.ss_family == AF_INET && na->sin_addr.s_addr != 0) + v->public_ip4 = na->sin_addr.s_addr; + else if (s->type == CFG_SERVER_TYPE_PUBLIC || s->type == CFG_SERVER_TYPE_UNKNOWN || nat_is_direct(s->nat_type)) + v->public_ip4 = v->local_ip4; } + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "sock_view[%d] sock=%s type=%d nat=%d role=%d inet=%d priv=%d eim=%d tcp=%d local=%08x public=%08x", + n - 1, s->name, s->type, s->nat_type, v->role, v->has_inet, v->has_priv, v->is_eim, v->is_tcp, + v->local_ip4, v->public_ip4); + s = s->next; + } + return n; +} diff --git a/src/routing_layer/sock_match.h b/src/routing_layer/sock_match.h new file mode 100644 index 00000000..3d7a527d --- /dev/null +++ b/src/routing_layer/sock_match.h @@ -0,0 +1,97 @@ +/** + * @file sock_match.h + * @brief Классификация сокетов и сопоставление адресов при подключении. + * + * ─── Общая идея ──────────────────────────────────────────────────────────── + * Узел имеет несколько сокетов с разной природой сети (публичный IP, NAT, + * скрытый/приватный, без интернета). Чтобы при подключении не создавать + * заведомо мёртвые линки, каждый наш сокет сводится к одной из трёх РОЛЕЙ, + * а каждый адрес пира — к одному из трёх КЛАССОВ. Соединение пробуем только + * для совместимых пар «наш сокет → адрес пира». Для удобства формируем + * список наших сокетов в плоском виде (struct sock_view): роль, локальный IP + * (interface), публичный IP (NAT) и флаги достижимости. + * + * ─── Роли нашего сокета (что он умеет как ИСТОЧНИК) ─────────────────────── + * DIRECT — публичный IP / EIM NAT / verified-direct: интернет в обе стороны. + * NAT — только исходящие (strict/unknown NAT или скрытый private): + * интернет есть, но входящих соединений нет. + * LOCAL — без интернета: type=local из конфига, либо любой сокет при + * недоступном NTP (узел оффлайн). Работает только в LAN. + * + * EIM-сокет приравнивается к DIRECT (endpoint-independent mapping пробивается + * снаружи, поэтому его публичный адрес достижим как у public). + * + * ─── Реклама адресов (что узел публикует в nodeinfo) ────────────────────── + * PUBLIC → свой публичный IP (INTERFACE). + * NAT + EIM → приватный IP (INTERFACE) + публичный IP (NAT). + * NAT + STRICT → только приватный IP (INTERFACE), публичный НЕ светим. + * LOCAL → только приватный IP (INTERFACE) — для связи двух клиентов + * в одной локалке. + * PRIVATE → скрыт, не рекламируется вовсе (но usable как источник). + * + * ─── Сопоставление (наш сокет → адрес пира) ─────────────────────────────── + * Адрес пира классифицируется: PUB (публичный, из интернета), PRIV + * (приватный/LAN), UNREACH (напрямую недостижим). Правила: + * UNREACH → не пробуем (strict/unknown NAT-адрес); + * PUB → нужен интернет у нашего сокета (role != LOCAL), плюс + * hairpin-фильтр: если оба узла EIM за одним NAT (одинаковый + * публичный IP) — по публичному адресу не ходим, остаётся LAN; + * PRIV → только с нашего сокета, имеющего приватный интерфейс + * (has_priv) — приватный адрес достижим лишь из той же сети. + */ +#ifndef SOCK_MATCH_H +#define SOCK_MATCH_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct UTUN_INSTANCE; +struct ETCP_SOCKET; + +/* Роли нашего сокета (как источника соединения) */ +#define SOCK_ROLE_DIRECT 0 /* публичный / EIM / verified-direct — интернет в обе стороны */ +#define SOCK_ROLE_NAT 1 /* только исходящие (strict/unknown NAT, скрытый private) */ +#define SOCK_ROLE_LOCAL 2 /* без интернета (type=local или NTP недоступен) */ + +/* Классы целевого адреса пира */ +#define SOCK_TC_PUB 0 /* публичный — достижим из интернета */ +#define SOCK_TC_PRIV 1 /* приватный / LAN */ +#define SOCK_TC_UNREACH 2 /* напрямую недостижим (strict/unknown NAT) */ + +/** + * Плоское представление нашего сокета для сопоставления. + * "ip сокета" — local_ip4 (interface), "публичный" — public_ip4 (NAT/прямой), + * "тип" — role + флаги has_inet/has_priv/is_eim. + */ +struct sock_view { + struct ETCP_SOCKET* sock; + uint8_t role; /* SOCK_ROLE_* */ + uint8_t has_inet; /* 1 — есть интернет (role != LOCAL) */ + uint8_t has_priv; /* 1 — есть приватный интерфейс (type != PUBLIC/UNKNOWN) */ + uint8_t is_eim; /* 1 — NAT + EIM (для hairpin) */ + uint8_t family; /* AF_INET / AF_INET6 */ + uint8_t is_tcp; + uint32_t local_ip4; /* interface_addr, network order (0 если нет) */ + uint32_t public_ip4; /* nat_addr / interface если direct, network order (0 если нет) */ +}; + +/* Роль нашего сокета по конфиг-типу, NAT-типу и доступности интернета. */ +uint8_t sock_role_of(uint8_t type, uint8_t nat_type, int ntp_reachable); + +/* Класс адреса пира по типу адреса (TOPO_ADDR_*), конфиг-типу и NAT-типу сокета. */ +uint8_t sock_target_class(uint8_t a_type, uint8_t tcfg, uint8_t tnat); + +/* 1 — наш сокет (view) подходит для подключения к целевому адресу (target_ip4, network order). */ +int sock_match(const struct sock_view* v, uint8_t a_type, uint8_t tcfg, uint8_t tnat, uint32_t target_ip4); + +/* Собирает список наших сокетов в плоском виде (v4). Возвращает число элементов. */ +int sock_collect_views(struct UTUN_INSTANCE* inst, struct sock_view* out, int max); + +#ifdef __cplusplus +} +#endif + +#endif /* SOCK_MATCH_H */ diff --git a/src/routing_layer/topo_node.c b/src/routing_layer/topo_node.c index 22875b9b..b6bf98a8 100644 --- a/src/routing_layer/topo_node.c +++ b/src/routing_layer/topo_node.c @@ -930,7 +930,7 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) { struct ETCP_SOCKET* e_sock = instance->etcp_sockets; while (e_sock) { if (e_sock->is_tcp) { e_sock = e_sock->next; continue; } - if (e_sock->type == CFG_SERVER_TYPE_PRIVATE || e_sock->type == CFG_SERVER_TYPE_LOCAL) { e_sock = e_sock->next; continue; } + if (e_sock->type == CFG_SERVER_TYPE_PRIVATE) { e_sock = e_sock->next; continue; } /* private скрыт, LOCAL рекламируем (LAN) */ if (e_sock->local_addr.ss_family == AF_INET) { { struct TOPO_SOCKMETA4* sm = memory_pool_alloc(instance->topo_groups->v4_sock_meta_pool); sm->id = e_sock->sock_id; sm->config_type = e_sock->type; sm->nat_type = e_sock->nat_type; @@ -947,9 +947,10 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) { a->type = TOPO_ADDR_INTERFACE; a->socket_id = e_sock->sock_id; a->protocol = TOPO_PROTO_UDP; a->next = v4_head; v4_head = a; } - /* адрес ПОСЛЕ NAT — дополнительно, если NAT определён (EIM). - DIRECT (nat==интерфейс) и STRICT не рекламируем отдельным адресом. */ - if (e_sock->nat_addr.ss_family == AF_INET && nat_sin->sin_addr.s_addr != 0 + /* адрес ПОСЛЕ NAT — дополнительно, если NAT определён (EIM) и есть интернет. + DIRECT (nat==интерфейс), STRICT и оффлайн не рекламируем отдельным адресом. */ + if (instance->ntp.reachable + && e_sock->nat_addr.ss_family == AF_INET && nat_sin->sin_addr.s_addr != 0 && e_sock->nat_type != NAT_VERIFIED_STRICT && e_sock->nat_type != NAT_VERIFIED_DIRECT) { struct TOPO_ADDR4* a = memory_pool_alloc(instance->topo_groups->v4_addr_pool); @@ -972,7 +973,7 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) { } { struct ETCP_SOCKET* ts = instance->etcp_sockets; while (ts) { if (!ts->is_tcp) { ts = ts->next; continue; } - if (ts->type == CFG_SERVER_TYPE_PRIVATE || ts->type == CFG_SERVER_TYPE_LOCAL) { ts = ts->next; continue; } + if (ts->type == CFG_SERVER_TYPE_PRIVATE) { ts = ts->next; continue; } /* private скрыт, LOCAL рекламируем (LAN) */ struct sockaddr_storage* addr = ts->interface_addr.ss_family ? &ts->interface_addr : &ts->local_addr; if (!addr || !addr->ss_family) { ts = ts->next; continue; } if (addr->ss_family == AF_INET) { diff --git a/src/transport_layer/node_conn_direct.c b/src/transport_layer/node_conn_direct.c index a3664d24..bf377813 100644 --- a/src/transport_layer/node_conn_direct.c +++ b/src/transport_layer/node_conn_direct.c @@ -14,6 +14,7 @@ #include "utun_instance.h" #include "topo_node.h" #include "topo_node_sqlite.h" +#include "sock_match.h" #include "topo_group.h" #include "config_parser.h" #include "../lib/debug_config.h" @@ -126,6 +127,7 @@ static struct TOPO_NODE* ncd_lookup_node(struct UTUN_INSTANCE* inst, uint64_t no */ /* Создаёт один UDP-линк на use_sock → sa, с вытеснением stale-линка и дедупом. */ +#define NCD_MAX_SOCKET_VIEWS 32 static int ncd_add_udp_link(struct ETCP_CONN* conn, struct ETCP_SOCKET* use_sock, struct sockaddr_storage* sa) { struct ETCP_LINK* stale = etcp_link_find_by_addr(use_sock, sa, 0); if (stale && stale->etcp == conn) return 0; @@ -167,6 +169,8 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni, uint64_t nid = entry->node_id; /* ---- IPv4 ---- */ + struct sock_view views[NCD_MAX_SOCKET_VIEWS]; + int nviews = sock_collect_views(inst, views, NCD_MAX_SOCKET_VIEWS); for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) { v4_addrs++; if (a->port == 0 || !(a->protocol & (TOPO_PROTO_UDP | TOPO_PROTO_TCP))) { DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[ncd] v4 skip proto=%02x port=%d node=0x%016llx", a->protocol, (int)a->port, (unsigned long long)nid); v4_skip++; continue; } @@ -175,11 +179,18 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni, struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; memcpy(&sin.sin_addr.s_addr, a->addr, 4); sin.sin_port = htons(a->port); struct sockaddr_storage sa; memcpy(&sa, &sin, sizeof(sin)); + uint32_t tip; memcpy(&tip, a->addr, 4); + const struct TOPO_SOCKMETA4* m = NULL; + for (const struct TOPO_SOCKMETA4* mm = ni->v4_sock_meta; mm; mm = mm->next) + if (mm->id == a->socket_id) { m = mm; break; } + uint8_t tcfg = m ? m->config_type : CFG_SERVER_TYPE_UNKNOWN; + uint8_t tnat = m ? m->nat_type : NAT_TYPE_UNKNOWN; if (a->protocol & TOPO_PROTO_TCP) { - for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) { - if (!s->is_tcp || s->local_addr.ss_family != AF_INET || s->type == CFG_SERVER_TYPE_PRIVATE) continue; - struct ETCP_LINK* tlink = etcp_link_new(conn, s, &sa, 0); + for (int vi = 0; vi < nviews; vi++) { + const struct sock_view* v = &views[vi]; + if (!v->is_tcp || !sock_match(v, a->type, tcfg, tnat, tip)) continue; + struct ETCP_LINK* tlink = etcp_link_new(conn, v->sock, &sa, 0); if (tlink) { tlink->is_tcp = 1; topo_node_apply_reality(tlink, topo_node_find_reality_sock(ni, a->socket_id)); etcp_tcp_link_start_connect(tlink, &sa, a->port); link_count++; } } } @@ -187,9 +198,10 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni, if (specific_sock && !specific_sock->is_tcp && specific_sock->local_addr.ss_family == AF_INET) { link_count += ncd_add_udp_link(conn, specific_sock, &sa); } else if (!specific_sock) { - for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) { - if (s->is_tcp || s->local_addr.ss_family != AF_INET || s->type == CFG_SERVER_TYPE_PRIVATE) continue; - link_count += ncd_add_udp_link(conn, s, &sa); + for (int vi = 0; vi < nviews; vi++) { + const struct sock_view* v = &views[vi]; + if (v->is_tcp || !sock_match(v, a->type, tcfg, tnat, tip)) continue; + link_count += ncd_add_udp_link(conn, v->sock, &sa); } } } diff --git a/src/utun_instance.c b/src/utun_instance.c index 4bf90ec7..d6f0b6e7 100644 --- a/src/utun_instance.c +++ b/src/utun_instance.c @@ -88,6 +88,7 @@ static int instance_init_common(struct UTUN_INSTANCE* instance, struct UASYNC* u instance->running = 0; instance->ua = ua; instance->config = config; + instance->ntp.reachable = 1; /* оптимистично: до первого NTP-цикла считаем, что интернет есть */ // Set name from config if (config->global.name[0] != '\0') { diff --git a/tests/Makefile.am b/tests/Makefile.am index 841e9cfd..f60605cb 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -52,6 +52,7 @@ check_PROGRAMS = \ test_broadcast \ test_conn_mgr \ test_conn_mgr_already_connected \ + test_sock_match \ test_invite_group_create \ test_etcp_connect \ test_node_conn_direct \ @@ -333,6 +334,10 @@ test_conn_mgr_already_connected_SOURCES = test_conn_mgr_already_connected.c test_conn_mgr_already_connected_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_conn_mgr_already_connected_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_sock_match_SOURCES = test_sock_match.c +test_sock_match_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/src/routing_layer -I$(top_srcdir)/lib +test_sock_match_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) + test_invite_group_create_SOURCES = test_invite_group_create.c test_invite_group_create_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_invite_group_create_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) diff --git a/tests/test_sock_match.c b/tests/test_sock_match.c new file mode 100644 index 00000000..c3fbff47 --- /dev/null +++ b/tests/test_sock_match.c @@ -0,0 +1,87 @@ +/** + * @file test_sock_match.c + * @brief Unit-тесты классификации сокетов и сопоставления адресов (sock_match). + * + * Тестирует только чистую логику sock_role_of / sock_target_class / sock_match + * без сети и без UTUN_INSTANCE — с синтетическими struct sock_view и адресами. + */ +#include +#include +#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" + +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; +} + +static void test_role_of(void) { + /* config -> role, при ntp_reachable=1 */ + CHECK(sock_role_of(CFG_SERVER_TYPE_PUBLIC, NAT_TYPE_UNKNOWN, 1) == SOCK_ROLE_DIRECT, "public -> DIRECT"); + CHECK(sock_role_of(CFG_SERVER_TYPE_UNKNOWN, NAT_TYPE_UNKNOWN, 1) == SOCK_ROLE_DIRECT, "unknown -> DIRECT"); + CHECK(sock_role_of(CFG_SERVER_TYPE_NAT, NAT_VERIFIED_EIM, 1) == SOCK_ROLE_DIRECT, "nat+EIM -> DIRECT"); + CHECK(sock_role_of(CFG_SERVER_TYPE_NAT, NAT_VERIFIED_DIRECT,1) == SOCK_ROLE_DIRECT, "nat+DIRECT -> DIRECT"); + CHECK(sock_role_of(CFG_SERVER_TYPE_NAT, NAT_VERIFIED_STRICT,1) == SOCK_ROLE_NAT, "nat+STRICT -> NAT"); + CHECK(sock_role_of(CFG_SERVER_TYPE_NAT, NAT_TYPE_UNKNOWN, 1) == SOCK_ROLE_NAT, "nat+unknown -> NAT"); + CHECK(sock_role_of(CFG_SERVER_TYPE_PRIVATE, NAT_TYPE_UNKNOWN, 1) == SOCK_ROLE_NAT, "private -> NAT"); + CHECK(sock_role_of(CFG_SERVER_TYPE_LOCAL, NAT_TYPE_UNKNOWN, 1) == SOCK_ROLE_LOCAL, "local -> LOCAL"); + /* NTP offline: все кроме LOCAL демотируются */ + CHECK(sock_role_of(CFG_SERVER_TYPE_PUBLIC, NAT_TYPE_UNKNOWN, 0) == SOCK_ROLE_LOCAL, "offline public -> LOCAL"); + CHECK(sock_role_of(CFG_SERVER_TYPE_NAT, NAT_VERIFIED_EIM, 0) == SOCK_ROLE_LOCAL, "offline nat+EIM -> LOCAL"); + CHECK(sock_role_of(CFG_SERVER_TYPE_PRIVATE, NAT_TYPE_UNKNOWN, 0) == SOCK_ROLE_LOCAL, "offline private -> LOCAL"); + CHECK(sock_role_of(CFG_SERVER_TYPE_LOCAL, NAT_TYPE_UNKNOWN, 0) == SOCK_ROLE_LOCAL, "offline local -> LOCAL"); +} + +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"); +} + +int main(void) { + test_role_of(); + 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); + return 0; +}