301 lines
16 KiB
C
301 lines
16 KiB
C
/* Настоящие UDP/INIT/AES-CCM: односторонний разрыв, повторный handshake и события живости. */
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include "etcp.h"
|
|
#include "etcp_api.h"
|
|
#include "etcp_connections.h"
|
|
#include "etcp_keepalive.h"
|
|
#include "utun_instance.h"
|
|
#include "test_clock.h"
|
|
#include "test_net.h"
|
|
#include "../lib/mem.h"
|
|
|
|
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "FAIL %s:%d: %s", __func__, __LINE__, #x); exit(1); } } while (0)
|
|
struct endpoint {
|
|
struct UTUN_INSTANCE* inst;
|
|
struct ETCP_SOCKET* socket;
|
|
struct ETCP_CONN* conn;
|
|
unsigned block_id, init_zero, response_one, up, down, reinit, received;
|
|
};
|
|
static struct endpoint ep[2];
|
|
static struct UASYNC* ua;
|
|
static unsigned close_action, close_fired;
|
|
static const char* pubkeys[] = {
|
|
"b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01",
|
|
"ce8871f07fa056c636d297115f231b08c29cdf94e0d440fce83a07c34416d36a"
|
|
};
|
|
static const char* privkeys[] = {
|
|
"704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f",
|
|
"38240cb82199e504686507f11f6eaa4f740fde6f0c425c495e49a523019a5d68"
|
|
};
|
|
|
|
/* Наблюдаем уже зашифрованный wire, отдельная копия crypto не меняет счётчики рабочего соединения. */
|
|
static ssize_t send_observer(socket_t fd, const void* bytes, size_t len, const struct sockaddr* addr,
|
|
socklen_t addr_len, struct ETCP_LINK* link, void* arg) {
|
|
struct endpoint* e = arg;
|
|
uint8_t plain[PACKET_DATA_SIZE]; size_t size = 0;
|
|
sc_context_t copy = link->etcp->crypto_ctx;
|
|
int decoded = sc_decrypt(©, bytes, len, plain, &size) == SC_OK;
|
|
if (!decoded && len > SC_PUBKEY_ENC_SIZE) {
|
|
copy = link->etcp->crypto_ctx;
|
|
decoded = sc_decrypt(©, bytes, len - SC_PUBKEY_ENC_SIZE, plain, &size) == SC_OK;
|
|
}
|
|
CHECK(decoded && size >= 4);
|
|
if ((plain[3] == ETCP_INIT_REQUEST || plain[3] == ETCP_INIT_REQUEST_NOINIT) && !(plain[2] & 1)) e->init_zero++;
|
|
if ((plain[3] == ETCP_INIT_RESPONSE || plain[3] == ETCP_INIT_RESPONSE_NOINIT) && (plain[2] & 1)) e->response_one++;
|
|
if (e->block_id && (e->block_id == 256 || e->block_id == link->local_link_id)) return len;
|
|
return socket_sendto(fd, bytes, len, addr, addr_len);
|
|
}
|
|
|
|
/* Подписка действует и на новые линки; close проверяется именно в цепочке уведомления DOWN. */
|
|
static void link_event(struct ETCP_CONN* conn, struct ETCP_LINK* link, int old_state, int old_status, void* arg) {
|
|
(void)old_state;
|
|
struct endpoint* e = arg;
|
|
link->send_hook = send_observer; link->send_hook_ctx = e;
|
|
if (e == &ep[1] && old_status && !link->link_status && close_action == 1) {
|
|
CHECK(!close_fired++); etcp_connection_close(conn);
|
|
}
|
|
if (e == &ep[1] && old_status && !link->link_status && close_action == 3) {
|
|
CHECK(!close_fired++); etcp_link_close(link);
|
|
}
|
|
}
|
|
|
|
static void conn_event(struct ETCP_CONN* conn, int event, void* arg) {
|
|
struct endpoint* e = arg;
|
|
if (event == ETCP_CBK_EVENT_UP) e->up++;
|
|
if (event == ETCP_CBK_EVENT_DOWN) e->down++;
|
|
if (event == ETCP_CBK_EVENT_REINIT) e->reinit++;
|
|
if (e == &ep[1] && event == ETCP_CBK_EVENT_DOWN && close_action == 2) {
|
|
CHECK(!close_fired++); etcp_connection_close(conn);
|
|
}
|
|
}
|
|
|
|
static void receive(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
|
unsigned side = conn->instance == ep[1].inst;
|
|
CHECK(entry->len == 2 && entry->dgram[0] == 253 && entry->dgram[1] == 17);
|
|
ep[side].received++; queue_dgram_free(entry); queue_entry_free(entry);
|
|
}
|
|
|
|
static struct ETCP_LINK* first_link(unsigned side) {
|
|
return ep[side].conn->links;
|
|
}
|
|
|
|
static void run_for(unsigned tb) { test_clock_run_for(ua, tb); }
|
|
|
|
/* Только ядро и одна явно выбранная UDP-пара: BGP/NCD не создают обходных путей. */
|
|
static void setup(unsigned swapped, unsigned count) {
|
|
memset(ep, 0, sizeof(ep)); close_action = close_fired = 0;
|
|
ua = uasync_create(); CHECK(ua);
|
|
for (unsigned side = 0; side < 2; side++) {
|
|
unsigned key = side ^ swapped;
|
|
char config[1536];
|
|
snprintf(config, sizeof(config), "[global]\nmy_public_key=%s\nmy_private_key=%s\n"
|
|
"keepalive_interval=100\nkeepalive_timeout=1000\nkeepalive_adaptive=0\nauto_sockets=no\n"
|
|
"[server:srv]\naddr=127.0.0.1:0\ntype=public\n[server:extra]\naddr=127.0.0.1:0\ntype=public\n"
|
|
"[allowed_keys]\nallow_all=1\n", pubkeys[key], privkeys[key]);
|
|
ep[side].inst = utun_instance_create_from_str(ua, config); CHECK(ep[side].inst);
|
|
CHECK(utun_core_start(ep[side].inst) == 0);
|
|
ep[side].socket = test_etcp_bound_socket(ep[side].inst, "srv");
|
|
etcp_add_link_status_cbk(ep[side].inst, link_event, &ep[side]);
|
|
CHECK(etcp_bind(ep[side].inst, 253, receive) == 0);
|
|
}
|
|
ep[0].conn = etcp_connection_create(ep[0].inst, "liveness"); CHECK(ep[0].conn);
|
|
ep[0].conn->peer_node_id = ep[1].inst->node_id;
|
|
CHECK(sc_init_ctx(&ep[0].conn->crypto_ctx, &ep[0].inst->my_keys) == SC_OK);
|
|
CHECK(sc_set_peer_public_key(&ep[0].conn->crypto_ctx, ep[1].inst->my_keys.public_key, SC_PEER_PUBKEY_BIN) == SC_OK);
|
|
etcp_conn_add_cbk(ep[0].conn, conn_event, &ep[0], ETCP_CBK_EVENT_UP | ETCP_CBK_EVENT_DOWN | ETCP_CBK_EVENT_REINIT);
|
|
for (unsigned i = 0; i < count; i++) {
|
|
struct ETCP_SOCKET* socket = ep[0].socket;
|
|
if (i) socket = test_etcp_bound_socket(ep[0].inst, "extra");
|
|
CHECK(etcp_link_new(ep[0].conn, socket, &ep[1].socket->interface_addr, 0));
|
|
}
|
|
uint64_t end = get_time_tb() + 30000;
|
|
while (get_time_tb() < end) {
|
|
test_clock_poll(ua, 10);
|
|
ep[1].conn = instance_find_conn(ep[1].inst, ep[0].inst->node_id);
|
|
if (ep[1].conn && ep[1].conn->links_up && ep[0].conn->links_up && ep[1].response_one) break;
|
|
}
|
|
CHECK(ep[1].conn && ep[0].conn->links_up && ep[1].conn->links_up);
|
|
etcp_conn_add_cbk(ep[1].conn, conn_event, &ep[1], ETCP_CBK_EVENT_UP | ETCP_CBK_EVENT_DOWN | ETCP_CBK_EVENT_REINIT);
|
|
CHECK(etcp_conn_ref_take(ep[1].conn) == 0);
|
|
run_for(1000);
|
|
CHECK(ep[0].init_zero && ep[1].response_one && ep[0].conn->initialized && ep[1].conn->initialized);
|
|
}
|
|
|
|
static void teardown(void) {
|
|
close_action = 0;
|
|
etcp_conn_ref_free(ep[1].conn);
|
|
for (unsigned side = 0; side < 2; side++) {
|
|
etcp_remove_link_status_cbk(ep[side].inst, link_event, &ep[side]);
|
|
utun_instance_destroy(ep[side].inst);
|
|
}
|
|
uasync_poll(ua, 0);
|
|
CHECK(ua->timer_alloc_count == ua->timer_free_count);
|
|
uasync_destroy(ua, 0);
|
|
}
|
|
|
|
/* При работающем B->A повтор INIT(0) не сохраняет ложный UP на A; восстановление не меняет эпоху. */
|
|
static void one_way(unsigned swapped, unsigned blocked_side) {
|
|
setup(swapped, 1);
|
|
uint64_t epoch[] = {ep[0].conn->reset_id, ep[1].conn->reset_id};
|
|
unsigned before = ep[0].init_zero;
|
|
ep[blocked_side].block_id = 256;
|
|
run_for(25000);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "One-way swapped=%u blocked_side=%u INIT0=%u up=%u/%u remote=%u/%u DOWN=%u/%u",
|
|
swapped, blocked_side, ep[0].init_zero-before, ep[0].conn->links_up, ep[1].conn->links_up,
|
|
first_link(0)->remote_keepalive, first_link(1)->remote_keepalive, ep[0].down, ep[1].down);
|
|
CHECK(!ep[0].conn->links_up && !ep[1].conn->links_up && ep[0].down == 1 && ep[1].down == 1);
|
|
if (blocked_side == 1) CHECK(ep[0].init_zero >= before + 2 && !first_link(1)->remote_keepalive && first_link(1)->recv_keepalive);
|
|
ep[blocked_side].block_id = 0; run_for(10000);
|
|
CHECK(ep[0].conn->links_up && ep[1].conn->links_up && ep[1].up == 1);
|
|
for (unsigned side = 0; side < 2; side++) {
|
|
CHECK(ep[side].conn->reset_id == epoch[side] && !ep[side].reinit);
|
|
struct ll_entry* entry = ll_alloc_lldgram(2); CHECK(entry);
|
|
entry->len = 2; entry->dgram[0] = 253; entry->dgram[1] = 17;
|
|
CHECK(etcp_send(ep[side].conn, entry) == 0);
|
|
}
|
|
run_for(1000); CHECK(ep[0].received == 1 && ep[1].received == 1);
|
|
teardown();
|
|
}
|
|
|
|
static void close_in_callback(unsigned action, unsigned keepalive) {
|
|
setup(0, 1); close_action = action;
|
|
if (keepalive) {
|
|
/* Настоящий зашифрованный KA(0) вызывает DOWN внутри обычного decrypted RX. */
|
|
first_link(0)->recv_keepalive = 0;
|
|
etcp_link_send_keepalive(first_link(0));
|
|
first_link(0)->recv_keepalive = 1;
|
|
run_for(1000);
|
|
} else {
|
|
ep[1].block_id = 256;
|
|
run_for(15000);
|
|
}
|
|
CHECK(close_fired == 1 && ep[1].conn->state == 2 && !ep[1].conn->normalizer && !ep[1].conn->callbacks_running);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Callback close: action=%u RX=%s PASS", action, keepalive ? "keepalive" : "INIT");
|
|
teardown();
|
|
}
|
|
|
|
/* INIT на неработающем пути не меняет доступность соединения с другим исправным линком. */
|
|
static void healthy_alternative(void) {
|
|
setup(0, 2);
|
|
struct ETCP_LINK* broken = first_link(0);
|
|
CHECK(broken->next && broken->link_status && broken->next->link_status);
|
|
ep[1].block_id = broken->remote_link_id;
|
|
run_for(15000); CHECK(!broken->link_status && ep[0].conn->links_up && ep[1].conn->links_up);
|
|
unsigned before = ep[0].init_zero;
|
|
etcp_link_enter_reinit(broken); run_for(5000);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Alternative: INIT0=%u broken=%u role=%u state=%u up=%u/%u DOWN=%u/%u block_id=%u",
|
|
ep[0].init_zero - before, broken->link_status, broken->is_server, broken->link_state,
|
|
ep[0].conn->links_up, ep[1].conn->links_up, ep[0].down, ep[1].down, ep[1].block_id);
|
|
/* При исправном обходном пути таймер подавляет повторы INIT; первый запрос всё равно принимается. */
|
|
CHECK(ep[0].init_zero == before + 1 && !broken->link_status && !ep[0].down && !ep[1].down);
|
|
struct ll_entry* entry = ll_alloc_lldgram(2); CHECK(entry);
|
|
entry->len = 2; entry->dgram[0] = 253; entry->dgram[1] = 17;
|
|
CHECK(etcp_send(ep[0].conn, entry) == 0); run_for(1000); CHECK(ep[1].received == 1);
|
|
ep[1].block_id = 0; run_for(10000);
|
|
CHECK(broken->link_status && !ep[0].down && !ep[1].down && !ep[0].reinit && !ep[1].reinit);
|
|
teardown();
|
|
}
|
|
|
|
/* Граница тела не включает timestamp/flag_up; отказ не меняет линк, соединение и владение pkt. */
|
|
static void response_boundaries(void) {
|
|
setup(0, 1);
|
|
struct ETCP_LINK* link = first_link(0);
|
|
struct ETCP_LINK* peer = first_link(1);
|
|
struct ETCP_INIT_RESPONSE_PKT response = {0};
|
|
for (unsigned i = 0; i < 8; i++) {
|
|
response.node_id[i] = ep[1].inst->node_id >> (56 - 8 * i);
|
|
response.reset_id[i] = ep[1].conn->reset_id >> (56 - 8 * i);
|
|
}
|
|
response.mtu[0] = peer->mtu_local >> 8; response.mtu[1] = peer->mtu_local;
|
|
response.link_id = peer->local_link_id; response.remote_socket_id = ep[1].socket->sock_id;
|
|
response.device_type = ep[1].inst->client_type;
|
|
response.keepalive[0] = ep[1].inst->keepalive_interval >> 8;
|
|
response.keepalive[1] = ep[1].inst->keepalive_interval;
|
|
memcpy(response.ed25519_pubkey, ep[1].inst->my_ed25519_pubkey, SC_PUBKEY_SIZE);
|
|
unsigned rejected = 0, accepted = 0;
|
|
for (unsigned variant = 0; variant < 2; variant++) {
|
|
response.code = variant ? ETCP_INIT_RESPONSE_NOINIT : ETCP_INIT_RESPONSE;
|
|
for (unsigned len = 1; len <= sizeof(response) + 1; len++) {
|
|
struct ETCP_DGRAM* pkt = memory_pool_alloc(ep[0].inst->pkt_pool); CHECK(pkt);
|
|
memcpy(pkt->data, &response, sizeof(response)); pkt->data[sizeof(response)] = 0;
|
|
if (len >= sizeof(response)) pkt->data[1] ^= 1; /* Полное тело с чужим node_id тоже недопустимо. */
|
|
pkt->timestamp = 123; pkt->flag_up = 0;
|
|
struct ETCP_LINK before_link;
|
|
struct ETCP_CONN before_conn;
|
|
memcpy(&before_link, link, sizeof(before_link));
|
|
memcpy(&before_conn, ep[0].conn, sizeof(before_conn));
|
|
int free_count = ep[0].inst->pkt_pool->free_count;
|
|
int result = etcp_packet_decrypted(ep[0].socket, pkt, link, len + ETCP_ENCRYPTED_HDR_SIZE);
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "INIT_RESPONSE validation code=%u body=%u result=%d", response.code, len, result);
|
|
CHECK(result != 0 && ep[0].inst->pkt_pool->free_count == free_count);
|
|
CHECK(!memory_pool_is_freed(ep[0].inst->pkt_pool, pkt));
|
|
CHECK(!memcmp(&before_link, link, sizeof(before_link)) && !memcmp(&before_conn, ep[0].conn, sizeof(before_conn)));
|
|
memory_pool_free(ep[0].inst->pkt_pool, pkt); rejected++;
|
|
}
|
|
const unsigned lengths[] = {sizeof(response), sizeof(response) + 1, sizeof(response) + 32};
|
|
for (unsigned i = 0; i < sizeof(lengths) / sizeof(lengths[0]); i++) for (unsigned flag = 0; flag < 2; flag++) {
|
|
struct ETCP_DGRAM* pkt = memory_pool_alloc(ep[0].inst->pkt_pool); CHECK(pkt);
|
|
memset(pkt->data, 0, lengths[i]); memcpy(pkt->data, &response, sizeof(response));
|
|
pkt->timestamp = 456; pkt->flag_up = flag;
|
|
uint64_t epoch = ep[0].conn->reset_id;
|
|
CHECK(etcp_packet_decrypted(ep[0].socket, pkt, link, lengths[i] + ETCP_ENCRYPTED_HDR_SIZE) == 0);
|
|
CHECK(link->recv_keepalive && link->remote_keepalive == flag && link->link_status == flag);
|
|
CHECK(link->last_recv_timestamp == 456 && !ep[0].conn->callbacks_running);
|
|
CHECK(ep[0].conn->reset_id == epoch && !ep[0].reinit); accepted++;
|
|
}
|
|
}
|
|
CHECK(ep[0].down == 6 && ep[0].up == 7);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "INIT_RESPONSE boundaries: rejected=%u accepted=%u (both codes, padding, flag_up=0/1)",
|
|
rejected, accepted);
|
|
teardown();
|
|
}
|
|
|
|
/* Удаление самого link из уведомления не оставляет RX со ссылкой на освобождённый объект. */
|
|
static void delete_in_callback(unsigned keepalive) {
|
|
setup(0, 2); close_action = 3;
|
|
struct ETCP_LINK* link = first_link(0);
|
|
uint64_t epoch = ep[1].conn->reset_id;
|
|
link->recv_keepalive = 0;
|
|
if (keepalive) etcp_link_send_keepalive(link);
|
|
else etcp_link_enter_reinit(link);
|
|
link->recv_keepalive = 1;
|
|
run_for(1000);
|
|
CHECK(close_fired == 1 && ep[1].conn->links && !ep[1].conn->links->next && ep[1].conn->links_up == 1);
|
|
CHECK(ep[1].conn->reset_id == epoch && !ep[1].reinit && !ep[1].down && !ep[1].conn->callbacks_running);
|
|
struct ll_entry* entry = ll_alloc_lldgram(2); CHECK(entry);
|
|
entry->len = 2; entry->dgram[0] = 253; entry->dgram[1] = 17;
|
|
CHECK(etcp_send(ep[0].conn, entry) == 0); run_for(10000); CHECK(ep[1].received == 1);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Callback deletes link: RX=%s, alternate delivery PASS", keepalive ? "keepalive" : "INIT");
|
|
teardown();
|
|
}
|
|
|
|
/* При отказе показать конкретные живые блоки вместо одного несовпавшего счётчика. */
|
|
static void check_memory(size_t baseline) {
|
|
if (u_get_allocated_count() == baseline) return;
|
|
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "Memory imbalance: baseline=%zu actual=%zu", baseline, u_get_allocated_count());
|
|
debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_DEBUG);
|
|
u_report_unfreed_blocks();
|
|
CHECK(u_get_allocated_count() == baseline);
|
|
}
|
|
|
|
int main(void) {
|
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
|
|
debug_set_category_level(DEBUG_CATEGORY_CRYPTO, DEBUG_LEVEL_DISABLED);
|
|
if (getenv("UTUN_TEST_DEBUG")) debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_DEBUG);
|
|
test_clock_init(10); CHECK(socket_platform_init() == 0);
|
|
size_t baseline = u_get_allocated_count();
|
|
response_boundaries(); check_memory(baseline);
|
|
for (unsigned swapped = 0; swapped < 2; swapped++) for (unsigned order = 0; order < 2; order++) {
|
|
one_way(swapped, 1 - order); check_memory(baseline);
|
|
}
|
|
for (unsigned keepalive = 0; keepalive < 2; keepalive++) for (unsigned action = 1; action <= 2; action++) {
|
|
close_in_callback(action, keepalive); check_memory(baseline);
|
|
}
|
|
for (unsigned keepalive = 0; keepalive < 2; keepalive++) { delete_in_callback(keepalive); check_memory(baseline); }
|
|
healthy_alternative(); check_memory(baseline);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "PASS encrypted INIT liveness: cold start, one-way loss, repeated INIT, recovery and callback close");
|
|
test_clock_finish(); socket_platform_cleanup(); return 0;
|
|
}
|