/* Real ETCP lifecycle, AES-CCM and common ingress; deterministic encrypted wire faults. */ #include #include #include #include "etcp.h" #include "etcp_api.h" #include "etcp_connections.h" #include "etcp_debug.h" #include "etcp_session.h" #include "config_parser.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #define CHECK(x) do { if (!(x)) { fprintf(stderr,"FAIL %s:%d: %s\n",__func__,__LINE__,#x); exit(1); } } while (0) struct endpoint { struct UTUN_INSTANCE inst; struct utun_config cfg; struct ETCP_CONN* c; struct ETCP_SOCKET sock; struct ETCP_LINK links[2]; }; struct frame { int from; size_t len; uint8_t bytes[PACKET_DATA_SIZE]; }; static struct endpoint ep[2]; static struct UASYNC* ua; static struct frame frames[4096], saved[4]; static unsigned head, tail, drops, reinit[2], down[2]; static int drop[2][3], blackhole, dead_primary; static char dump_message[512]; static unsigned dump_count, dump_warnings; // Перехватывает диагностический вывод без временных файлов и изменения библиотек. static void capture_dump(int level, const char* category, const char* message) { if (strcmp(category, "dump")) return; if (level == DEBUG_LEVEL_WARN) { dump_warnings++; return; } if (level != DEBUG_LEVEL_DEBUG) return; snprintf(dump_message, sizeof(dump_message), "%s", message); dump_count++; } // ACK/payload должны корректно декодировать все 32 бита, включая границу знакового int. static void debug_sequence_boundaries(void) { const uint32_t values[] = {0, 1, 0x7f000000, 0x7fffffff, 0x80000000, 0xff000000, UINT32_MAX}; struct ETCP_DGRAM* pkt = u_calloc(1, sizeof(*pkt) + ETCP_ACK_BASE_SIZE); CHECK(pkt); debug_level_t saved = g_debug_config.category_levels[DEBUG_CATEGORY_DUMP]; int console = g_debug_config.console_enabled; debug_set_category_level(DEBUG_CATEGORY_DUMP, DEBUG_LEVEL_DEBUG); debug_enable_console(0); debug_set_log_hook(capture_dump); for (unsigned i = 0; i < sizeof(values) / sizeof(values[0]); i++) { for (int ack = 0; ack < 2; ack++) { memset(pkt->data, 0, ETCP_ACK_BASE_SIZE); pkt->data[0] = ack ? ETCP_SECTION_ACK : ETCP_SECTION_PAYLOAD; pkt->data_len = ack ? ETCP_ACK_BASE_SIZE : 5; for (int j = 0; j < 4; j++) pkt->data[1 + ack + j] = (uint8_t)(values[i] >> (8 * j)); if (ack) pkt->data[6] = pkt->data[7] = 255; dump_count = 0; dump_message[0] = 0; etcp_dump_pkt_sections(pkt, NULL, 0); char expected[80]; if (ack) snprintf(expected, sizeof(expected), " ack=%u/0 dup=65535", values[i]); else snprintf(expected, sizeof(expected), " payload=0(seq=%u)", values[i]); CHECK(dump_count == 1 && strstr(dump_message, expected)); } } // Укороченная секция не должна читать отсутствующие байты или печатать выдуманный seq. for (int ack = 0; ack < 2; ack++) { pkt->data[0] = ack ? ETCP_SECTION_ACK : ETCP_SECTION_PAYLOAD; for (unsigned len = 0; len < (ack ? ETCP_ACK_BASE_SIZE : 5); len++) { pkt->data_len = len; dump_count = 0; dump_message[0] = 0; etcp_dump_pkt_sections(pkt, NULL, 1); CHECK(dump_count == 1 && !strstr(dump_message, ack ? " ack=" : " payload=")); } } // Длинный пакет обязан безопасно усекать дамп, в том числе при выключенной категории. u_free(pkt); pkt = u_calloc(1, sizeof(*pkt) + 32 * ETCP_ACK_BASE_SIZE); CHECK(pkt); for (int ack = 0; ack < 2; ack++) { unsigned width = ack ? ETCP_ACK_BASE_SIZE : 5; pkt->data_len = 32 * width; memset(pkt->data, 0, 32 * ETCP_ACK_BASE_SIZE); for (unsigned offset = 0; offset < pkt->data_len; offset += width) { pkt->data[offset] = ack ? ETCP_SECTION_ACK : ETCP_SECTION_TIMESTAMP; memset(pkt->data + offset + (ack ? 2 : 1), 255, width - (ack ? 2 : 1)); } for (int enabled = 0; enabled < 2; enabled++) { debug_set_category_level(DEBUG_CATEGORY_DUMP, enabled ? DEBUG_LEVEL_DEBUG : DEBUG_LEVEL_DISABLED); dump_count = dump_warnings = 0; dump_message[0] = 0; etcp_dump_pkt_sections(pkt, NULL, enabled); if (enabled) CHECK(dump_count == 1 && dump_warnings == 1 && strstr(dump_message, " [truncated]")); else CHECK(dump_count == 0 && dump_warnings == 0); } } debug_set_log_hook(NULL); debug_enable_console(console); debug_set_category_level(DEBUG_CATEGORY_DUMP, saved); u_free(pkt); puts("PASS diagnostic uint32 boundaries, truncated sections and long ACK/timestamp dumps"); } static ssize_t capture(socket_t fd, const void* data, size_t len, const struct sockaddr* addr, socklen_t alen, struct ETCP_LINK* link, void* arg) { (void)fd; (void)addr; (void)alen; int from = (int)(intptr_t)arg; CHECK(len <= sizeof(frames[0].bytes)); if (blackhole || (dead_primary && link->local_link_id == 1)) { drops++; return len; } CHECK(tail-head < 4096); struct frame* f = &frames[tail++ % 4096]; f->from = from; f->len = len; memcpy(f->bytes,data,len); return len; } static void event(struct ETCP_CONN* c, int ev, void* arg) { (void)c; int i = (int)(intptr_t)arg; if (ev == ETCP_CBK_EVENT_REINIT) reinit[i]++; if (ev == ETCP_CBK_EVENT_DOWN) down[i]++; } static void deliver(const struct frame* f, int allow_drop) { int to = 1-f->from; struct ETCP_DGRAM* p = memory_pool_alloc(ep[to].inst.pkt_pool); CHECK(p); size_t len = 0; CHECK(sc_decrypt(&ep[to].c->crypto_ctx,f->bytes,f->len,(uint8_t*)&p->timestamp,&len) == SC_OK); CHECK(len >= 4); int kind = p->data[0] - ETCP_SESSION_HELLO; if (allow_drop && kind >= 0 && kind < 3 && drop[f->from][kind]) { drop[f->from][kind]--; drops++; memory_pool_free(ep[to].inst.pkt_pool,p); return; } if (kind == 0 && !saved[0].len) saved[0] = *f; if (kind == 2 && !saved[1].len) saved[1] = *f; CHECK(etcp_packet_decrypted(&ep[to].sock,p,&ep[to].links[0],len) == 0); } static void pump(void) { uasync_poll(ua,1); unsigned end = tail; // callbacks can append replies while (head < end) { struct frame f = frames[head++ % 4096]; deliver(&f,1); } } static void converge(void) { uint64_t deadline = get_time_tb()+40000; while (get_time_tb() < deadline) { pump(); if (!ep[0].c->reinit_pending && !ep[1].c->reinit_pending && !ep[0].c->session_timer && !ep[1].c->session_timer && head == tail) break; } for (int i=0;i<2;i++) { CHECK(ep[i].c->initialized && !ep[i].c->session_required && !ep[i].c->reinit_pending); CHECK(!ep[i].c->session_timer && !ep[i].c->close_requested && !down[i]); CHECK(ep[i].c->peer_reset_id == ep[1-i].c->reset_id); } } static void setup(int multi) { memset(ep,0,sizeof(ep)); memset(drop,0,sizeof(drop)); memset(saved,0,sizeof(saved)); memset(reinit,0,sizeof(reinit)); memset(down,0,sizeof(down)); head=tail=drops=0; blackhole=dead_primary=0; ua = uasync_create(); CHECK(ua); for (int i=0;i<2;i++) { ep[i].inst.ua=ua; ep[i].inst.node_id=i+1; ep[i].inst.config=&ep[i].cfg; ep[i].inst.connections=queue_new(ua,16,0,8,"session_test"); ep[i].inst.pkt_pool=memory_pool_init(sizeof(struct ETCP_DGRAM)+PACKET_DATA_SIZE,"session_packets"); CHECK(sc_generate_keypair(&ep[i].inst.my_keys) == SC_OK); ep[i].c=etcp_connection_create(&ep[i].inst,"session_test"); CHECK(ep[i].c); ep[i].c->peer_node_id=2-i; ep[i].c->links_up=1; etcp_conn_ready(ep[i].c); etcp_conn_add_cbk(ep[i].c,event,(void*)(intptr_t)i,ETCP_CBK_EVENT_REINIT|ETCP_CBK_EVENT_DOWN); CHECK(sc_init_ctx(&ep[i].c->crypto_ctx,&ep[i].inst.my_keys) == SC_OK); ep[i].sock.instance=&ep[i].inst; for (int j=0;j<=multi;j++) { struct ETCP_LINK* l=&ep[i].links[j]; l->etcp=ep[i].c; l->conn=&ep[i].sock; l->local_link_id=j+1; l->initialized=l->link_status=l->recv_keepalive=l->remote_keepalive=1; l->link_state=LINK_STATE_CONNECTED; l->mtu=1280; l->remote_addr.ss_family=AF_INET; l->send_hook=capture; l->send_hook_ctx=(void*)(intptr_t)i; } if (multi) ep[i].links[0].next=&ep[i].links[1]; ep[i].c->links=&ep[i].links[0]; } for (int i=0;i<2;i++) { ep[i].c->peer_reset_id=ep[1-i].c->reset_id; CHECK(sc_set_peer_public_key(&ep[i].c->crypto_ctx,ep[1-i].inst.my_keys.public_key,0) == SC_OK); } } static void teardown(void) { for (int i=0;i<2;i++) { ep[i].c->links=NULL; // fixture owns synthetic authenticated links etcp_connection_close(ep[i].c); } uasync_poll(ua,0); for (int i=0;i<2;i++) { queue_free(ep[i].inst.connections); memory_pool_destroy(ep[i].inst.pkt_pool); } uasync_poll(ua,0); CHECK(ua->timer_alloc_count == ua->timer_free_count); uasync_destroy(ua,0); } static struct frame stream_packet(int data) { struct ETCP_DGRAM* p=memory_pool_alloc(ep[0].inst.pkt_pool); CHECK(p); p->link=&ep[0].links[0]; p->noencrypt_len=0; p->data_len=8; memset(p->data,0,8); p->data[0]=data ? 0 : 1; if (data) p->data[1]=99; CHECK(etcp_encrypt_send(p)>0); memory_pool_free(ep[0].inst.pkt_pool,p); CHECK(tail == head+1); struct frame f=frames[head++ % 4096]; return f; } static void data_header_validation(void) { setup(0); uint8_t data[ETCP_SESSION_HEADER_SIZE + ETCP_ACK_BASE_SIZE] = {0}; struct ETCP_CONN* c = ep[1].c; etcp_session_encode(data, ETCP_SESSION_DATA, c->peer_reset_id, c->reset_id); data[ETCP_SESSION_HEADER_SIZE] = ETCP_SECTION_ACK; CHECK(etcp_session_accept_data(c, data, sizeof(data))); CHECK(!etcp_session_accept_data(c, data, 0)); CHECK(!etcp_session_accept_data(c, data, ETCP_SESSION_HEADER_SIZE)); data[0] = ETCP_SECTION_ACK; CHECK(!etcp_session_accept_data(c, data, sizeof(data))); data[0] = ETCP_SESSION_DATA; data[ETCP_SESSION_HEADER_SIZE] = ETCP_SESSION_DATA; for (unsigned i = 3; i < 1024; i++) CHECK(!etcp_session_accept_data(c, data, sizeof(data))); CHECK(c->session_data_errors == 1024); data[ETCP_SESSION_HEADER_SIZE] = ETCP_SECTION_ACK; CHECK(etcp_session_accept_data(c, data, sizeof(data))); CHECK(c->session_data_errors == 1024 && c->initialized && !c->reinit_pending); teardown(); puts("PASS session DATA header validation and rejection counter"); } static void timestamp_boundaries(void) { setup(0); struct ETCP_LINK* link = &ep[0].links[0]; link->rtt_last = 1000; for (int i = 0; i < 2; i++) { struct ETCP_DGRAM* p = memory_pool_alloc(ep[0].inst.pkt_pool); CHECK(p); p->link = link; p->timestamp = UINT16_MAX; p->data_len = 5; p->data[0] = ETCP_SECTION_TIMESTAMP; uint16_t ret = get_current_timestamp() - (i ? 30000 : 10); p->data[1] = ret; p->data[2] = ret >> 8; p->data[3] = p->data[4] = i ? 0 : 255; ep[0].c->last_rtt_cb_time = get_time_tb(); // fixture не содержит topology etcp_conn_input(p); CHECK(link->jitter > 0); // предыдущий RTT нельзя перезаписывать до вычисления разницы CHECK(link->jitter <= (uint64_t)UINT16_MAX * 65536); CHECK(link->recv_dt_avg_tx != 0 && link->recv_dt_avg_rx != 0); } teardown(); puts("PASS timestamp wrap, negative offsets and changing RTT"); } static void receive_window_case(int wrap) { setup(0); struct ETCP_CONN* c = ep[0].c; ep[0].inst.data_pool = memory_pool_init(PACKET_DATA_SIZE, "window payload"); CHECK(ep[0].inst.data_pool); queue_set_callback(c->input_send_q, NULL, NULL); // выделенные seq наблюдаем до физической отправки c->next_tx_id = wrap ? 5 : MAX_INFLIGHT_SIZE + 1; c->rx_ack_till = c->next_tx_id - MAX_INFLIGHT_SIZE - 1; uint32_t next = c->next_tx_id, till = c->rx_ack_till; struct ETCP_FRAGMENT* f = (struct ETCP_FRAGMENT*)queue_entry_new(sizeof(*f) - sizeof(struct ll_entry)); CHECK(f); f->ll.dgram = memory_pool_alloc(ep[0].inst.data_pool); CHECK(f->ll.dgram); f->ll.dgram_pool = ep[0].inst.data_pool; f->ll.len = 1; CHECK(queue_data_put(c->input_queue, &f->ll) == 0); CHECK(c->input_queue->count == 1 && c->next_tx_id == next); for (int cumulative = 0; cumulative < 2; cumulative++) { struct ETCP_DGRAM* p = memory_pool_alloc(ep[0].inst.pkt_pool); CHECK(p); p->link = &ep[0].links[0]; p->data_len = cumulative ? 8 : 16; memset(p->data, 0, p->data_len); p->data[0] = ETCP_SECTION_ACK; p->data[1] = !cumulative; uint32_t ack = till + cumulative, selective = next - 1; for (int i = 0; i < 4; i++) { p->data[2+i] = ack >> (8*i); p->data[8+i] = selective >> (8*i); } etcp_conn_input(p); uasync_poll(ua, 0); if (!cumulative) CHECK(c->input_queue->count == 1 && c->next_tx_id == next); else CHECK(c->input_queue->count == 0 && c->next_tx_id == next + 1 && c->input_send_q->count == 1); } teardown(); memory_pool_destroy(ep[0].inst.data_pool); ep[0].inst.data_pool = NULL; printf("PASS receive-window backpressure and cumulative ACK resume (wrap=%d)\n", wrap); } int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); size_t baseline=u_get_allocated_count(); debug_sequence_boundaries(); CHECK(u_get_allocated_count() == baseline); const char* log=getenv("SESSION_TEST_LOG"); if (log) { debug_enable_file_output(log,1); debug_set_category_level(DEBUG_CATEGORY_ETCP,DEBUG_LEVEL_DEBUG); } data_header_validation(); CHECK(u_get_allocated_count() == baseline); timestamp_boundaries(); CHECK(u_get_allocated_count() == baseline); receive_window_case(0); CHECK(u_get_allocated_count() == baseline); receive_window_case(1); CHECK(u_get_allocated_count() == baseline); for (int scenario=0;scenario<9;scenario++) { setup(scenario == 5); saved[2]=stream_packet(0); saved[3]=stream_packet(1); if (scenario == 2 || scenario == 3) for (int i=0;i<2;i++) for (int j=0;j<3;j++) drop[i][j]=1; if (scenario == 5) dead_primary=1; etcp_conn_fatal_reinit(ep[scenario == 1 ? 1 : 0].c,"fault test"); if (scenario == 3) etcp_conn_fatal_reinit(ep[1].c,"simultaneous reset"); if (scenario == 4) { pump(); etcp_conn_fatal_reinit(ep[0].c,"second reset before confirmation"); } if (scenario == 6) { blackhole=1; ep[0].c->session_started=get_time_tb()-100000; CHECK(etcp_conn_ref_take(ep[0].c)==0); ep[0].c->links=NULL; // timeout closes a real connection, links belong to fixture while (ep[0].c->state!=2) pump(); CHECK(!ep[0].c->session_timer && ep[0].c->delete_complete); etcp_conn_ref_free(ep[0].c); ep[0].c=NULL; ep[1].c->links=NULL; etcp_connection_close(ep[1].c); uasync_poll(ua,0); for (int i=0;i<2;i++) { queue_free(ep[i].inst.connections); memory_pool_destroy(ep[i].inst.pkt_pool); } uasync_poll(ua,0); CHECK(ua->timer_alloc_count==ua->timer_free_count); uasync_destroy(ua,0); } else { converge(); if (scenario==2 || scenario==3 || scenario==5) CHECK(drops>0); uint64_t peer=ep[1].c->peer_reset_id, local=ep[0].c->reset_id; unsigned before0=reinit[0], before1=reinit[1]; ep[1].links[0].last_recv_local_time=123; deliver(&saved[2],0); deliver(&saved[3],0); CHECK(ep[1].links[0].last_recv_local_time==123); // stale ACK never reaches stream/liveness if (scenario==7) { if (saved[0].len) deliver(&saved[0],0); if (saved[1].len) { deliver(&saved[1],0); deliver(&saved[1],0); } converge(); } if (scenario==8) { uint8_t short_frame[25]={ETCP_SESSION_CONFIRM}; CHECK(etcp_session_receive(ep[0].c,short_frame,1)); CHECK(!etcp_session_accept_data(ep[0].c,short_frame,0)); etcp_conn_apply_peer_reset_id(ep[0].c,local); // stale/unproven physical INIT cannot switch peer CHECK(ep[0].c->peer_reset_id==ep[1].c->reset_id); } CHECK(ep[1].c->peer_reset_id==peer && ep[0].c->reset_id==local); CHECK(reinit[0]==before0 && reinit[1]==before1); teardown(); } CHECK(u_get_allocated_count()==baseline); printf("PASS session scenario %d\n",scenario+1); } puts("All ETCP session scenarios passed"); return 0; }