Browse Source

Диагностика коллизии линков + детектор дубликата node_id + reality-конфиг autogen

v2
evgeny 3 weeks ago
parent
commit
d886a2ff84
  1. 2
      lib/tcp_io.c
  2. 166
      src/config_updater.c
  3. 13
      src/routing_layer/conn_mgr_core.c
  4. 18
      src/transport_layer/etcp.c
  5. 21
      src/transport_layer/etcp_connections.c
  6. 41
      src/transport_layer/node_conn_direct.c
  7. 5
      tests/Makefile.am
  8. 192
      tests/test_reality_config.c
  9. 49
      tests/test_tcp_io.c
  10. 4
      utun.conf.sample

2
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).

166
src/config_updater.c

@ -8,6 +8,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
@ -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);

13
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;
}

18
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 этого пакета (на стороне отправителя, т.е. у нас)

21
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);
}

41
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);

5
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)

192
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 <stdio.h>
#include <string.h>
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;
}

49
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();
}

4
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)

Loading…
Cancel
Save