Merge origin/master with encrypted attachment ownership fixes
Validation: ./build.sh --no-gui -j4 and make check -j4 passed; 186 tests, no failures.
This commit is contained in:
commit
bafda5ae3a
38 files changed
+2854
-984
No files matched your search
+9
-1
@@ -62,6 +62,7 @@ check_PROGRAMS = \
|
||||
test_merkle_protocol \
|
||||
test_ncd_config \
|
||||
test_node_snapshot \
|
||||
test_topo_node_wire \
|
||||
test_route6_lib \
|
||||
test_etcp_transport \
|
||||
test_stcp_ping \
|
||||
@@ -732,6 +733,12 @@ test_node_snapshot_SOURCES = test_node_snapshot.c
|
||||
test_node_snapshot_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
|
||||
test_node_snapshot_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
|
||||
|
||||
test_topo_node_wire_SOURCES = test_topo_node_wire.c
|
||||
test_topo_node_wire_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
|
||||
if OS_LINUX
|
||||
test_topo_node_wire_LDFLAGS = -Wl,--wrap=u_malloc_impl -Wl,--wrap=u_calloc_impl -Wl,--wrap=memory_pool_alloc_impl
|
||||
endif
|
||||
|
||||
test_ncd_config_SOURCES = test_ncd_config.c
|
||||
test_ncd_config_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
|
||||
test_ncd_config_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
|
||||
@@ -1057,7 +1064,8 @@ test_etcp_multipath_LDFLAGS = $(test_clock_ldflags) -Wl,--wrap=etcp_dump_pkt_sec
|
||||
test_stcp_sigpipe_SOURCES = test_stcp_sigpipe.c
|
||||
test_stcp_sigpipe_LDADD = $(LIBUTUN) $(COMMON_LIBS)
|
||||
test_etcp_windows_LDADD = $(LIBUTUN) $(COMMON_LIBS)
|
||||
test_etcp_windows_LDFLAGS = $(test_clock_ldflags) -Wl,--wrap=get_time_tb -Wl,--wrap=etcp_encrypt_send -Wl,--wrap=memory_pool_alloc_impl \
|
||||
test_etcp_windows_LDFLAGS = $(test_clock_ldflags) -Wl,--wrap=get_time_tb -Wl,--wrap=get_time_us \
|
||||
-Wl,--wrap=etcp_encrypt_send -Wl,--wrap=memory_pool_alloc_impl \
|
||||
-Wl,--wrap=bbr_main -Wl,--wrap=uasync_set_timeout -Wl,--wrap=uasync_call_soon -Wl,--wrap=ll_alloc_lldgram \
|
||||
-Wl,--wrap=u_malloc_impl -Wl,--wrap=u_calloc_impl -Wl,--wrap=u_realloc_impl -Wl,--wrap=random_bytes
|
||||
|
||||
|
||||
+300
-4
@@ -2,9 +2,11 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <errno.h>
|
||||
#include "etcp.h"
|
||||
#include "etcp_api.h"
|
||||
#include "etcp_session.h"
|
||||
#include "etcp_loadbalancer.h"
|
||||
#include "node_conn_direct.h"
|
||||
#include "topo_node.h"
|
||||
#include "utun_instance.h"
|
||||
@@ -13,7 +15,13 @@
|
||||
#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)
|
||||
static void diagnostic_flush(void);
|
||||
static int diagnostic_flushing;
|
||||
#define CHECK(x) do { if (!(x)) { \
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL %s:%d: %s", __func__, __LINE__, #x); \
|
||||
if (!diagnostic_flushing) diagnostic_flush(); \
|
||||
exit(1); \
|
||||
} } while (0)
|
||||
#define PATHS 3
|
||||
#define TRACE_COUNT 131072
|
||||
#define CMD 253
|
||||
@@ -78,11 +86,194 @@ static const char* privkeys[2] = {
|
||||
"38240cb82199e504686507f11f6eaa4f740fde6f0c425c495e49a523019a5d68"
|
||||
};
|
||||
|
||||
/* Диагностика включается UTUN_MULTIPATH_DIAG=path.csv. Срезы и счётчики пишутся в память,
|
||||
* файл/лог — только после drain каждого стенда. Пики наблюдаются на TX/RX/ACK/выборе пути.
|
||||
* Интервал по умолчанию 100мс; UTUN_MULTIPATH_DIAG_MS=10..1000 позволяет уточнить короткий участок. */
|
||||
struct diagnostic_interval {
|
||||
uint64_t tx_data, fresh_rx, data_packets, retries, ack_only_packets, ack_only_wire, piggy_packets, piggy_bytes;
|
||||
uint64_t ack_entries, ack_delay_sum, ack_delay_max, ack_only_entries, ack_only_delay_sum, early_ack_entries;
|
||||
uint64_t ordinary_ack_packets, early_ack_packets; // молодые entries допустимы в пакете, где старейший ACK уже готов
|
||||
uint64_t control_packets, samples, limited, app_limited;
|
||||
uint64_t rtt_min, rtt_max, rtt_sum, rtt_samples, searches, window_blocked, pacer_blocked, observations;
|
||||
uint32_t flight_min, flight_max, cwnd_min, cwnd_max, send_max, ack_max;
|
||||
int64_t debt_min_us, debt_max_us;
|
||||
};
|
||||
struct diagnostic_sample {
|
||||
const char* phase;
|
||||
uint64_t time_us, interval_us, wire_bytes, channel_received, channel_lost, channel_dropped, app_bytes, ack_age_us, attempt_age_us;
|
||||
uint64_t pacer_ready_us;
|
||||
uint64_t queue_bytes[5];
|
||||
unsigned queue_count[5], side, path, fresh, retries, pending_bytes, normalizer_bytes, normalizer_waiter, normalizer_flush;
|
||||
unsigned unacked, budget, flight, attempts, cwnd, pace, mode, cycle, min_rtt, rtt, jitter, rto, bw, queue_cap;
|
||||
unsigned ack_count, ack_timer, ack_till, urgent_ack, pacer_timer, burst, stalled, rto_streak;
|
||||
unsigned channel_pending, channel_peak;
|
||||
unsigned app_suspended, app_resume, fragment_suspended, fragment_resume, waiter_deferred;
|
||||
unsigned tx_state, tx_scheduled, tx_delayed, tx_running, active_attempts, next_seq, peer_till, local_till;
|
||||
uint64_t channel_delay_max_us;
|
||||
int64_t debt_us;
|
||||
struct diagnostic_interval stats;
|
||||
};
|
||||
static FILE* diagnostic_file;
|
||||
static struct diagnostic_sample diagnostic_rows[16384]; // хватает и для трёх путей/10мс с полным drain
|
||||
static struct diagnostic_interval diagnostic_stats[2][PATHS];
|
||||
static struct dummynet_stats diagnostic_previous[2][PATHS];
|
||||
static unsigned diagnostic_count, diagnostic_active, diagnostic_period_us = 100000;
|
||||
static uint64_t diagnostic_last_us;
|
||||
|
||||
/* Состояние pacer в микросекундах: отрицательное значение — накопленный кредит, положительное — долг. */
|
||||
static int64_t diagnostic_debt(const struct ETCP_LINK* link) {
|
||||
return ((int64_t)link->shaper_load_time_tb - (int64_t)get_time_tb()) * 100 + (int64_t)(link->shaper_sub_nanotime / 10000);
|
||||
}
|
||||
|
||||
static void diagnostic_observe(struct diagnostic_interval* d, struct ETCP_LINK* link) {
|
||||
uint32_t flight = link->inflight_bytes, cwnd = link->inflight_lim_bytes;
|
||||
int64_t debt = diagnostic_debt(link);
|
||||
if (!d->observations || flight < d->flight_min) d->flight_min = flight;
|
||||
if (!d->observations || cwnd < d->cwnd_min) d->cwnd_min = cwnd;
|
||||
if (!d->observations || debt < d->debt_min_us) d->debt_min_us = debt;
|
||||
if (!d->observations || debt > d->debt_max_us) d->debt_max_us = debt;
|
||||
if (flight > d->flight_max) d->flight_max = flight;
|
||||
if (cwnd > d->cwnd_max) d->cwnd_max = cwnd;
|
||||
if ((unsigned)link->etcp->input_send_q->count > d->send_max) d->send_max = link->etcp->input_send_q->count;
|
||||
if ((unsigned)link->ack_q->count > d->ack_max) d->ack_max = link->ack_q->count;
|
||||
d->observations++;
|
||||
}
|
||||
|
||||
static struct diagnostic_interval* diagnostic_for(struct ETCP_LINK* link) {
|
||||
if (!diagnostic_active) return NULL;
|
||||
for (unsigned p = 0; p < path_count; p++)
|
||||
if (ss_get_port(&link->remote_addr) == dummynet_get_listen_port(channels[p])) {
|
||||
struct diagnostic_interval* d = &diagnostic_stats[link->etcp->instance == ep[1].inst][p];
|
||||
diagnostic_observe(d, link); return d;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Общие очереди соединения повторяются в строках его линков; интервальные счётчики относятся к одному пути. */
|
||||
static void diagnostic_snapshot(const char* phase) {
|
||||
if (!diagnostic_active) return;
|
||||
uint64_t now = get_time_us();
|
||||
if (now == diagnostic_last_us) return;
|
||||
for (unsigned side = 0; side < 2; side++) for (struct ETCP_LINK* l = ep[side].conn->links; l; l = l->next) {
|
||||
struct diagnostic_interval* d = diagnostic_for(l);
|
||||
if (!d) continue;
|
||||
unsigned p = (unsigned)(d - diagnostic_stats[side]);
|
||||
CHECK(diagnostic_count < sizeof(diagnostic_rows) / sizeof(diagnostic_rows[0]));
|
||||
struct diagnostic_sample* s = &diagnostic_rows[diagnostic_count++];
|
||||
memset(s, 0, sizeof(*s));
|
||||
struct ETCP_CONN* c = l->etcp;
|
||||
struct PKTNORM* pn = c->normalizer;
|
||||
const struct ll_queue* queues[] = {c->send_input_q, c->input_queue, c->input_send_q, c->input_wait_ack, c->recv_q};
|
||||
s->phase = phase; s->time_us = now - started; s->interval_us = now - diagnostic_last_us; s->side = side; s->path = p;
|
||||
for (unsigned q = 0; q < 5; q++) { s->queue_count[q] = queues[q]->count; s->queue_bytes[q] = queues[q]->total_bytes; }
|
||||
for (struct ll_entry* e = c->input_send_q->head; e; e = e->next)
|
||||
if (((struct INFLIGHT_PACKET*)e)->last_attempt_id) s->retries++; else s->fresh++;
|
||||
s->normalizer_bytes = pn->data_ptr; s->normalizer_waiter = !!pn->input_waiter_handle.internal;
|
||||
s->waiter_deferred = !!pn->input_waiter_handle.call_soon_id;
|
||||
s->normalizer_flush = !!pn->flush_timer; s->pending_bytes = pn->pending ? pn->pending->len - pn->pending_in_ptr : 0;
|
||||
s->app_suspended = !!c->send_input_q->callback_suspended; s->app_resume = !!c->send_input_q->resume_timeout_id;
|
||||
s->fragment_suspended = !!c->input_queue->callback_suspended; s->fragment_resume = !!c->input_queue->resume_timeout_id;
|
||||
s->tx_state = c->tx_state; s->tx_scheduled = c->tx_token != NULL; s->tx_delayed = c->tx_delayed;
|
||||
s->tx_running = c->tx_running; s->active_attempts = c->active_attempts;
|
||||
s->next_seq = c->next_tx_id; s->peer_till = c->rx_ack_till; s->local_till = c->last_delivered_id;
|
||||
s->unacked = c->unacked_bytes; s->budget = c->optimal_inflight;
|
||||
s->flight = l->inflight_bytes; s->attempts = l->inflight_packets; s->cwnd = l->inflight_lim_bytes; s->pace = l->bbr_pacing_rate;
|
||||
s->mode = l->bbr->mode; s->cycle = l->bbr->cycle_idx; s->min_rtt = l->bbr->min_rtt_us;
|
||||
s->rtt = l->data_rtt_us; s->jitter = l->data_jitter_us; s->rto = etcp_tx_rto_us(l);
|
||||
s->bw = l->bbr->bw_hi[0] > l->bbr->bw_hi[1] ? l->bbr->bw_hi[0] : l->bbr->bw_hi[1];
|
||||
if (s->bw > l->bbr->bw_lo) s->bw = l->bbr->bw_lo;
|
||||
s->queue_cap = l->bbr->queue_cwnd_cap; s->ack_count = l->ack_q->count;
|
||||
s->ack_timer = !!c->ack_resp_timer; s->ack_till = !!c->ack_till_deadline_us; s->urgent_ack = l->ack_reply_pending;
|
||||
if (l->ack_q->head) s->ack_age_us = now - ((struct ACK_PACKET*)l->ack_q->head)->received_at_us;
|
||||
if (l->attempts->head && ((struct ETCP_TX_ATTEMPT*)l->attempts->head)->sent_at_us)
|
||||
s->attempt_age_us = now - ((struct ETCP_TX_ATTEMPT*)l->attempts->head)->sent_at_us;
|
||||
s->debt_us = diagnostic_debt(l); s->pacer_timer = !!l->shaper_timer; s->burst = l->burst_active;
|
||||
uint64_t tb = get_time_tb();
|
||||
s->pacer_ready_us = l->shaper_load_time_tb >= tb + SHAPER_BURST_DELAY_TB ?
|
||||
(l->shaper_load_time_tb - tb - SHAPER_BURST_DELAY_TB + 1) * 100 : 0;
|
||||
s->stalled = l->tx_stalled; s->rto_streak = l->data_rto_streak; s->app_bytes = ep[1-side].bytes;
|
||||
const struct dummynet_stats* ds = dummynet_get_stats(channels[p], side);
|
||||
struct dummynet_stats* previous = &diagnostic_previous[side][p];
|
||||
CHECK(ds->sent_bytes >= previous->sent_bytes && ds->recv_bytes >= previous->recv_bytes);
|
||||
s->wire_bytes = ds->sent_bytes - previous->sent_bytes; s->channel_received = ds->recv_bytes - previous->recv_bytes;
|
||||
s->channel_lost = ds->lost - previous->lost; s->channel_dropped = ds->dropped - previous->dropped;
|
||||
s->channel_pending = ds->queue_size; s->channel_peak = ds->queue_max; s->channel_delay_max_us = ds->max_queue_delay_us;
|
||||
*previous = *ds; s->stats = *d; memset(d, 0, sizeof(*d));
|
||||
}
|
||||
diagnostic_last_us = now;
|
||||
}
|
||||
|
||||
/* Вывод после замера: dt_us обязателен для скоростей, особенно в коротком последнем срезе фазы. */
|
||||
static void diagnostic_flush(void) {
|
||||
if (!diagnostic_file) return;
|
||||
diagnostic_flushing = 1;
|
||||
for (unsigned n = 0; n < diagnostic_count; n++) {
|
||||
const struct diagnostic_sample* s = &diagnostic_rows[n];
|
||||
const struct diagnostic_interval* d = &s->stats;
|
||||
CHECK(fprintf(diagnostic_file, "%s,%x,%s,%llu,%llu,%u,%u", case_name, enabled_mask, s->phase,
|
||||
(unsigned long long)s->time_us, (unsigned long long)s->interval_us, s->side, s->path) > 0);
|
||||
for (unsigned q = 0; q < 5; q++)
|
||||
CHECK(fprintf(diagnostic_file, ",%u,%llu", s->queue_count[q], (unsigned long long)s->queue_bytes[q]) > 0);
|
||||
CHECK(fprintf(diagnostic_file, ",%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u",
|
||||
s->fresh, s->retries, s->normalizer_bytes, s->pending_bytes, s->normalizer_waiter, s->normalizer_flush,
|
||||
s->unacked, s->budget, s->flight, s->attempts, s->cwnd, s->pace, s->mode, s->cycle, s->min_rtt,
|
||||
s->rtt, s->jitter, s->rto, s->bw, s->queue_cap, s->ack_count, s->ack_timer) > 0);
|
||||
CHECK(fprintf(diagnostic_file, ",%u,%u,%llu,%llu,%lld,%u,%llu,%u,%u,%u,%u,%u,%llu,%llu,%llu,%llu,%llu,%llu",
|
||||
s->ack_till, s->urgent_ack, (unsigned long long)s->ack_age_us, (unsigned long long)s->attempt_age_us,
|
||||
(long long)s->debt_us, s->pacer_timer, (unsigned long long)s->pacer_ready_us, s->burst, s->stalled, s->rto_streak,
|
||||
s->channel_pending, s->channel_peak, (unsigned long long)s->channel_delay_max_us,
|
||||
(unsigned long long)s->wire_bytes, (unsigned long long)s->channel_received, (unsigned long long)s->channel_lost,
|
||||
(unsigned long long)s->channel_dropped, (unsigned long long)s->app_bytes) > 0);
|
||||
CHECK(fprintf(diagnostic_file, ",%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu",
|
||||
(unsigned long long)d->tx_data, (unsigned long long)d->fresh_rx, (unsigned long long)d->data_packets,
|
||||
(unsigned long long)d->retries, (unsigned long long)d->ack_only_packets, (unsigned long long)d->ack_only_wire,
|
||||
(unsigned long long)d->piggy_packets, (unsigned long long)d->piggy_bytes, (unsigned long long)d->ack_entries,
|
||||
(unsigned long long)d->ack_delay_sum, (unsigned long long)d->ack_delay_max, (unsigned long long)d->early_ack_entries,
|
||||
(unsigned long long)d->control_packets, (unsigned long long)d->samples, (unsigned long long)d->limited,
|
||||
(unsigned long long)d->app_limited, (unsigned long long)d->rtt_min, (unsigned long long)d->rtt_max) > 0);
|
||||
CHECK(fprintf(diagnostic_file, ",%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%u,%u,%u,%u,%u,%u,%lld,%lld",
|
||||
(unsigned long long)d->ack_only_entries, (unsigned long long)d->ack_only_delay_sum,
|
||||
(unsigned long long)d->rtt_sum, (unsigned long long)d->rtt_samples,
|
||||
(unsigned long long)d->searches, (unsigned long long)d->window_blocked,
|
||||
(unsigned long long)d->pacer_blocked, (unsigned long long)d->observations, d->flight_min, d->flight_max,
|
||||
d->cwnd_min, d->cwnd_max, d->send_max, d->ack_max, (long long)d->debt_min_us, (long long)d->debt_max_us) > 0);
|
||||
CHECK(fprintf(diagnostic_file, ",%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%llu,%llu\n", s->app_suspended, s->app_resume,
|
||||
s->fragment_suspended, s->fragment_resume, s->waiter_deferred, s->tx_state, s->tx_scheduled, s->tx_delayed,
|
||||
s->tx_running, s->active_attempts, s->next_seq, s->peer_till, s->local_till,
|
||||
(unsigned long long)d->ordinary_ack_packets, (unsigned long long)d->early_ack_packets) > 0);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MPATH %s mask=%x %s t_ms=%llu dt_us=%llu side=%u path=%u "
|
||||
"Q_app=%u Q_fragment=%u Q_send=%u(new=%u retry=%u) Q_ACK=%u age_us=%llu flight=%u/%u "
|
||||
"pace_Bps=%u debt_us=%lld ready_us=%llu timer=%u window_blocked_checks=%llu pacer_blocked_checks=%llu "
|
||||
"channel_pending=%u mode=%u/%u DATA_B=%llu ACKonly=%llu early_ACK_entries=%llu early_ACK_packets=%llu",
|
||||
case_name, enabled_mask, s->phase, (unsigned long long)(s->time_us/1000), (unsigned long long)s->interval_us,
|
||||
s->side, s->path, s->queue_count[0], s->queue_count[1], s->queue_count[2], s->fresh, s->retries,
|
||||
s->ack_count, (unsigned long long)s->ack_age_us, s->flight, s->cwnd, s->pace, (long long)s->debt_us,
|
||||
(unsigned long long)s->pacer_ready_us, s->pacer_timer,
|
||||
(unsigned long long)d->window_blocked, (unsigned long long)d->pacer_blocked,
|
||||
s->channel_pending, s->mode, s->cycle, (unsigned long long)d->tx_data,
|
||||
(unsigned long long)d->ack_only_packets, (unsigned long long)d->early_ack_entries, (unsigned long long)d->early_ack_packets);
|
||||
}
|
||||
CHECK(fflush(diagnostic_file) == 0);
|
||||
diagnostic_count = 0; diagnostic_flushing = 0;
|
||||
}
|
||||
|
||||
/* Выборочная диагностика модели: один sample на линк/100мс, без packet-level спама всего ETCP. */
|
||||
void __real_bbr_main(struct bbr*, const struct bbr_rate_sample*, uint32_t*, uint32_t*, uint32_t, uint32_t, int);
|
||||
void __wrap_bbr_main(struct bbr* model, const struct bbr_rate_sample* sample, uint32_t* cwnd, uint32_t* pacing,
|
||||
uint32_t mss, uint32_t inflight, int limited) {
|
||||
__real_bbr_main(model, sample, cwnd, pacing, mss, inflight, limited);
|
||||
if (diagnostic_active) for (int side = 0; side < 2; side++)
|
||||
for (struct ETCP_LINK* l = ep[side].conn->links; l; l = l->next) {
|
||||
if (l->bbr != model) continue;
|
||||
struct diagnostic_interval* d = diagnostic_for(l);
|
||||
if (!d) continue;
|
||||
d->samples++; d->limited += !!limited; d->app_limited += !!sample->is_app_limited;
|
||||
if (sample->rtt_us) {
|
||||
if (!d->rtt_min || sample->rtt_us < d->rtt_min) d->rtt_min = sample->rtt_us;
|
||||
if (sample->rtt_us > d->rtt_max) d->rtt_max = sample->rtt_us;
|
||||
d->rtt_sum += sample->rtt_us; d->rtt_samples++;
|
||||
}
|
||||
}
|
||||
if (!log_bbr_samples) return;
|
||||
uint64_t now = get_time_us();
|
||||
for (int side = 0; side < 2; side++) for (struct ETCP_LINK* l = ep[side].conn ? ep[side].conn->links : NULL; l; l = l->next) {
|
||||
@@ -103,16 +294,55 @@ void __wrap_bbr_main(struct bbr* model, const struct bbr_rate_sample* sample, ui
|
||||
}
|
||||
}
|
||||
|
||||
/* Обычный ACK-only может нести timestamp; запрос till и секции измерительной серии относятся к control. */
|
||||
static int diagnostic_ack_only(const struct ETCP_DGRAM* pkt, unsigned ack_size) {
|
||||
if (pkt->payload_len || pkt->burst_packet) return 0;
|
||||
for (unsigned off = ack_size; off < pkt->data_len; off += 5)
|
||||
if (pkt->data_len - off < 5 || pkt->data[off] != ETCP_SECTION_TIMESTAMP) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Статистика TX снимается только после принятия send; RX — перед разбором уже расшифрованного пакета. */
|
||||
static void observe_packet(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 && !sending) diagnostic_for(link);
|
||||
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 >= ETCP_ACK_BASE_SIZE); len = etcp_ack_section_size(data[1]); break;
|
||||
case ETCP_SECTION_ACK: {
|
||||
CHECK(remaining >= ETCP_ACK_BASE_SIZE); len = etcp_ack_section_size(data[1]); CHECK(len <= remaining);
|
||||
struct diagnostic_interval* d = sending ? diagnostic_for(link) : NULL;
|
||||
if (d) {
|
||||
int ack_only = diagnostic_ack_only(pkt, len);
|
||||
if (pkt->payload_len && data[1]) { d->piggy_packets++; d->piggy_bytes += len; }
|
||||
else if (ack_only) {
|
||||
d->ack_only_packets++;
|
||||
d->ack_only_wire += pkt->data_len + ETCP_ENCRYPTED_HDR_SIZE + ETCP_SESSION_HEADER_SIZE +
|
||||
SC_NONCE_SIZE + SC_CRC32_SIZE + SC_TAG_SIZE;
|
||||
} else if (!pkt->payload_len) d->control_packets++;
|
||||
uint32_t oldest_delay = 0;
|
||||
for (unsigned n = 0; n < data[1]; n++) {
|
||||
const uint8_t* a = data + ETCP_ACK_BASE_SIZE + n * ETCP_ACK_ENTRY_SIZE;
|
||||
uint32_t delay = (uint32_t)a[6] | ((uint32_t)a[7] << 8) | ((uint32_t)a[8] << 16) | ((uint32_t)a[9] << 24);
|
||||
d->ack_entries++; d->ack_delay_sum += delay;
|
||||
if (delay > oldest_delay) oldest_delay = delay;
|
||||
if (delay > d->ack_delay_max) d->ack_delay_max = delay;
|
||||
if (ack_only) {
|
||||
d->ack_only_entries++; d->ack_only_delay_sum += delay;
|
||||
if (!link->ack_reply_pending && delay < 2000) d->early_ack_entries++;
|
||||
}
|
||||
}
|
||||
if (ack_only && data[1] && !link->ack_reply_pending) {
|
||||
d->ordinary_ack_packets++;
|
||||
int till_due = link->etcp->ack_till_deadline_us && link->etcp->ack_till_deadline_us <= get_time_us();
|
||||
if (!till_due && oldest_delay < ETCP_ACK_DELAY_US) d->early_ack_packets++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ETCP_SECTION_ACK_REQUEST: len = 1; break;
|
||||
case ETCP_SECTION_TIMESTAMP: len = 5; break;
|
||||
case ETCP_SECTION_MEAS_TS: len = 9; break;
|
||||
@@ -127,6 +357,11 @@ static void observe_packet(struct ETCP_DGRAM* pkt, struct ETCP_LINK* link, int s
|
||||
uint16_t number = (uint16_t)data[5] | ((uint16_t)data[6] << 8);
|
||||
if (!sending) rx_trigger[side] = seq;
|
||||
if (sending) {
|
||||
struct diagnostic_interval* d = diagnostic_for(link);
|
||||
if (d) {
|
||||
d->tx_data += remaining - ETCP_PAYLOAD_HEADER_SIZE; d->data_packets++;
|
||||
if (trace->attempts) d->retries++;
|
||||
}
|
||||
if (measuring && fault_start && !side && trace->retry_ready) {
|
||||
uint64_t wait = now - trace->retry_ready; recovery.retry_count++;
|
||||
if (wait > recovery.ready_wait_us) { recovery.ready_wait_us = wait; recovery.ready_seq = seq; }
|
||||
@@ -139,6 +374,7 @@ static void observe_packet(struct ETCP_DGRAM* pkt, struct ETCP_LINK* link, int s
|
||||
fault_rto_us[path] = etcp_tx_rto_us(link);
|
||||
if (measuring) traffic[side][path].data += remaining - ETCP_PAYLOAD_HEADER_SIZE;
|
||||
} else if (!trace->first_rx) {
|
||||
if (diagnostic_active) diagnostic_stats[1-side][path].fresh_rx += remaining - ETCP_PAYLOAD_HEADER_SIZE;
|
||||
trace->first_rx = now;
|
||||
if (measuring) traffic[1-side][path].rx_unique += remaining - ETCP_PAYLOAD_HEADER_SIZE;
|
||||
if (measuring && fault_start && side && trace->last_number == number && trace->last_path == path) {
|
||||
@@ -192,12 +428,23 @@ int __wrap_queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
return __real_queue_data_put_with_index(q, entry);
|
||||
int result = __real_queue_data_put_with_index(q, entry);
|
||||
if (!result && diagnostic_active) for (unsigned side = 0; side < 2; side++)
|
||||
for (struct ETCP_LINK* l = ep[side].conn->links; l; l = l->next)
|
||||
if (q == l->ack_q) diagnostic_for(l); // ACK-пик виден до немедленной отправки/извлечения из очереди.
|
||||
return result;
|
||||
}
|
||||
|
||||
struct ETCP_LINK* __real_etcp_loadbalancer_select_link(struct ETCP_CONN*, uint32_t, const struct INFLIGHT_PACKET*);
|
||||
struct ETCP_LINK* __wrap_etcp_loadbalancer_select_link(struct ETCP_CONN* conn, uint32_t bytes, const struct INFLIGHT_PACKET* retry) {
|
||||
struct ETCP_LINK* selected = __real_etcp_loadbalancer_select_link(conn, bytes, retry);
|
||||
if (bytes && diagnostic_active) for (struct ETCP_LINK* l = conn->links; l; l = l->next) {
|
||||
struct diagnostic_interval* d = diagnostic_for(l);
|
||||
if (!d) continue;
|
||||
d->searches++;
|
||||
d->window_blocked += bytes > l->inflight_lim_bytes || l->inflight_bytes > l->inflight_lim_bytes - bytes;
|
||||
d->pacer_blocked += l->shaper_timer != NULL;
|
||||
}
|
||||
if (!measuring || !fault_start || conn != ep[0].conn || !retry || !selected || !retry->last_link) return selected;
|
||||
int previous = -1, chosen = -1;
|
||||
for (unsigned p = 0; p < path_count; p++) {
|
||||
@@ -312,6 +559,10 @@ static void setup(const struct profile* settings, unsigned count, unsigned mask)
|
||||
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;
|
||||
memset(diagnostic_stats, 0, sizeof(diagnostic_stats)); memset(diagnostic_previous, 0, sizeof(diagnostic_previous));
|
||||
diagnostic_count = 0; diagnostic_last_us = started; diagnostic_active = diagnostic_file != NULL;
|
||||
if (diagnostic_active) for (unsigned side = 0; side < 2; side++) for (unsigned p = 0; p < path_count; p++)
|
||||
if (channels[p]) diagnostic_previous[side][p] = *dummynet_get_stats(channels[p], side);
|
||||
}
|
||||
|
||||
/* Ограниченная очередь источника: latency не скрывается за неограниченным накоплением тестовых сообщений. */
|
||||
@@ -331,6 +582,7 @@ static void produce(unsigned side) {
|
||||
}
|
||||
|
||||
static void snapshot(const char* phase) {
|
||||
diagnostic_snapshot(phase);
|
||||
uint64_t now = get_time_us();
|
||||
for (int side = 0; side < 2; side++) {
|
||||
struct ETCP_CONN* conn = ep[side].conn;
|
||||
@@ -361,10 +613,13 @@ static void run_for(unsigned ms, int sending, const char* phase) {
|
||||
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 (get_time_us() >= next_snapshot) {
|
||||
snapshot(phase); next_snapshot = get_time_us() + (diagnostic_file ? diagnostic_period_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;
|
||||
}
|
||||
diagnostic_snapshot(phase);
|
||||
}
|
||||
|
||||
static void begin_measure(void) {
|
||||
@@ -375,6 +630,7 @@ static void begin_measure(void) {
|
||||
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);
|
||||
memset(diagnostic_previous, 0, sizeof(diagnostic_previous));
|
||||
}
|
||||
|
||||
static void expect(int passed, const char* requirement, uint64_t actual, uint64_t limit) {
|
||||
@@ -396,6 +652,7 @@ static uint64_t percentile(const struct endpoint* endpoint, unsigned percent) {
|
||||
|
||||
static void teardown(void) {
|
||||
measuring = 0;
|
||||
diagnostic_active = 0; diagnostic_flush();
|
||||
// Уничтожаем и активные 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);
|
||||
@@ -570,6 +827,41 @@ int main(int argc, char** argv) {
|
||||
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"); }
|
||||
const char* detail = getenv("UTUN_MULTIPATH_DIAG");
|
||||
if (detail) {
|
||||
const char* period = getenv("UTUN_MULTIPATH_DIAG_MS");
|
||||
if (period) {
|
||||
char* end = NULL; errno = 0;
|
||||
unsigned long ms = strtoul(period, &end, 10);
|
||||
if (errno || end == period || *end || ms < 10 || ms > 1000) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "Invalid UTUN_MULTIPATH_DIAG_MS='%s': expected 10..1000", period); return 1;
|
||||
}
|
||||
diagnostic_period_us = ms * 1000;
|
||||
}
|
||||
diagnostic_file = fopen(detail, "w");
|
||||
if (!diagnostic_file) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "Cannot open multipath diagnostic file '%s': errno=%d", detail, errno); return 1;
|
||||
}
|
||||
CHECK(fprintf(diagnostic_file,
|
||||
"case,mask,phase,time_us,dt_us,side,path,app_count,app_bytes,fragment_count,fragment_bytes,send_count,send_bytes,"
|
||||
"wait_ack_count,wait_ack_bytes,recv_count,recv_bytes,send_fresh,send_retries,normalizer_bytes,pending_bytes,"
|
||||
"normalizer_waiter,normalizer_flush,unacked_bytes,admission_budget,flight_bytes,attempts,cwnd_bytes,pacing_Bps,"
|
||||
"bbr_mode,bbr_cycle,min_rtt_us,srtt_us,jitter_us,rto_us,bw_q16,queue_cap_bytes,ack_count,ack_timer,"
|
||||
"till_pending,urgent_reply,ack_oldest_us,attempt_oldest_us,pacer_debt_us,pacer_timer,pacer_ready_us,burst,stalled,"
|
||||
"rto_streak,channel_pending,channel_peak_since_reset,channel_delay_max_since_reset_us,wire_sent_bytes,"
|
||||
"channel_received_bytes,channel_lost,channel_dropped,app_delivered_bytes,data_tx_bytes,fresh_rx_bytes,"
|
||||
"data_packets,retries,ack_only_packets,ack_only_wire_bytes,ack_piggy_packets,ack_piggy_bytes,ack_entries,"
|
||||
"ack_delay_sum_us,ack_delay_max_us,early_ordinary_ack_entries,control_packets,bbr_samples,cwnd_limited_samples,"
|
||||
"app_limited_samples,rtt_min_us,rtt_max_us,ack_only_entries,ack_only_delay_sum_us,rtt_sum_us,rtt_samples,"
|
||||
"selection_checks,selection_window_blocked,selection_pacer_blocked,observed_states,flight_min,flight_max,"
|
||||
"cwnd_min,cwnd_max,send_peak_observed,ack_peak_observed,debt_min_us,debt_max_us,app_callback_suspended,"
|
||||
"app_resume_scheduled,fragment_callback_suspended,fragment_resume_scheduled,normalizer_waiter_deferred,"
|
||||
"tx_state,tx_scheduled,tx_delayed,tx_running,active_attempts,next_tx_seq,peer_ack_till,local_delivered_till,"
|
||||
"ordinary_ack_only_packets,early_ordinary_ack_packets\n") > 0);
|
||||
debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Multipath diagnostics: period_us=%u file=%s; snapshots buffered until drain",
|
||||
diagnostic_period_us, detail);
|
||||
}
|
||||
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}}
|
||||
@@ -608,6 +900,10 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
CHECK(selected && argc <= 2);
|
||||
if (csv) CHECK(fclose(csv) == 0);
|
||||
if (diagnostic_file) {
|
||||
FILE* completed = diagnostic_file; diagnostic_file = NULL;
|
||||
CHECK(fclose(completed) == 0);
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Multipath completed: failures=%u", failures);
|
||||
test_clock_finish(); socket_platform_cleanup(); return failures ? 1 : 0;
|
||||
}
|
||||
@@ -33,13 +33,21 @@ static uint32_t last_mss;
|
||||
static unsigned fail_rx_message_at, fail_tx_inflight, fail_normalizer_timer, fail_normalizer_waiter, fail_session_timer_at;
|
||||
static unsigned no_alloc, unexpected_alloc, fail_rng, session_sends, received, receive_action;
|
||||
static uint8_t received_values[8];
|
||||
static unsigned ack_clock_active, capture_ack_timer, fail_ack_timer;
|
||||
static uint64_t ack_now_us;
|
||||
static timeout_callback_t captured_ack_callback;
|
||||
static void* captured_ack_arg;
|
||||
|
||||
/* Фиксируем только TB на время синхронной проверки pacer; uasync при этих часах не опрашивается. */
|
||||
uint64_t __real_get_time_tb(void);
|
||||
uint64_t __wrap_get_time_tb(void) {
|
||||
if (ack_clock_active) return ack_now_us / 100;
|
||||
return pacer_clock_active ? pacer_now_tb : __real_get_time_tb();
|
||||
}
|
||||
|
||||
uint64_t __real_get_time_us(void);
|
||||
uint64_t __wrap_get_time_us(void) { return ack_clock_active ? ack_now_us : __real_get_time_us(); }
|
||||
|
||||
void* __real_uasync_call_soon(struct UASYNC*, void*, timeout_callback_t);
|
||||
void* __wrap_uasync_call_soon(struct UASYNC* loop, void* arg, timeout_callback_t callback) {
|
||||
if (fail_normalizer_timer && conn && arg == conn->normalizer) { fail_normalizer_timer--; return NULL; }
|
||||
@@ -52,6 +60,8 @@ void* __wrap_uasync_call_soon(struct UASYNC* loop, void* arg, timeout_callback_t
|
||||
|
||||
void* __real_uasync_set_timeout(struct UASYNC*, int, void*, timeout_callback_t, const char*);
|
||||
void* __wrap_uasync_set_timeout(struct UASYNC* loop, int delay, void* arg, timeout_callback_t callback, const char* name) {
|
||||
if (capture_ack_timer && !strcmp(name, "etcp_ack_resp")) { captured_ack_callback = callback; captured_ack_arg = arg; }
|
||||
if (fail_ack_timer && !strcmp(name, "etcp_ack_resp")) { fail_ack_timer--; return NULL; }
|
||||
if (capture_shaper && name && !strcmp(name, "etcp_shaper")) {
|
||||
shaper_arm_count++; captured_shaper_delay = delay;
|
||||
captured_shaper_callback = callback; captured_shaper_arg = arg;
|
||||
@@ -152,6 +162,8 @@ static void setup(void) {
|
||||
fail_rx_message_at = fail_tx_inflight = fail_normalizer_timer = fail_normalizer_waiter = fail_session_timer_at = 0;
|
||||
no_alloc = unexpected_alloc = fail_rng = session_sends = received = receive_action = 0;
|
||||
memset(received_values, 0, sizeof(received_values));
|
||||
ack_clock_active = capture_ack_timer = fail_ack_timer = 0;
|
||||
captured_ack_callback = NULL; captured_ack_arg = NULL;
|
||||
last_sent_link = NULL; last_sent_request = last_sent_payload = 0;
|
||||
ua = uasync_create(); CHECK(ua);
|
||||
inst.ua = ua; inst.node_id = 1; inst.config = &cfg; cfg.global.bbr_max_cwnd = INFLIGHT_LIM_MAX;
|
||||
@@ -175,6 +187,7 @@ static void setup(void) {
|
||||
}
|
||||
|
||||
static void teardown(void) {
|
||||
ack_clock_active = 0;
|
||||
check_accounting();
|
||||
etcp_connection_close(conn); conn = NULL;
|
||||
uasync_poll(ua, 0);
|
||||
@@ -204,6 +217,12 @@ static struct ETCP_DGRAM* request(void) {
|
||||
return pkt;
|
||||
}
|
||||
|
||||
/* Wire/window-тесты моделируют уже ожидающий ACK, не меняя часы и не запуская sender через uasync. */
|
||||
static void mature_acks(struct ETCP_LINK* link) {
|
||||
for (struct ll_entry* e = link->ack_q->head; e; e = e->next)
|
||||
((struct ACK_PACKET*)e)->received_at_us = get_time_us() - ETCP_ACK_DELAY_US;
|
||||
}
|
||||
|
||||
static void fill(struct ETCP_LINK* target, unsigned count, unsigned len) {
|
||||
int up_a = a->link_status, up_b = b->link_status;
|
||||
a->link_status = target == a; b->link_status = target == b;
|
||||
@@ -788,6 +807,7 @@ static void retry_waits_for_alternative(void) {
|
||||
CHECK(!sends && packet->attempt_count == 1 && !packet->active_attempts && !a->retrans_timer && !b->retrans_timer);
|
||||
CHECK(packet->state == INFLIGHT_STATE_WAIT_SEND && packet->last_link == a && ua->timer_alloc_count == allocated);
|
||||
CHECK(etcp_tx_queue_ack(a, 99, etcp_tx_attempt_id(99, 1)) == 0);
|
||||
mature_acks(a);
|
||||
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == a && !pkt->payload_len && !pkt->attempt);
|
||||
memory_pool_free(inst.pkt_pool, pkt);
|
||||
a->burst_active = 1; a->burst_count = 1;
|
||||
@@ -1124,6 +1144,162 @@ static void independent_attempt_acks(void) {
|
||||
check_accounting(); teardown();
|
||||
}
|
||||
|
||||
/* Посторонний wake не снимает задержку ACK; точные границы проверяются без опроса uasync при ручных часах. */
|
||||
static void ack_absolute_deadline(void) {
|
||||
setup(); conn->got_initial_pkt = 1;
|
||||
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
|
||||
receive_fragment(a, 2, 1); CHECK(a->ack_q->count == 1 && conn->ack_resp_timer);
|
||||
etcp_conn_wake_sender(conn);
|
||||
struct ETCP_DGRAM* pkt = request();
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "ACK deadline: age_us=0 queued=%d sent_early=%u", a->ack_q->count, pkt != NULL);
|
||||
CHECK(!pkt && a->ack_q->count == 1);
|
||||
ack_now_us += 1999; CHECK(!request() && a->ack_q->count == 1);
|
||||
ack_now_us++; pkt = request(); CHECK(pkt && pkt->link == a && pkt->data[1] == 1);
|
||||
memory_pool_free(inst.pkt_pool, pkt); CHECK(!a->ack_q->count && !conn->ack_resp_timer);
|
||||
teardown();
|
||||
}
|
||||
|
||||
static void ack_piggyback_deadlines(void) {
|
||||
setup(); conn->got_initial_pkt = 1;
|
||||
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
|
||||
uint64_t begin = ack_now_us;
|
||||
receive_fragment(a, 2, 1); ack_now_us += 1000; receive_fragment(b, 3, 1);
|
||||
CHECK(conn->ack_resp_deadline_us == begin + ETCP_ACK_DELAY_US);
|
||||
enqueue(100);
|
||||
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == a && pkt->payload_len && pkt->data[1] == 1);
|
||||
memory_pool_free(inst.pkt_pool, pkt); CHECK(!a->ack_q->count && b->ack_q->count == 1);
|
||||
CHECK(conn->ack_resp_timer && conn->ack_resp_deadline_us == begin + 3000);
|
||||
ack_now_us = begin + 2000; CHECK(!request());
|
||||
ack_now_us = begin + 2999; CHECK(!request());
|
||||
ack_now_us++; pkt = request(); CHECK(pkt && pkt->link == b && !pkt->payload_len && pkt->data[1] == 1);
|
||||
memory_pool_free(inst.pkt_pool, pkt); CHECK(!conn->ack_resp_timer && !conn->ack_resp_deadline_us);
|
||||
ack_all(); teardown();
|
||||
}
|
||||
|
||||
/* Частичный piggyback меняет голову: прежний deadline больше не делает молодой ACK готовым. */
|
||||
static void ack_partial_piggyback(void) {
|
||||
setup(); conn->got_initial_pkt = 1; b->link_status = 0;
|
||||
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
|
||||
uint64_t begin = ack_now_us;
|
||||
receive_fragment(a, 2, 1); ack_now_us += 500; receive_fragment(a, 3, 1);
|
||||
unsigned max_body = a->mtu - WIRE_OVERHEAD(AF_INET);
|
||||
enqueue(max_body - ETCP_ACK_BASE_SIZE - ETCP_PAYLOAD_HEADER_SIZE - ETCP_ACK_ENTRY_SIZE - ETCP_ACK_RX_POINT_SIZE);
|
||||
ack_now_us = begin + 1999;
|
||||
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->payload_len && pkt->data[1] == 1 && a->ack_q->count == 1);
|
||||
memory_pool_free(inst.pkt_pool, pkt); CHECK(conn->ack_resp_deadline_us == begin + 2500);
|
||||
timeout_callback_t callback = captured_ack_callback; void* arg = captured_ack_arg;
|
||||
ack_now_us = begin + 2000; uasync_cancel_timeout(ua, conn->ack_resp_timer); callback(arg);
|
||||
CHECK(conn->ack_resp_timer && conn->ack_resp_deadline_us == begin + 2500 && !request());
|
||||
ack_now_us = begin + 2100; receive_fragment(a, 4, 1);
|
||||
CHECK(conn->ack_resp_deadline_us == begin + 2500);
|
||||
ack_now_us = begin + 2500; pkt = request(); CHECK(pkt && !pkt->payload_len && pkt->data[1] == 2);
|
||||
memory_pool_free(inst.pkt_pool, pkt); CHECK(!a->ack_q->count && !conn->ack_resp_timer);
|
||||
ack_all(); teardown();
|
||||
}
|
||||
|
||||
/* Новый RX между timer и отложенным sender не запускает для старой головы очередные 2мс. */
|
||||
static void ack_timer_interleaving(void) {
|
||||
setup(); conn->got_initial_pkt = 1;
|
||||
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
|
||||
uint64_t begin = ack_now_us;
|
||||
receive_fragment(a, 2, 1);
|
||||
timeout_callback_t callback = captured_ack_callback; void* arg = captured_ack_arg;
|
||||
ack_now_us += ETCP_ACK_DELAY_US;
|
||||
uasync_cancel_timeout(ua, conn->ack_resp_timer); callback(arg);
|
||||
CHECK(!conn->ack_resp_timer && conn->tx_token);
|
||||
receive_fragment(a, 3, 1); CHECK(!conn->ack_resp_timer && conn->tx_token);
|
||||
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->data[1] == 2 && !pkt->payload_len);
|
||||
const uint8_t* entry = pkt->data + ETCP_ACK_BASE_SIZE;
|
||||
uint32_t delay = (uint32_t)entry[6] | ((uint32_t)entry[7] << 8) | ((uint32_t)entry[8] << 16) | ((uint32_t)entry[9] << 24);
|
||||
CHECK(delay == ETCP_ACK_DELAY_US && ack_now_us == begin + ETCP_ACK_DELAY_US);
|
||||
memory_pool_free(inst.pkt_pool, pkt); teardown();
|
||||
}
|
||||
|
||||
static void ack_urgent_and_oom(void) {
|
||||
for (unsigned mode = 0; mode < 2; mode++) {
|
||||
setup(); conn->got_initial_pkt = 1;
|
||||
ack_now_us = get_time_us(); ack_clock_active = 1;
|
||||
fail_ack_timer = mode;
|
||||
receive_fragment(a, 2, 1);
|
||||
if (!mode) receive_till(a, 0, 1);
|
||||
CHECK(a->ack_reply_pending && !fail_ack_timer);
|
||||
CHECK(((struct ACK_PACKET*)a->ack_q->head)->received_at_us == ack_now_us);
|
||||
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->data[1] == 1 && !pkt->payload_len);
|
||||
memory_pool_free(inst.pkt_pool, pkt);
|
||||
CHECK(!a->ack_reply_pending && !conn->ack_resp_timer && !conn->ack_resp_deadline_us);
|
||||
teardown();
|
||||
}
|
||||
}
|
||||
|
||||
static void ack_partial_frame_and_lifecycle(void) {
|
||||
setup(); ack_now_us = get_time_us(); ack_clock_active = 1;
|
||||
for (unsigned n = 0; n < 200; n++) CHECK(etcp_tx_queue_ack(a, 100+n, etcp_tx_attempt_id(100+n, 1)) == 0);
|
||||
ack_now_us += ETCP_ACK_DELAY_US;
|
||||
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->data[1] && a->ack_q->count);
|
||||
memory_pool_free(inst.pkt_pool, pkt); CHECK(!conn->ack_resp_timer);
|
||||
pkt = request(); CHECK(pkt && pkt->data[1] && !a->ack_q->count);
|
||||
memory_pool_free(inst.pkt_pool, pkt); teardown();
|
||||
|
||||
setup(); ack_now_us = get_time_us(); ack_clock_active = 1;
|
||||
conn->ack_till_deadline_us = ack_now_us + ETCP_ACK_DELAY_US; conn->last_delivered_id = 17;
|
||||
CHECK(!request() && conn->ack_resp_timer);
|
||||
ack_now_us += ETCP_ACK_DELAY_US;
|
||||
pkt = request(); CHECK(pkt && !pkt->data[1] && pkt->data[2] == 17);
|
||||
memory_pool_free(inst.pkt_pool, pkt); CHECK(!conn->ack_till_deadline_us && !conn->ack_resp_timer);
|
||||
CHECK(etcp_tx_queue_ack(a, 99, etcp_tx_attempt_id(99, 1)) == 0); CHECK(!request() && conn->ack_resp_timer);
|
||||
etcp_conn_reset(conn); CHECK(!conn->ack_resp_timer && !conn->ack_resp_deadline_us && !conn->ack_till_deadline_us);
|
||||
teardown();
|
||||
}
|
||||
|
||||
static void ack_sender_failure(void) {
|
||||
setup(); conn->got_initial_pkt = 1; CHECK(etcp_conn_ref_take(conn) == 0);
|
||||
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
|
||||
receive_fragment(a, 2, 1);
|
||||
timeout_callback_t callback = captured_ack_callback; void* arg = captured_ack_arg;
|
||||
ack_now_us += ETCP_ACK_DELAY_US; fail_tx_timer = 1;
|
||||
uasync_cancel_timeout(ua, conn->ack_resp_timer); callback(arg);
|
||||
CHECK(!fail_tx_timer && conn->close_requested && !conn->ack_resp_timer);
|
||||
ack_clock_active = 0; uasync_drain_immediate(ua);
|
||||
CHECK(conn->state == 2 && !conn->links && !conn->ack_resp_deadline_us && !conn->ack_till_deadline_us);
|
||||
etcp_conn_ref_free(conn); teardown();
|
||||
}
|
||||
|
||||
/* DOWN не мешает срокам других путей; UP возвращает старые ACK сразу. Удаление головы не оставляет чужой deadline. */
|
||||
static void ack_link_lifecycle(void) {
|
||||
for (unsigned remove = 0; remove < 2; remove++) {
|
||||
setup(); conn->got_initial_pkt = 1;
|
||||
ack_now_us = get_time_us(); ack_clock_active = 1;
|
||||
uint64_t begin = ack_now_us;
|
||||
receive_fragment(a, 2, 1); ack_now_us += 1000; receive_fragment(b, 3, 1);
|
||||
if (remove) { etcp_link_close(a); a = NULL; }
|
||||
else a->link_status = 0;
|
||||
ack_now_us = begin + 2000; CHECK(!request() && conn->ack_resp_deadline_us == begin + 3000);
|
||||
ack_now_us = begin + 3000;
|
||||
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == b && pkt->data[1] == 1);
|
||||
memory_pool_free(inst.pkt_pool, pkt); CHECK(!conn->ack_resp_timer);
|
||||
if (!remove) {
|
||||
a->link_status = 1; etcp_conn_wake_sender(conn);
|
||||
pkt = request(); CHECK(pkt && pkt->link == a && pkt->data[1] == 1);
|
||||
memory_pool_free(inst.pkt_pool, pkt);
|
||||
}
|
||||
teardown();
|
||||
}
|
||||
}
|
||||
|
||||
/* Готовый DATA другого пути не должен обходить уже наступивший срок ACK. */
|
||||
static void ack_due_other_link(void) {
|
||||
setup(); conn->got_initial_pkt = 1;
|
||||
ack_now_us = get_time_us(); ack_clock_active = 1;
|
||||
receive_fragment(b, 2, 1); b->inflight_lim_bytes = 1;
|
||||
ack_now_us += ETCP_ACK_DELAY_US;
|
||||
struct INFLIGHT_PACKET* fresh = enqueue(100);
|
||||
struct ETCP_DGRAM* pkt = request();
|
||||
CHECK(pkt && pkt->link == b && !pkt->payload_len && pkt->data[1] == 1 && fresh->state == INFLIGHT_STATE_WAIT_SEND);
|
||||
memory_pool_free(inst.pkt_pool, pkt);
|
||||
pkt = request(); CHECK(pkt && pkt->link == a && pkt->payload_len == 100);
|
||||
memory_pool_free(inst.pkt_pool, pkt); ack_all(); teardown();
|
||||
}
|
||||
|
||||
static void receive_admission_and_path_acks(void) {
|
||||
setup(); conn->got_initial_pkt = 1;
|
||||
fail_rx_data = 1; receive_fragment(a, 2, 1);
|
||||
@@ -1250,6 +1426,7 @@ static void receiver_point_wire(void) {
|
||||
CHECK(a->rx_data_bytes == 2 && a->ack_q->count == 1 && conn->recv_q->count == 1);
|
||||
a->rx_data_bytes = UINT64_C(0xfedcba9876543210); a->rx_data_time_us = UINT64_C(0x0011223344556677);
|
||||
const uint8_t expected[] = {0x10,0x32,0x54,0x76,0x98,0xba,0xdc,0xfe,0x77,0x66,0x55,0x44,0x33,0x22,0x11,0x00};
|
||||
mature_acks(a);
|
||||
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->data[1] == 1 && !pkt->payload_len);
|
||||
CHECK(pkt->data_len == etcp_ack_section_size(1) && !memcmp(pkt->data+ETCP_ACK_BASE_SIZE+ETCP_ACK_ENTRY_SIZE,expected,16));
|
||||
memory_pool_free(inst.pkt_pool,pkt);
|
||||
@@ -1266,12 +1443,14 @@ static void receiver_point_wire(void) {
|
||||
else CHECK(!pkt->data[1] && a->ack_q->count == 1);
|
||||
memory_pool_free(inst.pkt_pool,pkt);
|
||||
if (a->ack_q->count) {
|
||||
mature_acks(a);
|
||||
pkt = request(); CHECK(pkt && pkt->data[1] == 1 && !pkt->payload_len);
|
||||
memory_pool_free(inst.pkt_pool,pkt);
|
||||
}
|
||||
ack(seq);
|
||||
}
|
||||
for (unsigned i = 100; i < 300; i++) receive_fragment(a,i,1000+i);
|
||||
mature_acks(a);
|
||||
unsigned count = (max_body-ETCP_ACK_BASE_SIZE-ETCP_ACK_RX_POINT_SIZE)/ETCP_ACK_ENTRY_SIZE;
|
||||
pkt = request(); CHECK(pkt && pkt->data[1] == count && pkt->data_len <= max_body && a->ack_q->count == 200-(int)count);
|
||||
memory_pool_free(inst.pkt_pool,pkt);
|
||||
@@ -1472,6 +1651,7 @@ static void compact_attempt_identity(void) {
|
||||
conn->got_initial_pkt = 1;
|
||||
receive_fragment(a,2,1); receive_fragment(a,3,1); receive_fragment(a,2,1);
|
||||
CHECK(a->ack_q->count == 2 && a->rx_data_bytes == 3);
|
||||
mature_acks(a);
|
||||
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && !pkt->payload_len && pkt->data[1] == 2);
|
||||
const uint8_t entry1[] = {2,0,0,0,1,0}, entry2[] = {3,0,0,0,1,0};
|
||||
CHECK(!memcmp(pkt->data+ETCP_ACK_BASE_SIZE,entry1,sizeof(entry1)));
|
||||
@@ -1545,6 +1725,8 @@ int main(void) {
|
||||
retry_waits_for_alternative, retry_alternative_lifecycle,
|
||||
send_queue_scan, send_queue_input_admission,
|
||||
retry_full_round_history, retry_history_slot_reuse, fast_stall_progress, stalled_retry_selection, input_admission_callbacks,
|
||||
ack_absolute_deadline, ack_piggyback_deadlines, ack_partial_piggyback, ack_timer_interleaving,
|
||||
ack_urgent_and_oom, ack_partial_frame_and_lifecycle, ack_sender_failure, ack_link_lifecycle, ack_due_other_link,
|
||||
receive_admission_and_path_acks, peer_window_queries, pacing_continuity, pacing_data_units, attempt_memory_limits,
|
||||
normalizer_rx_oom, normalizer_tx_oom, normalizer_callback_abort, reset_without_allocation, restart_allocation_failures,
|
||||
receiver_bandwidth_bounds, receiver_point_lifecycle, receiver_point_wire, receiver_point_truncated,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "utun_instance.h"
|
||||
#include "routing_layer/topo_group.h"
|
||||
#include "routing_layer/topo_node_sqlite.h"
|
||||
#include "routing_layer/route_lib.h"
|
||||
#include "transport_layer/secure_channel.h"
|
||||
#include "transport_layer/etcp.h"
|
||||
#include "config_parser.h"
|
||||
@@ -63,6 +64,82 @@ static void test_zero_port_schema(void) {
|
||||
sqlite3_close(db);
|
||||
}
|
||||
|
||||
/* Прерывает VM SQLite для проверки незавершённого чтения списка. */
|
||||
static int interrupt_sql(void* arg) { (void)arg; return 1; }
|
||||
|
||||
/* Отказ SQL не должен выглядеть успешным сохранением или чтением узла. */
|
||||
static void test_verified_write(void) {
|
||||
sqlite3* db = NULL; uint8_t key[32] = {1};
|
||||
assert(sqlite3_open(":memory:", &db) == SQLITE_OK && !topo_node_sqlite_init(db));
|
||||
assert(!topo_node_sqlite_node_update_verified(db, 7, "before", key, key, 100, 1));
|
||||
assert(sqlite3_exec(db, "CREATE TRIGGER reject_node BEFORE INSERT ON nodes "
|
||||
"BEGIN SELECT RAISE(ABORT,'test insert rejection'); END", NULL, NULL, NULL) == SQLITE_OK);
|
||||
assert(topo_node_sqlite_node_update_verified(db, 8, "rejected", key, key, 101, 1) < 0);
|
||||
assert(sqlite3_exec(db, "DROP TRIGGER reject_node; CREATE TRIGGER reject_node BEFORE UPDATE ON nodes "
|
||||
"BEGIN SELECT RAISE(ABORT,'test update rejection'); END", NULL, NULL, NULL) == SQLITE_OK);
|
||||
assert(topo_node_sqlite_node_update_verified(db, 7, "rejected", key, key, 101, 1) < 0);
|
||||
assert(topo_node_sqlite_node_set_online(db, 7, 0) < 0 && topo_node_sqlite_node_get_online(db, 7) == 1);
|
||||
sqlite3_stmt* st = NULL;
|
||||
assert(sqlite3_prepare_v2(db, "SELECT name,update_ts FROM nodes ORDER BY node_id", -1, &st, NULL) == SQLITE_OK);
|
||||
assert(sqlite3_step(st) == SQLITE_ROW && !strcmp((const char*)sqlite3_column_text(st, 0), "before"));
|
||||
assert(sqlite3_column_int64(st, 1) == 100 && sqlite3_step(st) == SQLITE_DONE); sqlite3_finalize(st);
|
||||
assert(sqlite3_exec(db, "DROP TRIGGER reject_node", NULL, NULL, NULL) == SQLITE_OK);
|
||||
assert(!topo_node_sqlite_node_update_verified(db, 7, "after", key, key, 101, 1));
|
||||
assert(sqlite3_prepare_v2(db, "SELECT name,update_ts FROM nodes WHERE node_id=7", -1, &st, NULL) == SQLITE_OK);
|
||||
assert(sqlite3_step(st) == SQLITE_ROW && !strcmp((const char*)sqlite3_column_text(st, 0), "after"));
|
||||
assert(sqlite3_column_int64(st, 1) == 101); sqlite3_finalize(st);
|
||||
uint8_t signature[64] = {0};
|
||||
assert(!topo_node_sqlite_channel_put(db, "123", "test", 7, key, NULL, key, NULL, signature));
|
||||
assert(!topo_node_sqlite_member_placeholder_put(db, "123", 7, key, key));
|
||||
assert(!topo_node_sqlite_channel_put(db, "124", "test", 7, key, NULL, key, NULL, signature));
|
||||
assert(!topo_node_sqlite_member_placeholder_put(db, "124", 7, key, key));
|
||||
assert(!topo_node_sqlite_set_connected(db, "123", 7, 1));
|
||||
uint64_t* ids = NULL; int count = 0;
|
||||
assert(!topo_node_sqlite_get_connected_peers(db, "123", &ids, &count) && count == 1 && ids[0] == 7); u_free(ids);
|
||||
assert(!topo_node_sqlite_get_member_channels(db, 7, &ids, &count) && count == 2);
|
||||
assert((ids[0] == 123 && ids[1] == 124) || (ids[0] == 124 && ids[1] == 123)); u_free(ids);
|
||||
for (int query = 0; query < 5; query++) {
|
||||
ids = (void*)(uintptr_t)1; count = 99;
|
||||
sqlite3_progress_handler(db, 1, interrupt_sql, NULL);
|
||||
int rc = query == 0 ? topo_node_sqlite_get_connected_peers(db, "123", &ids, &count) :
|
||||
query == 1 ? topo_node_sqlite_get_supernode_peers(db, "123", &ids, &count) :
|
||||
query == 2 ? topo_node_sqlite_get_public_peers(db, "123", &ids, &count, 0) :
|
||||
query == 3 ? topo_node_sqlite_get_local_peers(db, "123", &ids, &count, 0) :
|
||||
topo_node_sqlite_get_member_channels(db, 7, &ids, &count);
|
||||
sqlite3_progress_handler(db, 0, NULL, NULL);
|
||||
assert(rc < 0 && !ids && !count && !sqlite3_next_stmt(db, NULL));
|
||||
}
|
||||
assert(sqlite3_exec(db, "UPDATE peers_123 SET node_type=2; UPDATE peers_124 SET node_type=4;"
|
||||
"CREATE TRIGGER reject_classification BEFORE UPDATE ON peers_124 "
|
||||
"BEGIN SELECT RAISE(ABORT,'test classification rejection'); END", NULL, NULL, NULL) == SQLITE_OK);
|
||||
assert(topo_node_sqlite_nodeinfo_updated(db, 7) < 0);
|
||||
assert(topo_node_sqlite_get_node_type(db, "123", 7) == 2 && topo_node_sqlite_get_node_type(db, "124", 7) == 4);
|
||||
assert(sqlite3_exec(db, "DROP TRIGGER reject_classification; CREATE TRIGGER reject_channel BEFORE DELETE ON channels "
|
||||
"BEGIN SELECT RAISE(ABORT,'test channel deletion rejection'); END", NULL, NULL, NULL) == SQLITE_OK);
|
||||
assert(topo_node_sqlite_channel_delete(db, "124") < 0 && topo_node_sqlite_member_in_channel(db, "124", 7));
|
||||
assert(!topo_node_sqlite_channel_get(db, "124", NULL, 0, NULL, NULL, NULL, NULL));
|
||||
assert(sqlite3_exec(db, "DROP TRIGGER reject_channel", NULL, NULL, NULL) == SQLITE_OK);
|
||||
assert(!topo_node_sqlite_nodeinfo_updated(db, 7) && topo_node_sqlite_get_node_type(db, "123", 7) == 0);
|
||||
assert(!topo_node_sqlite_channel_delete(db, "124"));
|
||||
assert(topo_node_sqlite_channel_get(db, "124", NULL, 0, NULL, NULL, NULL, NULL) < 0);
|
||||
assert(sqlite3_exec(db, "CREATE TRIGGER reject_member BEFORE DELETE ON peers_123 "
|
||||
"BEGIN SELECT RAISE(ABORT,'test member deletion rejection'); END", NULL, NULL, NULL) == SQLITE_OK);
|
||||
assert(topo_node_sqlite_member_del(db, "123", 7) < 0 && topo_node_sqlite_member_in_channel(db, "123", 7));
|
||||
assert(sqlite3_exec(db, "DROP TRIGGER reject_member; UPDATE peers_123 SET join_sig=X'01'; "
|
||||
"UPDATE channels SET x25519_pubkey=X'01'", NULL, NULL, NULL) == SQLITE_OK);
|
||||
uint64_t timestamp = 77; memset(signature, 0xa5, sizeof(signature));
|
||||
assert(topo_node_sqlite_member_get_join(db, "123", 7, signature, ×tamp) < 0);
|
||||
assert(timestamp == 77 && signature[0] == 0xa5);
|
||||
memset(key, 0xa5, sizeof(key));
|
||||
assert(topo_node_sqlite_channel_get(db, "123", NULL, 0, NULL, key, NULL, NULL) < 0 && key[0] == 0xa5);
|
||||
assert(!topo_node_sqlite_member_del(db, "123", 7) && !topo_node_sqlite_member_in_channel(db, "123", 7));
|
||||
assert(!topo_node_sqlite_node_set_online(db, 7, 0) && !topo_node_sqlite_node_get_online(db, 7));
|
||||
assert(sqlite3_exec(db, "DROP TABLE nodes", NULL, NULL, NULL) == SQLITE_OK);
|
||||
assert(topo_node_sqlite_node_update_verified(db, 7, "no table", key, key, 102, 1) < 0);
|
||||
sqlite3_close(db);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] verified node writes: insert/update, rejected writes, prepare failure and stored state");
|
||||
}
|
||||
|
||||
/* Новый wire и SQL сохраняют адреса, протоколы, классификацию и REALITY; смена формата оставляет bootstrap. */
|
||||
static void test_compact_addresses(struct UTUN_INSTANCE *inst) {
|
||||
test_zero_port_schema();
|
||||
@@ -142,11 +219,47 @@ static void test_compact_addresses(struct UTUN_INSTANCE *inst) {
|
||||
sqlite3_close(db); inst->topo_sqlite_db = saved_db;
|
||||
}
|
||||
|
||||
/* Каждый размер буфера: длина, NUL, защитные байты, сохранённый префикс и маркер усечения. */
|
||||
static void test_node_format(struct TOPO_GROUP* group) {
|
||||
char full[8192];
|
||||
int full_len = topo_node_format_all(group, full, sizeof(full));
|
||||
assert(full_len > 0 && (size_t)full_len == strlen(full) && !strstr(full, "[TRUNCATED]"));
|
||||
assert(strstr(full, "=== BGP Nodes dump:"));
|
||||
if (group->nodes->head) assert(strstr(full, " v4addr[0]:") && strstr(full, " path[0]:"));
|
||||
const char marker[] = "\n[TRUNCATED]\n";
|
||||
debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_ERROR);
|
||||
for (size_t capacity = 0; capacity <= (size_t)full_len + 2; capacity++) {
|
||||
uint8_t* storage = u_malloc(capacity + 2);
|
||||
assert(storage); memset(storage, 0xa5, capacity + 2);
|
||||
char* buf = (char*)storage + 1;
|
||||
int length = topo_node_format_all(group, buf, capacity);
|
||||
assert(storage[0] == 0xa5 && storage[capacity + 1] == 0xa5);
|
||||
if (!capacity) { assert(!length); u_free(storage); continue; }
|
||||
size_t expected = (size_t)full_len < capacity ? (size_t)full_len : capacity - 1;
|
||||
assert(length >= 0 && (size_t)length == expected);
|
||||
assert(memchr(buf, '\0', capacity) == buf + length);
|
||||
if (capacity > (size_t)full_len) assert(!memcmp(buf, full, full_len + 1));
|
||||
else {
|
||||
size_t marker_len = expected < sizeof(marker) - 1 ? expected : sizeof(marker) - 1;
|
||||
assert(!memcmp(buf, full, expected - marker_len));
|
||||
assert(!memcmp(buf + expected - marker_len, marker, marker_len));
|
||||
}
|
||||
u_free(storage);
|
||||
}
|
||||
char empty[] = "old";
|
||||
assert(!topo_node_format_all(NULL, empty, sizeof(empty)) && !empty[0]);
|
||||
assert(!topo_node_format_all(group, NULL, sizeof(empty)));
|
||||
debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] node dump: nodes=%d capacities=0..%d, lengths, guards, NUL and truncation",
|
||||
queue_entry_count(group->nodes), full_len + 2);
|
||||
}
|
||||
|
||||
static void test_bgp(struct UTUN_INSTANCE* signer) {
|
||||
struct UTUN_INSTANCE receiver = { .ua = signer->ua, .topo_groups = signer->topo_groups };
|
||||
struct TOPO_GROUP group = { .instance = &receiver, .group_id = 10, .group_type = TOPO_GROUP_TYPE_CHAT };
|
||||
struct ETCP_CONN from = { .instance = &receiver, .peer_node_id = 7 };
|
||||
group.nodes = queue_new(receiver.ua, 16, 0, 8, "snapshot_bgp");
|
||||
test_node_format(&group);
|
||||
struct TOPO_NODE* ni = record(signer, INT64_MAX - 7, "BGP", 1);
|
||||
from.peer_node_id = ni->node_id; from.links_up = 1;
|
||||
struct TOPO_GROUP_NODE nq = {0};
|
||||
@@ -158,7 +271,13 @@ static void test_bgp(struct UTUN_INSTANCE* signer) {
|
||||
assert(peer && peer->last_timestamp == ni->timestamp && queue_entry_count(peer->paths) == 1);
|
||||
assert(topo_group_process_nodeinfo(&group, &from, packet, (size_t)len + sizeof(struct TOPOMSG_HEADER)) == 0);
|
||||
assert(queue_entry_count(peer->paths) == 1);
|
||||
test_node_format(&group);
|
||||
struct TOPOMSG_NODEINFO_PKT* wire = (struct TOPOMSG_NODEINFO_PKT*)packet;
|
||||
wire->node.local_v4_subnets = 1; /* malformed CHAT не меняет уже принятую запись и маршрут */
|
||||
assert(topo_group_process_nodeinfo(&group, &from, packet, (size_t)len + sizeof(struct TOPOMSG_HEADER)) < 0);
|
||||
assert(topo_node_find_by_id(&group, ni->node_id) == peer && queue_entry_count(peer->paths) == 1);
|
||||
assert(peer->last_timestamp == ni->timestamp && topo_node_registry_find(signer->topo_groups, ni->node_id)->timestamp == ni->timestamp);
|
||||
wire->node.local_v4_subnets = 0;
|
||||
wire->node.timestamp++; /* подделка не удаляет узел и ранее принятый маршрут */
|
||||
assert(topo_group_process_nodeinfo(&group, &from, packet, (size_t)len + sizeof(struct TOPOMSG_HEADER)) < 0);
|
||||
assert(topo_node_find_by_id(&group, ni->node_id) == peer && queue_entry_count(peer->paths) == 1);
|
||||
@@ -174,6 +293,44 @@ static void test_bgp(struct UTUN_INSTANCE* signer) {
|
||||
queue_free(group.nodes);
|
||||
}
|
||||
|
||||
/* Замена подсети при том же количестве и откат ошибок сохраняют согласованные local_node/route table. */
|
||||
static void test_local_group_update(struct UTUN_INSTANCE* inst) {
|
||||
sqlite3* saved_db = inst->topo_sqlite_db;
|
||||
assert(sqlite3_open(":memory:", &inst->topo_sqlite_db) == SQLITE_OK && !topo_node_sqlite_init(inst->topo_sqlite_db));
|
||||
struct CFG_ROUTE_ENTRY subnet = {.ip.family = AF_INET, .netmask = 24};
|
||||
subnet.ip.addr.v4.s_addr = htonl(0x0a010000);
|
||||
struct utun_config config = {.my_subnets = &subnet};
|
||||
inst->config = &config; inst->rt = route_table_create(inst->node_id); assert(inst->rt);
|
||||
struct TOPO_GROUP group = {.instance = inst, .group_type = TOPO_GROUP_TYPE_UTUN, .group_id = TOPO_GROUP_UTUN};
|
||||
memcpy(group.ed25519_public_key, inst->my_ed25519_pubkey, 32);
|
||||
assert(topo_group_update_my_nodeinfo(inst, &group) == 1 && route_lookup(inst->rt, 0x0a010001));
|
||||
subnet.ip.addr.v4.s_addr = htonl(0x0a020000);
|
||||
assert(topo_group_update_my_nodeinfo(inst, &group) == 1 && route_lookup(inst->rt, 0x0a020001));
|
||||
assert(!route_lookup(inst->rt, 0x0a010001));
|
||||
struct TOPO_GROUP_NODE* before = group.local_node;
|
||||
struct TOPO_NODE* identity = topo_node_registry_find(inst->topo_groups, inst->node_id);
|
||||
uint64_t timestamp = identity->timestamp; uint32_t refs = identity->group_ref_count;
|
||||
assert(sqlite3_exec(inst->topo_sqlite_db, "CREATE TRIGGER reject_local BEFORE UPDATE ON node_snapshots "
|
||||
"BEGIN SELECT RAISE(ABORT,'test local identity rejection'); END", NULL, NULL, NULL) == SQLITE_OK);
|
||||
subnet.ip.addr.v4.s_addr = htonl(0x0a030000);
|
||||
assert(topo_group_update_my_nodeinfo(inst, &group) < 0 && group.local_node == before);
|
||||
assert(route_lookup(inst->rt, 0x0a020001) && !route_lookup(inst->rt, 0x0a030001));
|
||||
assert(identity->timestamp == timestamp && identity->group_ref_count == refs && !topo_node_verify(identity));
|
||||
assert(sqlite3_exec(inst->topo_sqlite_db, "DROP TRIGGER reject_local", NULL, NULL, NULL) == SQLITE_OK);
|
||||
struct TOPO_SUBNET4 remote_sub = {.addr = {10, 3, 0, 0}, .prefix_length = 24};
|
||||
struct TOPO_NODESUBNETS remote_subnets = {.v4_subnets = &remote_sub};
|
||||
struct TOPO_GROUP_NODE remote = {.node_id = 999, .subnets = &remote_subnets};
|
||||
assert(route_insert(inst->rt, &remote));
|
||||
assert(topo_group_update_my_nodeinfo(inst, &group) < 0 && group.local_node == before);
|
||||
assert(route_lookup(inst->rt, 0x0a020001)->v_node_info == before && route_lookup(inst->rt, 0x0a030001)->v_node_info == &remote);
|
||||
assert(identity->timestamp == timestamp && identity->group_ref_count == refs && !topo_node_verify(identity));
|
||||
route_table_destroy(inst->rt); inst->rt = NULL;
|
||||
topo_nodeq_free_group_fields(inst->topo_groups, group.local_node); u_free(group.local_node);
|
||||
assert(!inst->topo_groups->node_registry->head);
|
||||
sqlite3_close(inst->topo_sqlite_db); inst->topo_sqlite_db = saved_db; inst->config = NULL;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] local group: same-count subnet replacement, signature failure and route conflict rollback");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
debug_config_init();
|
||||
debug_set_level(DEBUG_LEVEL_DEBUG);
|
||||
@@ -187,6 +344,8 @@ int main(void) {
|
||||
groups.v4_sock_meta_pool = memory_pool_init(sizeof(struct TOPO_SOCKMETA4), "snapshot_meta4");
|
||||
groups.v6_sock_meta_pool = memory_pool_init(sizeof(struct TOPO_SOCKMETA6), "snapshot_meta6");
|
||||
groups.v6_addr_pool = memory_pool_init(sizeof(struct TOPO_ADDR6), "snapshot_v6");
|
||||
groups.v4_subnet_pool = memory_pool_init(sizeof(struct TOPO_SUBNET4), "snapshot_subnet4");
|
||||
groups.v6_subnet_pool = memory_pool_init(sizeof(struct TOPO_SUBNET6), "snapshot_subnet6");
|
||||
assert(groups.v4_addr_pool && groups.v4_sock_meta_pool && groups.v6_sock_meta_pool && groups.v6_addr_pool);
|
||||
assert(sc_generate_keypair(&inst.my_keys) == SC_OK);
|
||||
memcpy(inst.my_ed25519_privkey, inst.my_keys.private_key, 32);
|
||||
@@ -197,7 +356,9 @@ int main(void) {
|
||||
inst.node_id = sc_derive_node_id_from_pubkey(inst.my_keys.public_key);
|
||||
assert(sqlite3_open(":memory:", &inst.topo_sqlite_db) == SQLITE_OK);
|
||||
assert(topo_node_sqlite_init(inst.topo_sqlite_db) == 0);
|
||||
test_verified_write();
|
||||
test_compact_addresses(&inst);
|
||||
test_local_group_update(&inst);
|
||||
|
||||
struct TOPO_NODE* first = record(&inst, 100, "old", 1);
|
||||
assert(topo_node_verify(first) == 0);
|
||||
@@ -260,6 +421,8 @@ int main(void) {
|
||||
memory_pool_destroy(groups.v4_sock_meta_pool);
|
||||
memory_pool_destroy(groups.v6_sock_meta_pool);
|
||||
memory_pool_destroy(groups.v6_addr_pool);
|
||||
memory_pool_destroy(groups.v4_subnet_pool);
|
||||
memory_pool_destroy(groups.v6_subnet_pool);
|
||||
sqlite3_close(inst.topo_sqlite_db);
|
||||
uasync_destroy(inst.ua, 0);
|
||||
return 0;
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
#include "topo_node.h"
|
||||
#include "topo_group.h"
|
||||
#include "etcp_connections.h"
|
||||
#include "utun_instance.h"
|
||||
#include "../lib/debug_config.h"
|
||||
#include "../lib/mem.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -67,7 +69,10 @@ int main(void) {
|
||||
uint8_t buf[2048];
|
||||
int len = topo_node_serialize(&ni, &nq, 0x1234, 0, buf, sizeof(buf), 0);
|
||||
CHECK(len > 0, "serialize ok");
|
||||
struct TOPO_GROUP g; memset(&g, 0, sizeof(g));
|
||||
struct UTUN_INSTANCE instance = {0};
|
||||
struct TOPO_GROUPS groups = {.instance = &instance};
|
||||
instance.topo_groups = &groups;
|
||||
struct TOPO_GROUP g = {.instance = &instance};
|
||||
struct TOPO_NODE* out_ni = NULL;
|
||||
struct TOPO_NODESUBNETS* out_sub = NULL;
|
||||
uint64_t* hop = NULL; uint8_t hopc = 0; uint16_t rtt = 0;
|
||||
@@ -87,6 +92,9 @@ int main(void) {
|
||||
CHECK(rr1 != NULL && !strcmp(rr1->server_name, long_sni), "round-trip: full server_name");
|
||||
CHECK(rr1 != NULL && rr1->version[0] == 1 && rr1->version[1] == 2 && rr1->version[2] == 3, "round-trip: version");
|
||||
}
|
||||
CHECK(!out_sub && !hop && !hopc && !rtt, "round-trip: empty optional sections");
|
||||
topo_node_destroy(&groups, out_ni);
|
||||
u_free(hop);
|
||||
|
||||
if (test_failed) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "=== Reality BGP Test: FAILED ===");
|
||||
|
||||
+89
-21
@@ -1,16 +1,4 @@
|
||||
/**
|
||||
* FULL REFACTORED TEST for new NODEINFO-based routing system
|
||||
*
|
||||
* Tests:
|
||||
* - TOPO_GROUP_NODE creation and insertion
|
||||
* - paths management (add/remove)
|
||||
* - versioning (ver field)
|
||||
* - WITHDRAW when no paths left
|
||||
* - Performance with 1000 nodes
|
||||
* - Overlap prevention and lookup
|
||||
*
|
||||
* Status: Complete coverage of current route_lib + route_bgp
|
||||
*/
|
||||
/* IPv4-таблица: владение, непересекающиеся подсети, атомарный отказ и поиск на границах. */
|
||||
|
||||
|
||||
#include <stdio.h>
|
||||
@@ -44,13 +32,8 @@ struct UTUN_INSTANCE {
|
||||
static struct TOPO_GROUP_NODE* create_test_node(uint64_t node_id, uint8_t ver, uint32_t subnet_net, uint8_t prefix) {
|
||||
struct TOPO_GROUP_NODE* nq = u_calloc(1, sizeof(struct TOPO_GROUP_NODE));
|
||||
if (!nq) return NULL;
|
||||
struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE));
|
||||
if (!ni) { u_free(nq); return NULL; }
|
||||
ni->group_ref_count = 1;
|
||||
ni->node_id = htobe64(node_id);
|
||||
ni->ver = ver;
|
||||
nq->node_id = ni->node_id;
|
||||
nq->paths = queue_new(NULL, 16, 0, 0, "test_paths");
|
||||
(void)ver;
|
||||
nq->node_id = node_id;
|
||||
struct TOPO_NODESUBNETS* r = u_calloc(1, sizeof(struct TOPO_NODESUBNETS));
|
||||
if (r) {
|
||||
struct TOPO_SUBNET4* sn = u_calloc(1, sizeof(struct TOPO_SUBNET4));
|
||||
@@ -152,11 +135,12 @@ static void test_performance_1000_nodes(void) {
|
||||
struct ROUTE_TABLE *t = route_table_create(0);
|
||||
clock_t start = clock();
|
||||
|
||||
struct TOPO_GROUP_NODE *nodes[1000];
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
uint32_t sub = 0x0a000000 + (i * 0x100); /* distinct 10.0.x.0/24 to avoid overlap */
|
||||
struct TOPO_GROUP_NODE* nq = create_test_node(0x1000000ULL + i, 1, sub, 24);
|
||||
nodes[i] = nq;
|
||||
route_insert(t, nq);
|
||||
/* note: nq not freed here (table holds ref to it), freed in test cleanup if needed */
|
||||
}
|
||||
|
||||
clock_t end = clock();
|
||||
@@ -166,6 +150,87 @@ static void test_performance_1000_nodes(void) {
|
||||
printf("(%.3fs for 1000 nodes) ", time);
|
||||
|
||||
route_table_destroy(t);
|
||||
for (int i = 0; i < 1000; i++) free_test_node(nodes[i]);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Проверяет оба порядка вложенных подсетей, дубликаты, host-биты и соседние диапазоны. */
|
||||
static void test_overlap_and_lookup(void) {
|
||||
TEST("overlap rejection and disjoint lookup");
|
||||
struct { uint32_t first, second; uint8_t p1, p2; bool accepted; } cases[] = {
|
||||
{0x0a000000, 0x0a010000, 8, 24, false},
|
||||
{0x0a010000, 0x0a000000, 24, 8, false},
|
||||
{0x0a010001, 0x0a0100ff, 24, 24, false},
|
||||
{0x0a010001, 0x0a010001, 32, 32, false},
|
||||
{0x0a010000, 0x0a010002, 31, 31, true},
|
||||
{0x0a010001, 0x0a010002, 32, 32, true},
|
||||
{0, 0x0a000001, 0, 32, false},
|
||||
{0x0a000001, 0, 32, 0, false}
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
||||
struct ROUTE_TABLE *t = route_table_create(1);
|
||||
struct TOPO_GROUP_NODE *a = create_test_node(1, 0, cases[i].first, cases[i].p1);
|
||||
struct TOPO_GROUP_NODE *b = create_test_node(2, 0, cases[i].second, cases[i].p2);
|
||||
ASSERT(route_insert(t, a), "first insertion");
|
||||
ASSERT_EQ(route_insert(t, b), cases[i].accepted, "overlap decision");
|
||||
ASSERT_EQ(t->count, cases[i].accepted ? 2 : 1, "atomic table count");
|
||||
ASSERT_EQ(t->stats.local_routes, 1, "local route count");
|
||||
ASSERT_EQ(t->stats.learned_routes, cases[i].accepted ? 1 : 0, "learned route count");
|
||||
struct ROUTE_ENTRY *found = route_lookup(t, cases[i].first);
|
||||
ASSERT(found && found->v_node_info == a, "first route lookup");
|
||||
if (cases[i].accepted) {
|
||||
found = route_lookup(t, cases[i].second);
|
||||
ASSERT(found && found->v_node_info == b, "second route lookup");
|
||||
}
|
||||
route_delete(t, a);
|
||||
route_delete(t, b);
|
||||
ASSERT_EQ(t->count, 0, "delete clears routes");
|
||||
route_table_destroy(t); free_test_node(a); free_test_node(b);
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Ни поздний конфликт набора, ни неверный префикс не должны оставлять частичную вставку. */
|
||||
static void test_batch_atomicity(void) {
|
||||
TEST("batch rejection leaves table unchanged");
|
||||
struct ROUTE_TABLE *t = route_table_create(1);
|
||||
struct TOPO_GROUP_NODE *a = create_test_node(1, 0, 0x0a000000, 8);
|
||||
struct TOPO_GROUP_NODE *b = create_test_node(2, 0, 0xc0a80000, 24);
|
||||
ASSERT(route_insert(t, a), "base route");
|
||||
struct TOPO_SUBNET4 second = {0};
|
||||
uint32_t network = htonl(0x0a010000);
|
||||
memcpy(second.addr, &network, 4); second.prefix_length = 24;
|
||||
b->subnets->v4_subnets->next = &second;
|
||||
ASSERT(!route_insert(t, b), "late overlap with table");
|
||||
ASSERT_EQ(t->count, 1, "no partial insert before table conflict");
|
||||
network = htonl(0xc0a80080); memcpy(second.addr, &network, 4);
|
||||
ASSERT(!route_insert(t, b), "overlap inside batch");
|
||||
ASSERT_EQ(t->count, 1, "no partial insert before batch conflict");
|
||||
second.prefix_length = 33;
|
||||
ASSERT(!route_insert(t, b), "invalid prefix rejected");
|
||||
ASSERT_EQ(t->count, 1, "invalid prefix preserves table");
|
||||
ASSERT_EQ(t->stats.learned_routes, 0, "rejection preserves statistics");
|
||||
b->subnets->v4_subnets->next = NULL;
|
||||
route_table_destroy(t); free_test_node(a); free_test_node(b);
|
||||
PASS();
|
||||
}
|
||||
|
||||
static void test_lookup_boundaries(void) {
|
||||
TEST("normalized subnet boundaries and default route");
|
||||
struct ROUTE_TABLE *t = route_table_create(1);
|
||||
struct TOPO_GROUP_NODE *a = create_test_node(1, 0, 0x0a00007b, 24);
|
||||
ASSERT(route_insert(t, a), "noncanonical address accepted");
|
||||
ASSERT_EQ(t->entries[0].network, 0x0a000000, "network normalized");
|
||||
ASSERT(route_lookup(t, 0x0a000000), "first address");
|
||||
ASSERT(route_lookup(t, 0x0a0000ff), "last address");
|
||||
ASSERT(!route_lookup(t, 0x09ffffff), "before range");
|
||||
ASSERT(!route_lookup(t, 0x0a000100), "after range");
|
||||
route_delete(t, a);
|
||||
a->subnets->v4_subnets->prefix_length = 0;
|
||||
ASSERT(route_insert(t, a), "default route");
|
||||
ASSERT(route_lookup(t, 0), "default first address");
|
||||
ASSERT(route_lookup(t, UINT32_MAX), "default last address");
|
||||
route_table_destroy(t); free_test_node(a);
|
||||
PASS();
|
||||
}
|
||||
|
||||
@@ -187,6 +252,9 @@ int main(void) {
|
||||
test_table_create_destroy();
|
||||
test_nodeinfo_insert();
|
||||
test_versioning();
|
||||
test_overlap_and_lookup();
|
||||
test_batch_atomicity();
|
||||
test_lookup_boundaries();
|
||||
test_performance_1000_nodes();
|
||||
test_destroy_refcount();
|
||||
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
/* Ручной NODEINFO: границы секций, необязательные выходы и атомарная очистка при отказах памяти. */
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <openssl/sha.h>
|
||||
#include "utun_instance.h"
|
||||
#include "routing_layer/topo_group.h"
|
||||
#include "routing_layer/topo_node_sqlite.h"
|
||||
#include "routing_layer/route_lib.h"
|
||||
#include "config_parser.h"
|
||||
#include "transport_layer/etcp.h"
|
||||
#include "../lib/debug_config.h"
|
||||
#include "../lib/mem.h"
|
||||
#include "../lib/memory_pool.h"
|
||||
#include "../lib/u_async.h"
|
||||
|
||||
struct wire_fixture {
|
||||
struct UTUN_INSTANCE inst;
|
||||
struct TOPO_GROUPS groups;
|
||||
struct TOPO_GROUP group;
|
||||
struct memory_pool *pools[6];
|
||||
uint8_t wire[2048];
|
||||
size_t length, subnet4_offset, subnet6_offset, hops_offset;
|
||||
uint64_t hops[2];
|
||||
};
|
||||
|
||||
#ifdef __linux__
|
||||
static unsigned fail_at, attempts, inside_pool;
|
||||
static int track_allocations;
|
||||
|
||||
static int allocation_fails(const char *where) {
|
||||
if (!track_allocations || (!strstr(where, "topo_node.c:") && !strstr(where, "topo_node_sqlite.c:"))) return 0;
|
||||
attempts++;
|
||||
if (attempts != fail_at) return 0;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_BGP, "[fault] NODEINFO allocation %u rejected at %s", attempts, where);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void *__real_u_malloc_impl(uint32_t, const char *);
|
||||
void *__wrap_u_malloc_impl(uint32_t size, const char *where) {
|
||||
if (!inside_pool && allocation_fails(where)) return NULL;
|
||||
return __real_u_malloc_impl(size, where);
|
||||
}
|
||||
|
||||
void *__real_u_calloc_impl(uint32_t, uint32_t, const char *);
|
||||
void *__wrap_u_calloc_impl(uint32_t count, uint32_t size, const char *where) {
|
||||
if (!inside_pool && allocation_fails(where)) return NULL;
|
||||
return __real_u_calloc_impl(count, size, where);
|
||||
}
|
||||
|
||||
void *__real_memory_pool_alloc_impl(struct memory_pool *, const char *);
|
||||
void *__wrap_memory_pool_alloc_impl(struct memory_pool *pool, const char *where) {
|
||||
if (allocation_fails(where)) return NULL;
|
||||
inside_pool++;
|
||||
void *result = __real_memory_pool_alloc_impl(pool, where);
|
||||
inside_pool--;
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void fixture_init(struct wire_fixture *f) {
|
||||
memset(f, 0, sizeof(*f));
|
||||
f->inst.topo_groups = &f->groups; f->groups.instance = &f->inst; f->group.instance = &f->inst;
|
||||
size_t sizes[] = {sizeof(struct TOPO_SOCKMETA4), sizeof(struct TOPO_ADDR4), sizeof(struct TOPO_SOCKMETA6),
|
||||
sizeof(struct TOPO_ADDR6), sizeof(struct TOPO_SUBNET4), sizeof(struct TOPO_SUBNET6)};
|
||||
for (size_t i = 0; i < 6; i++) { f->pools[i] = memory_pool_init(sizes[i], "node_wire"); assert(f->pools[i]); }
|
||||
f->groups.v4_sock_meta_pool = f->pools[0]; f->groups.v4_addr_pool = f->pools[1];
|
||||
f->groups.v6_sock_meta_pool = f->pools[2]; f->groups.v6_addr_pool = f->pools[3];
|
||||
f->groups.v4_subnet_pool = f->pools[4]; f->groups.v6_subnet_pool = f->pools[5];
|
||||
f->hops[0] = 0x1111111111111111ULL; f->hops[1] = 0x2222222222222222ULL;
|
||||
}
|
||||
|
||||
static void fixture_destroy(struct wire_fixture *f) {
|
||||
for (size_t i = 0; i < 6; i++) memory_pool_destroy(f->pools[i]);
|
||||
}
|
||||
|
||||
/* Пакет собирается независимо от production encoder; по два элемента проверяют очистку частичных списков. */
|
||||
static void fixture_packet(struct wire_fixture *f, uint8_t flags, uint8_t hops) {
|
||||
memset(f->wire, 0, sizeof(f->wire));
|
||||
struct TOPOMSG_NODE *h = (struct TOPOMSG_NODE *)f->wire;
|
||||
h->node_id = 9; h->flags = flags; h->node_name_len = 4; h->cumulative_rtt = 123;
|
||||
h->local_v4_sockets = h->local_v4_addrs = h->local_v6_sockets = h->local_v6_addrs = h->local_reality_socks = 2;
|
||||
if (flags & TOPO_FLAG_SEND_SUBNETS) h->local_v4_subnets = h->local_v6_subnets = 2;
|
||||
h->hop_count = hops;
|
||||
uint8_t *p = f->wire + TOPOMSG_NODE_HDR_SIZE;
|
||||
memcpy(p, "wire", 4); p += 4;
|
||||
for (unsigned i = 0; i < 2; i++) {
|
||||
struct TOPOMSG_SOCKMETA4 m = {.id = i}; memcpy(p, &m, sizeof(m)); p += sizeof(m);
|
||||
}
|
||||
for (unsigned i = 0; i < 2; i++) {
|
||||
struct TOPOMSG_ADDR4 a = {.addr = {10, 0, 0, i + 1}, .port = 9000 + i, .socket_id = i, .protocol = TOPO_PROTO_UDP};
|
||||
memcpy(p, &a, sizeof(a)); p += sizeof(a);
|
||||
}
|
||||
for (unsigned i = 0; i < 2; i++) {
|
||||
struct TOPOMSG_SOCKMETA6 m = {.id = i}; memcpy(p, &m, sizeof(m)); p += sizeof(m);
|
||||
}
|
||||
for (unsigned i = 0; i < 2; i++) {
|
||||
struct TOPOMSG_ADDR6 a = {.addr = {0x20, 1, 0xdb, 8}, .port = 9100 + i, .socket_id = i, .protocol = TOPO_PROTO_TCP};
|
||||
a.addr[15] = i + 1; memcpy(p, &a, sizeof(a)); p += sizeof(a);
|
||||
}
|
||||
for (unsigned i = 0; i < 2; i++) {
|
||||
struct TOPOMSG_REALITY_SOCK r = {.socket_id = i};
|
||||
memcpy(r.server_name, i ? "second.test" : "first.test", i ? 12 : 11);
|
||||
memcpy(p, &r, sizeof(r)); p += sizeof(r);
|
||||
}
|
||||
f->subnet4_offset = p - f->wire;
|
||||
if (flags & TOPO_FLAG_SEND_SUBNETS) for (unsigned i = 0; i < 2; i++) {
|
||||
struct TOPOMSG_SUBNET4 s = {.addr = {10, i, 0, 0}, .prefix_length = 24};
|
||||
memcpy(p, &s, sizeof(s)); p += sizeof(s);
|
||||
}
|
||||
f->subnet6_offset = p - f->wire;
|
||||
if (flags & TOPO_FLAG_SEND_SUBNETS) for (unsigned i = 0; i < 2; i++) {
|
||||
struct TOPOMSG_SUBNET6 s = {.addr = {0x20, 1, 0xdb, 8}, .prefix_length = 64};
|
||||
s.addr[7] = i; memcpy(p, &s, sizeof(s)); p += sizeof(s);
|
||||
}
|
||||
f->hops_offset = p - f->wire;
|
||||
memcpy(p, f->hops, hops * sizeof(*f->hops)); p += hops * sizeof(*f->hops);
|
||||
f->length = p - f->wire;
|
||||
assert(f->length <= sizeof(f->wire));
|
||||
}
|
||||
|
||||
/* Свободные блоки остаются в кешах пулов; сравниваем только реально удерживаемые объекты. */
|
||||
static size_t live_allocations(void) {
|
||||
return u_get_allocated_count() - memory_pool_get_total_free_blocks();
|
||||
}
|
||||
|
||||
static void result_free(struct wire_fixture *f, struct TOPO_NODE *node, struct TOPO_NODESUBNETS *subnets, uint64_t *hops) {
|
||||
topo_node_destroy(&f->groups, node);
|
||||
struct TOPO_GROUP_NODE temporary = {.subnets = subnets};
|
||||
topo_nodeq_free_group_fields(&f->groups, &temporary);
|
||||
u_free(hops);
|
||||
}
|
||||
|
||||
static void expect_rejected(struct wire_fixture *f, size_t length, int keep_subnets) {
|
||||
struct TOPO_NODE *node = (void *)(uintptr_t)1;
|
||||
struct TOPO_NODESUBNETS *subnets = (void *)(uintptr_t)1;
|
||||
uint64_t *hops = (void *)(uintptr_t)1;
|
||||
uint8_t count = 99; uint16_t rtt = 99;
|
||||
size_t before = live_allocations();
|
||||
assert(topo_node_deserialize(&f->group, f->wire, length, &node, keep_subnets ? &subnets : NULL, &hops, &count, &rtt) < 0);
|
||||
assert(!node && !hops && !count && !rtt);
|
||||
if (keep_subnets) assert(!subnets);
|
||||
assert(live_allocations() == before);
|
||||
}
|
||||
|
||||
static void test_invalid_packets(struct wire_fixture *f) {
|
||||
fixture_packet(f, 0, 2);
|
||||
struct TOPOMSG_NODE *h = (struct TOPOMSG_NODE *)f->wire;
|
||||
h->local_v4_subnets = 1;
|
||||
expect_rejected(f, f->length, 1);
|
||||
assert(topo_node_dyn_size(h) < 0);
|
||||
h->local_v4_subnets = 0; h->local_v6_subnets = 1;
|
||||
expect_rejected(f, f->length, 0);
|
||||
assert(topo_node_dyn_size(h) < 0);
|
||||
fixture_packet(f, TOPO_FLAG_SEND_SUBNETS, 2);
|
||||
for (size_t length = 0; length < f->length; length++) expect_rejected(f, length, length % 2);
|
||||
expect_rejected(f, f->length + 1, 1);
|
||||
for (int keep = 0; keep < 2; keep++) {
|
||||
struct TOPOMSG_SUBNET4 *v4 = (struct TOPOMSG_SUBNET4 *)(f->wire + f->subnet4_offset);
|
||||
v4->prefix_length = 33; expect_rejected(f, f->length, keep); v4->prefix_length = 24;
|
||||
struct TOPOMSG_SUBNET6 *v6 = (struct TOPOMSG_SUBNET6 *)(f->wire + f->subnet6_offset);
|
||||
v6->prefix_length = 129; expect_rejected(f, f->length, keep); v6->prefix_length = 64;
|
||||
}
|
||||
struct TOPOMSG_SOCKMETA4 *meta = (struct TOPOMSG_SOCKMETA4 *)(f->wire + TOPOMSG_NODE_HDR_SIZE + 4);
|
||||
meta[1].mapping = UINT8_MAX; expect_rejected(f, f->length, 1);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] NODEINFO malformed counters, all truncations, tail, prefixes and partial metadata");
|
||||
}
|
||||
|
||||
static void test_optional_outputs(struct wire_fixture *f) {
|
||||
for (unsigned with_subnets = 0; with_subnets < 2; with_subnets++) for (unsigned with_hops = 0; with_hops < 2; with_hops++) {
|
||||
fixture_packet(f, with_subnets ? TOPO_FLAG_SEND_SUBNETS : 0, with_hops ? 2 : 0);
|
||||
for (unsigned outputs = 0; outputs < 16; outputs++) {
|
||||
struct TOPO_NODE *node = NULL; struct TOPO_NODESUBNETS *subnets = NULL;
|
||||
uint64_t *hops = NULL; uint8_t count = 99; uint16_t rtt = 99;
|
||||
size_t before = live_allocations();
|
||||
assert(!topo_node_deserialize(&f->group, f->wire, f->length, &node, outputs & 1 ? &subnets : NULL,
|
||||
outputs & 2 ? &hops : NULL, outputs & 4 ? &count : NULL, outputs & 8 ? &rtt : NULL));
|
||||
assert(node && !strcmp(node->node_name, "wire"));
|
||||
assert(node->v4_addrs->addr[3] == 1 && node->v4_addrs->next->addr[3] == 2);
|
||||
assert(node->v6_addrs->addr[15] == 1 && node->v6_addrs->next->addr[15] == 2);
|
||||
assert(!strcmp(node->reality_socks->server_name, "first.test"));
|
||||
assert(!strcmp(node->reality_socks->next->server_name, "second.test"));
|
||||
if (outputs & 1) {
|
||||
assert(!!subnets == !!with_subnets);
|
||||
if (subnets) assert(topo_list_count((struct _topo_head *)subnets->v4_subnets) == 2 &&
|
||||
topo_list_count((struct _topo_head *)subnets->v6_subnets) == 2);
|
||||
}
|
||||
if (outputs & 2) {
|
||||
assert(!!hops == !!with_hops);
|
||||
if (hops) assert(!memcmp(hops, f->hops, sizeof(f->hops)));
|
||||
}
|
||||
if (outputs & 4) assert(count == (with_hops ? 2 : 0));
|
||||
if (outputs & 8) assert(rtt == 123);
|
||||
result_free(f, node, subnets, hops);
|
||||
assert(live_allocations() == before);
|
||||
}
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] NODEINFO 64 flag/hop/output combinations preserve identity, hops and ownership");
|
||||
}
|
||||
|
||||
static void test_encoder(struct wire_fixture *f) {
|
||||
fixture_packet(f, TOPO_FLAG_SEND_SUBNETS, 0);
|
||||
struct TOPO_NODE *node = NULL; struct TOPO_NODESUBNETS *subnets = NULL;
|
||||
assert(!topo_node_deserialize(&f->group, f->wire, f->length, &node, &subnets, NULL, NULL, NULL));
|
||||
struct TOPO_GROUP_NODE nq = {.subnets = subnets};
|
||||
uint8_t encoded[2048];
|
||||
for (unsigned with_subnets = 0; with_subnets < 2; with_subnets++) {
|
||||
uint8_t flags = with_subnets ? TOPO_FLAG_SEND_SUBNETS : 0;
|
||||
int length = topo_node_serialize(node, &nq, 42, flags, encoded, sizeof(encoded), 123);
|
||||
assert(length > 0);
|
||||
const struct TOPOMSG_NODE *h = (const struct TOPOMSG_NODE *)encoded;
|
||||
assert(h->local_v4_subnets == (with_subnets ? 2 : 0) && h->local_v6_subnets == (with_subnets ? 2 : 0));
|
||||
assert((size_t)length == TOPOMSG_NODE_HDR_SIZE + (size_t)topo_node_dyn_size(h));
|
||||
assert(topo_node_serialize(node, &nq, 42, flags, encoded, length - 1, 123) < 0);
|
||||
assert(topo_node_serialize(node, &nq, 42, flags, encoded, length, 123) == length);
|
||||
struct TOPO_NODE *copy = NULL;
|
||||
assert(!topo_node_deserialize(&f->group, encoded, length, ©, NULL, NULL, NULL, NULL));
|
||||
topo_node_destroy(&f->groups, copy);
|
||||
}
|
||||
result_free(f, node, subnets, NULL);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] NODEINFO encoder emits only selected sections and checks exact buffer capacity");
|
||||
}
|
||||
|
||||
static void test_empty_and_arguments(struct wire_fixture *f) {
|
||||
memset(f->wire, 0, sizeof(f->wire));
|
||||
struct TOPOMSG_NODE *h = (struct TOPOMSG_NODE *)f->wire;
|
||||
for (unsigned with_flag = 0; with_flag < 2; with_flag++) {
|
||||
h->flags = with_flag ? TOPO_FLAG_SEND_SUBNETS : 0;
|
||||
struct TOPO_NODE *node = NULL; struct TOPO_NODESUBNETS *subnets = (void *)(uintptr_t)1;
|
||||
uint64_t *hops = (void *)(uintptr_t)1; uint8_t count = 99; uint16_t rtt = 99;
|
||||
assert(!topo_node_deserialize(&f->group, f->wire, TOPOMSG_NODE_HDR_SIZE, &node, &subnets, &hops, &count, &rtt));
|
||||
assert(node && !node->node_name && !node->v4_addrs && !node->v6_addrs && !subnets && !hops && !count && !rtt);
|
||||
result_free(f, node, subnets, hops);
|
||||
}
|
||||
struct TOPO_NODE *node = (void *)(uintptr_t)1;
|
||||
assert(topo_node_deserialize(NULL, f->wire, TOPOMSG_NODE_HDR_SIZE, &node, NULL, NULL, NULL, NULL) < 0 && !node);
|
||||
assert(topo_node_deserialize(&f->group, NULL, TOPOMSG_NODE_HDR_SIZE, &node, NULL, NULL, NULL, NULL) < 0 && !node);
|
||||
assert(topo_node_deserialize(&f->group, f->wire, TOPOMSG_NODE_HDR_SIZE, NULL, NULL, NULL, NULL, NULL) < 0);
|
||||
assert(topo_node_dyn_size(NULL) < 0);
|
||||
fixture_packet(f, TOPO_FLAG_SEND_SUBNETS, 0);
|
||||
struct TOPOMSG_SUBNET4 *v4 = (struct TOPOMSG_SUBNET4 *)(f->wire + f->subnet4_offset);
|
||||
struct TOPOMSG_SUBNET6 *v6 = (struct TOPOMSG_SUBNET6 *)(f->wire + f->subnet6_offset);
|
||||
v4[0].prefix_length = 0; v4[1].prefix_length = 32;
|
||||
v6[0].prefix_length = 0; v6[1].prefix_length = 128;
|
||||
assert(!topo_node_deserialize(&f->group, f->wire, f->length, &node, NULL, NULL, NULL, NULL));
|
||||
topo_node_destroy(&f->groups, node);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] NODEINFO empty sections, missing arguments and prefix boundaries");
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
static void test_allocation_failures(struct wire_fixture *f) {
|
||||
fixture_packet(f, TOPO_FLAG_SEND_SUBNETS, 2);
|
||||
struct TOPO_NODE *node = NULL; struct TOPO_NODESUBNETS *subnets = NULL;
|
||||
uint64_t *hops = NULL; uint8_t count = 0;
|
||||
attempts = 0; fail_at = 0; track_allocations = 1;
|
||||
int rc = topo_node_deserialize(&f->group, f->wire, f->length, &node, &subnets, &hops, &count, NULL);
|
||||
track_allocations = 0;
|
||||
assert(!rc && attempts > 10);
|
||||
unsigned total = attempts;
|
||||
result_free(f, node, subnets, hops);
|
||||
for (unsigned nth = 1; nth <= total; nth++) {
|
||||
attempts = 0; fail_at = nth; track_allocations = 1;
|
||||
expect_rejected(f, f->length, 1);
|
||||
track_allocations = 0;
|
||||
assert(attempts == nth);
|
||||
}
|
||||
fail_at = 0;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] NODEINFO all %u allocation failures leave empty outputs and no live allocations", total);
|
||||
}
|
||||
|
||||
/* SQL bootstrap обязан освободить уже прочитанные адреса/Reality при отказе любого следующего выделения. */
|
||||
static void test_bootstrap_allocation_failures(struct wire_fixture *f) {
|
||||
fixture_packet(f, 0, 0);
|
||||
struct TOPO_NODE *node = NULL;
|
||||
assert(!topo_node_deserialize(&f->group, f->wire, f->length, &node, NULL, NULL, NULL, NULL));
|
||||
node->v4_addrs->protocol = node->v6_addrs->protocol = TOPO_PROTO_TCP | TOPO_PROTO_REALITY;
|
||||
sqlite3 *db = NULL;
|
||||
assert(sqlite3_open(":memory:", &db) == SQLITE_OK && !topo_node_sqlite_init(db));
|
||||
assert(!topo_node_sqlite_node_update_verified(db, node->node_id, "bootstrap", node->public_key, node->ed25519_public_key, 1, 1));
|
||||
assert(!topo_node_sqlite_addrs_put(db, node->node_id, node));
|
||||
attempts = 0; fail_at = 0; track_allocations = 1;
|
||||
struct TOPO_NODE *loaded = topo_node_sqlite_node_load(db, &f->groups, node->node_id);
|
||||
track_allocations = 0;
|
||||
assert(loaded && loaded->v4_addrs && loaded->v6_addrs && loaded->reality_socks && attempts > 8);
|
||||
unsigned total = attempts;
|
||||
topo_node_destroy(&f->groups, loaded);
|
||||
for (unsigned nth = 1; nth <= total; nth++) {
|
||||
size_t before = live_allocations();
|
||||
attempts = 0; fail_at = nth; track_allocations = 1;
|
||||
loaded = topo_node_sqlite_node_load(db, &f->groups, node->node_id);
|
||||
track_allocations = 0;
|
||||
assert(!loaded && attempts == nth && live_allocations() == before);
|
||||
assert(sqlite3_next_stmt(db, NULL) == NULL);
|
||||
}
|
||||
fail_at = 0;
|
||||
assert(sqlite3_exec(db, "DROP TABLE node_addresses", NULL, NULL, NULL) == SQLITE_OK);
|
||||
size_t before = live_allocations();
|
||||
assert(!topo_node_sqlite_node_load(db, &f->groups, node->node_id) && live_allocations() == before);
|
||||
assert(sqlite3_next_stmt(db, NULL) == NULL);
|
||||
topo_node_destroy(&f->groups, node); sqlite3_close(db);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] SQL bootstrap: %u allocation failures and SQL failure leave no live objects/statements", total);
|
||||
}
|
||||
|
||||
/* Уже опубликованный local_node и его маршруты переживают любой отказ подготовки новой группы. */
|
||||
static void test_local_allocation_failures(struct wire_fixture *f) {
|
||||
struct CFG_ROUTE_ENTRY v6 = {.ip.family = AF_INET6, .netmask = 64};
|
||||
memcpy(&v6.ip.addr.v6, (uint8_t[]){0x20, 1, 0xdb, 8}, 4);
|
||||
struct CFG_ROUTE_ENTRY v4 = {.next = &v6, .ip.family = AF_INET, .netmask = 24};
|
||||
v4.ip.addr.v4.s_addr = htonl(0x0a010000);
|
||||
struct utun_config config = {.my_subnets = &v4}; f->inst.config = &config;
|
||||
f->inst.ua = uasync_create(); assert(f->inst.ua);
|
||||
f->groups.node_registry = queue_new(f->inst.ua, 16, 0, 8, "local_wire_test"); assert(f->groups.node_registry);
|
||||
assert(sc_generate_keypair(&f->inst.my_keys) == SC_OK);
|
||||
uint8_t seed[SHA512_DIGEST_LENGTH]; SHA512(f->inst.my_keys.private_key, 32, seed);
|
||||
memcpy(f->inst.my_ed25519_privkey, seed, 32);
|
||||
assert(sc_derive_ed25519_pubkey(f->inst.my_keys.private_key, f->inst.my_ed25519_pubkey) == SC_OK);
|
||||
f->inst.node_id = sc_derive_node_id_from_pubkey(f->inst.my_keys.public_key);
|
||||
f->inst.rt = route_table_create(f->inst.node_id); assert(f->inst.rt);
|
||||
assert(!topo_node_init_self(&f->inst));
|
||||
f->group.group_type = TOPO_GROUP_TYPE_UTUN;
|
||||
assert(topo_group_update_my_nodeinfo(&f->inst, &f->group) == 1);
|
||||
v4.ip.addr.v4.s_addr = htonl(0x0a020000);
|
||||
attempts = 0; fail_at = 0; track_allocations = 1;
|
||||
int rc = topo_group_update_my_nodeinfo(&f->inst, &f->group);
|
||||
track_allocations = 0;
|
||||
assert(rc == 1 && attempts >= 4); unsigned total = attempts;
|
||||
struct TOPO_GROUP_NODE* old = f->group.local_node;
|
||||
struct TOPO_NODE* node = topo_node_registry_find(&f->groups, f->inst.node_id);
|
||||
uint64_t timestamp = node->timestamp; uint32_t refs = node->group_ref_count;
|
||||
v4.ip.addr.v4.s_addr = htonl(0x0a030000);
|
||||
for (unsigned nth = 1; nth <= total; nth++) {
|
||||
size_t before = live_allocations();
|
||||
attempts = 0; fail_at = nth; track_allocations = 1;
|
||||
rc = topo_group_update_my_nodeinfo(&f->inst, &f->group);
|
||||
track_allocations = 0;
|
||||
assert(rc < 0 && attempts == nth && f->group.local_node == old && live_allocations() == before);
|
||||
assert(node->timestamp == timestamp && node->group_ref_count == refs && !topo_node_verify(node));
|
||||
assert(route_lookup(f->inst.rt, 0x0a020001)->v_node_info == old && !route_lookup(f->inst.rt, 0x0a030001));
|
||||
}
|
||||
fail_at = 0;
|
||||
topo_nodeq_free_group_fields(&f->groups, old); u_free(old); f->group.local_node = NULL;
|
||||
topo_node_registry_unref(&f->groups, f->inst.node_id);
|
||||
assert(!f->groups.node_registry->head); queue_free(f->groups.node_registry); f->groups.node_registry = NULL;
|
||||
route_table_destroy(f->inst.rt); f->inst.rt = NULL;
|
||||
uasync_destroy(f->inst.ua, 0); f->inst.ua = NULL; f->inst.config = NULL;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] local group: %u allocation failures preserve identity, routes and ownership", total);
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(void) {
|
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG);
|
||||
size_t before = u_get_allocated_count();
|
||||
struct wire_fixture fixture;
|
||||
fixture_init(&fixture);
|
||||
test_invalid_packets(&fixture);
|
||||
test_optional_outputs(&fixture);
|
||||
test_encoder(&fixture);
|
||||
test_empty_and_arguments(&fixture);
|
||||
#ifdef __linux__
|
||||
test_allocation_failures(&fixture);
|
||||
test_bootstrap_allocation_failures(&fixture);
|
||||
test_local_allocation_failures(&fixture);
|
||||
#endif
|
||||
fixture_destroy(&fixture);
|
||||
assert(u_get_allocated_count() == before);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] NODEINFO wire suite finished without leaks");
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user