|
|
|
|
@ -1,5 +1,5 @@
|
|
|
|
|
// test_etcp_link_stress.c - Multilink test: добавляет линки в 5 вариантах (V1/V2 one-way,
|
|
|
|
|
// V3a/b/c коллизия), проверяет per-link трафик и инвариант «нет реинита пока жив хотя бы один линк».
|
|
|
|
|
// test_etcp_link_stress.c — одностороннее добавление линков, настоящие и ложные коллизии INIT,
|
|
|
|
|
// уникальность пар сокет/адрес, per-link трафик и сохранение надёжного потока при failover.
|
|
|
|
|
#include <stdio.h> |
|
|
|
|
#include <stdlib.h> |
|
|
|
|
#include <string.h> |
|
|
|
|
@ -174,15 +174,32 @@ static struct ETCP_LINK* first_link_on_socket(struct ETCP_SOCKET* sock) {
|
|
|
|
|
return lqe ? lqe->link : NULL; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static uint64_t client_link_acked(uint16_t srv_port) { |
|
|
|
|
struct ETCP_LINK* l = find_link_by_remote_port(get_conn(client_instance), srv_port); |
|
|
|
|
static uint64_t client_link_acked(struct ETCP_SOCKET* sock, uint16_t srv_port) { |
|
|
|
|
struct sockaddr_storage addr; mk_addr(&addr, srv_port); |
|
|
|
|
struct ETCP_LINK* l = etcp_link_find_by_addr(sock, &addr, 0); |
|
|
|
|
return l ? l->acked_packets : 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static void dump_state(void) { |
|
|
|
|
printf("--- STATE DUMP ---\n"); |
|
|
|
|
if (server_instance) etcp_dump_all_conns(server_instance); |
|
|
|
|
if (client_instance) etcp_dump_all_conns(client_instance); |
|
|
|
|
debug_level_t saved = g_debug_config.category_levels[DEBUG_CATEGORY_ETCP_DUMP]; |
|
|
|
|
debug_set_category_level(DEBUG_CATEGORY_ETCP_DUMP, DEBUG_LEVEL_DEBUG); |
|
|
|
|
if (server_instance) etcp_dump_all(server_instance); |
|
|
|
|
if (client_instance) etcp_dump_all(client_instance); |
|
|
|
|
struct UTUN_INSTANCE* instances[] = { server_instance, client_instance }; |
|
|
|
|
for (int i = 0; i < 2; i++) { |
|
|
|
|
struct ETCP_CONN* conn = get_conn(instances[i]); |
|
|
|
|
if (!conn) continue; |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: links=%d up=%d reinit=%u", |
|
|
|
|
i ? "client" : "server", count_links(conn), count_links_up(conn), conn->reinit_count); |
|
|
|
|
for (struct ETCP_LINK* link = conn->links; link; link = link->next) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "link=%u/%u socket=%s local=%s remote=%s server=%u up=%u init=%u state=%u", |
|
|
|
|
link->local_link_id, link->remote_link_id, link->conn ? link->conn->name : "tcp", |
|
|
|
|
sockaddr_storage_to_str(&link->local_bound_addr).str, sockaddr_storage_to_str(&link->remote_addr).str, |
|
|
|
|
link->is_server, link->link_status, link->initialized, link->link_state); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
debug_set_category_level(DEBUG_CATEGORY_ETCP_DUMP, saved); |
|
|
|
|
printf("--- END DUMP ---\n"); |
|
|
|
|
fflush(stdout); |
|
|
|
|
} |
|
|
|
|
@ -194,6 +211,64 @@ static void dump_state(void) {
|
|
|
|
|
dump_state(); \
|
|
|
|
|
goto fail; } } while (0) |
|
|
|
|
|
|
|
|
|
// Проверяет уникальность UDP-путей и соответствие списка линков индексу каждого сокета.
|
|
|
|
|
static int check_unique_links(struct ETCP_CONN* conn) { |
|
|
|
|
for (struct ETCP_LINK* link = conn->links; link; link = link->next) { |
|
|
|
|
if (link->is_tcp) continue; |
|
|
|
|
if (etcp_link_find_by_addr(link->conn, &link->remote_addr, 0) != link) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "link absent/replaced in socket index: conn=%s socket=%s link=%u remote=%s", |
|
|
|
|
conn->log_name, link->conn ? link->conn->name : "-", link->local_link_id, |
|
|
|
|
sockaddr_storage_to_str(&link->remote_addr).str); |
|
|
|
|
return -1; |
|
|
|
|
} |
|
|
|
|
if (queue_check_consistency(link->conn->links_queue) != 0) return -1; |
|
|
|
|
} |
|
|
|
|
return 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Проверяет выбор collision-линка независимо от порядка списка и состояния handshake.
|
|
|
|
|
static int test_collision_selection(void) { |
|
|
|
|
struct ETCP_CONN conn = {0}; |
|
|
|
|
struct ETCP_SOCKET sockets[3] = {0}; |
|
|
|
|
struct ETCP_LINK links[4] = {0}; |
|
|
|
|
struct sockaddr_storage addr; |
|
|
|
|
mk_addr(&addr, 21000); |
|
|
|
|
for (int i = 0; i < 4; i++) { |
|
|
|
|
links[i].conn = &sockets[i == 0 ? 1 : 0]; |
|
|
|
|
links[i].etcp = &conn; |
|
|
|
|
links[i].remote_addr = addr; |
|
|
|
|
links[i].local_link_id = i + 1; |
|
|
|
|
links[i].next = i < 3 ? &links[i + 1] : NULL; |
|
|
|
|
} |
|
|
|
|
conn.links = links; |
|
|
|
|
links[1].is_server = 1; |
|
|
|
|
mk_addr(&links[2].remote_addr, 21001); |
|
|
|
|
CHECK(etcp_select_collision_link(&conn, &sockets[0], &addr) == &links[3], "selected wrong socket/role/port"); |
|
|
|
|
CHECK(etcp_select_collision_link(&conn, &sockets[1], &addr) == &links[0], "second local socket not distinguished"); |
|
|
|
|
CHECK(!etcp_select_collision_link(&conn, &sockets[2], &addr), "unrelated socket reported collision"); |
|
|
|
|
links[3].initialized = links[3].link_status = 1; |
|
|
|
|
CHECK(etcp_select_collision_link(&conn, &sockets[0], &addr) == &links[3], "established outbound excluded from recovery"); |
|
|
|
|
links[3].is_server = 1; |
|
|
|
|
CHECK(!etcp_select_collision_link(&conn, &sockets[0], &addr), "inbound link reported collision"); |
|
|
|
|
links[3].is_server = 0; |
|
|
|
|
((struct sockaddr_in*)&addr)->sin_addr.s_addr = htonl(0x7f000002); |
|
|
|
|
CHECK(!etcp_select_collision_link(&conn, &sockets[0], &addr), "different peer IP reported collision"); |
|
|
|
|
memset(&addr, 0, sizeof(addr)); |
|
|
|
|
struct sockaddr_in6* addr6 = (struct sockaddr_in6*)&addr; |
|
|
|
|
addr6->sin6_family = AF_INET6; addr6->sin6_addr.s6_addr[15] = 1; addr6->sin6_port = htons(21000); |
|
|
|
|
CHECK(!etcp_select_collision_link(&conn, &sockets[0], &addr), "different address family reported collision"); |
|
|
|
|
links[0].remote_addr = links[3].remote_addr = addr; |
|
|
|
|
CHECK(etcp_select_collision_link(&conn, &sockets[0], &addr) == &links[3], "IPv6 selected wrong local socket"); |
|
|
|
|
addr6->sin6_addr.s6_addr[15] = 2; |
|
|
|
|
CHECK(!etcp_select_collision_link(&conn, &sockets[0], &addr), "different IPv6 peer reported collision"); |
|
|
|
|
conn.links = NULL; |
|
|
|
|
CHECK(!etcp_select_collision_link(&conn, &sockets[0], &addr), "empty connection reported collision"); |
|
|
|
|
printf("collision selection: socket, role, port, IPv4/IPv6, pending/established — PASSED\n"); |
|
|
|
|
return 0; |
|
|
|
|
fail: |
|
|
|
|
return -1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ===== poll conditions =====
|
|
|
|
|
|
|
|
|
|
static int c_conn_up_both(void) { return conn_up(server_instance) && conn_up(client_instance); } |
|
|
|
|
@ -201,6 +276,11 @@ static int c_link_up(void) { return g_link && g_link->initialized && g_link->lin
|
|
|
|
|
static int c_link_down(void) { return g_link && g_link->link_status == 0; } |
|
|
|
|
static int c_socket_link_up(void) { struct ETCP_LINK* l = first_link_on_socket(g_sock); return l && l->initialized && l->link_status == 1; } |
|
|
|
|
static int c_link_on_port_up(void) { struct ETCP_LINK* l = find_link_by_remote_port(g_conn, g_port); return l && l->initialized && l->link_status == 1; } |
|
|
|
|
static int c_link_on_pair_up(void) { |
|
|
|
|
struct sockaddr_storage addr; mk_addr(&addr, g_port); |
|
|
|
|
struct ETCP_LINK* link = etcp_link_find_by_addr(g_sock, &addr, 0); |
|
|
|
|
return link && link->etcp == g_conn && link->initialized && link->link_status == 1; |
|
|
|
|
} |
|
|
|
|
static int c_client_all_down(void) { struct ETCP_CONN* c = get_conn(client_instance); return !c || c->links_up == 0; } |
|
|
|
|
static int c_server_all_down(void) { struct ETCP_CONN* c = get_conn(server_instance); return !c || c->links_up == 0; } |
|
|
|
|
|
|
|
|
|
@ -258,14 +338,14 @@ static int run_traffic(int n, uint64_t timeout_ms) {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Проверяет, что конкретный линк (по серверному порту) реально перенёс данные.
|
|
|
|
|
// Проверяет трафик конкретной пары: клиентский сокет + серверный порт.
|
|
|
|
|
// Линк может пропустить короткий burst из-за keepalive-стабилизации link_status,
|
|
|
|
|
// поэтому повторяем до нескольких keepalive-тиков. Возвращает 0 если линк нёс трафик.
|
|
|
|
|
static int check_link_traffic(uint16_t srv_port, const char* label) { |
|
|
|
|
static int check_link_traffic(struct ETCP_SOCKET* sock, uint16_t srv_port, const char* label) { |
|
|
|
|
for (int attempt = 0; attempt < 6; attempt++) { |
|
|
|
|
uint64_t before = client_link_acked(srv_port); |
|
|
|
|
uint64_t before = client_link_acked(sock, srv_port); |
|
|
|
|
if (run_traffic(TRAFFIC_PACKETS, TRAFFIC_TIMEOUT_MS) != 0) return -1; |
|
|
|
|
if (client_link_acked(srv_port) > before) return 0; |
|
|
|
|
if (client_link_acked(sock, srv_port) > before) return 0; |
|
|
|
|
uasync_poll(ua, 50); |
|
|
|
|
} |
|
|
|
|
printf(" [%s] link never carried traffic\n", label); |
|
|
|
|
@ -327,7 +407,7 @@ static int session_a(void) {
|
|
|
|
|
|
|
|
|
|
// -- per-link трафик на исходном линке --
|
|
|
|
|
printf("[A] traffic on initial link...\n"); |
|
|
|
|
CHECK(check_link_traffic(server_port, "initial") == 0, "initial link carried no traffic"); |
|
|
|
|
CHECK(check_link_traffic(c0, server_port, "initial") == 0, "initial link carried no traffic"); |
|
|
|
|
CHECK(sc->reinit_count == srv_reinit0 && cc->reinit_count == cli_reinit0, "reinit during initial traffic"); |
|
|
|
|
|
|
|
|
|
// -- V1: клиент дозванивается до нового серверного сокета S1 --
|
|
|
|
|
@ -343,7 +423,7 @@ static int session_a(void) {
|
|
|
|
|
g_sock = s1; |
|
|
|
|
CHECK(poll_until(c_socket_link_up, LINK_UP_TIMEOUT_MS, "V1 link up (server)") == 0, "V1 server link did not come up"); |
|
|
|
|
CHECK(sc->reinit_count == srv_reinit0 && cc->reinit_count == cli_reinit0, "reinit during V1"); |
|
|
|
|
CHECK(check_link_traffic(sp1, "V1") == 0, "V1 link carried no traffic"); |
|
|
|
|
CHECK(check_link_traffic(c0, sp1, "V1") == 0, "V1 link carried no traffic"); |
|
|
|
|
|
|
|
|
|
// -- V2: сервер дозванивается до клиента --
|
|
|
|
|
int sp2 = alloc_port(); |
|
|
|
|
@ -358,7 +438,7 @@ static int session_a(void) {
|
|
|
|
|
g_conn = cc; g_port = sp2; |
|
|
|
|
CHECK(poll_until(c_link_on_port_up, LINK_UP_TIMEOUT_MS, "V2 link up (client)") == 0, "V2 client link did not come up"); |
|
|
|
|
CHECK(sc->reinit_count == srv_reinit0 && cc->reinit_count == cli_reinit0, "reinit during V2"); |
|
|
|
|
CHECK(check_link_traffic(sp2, "V2") == 0, "V2 link carried no traffic"); |
|
|
|
|
CHECK(check_link_traffic(c0, sp2, "V2") == 0, "V2 link carried no traffic"); |
|
|
|
|
|
|
|
|
|
printf("[A] after adds: server links_up=%d/%d client links_up=%d/%d\n", |
|
|
|
|
count_links_up(sc), count_links(sc), count_links_up(cc), count_links(cc)); |
|
|
|
|
@ -455,15 +535,17 @@ static int session_collision(int which) {
|
|
|
|
|
|
|
|
|
|
if (which == COL_A) { |
|
|
|
|
CHECK(run_traffic(TRAFFIC_PACKETS, TRAFFIC_TIMEOUT_MS) == 0, "traffic during collision failed"); |
|
|
|
|
g_conn = cc; g_port = sp3; |
|
|
|
|
CHECK(poll_until(c_link_on_port_up, COLLISION_TIMEOUT_MS, "collision client link up") == 0, "client link down"); |
|
|
|
|
g_conn = sc; g_port = cp1; |
|
|
|
|
CHECK(poll_until(c_link_on_port_up, COLLISION_TIMEOUT_MS, "collision server link up") == 0, "server link down"); |
|
|
|
|
struct ETCP_LINK* cl = find_link_by_remote_port(cc, sp3); |
|
|
|
|
struct ETCP_LINK* sl = find_link_by_remote_port(sc, cp1); |
|
|
|
|
g_conn = cc; g_sock = c1; g_port = sp3; |
|
|
|
|
CHECK(poll_until(c_link_on_pair_up, COLLISION_TIMEOUT_MS, "collision client link up") == 0, "client link down"); |
|
|
|
|
g_conn = sc; g_sock = s3; g_port = cp1; |
|
|
|
|
CHECK(poll_until(c_link_on_pair_up, COLLISION_TIMEOUT_MS, "collision server link up") == 0, "server link down"); |
|
|
|
|
struct ETCP_LINK* cl = etcp_link_find_by_addr(c1, &s3_addr, 0); |
|
|
|
|
struct ETCP_LINK* sl = etcp_link_find_by_addr(s3, &c1_addr, 0); |
|
|
|
|
CHECK(cl->is_server != sl->is_server, "collision left both ends in the same role"); |
|
|
|
|
CHECK(count_links(sc) == 2 && count_links(cc) == 2, "collision created duplicate links"); |
|
|
|
|
CHECK(check_link_traffic(sp3, "collision") == 0, "new link carried no traffic"); |
|
|
|
|
CHECK(sl->is_server == (sc->instance->node_id > cc->instance->node_id), "collision elected wrong initiator"); |
|
|
|
|
CHECK(sl->remote_link_id == cl->local_link_id && cl->remote_link_id == sl->local_link_id, "collision IDs disagree"); |
|
|
|
|
CHECK(check_unique_links(sc) == 0 && check_unique_links(cc) == 0, "duplicate path or broken socket index"); |
|
|
|
|
CHECK(check_link_traffic(c1, sp3, "collision") == 0, "new link carried no traffic"); |
|
|
|
|
} else { |
|
|
|
|
struct ETCP_LINK* good = (which == COL_B) ? ls3 : lc3; |
|
|
|
|
g_link = good; |
|
|
|
|
@ -480,6 +562,7 @@ static int session_collision(int which) {
|
|
|
|
|
|
|
|
|
|
CHECK(sc->reinit_count == srv_reinit0 && cc->reinit_count == cli_reinit0, "link add reset the stream"); |
|
|
|
|
CHECK(sc->reset_id == srv_epoch && cc->reset_id == cli_epoch, "link add changed session epochs"); |
|
|
|
|
CHECK(check_unique_links(sc) == 0 && check_unique_links(cc) == 0, "duplicate path after link add"); |
|
|
|
|
printf("[%s] PASSED\n", names[which]); |
|
|
|
|
stop_instances(); |
|
|
|
|
return 0; |
|
|
|
|
@ -489,16 +572,66 @@ fail:
|
|
|
|
|
return -1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Один remote IP:port на двух локальных сокетах — два пути, а не коллизия INIT.
|
|
|
|
|
static int session_cross_socket(void) { |
|
|
|
|
printf("\n=== Session: same peer endpoint, different local sockets ===\n"); |
|
|
|
|
if (start_instances() != 0) { stop_instances(); return -1; } |
|
|
|
|
struct ETCP_CONN* sc = get_conn(server_instance); |
|
|
|
|
struct ETCP_CONN* cc = get_conn(client_instance); |
|
|
|
|
struct ETCP_SOCKET* s0 = server_instance->etcp_sockets; |
|
|
|
|
CHECK(sc && cc && s0, "missing initial connection/socket"); |
|
|
|
|
CHECK(sc->instance->node_id < cc->instance->node_id, "fixture requires server to win collision arbitration"); |
|
|
|
|
CHECK(run_traffic(TRAFFIC_PACKETS, TRAFFIC_TIMEOUT_MS) == 0, "initial traffic failed"); |
|
|
|
|
uint32_t srv_reinit = sc->reinit_count, cli_reinit = cc->reinit_count; |
|
|
|
|
uint64_t srv_epoch = sc->reset_id, cli_epoch = cc->reset_id; |
|
|
|
|
int sp1 = alloc_port(), cp1 = alloc_port(); |
|
|
|
|
struct ETCP_SOCKET* s1 = add_udp_socket(server_instance, sp1, "cross_s1"); |
|
|
|
|
struct ETCP_SOCKET* c1 = add_udp_socket(client_instance, cp1, "cross_c1"); |
|
|
|
|
CHECK(s1 && c1, "failed to add sockets"); |
|
|
|
|
struct sockaddr_storage c1_addr, s0_addr, s1_addr; |
|
|
|
|
mk_addr(&c1_addr, cp1); mk_addr(&s0_addr, server_port); mk_addr(&s1_addr, sp1); |
|
|
|
|
CHECK(etcp_link_new(sc, s1, &c1_addr, 0), "outbound S1 -> C1 failed"); |
|
|
|
|
CHECK(etcp_link_new(cc, c1, &s0_addr, 0), "outbound C1 -> S0 failed"); |
|
|
|
|
struct ETCP_SOCKET* sockets[] = {s0, s1, c1, c1}; |
|
|
|
|
struct ETCP_CONN* conns[] = {sc, sc, cc, cc}; |
|
|
|
|
uint16_t ports[] = {cp1, cp1, server_port, sp1}; |
|
|
|
|
for (int i = 0; i < 4; i++) { |
|
|
|
|
g_sock = sockets[i]; g_conn = conns[i]; g_port = ports[i]; |
|
|
|
|
CHECK(poll_until(c_link_on_pair_up, LINK_UP_TIMEOUT_MS, "independent path handshake") == 0, |
|
|
|
|
"path socket=%s -> port=%u did not establish", g_sock->name, g_port); |
|
|
|
|
} |
|
|
|
|
struct ETCP_LINK* in = etcp_link_find_by_addr(s0, &c1_addr, 0); |
|
|
|
|
struct ETCP_LINK* out = etcp_link_find_by_addr(s1, &c1_addr, 0); |
|
|
|
|
struct ETCP_LINK* peer_out = etcp_link_find_by_addr(c1, &s0_addr, 0); |
|
|
|
|
struct ETCP_LINK* peer_in = etcp_link_find_by_addr(c1, &s1_addr, 0); |
|
|
|
|
CHECK(in != out && in->is_server == 1 && out->is_server == 0, "independent paths lost their roles"); |
|
|
|
|
CHECK(peer_out->is_server == 0 && peer_in->is_server == 1, "peer paths lost their roles"); |
|
|
|
|
CHECK(in->remote_link_id == peer_out->local_link_id && peer_out->remote_link_id == in->local_link_id, "S0/C1 IDs disagree"); |
|
|
|
|
CHECK(out->remote_link_id == peer_in->local_link_id && peer_in->remote_link_id == out->local_link_id, "S1/C1 IDs disagree"); |
|
|
|
|
CHECK(check_unique_links(sc) == 0 && check_unique_links(cc) == 0, "duplicate independent path"); |
|
|
|
|
CHECK(check_link_traffic(c1, server_port, "S0/C1") == 0, "S0 path carried no traffic"); |
|
|
|
|
CHECK(check_link_traffic(c1, sp1, "S1/C1") == 0, "S1 path carried no traffic"); |
|
|
|
|
CHECK(sc->reinit_count == srv_reinit && cc->reinit_count == cli_reinit, "independent paths reset stream"); |
|
|
|
|
CHECK(sc->reset_id == srv_epoch && cc->reset_id == cli_epoch, "independent paths changed epochs"); |
|
|
|
|
printf("independent S0/C1 and S1/C1 paths — PASSED\n"); |
|
|
|
|
stop_instances(); |
|
|
|
|
return 0; |
|
|
|
|
fail: |
|
|
|
|
stop_instances(); |
|
|
|
|
return -1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
int main(void) { |
|
|
|
|
if (create_temp_configs() != 0) return 1; |
|
|
|
|
|
|
|
|
|
debug_config_init(); |
|
|
|
|
debug_set_level(DEBUG_LEVEL_ERROR); |
|
|
|
|
if (getenv("UTUN_TEST_DEBUG")) { |
|
|
|
|
debug_set_level(DEBUG_LEVEL_DEBUG); |
|
|
|
|
debug_set_level(DEBUG_LEVEL_WARN); |
|
|
|
|
debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_DEBUG); |
|
|
|
|
debug_set_category_level(DEBUG_CATEGORY_CONNECTION, DEBUG_LEVEL_DEBUG); |
|
|
|
|
debug_set_category_level(DEBUG_CATEGORY_KEEPALIVE, DEBUG_LEVEL_DEBUG); |
|
|
|
|
debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG); |
|
|
|
|
debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_DEBUG); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -509,10 +642,12 @@ int main(void) {
|
|
|
|
|
ua = uasync_create(); |
|
|
|
|
|
|
|
|
|
int rc = 0; |
|
|
|
|
rc |= test_collision_selection() != 0; |
|
|
|
|
rc |= session_a() != 0; |
|
|
|
|
rc |= session_collision(COL_A) != 0; |
|
|
|
|
rc |= session_collision(COL_B) != 0; |
|
|
|
|
rc |= session_collision(COL_C) != 0; |
|
|
|
|
rc |= session_cross_socket() != 0; |
|
|
|
|
|
|
|
|
|
if (ua) { uasync_poll(ua, 0); uasync_destroy(ua, 0); ua = NULL; } |
|
|
|
|
cleanup_temp_configs(); |
|
|
|
|
|