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
band 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
dstthat resolves to no local vertex answersFWD{REPLY}withkind=ERROR, source-routed home along thesrcthe 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.
dststrictly 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_PLANEis 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 <optional>
35#include <span>
36#include <string_view>
37#include <vector>
38
39#include "libtracer/fwd_router.hpp"
40#include "libtracer/tlv_emit.hpp"
41#include "libtracer/tracer.hpp"
42
43namespace {
44
45using tr::graph::fwd_op_t;
46using tr::graph::graph_t;
47using tr::graph::path_t;
48using tr::graph::reply_kind_t;
49using tr::graph::role_t;
50using tr::wire::opt_t;
51using tr::wire::type_t;
52
53/** @brief Report expectation @p what and record a failure on @p ok. */
54void check(bool& ok, bool cond, const char* what) {
55 std::printf(" [%s] %s\n", cond ? "ok" : "FAIL", what);
56 ok = ok && cond;
57}
58
59/** @brief A `transport_t` that keeps every frame handed to it — the "wire", made inspectable. */
60struct recording_link_t : tr::net::transport_t {
61 std::vector<std::vector<std::byte>> sent; /**< @brief Frames emitted on this link, in order. */
62 void send(std::span<const std::byte> frame) override {
63 sent.emplace_back(frame.begin(), frame.end());
64 }
65 void send(std::span<const std::span<const std::byte>> iov) override {
66 std::vector<std::byte> flat;
67 for (const auto part : iov) flat.insert(flat.end(), part.begin(), part.end());
68 sent.push_back(std::move(flat));
69 }
70};
71
72/** @brief A `PATH` TLV over @p segs — RFC-0018 packed segment records, `opt.PL` clear. */
73std::vector<std::byte> path_tlv(std::initializer_list<std::string_view> segs) {
74 std::vector<std::byte> body;
75 for (std::string_view s : segs) (void)tr::wire::emit_path_segment(body, s);
76 std::vector<std::byte> out;
77 tr::wire::emit_tlv(out, type_t::PATH, opt_t{}, body);
78 return out;
79}
80
81/** @brief A one-byte `VALUE` TLV carrying @p v. */
82std::vector<std::byte> value_tlv(std::uint8_t v) {
83 const std::byte b{v};
84 std::vector<std::byte> out;
85 tr::wire::emit_tlv(out, type_t::VALUE, opt_t{}, std::span<const std::byte>(&b, 1));
86 return out;
87}
88
89/** @brief `FWD[.pl]{ VALUE op, PATH dst, PATH src, VALUE payload }` (RFC-0004 §B child order). */
90std::vector<std::byte> fwd_write(std::initializer_list<std::string_view> dst,
91 std::initializer_list<std::string_view> src) {
92 std::vector<std::byte> body = value_tlv(static_cast<std::uint8_t>(fwd_op_t::WRITE));
93 for (const auto& part : {path_tlv(dst), path_tlv(src), value_tlv(0x2A)}) {
94 body.insert(body.end(), part.begin(), part.end());
95 }
96 std::vector<std::byte> out;
97 tr::wire::emit_tlv(out, type_t::FWD, opt_t{.pl = true}, body);
98 return out;
99}
100
101/** @brief The `kind` of a `FWD{REPLY}` frame, or `std::nullopt` if @p frame is not one. */
102std::optional<reply_kind_t> reply_kind(std::span<const std::byte> frame) {
103 const auto tlv = tr::wire::decode(frame);
104 // Child order is `VALUE op, PATH dst, PATH src, VALUE kind, …` — the kind is child 3.
105 if (!tlv || tlv->children.size() < 4 || tlv->children[3].payload.size() != 1)
106 return std::nullopt;
107 return static_cast<reply_kind_t>(tlv->children[3].payload[0]);
108}
109
110} // namespace
111
112int main() {
113 bool ok = true;
114 graph_t g;
115 tr::net::fwd_router_t router(g);
116
117 // One child, named "b"; one local vertex, at /sensor/temp. Both spellings below are
118 // ordinary paths — nothing marks one of them as "remote".
119 recording_link_t to_b, to_client;
120 if (!router.add_child("b", to_b) || !router.add_child("cli", to_client)) {
121 std::fprintf(stderr, "route_terminus_or_forward: add_child failed — nothing registered\n");
122 return 1;
123 }
124 (void)g.register_vertex(path_t("/sensor/temp"), role_t::STORED_VALUE);
125
126 // Arm 1 — the leading route segment names a child: FORWARD. Nothing is written here.
127 router.on_frame("cli", fwd_write({"b", "sensor", "temp"}, {}));
128 check(ok, to_b.sent.size() == 1, "\"b\" names a child, so the frame left on that child");
129 check(ok, !g.read(path_t("/sensor/temp")).has_value(),
130 "and the identically-named LOCAL vertex was not touched");
131
132 // Arm 2 — the leading route segment names no child: TERMINUS. The same trailing route
133 // segments are now an address, and the write lands in this node's graph.
134 to_b.sent.clear();
135 router.on_frame("cli", fwd_write({"sensor", "temp"}, {}));
136 check(ok, to_b.sent.empty(), "\"sensor\" names no child, so nothing was forwarded");
137 check(ok, g.read(path_t("/sensor/temp")).has_value(),
138 "this node was the terminus: the op applied to the LOCAL vertex");
139
140 // Arm 3 — terminus, but the address resolves to nothing. The refusal is ADDRESSED, not
141 // dropped: a REPLY with kind=ERROR goes back down the src the request accumulated.
142 to_client.sent.clear();
143 router.on_frame("cli", fwd_write({"no", "such", "vertex"}, {"app"}));
144 check(ok, to_client.sent.size() == 1,
145 "an unresolvable dst ANSWERS on the inbound link — it is not swallowed");
146 check(ok, !to_client.sent.empty() && reply_kind(to_client.sent[0]) == reply_kind_t::ERROR,
147 "and the answer is FWD{REPLY} kind=ERROR, so the caller sees a status, not a timeout");
148
149 std::printf(
150 "one test, two behaviours: \"b\" is a child (forward), \"sensor\" is not (terminus)\n");
151 return ok ? 0 : 1;
152}
See also: fwd-router module · addressing · communication flows · the source route · the child table.