Files
utun2/tests/test_etcp_multipath.c
T

434 lines
25 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Настоящие ETCP/NCD/BBR поверх независимых UDP-каналов. Строгие проверки скорости и пауз при DROP/HOLD. */
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "etcp.h"
#include "etcp_api.h"
#include "etcp_session.h"
#include "node_conn_direct.h"
#include "topo_node.h"
#include "utun_instance.h"
#include "dummynet.h"
#include "test_net.h"
#include "test_clock.h"
#include "../lib/mem.h"
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL %s:%d: %s", __func__, __LINE__, #x); exit(1); } } while (0)
#define PATHS 3
#define TRACE_COUNT 131072
#define CMD 253
#define SAMPLE_MS 4000
#define WARMUP_MS 3000
#define STALL_MS 3000
#define MAX_GAP_US 100000
struct profile { unsigned rtt_ms, kbps; };
struct message { uint8_t cmd, from, reserved[2]; uint32_t seq; uint64_t enqueued; uint8_t body[1008]; };
struct trace { uint64_t first_tx, first_rx; uint32_t attempts; uint8_t first_path; };
struct endpoint {
struct UTUN_INSTANCE* inst;
struct NODE_CONN_DIRECT* owner;
struct ETCP_CONN* conn;
struct ETCP_SOCKET* sockets[PATHS];
unsigned sent, received;
uint64_t bytes, last_delivery, max_gap, max_latency, latency_hist[10001], samples;
};
struct path_stats { uint64_t data, retries, rx_unique; };
static struct UASYNC* ua;
static struct endpoint ep[2];
static struct dummynet* channels[PATHS];
static struct profile profiles[PATHS];
static struct trace traces[2][TRACE_COUNT];
static struct path_stats traffic[2][PATHS];
static unsigned path_count, failures, enabled_mask, bidirectional, low_load, measuring;
static uint64_t started, next_message[2], measure_start, fault_start;
static uint32_t fault_seq;
static unsigned fault_trace_count;
static FILE* csv;
static const char* case_name;
static const char* pubkeys[2] = {
"b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01",
"ce8871f07fa056c636d297115f231b08c29cdf94e0d440fce83a07c34416d36a"
};
static const char* privkeys[2] = {
"704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f",
"38240cb82199e504686507f11f6eaa4f740fde6f0c425c495e49a523019a5d68"
};
/* Наблюдение не заменяет передачу/приём и не меняет состояние протокола. */
void __real_etcp_dump_pkt_sections(struct ETCP_DGRAM*, struct ETCP_LINK*, int);
void __wrap_etcp_dump_pkt_sections(struct ETCP_DGRAM* pkt, struct ETCP_LINK* link, int sending) {
int side = link->etcp->instance == ep[0].inst ? 0 : 1, path = -1;
for (unsigned p = 0; p < path_count; p++)
if (ss_get_port(&link->remote_addr) == dummynet_get_listen_port(channels[p])) path = p;
if (path >= 0) for (unsigned offset = 0; offset < pkt->data_len;) {
const uint8_t* data = pkt->data + offset;
unsigned remaining = pkt->data_len - offset, len;
switch (data[0]) {
case ETCP_SECTION_ACK: CHECK(remaining >= 8); len = 8 + 8 * data[1]; break;
case ETCP_SECTION_TIMESTAMP: len = 5; break;
case ETCP_SECTION_MEAS_TS: len = 9; break;
case ETCP_SECTION_MEAS_RESP: len = 13; break;
case ETCP_SECTION_FILLER: CHECK(remaining >= 3); len = 3 + data[1] + (data[2] << 8); break;
case ETCP_SECTION_PAYLOAD: {
CHECK(remaining >= 5);
uint32_t seq = (uint32_t)data[1] | ((uint32_t)data[2] << 8) | ((uint32_t)data[3] << 16) | ((uint32_t)data[4] << 24);
CHECK(seq < TRACE_COUNT);
struct trace* trace = &traces[sending ? side : 1-side][seq];
uint64_t now = get_time_us();
if (sending) {
if (trace->attempts++ && measuring) traffic[side][path].retries++;
if (!trace->first_tx) { trace->first_tx = now; trace->first_path = path; }
if (measuring) traffic[side][path].data += remaining - 5;
} else if (!trace->first_rx) {
trace->first_rx = now;
if (measuring) traffic[1-side][path].rx_unique += remaining - 5;
}
if (fault_start && (seq == fault_seq || (sending && trace->attempts > 1 && fault_trace_count++ < 32)))
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "FAULT trace dir=%d seq=%u path=%d tx=%d attempts=%u elapsed_us=%llu", side,
seq, path, sending, trace->attempts, (unsigned long long)(now - fault_start));
len = remaining; break;
}
default: len = remaining; break;
}
CHECK(len <= remaining); offset += len;
}
__real_etcp_dump_pkt_sections(pkt, link, sending);
}
static void receive_message(struct ETCP_CONN* conn, struct ll_entry* entry) {
int side = conn->instance == ep[0].inst ? 0 : 1;
struct endpoint* dest = &ep[side];
CHECK(entry->len == sizeof(struct message));
struct message msg; memcpy(&msg, entry->dgram, sizeof(msg));
CHECK(msg.cmd == CMD && msg.from == 1-side && msg.seq == dest->received);
for (unsigned i = 0; i < sizeof(msg.body); i++) CHECK(msg.body[i] == (uint8_t)(msg.seq * 3 + msg.from + i));
dest->received++; dest->bytes += sizeof(msg);
uint64_t now = get_time_us();
if (measuring) {
uint64_t gap = now - dest->last_delivery, latency = now - msg.enqueued;
if (gap > dest->max_gap) dest->max_gap = gap;
if (latency > dest->max_latency) dest->max_latency = latency;
dest->latency_hist[latency / 1000 < 10000 ? latency / 1000 : 10000]++; dest->samples++;
}
dest->last_delivery = now;
queue_dgram_free(entry); queue_entry_free(entry);
}
static void create_endpoint(unsigned side) {
char config[2048], listeners[768] = "";
for (unsigned p = 0; p < path_count; p++) {
char text[160]; snprintf(text, sizeof(text), "[server:p%u]\naddr=127.0.0.1:0\ntype=public\n", p);
strcat(listeners, text);
}
snprintf(config, sizeof(config), "[global]\nmy_node_name=multipath-%u\nmy_public_key=%s\nmy_private_key=%s\n"
"keepalive_interval=100\nkeepalive_timeout=2000\nkeepalive_adaptive=0\nauto_sockets=no\n"
"[allowed_keys]\nallow_all=1\n%s",
side, pubkeys[side], privkeys[side], listeners);
ep[side].inst = utun_instance_create_from_str(ua, config); CHECK(ep[side].inst);
CHECK(utun_core_start(ep[side].inst) == 0);
CHECK(etcp_bind(ep[side].inst, CMD, receive_message) == 0);
for (unsigned p = 0; p < path_count; p++) {
char name[16]; snprintf(name, sizeof(name), "p%u", p);
ep[side].sockets[p] = test_etcp_bound_socket(ep[side].inst, name);
}
}
static void setup(const struct profile* settings, unsigned count, unsigned mask) {
memset(ep, 0, sizeof(ep)); memset(channels, 0, sizeof(channels));
memset(traces, 0, sizeof(traces)); memset(traffic, 0, sizeof(traffic));
memset(next_message, 0, sizeof(next_message));
path_count = count; enabled_mask = mask; measuring = 0; fault_start = 0; fault_trace_count = 0;
memcpy(profiles, settings, count * sizeof(*settings));
ua = uasync_create(); CHECK(ua);
create_endpoint(0); create_endpoint(1);
for (unsigned p = 0; p < count; p++) {
if (!(mask & (1u << p))) continue;
channels[p] = dummynet_create(ua, "127.0.0.1", 0); CHECK(channels[p]);
for (int dir = 0; dir < 2; dir++) CHECK(dummynet_set_direction(channels[p], dir, settings[p].rtt_ms / 2, 0,
settings[p].kbps, 512, 0, "127.0.0.1", ss_get_port(&ep[1-dir].sockets[p]->local_addr)) == 0);
struct TOPO_ADDR4 addr = { .protocol = TOPO_PROTO_UDP, .port = dummynet_get_listen_port(channels[p]) };
uint32_t ip = htonl(INADDR_LOOPBACK); memcpy(addr.addr, &ip, sizeof(ip));
struct TOPO_NODE peer = { .node_id = ep[1].inst->node_id, .node_name = "multipath-peer", .v4_addrs = &addr };
memcpy(peer.public_key, ep[1].inst->my_keys.public_key, SC_PUBKEY_SIZE);
struct NODE_CONN_DIRECT* extra = NULL;
CHECK(node_conn_direct_open_node(ep[0].inst, peer.node_id, NULL, NULL, &extra, &peer, ep[0].sockets[p]) >= 0);
if (!ep[0].owner) ep[0].owner = extra;
else node_conn_direct_close(extra);
}
uint64_t end = get_time_us() + 10000000;
int ready = 0;
while (get_time_us() < end) {
test_clock_poll(ua, 10); ready = 1;
for (int i = 0; i < 2; i++) {
ep[i].conn = instance_find_conn(ep[i].inst, ep[1-i].inst->node_id);
struct ETCP_CONN* c = ep[i].conn;
if (!c || !c->initialized || !c->links_up || c->session_required || c->reinit_pending) { ready = 0; continue; }
for (unsigned p = 0; p < count; p++) if (mask & (1u << p)) {
int found = 0;
for (struct ETCP_LINK* l = c->links; l; l = l->next)
if (ss_get_port(&l->remote_addr) == dummynet_get_listen_port(channels[p]) && l->initialized && l->link_status) found++;
if (found != 1) ready = 0;
}
}
if (ready) break;
}
CHECK(ready);
struct TOPO_NODE peer = { .node_id = ep[0].inst->node_id, .node_name = "held-peer" };
memcpy(peer.public_key, ep[0].inst->my_keys.public_key, SC_PUBKEY_SIZE);
CHECK(node_conn_direct_open_node(ep[1].inst, peer.node_id, NULL, NULL, &ep[1].owner, &peer, NULL) >= 0);
started = get_time_us();
for (int i = 0; i < 2; i++) ep[i].last_delivery = started;
}
/* Ограниченная очередь источника: latency не скрывается за неограниченным накоплением тестовых сообщений. */
static void produce(unsigned side) {
struct endpoint* source = &ep[side];
uint64_t now = get_time_us();
if (low_load && now < next_message[side]) return;
for (unsigned budget = 0; budget < 64 && source->conn->send_input_q->count < 4; budget++) {
if (source->sent - ep[1-side].received >= 2048) break;
struct ll_entry* entry = ll_alloc_lldgram(sizeof(struct message)); CHECK(entry);
struct message msg = { .cmd = CMD, .from = side, .seq = source->sent, .enqueued = now };
for (unsigned i = 0; i < sizeof(msg.body); i++) msg.body[i] = msg.seq * 3 + side + i;
memcpy(entry->dgram, &msg, sizeof(msg)); entry->len = sizeof(msg);
CHECK(etcp_send(source->conn, entry) == 0); source->sent++;
if (low_load) { next_message[side] = now + 10000; break; }
}
}
static void snapshot(const char* phase) {
uint64_t now = get_time_us();
for (int side = 0; side < 2; side++) {
struct ETCP_CONN* conn = ep[side].conn;
for (struct ETCP_LINK* l = conn->links; l; l = l->next) {
uint64_t occupied = 0; unsigned packets = 0;
for (struct ll_entry* e = conn->input_wait_ack->head; e; e = e->next) {
struct INFLIGHT_PACKET* pkt = (struct INFLIGHT_PACKET*)e;
CHECK(pkt->state == INFLIGHT_STATE_WAIT_ACK);
if (pkt->last_link == l) { occupied += pkt->ll.len; packets++; }
}
CHECK(l->inflight_bytes == occupied && l->inflight_packets == packets);
int p = -1;
for (unsigned j = 0; j < path_count; j++)
if (ss_get_port(&l->remote_addr) == dummynet_get_listen_port(channels[j])) p = j;
if (p < 0) continue;
const struct dummynet_stats* ds = dummynet_get_stats(channels[p], side);
if (csv) fprintf(csv, "%s,%x,%s,%llu,%d,%d,%u,%u,%u,%u,%u,%u,%d,%d,%llu,%llu,%llu,%u,%llu,%llu,%d,%u,%llu\n",
case_name, enabled_mask, phase, (unsigned long long)((now-started)/1000), side, p, l->rtt_last, l->bandwidth,
l->inflight_bytes, l->inflight_lim_bytes, conn->rtt_avg_10, conn->jitter, conn->input_send_q->count,
conn->recv_q->count, (unsigned long long)ds->sent_bytes, (unsigned long long)traffic[side][p].rx_unique,
(unsigned long long)traffic[side][p].retries, ds->queue_size,
(unsigned long long)ep[1-side].bytes, (unsigned long long)ep[1-side].max_gap,
ep[1-side].conn->recv_q->count, ep[1-side].conn->last_delivered_id+1, (unsigned long long)ds->dropped);
}
CHECK(conn->unacked_bytes == conn->input_send_q->total_bytes + conn->input_wait_ack->total_bytes);
}
}
static void run_for(unsigned ms, int sending, const char* phase) {
uint64_t end = get_time_us() + ms * 1000ULL, next_snapshot = get_time_us();
while (get_time_us() < end) {
if (sending) { produce(0); if (bidirectional) produce(1); }
test_clock_poll(ua, 10);
for (int i = 0; i < 2; i++) CHECK(ep[i].conn->state != 2 && !ep[i].conn->reinit_pending);
if (get_time_us() >= next_snapshot) { snapshot(phase); next_snapshot = get_time_us() + 100000; }
if (!sending && ep[0].sent == ep[1].received && ep[1].sent == ep[0].received &&
!ep[0].conn->unacked_bytes && !ep[1].conn->unacked_bytes) break;
}
}
static void begin_measure(void) {
measure_start = get_time_us(); measuring = 1;
memset(traffic, 0, sizeof(traffic));
for (int i = 0; i < 2; i++) {
ep[i].max_gap = ep[i].max_latency = ep[i].samples = 0; ep[i].last_delivery = measure_start;
memset(ep[i].latency_hist, 0, sizeof(ep[i].latency_hist));
}
for (unsigned p = 0; p < path_count; p++) if (channels[p]) dummynet_reset_stats(channels[p], -1);
}
static void expect(int passed, const char* requirement, uint64_t actual, uint64_t limit) {
if (passed) return;
failures++;
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "FAIL %s: %s actual=%llu limit=%llu", case_name, requirement,
(unsigned long long)actual, (unsigned long long)limit);
}
static uint64_t percentile(const struct endpoint* endpoint, unsigned percent) {
uint64_t sum = 0, target = (endpoint->samples * percent + 99) / 100;
if (!target) return 0;
for (unsigned ms = 0; ms <= 10000; ms++) {
sum += endpoint->latency_hist[ms];
if (sum >= target) return ms;
}
return 10000;
}
static void teardown(void) {
measuring = 0;
// Уничтожаем и активные delay/HOLD пакеты: эмуляторы не должны пережить свои endpoints.
for (unsigned p = 0; p < path_count; p++) { dummynet_destroy(channels[p]); channels[p] = NULL; }
for (int i = 0; i < 2; i++) node_conn_direct_close(ep[i].owner);
uasync_drain_immediate(ua);
for (int i = 0; i < 2; i++) { utun_instance_destroy(ep[i].inst); ep[i].inst = NULL; }
uasync_poll(ua, 0); CHECK(ua->timer_alloc_count == ua->timer_free_count); uasync_destroy(ua, 0);
}
/* Полезные байты приложения — отдельный критерий; FILLER/повторы не могут улучшить результат. */
static uint64_t measure_rate(const struct profile* settings, unsigned count, unsigned mask) {
setup(settings, count, mask); low_load = 0;
run_for(WARMUP_MS, 1, "warmup"); begin_measure();
uint64_t before[2] = {ep[0].bytes, ep[1].bytes};
run_for(SAMPLE_MS, 1, "steady");
uint64_t elapsed = get_time_us() - measure_start, rate = 0;
for (unsigned side = 0; side <= bidirectional; side++) {
uint64_t useful = (ep[1-side].bytes-before[1-side]) * 8000 / elapsed, capacity = 0, unique_total = 0;
for (unsigned p = 0; p < count; p++) if (mask & (1u << p)) {
uint64_t wire = dummynet_get_stats(channels[p], side)->sent_bytes * 8000 / elapsed;
uint64_t unique = traffic[side][p].rx_unique * 8000 / elapsed;
unique_total += traffic[side][p].rx_unique;
capacity += settings[p].kbps;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RATE %s dir=%u path=%u RTT=%u BWlim=%u wire=%llu unique=%llu kbps retries=%llu",
case_name, side, p, settings[p].rtt_ms, settings[p].kbps, (unsigned long long)wire,
(unsigned long long)unique, (unsigned long long)traffic[side][p].retries);
expect(wire >= settings[p].kbps * 90 / 100, "channel wire utilization >=90%", wire, settings[p].kbps * 90 / 100);
expect(unique >= settings[p].kbps * 80 / 100, "channel unique DATA >=80%", unique, settings[p].kbps * 80 / 100);
}
uint64_t fresh = unique_total * 8000 / elapsed;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "GOODPUT %s dir=%u application=%llu fresh_DATA=%llu capacity=%llu kbps", case_name, side,
(unsigned long long)useful, (unsigned long long)fresh, (unsigned long long)capacity);
// Нельзя пройти за счёт выдачи фрагментов, накопленных до начала измерения.
if (fresh < useful) useful = fresh;
if (!side) rate = useful;
expect(useful >= capacity * 85 / 100, "application goodput >=85% capacity", useful, capacity * 85 / 100);
}
measuring = 0;
run_for(10000, 0, "drain");
for (int i = 0; i < 2; i++) expect(ep[i].sent == ep[1-i].received, "complete ordered delivery after drain",
ep[1-i].received, ep[i].sent);
teardown(); return rate;
}
/* Не ждём восстановления отказавшего пути: проверяем паузу и скорость пока DROP/HOLD ещё включён. */
static void stalled(const struct profile* settings, unsigned count, unsigned broken, unsigned directions,
enum dummynet_mode mode, int sparse, unsigned duration_ms) {
low_load = sparse;
setup(settings, count, (1u << count)-1);
run_for(WARMUP_MS, 1, "warmup");
// Выбираем момент, когда на каждом останавливаемом пути есть действительно неподтверждённый пакет.
uint64_t deadline = get_time_us() + 2000000;
unsigned pending = 0;
fault_seq = 0;
while (get_time_us() < deadline) {
pending = 0; fault_seq = 0;
for (struct ll_entry* entry = ep[0].conn->input_wait_ack->head; entry; entry = entry->next) {
struct INFLIGHT_PACKET* pkt = (struct INFLIGHT_PACKET*)entry;
if (!pkt->last_link || ((directions & 1) && traces[0][pkt->seq].first_rx)) continue;
for (unsigned p = 0; p < count; p++) if ((broken & (1u << p)) &&
ss_get_port(&pkt->last_link->remote_addr) == dummynet_get_listen_port(channels[p])) {
// Пакет должен ещё находиться в propagation delay, а не уже лежать в сокете получателя.
if ((directions & 1) && get_time_us() - traces[0][pkt->seq].first_tx >= settings[p].rtt_ms * 500ULL) continue;
pending |= 1u << p;
if (!fault_seq || pkt->seq < fault_seq) fault_seq = pkt->seq;
}
}
if (pending == broken) break;
run_for(1, 1, "arm-fault");
}
CHECK(pending == broken && fault_seq);
begin_measure(); fault_start = get_time_us();
uint32_t epochs[2] = {ep[0].conn->reinit_count, ep[1].conn->reinit_count};
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "FAULT %s mode=%d paths=%x directions=%x seq=%u duration_ms=%u sparse=%d",
case_name, mode, broken, directions, fault_seq, duration_ms, sparse);
for (unsigned p = 0; p < count; p++) if (broken & (1u << p))
for (unsigned dir = 0; dir < 2; dir++) if (directions & (1u << dir))
CHECK(dummynet_set_mode(channels[p], dir, mode) == 0);
uint64_t before = ep[1].bytes;
run_for(duration_ms > 1000 ? 1000 : duration_ms, 1, "fault-transition");
uint64_t steady_bytes = ep[1].bytes, steady_start = get_time_us();
if (duration_ms > 1000) run_for(duration_ms - 1000, 1, "fault-steady");
uint64_t now = get_time_us(), tail_gap = now - ep[1].last_delivery;
if (tail_gap > ep[1].max_gap) ep[1].max_gap = tail_gap;
uint64_t capacity = 0;
for (unsigned p = 0; p < count; p++) if (!(broken & (1u << p))) capacity += settings[p].kbps;
uint64_t rate = now > steady_start ? (ep[1].bytes-steady_bytes) * 8000 / (now-steady_start) : 0;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "FAULT result %s delivered=%llu gap_us=%llu p50=%llu p95=%llu p99=%llu max_latency_us=%llu "
"steady_kbps=%llu healthy_capacity=%llu missing_seq=%u first_rx_us=%llu attempts=%u", case_name,
(unsigned long long)(ep[1].bytes-before), (unsigned long long)ep[1].max_gap,
(unsigned long long)percentile(&ep[1], 50), (unsigned long long)percentile(&ep[1], 95),
(unsigned long long)percentile(&ep[1], 99), (unsigned long long)ep[1].max_latency,
(unsigned long long)rate, (unsigned long long)capacity, fault_seq,
(unsigned long long)(traces[0][fault_seq].first_rx > fault_start ? traces[0][fault_seq].first_rx-fault_start : 0),
traces[0][fault_seq].attempts);
if (duration_ms > 100) expect(ep[1].bytes > before, "delivery during active fault", ep[1].bytes-before, 1);
expect(ep[1].max_gap <= MAX_GAP_US, "maximum delivery pause <=100ms", ep[1].max_gap, MAX_GAP_US);
if (sparse) expect(ep[1].max_latency <= MAX_GAP_US, "bounded-source latency <=100ms", ep[1].max_latency, MAX_GAP_US);
else if (duration_ms > 1000) expect(rate >= capacity * 85 / 100,
"fault steady goodput >=85% healthy capacity", rate, capacity * 85 / 100);
for (int i = 0; i < 2; i++) CHECK(ep[i].conn->reinit_count == epochs[i] && ep[i].conn->links_up);
measuring = duration_ms <= 100;
for (unsigned p = 0; p < count; p++) if (broken & (1u << p))
for (unsigned dir = 0; dir < 2; dir++) CHECK(dummynet_set_mode(channels[p], dir, DUMMYNET_RUN) == 0);
run_for(1000, 1, "recovery");
if (duration_ms <= 100) {
expect(ep[1].samples > 0, "delivery after short HOLD", ep[1].samples, 1);
expect(ep[1].max_gap <= MAX_GAP_US, "short HOLD including resume: pause <=100ms", ep[1].max_gap, MAX_GAP_US);
expect(ep[1].max_latency <= MAX_GAP_US, "short HOLD including resume: latency <=100ms", ep[1].max_latency, MAX_GAP_US);
}
measuring = 0;
run_for(2000, 1, "recovery"); run_for(10000, 0, "drain");
for (int i = 0; i < 2; i++) expect(ep[i].sent == ep[1-i].received, "complete delivery after recovery",
ep[1-i].received, ep[i].sent);
teardown();
}
int main(int argc, char** argv) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); test_clock_init(10);
if (getenv("UTUN_TEST_DEBUG")) debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_DEBUG);
CHECK(socket_platform_init() == 0);
const char* output = getenv("UTUN_MULTIPATH_CSV");
if (output) { csv = fopen(output, "w"); CHECK(csv); fprintf(csv,
"case,mask,phase,time_ms,direction,path,rtt_tb,pacing_kbps,inflight,cwnd,conn_rtt_tb,jitter,send_q,local_recv_q,wire_bytes,"
"unique_bytes,retries,channel_q,app_bytes,max_gap_us,dest_recv_q,needed_seq,queue_drops\n"); }
struct profile matrix[][3] = {
{{20,8000},{20,8000}}, {{20,2000},{20,8000}}, {{20,8000},{200,8000}},
{{20,2000},{200,8000}}, {{20,8000},{200,2000}}, {{20,2000},{80,8000},{200,2000}}, {{20,2000},{80,8000}}
};
const char* names[] = {"equal", "bandwidth", "rtt", "fast-distant", "slow-distant", "three", "bidirectional"};
size_t baseline = u_get_allocated_count();
unsigned selected = 0;
for (unsigned n = 0; n < sizeof(names)/sizeof(names[0]); n++) {
if (argc > 1 && strcmp(argv[1], names[n])) continue;
selected++;
bidirectional = n == 6;
case_name = names[n]; unsigned count = n == 5 ? 3 : 2;
uint64_t solo_sum = 0;
for (unsigned p = 0; p < count; p++) solo_sum += measure_rate(matrix[n], count, 1u << p);
uint64_t aggregate = measure_rate(matrix[n], count, (1u << count)-1);
expect(aggregate >= solo_sum * 90 / 100, "aggregation >=90% sum of solo rates", aggregate, solo_sum * 90 / 100);
CHECK(u_get_allocated_count() == baseline);
}
struct { const char* name; unsigned count, broken, directions, sparse, ms; enum dummynet_mode mode; } faults[] = {
{"drop",2,2,3,1,3000,DUMMYNET_DROP}, {"hold",2,2,3,1,3000,DUMMYNET_HOLD},
{"drop-forward",2,2,1,1,3000,DUMMYNET_DROP}, {"drop-reverse",2,2,2,1,3000,DUMMYNET_DROP},
{"short-hold",2,2,3,1,30,DUMMYNET_HOLD}, {"slow-drop",2,2,3,1,3000,DUMMYNET_DROP},
{"two-stuck",3,6,3,1,3000,DUMMYNET_DROP}, {"load-drop",2,2,3,0,5000,DUMMYNET_DROP},
{"load-hold",2,2,3,0,5000,DUMMYNET_HOLD}
};
for (unsigned n = 0; n < sizeof(faults)/sizeof(faults[0]); n++) {
if (argc > 1 && strcmp(argv[1], faults[n].name)) continue;
selected++;
case_name = faults[n].name;
bidirectional = 0;
struct profile settings[] = {{20,8000},{20,8000},{20,2000}};
if (!strcmp(case_name, "slow-drop")) settings[1].rtt_ms = 200;
if (faults[n].count == 3) { settings[1].rtt_ms = 80; settings[2].rtt_ms = 200; }
stalled(settings, faults[n].count, faults[n].broken, faults[n].directions, faults[n].mode, faults[n].sparse, faults[n].ms);
CHECK(u_get_allocated_count() == baseline);
}
CHECK(selected && argc <= 2);
if (csv) CHECK(fclose(csv) == 0);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Multipath completed: failures=%u", failures);
test_clock_finish(); socket_platform_cleanup(); return failures ? 1 : 0;
}