Skip to documentation
SLOP

tiny.reticulum.node.transport.link.relay

Reference 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.

No direct callersNo direct callsnode.transport.linkrelay
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callsprivate sourcelib.reticulum.src.node.linkrouteRequestnode.transport.link.relayrequestnode.transport.link.relayappliesToRequest
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callsnode.inboundrunprivate sourcelib.reticulum.src.node.linkrouteLinkPacketprivate sourcelib.reticulum.src.node.linkrouteProofnode.transport.link.relayfind
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callersnode.inbounddiagnosticnode.outboundframeCountnode.outboundreservenode.outboundtransmitprivate sourcelib.reticulum.src.node.transport.link.relayarm+2 morenode.transport.link.relayproof
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsprivate sourcelib.reticulum.src.node.linkrouteRequestnode.inbounddiagnosticnode.outboundframeCountnode.outboundreservenode.outboundtransmitnode.transport.link.relayappliesToRequest+5 morenode.transport.link.relayrequest
Static calls · unresolved targets: 0 · external targets: 5.
Called byCallsnode.inboundtimerExpirednode.outboundreserveprivate sourcelib.reticulum.src.node.transport.link.relayarmprivate sourcelib.reticulum.src.node.transport.link.relaymarksUnresponsivenode.transport.link.relaytimerExpired
Static calls · unresolved targets: 0 · external targets: 4.
Called byCallsprivate sourcelib.reticulum.src.node.linkrouteLinkPacketnode.inbounddiagnosticnode.outboundframeCountnode.outboundreservenode.outboundtransmitprivate sourcelib.reticulum.src.node.transport.link.relayarm+2 morenode.transport.link.relaytraffic
Static calls · unresolved targets: 0 · external targets: 2.

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.

Complete call list for node.transport.link.relay.request

10 direct calls.

Complete call list for node.transport.link.relay.traffic

7 direct calls.

Audit

Definitions7
Public names7
Members0
Version26.7.0
Revisiondaab053ee433