From d886a2ff84f74437eac511418e41857b5dfcd250 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 10 Sep 2026 14:40:45 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=B8=D0=B0=D0=B3=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D0=BA=D0=B0=20=D0=BA=D0=BE=D0=BB=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=BB=D0=B8=D0=BD=D0=BA=D0=BE=D0=B2=20+=20?= =?UTF-8?q?=D0=B4=D0=B5=D1=82=D0=B5=D0=BA=D1=82=D0=BE=D1=80=20=D0=B4=D1=83?= =?UTF-8?q?=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0=D1=82=D0=B0=20node=5Fid=20+=20re?= =?UTF-8?q?ality-=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=20autogen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tcp_io.c | 2 + src/config_updater.c | 166 ++++++++++++++++++++- src/routing_layer/conn_mgr_core.c | 13 ++ src/transport_layer/etcp.c | 18 ++- src/transport_layer/etcp_connections.c | 21 ++- src/transport_layer/node_conn_direct.c | 41 ++++++ tests/Makefile.am | 5 + tests/test_reality_config.c | 192 +++++++++++++++++++++++++ tests/test_tcp_io.c | 49 +++++-- utun.conf.sample | 4 +- 10 files changed, 487 insertions(+), 24 deletions(-) create mode 100644 tests/test_reality_config.c diff --git a/lib/tcp_io.c b/lib/tcp_io.c index 017c0c76..025847b4 100644 --- a/lib/tcp_io.c +++ b/lib/tcp_io.c @@ -222,6 +222,7 @@ static void read_cb(socket_t sock, void* arg) { } else if (n == 0) { memory_pool_free(tc->data_pool, buf); queue_entry_free(e); + if (tc->fin_remote) return; tc->fin_remote = 1; DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: FIN fd=%d", (int)tc->sock); uasync_set_socket_read(tc->ua, tc->socket_id, 0); @@ -416,6 +417,7 @@ static void error_cb(socket_t sock, void* arg) { if (!tc || tc->sock == SOCKET_INVALID) return; int so_err = 0; socklen_t so_len = sizeof(so_err); if (getsockopt(tc->sock, SOL_SOCKET, SO_ERROR, &so_err, &so_len) == 0 && so_err == 0) { + if (tc->fin_remote) return; // Грациозное закрытие peer (FIN, EPOLLHUP) при приостановленном чтении: FIN пришёл не как // EPOLLIN (чтение отключено backpressure-ом), а как HUP. Возобновляем чтение, чтобы // read_cb дослил остаток и прочитал FIN (recv()==0 → fin_remote → отложенный on_fin). diff --git a/src/config_updater.c b/src/config_updater.c index e040c1a9..98ab3cd5 100644 --- a/src/config_updater.c +++ b/src/config_updater.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -103,6 +104,123 @@ static char* find_option(char *buf, size_t buf_len, const char *option) { return NULL; } +// Смещение байта сразу после ']' заголовка секции "[name]" (регистронезависимо), -1 если нет. +static long find_section_header(const char *buf, size_t buf_len, const char *name) { + size_t name_len = strlen(name); + size_t i = 0; + while (i < buf_len) { + if (buf[i] == '[' && i + name_len + 2 <= buf_len && + strncasecmp(buf + i + 1, name, name_len) == 0 && buf[i + name_len + 1] == ']') { + return (long)(i + name_len + 2); + } + const char *nl = memchr(buf + i, '\n', buf_len - i); + if (!nl) break; + i = (size_t)(nl - buf) + 1; + } + return -1; +} + +// Смещение строки, начинающейся с "option=", в диапазоне [start, end), -1 если нет. +static long find_option_in_range(const char *buf, size_t start, size_t end, const char *option) { + size_t olen = strlen(option); + size_t i = start; + while (i < end) { + if (i + olen <= end && strncmp(buf + i, option, olen) == 0 && buf[i + olen] == '=') { + return (long)i; + } + const char *nl = memchr(buf + i, '\n', end - i); + if (!nl) break; + i = (size_t)(nl - buf) + 1; + } + return -1; +} + +// Заменить строку option=value внутри секции [section], либо вставить её сразу после +// строки заголовка секции (без пустых строк). Возвращает 0 / -1. +static int insert_or_replace_in_section(char **buf, size_t *buf_len, size_t *buf_capacity, + const char *section, const char *option, const char *value) { + if (!buf || !*buf || !buf_len || !buf_capacity || !section || !option || !value) return -1; + + long hdr_off = find_section_header(*buf, *buf_len, section); + if (hdr_off < 0) return -1; + size_t sec_start = (size_t)hdr_off; + + char *next_sec = memchr(*buf + sec_start, '[', *buf_len - sec_start); + size_t sec_end = next_sec ? (size_t)(next_sec - *buf) : *buf_len; + + long opt_off = find_option_in_range(*buf, sec_start, sec_end, option); + + if (opt_off >= 0) { + size_t opos = (size_t)opt_off; + char *nl = memchr(*buf + opos, '\n', sec_end - opos); + int has_trailing_nl = (nl != NULL); + size_t line_end = nl ? (size_t)(nl - *buf) + 1 : sec_end; + size_t old_len = line_end - opos; + + char new_line[MAX_LINE_LEN]; + int new_len = snprintf(new_line, sizeof(new_line), "%s=%s%s", option, value, has_trailing_nl ? "\n" : ""); + if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1; + + long len_diff = (long)new_len - (long)old_len; + if ((long)*buf_len + len_diff + 1 > (long)*buf_capacity) { + *buf_capacity = *buf_len + len_diff + 1024; + char *new_buf = u_realloc(*buf, *buf_capacity); + if (!new_buf) return -1; + *buf = new_buf; + hdr_off = find_section_header(*buf, *buf_len, section); + if (hdr_off < 0) return -1; + sec_start = (size_t)hdr_off; + next_sec = memchr(*buf + sec_start, '[', *buf_len - sec_start); + sec_end = next_sec ? (size_t)(next_sec - *buf) : *buf_len; + opt_off = find_option_in_range(*buf, sec_start, sec_end, option); + if (opt_off < 0) return -1; + opos = (size_t)opt_off; + nl = memchr(*buf + opos, '\n', sec_end - opos); + has_trailing_nl = (nl != NULL); + line_end = nl ? (size_t)(nl - *buf) + 1 : sec_end; + } + + memmove(*buf + opos + new_len, *buf + line_end, *buf_len - line_end + 1); + memcpy(*buf + opos, new_line, new_len); + *buf_len += len_diff; + } else { + size_t ins = sec_start; + int need_leading_nl = 0; + if (ins < *buf_len && (*buf)[ins] == '\n') { + ins += 1; + } else { + need_leading_nl = 1; + } + + char new_line[MAX_LINE_LEN]; + int new_len = snprintf(new_line, sizeof(new_line), "%s%s=%s\n", need_leading_nl ? "\n" : "", option, value); + if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1; + + if (*buf_len + (size_t)new_len + 1 > *buf_capacity) { + *buf_capacity = *buf_len + (size_t)new_len + 1024; + char *new_buf = u_realloc(*buf, *buf_capacity); + if (!new_buf) return -1; + *buf = new_buf; + hdr_off = find_section_header(*buf, *buf_len, section); + if (hdr_off < 0) return -1; + ins = (size_t)hdr_off; + need_leading_nl = 0; + if (ins < *buf_len && (*buf)[ins] == '\n') { + ins += 1; + } else { + need_leading_nl = 1; + } + new_len = snprintf(new_line, sizeof(new_line), "%s%s=%s\n", need_leading_nl ? "\n" : "", option, value); + if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1; + } + + memmove(*buf + ins + new_len, *buf + ins, *buf_len - ins + 1); + memcpy(*buf + ins, new_line, new_len); + *buf_len += new_len; + } + return 0; +} + static int insert_or_replace_option(char **buf, size_t *buf_len, size_t *buf_capacity, const char *option, const char *value) { if (!buf || !*buf || !buf_len || !buf_capacity || !option || !value) return -1; @@ -301,10 +419,42 @@ int config_ensure_keys_and_node_id(const char *filename) { (unsigned long long)global->my_node_id, (unsigned long long)new_node_id, filename); } - DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Validation results - need_priv_key=%d, need_pub_key=%d, need_node_id=%d", - need_priv_key, need_pub_key, need_node_id); + // ── Step 4: reality-камуфляж — private_key и short_ids при enabled=1 ── + int need_reality_priv = 0, need_reality_ids = 0; + char new_reality_priv_hex[REALITY_AUTH_KEY_SIZE * 2 + 1] = {0}; + char new_short_ids_str[4 * (REALITY_SHORT_ID_SIZE * 2) + 4] = {0}; + + if (global->reality.enabled) { + if (!global->reality.has_private_key) { + need_reality_priv = 1; + DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Reality private_key missing, generating new one in %s", filename); + uint8_t rpriv[REALITY_AUTH_KEY_SIZE], rpub[REALITY_AUTH_KEY_SIZE]; + if (reality_generate_keypair(rpriv, rpub) != REALITY_OK) { free_config(config); u_free(file_buf); return -1; } + bytes_to_hex(rpriv, REALITY_AUTH_KEY_SIZE, new_reality_priv_hex, sizeof(new_reality_priv_hex)); + } + if (global->reality.short_id_count == 0) { + need_reality_ids = 1; + DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Reality short_ids missing, generating 4 ids in %s", filename); + char *p = new_short_ids_str; + size_t remain = sizeof(new_short_ids_str); + for (int i = 0; i < 4; i++) { + uint8_t sid[REALITY_SHORT_ID_SIZE]; + if (random_bytes(sid, sizeof(sid)) != 0) { free_config(config); u_free(file_buf); return -1; } + char hex[REALITY_SHORT_ID_SIZE * 2 + 1]; + bytes_to_hex(sid, REALITY_SHORT_ID_SIZE, hex, sizeof(hex)); + int w = snprintf(p, remain, "%s%s", i ? "," : "", hex); + if (w < 0 || (size_t)w >= remain) { free_config(config); u_free(file_buf); return -1; } + p += w; remain -= (size_t)w; + } + } + } + + DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Validation results - need_priv_key=%d, need_pub_key=%d, need_node_id=%d, need_reality_priv=%d, need_reality_ids=%d", + need_priv_key, need_pub_key, need_node_id, need_reality_priv, need_reality_ids); - if (!need_priv_key && !need_pub_key && !need_node_id) { free_config(config); u_free(file_buf); return 0; } + if (!need_priv_key && !need_pub_key && !need_node_id && !need_reality_priv && !need_reality_ids) { + free_config(config); u_free(file_buf); return 0; + } char cfg_name[MAX_CONN_NAME_LEN]; snprintf(cfg_name, sizeof(cfg_name), "%s", global->name[0] ? global->name : "utun"); @@ -351,6 +501,14 @@ int config_ensure_keys_and_node_id(const char *filename) { DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing my_public_key to %s", filename); if (insert_or_replace_option(&work_buf, &work_len, &buf_capacity, "my_public_key", new_pub_key) < 0) ret = -1; } + if (need_reality_ids && ret == 0) { + DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing reality short_ids to %s", filename); + if (insert_or_replace_in_section(&work_buf, &work_len, &buf_capacity, "reality", "short_ids", new_short_ids_str) < 0) ret = -1; + } + if (need_reality_priv && ret == 0) { + DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing reality private_key to %s", filename); + if (insert_or_replace_in_section(&work_buf, &work_len, &buf_capacity, "reality", "private_key", new_reality_priv_hex) < 0) ret = -1; + } } if (ret == 0 && work_buf) { @@ -358,7 +516,7 @@ int config_ensure_keys_and_node_id(const char *filename) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to write updated config file: %s", filename); ret = -1; } else { - DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Config file updated: %s (gen_priv=%d gen_pub=%d gen_nodeid=%d)", filename, need_priv_key, need_pub_key, need_node_id); + DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Config file updated: %s (gen_priv=%d gen_pub=%d gen_nodeid=%d gen_reality_priv=%d gen_reality_ids=%d)", filename, need_priv_key, need_pub_key, need_node_id, need_reality_priv, need_reality_ids); } } u_free(work_buf); diff --git a/src/routing_layer/conn_mgr_core.c b/src/routing_layer/conn_mgr_core.c index 9e99cf9a..c433a40f 100644 --- a/src/routing_layer/conn_mgr_core.c +++ b/src/routing_layer/conn_mgr_core.c @@ -102,10 +102,21 @@ uint8_t cm_sock_v6_classify(const struct ETCP_SOCKET* s) { return cm_classify_v6_addr(a6); } +static void cm_warn_addr_busy(struct ETCP_CONN* conn, struct ETCP_SOCKET* s, struct sockaddr_storage* sa) { + struct ETCP_LINK* st = etcp_link_find_by_addr(s, sa, 0); + if (st && st->etcp != conn) { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "conn_mgr: addr %s busy by [%s] state=%d init=%d up=%d (adding for [%s] state=%d)", + sockaddr_storage_to_str(sa).str, + st->etcp->log_name, st->etcp->state, st->etcp->initialized, st->etcp->links_up, + conn->log_name, conn->state); + } +} + void cm_add_v4_link(struct ETCP_CONN* conn, const uint8_t* addr, uint16_t port, struct ETCP_SOCKET* s) { struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; memcpy(&sin.sin_addr.s_addr, addr, 4); sin.sin_port = port; struct sockaddr_storage sa; memcpy(&sa, &sin, sizeof(sin)); + cm_warn_addr_busy(conn, s, &sa); etcp_link_new(conn, s, &sa, 0); } @@ -114,6 +125,7 @@ void cm_add_v6_link(struct ETCP_CONN* conn, const uint8_t addr[16], uint16_t por memcpy(&sin6.sin6_addr, addr, 16); sin6.sin6_port = htons(port); if (cm_classify_v6_addr(addr) == CM_V6_LL) sin6.sin6_scope_id = s->netif_index; struct sockaddr_storage sa; memcpy(&sa, &sin6, sizeof(sin6)); + cm_warn_addr_busy(conn, s, &sa); etcp_link_new(conn, s, &sa, 0); } @@ -555,6 +567,7 @@ static void cm_direct_add_links(struct ETCP_CONN* conn, struct CONN_MGR_ENTRY* e 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; } diff --git a/src/transport_layer/etcp.c b/src/transport_layer/etcp.c index e9c89c9b..ce64d41e 100644 --- a/src/transport_layer/etcp.c +++ b/src/transport_layer/etcp.c @@ -689,6 +689,16 @@ void etcp_conn_queue_set_ready(struct ETCP_CONN* conn) { queue_entry_free(conn->conn_queue_entry); } + struct ll_entry* existing = queue_find_data_by_index(conn->instance->connections, (const uint8_t*)&conn->peer_node_id); + if (existing) { + struct conn_queue_entry* ece = (struct conn_queue_entry*)existing->data; + DEBUG_ERROR(DEBUG_CATEGORY_ETCP, + "[%s] queue_set_ready: DUPLICATE node_id key=0x%016llx already indexed by [%s] conn=%p state=%d (this=%p)", + conn->log_name, (unsigned long long)conn->peer_node_id, + ece->conn ? ece->conn->log_name : "?", (void*)(ece->conn), + ece->conn ? ece->conn->state : -1, (void*)conn); + } + struct ll_entry* qe = queue_entry_new(sizeof(struct conn_queue_entry)); if (!qe) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[%s] failed to alloc queue entry for ready", conn->log_name); return; } struct conn_queue_entry* ce = (struct conn_queue_entry*)qe->data; @@ -1282,8 +1292,8 @@ struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* etcp) { link->last_recv_updated=0; if (dt<1000000) { dgram->data[ptr++]=ETCP_SECTION_TIMESTAMP; - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "[%s] KA-TX: ts_section dt=%llu tcp=%d", - link->etcp->log_name, (unsigned long long)dt, dgram->link->is_tcp); + DEBUG_DEBUG(DEBUG_CATEGORY_KEEPALIVE, "[%s] KA-TX: ts_section dt=%llu tcp=%d", + link->etcp->log_name, (unsigned long long)dt, dgram->link->is_tcp); uint16_t t=link->last_recv_timestamp + dt; dgram->data[ptr++]=t; @@ -1574,8 +1584,8 @@ void etcp_conn_input(struct ETCP_DGRAM* pkt) { uint16_t ret_ts=data[1] | (data[2]<<8);// cur_ts=ret_ts = RTT uint16_t new_rtt=cur_ts-ret_ts; pkt->link->rtt_last=new_rtt; - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "[%s] KA-RTT: rtt=%u cur=%u ret=%u dlen=%u", - etcp->log_name, new_rtt, cur_ts, ret_ts, pkt->data_len); + DEBUG_DEBUG(DEBUG_CATEGORY_KEEPALIVE, "[%s] KA-RTT: rtt=%u cur=%u ret=%u dlen=%u", + etcp->log_name, new_rtt, cur_ts, ret_ts, pkt->data_len); int recv_dt_tx1=data[3] | (data[4]<<8);// localtime удаленной стороны момента принятия пакета - timestamp этого пакета (на стороне отправителя, т.е. у нас) diff --git a/src/transport_layer/etcp_connections.c b/src/transport_layer/etcp_connections.c index bea2afdb..db09e303 100644 --- a/src/transport_layer/etcp_connections.c +++ b/src/transport_layer/etcp_connections.c @@ -332,12 +332,23 @@ static int insert_link_queue(struct ETCP_SOCKET* e_sock, struct ETCP_LINK* link) if (dup_qe) { struct link_queue_entry* dup_lqe = (struct link_queue_entry*)dup_qe->data; if (dup_lqe->link && dup_lqe->link->etcp != link->etcp) { + struct ETCP_CONN* new_c = link->etcp; + struct ETCP_CONN* old_c = dup_lqe->link->etcp; + uint64_t new_qkey = 0, old_qkey = 0; + if (new_c->conn_queue_entry) new_qkey = ((struct conn_queue_entry*)new_c->conn_queue_entry->data)->peer_node_id; + if (old_c->conn_queue_entry) old_qkey = ((struct conn_queue_entry*)old_c->conn_queue_entry->data)->peer_node_id; DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, - "!!!!!!!!!!!! LINK ADDR COLLISION !!!!!!!!!!!! " - "addr=%s new_conn=%s(peer=0x%016llx) already_used_by=%s(peer=0x%016llx)", + "!!!!!!!!!!!! LINK ADDR COLLISION !!!!!!!!!!!! addr=%s " + "new: conn=%p [%s] peer=0x%016llx state=%d init=%d up=%d srv=%d tcp=%d qkey=0x%016llx " + "old: conn=%p [%s] peer=0x%016llx state=%d init=%d up=%d srv=%d tcp=%d qkey=0x%016llx", sockaddr_storage_to_str(&link->remote_addr).str, - link->etcp->log_name, (unsigned long long)link->etcp->peer_node_id, - dup_lqe->link->etcp->log_name, (unsigned long long)dup_lqe->link->etcp->peer_node_id); + (void*)new_c, new_c->log_name, (unsigned long long)new_c->peer_node_id, + new_c->state, new_c->initialized, new_c->links_up, link->is_server, link->is_tcp, + (unsigned long long)new_qkey, + (void*)old_c, old_c->log_name, (unsigned long long)old_c->peer_node_id, + old_c->state, old_c->initialized, old_c->links_up, + dup_lqe->link->is_server, dup_lqe->link->is_tcp, + (unsigned long long)old_qkey); return -1; } DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "insert_link_queue: replacing stale DUP addr in [%s] old_link=%p", e_sock->name, dup_lqe->link); @@ -2275,7 +2286,7 @@ int etcp_packet_decrypted(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, pkt->noencrypt_len=0; pkt->link=link; if (pkt->data_len && pkt->data[0] != ETCP_KEEPALIVE) { - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[%s] decrypt: code=%02x dlen=%u plen=%zu recv=%d sock=%p", + DEBUG_DEBUG(DEBUG_CATEGORY_CRYPTO, "[%s] decrypt: code=%02x dlen=%u plen=%zu recv=%d sock=%p", link->etcp->log_name, pkt->data[0], pkt->data_len, pkt_len, link->recv_keepalive, (void*)e_sock); } diff --git a/src/transport_layer/node_conn_direct.c b/src/transport_layer/node_conn_direct.c index 4a0f5df0..dc6eb536 100644 --- a/src/transport_layer/node_conn_direct.c +++ b/src/transport_layer/node_conn_direct.c @@ -85,6 +85,28 @@ static void ncd_registry_remove(struct UTUN_INSTANCE* inst, struct ncd_entry* en while (*pp) { if (*pp == entry) { *pp = entry->next; return; } pp = &(*pp)->next; } } +/* TEMP DEBUG: сканирует inst->connections и выводит conn'ы с peer_node_id==node_id, + * которые instance_find_conn мог не вернуть (key=0 или дубликат ключа). */ +static void ncd_debug_scan_conns(struct UTUN_INSTANCE* inst, uint64_t node_id, const char* tag) { + struct ll_queue* q = inst->connections; + if (!q) { DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[ncd] %s node=0x%016llx: connections=NULL", tag, (unsigned long long)node_id); return; } + int found = 0; + struct ll_entry* e = q->head; + while (e) { + struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; + if (ce && ce->conn && ce->conn->peer_node_id == node_id) { + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, + "[ncd] %s node=0x%016llx: conn=%p [%s] state=%d qkey=0x%016llx links=%p up=%d init=%d", + tag, (unsigned long long)node_id, (void*)ce->conn, ce->conn->log_name, + ce->conn->state, (unsigned long long)ce->peer_node_id, (void*)ce->conn->links, + ce->conn->links_up, ce->conn->initialized); + found = 1; + } + e = e->next; + } + if (!found) DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[ncd] %s node=0x%016llx: NO conn in queue", tag, (unsigned long long)node_id); +} + /* ═══════════ поиск узла ═══════════ */ /* * Загружает информацию об узле (адреса, pubkey) для создания линков. @@ -145,6 +167,17 @@ static int ncd_add_udp_link(struct ETCP_CONN* conn, struct ETCP_SOCKET* use_sock return 0; } } + if (stale && stale->etcp != conn + && !memcmp(conn->crypto_ctx.peer_public_key, stale->etcp->crypto_ctx.peer_public_key, SC_PUBKEY_SIZE)) + { + DEBUG_ERROR(DEBUG_CATEGORY_ETCP, + "[ncd] add_udp_link: SAME-PUBKEY DUPLICATE conn at %s — new=%p [%s] state=%d / old=%p [%s] state=%d init=%d up=%d", + sockaddr_storage_to_str(sa).str, + (void*)conn, conn->log_name, conn->state, + (void*)stale->etcp, stale->etcp->log_name, stale->etcp->state, + stale->etcp->initialized, stale->etcp->links_up); + return 0; + } return etcp_link_new(conn, use_sock, sa, 0) ? 1 : 0; } @@ -602,6 +635,12 @@ int node_conn_direct_open(struct UTUN_INSTANCE* inst, uint64_t node_id, return NCD_REUSED; } + if (conn) { + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[ncd] open node=0x%016llx: instance_find_conn=[%s] state=%d links=%p up=%d — NOT reused (state!=1), creating new", + (unsigned long long)node_id, conn->log_name, conn->state, (void*)conn->links, conn->links_up); + } + ncd_debug_scan_conns(inst, node_id, "open-new"); + /* 3. Новое подключение */ struct TOPO_NODE* ni = ncd_lookup_node(inst, node_id); if (!ni) { @@ -756,6 +795,8 @@ int node_conn_direct_open_node(struct UTUN_INSTANCE* inst, uint64_t node_id, return NCD_REUSED; }} + ncd_debug_scan_conns(inst, node_id, "open_node-new"); + /* 3. Новое подключение — используем переданный ni (временный, не владеем) */ { char conn_name[MAX_CONN_NAME_LEN]; if (ni->node_name && ni->node_name[0]) strncpy(conn_name, ni->node_name, MAX_CONN_NAME_LEN - 1); diff --git a/tests/Makefile.am b/tests/Makefile.am index 6dbef537..30a707ba 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -77,6 +77,7 @@ check_PROGRAMS = \ test_etcp_link_stress \ test_reality_hello \ test_reality_bgp \ + test_reality_config \ bench_timeout_heap \ bench_uasync_timeouts @@ -132,6 +133,10 @@ test_reality_bgp_SOURCES = test_reality_bgp.c test_reality_bgp_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/src/routing_layer -I$(top_srcdir)/lib test_reality_bgp_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_reality_config_SOURCES = test_reality_config.c +test_reality_config_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib +test_reality_config_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) + test_transport_SOURCES = test_stcp_link.c test_transport_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_transport_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) diff --git a/tests/test_reality_config.c b/tests/test_reality_config.c new file mode 100644 index 00000000..b7b941fb --- /dev/null +++ b/tests/test_reality_config.c @@ -0,0 +1,192 @@ +// test_reality_config.c — автогенерация reality private_key и short_ids в конфиге +// +// Проверяет config_ensure_keys_and_node_id(): +// 1. [reality] enabled=1 без private_key/short_ids → генерируются (privkey + 4 id). +// 2. Пустая строка short_ids= заменяется на 4 id (без пустых строк и дублей). +// 3. [reality] enabled=0 → ничего не генерируется. +// 4. Без секции [reality] → ничего не генерируется. +// 5. Идемпотентность: повторный вызов не добавляет дублей. +#include "config_parser.h" +#include "config_updater.h" +#include "../lib/debug_config.h" +#include "test_utils.h" +#include +#include + +static int test_failed = 0; + +#define CHECK(expr, msg) do { \ + if (!(expr)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "FAIL: %s", msg); test_failed = 1; } \ + else { DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS: %s", msg); } \ +} while (0) + +static int write_file(const char *path, const char *content) { + FILE *f = fopen(path, "w"); + if (!f) return -1; + fputs(content, f); + fclose(f); + return 0; +} + +static int read_file(const char *path, char *buf, size_t cap) { + FILE *f = fopen(path, "rb"); + if (!f) return -1; + size_t n = fread(buf, 1, cap - 1, f); + buf[n] = '\0'; + fclose(f); + return (int)n; +} + +// Возвращает: 0 — в секции [section] нет пустых строк, option встречается ровно want раз. +// 1 — секции нет, 2 — есть пустая строка, 3 — неверное число вхождений option. +static int check_section(const char *content, const char *section, const char *option, int want) { + char hdr[64]; snprintf(hdr, sizeof(hdr), "[%s]", section); + const char *sec = strstr(content, hdr); + if (!sec) return 1; + sec += strlen(hdr); + while (*sec == '\r' || *sec == '\n') sec++; + const char *end = strchr(sec, '['); + if (!end) end = content + strlen(content); + + int found = 0, empty = 0; + const char *line = sec; + while (line < end) { + const char *nl = strchr(line, '\n'); + const char *line_end = (nl && nl < end) ? nl : end; + size_t len = (size_t)(line_end - line); + while (len && (line[0] == ' ' || line[0] == '\t' || line[0] == '\r')) { line++; len--; } + if (len == 0) empty++; + if (option && strncmp(line, option, strlen(option)) == 0 && line[strlen(option)] == '=') found++; + line = nl ? nl + 1 : end; + } + if (empty) return 2; + if (found != want) return 3; + return 0; +} + +int main(void) { + debug_config_init(); + debug_set_level(DEBUG_LEVEL_INFO); + + char tdir[] = "/tmp/utun_reality_cfg_XXXXXX"; + if (test_mkdtemp(tdir) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "mkdtemp failed"); return 1; } + char path[512]; + snprintf(path, sizeof(path), "%s/r.conf", tdir); + + // ── Сценарий 1: enabled=1 без ключей → генерация ── + { + const char *cfg = + "[global]\nmy_node_name=test\n" + "[reality]\n" + "enabled=1\n" + "server_name=www.microsoft.com\n" + "dest=www.microsoft.com:443\n"; + CHECK(write_file(path, cfg) == 0, "write config (scenario 1)"); + CHECK(config_ensure_keys_and_node_id(path) == 0, "ensure keys (scenario 1)"); + struct utun_config *uc = parse_config(path); + CHECK(uc != NULL, "parse after generate (scenario 1)"); + if (uc) { + CHECK(uc->global.reality.enabled == 1, "enabled=1"); + CHECK(uc->global.reality.has_private_key == 1, "private_key generated"); + CHECK(uc->global.reality.short_id_count == 4, "4 short_ids generated"); + free_config(uc); + } + char content[4096]; + CHECK(read_file(path, content, sizeof(content)) > 0, "read file (scenario 1)"); + CHECK(check_section(content, "reality", NULL, 0) == 0, "no empty lines in [reality]"); + CHECK(check_section(content, "reality", "private_key", 1) == 0, "exactly one private_key"); + CHECK(check_section(content, "reality", "short_ids", 1) == 0, "exactly one short_ids"); + } + + // ── Сценарий 2: пустая строка short_ids= заменяется ── + { + const char *cfg = + "[global]\nmy_node_name=test\n" + "[reality]\n" + "enabled=1\n" + "short_ids=\n" + "server_name=www.microsoft.com\n"; + CHECK(write_file(path, cfg) == 0, "write config (scenario 2)"); + CHECK(config_ensure_keys_and_node_id(path) == 0, "ensure keys (scenario 2)"); + struct utun_config *uc = parse_config(path); + CHECK(uc != NULL, "parse after generate (scenario 2)"); + if (uc) { + CHECK(uc->global.reality.short_id_count == 4, "4 short_ids generated (replace)"); + CHECK(uc->global.reality.has_private_key == 1, "private_key generated (scenario 2)"); + free_config(uc); + } + char content[4096]; + CHECK(read_file(path, content, sizeof(content)) > 0, "read file (scenario 2)"); + CHECK(check_section(content, "reality", NULL, 0) == 0, "no empty lines (scenario 2)"); + CHECK(check_section(content, "reality", "short_ids", 1) == 0, "short_ids replaced, no dup (scenario 2)"); + } + + // ── Сценарий 3: enabled=0 → без генерации ── + { + const char *cfg = + "[global]\nmy_node_name=test\n" + "[reality]\n" + "enabled=0\n" + "server_name=www.microsoft.com\n"; + CHECK(write_file(path, cfg) == 0, "write config (scenario 3)"); + CHECK(config_ensure_keys_and_node_id(path) == 0, "ensure keys (scenario 3)"); + struct utun_config *uc = parse_config(path); + CHECK(uc != NULL, "parse (scenario 3)"); + if (uc) { + CHECK(uc->global.reality.has_private_key == 0, "no private_key (disabled)"); + CHECK(uc->global.reality.short_id_count == 0, "no short_ids (disabled)"); + free_config(uc); + } + } + + // ── Сценарий 4: без секции [reality] → без генерации ── + { + const char *cfg = "[global]\nmy_node_name=test\n"; + CHECK(write_file(path, cfg) == 0, "write config (scenario 4)"); + CHECK(config_ensure_keys_and_node_id(path) == 0, "ensure keys (scenario 4)"); + struct utun_config *uc = parse_config(path); + CHECK(uc != NULL, "parse (scenario 4)"); + if (uc) { + CHECK(uc->global.reality.enabled == 0, "reality disabled (no section)"); + CHECK(uc->global.reality.has_private_key == 0, "no private_key (no section)"); + CHECK(uc->global.reality.short_id_count == 0, "no short_ids (no section)"); + free_config(uc); + } + } + + // ── Сценарий 5: идемпотентность ── + { + const char *cfg = + "[global]\nmy_node_name=test\n" + "[reality]\n" + "enabled=1\n" + "server_name=www.microsoft.com\n"; + CHECK(write_file(path, cfg) == 0, "write config (scenario 5)"); + CHECK(config_ensure_keys_and_node_id(path) == 0, "ensure #1 (scenario 5)"); + struct utun_config *uc1 = parse_config(path); + CHECK(uc1 != NULL, "parse #1 (scenario 5)"); + CHECK(config_ensure_keys_and_node_id(path) == 0, "ensure #2 (scenario 5)"); + struct utun_config *uc2 = parse_config(path); + CHECK(uc2 != NULL, "parse #2 (scenario 5)"); + if (uc1 && uc2) { + CHECK(uc1->global.reality.short_id_count == 4 && uc2->global.reality.short_id_count == 4, "ids stable"); + CHECK(memcmp(uc1->global.reality.private_key, uc2->global.reality.private_key, REALITY_AUTH_KEY_SIZE) == 0, "privkey stable"); + free_config(uc1); + free_config(uc2); + } + char content[4096]; + CHECK(read_file(path, content, sizeof(content)) > 0, "read file (scenario 5)"); + CHECK(check_section(content, "reality", "private_key", 1) == 0, "no dup private_key (scenario 5)"); + CHECK(check_section(content, "reality", "short_ids", 1) == 0, "no dup short_ids (scenario 5)"); + } + + test_unlink(path); + test_rmdir(tdir); + + if (test_failed) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "=== Reality Config Test: FAILED ==="); + return 1; + } + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "=== Reality Config Test: PASSED ==="); + return 0; +} diff --git a/tests/test_tcp_io.c b/tests/test_tcp_io.c index 871d4663..16335e5b 100644 --- a/tests/test_tcp_io.c +++ b/tests/test_tcp_io.c @@ -125,10 +125,11 @@ static void test_basic_send_recv(void) { queue_entry_free(e); queue_resume_callback(tc->read_queue); - // Закрываем peer — EPOLLHUP обработан через error_cb (handle_error) + // Закрываем peer — грациозный FIN (SO_ERROR==0), обрабатывается через on_fin, не on_error close(sv[1]); uasync_poll(ua, 10); - ASSERT_EQ(g_error_count, 1, "error_cb not called on peer close"); + ASSERT_EQ(g_fin_count, 1, "on_fin not called on peer close"); + ASSERT_EQ(g_error_count, 0, "on_error should not be called on graceful close"); tcp_conn_destroy(tc); close(sv[0]); @@ -290,19 +291,49 @@ static void test_error_callback(void) { ASSERT_TRUE(ua != NULL, "uasync_create failed"); reset_counters(); - int sv[2]; - ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0, "socketpair failed"); - for (int i = 0; i < 2; i++) fcntl(sv[i], F_SETFL, fcntl(sv[i], F_GETFL, 0) | O_NONBLOCK); + // Грациозный close() — это FIN, не ошибка. Для реальной ошибки (ECONNRESET) + // используем loopback TCP + SO_LINGER(1,0), который шлёт RST вместо FIN. + int listen_fd = socket(AF_INET, SOCK_STREAM, 0); + ASSERT_TRUE(listen_fd >= 0, "socket failed"); - struct tcp_conn* tc = tcp_conn_create(ua, sv[0], 1500, 8192, 32, 8, 0, on_fin, on_error, NULL); + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + ASSERT_EQ(bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)), 0, "bind failed"); + socklen_t alen = sizeof(addr); + getsockname(listen_fd, (struct sockaddr*)&addr, &alen); + ASSERT_EQ(listen(listen_fd, 1), 0, "listen failed"); + + int client_fd = socket(AF_INET, SOCK_STREAM, 0); + ASSERT_TRUE(client_fd >= 0, "socket failed"); + fcntl(client_fd, F_SETFL, fcntl(client_fd, F_GETFL, 0) | O_NONBLOCK); + + int ret = connect(client_fd, (struct sockaddr*)&addr, sizeof(addr)); + ASSERT_TRUE(ret < 0 && errno == EINPROGRESS, "connect should return EINPROGRESS"); + + int server_fd = accept(listen_fd, NULL, NULL); + ASSERT_TRUE(server_fd >= 0, "accept failed"); + + struct tcp_conn* tc = tcp_conn_create(ua, client_fd, 1500, 8192, 32, 8, 0, on_fin, on_error, NULL); ASSERT_TRUE(tc != NULL, "tcp_conn_create failed"); - close(sv[1]); - uasync_poll(ua, 10); + // Дожидаемся установления соединения на стороне клиента + for (int i = 0; i < 50 && !tc->connected; i++) uasync_poll(ua, 10); + ASSERT_EQ(tc->connected, 1, "connect not completed"); + + // Жёсткий сброс: SO_LINGER(1,0) → close() шлёт RST → peer получает ECONNRESET + struct linger lg = {1, 0}; + setsockopt(server_fd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg)); + close(server_fd); + + for (int i = 0; i < 50 && tc->error == 0; i++) uasync_poll(ua, 10); ASSERT_EQ(tc->error, 1, "error not set on broken connection"); + ASSERT_EQ(g_error_count, 1, "on_error not called on broken connection"); tcp_conn_destroy(tc); - close(sv[0]); + close(listen_fd); uasync_destroy(ua, 0); TEST_PASS(); } diff --git a/utun.conf.sample b/utun.conf.sample index 0172c4a0..ae7be0d4 100644 --- a/utun.conf.sample +++ b/utun.conf.sample @@ -122,8 +122,8 @@ allow=all #enabled=1 #server_name=www.microsoft.com # SNI-таргет (каким сайтом прикидываемся) #dest=www.microsoft.com:443 # host:port реального сайта для релея -#private_key=<64 hex X25519> # static приватный ключ сервера -#short_ids=0102030405060708,aabbccddeeff0011 # список short_id (по 16 hex, через запятую) +#private_key=<64 hex X25519> # static приватный ключ сервера (генерируется автоматически, если не задан) +#short_ids=0102030405060708,aabbccddeeff0011 # список short_id (по 16 hex, через запятую; 4 шт. генерируются автоматически, если не заданы) #version=1.0.0 # версия протокола utun (по умолчанию 1.0.0) #time_window=30 # допуск timestamp, сек (антиреплей) #fingerprint=chrome # TLS-отпечаток (только chrome)