tiny.reticulum.node.transport.link.relay
Defined in node.transport.link.
A node running this code passes encrypted sessions that belong to other pairs of nodes through itself.
API (6)
Actions
Public operations.
appliesToRequest: Answers whether an arriving link request is one for this node to pass on, for the receive path to tell a request this node should carry from one it should answer itself.find: Looks a link id up among the relays this node keeps for other pairs, so the receive path tells whether an arriving packet belongs to a session this node carries.proof: Sends the signed link request proof back toward the initiator, as Reticulum@1.5.0 RNS/Transport.py:2535-2600 does.request: Sends an arriving link request on toward its destination and writes the relay that lets the proof and the later traffic find their way, as Reticulum@1.5.0 RNS/Transport.py:1935-1946,1968-2010,2021-2022 does.timerExpired: Clears every relay whose second has come and arms the timer again at the soonest second still ahead, as Reticulum@1.5.0 RNS/Transport.py:843-856 does.traffic: Moves one packet of a relayed link to the side it is bound for, as Reticulum@1.5.0 RNS/Transport.py:2030-2077 does.
Source
Source: lib/reticulum/src/node/transport/link/relay.zig
zig
//! A node running this code passes encrypted sessions that belong to other//! pairs of nodes through itself.//!//! Two programs that want an encrypted session between them may sit many hops//! apart, so the nodes in between have to pass the request outward, the answer//! back, and then every packet of the session in both directions. A node in the//! middle holds none of the session keys and reads none of the traffic.//!//! The node cannot read a session packet, so it decides which way the packet//! goes from the hop count (the byte in the packet header that rises by one at//! every node the packet passes through) and the network interface it arrived//! on. Only the hops byte may change on a carried packet, so the node rewrites//! that byte and leaves the rest of the frame untouched. A session that is//! requested and never answered would hold its record for good, so each record//! carries a deadline of six seconds for every hop that still remains. The//! record store holds a fixed number of records, so a request that arrives with//! the store full is dropped whole, leaving no half-written record.//!//! Reticulum 1.5.0, the reference implementation this package is a port of,//! pinned to one upstream commit, keeps a record per carried session at every//! node that carries traffic for others, keyed by the 16-byte id both ends//! derive from the request. Each record carries a deadline per remaining hop//! for the signed answer, and a sweep drops records past their deadline. This//! port follows that design and cites the reference by file and line//! throughout. An arriving session request goes out one hop nearer its//! destination, and the node writes down what it did, as//! Reticulum@1.5.0 RNS/Transport.py:1968-2010 lays out. The signed answer to//! the request travels back the other way, and the record it answers is marked//! as having passed that answer back toward the end that opened the session,//! following Reticulum@1.5.0 RNS/Transport.py:2535-2600. Every later packet of//! the session moves to whichever side it is bound for, as//! Reticulum@1.5.0 RNS/Transport.py:2030-2077 does. A record leaves the store//! once its clock runs out, following//! Reticulum@1.5.0 RNS/Transport.py:843-856,902. The lineage is checkable in//! this tree: the package README pins the upstream commit the port follows, and//! the conformance corpus under src/conformance is generated from that release//! and drives the differential tests.//!//! This node passes each encrypted session between two endpoints, named by a//! 16-byte id that both ends derive from the request packet (a *link*). For//! each link that crosses it, the node keeps a record holding both network//! interfaces, the hop count in each direction, and the deadline by which the//! signed answer must arrive (a *relay*). Each request waits for a signed//! packet returned to confirm an earlier packet, which for a link request is//! the receiving end accepting the link (a *proof*).//!//! The first departure concerns frame size. A link request or its proof may//! carry three bytes past its fixed body (*signalling bytes*). Every network//! interface in this package leaves its frame size unstated, so the node takes//! the signalling bytes off each request it passes on and resizes nothing. A//! network interface at Reticulum@1.5.0 RNS/Transport.py:1977-1996 may state a//! frame size, and a relay there resizes a request toward any interface that//! states one.//!//! The second departure concerns how a refusal reaches the caller. This node//! reports each refusal in a record the step appends, naming why it turned the//! packet away (a *diagnostic*). These are `link_relay_early` for a packet on a//! relay whose proof has yet to pass, `link_relay_no_direction` for one whose//! hop count fits neither side, and `link_entries_full` for a request that//! arrives with the store full. Reticulum@1.5.0 RNS/Transport.py:2036-2037,2074//! writes a warning to a log or raises a protocol violation for those same//! three cases.//!//! The third departure concerns when a relay leaves the store. This node asks//! its caller to wake it at the soonest deadline any relay holds (a *node//! timer*), so each relay goes at the second it named.//! Reticulum@1.5.0 RNS/Transport.py:245,843-856 wakes once a second and clears//! every relay whose deadline has passed by then.//!//! The fourth departure concerns what a dead relay says about its path. This//! node keeps what it learned from a destination's broadcast about reaching//! that destination: the network interface to send on, the neighbor to name as//! the next hop, and how many hops away the destination sits (a *path*). A//! relay that goes without its proof leaves the path to its destination marked//! as failed at once, which lets a farther broadcast take the path over, and//! this node queues no path request.//! Reticulum@1.5.0 RNS/Transport.py:858-865,904-926 marks the path only once//! enough time has passed since it last asked after one, an instant this node//! keeps no record of while it carries traffic for others.//!//! The fifth departure concerns a repeated request. A link request whose id//! already sits in the store leaves every relay standing and reports//! `link_request_duplicate`. Reticulum@1.5.0 RNS/Transport.py:2009 writes the//! new relay over the one already there.//!//! - *link id*: the 16-byte name of one link, the same at every node the link//! crosses.//! - *carrier*: one network interface the node sends and receives over, named//! by a small integer index.//! - *destination*: the 16-byte hash that names one addressable endpoint on the//! network.//! - *initiator*: the end of a link that sent the request.//! - *responder*: the end of a link the request was sent to.//! - *transport node*: a node that carries traffic on behalf of other nodes, as//! well as sending and receiving its own.//! - *validated*: the mark a relay takes once this node has passed the//! answering proof back toward the initiator.//! - *unresponsive*: the mark a path carries once something that used it//! failed, which lets a farther announce take the path over.//! - *announce*: a packet carrying a destination's public keys and a signature,//! so any node that hears it learns that destination and how far away it//! sits.const std = @import("std");const reticulum = @import("../../../root.zig");const node = reticulum.node;const packet = reticulum.packet;const wire = reticulum.wire;const link_entries = node.transport.link.entries;const Ed25519 = std.crypto.sign.Ed25519;/// Sets how long a forwarded request may stay unanswered before its relay is/// dropped: six seconds for each hop that still remains, the allowance/// Reticulum@1.5.0 RNS/Link.py:96 gives a request proof.const per_hop_timeout: node.Seconds = 6;/// Answers whether an arriving link request is one for this node to pass on,/// for the receive path to tell a request this node should carry from one it/// should answer itself. That holds when the node carries traffic for others,/// the packet names a node it is crossing, and the destination belongs to no/// address of this node.pub fn appliesToRequest(node_owner: *node.Node, value: wire.Packet) bool { std.debug.assert(value.packet_type == .link_request); if (!node_owner.transport.enabled) return false; if (value.transport_id == null) return false; return node_owner.destinations.find(value.destination) == null;}/// Looks a link id up among the relays this node keeps for other pairs, so the/// receive path tells whether an arriving packet belongs to a session this node/// carries. A node that carries no traffic for others gives null.pub fn find(node_owner: *node.Node, link_id: [16]u8) ?*link_entries.Entry { if (!node_owner.transport.enabled) return null; return node_owner.transport.link_entries.find(link_id);}fn strippedRequest(frame: []const u8, signalled: bool) []const u8 { if (!signalled) return frame; std.debug.assert(frame.len > wire.link.signalling_bytes); return frame[0 .. frame.len - wire.link.signalling_bytes];}fn arm(node_owner: *node.Node) node.StepError!void { const soonest = node_owner.transport.link_entries.earliest() orelse { _ = node_owner.timers.cancel(.link_entries); return; }; if (node_owner.timers.scheduledAt(.link_entries)) |at| { if (at == soonest) return; } node_owner.timers.schedule(.link_entries, soonest) catch return error.TimerFull; node_owner.effects.push(.{ .schedule_timer = .{ .id = .link_entries, .at = soonest, } }) catch return error.EffectsFull;}/// Sends an arriving link request on toward its destination and writes the/// relay that lets the proof and the later traffic find their way, as/// Reticulum@1.5.0 RNS/Transport.py:1935-1946,1968-2010,2021-2022 does. What/// leaves has the three signalling bytes taken off, and the link id the/// initiator computed travels on unchanged. Six seconds for each hop still/// between this node and the destination sets the deadline the relay holds its/// proof to. A destination this node holds no path to reports `no_path`. A/// store already holding `link_entries_max` relays reports `link_entries_full`,/// and every relay in it stands.pub fn request( node_owner: *node.Node, value: wire.Packet, raw: []const u8, frame: node.CarrierFrame, hash: packet.Hash,) node.StepError!void { std.debug.assert(appliesToRequest(node_owner, value)); const link_id = wire.link.linkId(raw) catch unreachable; const path = node_owner.transport.paths.find(value.destination, frame.now) orelse return node.inbound.diagnostic(node_owner, .no_path, hash); std.debug.assert(path.hops >= 1); if (node_owner.transport.link_entries.find(link_id) != null) { return node.inbound.diagnostic(node_owner, .link_request_duplicate, hash); } if (node_owner.transport.link_entries.full()) { return node.inbound.diagnostic(node_owner, .link_entries_full, hash); } const fanout: node.outbound.Fanout = .{ .one = path.carrier }; const carriers = node.outbound.frameCount(node_owner, fanout); if (carriers == 0) return node.inbound.diagnostic(node_owner, .no_path, hash); if (!node_owner.timers.canScheduleAfterCancel(.link_entries, null)) return error.TimerFull; try node.outbound.reserve(node_owner, carriers + 1, carriers); const rewritten = if (path.hops > 1) node.transport.rewrite.forward(raw, value.hops, path.next_hop, node_owner.scratch) else node.transport.rewrite.strip(raw, value.hops, node_owner.scratch); const signalled = value.payload.len == wire.link.signalled_request_bytes; const forwarded = strippedRequest(rewritten, signalled); const deadline = frame.now +| per_hop_timeout * @as(node.Seconds, @max(1, path.hops)); _ = node_owner.transport.link_entries.insert(.{ .link_id = link_id, .destination = value.destination, .next_hop = path.next_hop, .timestamp = frame.now, .proof_deadline = deadline, .next_hop_carrier = path.carrier, .receiving_carrier = frame.interface, .remaining_hops = path.hops, .taken_hops = value.hops, .validated = false, }) catch unreachable; path.timestamp = frame.now; try arm(node_owner); try node.outbound.transmit(node_owner, fanout, forwarded);}fn direction(entry: *const link_entries.Entry, value: wire.Packet, received: u8) ?u8 { if (entry.next_hop_carrier == entry.receiving_carrier) { if (value.hops == entry.remaining_hops or value.hops == entry.taken_hops) { return entry.next_hop_carrier; } return null; } if (received == entry.next_hop_carrier and value.hops == entry.remaining_hops) { return entry.receiving_carrier; } if (received == entry.receiving_carrier and value.hops == entry.taken_hops) { return entry.next_hop_carrier; } return null;}/// Moves one packet of a relayed link to the side it is bound for, as/// Reticulum@1.5.0 RNS/Transport.py:2030-2077 does. Which side that is follows/// from the packet hop count together with the carrier it came in on. The hop/// count is the one byte the node writes over. A packet is marked seen only/// after the node has a side to send it to, so one overheard on a carrier both/// sides share still moves. A packet arriving on a relay whose proof has yet to/// pass reports `link_relay_early`.pub fn traffic( node_owner: *node.Node, entry: *link_entries.Entry, value: wire.Packet, raw: []const u8, frame: node.CarrierFrame, hash: packet.Hash,) node.StepError!void { std.debug.assert(node_owner.transport.enabled); if (!entry.validated) { return node.inbound.diagnostic(node_owner, .link_relay_early, hash); } const outgoing = direction(entry, value, frame.interface) orelse { return node.inbound.diagnostic(node_owner, .link_relay_no_direction, hash); }; const fanout: node.outbound.Fanout = .{ .one = outgoing }; const carriers = node.outbound.frameCount(node_owner, fanout); if (carriers == 0) return node.inbound.diagnostic(node_owner, .no_path, hash); if (!node_owner.timers.canScheduleAfterCancel(.link_entries, null)) return error.TimerFull; try node.outbound.reserve(node_owner, carriers + 1, carriers); _ = node_owner.duplicate_hashes.insert(hash); const forwarded = node.transport.rewrite.rehop(raw, value.hops, node_owner.scratch); entry.timestamp = frame.now; try arm(node_owner); try node.outbound.transmit(node_owner, fanout, forwarded);}fn validates(node_owner: *node.Node, entry: *const link_entries.Entry, payload: []const u8) bool { const known = node_owner.known_identities.recall(entry.destination) orelse return false; const decoded = wire.link.Proof.decode(payload) catch return false; var signed: [wire.link.signed_proof_bytes_max]u8 = undefined; const message = wire.link.signedProof( entry.link_id, decoded.encryption_public, known.public_key[32..64].*, decoded.signalling, &signed, ); const signer = Ed25519.PublicKey.fromBytes(known.public_key[32..64].*) catch return false; Ed25519.Signature.fromBytes(decoded.signature).verify(message, signer) catch return false; return true;}/// Sends the signed link request proof back toward the initiator, as/// Reticulum@1.5.0 RNS/Transport.py:2535-2600 does. A proof arriving at a/// different hop count than the relay expects, whose signature checks out,/// first resets the relay and the path to the distance that proof traveled, as/// Reticulum@1.5.0 RNS/Transport.py:2540-2562 does. A proof that arrives at the/// expected hop count on the carrier the path leads out over, and checks out/// against the identity this node recalls for the destination, marks the relay/// validated and goes back over the carrier the request came in on. The relay/// timestamp stays at the second its request arrived, so one that is validated/// and then carries nothing runs out 900 seconds after that second. Any other/// proof is named in a diagnostic and goes no further. Nothing moves until the/// last thing that could refuse has been checked, so a proof that finds no/// carrier out, no free timer slot, or no room among the effects touches/// neither the relay nor the path.pub fn proof( node_owner: *node.Node, entry: *link_entries.Entry, value: wire.Packet, raw: []const u8, frame: node.CarrierFrame, hash: packet.Hash,) node.StepError!void { std.debug.assert(node_owner.transport.enabled); std.debug.assert(value.context == .lrproof); const from_next_hop = frame.interface == entry.next_hop_carrier; const rebalances = value.hops != entry.remaining_hops and from_next_hop and !entry.validated and validates(node_owner, entry, value.payload); if (!rebalances and value.hops != entry.remaining_hops) { return node.inbound.diagnostic(node_owner, .link_relay_no_direction, hash); } if (!from_next_hop) { return node.inbound.diagnostic(node_owner, .proof_relay_wrong_interface, hash); } if (!rebalances and !validates(node_owner, entry, value.payload)) { return node.inbound.diagnostic(node_owner, .proof_rejected, hash); } const fanout: node.outbound.Fanout = .{ .one = entry.receiving_carrier }; const carriers = node.outbound.frameCount(node_owner, fanout); if (carriers == 0) return node.inbound.diagnostic(node_owner, .no_path, hash); if (!node_owner.timers.canScheduleAfterCancel(.link_entries, null)) return error.TimerFull; try node.outbound.reserve(node_owner, carriers + 1, carriers); const forwarded = node.transport.rewrite.rehop(raw, value.hops, node_owner.scratch); if (rebalances) { entry.remaining_hops = value.hops; if (node_owner.transport.paths.find(entry.destination, frame.now)) |path| { path.hops = value.hops; } } entry.validated = true; try arm(node_owner); try node.outbound.transmit(node_owner, fanout, forwarded);}/// Answers whether a relay leaving the store should cast doubt on the path it/// went out over. Reticulum@1.5.0 RNS/Transport.py:886-889,908-915 casts that/// doubt when either the destination or the initiator sat a single hop off, on/// the reading that a silent neighbor has moved.fn marksUnresponsive( node_owner: *node.Node, entry: *const link_entries.Entry, now: node.Seconds,) bool { if (entry.validated) return false; if (entry.taken_hops == 1) return true; return node_owner.transport.paths.hopsTo(entry.destination, now) == 1;}/// Clears every relay whose second has come and arms the timer again at the/// soonest second still ahead, as Reticulum@1.5.0 RNS/Transport.py:843-856/// does. A relay that went without its proof leaves its destination path marked/// unresponsive, which lets the next announce stamped at the same emission take/// that path over, as Reticulum@1.5.0 RNS/Transport.py:902,2208-2212 arranges.pub fn timerExpired(node_owner: *node.Node, value: node.TimerExpired) node.StepError!void { std.debug.assert(value.id == .link_entries); try node.outbound.reserve(node_owner, 1, 0); _ = node_owner.timers.cancel(value.id); while (node_owner.transport.link_entries.expired(value.now)) |entry| { const destination = entry.destination; const marks = marksUnresponsive(node_owner, entry, value.now); const removed = node_owner.transport.link_entries.remove(entry.link_id); std.debug.assert(removed); if (!marks) continue; _ = node_owner.transport.paths.markUnresponsive(destination, value.now); } try arm(node_owner);}Source: lib/reticulum/src/node/transport/link/root.zig:48
zig
pub const relay = @import("relay.zig");Complete call list for node.transport.link.relay.proof
7 direct calls.
tiny.reticulum.node.inbound.diagnostic[function] atlib/reticulum/src/node/inbound.zig:12tiny.reticulum.node.outbound.frameCount[function] atlib/reticulum/src/node/outbound.zig:175tiny.reticulum.node.outbound.reserve[function] atlib/reticulum/src/node/outbound.zig:184tiny.reticulum.node.outbound.transmit[function] atlib/reticulum/src/node/outbound.zig:213lib.reticulum.src.node.transport.link.relay.arm[function] — private source atlib/reticulum/src/node/transport/link/relay.zig:145in nearest public ownertiny.reticulum.node.transport.link.relaylib.reticulum.src.node.transport.link.relay.validates[function] — private source atlib/reticulum/src/node/transport/link/relay.zig:266in nearest public ownertiny.reticulum.node.transport.link.relaytiny.reticulum.node.transport.rewrite.rehop[function] atlib/reticulum/src/node/transport/rewrite.zig:68
Complete call list for node.transport.link.relay.request
10 direct calls.
tiny.reticulum.node.inbound.diagnostic[function] atlib/reticulum/src/node/inbound.zig:12tiny.reticulum.node.outbound.frameCount[function] atlib/reticulum/src/node/outbound.zig:175tiny.reticulum.node.outbound.reserve[function] atlib/reticulum/src/node/outbound.zig:184tiny.reticulum.node.outbound.transmit[function] atlib/reticulum/src/node/outbound.zig:213tiny.reticulum.node.transport.link.relay.appliesToRequest[function] atlib/reticulum/src/node/transport/link/relay.zig:124lib.reticulum.src.node.transport.link.relay.arm[function] — private source atlib/reticulum/src/node/transport/link/relay.zig:145in nearest public ownertiny.reticulum.node.transport.link.relaylib.reticulum.src.node.transport.link.relay.strippedRequest[function] — private source atlib/reticulum/src/node/transport/link/relay.zig:139in nearest public ownertiny.reticulum.node.transport.link.relaytiny.reticulum.node.transport.rewrite.forward[function] atlib/reticulum/src/node/transport/rewrite.zig:36tiny.reticulum.node.transport.rewrite.strip[function] atlib/reticulum/src/node/transport/rewrite.zig:52tiny.reticulum.wire.link.linkId[function] atlib/reticulum/src/wire/link.zig:265
Complete call list for node.transport.link.relay.traffic
7 direct calls.
tiny.reticulum.node.inbound.diagnostic[function] atlib/reticulum/src/node/inbound.zig:12tiny.reticulum.node.outbound.frameCount[function] atlib/reticulum/src/node/outbound.zig:175tiny.reticulum.node.outbound.reserve[function] atlib/reticulum/src/node/outbound.zig:184tiny.reticulum.node.outbound.transmit[function] atlib/reticulum/src/node/outbound.zig:213lib.reticulum.src.node.transport.link.relay.arm[function] — private source atlib/reticulum/src/node/transport/link/relay.zig:145in nearest public ownertiny.reticulum.node.transport.link.relaylib.reticulum.src.node.transport.link.relay.direction[function] — private source atlib/reticulum/src/node/transport/link/relay.zig:216in nearest public ownertiny.reticulum.node.transport.link.relaytiny.reticulum.node.transport.rewrite.rehop[function] atlib/reticulum/src/node/transport/rewrite.zig:68
Audit
| Definitions | 7 |
|---|---|
| Public names | 7 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |