Terminus or forward — one test decides (L4 routing)

A node does not classify traffic into “local” and “remote”, and a FWD carries no flag saying which it is. Every inbound frame is put to the same question: is the leading dst route segment one of my registered children? Yes ⇒ forward it. No ⇒ I am the terminus — resolve the whole remaining dst as a local address and apply the op (RFC-0004 §D, ADR-0035).

The consequence worth internalising: the same bytes are a forward at one node and a terminus at the next, decided entirely by each node’s own child table. /sensor/temp is a route while a child is called sensor, and an address the moment that child is gone. Addressing and routing share one namespace on purpose (CONTEXT.md §Path-as-route).

What to notice

  • The example wires the ambiguity deliberately. One node holds a child named b and a local vertex at /sensor/temp, and both arms are exercised against it — so the forward arm proves the local vertex was not touched, and the terminus arm proves it can be.

  • The terminus failure is ADDRESSED, not dropped. A dst that resolves to no local vertex answers FWD{REPLY} with kind=ERROR, source-routed home along the src the request accumulated. A forwarder that silently swallowed unroutable frames would turn every addressing typo into a timeout instead of a status (RFC-0002’s model, carried on the FWD plane).

  • The reply goes out on the link the request arrived on. That is the per-hop retrace, not a lookup — the terminus does not have to know where the origin is, only which link spoke to it.

  • Nothing about the frame is hop-aware. No TTL, no hop count, no visited set. dst strictly shrinks per hop, which is what makes both planes loop-free by construction (CONTEXT.md §Loop freedom).

  • This target needs the FWD net plane. It is built only when LIBTRACER_NET_PLANE is on (the default). Nothing in it is conditional at run time.

Source

  1/*
  2 * SPDX-License-Identifier: Apache-2.0
  3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
  4 */
  5
  6/**
  7 * @file
  8 * @brief ONE CONCEPT — one test decides a hop's whole behaviour: does the leading `dst`
  9 *        route segment name a CHILD?
 10 *
 11 * A node does not classify frames into "local" and "remote" traffic, and a `dst` carries no
 12 * flag saying which it is. Every inbound `FWD` is put to the same question — is the leading
 13 * `dst` route segment one of my registered children? Yes ⇒ forward it (dst-shrink / src-grow, see
 14 * route_dst_is_source_route). No ⇒ **I am the terminus**: resolve the whole remaining `dst` as
 15 * a local address and apply the op (RFC-0004 §D, ADR-0035).
 16 *
 17 * The consequence worth internalising: the SAME bytes are a forward at one node and a terminus
 18 * at the next, decided entirely by each node's own child table. `/sensor/temp` is a route while
 19 * a child is called `sensor`, and an address the moment that child is gone. Addressing and
 20 * routing share one namespace on purpose (`CONTEXT.md` §Path-as-route).
 21 *
 22 * The terminus arm also has a failure mode that is NOT a drop: a `dst` that resolves to no
 23 * local vertex answers `FWD{REPLY}` with `kind=ERROR`, source-routed home along the `src` the
 24 * request accumulated. A forwarder that silently swallowed unroutable frames would make every
 25 * addressing typo a timeout instead of an error.
 26 *
 27 * Runs under ctest as `example_route_terminus_or_forward`; returns non-zero on any failed check.
 28 */
 29
 30#include <cstddef>
 31#include <cstdint>
 32#include <cstdio>
 33#include <initializer_list>
 34#include <iterator>
 35#include <optional>
 36#include <span>
 37#include <string_view>
 38#include <vector>
 39
 40#include "libtracer/fwd_router.hpp"
 41#include "libtracer/tlv_emit.hpp"
 42#include "libtracer/tracer.hpp"
 43
 44namespace {
 45
 46using tr::graph::fwd_op_t;
 47using tr::graph::graph_t;
 48using tr::graph::path_t;
 49using tr::graph::reply_kind_t;
 50using tr::graph::role_t;
 51using tr::wire::opt_t;
 52using tr::wire::type_t;
 53
 54/** @brief Report expectation @p what and record a failure on @p ok. */
 55void check(bool& ok, bool cond, const char* what) {
 56    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
 57    ok = ok && cond;
 58}
 59
 60/** @brief A `transport_t` that keeps every frame handed to it — the "wire", made inspectable. */
 61struct recording_link_t : tr::net::transport_t {
 62    std::vector<std::vector<std::byte>> sent; /**< @brief Frames emitted on this link, in order. */
 63    void send(std::span<const std::byte> frame) override {
 64        sent.emplace_back(frame.begin(), frame.end());
 65    }
 66    void send(std::span<const std::span<const std::byte>> iov) override {
 67        std::vector<std::byte> flat;
 68        for (const auto part : iov) flat.insert(flat.end(), part.begin(), part.end());
 69        sent.push_back(std::move(flat));
 70    }
 71};
 72
 73/** @brief A `PATH` TLV over @p segs — RFC-0018 packed segment records, `opt.PL` clear. */
 74std::vector<std::byte> path_tlv(std::initializer_list<std::string_view> segs) {
 75    std::vector<std::byte> body;
 76    for (std::string_view s : segs) (void)tr::wire::emit_path_segment(body, s);
 77    std::vector<std::byte> out;
 78    tr::wire::emit_tlv(out, type_t::PATH, opt_t{}, body);
 79    return out;
 80}
 81
 82/** @brief A one-byte `VALUE` TLV carrying @p v. */
 83std::vector<std::byte> value_tlv(std::uint8_t v) {
 84    const std::byte b{v};
 85    std::vector<std::byte> out;
 86    tr::wire::emit_tlv(out, type_t::VALUE, opt_t{}, std::span<const std::byte>(&b, 1));
 87    return out;
 88}
 89
 90/** @brief `FWD[.pl]{ VALUE op, PATH dst, PATH src, VALUE payload }` (RFC-0004 §B child order). */
 91std::vector<std::byte> fwd_write(std::initializer_list<std::string_view> dst,
 92                                 std::initializer_list<std::string_view> src) {
 93    std::vector<std::byte> body = value_tlv(static_cast<std::uint8_t>(fwd_op_t::WRITE));
 94    for (const auto& part : {path_tlv(dst), path_tlv(src), value_tlv(0x2A)}) {
 95        body.insert(body.end(), part.begin(), part.end());
 96    }
 97    std::vector<std::byte> out;
 98    tr::wire::emit_tlv(out, type_t::FWD, opt_t{.pl = true}, body);
 99    return out;
100}
101
102/** @brief The `kind` of a `FWD{REPLY}` frame, or `std::nullopt` if @p frame is not one. */
103std::optional<reply_kind_t> reply_kind(std::span<const std::byte> frame) {
104    const auto tlv = tr::wire::tlv_node_t::over(frame);
105    if (!tlv) return std::nullopt;
106    // Child order is `VALUE op, PATH dst, PATH src, VALUE kind, …` — the kind is child 3.
107    const tr::wire::tlv_children_t kids = tlv->children();
108    const auto kind = std::ranges::next(kids.begin(), 3, kids.end());
109    if (kind == kids.end() || (*kind).payload().size() != 1) return std::nullopt;
110    return static_cast<reply_kind_t>((*kind).payload()[0]);
111}
112
113}  // namespace
114
115int main() {
116    bool ok = true;
117    graph_t g;
118    tr::net::fwd_router_t router(g);
119
120    // One child, named "b"; one local vertex, at /sensor/temp. Both spellings below are
121    // ordinary paths — nothing marks one of them as "remote".
122    recording_link_t to_b, to_client;
123    if (!router.add_child("b", to_b) || !router.add_child("cli", to_client)) {
124        std::fprintf(stderr, "route_terminus_or_forward: add_child failed — nothing registered\n");
125        return 1;
126    }
127    (void)g.register_vertex(path_t("/sensor/temp"), role_t::STORED_VALUE);
128
129    // Arm 1 — the leading route segment names a child: FORWARD. Nothing is written here.
130    router.on_frame("cli", fwd_write({"b", "sensor", "temp"}, {}));
131    check(ok, to_b.sent.size() == 1, "\"b\" names a child, so the frame left on that child");
132    check(ok, !g.read(path_t("/sensor/temp")).has_value(),
133          "and the identically-named LOCAL vertex was not touched");
134
135    // Arm 2 — the leading route segment names no child: TERMINUS. The same trailing route
136    // segments are now an address, and the write lands in this node's graph.
137    to_b.sent.clear();
138    router.on_frame("cli", fwd_write({"sensor", "temp"}, {}));
139    check(ok, to_b.sent.empty(), "\"sensor\" names no child, so nothing was forwarded");
140    check(ok, g.read(path_t("/sensor/temp")).has_value(),
141          "this node was the terminus: the op applied to the LOCAL vertex");
142
143    // Arm 3 — terminus, but the address resolves to nothing. The refusal is ADDRESSED, not
144    // dropped: a REPLY with kind=ERROR goes back down the src the request accumulated.
145    to_client.sent.clear();
146    router.on_frame("cli", fwd_write({"no", "such", "vertex"}, {"app"}));
147    check(ok, to_client.sent.size() == 1,
148          "an unresolvable dst ANSWERS on the inbound link — it is not swallowed");
149    check(ok, !to_client.sent.empty() && reply_kind(to_client.sent[0]) == reply_kind_t::ERROR,
150          "and the answer is FWD{REPLY} kind=ERROR, so the caller sees a status, not a timeout");
151
152    std::printf(
153        "one test, two behaviours: \"b\" is a child (forward), \"sensor\" is not (terminus)\n");
154    return ok ? 0 : 1;
155}

See also: fwd-router module · addressing · communication flows · the source route · the child table.