aboutsummaryrefslogtreecommitdiffstats
path: root/link
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-29 14:39:27 +0100
committereunhhu <[email protected]>2026-09-29 14:39:27 +0100
commitdee0c1442202272de1f4a8ebe555af35874d691b (patch)
treebbdbca2ff30ec155b48cff7b8c601a16abda03c9 /link
parent0bbc12658c63f5f5fce2683798d4ac08b1e26e08 (diff)
downloadpure-dee0c1442202272de1f4a8ebe555af35874d691b.tar.gz
pure-dee0c1442202272de1f4a8ebe555af35874d691b.zip
Begin an epoch only from the answer to this offer, not an earlier one
A sender that took its epoch for lost offers epoch 1 again with a new key. The receiver accepts it in the turn, after the answers; until then its answers still carried the ACCEPT of the earlier offer, which named the same epoch. The sender began its epoch from that one, the two sides held different secrets, and 2 s later it took the epoch for lost again: a turn of that each time the order fell so. Now an ACCEPT carries the first four bytes of the key it answers, and a sender begins its epoch only from one that answers its own. test-link cuts the path for 3 s the moment the sender begins an epoch, three times; a sender's counts say how many epochs it took for lost, and it must be three. Before, it was four.
Diffstat (limited to 'link')
-rw-r--r--link/link-protocol.c19
-rw-r--r--link/link-protocol.h13
-rw-r--r--link/test-link.c40
3 files changed, 52 insertions, 20 deletions
diff --git a/link/link-protocol.c b/link/link-protocol.c
index 1f43e6c..8845ea5 100644
--- a/link/link-protocol.c
+++ b/link/link-protocol.c
@@ -22,8 +22,9 @@ enum { F_PADDING = 0, F_STREAM = 1, F_RECORD = 2, F_ACK = 3, F_STORED = 4, F_PIN
F_RESPONSE = 8, F_OFFER = 9, F_ACCEPT = 10, STORED_GAP = 1 };
/* A packet's first byte is VERSION with the sending side's epoch (mod 16)
* in its low four bits; OFFER and ACCEPT are an epoch's number (u32) and a
- * public key made for it. */
-enum { EPOCH_MASK = 15, EPOCH_FRAME = 1 + 4 + PURE_SEAL_KEY };
+ * public key made for it, and ACCEPT the first bytes of the key it answers
+ * (a sender may offer an epoch again with another key). */
+enum { EPOCH_MASK = 15, EPOCH_FRAME = 1 + 4 + PURE_SEAL_KEY, ANSWERS = 4, ACCEPT_FRAME = EPOCH_FRAME + ANSWERS };
enum { RECORDS_HEAD = 10 }; /* type, the first number, the count */
enum { RANGES = 32, RUNS = 2, UNITS = 64, CREDIT = 1024, BATCH = 256, RECORD_HEAD = 19 };
enum { WINDOW_FIRST = 10 * DATAGRAM, WINDOW_LEAST = 2 * DATAGRAM };
@@ -712,10 +713,12 @@ void pure_link_sender_take(PureLinkSender *l, const unsigned char *packet, size_
if (l->responding < RESPONSES) ++l->responding;
}
at += 8;
- } else if (type == F_ACCEPT) { /* the epoch offered begins */
- if (at + EPOCH_FRAME - 1 > size) break;
- if (l->offering && get32(plain + at) == l->offer_epoch) begin_epoch(l, plain + at + 4, now);
- at += EPOCH_FRAME - 1;
+ } else if (type == F_ACCEPT) { /* the epoch offered begins: an answer to this offer, not one before */
+ if (at + ACCEPT_FRAME - 1 > size) break;
+ if (l->offering && get32(plain + at) == l->offer_epoch &&
+ !memcmp(plain + at + EPOCH_FRAME - 1, l->offer_public, ANSWERS))
+ begin_epoch(l, plain + at + 4, now);
+ at += ACCEPT_FRAME - 1;
} else if (type != F_PING)
break; /* padding, or what this side does not know: the rest */
}
@@ -954,6 +957,7 @@ void pure_link_sender_tick(PureLinkSender *l, uint64_t now) {
offer(l, 1);
l->renegotiating = true;
l->offer_again = 0;
+ ++l->counts.fallbacks;
}
if (l->loss_time && l->loss_time <= now) detect_lost(l, now);
else if (l->eliciting && l->last_eliciting + pto_period(l) <= now) {
@@ -2165,7 +2169,8 @@ static void answer(PureLinkReceiver *s, uint64_t now, PureLinkSend *send, void *
body[at] = F_ACCEPT;
put32(body + at + 1, r->accept_epoch);
memcpy(body + at + 5, r->accept_public, PURE_SEAL_KEY);
- at += EPOCH_FRAME;
+ memcpy(body + at + EPOCH_FRAME, r->offered, ANSWERS);
+ at += ACCEPT_FRAME;
}
if (r->ack_due && r->got_count) {
body[at++] = F_ACK;
diff --git a/link/link-protocol.h b/link/link-protocol.h
index f54514b..a8dd826 100644
--- a/link/link-protocol.h
+++ b/link/link-protocol.h
@@ -95,7 +95,9 @@
* epoch's secret, until it is accepted
* 0x0a ACCEPT u32 epoch, a public key: the receiver's half, until a
* packet of that epoch opens (the same offer again gets the
- * same; one a second for a run)
+ * same; one a second for a sender), then the first four
+ * bytes of the key offered it answers (a sender that took
+ * its epoch for lost offers epoch 1 again with another)
* 0x00 PADDING to the end of the packet: a full packet sent in a burst is
* a whole datagram
* Losses are detected as RFC 9002 says, with RACK's patience for packets
@@ -114,12 +116,13 @@ enum { PURE_LINK_DATAGRAM = 1200, PURE_LINK_DATAGRAM_MOST = 1452, PURE_LINK_NAME
* packets lost and acknowledged, packets received, deliveries handed over,
* the last number stored; packets called lost that came late, and
* congestion events undone for it; runs keyed (keys made from a secret),
- * secrets made (X25519 of the nodes' keys), epochs begun; a receiver's senders woken (their active
- * state made), of them with a secret remembered (no X25519), let go when
- * idle, and active now. */
+ * secrets made (X25519), epochs begun, and a sender's epochs taken for lost
+ * (begun again from the nodes' keys); a receiver's senders woken (their
+ * active state made), of them with a secret remembered (no X25519), let go
+ * when idle, and active now. */
typedef struct {
uint64_t packets, records, resent, lost, acknowledged, received, deliveries, stored, late, undone, keyed, secrets,
- epochs, woken, remembered, swept, active;
+ epochs, fallbacks, woken, remembered, swept, active;
} PureLinkCounts;
/* ---- The sending side: one output's saved sends to a receiver's input, as
diff --git a/link/test-link.c b/link/test-link.c
index 957c383..0cfd3e4 100644
--- a/link/test-link.c
+++ b/link/test-link.c
@@ -83,6 +83,10 @@ typedef struct {
uint64_t attacker_in, attacker_out, bent, replayed;
unsigned loss, doubled, spread_ms; /* percent, percent, the most a packet is held back */
uint64_t now, sent;
+ /* The path cut (nothing passes) until then: cuts times, 3 s each, the
+ * moment the sender begins an epoch (before the receiver has seen it). */
+ uint64_t cut_until;
+ unsigned cuts;
/* The receiving app: its last heard (kept across the receiver's
* restarts), what it said of deliveries (given at the next turn). */
uint64_t heard, applied, deliveries;
@@ -188,6 +192,7 @@ typedef struct {
bool tamper; /* every packet comes changed three ways first */
uint64_t epoch_ms; /* epochs this long (0: an hour) */
bool forgetful; /* the receiver keeps no memo: it starts again knowing no epoch */
+ unsigned cuts; /* the path cut for 3 s so many times, as an epoch begins */
} Case;
static bool nothing_sent(void *context, const unsigned char *packet, size_t size, const unsigned char *to, size_t to_size) {
@@ -353,7 +358,7 @@ static Outcome run(const Case *c, const char *dir) {
unlink(memo);
World w = {.packets = malloc(PACKETS * sizeof(Packet)), .loss = c->loss, .doubled = c->doubled,
.spread_ms = c->spread_ms, .fail_every = c->fail_every, .replay = c->replay, .tamper = c->tamper,
- .history = malloc(HISTORY * sizeof(Packet))};
+ .cuts = c->cuts, .history = malloc(HISTORY * sizeof(Packet))};
if (!w.packets || !w.history) exit(1);
char from[8] = "sender0";
epoch_ms = c->epoch_ms;
@@ -410,9 +415,19 @@ static Outcome run(const Case *c, const char *dir) {
++k;
continue;
}
+ if (w.now < w.cut_until) { /* the path is cut */
+ *p = w.packets[--w.count];
+ continue;
+ }
if (w.tamper) tamper(&w, s, r, p, from);
if (p->to_receiver) pure_link_receiver_take(r, p->bytes, p->size, from, 7, w.now);
- else pure_link_sender_take(s, p->bytes, p->size, w.now);
+ else {
+ PureLinkCounts before, after;
+ pure_link_sender_counts(s, &before);
+ pure_link_sender_take(s, p->bytes, p->size, w.now);
+ pure_link_sender_counts(s, &after);
+ if (w.cuts && after.epochs > before.epochs) --w.cuts, w.cut_until = w.now + 3000 * MS;
+ }
w.history[w.history_count++ % HISTORY] = *p;
*p = w.packets[--w.count];
}
@@ -442,6 +457,7 @@ static Outcome run(const Case *c, const char *dir) {
pure_link_sender_counts(s, &o.sent);
pure_link_receiver_counts(r, &o.heard);
o.sent.packets += sent_before.packets, o.sent.resent += sent_before.resent, o.sent.epochs += sent_before.epochs;
+ o.sent.fallbacks += sent_before.fallbacks;
o.heard.woken += heard_before.woken, o.heard.swept += heard_before.swept;
CHECK(w.heard == c->records && w.applied == c->records, "%s: applied %llu, heard %llu of %llu", c->name,
(unsigned long long)w.applied, (unsigned long long)w.heard, (unsigned long long)c->records);
@@ -605,7 +621,7 @@ int main(void) {
return 1;
}
/* A clean network: nothing is sent twice. */
- Case clean = {"clean", 3000, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 0, false};
+ Case clean = {"clean", 3000, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 0, false, 0};
Outcome o = run(&clean, dir);
CHECK(o.sent.resent == 0 && o.sent.lost == 0, "clean: %llu sent again, %llu lost", (unsigned long long)o.sent.resent,
(unsigned long long)o.sent.lost);
@@ -614,7 +630,7 @@ int main(void) {
* starting again: the receiver remembers its secret, so it wakes the
* sender without an X25519. */
for (uint64_t s = 1; s <= 4; ++s) {
- Case rough = {"rough", 3000, s * 7919, 10, 3, 20, 37, 1500, 800, 2200, 300, 0, false, 0, false};
+ Case rough = {"rough", 3000, s * 7919, 10, 3, 20, 37, 1500, 800, 2200, 300, 0, false, 0, false, 0};
o = run(&rough, dir);
CHECK(o.sent.resent > 0 && o.heard.swept >= 1 && o.heard.woken >= 1 && o.heard.remembered >= 1,
"rough: sent again %llu, swept %llu, woken %llu, remembered %llu", (unsigned long long)o.sent.resent,
@@ -624,21 +640,29 @@ int main(void) {
/* An attacker on the path: packets changed on the way, and packets of
* the past sent again, from the sender's address and its own, across the
* same restarts and pause. */
- Case changed = {"changed", 1500, 31, 3, 0, 5, 0, 0, 0, 0, 0, 0, true, 0, false};
+ Case changed = {"changed", 1500, 31, 3, 0, 5, 0, 0, 0, 0, 0, 0, true, 0, false, 0};
run(&changed, dir);
for (uint64_t s = 1; s <= 2; ++s) {
- Case again = {"replayed", 3000, s * 104729, 5, 3, 20, 37, 1500, 800, 2200, 300, 25, false, 0, false};
+ Case again = {"replayed", 3000, s * 104729, 5, 3, 20, 37, 1500, 800, 2200, 300, 25, false, 0, false, 0};
run(&again, dir);
}
/* Epochs a fifth of a second long (a new one a second, as the receiver
* accepts), and a receiver that starts again with no memo: it knows no
* epoch, and the sender begins them again. */
- Case epochs = {"epochs", 3000, 271, 5, 3, 20, 37, 1500, 800, 2200, 300, 10, false, 200, false};
+ Case epochs = {"epochs", 3000, 271, 5, 3, 20, 37, 1500, 800, 2200, 300, 10, false, 200, false, 0};
o = run(&epochs, dir);
CHECK(o.sent.epochs >= 10, "epochs: %llu begun", (unsigned long long)o.sent.epochs);
- Case forgetful = {"forgetful", 3000, 409, 5, 3, 20, 37, 0, 800, 0, 0, 0, false, 0, true};
+ Case forgetful = {"forgetful", 3000, 409, 5, 3, 20, 37, 0, 800, 0, 0, 0, false, 0, true, 0};
o = run(&forgetful, dir);
CHECK(o.sent.epochs >= 2, "forgetful: %llu epochs begun (none again)", (unsigned long long)o.sent.epochs);
+ /* The path cut for 3 s the moment the sender begins an epoch, three
+ * times: the receiver never saw the epoch used, and the sender, hearing
+ * nothing, offers another epoch 1 with a new key. The accept of the old
+ * offer must not begin it. */
+ Case cut = {"cut", 3000, 919, 0, 0, 20, 37, 0, 0, 0, 0, 0, false, 200, false, 3};
+ o = run(&cut, dir);
+ CHECK(o.sent.epochs >= 4 && o.sent.fallbacks == 3, "cut: %llu epochs begun, %llu taken for lost (3 cuts)",
+ (unsigned long long)o.sent.epochs, (unsigned long long)o.sent.fallbacks);
impostors(dir);
flood(dir);
char command[600];