A mount run is consumed whole (L4 routing)¶
A child’s name does not have to be one route segment. It is a mount path — "up",
"ws-server/up", the RFC-0014 shape "net/<module>/<name>", or something deeper — and the
forward path matches it as a unit: one pass over the registry, each slot tested against the
prefix of that slot’s own width, longest match wins
(ADR-0061,
RFC-0014
S2a). There is no compile-time bound on K to raise; width is bounded only by the path-depth
budget every address already spends from
(#523).
The half that is easy to get wrong is the other direction. Strip-K on dst must be matched by
grow-K on src: the hop prepends the full mount run of the link the frame arrived on, not one
route segment and not a truncation. Prepending less produces a return route that no longer names
the inbound link once names are per-module scoped — a reply that cannot get home (the ADR-0061
erratum).
What to notice¶
Both sides of the example are qualified. A three-wide inbound mount and a three-wide outbound one, checked byte-for-byte, so the assertion covers grow-K as well as strip-K. A one-wide inbound link would have let a truncating
srcgrow pass.Longest prefix, not first match. A deeper mount registered alongside a shallower one that also matches takes the frame, and strips its own width. That is what keeps two modules’ same-named connections distinct.
net/<module>/<name>is a convention, not a parse. The registry stores the name and matches it as route segments; nothing incore/splits it into fields.“Each hop strips one route segment” is the wrong sentence and
CONTEXT.md§Path-as-route lists it as vocabulary to avoid. The source-route page shows the one-wide case because it reads more clearly; this page is the general rule.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 — a mount name is any number of route segments, and a hop consumes the
9 * WHOLE width in one step (RFC-0014 strip-K).
10 *
11 * A child's name does not have to be one route segment. It is a mount PATH — `"up"`,
12 * `"ws-server/up"`, the RFC-0014 shape `"net/<module>/<name>"`, or something deeper — and the
13 * forward path matches it as a unit: one pass over the registry, each slot tested against the
14 * prefix of that slot's OWN width, longest match wins (`child_registry_t::longest_prefix`,
15 * ADR-0061). There is no compile-time bound on K to raise; the width is bounded only by the
16 * path-depth budget every address already spends from (#523).
17 *
18 * The half that is easy to get wrong is the OTHER direction. Strip-K on `dst` must be matched
19 * by grow-K on `src`: the hop prepends the full mount path of the link the frame arrived on,
20 * not a single route segment and not a truncation of it. Prepending less would produce a return
21 * route that no longer names the inbound link once names are per-module scoped — a reply that
22 * cannot get home (the ADR-0061 erratum).
23 *
24 * So the example wires BOTH sides qualified: a three-wide inbound mount and a three-wide
25 * outbound one, and checks the emitted frame byte-for-byte. It also checks the property that
26 * makes longest-prefix worth having — a deeper mount wins over a shallower one that also
27 * matches, which is how two modules keep same-named connections distinct.
28 *
29 * Runs under ctest as `example_route_qualified_mount`; returns non-zero on any failed check.
30 */
31
32#include <cstddef>
33#include <cstdint>
34#include <cstdio>
35#include <initializer_list>
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::wire::opt_t;
49using tr::wire::type_t;
50
51/** @brief Report expectation @p what and record a failure on @p ok. */
52void check(bool& ok, bool cond, const char* what) {
53 std::printf(" [%s] %s\n", cond ? "ok" : "FAIL", what);
54 ok = ok && cond;
55}
56
57/** @brief A `transport_t` that keeps every frame handed to it — the "wire", made inspectable. */
58struct recording_link_t : tr::net::transport_t {
59 std::vector<std::vector<std::byte>> sent; /**< @brief Frames emitted on this link, in order. */
60 void send(std::span<const std::byte> frame) override {
61 sent.emplace_back(frame.begin(), frame.end());
62 }
63 void send(std::span<const std::span<const std::byte>> iov) override {
64 std::vector<std::byte> flat;
65 for (const auto part : iov) flat.insert(flat.end(), part.begin(), part.end());
66 sent.push_back(std::move(flat));
67 }
68};
69
70/** @brief A `PATH` TLV over @p segs — RFC-0018 packed segment records, `opt.PL` clear. */
71std::vector<std::byte> path_tlv(std::initializer_list<std::string_view> segs) {
72 std::vector<std::byte> body;
73 for (std::string_view s : segs) (void)tr::wire::emit_path_segment(body, s);
74 std::vector<std::byte> out;
75 tr::wire::emit_tlv(out, type_t::PATH, opt_t{}, body);
76 return out;
77}
78
79/** @brief A one-byte `VALUE` TLV carrying @p v. */
80std::vector<std::byte> value_tlv(std::uint8_t v) {
81 const std::byte b{v};
82 std::vector<std::byte> out;
83 tr::wire::emit_tlv(out, type_t::VALUE, opt_t{}, std::span<const std::byte>(&b, 1));
84 return out;
85}
86
87/** @brief `FWD[.pl]{ VALUE op, PATH dst, PATH src, VALUE payload }` (RFC-0004 §B child order). */
88std::vector<std::byte> fwd_write(std::initializer_list<std::string_view> dst,
89 std::initializer_list<std::string_view> src) {
90 std::vector<std::byte> body = value_tlv(static_cast<std::uint8_t>(fwd_op_t::WRITE));
91 for (const auto& part : {path_tlv(dst), path_tlv(src), value_tlv(0x2A)}) {
92 body.insert(body.end(), part.begin(), part.end());
93 }
94 std::vector<std::byte> out;
95 tr::wire::emit_tlv(out, type_t::FWD, opt_t{.pl = true}, body);
96 return out;
97}
98
99} // namespace
100
101int main() {
102 bool ok = true;
103 graph_t g;
104 tr::net::fwd_router_t router(g);
105
106 // Both links are RFC-0014 qualified mounts, three route segments each. `net/<module>/<name>`
107 // is a convention, not a parse: the registry stores the name and matches it as route segments.
108 recording_link_t downstream, upstream;
109 if (!router.add_child("net/ws-client/b", downstream) ||
110 !router.add_child("net/ws-server/cli", upstream)) {
111 std::fprintf(stderr, "route_qualified_mount: add_child failed — nothing registered\n");
112 return 1;
113 }
114
115 router.on_frame("net/ws-server/cli",
116 fwd_write({"net", "ws-client", "b", "sensor", "temp"}, {}));
117 check(ok, downstream.sent.size() == 1, "the three-wide mount matched and forwarded");
118 if (downstream.sent.size() != 1) return 1;
119
120 // K = 3 off dst, K = 3 onto src, in one hop. Anything less on either side is a defect:
121 // too little stripped loops the frame, too little grown loses the reply.
122 const std::vector<std::byte> expected =
123 fwd_write({"sensor", "temp"}, {"net", "ws-server", "cli"});
124 check(ok, downstream.sent[0] == expected,
125 "dst lost all THREE mount route segments and src gained all three — byte-exact");
126
127 // Longest-prefix, not first-match: a deeper mount that also matches wins. This is what
128 // keeps two modules' same-named connections from colliding.
129 recording_link_t deeper;
130 const bool added_deeper = router.add_child("net/ws-client/b/inner", deeper);
131 check(ok, added_deeper, "a deeper mount registers alongside the shallower one");
132 downstream.sent.clear();
133 router.on_frame("net/ws-server/cli", fwd_write({"net", "ws-client", "b", "inner", "leaf"}, {}));
134 check(ok, deeper.sent.size() == 1 && downstream.sent.empty(),
135 "the LONGEST matching mount took the frame, not the first one that matched");
136 check(
137 ok,
138 !deeper.sent.empty() && deeper.sent[0] == fwd_write({"leaf"}, {"net", "ws-server", "cli"}),
139 "and it stripped its own width — four route segments, not the shallower mount's three");
140
141 std::printf("mount width is per-slot: stripped 3 for /net/ws-client/b, 4 for its /inner\n");
142 return ok ? 0 : 1;
143}
See also: fwd-router module · transports are vertices · addressing · the child table · multi-hop.