Skip to content

(17a) net: LLDP TLV framing and the mandatory TLVs - #1906

Draft
daniel-noland wants to merge 2 commits into
pr/daniel-noland/capturefrom
pr/daniel-noland/lldp
Draft

daniel-noland wants to merge 2 commits into
pr/daniel-noland/capturefrom
pr/daniel-noland/lldp

Conversation

@daniel-noland

@daniel-noland daniel-noland commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Native LLDP for the gateway. The dataplane owns its NICs, so nothing else can speak LLDP for them, and lldpd/lldpad are hostile to automation. LLDP itself is small, so the dataplane transmits and receives it.

This used to be one PR. It is now a stack of seven, each building on the one before:

Stacked on (16) #1905, capture. (18) #1907, rate limiting, and (19) #1919, traffic protection, build on top.

This PR: TLV framing and the mandatory TLVs

The first of three PRs adding net::lldp, a type-safe, byte-lossless parser and builder for LLDP data units.

  • TLV framing (tlv): the 7-bit type and 9-bit length header, and RawTlv.
  • Octet strings (octets): Octets<MIN, MAX> carries each field's length bounds in its type.
  • Network addresses (address), shared by the chassis ID, port ID and management address.
  • The mandatory records: chassis ID, port ID, time to live, and the end marker.
  • Display for the identifiers, which show lldp neighbors uses later.

The design rules, which hold for the whole module:

  • Invariants live in the types. A subtype is implied by the enum variant rather than stored beside its value.
  • Parsing is byte-lossless. Every value deparses to the octets it came from, which property tests check at every level.
  • Reserved values are rejected, but where the standard is soft the parser stays permissive.
  • Serde goes through the checked constructors, so deserialising can't bypass them.

It lives in net rather than a crate of its own because it needs crate-private pieces (LengthError has no public constructor) and reuses Mac, the IP types and the bolero scaffolding.

Doc comments that mention Lldpdu are plain code spans here. Lldpdu arrives in the third PR, which turns them into links.

Testing

Property tests for every type: round trip, parse ∘ deparse = identity, and rejection of reserved values.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.92008% with 169 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
net/src/lldp/tlv.rs 74.83% 72 Missing and 3 partials ⚠️
net/src/lldp/address.rs 79.43% 29 Missing ⚠️
net/src/lldp/chassis_id.rs 90.59% 19 Missing ⚠️
net/src/lldp/octets.rs 85.03% 19 Missing ⚠️
net/src/lldp/port_id.rs 91.27% 15 Missing ⚠️
net/src/lldp/ttl.rs 88.34% 12 Missing ⚠️

📢 Thoughts on this report? Let us know!

daniel-noland and others added 2 commits October 16, 2026 01:58
The first of three changes adding `net::lldp`: the TLV framing, the
octet strings and network addresses the identifiers are built from, and
the three records every LLDPDU starts with (chassis ID, port ID, time to
live), plus the end marker.  The optional TLVs and the LLDPDU itself
follow.

`net::lldp` is a type safe parser and builder for LLDP data units, so
that reading what a neighbour advertises and crafting what we advertise
back does not come down to hand rolled offsets over a TLV stream.

This lives in `net` rather than in a crate of its own because it needs
pieces that are deliberately crate private: `LengthError`'s fields have
no public constructor, so an out of crate `Parse` impl cannot report a
truncated buffer at all.  It also reuses `Mac`, the IP address types,
`Vid` and the bolero scaffolding instead of duplicating them.  Splitting
it out later is mechanical; starting outside would have forced `net` to
widen its public API first.

The shape of an LLDPDU is encoded in the types rather than checked at
run time, per the "enforce invariants at compile time" guidance in
development/code/README.md.  802.1AB requires a chassis ID, a port ID
and a time to live, in that order, then optional TLVs, then an end
marker, so `Lldpdu` holds the mandatory three in named fields and emits
the marker itself.  Likewise a subtype is implied by the enum variant
rather than stored beside the value it describes, `Octets<MIN, MAX>`
carries each field's length bounds in its type, and the escape hatches
for unmodelled TLVs refuse anything with a structured representation.

That last point is what keeps the mapping between octets and values one
to one in both directions.  Every level property tests it; the LLDPDU
level does so both for arbitrary input and for single octet mutations of
a valid frame, since random bytes rarely reach past the first TLV.

Reserved values are rejected, following `InvalidVni::ReservedZero`, but
where the standard is soft the parser stays permissive: unknown TLV
types, address families and OUIs round trip verbatim, and reserved
capability bits are preserved rather than masked.  Serde routes through
the checked constructors, so deserialization is not a back door around
the invariants parsing enforces.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Show chassis and port IDs, network addresses and LLDP octet strings in a
form a person can read: octets as text when they are printable UTF-8
and as colon-separated hex otherwise, since LLDP carries both names
meant to be read and identifiers that are not, often under the same TLV
type; addresses as IPs; identifiers as their value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/capture branch from c7bc0e6 to 4207243 Compare October 16, 2026 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant