306 lines
16 KiB
C
306 lines
16 KiB
C
/* Real ETCP lifecycle, AES-CCM and common ingress; deterministic encrypted wire faults. */
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#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;
|
|
}
|