connection config — the SPEC config keys (L4)¶
In one paragraph
Creating a connection is an ordinary write — write /net/<module>/conn = SPEC{name, config} at the RFC-0014 creator endpoint, or the superseded write /net:children[] += SPEC{type, name, config} — and everything the new link needs is in that config
SETTINGS TLV.
Its keys come in two families. The universal ones (kind, addr, port,
role, …) are parsed centrally into tr::net::conn_settings_t, which every
transport kind shares. The kind-private ones are parsed by the selected kind’s
own factory, module-side, and never land on that shared record — so quic reads
cert/key/ca/insecure, ws and tcp read peer_named/max_peers, can
reads its bus identity and ingress bounds, and none of them can see each other’s
vocabulary. This page is the key-by-key reference for both families, and the two
keys most worth reading before you ship are ca and insecure: a SPEC-created
quic/webtransport dialer verifies its peer’s certificate by default.
What this page is, and what it is not¶
This is the reference home for the connection-creation config keys. Before it
existed, a key was discoverable only from the factory’s own Doxygen block or from
core/CHANGELOG.md — including the two quic keys that decide whether a dialer
authenticates its peer at all.
It is not the creation protocol: the SPEC shape, the ACL gate on the append, the
/net/<module>/<name> mount and the liveness value all belong to
fwd-router § the /net connection model. It is not the walk
either — the positional pair grammar and its forward-compat rules are
config § runtime configuration. This page is the vocabulary: which
keys exist, which factory reads each one, what wire value each takes, and what it
does.
Its subject is deliberately bounded to what the transport-side readers read. The
creation SPEC’s own envelope (type, name, config) and the SUBSCRIBER QoS
SETTINGS read the same positional grammar — since
#985 through the same
tr::wire::config_reader_t type — but they are not connection config, and the ACL
SETTINGS walk keeps its own reject-unknown-keys code
(#906).
The shape of a config¶
A config value is a SETTINGS TLV whose children are positional
(NAME key, value) pairs. String-valued keys take a NAME child; integer and
boolean keys take a VALUE child holding a little-endian unsigned integer. Four
rules from the shared reader apply to every key on this page:
Unknown pairs are ignored, whole — key and value — so a newer peer may send keys this node has never heard of.
A wrong-typed value is ignored as though the key were absent; so is a
VALUEpayload whose size is not exactly the width this table gives (an empty payload being that rule’s trivial case). A u16portsent as four bytes reads as absent rather than silently dropping its high bytes, and a u32keepalivesent as two reads as absent rather than zero-extending (#928).A repeated key resolves to its last well-formed occurrence.
A child that is not a
NAMEwhere a key belongs stops the walk. Every key after that point reads as absent.
The consequence that bites: there is no error return for a misspelled or
mistyped key. insecrue = 1 and insecure = "1" (a NAME value where a VALUE
belongs) both create a connection that silently took the default. The gate against
that is reading the table below, not a status code.
Writing one — tr::net::conn_spec_t¶
The library ships the encoder as well as the reader (libtracer/conn_spec.hpp).
conn_spec_t is a fluent builder over the shape above: each setter appends one
(NAME key, value) pair and returns *this, and the terminal bytes() / view()
wrap the pairs in the config SETTINGS and the whole thing in the SPEC.
using tr::net::conn_role_t;
using tr::net::conn_spec_t;
// The one-call form — the 90% case.
graph.write(*path_t::parse("/net:children[]"),
tr::net::conn_spec("client", "up", conn_role_t::DIAL, 8080, "ws", "127.0.0.1"));
// The builder, for the rest: universal keys by name, kind-private keys as pairs.
graph.write(*path_t::parse("/net:children[]"), conn_spec_t("listener", "srv")
.role(conn_role_t::LISTEN)
.port(8080)
.kind("ws")
.max_frame(4096)
.flag("peer_named", true) // ws-private
.u32("max_peers", 8) // ws-private
.view());
// The RFC-0014 creator-endpoint spelling (S2b): the module in the PATH fixes both the
// transport and the role, so the SPEC carries neither a `type` nor a `role` — the same
// config keys, one door down.
graph.write(*path_t::parse("/net/ws-server/conn"), conn_spec_t("srv")
.port(8080)
.max_frame(4096)
.flag("peer_named", true)
.view());
// Removal is the other half of that one control: a bare NAME, told apart by TLV type.
graph.write(*path_t::parse("/net/ws-server/conn"), tr::net::conn_remove("srv"));
The named setters are exactly the universal keys of the next section; the generic
text / u8 / u16 / u32 / flag pairs carry a kind’s private vocabulary,
which the builder deliberately does not know (that coupling is what
ADR-0043
§5 forbids). Their names mirror config_reader_t’s accessors, so the encode and
decode vocabularies cannot drift apart.
Two properties are worth knowing before you build one:
A builder on which no setter ran emits no
configat all — that is theprovide_link-staged spelling, where the module comes from the staging rather than from akind.There is no
modulekey, and the builder invents none. A SPEC names its module throughkindtogether withrole; see thekindrow below.
The builder does not validate a key against a kind — it cannot, for the reason above — so the misspelling hazard in the paragraph before this section is unchanged. What it removes is the other failure mode: a private near-copy of the encoder per call site, each free to drift.
Universal keys — parsed into conn_settings_t¶
Read once, centrally, for every kind (core/src/transport_vertex.cpp). A kind’s
factory receives the parsed record alongside the raw config TLV.
key |
value |
applies to |
default |
meaning |
|---|---|---|---|---|
|
|
both |
empty |
Selects the transport factory ( |
|
|
DIAL |
empty |
Peer address, IPv4 dotted-quad. A DIAL with it empty answers |
|
|
both |
absent |
Peer port on a DIAL, bind port on a LISTEN. On a DIAL, |
|
|
both |
the child type’s default |
|
|
|
both |
|
Keepalive interval in ms. Nothing reads it: |
|
|
both |
|
Per-connection inbound frame cap in bytes, honoured by five of the six kinds — |
|
|
DIAL |
|
Self-heal retry interval in ms (RFC-0014 §4), consumed by the S5 liveness engine ( |
|
|
DIAL |
|
How long one dial attempt waits for |
Kind-private keys¶
Each kind’s factory parses its own keys out of the raw config TLV. Nothing here is
a field on conn_settings_t, and no kind can read another kind’s key.
tcp — the LISTEN-side bus facet¶
key |
value |
applies to |
default |
meaning |
|---|---|---|---|---|
|
|
LISTEN |
|
Non-zero exposes the |
|
|
LISTEN |
|
Concurrent-peer admission cap (RFC-0006); a connection beyond it is accepted and immediately closed. |
|
|
DIAL + LISTEN |
|
The peer liveness window (#838): how long a peer may fail to take bytes before it is treated as broken. It bounds every send (and the write-mutex hold it takes) — a broadcast record gets |
The first two are ignored on a DIAL: a client has exactly one peer, itself. liveness_window
applies to both roles — a dialled client can be stalled by its server just as a listener can by
a peer.
ws — the same keys, verbatim¶
key |
value |
applies to |
default |
meaning |
|---|---|---|---|---|
|
|
LISTEN |
|
As |
|
|
LISTEN |
|
As |
|
|
DIAL + LISTEN |
|
As |
|
|
DIAL + LISTEN |
|
Bytes. The pre-auth opening-handshake request budget (#934): the most an unauthenticated host — one that has completed a TCP connect and nothing else, no ACL, no subscription, no router — may make this node accumulate before its HTTP Upgrade request (LISTEN) or |
udp — no kind-private keys¶
The udp factory constructs no config reader at all: addr/port/role/max_frame
from the universal set are the whole of its configuration. A DIAL binds an ephemeral
local port and targets addr:port; a LISTEN binds port (or an OS-granted one when the
key is spelled 0) and learns its peer from the first inbound datagram’s source.
This is a checked claim, not an omission — see How this page is kept true.
can — bus identity and ingress bounds¶
Of the six transport factories in this tree, can is the only one that ignores
conn_settings_t outright — its lambda takes the record as an unnamed parameter — so
addr, port, keepalive and max_frame do nothing on a can connection. (kind and role still matter — they are consumed
by the connection vertex itself, to select this factory and to resolve the module
the vertex mounts under.)
key |
value |
applies to |
default |
meaning |
|---|---|---|---|---|
|
|
both |
— (required) |
The SocketCAN interface ( |
|
|
both |
— (required) |
This node’s id, the |
|
|
both |
|
Protocol-version prefix, the top 4 bits of the CAN ID, so distinct versions occupy disjoint arbitration bands. Above |
|
|
both |
empty |
The path advertised for this node’s groups. Same spelling as the |
|
|
both |
|
Non-zero selects CAN-FD framing (≤64 B data fields) instead of classic (≤8 B). |
|
|
both |
|
Peer liveness window (ADR-0044): a peer silent longer than this leaves the enumeration. |
|
|
both |
|
Live reassembly-group ceiling. |
|
|
both |
|
Ceiling on data slices parked awaiting their advertise. |
|
|
both |
|
RX staleness window: a parked slice or an incomplete group untouched this long is reclaimed, so a lost advertise cannot pin one forever. |
The two count caps and the pmr resource behind them are the ingress-bounding seam;
the byte-level story is on can.
quic and webtransport — the TLS material¶
Two modules, one identical key set, both in the separate libtracer_quic target.
Two keys are the LISTEN-side served credential; two are the DIAL-side trust
decision.
key |
value |
applies to |
default |
meaning |
|---|---|---|---|---|
|
|
LISTEN |
— (required) |
PEM server-certificate path. Absent answers |
|
|
LISTEN |
— (required) |
PEM private-key path matching |
|
|
DIAL |
empty ⇒ the system trust store |
PEM CA-bundle the peer’s certificate is verified against, instead of the system trust store. |
|
|
DIAL |
|
DEV ONLY. Non-zero skips server-certificate validation entirely. |
webtransport reads the same four, with the same meanings, plus two keys quic
has no use for — it is the only kind here with an HTTP layer, so it is the only
one with a resource to name and the only one with an H3 handshake to bound:
key |
value |
applies to |
default |
meaning |
|---|---|---|---|---|
|
|
LISTEN |
— (required) |
PEM server-certificate path. |
|
|
LISTEN |
— (required) |
PEM private-key path matching |
|
|
DIAL |
empty ⇒ the system trust store |
PEM CA-bundle to verify the peer against. |
|
|
DIAL |
|
DEV ONLY. Skips server-certificate validation. |
|
|
DIAL + LISTEN |
|
Bytes. The pre-auth HTTP/3 handshake budget (#1408): the most a peer that has completed a QUIC handshake and nothing else — no session, no ACL, no subscription, no router — may make this node accumulate before its H3 material is refused. |
|
|
DIAL |
|
The extended CONNECT |
tools/gen-dev-cert.sh emits a self-signed pair for the LISTEN side.
A dial to the wrong resource is not a distinguishable failure: the server refuses
the CONNECT, the session never establishes, and creation answers TRANSPORT_DOWN — the
same status a certificate rejection gives. That holds for a well-formed path
naming a resource the server does not serve; the one case taken out of it by
#1039 is a path that is not origin-form at all, which is refused at creation
with TYPE_MISMATCH rather than dialled. Before #1023 there was no key at all
and the factory hard-coded /, so a SPEC could reach only a root-served session
and any other server needed the direct constructor plus provide_link. On the
LISTEN side, webtransport_transport_t::session_path() reports the :path the
accepted CONNECT named — an observation, never an admission decision.
Certificate trust on a SPEC-created dialer¶
Four points, in the order an integrator meets them.
The default is verify. A quic or webtransport dialer created from a SPEC
with neither trust key validates the peer’s certificate against the system trust
store, and a certificate that does not chain to it is refused: the handshake fails
and creation answers TRANSPORT_DOWN. Anything dialing a self-signed peer must say so,
with ca or with insecure = 1. This is a change of behaviour, not a restatement
of one: before #918 the DIAL branch hard-coded no-verify and returned before
the kind-private parse ran at all, so every SPEC-created dialer skipped validation
and no config key existed that could change it. core/tests/quic_test.cpp and
core/tests/webtransport_test.cpp drive all five legs — no key, insecure = 1,
ca = the peer’s own cert, insecure = 0, and an unrelated ca bundle that is
genuinely consulted and still refuses.
insecure wins when both are set. The credential is built with a single
if (insecure) … else if (!ca.empty()) …, so insecure = 1 together with a ca
path is a no-verify dial and the bundle is not consulted. That is deliberate: it
matches the quic_dial_tls_t contract the direct-construction path already had, and
it errs toward the mode the operator wrote down explicitly rather than toward a
silently half-applied one.
Malformed insecure fails secure. Every way of getting the key wrong resolves
to verify, because the reader returns “absent” and the field keeps its false
default: the key omitted, the value sent as a NAME instead of a VALUE, an empty
VALUE payload, and — because a VALUE is little-endian — a wide payload whose
first byte is zero, which is what a big-endian 1 looks like on the wire. There
is no spelling of a broken insecure that turns validation off.
insecure = 0 is not a weaker opt-out. It is the explicit spelling of the
default, and it verifies.
Why these are not conn_settings_t fields¶
conn_settings_t carries only the keys every transport kind shares. That
leanness is a ruling, not an accident (ADR-0043 §5): a kind’s private configuration
is parsed by that kind’s own factory, inside its own module.
The reason is the module boundary. quic lives in a separate link target; a device
that does not link it contains zero QUIC schema, no msquic reference and no feature
macro. Putting cert/key/ca/insecure on the shared record would put TLS
vocabulary into the connection settings of a 16 KB MCU that will never speak TLS —
and it would grow once per kind, forever, for keys no other kind can use.
So the answer to “where do I add my kind’s new key?” is: in your factory, read
out of the raw config TLV it already receives, and in a block on this page. Not on
conn_settings_t.
Pitfalls¶
A typo is silent. No status distinguishes “key absent” from “key misspelled” from “key sent with the wrong TLV type” — all three take the default. The connection comes up looking healthy.
canignores the universal keys. Settingportormax_frameon acanconnection changes nothing; its identity isifname+node.keepalivehas no consumer at all. It is parsed intoconn_settings_tand no transport in the tree reads the field.backoffandconnect_timeoutare dormant. They parse, they land inconn_settings_t, and nothing reads them yet.max_framecannot be used to buy headroom. It is TIGHTEN-ONLY: every framed kind resolves it throughlength_prefix_framer::configured_cap, which clamps a non-zero value to the 16 MiB protocol default, somax_frame = 32 MiBstill tears down a 20 MiB frame as malformed (#1035). It is an ingress bound you can clamp, never loosen — a config-writable key must not be able to widen what an unauthenticated peer can make the node buffer.udptightens too, for its own reason: a datagram cannot exceed 64 KiB, so a value above that is simply inert there. If a deployment genuinely needs larger frames, the 16 MiB ceiling is a source-level constant (length_prefix_framer::kDefaultMaxFrame), not a knob.peer_namedis off by default, so a SPEC-createdtcp/wslistener is a broadcast link and one request over it draws one reply per peer.peer_namedmay be REFUSED outright, not downgraded. A target that closed the ADR-0044 bus module out (tr::graph::default_config_t::kBusLinks = false— see transport.md §”Closing the bus module out at build time”) carries no peer-named tier at all, so both stream factories answerpeer_named = 1withTYPE_MISMATCHand create no connection. The status is deliberately the permanent one rather than the transientTRANSPORT_DOWNa failed bind gets: no retry will make that build grow a bus facet. Serving the key as if it had said0would be worse than either, because the listener’s own per-frame tier select reads its constructed mode — a listener demoted only at the router would keep delivering peer-named into a sink nothing installed.The builder types the universal keys, not the kind-private ones.
conn_spec_t’s named setters (#902) makekind,addr,port,roleand the four u32s unmisspellable, and they replaced the sixteen hand-written emitters that used to exist. A kind’s PRIVATE keys still go through the generictext/u8/u16/u32/flagpairs — the builder cannot know them without the coupling ADR-0043 §5 forbids — so for those, a key’s spelling is still only as good as the string literal next to it.
How this page is kept true¶
Every key table above sits inside a marker block naming the source file that reads
it, and tools/check_config_keys.py derives the same information from that file —
it finds each config_reader_t construction and reads the accessor calls
on it, so the key and its wire value type come from the code, not from a
maintainer’s memory. The gate fails on three things: a key the source reads and the
page omits, a key the page lists and the source no longer reads, and a source file
that reads connection config with no block on this page at all. The last one is
what keeps the sweep honest — a new kind cannot be added with its keys documented
nowhere, and udp’s “no kind-private keys” is a derived fact rather than a claim.
The scope of that gate is connection config, and the scope is deliberate: the
creation-SPEC envelope and the SUBSCRIBER QoS parse in core/src/graph.cpp read
their grammar through the same shared config_reader_t since
#985, so that file is
explicitly excluded from the sweep — its keys are not connection config — and the
ACL walk constructs no reader at all.
tools/check_config_keys.py # the gate
tools/check_config_keys.py --list # the derived inventory, one key per line