A kind is a NAME, resolved twice (transport plane)¶
Nothing in the routing plane knows the word tcp. A connection is created from a SPEC carrying
kind = <name>, and that name is looked up in two registries the application fills:
the factory catalog —
register_transport_type(kind, factory)— which decides what gets constructed;the module declaration —
register_module(module, kind, role)— which mints the creator endpoint the SPEC is written to, decides where the connection mounts (/net/<module>/<name>), and fixes the role positionally. Since RFC-0014 S7 retired the/net:children[]creation door, this is the only way a connection comes into being — and it is why the SPEC carries notypeand norole.
Both are open and both are strict. The example registers a kind that exists nowhere in the
library, creates a connection of it with an ordinary SPEC write to that module’s creator
endpoint /net/<module>/conn, watches the routing plane wire it up — and then asks for a kind
nobody registered and gets SCHEMA_NOT_FOUND.
What to notice¶
The library declares no modules, not even for its own kinds (ADR-0073 §4).
kTcpClientSuggestedModuleis a suggestion a header offers, never a registration a constructor performs. Until the application declares it,module_for("tcp", DIAL)is refused.An unregistered kind is REFUSED, never defaulted. A fallback transport would be worse than a failure: “some link came up” is indistinguishable from the right one until traffic silently goes nowhere. And nothing is registered on the way to refusing.
SCHEMA_NOT_FOUNDis the one verdict for both halves — a missing module and a missing factory answer the same way, because from the creator’s side they are the same fact: this catalog has no such entry.quicandwebtransportare the shipped out-of-tree case. They live in a separatelibtracer_quictarget that needs msquic, and nothing in the core references them (ADR-0043). What they use to join a node is this page and nothing more —quic_transport_factory()passed toregister_transport_type, exactly like the kind invented here.canis registered the same way.The factory parses its own private keys. Universal keys (
addr,port,kind,max_frame, …) arrive already parsed inconn_settings_t— withrolefilled in from the module’s declaration rather than from the wire; a kind’s private config (quic’stlsprofile name) is the factory’s business, read out of the raw config TLV. That split is what keepsconn_settings_tlean (ADR-0043 §5) — no kind-specific field ever lands in the shared record.This target needs the net plane (
LIBTRACER_NET_PLANE, the default) fortransport_vertex_t. It opens no socket: the kind it registers is an in-process one, so nothing here 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 transport KIND is a run-time NAME resolved through two registries
9 * the application fills, so a kind the core has never heard of mounts on exactly the
10 * same terms as a built-in one, and a kind nobody registered is REFUSED rather than
11 * defaulted to something plausible.
12 *
13 * Nothing in the routing plane knows the word `tcp`. A connection is created by writing a
14 * SPEC to a module's creator endpoint `/net/<module>/conn`, and the `kind = <name>` that
15 * SPEC carries is looked up twice:
16 *
17 * 1. the FACTORY catalog — `register_transport_type(kind, factory)` — which decides what
18 * object gets constructed. `udp`, `tcp` and `ws` are pre-registered by the default
19 * `transport_vertex_t` constructor only because they are compiled into the core;
20 * `can`, `quic` and `webtransport` ship their own factories (`can_transport_factory()`,
21 * `quic_transport_factory()`) and are registered by the application, through this
22 * identical call. An embedder's own kind is a third case of the same one.
23 * 2. the MODULE declaration — `register_module(module, kind, role)` — which decides WHERE
24 * the connection mounts, `/net/<module>/<name>`. The library declares NONE of these,
25 * not even for its built-ins (ADR-0073 §4): `kTcpClientSuggestedModule` is a suggestion
26 * a header offers, never a registration a constructor performs.
27 *
28 * The module declaration is also why the SPEC carries no `type` and no `role`. The retired
29 * `/net:children[]` door needed both, because a flat write had nothing else to say what was
30 * being built or which way it faced; the endpoint door puts that in the PATH — a module is
31 * declared for exactly one `(kind, role)`, so writing to `/net/tcp-client/conn` already
32 * means "a tcp DIAL". Two data fields the creator could contradict became one addressed
33 * door it cannot (RFC-0014 S7).
34 *
35 * Both halves are open and both are strict, which is the actual claim: this example
36 * registers a kind that exists nowhere in the library, creates a connection of it with an
37 * ordinary SPEC written to that kind's module endpoint, and watches the routing plane wire
38 * it up — and then asks for a kind nobody registered and gets `SCHEMA_NOT_FOUND` instead of
39 * a default.
40 *
41 * `quic` and `webtransport` are the shipped instances of the out-of-tree case: they live in
42 * a separate `libtracer_quic` target that needs msquic, and nothing in the core references
43 * them (ADR-0043). What they use to join a node is this page and nothing more.
44 *
45 * Needs the net plane (`LIBTRACER_NET_PLANE`, on by default) for `transport_vertex_t`; no
46 * sockets are opened, because the kind registered here is an in-process one. Runs under
47 * ctest as `example_net_kind_catalog`; returns non-zero on any failed check.
48 */
49
50#include <cstddef>
51#include <cstdint>
52#include <cstdio>
53#include <memory>
54#include <span>
55#include <string>
56#include <utility>
57#include <vector>
58
59#include "libtracer/conn_spec.hpp"
60#include "libtracer/fwd_router.hpp"
61#include "libtracer/tracer.hpp"
62#include "libtracer/transport_tcp.hpp"
63#include "libtracer/transport_vertex.hpp"
64
65namespace {
66
67using tr::graph::graph_t;
68using tr::graph::path_t;
69using tr::net::conn_role_t;
70
71/** @brief Report expectation @p what and record a failure on @p ok. */
72void check(bool& ok, bool cond, const char* what) {
73 std::printf(" [%s] %s\n", cond ? "ok" : "FAIL", what);
74 ok = ok && cond;
75}
76
77/**
78 * @brief The whole of a new transport kind: a `transport_t` that keeps what it is given.
79 *
80 * A real kind opens something. This one does not, which is the cleanest way to show that
81 * the catalog cares about nothing except that a factory answers with a `transport_t`.
82 */
83class demo_link_t final : public tr::net::transport_t {
84 public:
85 /** @brief How many frames this link was asked to emit. */
86 std::size_t sent = 0;
87 void send(std::span<const std::byte> frame) override {
88 (void)frame;
89 ++sent;
90 }
91};
92
93} // namespace
94
95int main() {
96 bool ok = true;
97
98 graph_t g;
99 tr::net::fwd_router_t router(g);
100 // The default constructor registers the factories for the kinds this BUILD compiled —
101 // udp/tcp/ws. It registers no module names at all, for any of them.
102 tr::net::transport_vertex_t net(g, router);
103
104 // 1. A compiled-in kind is still unusable until the application says where it mounts.
105 // This refusal is the declared-only rule: the catalog is the app's, not the library's.
106 std::printf("the library declares no modules, not even for its own kinds:\n");
107 const auto before = net.module_for("tcp", conn_role_t::DIAL);
108 check(ok, !before.has_value(), "module_for('tcp', DIAL) is refused before the app declares it");
109 check(ok, !before.has_value() && before.error() == tr::graph::status_t::SCHEMA_NOT_FOUND,
110 "…as SCHEMA_NOT_FOUND — an absent catalog entry, not a malformed request");
111
112 const auto declared = net.register_module(std::string(tr::net::kTcpClientSuggestedModule),
113 "tcp", conn_role_t::DIAL);
114 check(ok, declared.has_value(),
115 "the application declares the module, adopting the header's "
116 "suggested name");
117 const auto after = net.module_for("tcp", conn_role_t::DIAL);
118 check(ok, after.has_value() && *after == "tcp-client", "…and now the kind resolves to it");
119
120 // 2. A kind the library has never heard of. Two calls — the same two calls `quic` makes.
121 std::printf("a kind the core does not contain:\n");
122 net.register_transport_type(
123 "demo",
124 [](const tr::net::conn_settings_t& settings, const tr::wire::tlv_node_t* raw_config,
125 tr::mem::block_source_t& src) -> tr::graph::result_t<tr::net::transport_ptr_t> {
126 // A real factory parses its kind-PRIVATE keys out of `raw_config` here (quic's
127 // `tls` profile name is the shipped example); the universal keys are already
128 // parsed into `settings`. This one needs neither, and says so.
129 (void)settings;
130 (void)raw_config;
131 return tr::net::make_transport<demo_link_t>(src);
132 });
133 check(ok, net.register_module("demo-client", "demo", conn_role_t::DIAL).has_value(),
134 "register_module accepts a kind that exists only in this file");
135
136 // 3. Create one, the ordinary way: a SPEC written to the module's creator endpoint.
137 // Nothing in this write is kind-specific except the four letters of the name — and no
138 // `type`/`role` pair, because `/net/demo-client/conn` already says both.
139 const auto created =
140 g.write(path_t("/net/demo-client/conn"), tr::net::conn_spec("one", /*port=*/0, "demo"));
141 check(ok, created.has_value(), "SPEC{ name=one, kind=demo } created a connection");
142 check(ok, router.registry().by_name("net/demo-client/one") != nullptr,
143 "…and the routing plane wired it in under /net/<module>/<name>");
144 check(ok, g.read(path_t("/net/demo-client/one")).has_value(),
145 "…with a connection vertex addressable in the graph");
146
147 // 4. And a kind nobody registered. The refusal is the point: an unresolvable kind must
148 // not fall back to a default transport, because "some link came up" is indistinguishable
149 // from the right one until traffic silently goes nowhere.
150 // Declaring a module for it is allowed and does not help: `register_module` records
151 // where a kind WOULD mount, it does not promise anybody can build one. The factory
152 // catalog is the half that constructs, and it is consulted at the write.
153 std::printf("a kind nobody registered:\n");
154 check(ok, net.register_module("nosuch-client", "nosuch", conn_role_t::DIAL).has_value(),
155 "a module may be declared for a kind no factory answers for");
156 const auto refused =
157 g.write(path_t("/net/nosuch-client/conn"), tr::net::conn_spec("two", /*port=*/0, "nosuch"));
158 check(ok, !refused.has_value(), "SPEC{ kind=nosuch } was refused");
159 check(ok, !refused.has_value() && refused.error() == tr::graph::status_t::SCHEMA_NOT_FOUND,
160 "…as SCHEMA_NOT_FOUND — the same verdict a missing module gives");
161 check(ok, router.registry().by_name("net/nosuch-client/two") == nullptr,
162 "…and nothing was registered on the way to refusing");
163
164 std::printf("catalog: 1 kind declared by the app, 1 kind invented here, 1 refused\n");
165 return ok ? 0 : 1;
166}
See also: connection config · transport module · transports are vertices · module catalog reference · the seam a factory has to satisfy.