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 src grow 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 in core/ 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_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 — 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.