514 lines
25 KiB
C
514 lines
25 KiB
C
/* Настоящие multicast/router, BGP-маршруты и uasync; ETCP-пакеты передаются в памяти.
|
|
* Управляемые очереди позволяют проверять точную границу, независимые ветки и смену пути. */
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "../lib/debug_config.h"
|
|
#include "../lib/mem.h"
|
|
#include "etcp.h"
|
|
#include "etcp_router.h"
|
|
#include "multicast.h"
|
|
#include "pkt_normalizer.h"
|
|
#include "topo_group.h"
|
|
#include "utun_instance.h"
|
|
#include "test_clock.h"
|
|
|
|
#define CHECK(x) do { if (!(x)) { \
|
|
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL %s:%d: %s", __func__, __LINE__, #x); abort(); \
|
|
} } while (0)
|
|
#define A 0
|
|
#define B 1
|
|
#define C 2
|
|
#define D 3
|
|
#define GROUPS 2
|
|
|
|
struct wire {
|
|
struct ETCP_CONN conn;
|
|
struct wire* reverse;
|
|
struct ll_entry* blocker;
|
|
unsigned packets, max_recipients;
|
|
};
|
|
|
|
struct fixture {
|
|
struct UASYNC* ua;
|
|
struct UTUN_INSTANCE* inst[4];
|
|
struct TOPO_GROUP* group[4][GROUPS];
|
|
struct wire wire[4][4];
|
|
};
|
|
|
|
struct received {
|
|
unsigned count;
|
|
size_t len;
|
|
uint8_t prefix[16];
|
|
int unsubscribe;
|
|
};
|
|
|
|
struct losses {
|
|
unsigned count;
|
|
struct multicast_loss item[256];
|
|
uint64_t ids[256][8];
|
|
int close;
|
|
};
|
|
|
|
static void release(struct ll_entry* e) {
|
|
if (e) { queue_dgram_free(e); queue_entry_free(e); }
|
|
}
|
|
|
|
static void clear_queue(struct ll_queue* q) {
|
|
if (!q) return;
|
|
struct ll_entry* e;
|
|
while ((e = queue_data_get(q))) release(e);
|
|
queue_free(q);
|
|
}
|
|
|
|
static void receive(struct multicast_subscription* sub, const uint8_t* payload, size_t len, void* arg) {
|
|
struct received* r = arg;
|
|
r->count++; r->len = len;
|
|
memset(r->prefix, 0, sizeof(r->prefix));
|
|
if (len) memcpy(r->prefix, payload, len < 16 ? len : 16);
|
|
if (r->unsubscribe) multicast_unsubscribe(sub);
|
|
}
|
|
|
|
static void loss(struct UTUN_INSTANCE* inst, const struct multicast_loss* event, void* arg) {
|
|
struct losses* l = arg;
|
|
CHECK(l->count < 256);
|
|
unsigned i = l->count++;
|
|
l->item[i] = *event;
|
|
unsigned n = event->recipient_count < 8 ? event->recipient_count : 8;
|
|
memcpy(l->ids[i], event->recipients, n * sizeof(uint64_t));
|
|
l->item[i].recipients = l->ids[i];
|
|
if (l->close) multicast_close(inst, event->descriptor);
|
|
}
|
|
|
|
static void wire_send(struct ll_queue* q, void* arg) {
|
|
struct wire* w = arg;
|
|
struct ll_entry* e = queue_data_get(q);
|
|
if (e) {
|
|
CHECK(e != w->blocker && e->len > 0);
|
|
if (e->dgram[0] == ETCP_ID_MULTICAST) {
|
|
w->packets++;
|
|
unsigned n = e->dgram[11] | (unsigned)e->dgram[12] << 8;
|
|
if (n > w->max_recipients) w->max_recipients = n;
|
|
}
|
|
struct ETCP_CONN* peer = &w->reverse->conn;
|
|
etcp_recv_fn cb = peer->instance->api_bindings.callbacks[e->dgram[0]];
|
|
CHECK(cb);
|
|
cb(peer, e);
|
|
}
|
|
queue_resume_callback(q);
|
|
}
|
|
|
|
static void block(struct wire* w) {
|
|
CHECK(!w->blocker && w->conn.send_input_q->count == 0);
|
|
queue_set_callback(w->conn.send_input_q, NULL, NULL);
|
|
w->blocker = ll_alloc_lldgram(1); CHECK(w->blocker);
|
|
w->blocker->len = 1; w->blocker->dgram[0] = 0xff;
|
|
CHECK(queue_data_put(w->conn.send_input_q, w->blocker) == 0);
|
|
}
|
|
|
|
static void unblock(struct wire* w) {
|
|
CHECK(w->blocker);
|
|
CHECK(queue_data_get(w->conn.send_input_q) == w->blocker);
|
|
release(w->blocker); w->blocker = NULL;
|
|
queue_set_callback(w->conn.send_input_q, wire_send, w);
|
|
queue_resume_callback(w->conn.send_input_q);
|
|
}
|
|
|
|
static void route(struct fixture* f, int from, int group, uint64_t dst, int via) {
|
|
struct TOPO_GROUP* g = f->group[from][group];
|
|
struct TOPO_GROUP_NODE* node = topo_node_find_by_id(g, dst);
|
|
if (!node) {
|
|
node = (struct TOPO_GROUP_NODE*)queue_entry_new(sizeof(*node) - sizeof(struct ll_entry)); CHECK(node);
|
|
node->node_id = dst;
|
|
CHECK(queue_data_put_with_index(g->nodes, &node->ll) == 0);
|
|
}
|
|
topo_nodeq_clear_paths(node);
|
|
if (via >= 0) {
|
|
uint64_t hop = via + 1;
|
|
CHECK(f->wire[from][via].conn.send_input_q);
|
|
CHECK(topo_group_add_path(node, &f->wire[from][via].conn, &hop, 1, 0) == 0);
|
|
}
|
|
}
|
|
|
|
static struct fixture* setup(void) {
|
|
struct fixture* f = u_calloc(1, sizeof(*f)); CHECK(f);
|
|
f->ua = uasync_create(); CHECK(f->ua);
|
|
for (int i = 0; i < 4; i++) {
|
|
struct UTUN_INSTANCE* inst = f->inst[i] = u_calloc(1, sizeof(*inst)); CHECK(inst);
|
|
inst->node_id = i + 1; inst->ua = f->ua;
|
|
inst->connections = queue_new(f->ua, 16, 0, 8, "mc_test_connections"); CHECK(inst->connections);
|
|
inst->topo_groups = u_calloc(1, sizeof(*inst->topo_groups)); CHECK(inst->topo_groups);
|
|
inst->topo_groups->instance = inst;
|
|
inst->topo_groups->group_list = queue_new(f->ua, 16, 0, 8, "mc_test_groups"); CHECK(inst->topo_groups->group_list);
|
|
for (int j = 0; j < GROUPS; j++) {
|
|
struct TOPO_GROUP* g = (struct TOPO_GROUP*)queue_entry_new(sizeof(*g) - sizeof(struct ll_entry)); CHECK(g);
|
|
g->instance = inst; g->group_id = j ? 0x55 : TOPO_GROUP_UTUN;
|
|
g->nodes = queue_new(f->ua, 256, 0, 8, "mc_test_nodes"); CHECK(g->nodes);
|
|
g->senders_list = queue_new(f->ua, 0, 0, 0, "mc_test_peers"); CHECK(g->senders_list);
|
|
CHECK(queue_data_put_with_index(inst->topo_groups->group_list, &g->ll) == 0);
|
|
f->group[i][j] = g;
|
|
}
|
|
CHECK(etcp_router_init(inst) == 0);
|
|
CHECK(multicast_init(inst) == 0);
|
|
}
|
|
const int edges[][2] = {{A, B}, {B, C}, {B, D}, {A, D}, {D, C}};
|
|
for (unsigned i = 0; i < sizeof(edges) / sizeof(edges[0]); i++) {
|
|
for (int dir = 0; dir < 2; dir++) {
|
|
int a = edges[i][dir], b = edges[i][1 - dir];
|
|
struct wire* w = &f->wire[a][b];
|
|
w->reverse = &f->wire[b][a]; w->conn.instance = f->inst[a];
|
|
w->conn.peer_node_id = b + 1; w->conn.state = 1; w->conn.links_up = 1;
|
|
w->conn.send_input_q = queue_new(f->ua, 0, 0, 0, "mc_test_wire"); CHECK(w->conn.send_input_q);
|
|
queue_set_callback_defer(w->conn.send_input_q, 1);
|
|
queue_set_callback(w->conn.send_input_q, wire_send, w);
|
|
struct ll_entry* e = queue_entry_new(sizeof(struct conn_queue_entry)); CHECK(e);
|
|
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data;
|
|
ce->peer_node_id = b + 1; ce->conn = &w->conn;
|
|
CHECK(queue_data_put_with_index(f->inst[a]->connections, e) == 0);
|
|
for (int g = 0; g < GROUPS; g++) {
|
|
e = queue_entry_new(sizeof(struct TOPO_GROUP_CONN_ITEM)); CHECK(e);
|
|
struct TOPO_GROUP_CONN_ITEM* peer = (struct TOPO_GROUP_CONN_ITEM*)e->data;
|
|
peer->group = f->group[a][g]; peer->node_id = b + 1; peer->conn = &w->conn; peer->accepted = 1;
|
|
CHECK(queue_data_put(f->group[a][g]->senders_list, e) == 0);
|
|
}
|
|
}
|
|
}
|
|
const int next[4][4] = {{-1, B, B, B}, {A, -1, C, D}, {B, B, -1, D}, {B, B, C, -1}};
|
|
for (int a = 0; a < 4; a++) for (int b = 0; b < 4; b++) if (a != b)
|
|
for (int g = 0; g < GROUPS; g++) route(f, a, g, b + 1, next[a][b]);
|
|
return f;
|
|
}
|
|
|
|
/* Дренируем готовую работу независимо от скорости CPU/Valgrind, не сдвигая будущие окна. */
|
|
static void pump(struct fixture* f) {
|
|
for (int i = 0; i < 1024; i++) {
|
|
uasync_drain_immediate(f->ua); uasync_poll(f->ua, 0);
|
|
TimeoutEntry next;
|
|
if (!f->ua->immediate_queue_head &&
|
|
(timeout_heap_peek(f->ua->timeout_heap, &next) || next.expiration > get_time_us() / 1000)) return;
|
|
}
|
|
CHECK(0 && "multicast immediate work did not settle");
|
|
}
|
|
|
|
static struct multicast_stats stats(struct fixture* f, int node) {
|
|
struct multicast_stats s; CHECK(multicast_get_stats(f->inst[node], &s) == 0); return s;
|
|
}
|
|
|
|
static void teardown(struct fixture* f) {
|
|
for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++)
|
|
if (f->wire[i][j].conn.send_input_q) queue_set_callback(f->wire[i][j].conn.send_input_q, NULL, NULL);
|
|
for (int i = 0; i < 4; i++) multicast_destroy(f->inst[i]);
|
|
for (int i = 0; i < 4; i++) etcp_router_destroy(f->inst[i]);
|
|
for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) etcp_router_transit_queues_destroy(&f->wire[i][j].conn);
|
|
uasync_poll(f->ua, 0); /* Освободить помеченные отменёнными heap-таймеры. */
|
|
uasync_drain_immediate(f->ua);
|
|
for (int i = 0; i < 4; i++) {
|
|
for (int j = 0; j < 4; j++) clear_queue(f->wire[i][j].conn.send_input_q);
|
|
for (int g = 0; g < GROUPS; g++) {
|
|
struct TOPO_GROUP* group = f->group[i][g];
|
|
for (struct ll_entry* e = group->nodes->head; e; e = e->next) topo_nodeq_clear_paths((struct TOPO_GROUP_NODE*)e);
|
|
clear_queue(group->nodes); clear_queue(group->senders_list);
|
|
}
|
|
clear_queue(f->inst[i]->topo_groups->group_list);
|
|
u_free(f->inst[i]->topo_groups);
|
|
clear_queue(f->inst[i]->connections);
|
|
CHECK(!f->inst[i]->conn_status_cbks);
|
|
u_free(f->inst[i]);
|
|
}
|
|
CHECK(f->ua->timer_alloc_count == f->ua->timer_free_count);
|
|
uasync_destroy(f->ua, 0); u_free(f);
|
|
}
|
|
|
|
static void test_delivery(void) {
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "multicast: tree splitting, copying, subscriptions and source isolation");
|
|
struct fixture* f = setup();
|
|
struct losses l = {0};
|
|
int fd = multicast_open(f->group[A][0], loss, &l); CHECK(fd == 1);
|
|
struct received rx[4] = {{0}};
|
|
struct multicast_subscription* sub[4];
|
|
for (int i = 0; i < 4; i++) { sub[i] = multicast_subscribe(f->group[i][0], 1, fd, receive, &rx[i]); CHECK(sub[i]); }
|
|
CHECK(!multicast_subscribe(f->group[C][0], 1, fd, receive, &rx[C]));
|
|
uint64_t ids[] = {4, 3, 2, 1, 3};
|
|
char data[] = "copied";
|
|
CHECK(multicast_send(f->inst[A], fd, ids, 5, data, sizeof(data)) == 0);
|
|
CHECK(rx[A].count == 0); memset(data, 'x', sizeof(data)); memset(ids, 0, sizeof(ids));
|
|
pump(f);
|
|
for (int i = 0; i < 4; i++) CHECK(rx[i].count == 1 && memcmp(rx[i].prefix, "copied", 7) == 0);
|
|
CHECK(f->wire[A][B].packets == 1 && f->wire[A][B].max_recipients == 3);
|
|
CHECK(f->wire[B][C].packets == 1 && f->wire[B][D].packets == 1);
|
|
multicast_unsubscribe(sub[B]); sub[B] = NULL;
|
|
uint64_t bcd[] = {2, 3, 4};
|
|
CHECK(multicast_send(f->inst[A], fd, bcd, 3, NULL, 0) == 0);
|
|
pump(f); CHECK(rx[B].count == 1 && rx[C].count == 2 && rx[C].len == 0 && rx[D].count == 2);
|
|
CHECK(stats(f, B).unsubscribed == 1 && l.count == 0);
|
|
int other = multicast_open(f->group[D][0], loss, &l); CHECK(other == fd);
|
|
CHECK(multicast_send(f->inst[D], other, bcd + 1, 1, "other source", 12) == 0);
|
|
pump(f); CHECK(rx[C].count == 2 && stats(f, C).unsubscribed == 1);
|
|
for (int i = 0; i < 4; i++) multicast_unsubscribe(sub[i]);
|
|
teardown(f);
|
|
}
|
|
|
|
static void test_lifetime(void) {
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "multicast: deferred callbacks, close/unsubscribe, group isolation");
|
|
struct fixture* f = setup();
|
|
struct losses l = {0};
|
|
int fd = multicast_open(f->group[A][0], loss, &l);
|
|
int other = multicast_open(f->group[A][1], loss, &l); CHECK(fd == 1 && other == 2);
|
|
uint64_t self = 1, remote = 2;
|
|
struct received rx = {0}, rx_other = {0};
|
|
struct multicast_subscription* sub = multicast_subscribe(f->group[A][0], 1, fd, receive, &rx); CHECK(sub);
|
|
CHECK(multicast_send(f->inst[A], fd, &self, 1, "pending", 7) == 0);
|
|
multicast_unsubscribe(sub);
|
|
sub = multicast_subscribe(f->group[A][0], 1, fd, receive, &rx); CHECK(sub);
|
|
pump(f); CHECK(rx.count == 0);
|
|
rx.unsubscribe = 1;
|
|
CHECK(multicast_send(f->inst[A], fd, &self, 1, "first", 5) == 0);
|
|
CHECK(multicast_send(f->inst[A], fd, &self, 1, "second", 6) == 0);
|
|
pump(f); CHECK(rx.count == 1 && stats(f, A).delivery_bytes == 0);
|
|
sub = multicast_subscribe(f->group[A][0], 1, fd, receive, &rx); CHECK(sub);
|
|
CHECK(multicast_send(f->inst[A], fd, &self, 1, "close", 5) == 0);
|
|
multicast_close(f->inst[A], fd);
|
|
pump(f); CHECK(rx.count == 1 && stats(f, A).delivery_bytes == 0);
|
|
multicast_unsubscribe(sub);
|
|
fd = multicast_open(f->group[A][0], loss, &l); CHECK(fd == 3);
|
|
sub = multicast_subscribe(f->group[B][0], 1, fd, receive, &rx); CHECK(sub);
|
|
struct multicast_subscription* sub_other = multicast_subscribe(f->group[B][1], 1, other, receive, &rx_other); CHECK(sub_other);
|
|
block(&f->wire[A][B]);
|
|
CHECK(multicast_send(f->inst[A], fd, &remote, 1, "cancel", 6) == 0);
|
|
CHECK(multicast_send(f->inst[A], other, &remote, 1, "keep", 4) == 0);
|
|
pump(f); CHECK(stats(f, A).queues == 1);
|
|
multicast_close_group(f->inst[A], f->group[A][0]->group_id);
|
|
unblock(&f->wire[A][B]); pump(f);
|
|
CHECK(rx.count == 1 && rx_other.count == 1 && memcmp(rx_other.prefix, "keep", 4) == 0);
|
|
CHECK(stats(f, A).descriptors == 1);
|
|
multicast_close_group(f->inst[B], f->group[B][0]->group_id);
|
|
multicast_unsubscribe(sub);
|
|
/* Handle остаётся пригодным для unsubscribe после уничтожения ядра. */
|
|
teardown(f); multicast_unsubscribe(sub_other);
|
|
}
|
|
|
|
static void test_overload(void) {
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "multicast: exact 256 KiB boundary, healthy branch and timer-only tail report");
|
|
struct fixture* f = setup();
|
|
struct losses l = {0};
|
|
int fd = multicast_open(f->group[A][0], loss, &l); CHECK(fd > 0);
|
|
struct received c = {0}, d = {0};
|
|
struct multicast_subscription* sc = multicast_subscribe(f->group[C][0], 1, fd, receive, &c); CHECK(sc);
|
|
struct multicast_subscription* sd = multicast_subscribe(f->group[D][0], 1, fd, receive, &d); CHECK(sd);
|
|
block(&f->wire[B][C]);
|
|
uint64_t both[] = {3, 4}, one = 3;
|
|
uint8_t payload[8146]; memset(payload, 0xab, sizeof(payload));
|
|
for (int i = 0; i < 32; i++) {
|
|
CHECK(multicast_send(f->inst[A], fd, both, 2, payload, sizeof(payload)) == 0);
|
|
pump(f);
|
|
}
|
|
CHECK(stats(f, B).queued_bytes == MULTICAST_QUEUE_BYTES && stats(f, B).overload_packets == 0);
|
|
CHECK(c.count == 0 && d.count == 32);
|
|
uint64_t first_drop = get_time_tb();
|
|
CHECK(multicast_send(f->inst[A], fd, &one, 1, "one", 3) == 0);
|
|
CHECK(l.count == 0); pump(f);
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "first report: losses=%u pending=%zu sent=%llu received=%llu overload=%llu",
|
|
l.count, stats(f, B).report_bytes, (unsigned long long)stats(f, B).reports_sent,
|
|
(unsigned long long)stats(f, A).reports_received, (unsigned long long)stats(f, B).overload_packets);
|
|
CHECK(l.count == 1);
|
|
CHECK(l.item[0].src == 2 && l.item[0].dst == 3 && l.item[0].source_node_id == 1 && l.item[0].descriptor == fd);
|
|
CHECK(l.item[0].packets == 1 && l.item[0].recipient_count == 1 && l.ids[0][0] == 3);
|
|
CHECK(l.item[0].prefix_len == 3 && memcmp(l.item[0].prefix, "one", 3) == 0);
|
|
for (int i = 3; i < 16; i++) CHECK(l.item[0].prefix[i] == 0);
|
|
CHECK(multicast_send(f->inst[A], fd, &one, 1, "two", 3) == 0);
|
|
CHECK(multicast_send(f->inst[A], fd, &one, 1, "three", 5) == 0);
|
|
route(f, A, 0, 5, B); route(f, B, 0, 5, C);
|
|
uint64_t different[] = {3, 5};
|
|
CHECK(multicast_send(f->inst[A], fd, different, 2, "set", 3) == 0);
|
|
pump(f); CHECK(l.count == 1);
|
|
uint64_t now = get_time_tb();
|
|
CHECK(now < first_drop + 2500);
|
|
test_clock_run_for(f->ua, (int)(first_drop + 2500 - now)); CHECK(l.count == 1);
|
|
test_clock_run_for(f->ua, 700); pump(f); CHECK(l.count == 3);
|
|
CHECK(l.item[1].packets == 2 && l.item[1].recipient_count == 1 && memcmp(l.item[1].prefix, "two", 3) == 0);
|
|
CHECK(l.item[2].packets == 1 && l.item[2].recipient_count == 2 && l.ids[2][0] == 3 && l.ids[2][1] == 5);
|
|
CHECK(stats(f, B).overload_packets == 4 && stats(f, B).queued_bytes == MULTICAST_QUEUE_BYTES);
|
|
unblock(&f->wire[B][C]); pump(f);
|
|
CHECK(c.count == 32 && stats(f, B).queued_bytes == 0);
|
|
test_clock_run_for(f->ua, 3100); CHECK(stats(f, B).report_bytes == 0);
|
|
multicast_unsubscribe(sc); multicast_unsubscribe(sd); teardown(f);
|
|
}
|
|
|
|
static void test_report_budget(void) {
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "multicast: bounded 16 KiB reports and closing a descriptor inside its loss callback");
|
|
struct fixture* f = setup();
|
|
struct losses l = {0};
|
|
int fd = multicast_open(f->group[A][0], loss, &l);
|
|
block(&f->wire[A][B]);
|
|
uint64_t dst = 2;
|
|
uint8_t payload[8154] = {0};
|
|
for (int i = 0; i < 32; i++) CHECK(multicast_send(f->inst[A], fd, &dst, 1, payload, sizeof(payload)) == 0);
|
|
CHECK(stats(f, A).queued_bytes == MULTICAST_QUEUE_BYTES);
|
|
for (int i = 0; i < 200; i++) {
|
|
int descriptor = multicast_open(f->group[A][0], loss, &l); CHECK(descriptor > 0);
|
|
CHECK(multicast_send(f->inst[A], descriptor, &dst, 1, "loss", 4) == 0);
|
|
CHECK(stats(f, A).report_bytes <= MULTICAST_REPORT_BYTES);
|
|
}
|
|
CHECK(stats(f, A).reports_evicted > 0 && l.count == 0);
|
|
pump(f); CHECK(l.count > 0 && l.count < 200);
|
|
CHECK(stats(f, A).queues == 1);
|
|
/* Два готовых уведомления одного FD; close в первом отменяет второе. */
|
|
struct losses close_loss = { .close = 1 };
|
|
int closing = multicast_open(f->group[A][0], loss, &close_loss);
|
|
uint64_t missing[] = {900, 901};
|
|
CHECK(multicast_send(f->inst[A], closing, missing, 1, "first", 5) == 0);
|
|
CHECK(multicast_send(f->inst[A], closing, missing + 1, 1, "second", 6) == 0);
|
|
pump(f); CHECK(close_loss.count == 1 && close_loss.item[0].reason == MULTICAST_NO_ROUTE);
|
|
multicast_close(f->inst[A], fd); CHECK(stats(f, A).queued_bytes == 0);
|
|
teardown(f);
|
|
}
|
|
|
|
static void test_reroute(void) {
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "multicast: route change behind blocked head and ETCP DELETE with pending waiter");
|
|
struct fixture* f = setup();
|
|
struct losses l = {0}; struct received c = {0}, d = {0};
|
|
int fd = multicast_open(f->group[A][0], loss, &l);
|
|
struct multicast_subscription* sc = multicast_subscribe(f->group[C][0], 1, fd, receive, &c); CHECK(sc);
|
|
struct multicast_subscription* sd = multicast_subscribe(f->group[D][0], 1, fd, receive, &d); CHECK(sd);
|
|
uint64_t b = 2, target = 3;
|
|
block(&f->wire[A][B]);
|
|
CHECK(multicast_send(f->inst[A], fd, &b, 1, "blocked head", 12) == 0);
|
|
CHECK(multicast_send(f->inst[A], fd, &target, 1, "reroute", 7) == 0);
|
|
pump(f);
|
|
route(f, A, 0, 3, D); multicast_routes_changed(f->inst[A]); pump(f);
|
|
CHECK(c.count == 1 && memcmp(c.prefix, "reroute", 7) == 0 && f->wire[A][B].packets == 0);
|
|
route(f, A, 0, 2, D);
|
|
struct wire* old = &f->wire[A][B];
|
|
old->conn.links_up = 0; old->conn.state = 2;
|
|
etcp_fire_conn_status(&old->conn, ETCP_CONN_STATUS_DELETE);
|
|
clear_queue(old->conn.send_input_q); old->conn.send_input_q = NULL; old->blocker = NULL;
|
|
pump(f); CHECK(stats(f, A).queued_bytes == 0 && l.count == 0);
|
|
CHECK(stats(f, B).unsubscribed == 1);
|
|
multicast_unsubscribe(sc); multicast_unsubscribe(sd); teardown(f);
|
|
}
|
|
|
|
static void put_le(uint8_t* p, uint64_t value, unsigned n) {
|
|
for (unsigned i = 0; i < n; i++) p[i] = value >> (i * 8);
|
|
}
|
|
|
|
static struct ll_entry* data_packet(uint16_t fd, const uint64_t* ids, unsigned count, const uint64_t* path, unsigned hops) {
|
|
unsigned len = MULTICAST_HEADER_SIZE + count * 8 + hops * 8 + 1;
|
|
struct ll_entry* e = ll_alloc_lldgram(len); CHECK(e); e->len = len;
|
|
e->dgram[0] = ETCP_ID_MULTICAST; put_le(e->dgram + 1, TOPO_GROUP_UTUN, 8);
|
|
put_le(e->dgram + 9, fd, 2); put_le(e->dgram + 11, count, 2); put_le(e->dgram + 13, 1, 8);
|
|
for (unsigned i = 0; i < count; i++) put_le(e->dgram + MULTICAST_HEADER_SIZE + i * 8, ids[i], 8);
|
|
for (unsigned i = 0; i < hops; i++) put_le(e->dgram + MULTICAST_HEADER_SIZE + count * 8 + i * 8, path[i], 8);
|
|
e->dgram[len - 1] = hops; return e;
|
|
}
|
|
|
|
static void test_validation(void) {
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "multicast: malformed wire, loop prevention and argument rejection without partial send");
|
|
struct fixture* f = setup();
|
|
struct losses l = {0}; struct received r = {0};
|
|
int fd = multicast_open(f->group[A][0], loss, &l);
|
|
struct multicast_subscription* sub = multicast_subscribe(f->group[B][0], 1, fd, receive, &r); CHECK(sub);
|
|
uint64_t ids[] = {2, 2}, path[] = {1, 2, 4};
|
|
etcp_recv_fn recv = f->inst[B]->api_bindings.callbacks[ETCP_ID_MULTICAST];
|
|
recv(&f->wire[B][A].conn, data_packet(fd, ids, 2, path, 1)); /* повтор адресата */
|
|
recv(&f->wire[B][A].conn, data_packet(fd, ids, 1, path, 2)); /* последний hop не равен пиру */
|
|
recv(&f->wire[B][D].conn, data_packet(fd, ids, 1, path, 3)); /* повторное посещение B */
|
|
struct ll_entry* e = data_packet(fd, ids, 1, path, 1); e->len--;
|
|
recv(&f->wire[B][A].conn, e); /* обрезанный trailer */
|
|
CHECK(stats(f, B).invalid_packets == 3 && stats(f, B).loop_packets == 1);
|
|
uint8_t payload[PKTNORM_MAX_DGRAM_SIZE] = {0};
|
|
CHECK(multicast_send(f->inst[A], fd, ids, 1, payload, sizeof(payload)) < 0);
|
|
ids[1] = 0;
|
|
CHECK(multicast_send(f->inst[A], fd, ids, 2, NULL, 0) < 0);
|
|
CHECK(multicast_send(f->inst[A], 0, ids, 1, NULL, 0) < 0);
|
|
pump(f); CHECK(r.count == 0 && l.count == 0 && f->wire[A][B].packets == 0);
|
|
multicast_unsubscribe(sub); teardown(f);
|
|
}
|
|
|
|
static void test_long_report(void) {
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "multicast: longest recipient list and loss report split into router-sized parts");
|
|
struct fixture* f = setup();
|
|
struct losses l = {0};
|
|
int fd = multicast_open(f->group[A][0], loss, &l);
|
|
block(&f->wire[B][C]);
|
|
uint64_t dst = 3;
|
|
uint8_t payload[8146] = {0};
|
|
for (int i = 0; i < 32; i++) {
|
|
CHECK(multicast_send(f->inst[A], fd, &dst, 1, payload, sizeof(payload)) == 0); pump(f);
|
|
}
|
|
CHECK(stats(f, B).queued_bytes == MULTICAST_QUEUE_BYTES);
|
|
enum { COUNT = (PKTNORM_MAX_DGRAM_SIZE - MULTICAST_HEADER_SIZE - MULTICAST_PATH_RESERVE) / 8 };
|
|
uint64_t ids[COUNT];
|
|
for (unsigned i = 0; i < COUNT; i++) {
|
|
ids[i] = i + 100; route(f, A, 0, ids[i], B); route(f, B, 0, ids[i], C);
|
|
}
|
|
CHECK(multicast_send(f->inst[A], fd, ids, COUNT, NULL, 0) == 0); pump(f);
|
|
CHECK(l.count == 2 && l.item[0].recipient_count + l.item[1].recipient_count == COUNT);
|
|
CHECK(l.item[0].packets == 1 && l.item[1].packets == 1 && !l.item[0].prefix_len && !l.item[1].prefix_len);
|
|
CHECK(l.ids[0][0] == 100 && l.ids[1][0] == 100 + l.item[0].recipient_count);
|
|
CHECK(stats(f, B).reports_sent == 2 && stats(f, A).reports_received == 2);
|
|
teardown(f);
|
|
}
|
|
|
|
static void test_report_backpressure(void) {
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "multicast: blocked report transport, eviction, restart and stale closed descriptor");
|
|
struct fixture* f = setup();
|
|
struct losses l = {0};
|
|
int fd = multicast_open(f->group[A][0], loss, &l);
|
|
block(&f->wire[B][C]); block(&f->wire[B][A]);
|
|
uint64_t dst = 3;
|
|
uint8_t payload[8146] = {0};
|
|
for (int i = 0; i < 32; i++) {
|
|
CHECK(multicast_send(f->inst[A], fd, &dst, 1, payload, sizeof(payload)) == 0); pump(f);
|
|
}
|
|
int closed = 0;
|
|
for (int i = 0; i < 200; i++) {
|
|
int descriptor = multicast_open(f->group[A][0], loss, &l); CHECK(descriptor > 0);
|
|
if (!i) closed = descriptor;
|
|
CHECK(multicast_send(f->inst[A], descriptor, &dst, 1, "congested", 9) == 0); pump(f);
|
|
CHECK(stats(f, B).report_bytes <= MULTICAST_REPORT_BYTES);
|
|
}
|
|
CHECK(l.count == 0 && stats(f, B).reports_evicted > 0);
|
|
multicast_close(f->inst[A], closed);
|
|
/* Перезапуск router очищает send_q и отменяет её waiters; таймер должен продолжить отчёты. */
|
|
etcp_router_conn_restart(f->inst[B], f->group[B][0]->group_id, 1, ETCP_RT_ID_MULTICAST_REPORT);
|
|
unblock(&f->wire[B][A]); test_clock_run_for(f->ua, 10000);
|
|
CHECK(l.count > 0 && l.count < 200);
|
|
for (unsigned i = 0; i < l.count; i++) {
|
|
CHECK(l.item[i].descriptor != closed && l.item[i].reason == MULTICAST_OVERLOAD);
|
|
CHECK(l.item[i].src == 2 && l.item[i].dst == 3 && l.item[i].packets == 1);
|
|
}
|
|
CHECK(stats(f, B).report_bytes == 0);
|
|
teardown(f);
|
|
}
|
|
|
|
static void test_descriptor_wrap(void) {
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "multicast: sequential 16-bit IDs, exhaustion, wrap and skipping occupied IDs");
|
|
struct fixture* f = setup();
|
|
struct losses l = {0};
|
|
for (int i = 1; i <= UINT16_MAX; i++) CHECK(multicast_open(f->group[A][i % GROUPS], loss, &l) == i);
|
|
CHECK(multicast_open(f->group[A][0], loss, &l) < 0);
|
|
multicast_close(f->inst[A], 2); multicast_close(f->inst[A], 65000);
|
|
CHECK(multicast_open(f->group[A][0], loss, &l) == 2);
|
|
CHECK(multicast_open(f->group[A][0], loss, &l) == 65000);
|
|
CHECK(stats(f, A).descriptors == UINT16_MAX);
|
|
teardown(f);
|
|
}
|
|
|
|
int main(void) {
|
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_categories(DEBUG_CATEGORY_ALL);
|
|
debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_INFO);
|
|
test_clock_init(1000);
|
|
if (getenv("UTUN_TEST_DEBUG")) {
|
|
debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG);
|
|
debug_set_category_level(DEBUG_CATEGORY_ETCPROUTE, DEBUG_LEVEL_TRACE);
|
|
debug_set_category_level(DEBUG_CATEGORY_TIMERS, DEBUG_LEVEL_DEBUG);
|
|
}
|
|
test_delivery(); test_lifetime(); test_overload(); test_report_budget();
|
|
test_reroute(); test_validation(); test_long_report(); test_report_backpressure();
|
|
debug_set_category_level(DEBUG_CATEGORY_ETCPROUTE, DEBUG_LEVEL_WARN);
|
|
test_descriptor_wrap();
|
|
test_clock_finish();
|
|
printf("PASS multicast: delivery, lifetime, overload, reports, reroute, validation, descriptor wrap\n");
|
|
return 0;
|
|
}
|