aboutsummaryrefslogtreecommitdiffstats
path: root/link/link-protocol.h
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-29 07:17:36 +0100
committereunhhu <[email protected]>2026-09-29 07:17:36 +0100
commit7c6595c8a9229a2208ed5bd75b2dee249187923d (patch)
tree93997bffc769e645ace36519e5948553dddff833 /link/link-protocol.h
parentfcf7cdbe0a971825da0bb087383c60e714ab2be0 (diff)
downloadpure-7c6595c8a9229a2208ed5bd75b2dee249187923d.tar.gz
pure-7c6595c8a9229a2208ed5bd75b2dee249187923d.zip
Keep a link's two sides as state alone, the sockets and threads apart
link/link-protocol.h is the protocol with no socket, thread or clock of its own: packets and time in, packets out. link/link.h carries it over UDP as before. A sender starts small (8 flights, 16 records, a 4 KiB ring) so one thread can hold many. The transport's CPU a record is the same within 2% (five rounds each way, 61 B to 16 KiB). test-link runs both sides through a network of its own: loss, packets held back and doubled, failed saves, an idle sweep, and both sides starting again, every record applied once as sent.
Diffstat (limited to 'link/link-protocol.h')
-rw-r--r--link/link-protocol.h160
1 files changed, 160 insertions, 0 deletions
diff --git a/link/link-protocol.h b/link/link-protocol.h
new file mode 100644
index 0000000..b0f8ef3
--- /dev/null
+++ b/link/link-protocol.h
@@ -0,0 +1,160 @@
+#ifndef PURE_LINK_PROTOCOL_H
+#define PURE_LINK_PROTOCOL_H
+#include "seal.h"
+
+/* Links without connections (docs/design/011-pure-transport.md), as state
+ * alone: no socket, thread or clock of their own. The owner gives each side
+ * the packets that came and the time (ns, monotonic), and sends the packets
+ * each side makes; link/link.h is such an owner over UDP. So one thread may
+ * carry many links, and a test may carry both sides through a network of its
+ * own making.
+ *
+ * A link's lasting state is the apps' own: the sender's outbox (what to send
+ * and send again) and the receiver's heard numbers (what it applied, saved
+ * with the events). What either side keeps in memory is a cache, so either
+ * may restart and the other goes on with the next packet.
+ *
+ * A packet is a header in the clear, then frames sealed with AES-128-GCM
+ * (the header as associated data):
+ * u8 0x70 (version 0), u32le the key id of the node that sent it (FNV-1a
+ * of its public key; two keys may share an id, and a receiver tries the
+ * keys it allows with it), u64le that node's run (random, new each time it
+ * starts, so no nonce repeats under a key across restarts), u64le the
+ * packet's number in its run.
+ * Keys come from the secret the two nodes' keys share (X25519) by
+ * HKDF-SHA256, with both public keys and the sending node's run: a sender's
+ * packets use its own run; a receiver's answers add the run they answer,
+ * so an answer opens only in that run. The nonce is the key's base with the
+ * packet's number in its last eight bytes (XOR, big-endian, as QUIC does).
+ *
+ * Frames (numbers little-endian):
+ * 0x01 STREAM source (u8 length, bytes), input (u8 length, bytes), u32 the
+ * shape of the messages (the sender's output and the
+ * receiver's input must have the same): the stream the
+ * records after it are of
+ * 0x02 RECORD u64 number, u32 size, u32 offset, u16 length, bytes: a piece
+ * of a saved send (pieces are 1000 bytes, the last shorter)
+ * 0x03 ACK u64 largest number, u32 delay (µs), u8 ranges, each as u64
+ * first and u64 last, newest first
+ * 0x04 STORED source, input (as STREAM), u64 the last number heard
+ * (applied and saved), u8 flags (1: a record after a gap was
+ * not taken: send what follows it again), u32 credit (records
+ * it takes past that)
+ * 0x05 PING to be acknowledged
+ * 0x06 RECORDS u64 the first one's number, u8 count, then whole records one
+ * after another (numbers following on), each its size (a
+ * varint: LEB128) and its bytes: a send that fits the packet
+ * whole, with 1 to 3 bytes of framing, not the 19 of RECORD
+ * 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; congestion control is its NewReno
+ * (RFC 9002 §7). */
+
+/* A packet is at most PURE_LINK_DATAGRAM bytes; a source's or an input's
+ * name is 1 to PURE_LINK_NAME bytes; a receiver keeps where a packet came
+ * from (the socket's address) in PURE_LINK_ADDRESS bytes. */
+enum { PURE_LINK_DATAGRAM = 1200, PURE_LINK_NAME = 64, PURE_LINK_ADDRESS = 28 };
+
+/* Counts for a host's report: packets and records sent, records sent again,
+ * packets lost and acknowledged, packets received, deliveries handed over,
+ * the last number stored; 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, woken, remembered, swept, active;
+} PureLinkCounts;
+
+/* ---- The sending side: one output's saved sends to a receiver's input, as
+ * source (the receiver's name for it), their messages of shape (the
+ * receiver's input must have the same: core/module.h's pure_module_shape),
+ * from own (this node's key, whose public half is own_public). NULL when
+ * the names are not 1 to PURE_LINK_NAME bytes, or memory runs out. */
+typedef struct PureLinkSender PureLinkSender;
+PureLinkSender *pure_link_sender(const char *source, const char *input, uint32_t shape, const PureSealKey *own,
+ const unsigned char own_public[PURE_SEAL_KEY]);
+/* The receiver's public key: the keys made from it (one X25519). A sender
+ * makes no packet before it has one. */
+bool pure_link_sender_peer(PureLinkSender *, const unsigned char peer[PURE_SEAL_KEY]);
+/* A saved send, numbered one past the last queued (any number when none
+ * waits); false when it cannot be kept. */
+bool pure_link_sender_queue(PureLinkSender *, uint64_t seq, const unsigned char *bytes, size_t size);
+/* A packet that came (the receiver's answers; others are let be). */
+void pure_link_sender_take(PureLinkSender *, const unsigned char *packet, size_t size, uint64_t now);
+/* The timers due by now: losses found, probes, records sent again. */
+void pure_link_sender_tick(PureLinkSender *, uint64_t now);
+/* The next packet to send into out, and its number: its size, or 0 when
+ * there is none (nothing to send, or the window is full). whole: a full
+ * packet is padded to PURE_LINK_DATAGRAM (so packets may go in a burst of
+ * whole datagrams, as UDP GSO sends them). */
+size_t pure_link_sender_packet(PureLinkSender *, unsigned char out[PURE_LINK_DATAGRAM], bool whole, uint64_t now,
+ uint64_t *number);
+/* A packet made but not sent (the socket refused it): what it carried is
+ * sent again, and it counts as never sent. */
+void pure_link_sender_refused(PureLinkSender *, uint64_t number);
+/* When the next timer is due (ns), or 0 for none. */
+uint64_t pure_link_sender_due(const PureLinkSender *);
+/* The last number queued; the last the receiver stored. */
+uint64_t pure_link_sender_queued(const PureLinkSender *);
+uint64_t pure_link_sender_stored(const PureLinkSender *);
+void pure_link_sender_counts(const PureLinkSender *, PureLinkCounts *);
+void pure_link_sender_free(PureLinkSender *);
+
+/* ---- The receiving side: packets from the senders allowed (a source name
+ * and the key it seals with, a few bytes each however many there are: see
+ * pure_link_idle). A stream (a source to an input) goes to the host's input
+ * its name and shape find (its number, or -1: the stream is refused). Whole
+ * records of a stream, in order, go to deliver as one delivery; the host
+ * applies them from the one after the source's last heard number, stops at
+ * a gap, saves, and then says so with a done (pure_link_receiver_turn).
+ * Records wait here until then, and only the saved ones are acknowledged as
+ * stored. */
+typedef struct {
+ uint32_t input;
+ const char *source;
+ uint64_t first;
+ unsigned count;
+ const unsigned char *records; /* count of: u32le size, bytes */
+ size_t size;
+ void *token; /* for its done */
+} PureLinkDelivery;
+typedef struct {
+ int (*input)(void *context, const char *name, uint32_t shape);
+ bool (*deliver)(void *context, const PureLinkDelivery *);
+ void *context;
+} PureLinkHost;
+/* A delivery's end: heard, the last number heard; stored, its save
+ * succeeded (else it is handed over again); gap, it stopped at a gap. */
+typedef struct { void *token; uint64_t heard; bool stored, gap; } PureLinkDone;
+/* A packet to send to an address a packet came from: whether it went. */
+typedef bool PureLinkSend(void *context, const unsigned char *packet, size_t size, const unsigned char *to,
+ size_t to_size);
+
+typedef struct PureLinkReceiver PureLinkReceiver;
+PureLinkReceiver *pure_link_receiver(const PureSealKey *own, const unsigned char own_public[PURE_SEAL_KEY],
+ const PureLinkHost *host, char *error, size_t size);
+bool pure_link_allow(PureLinkReceiver *, const char *source, const unsigned char key[PURE_SEAL_KEY], char *error,
+ size_t size);
+/* A sender quiet for ms (30 s at first) with nothing of it waiting lets its
+ * keys, addresses and streams go: an idle link costs the receiver only its
+ * allowing, and the sender's next packet makes them again (one X25519). */
+void pure_link_idle(PureLinkReceiver *, uint64_t ms);
+/* Remember the secret each sender's key shares with the node's in a file
+ * (path, 0600; begun again for another node's key): a place a sender in
+ * the order allowed, mapped, so a sender at rest takes no memory for it.
+ * A node that starts again then makes no X25519 for a sender it heard from
+ * before. After the senders are allowed, before the first packet. */
+bool pure_link_remember(PureLinkReceiver *, const char *path, char *error, size_t size);
+/* A packet that came from an address (from_size bytes, at most
+ * PURE_LINK_ADDRESS kept): answers to its sender go there. */
+void pure_link_receiver_take(PureLinkReceiver *, const unsigned char *packet, size_t size, const void *from,
+ size_t from_size, uint64_t now);
+/* After the packets that came: the dones (count of them) applied, whole
+ * records handed over, answers sent through send, and senders at rest let
+ * go. */
+void pure_link_receiver_turn(PureLinkReceiver *, const PureLinkDone *done, size_t count, uint64_t now,
+ PureLinkSend *send, void *context);
+/* When a turn is due with no packet (ns), or 0 for none. */
+uint64_t pure_link_receiver_due(const PureLinkReceiver *);
+void pure_link_receiver_counts(const PureLinkReceiver *, PureLinkCounts *);
+void pure_link_receiver_free(PureLinkReceiver *);
+#endif