Skip to content

ef: add llc, stp, stp_rst and stp_tcn headers for 802.1D/802.1Q BPDUs - #21

Open
igudich wants to merge 4 commits into
masterfrom
master.stp_bpdu
Open

igudich wants to merge 4 commits into
masterfrom
master.stp_bpdu

Conversation

@igudich

@igudich igudich commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Adds 802.2 LLC framing and the three spanning-tree BPDUs, so STP and RSTP frames can be built and matched field by field instead of as opaque hex.

ef hex eth dmac 01:80:c2:00:00:00 smac 00:11:22:33:44:55 \
       llc stp_rst role 3 proposal 1 learning 1 forwarding 1

Headers

  • llc: DSAP/SSAP/control, defaulting to the STP SAPs (0x42/0x42/0x03). It writes the 802.3 length into the et field of the header below it, eth or the outermost VLAN tag. A computed length above 1500 is refused; an explicit et still wins.
  • stp: 802.1D configuration BPDU, 35 octets.
  • stp_rst: 802.1Q clause 14.4 RST BPDU, 36 octets: the configuration body plus Version 1 Length. stp ver 2 cannot stand in for it, because it leaves the body one octet short and a receiver discards it as malformed.
  • stp_tcn: topology change notification, 4 octets.

The three BPDUs are separate templates, following the MRP precedent of one template per message type. The commit messages give the reasons.

Flags octet

All eight bits of 802.1Q table 13-5 are named: tc, proposal, role (2 bits), learning, forwarding, agreement, tcack. The whole byte is still available as flags.

For rx matching the bit fields compose with flags:

Spec flags mask
flags ign agreement 1 0x40
tc ign 0xfe
role ign 0xf3

Change to shared code

tc ign needed a change to the mask builder in src/ef.c. field_t gains two members: overlay marks a field that is a view of bits another field already covers, and rx_match_ign records that a field was given ign. After the existing mask loop, which is unchanged, an ignored overlay writes zeros over its own bits.

Only the 14 STP/RST flag aliases set overlay, so every other header's mask is unchanged. IFH headers also overlap fields, but as alternative encodings, so they must not get this behaviour. A test pins ifh-oc1 ign b1-rew-op 0 b0-masq-port ign to its current mask.

Testing

  • New test/test-llc.cxx, and test/test-stp.cxx with byte-for-byte BPDU layouts, every flag bit, rx masks and the IFH case.
  • Every commit builds without warnings and passes ctest on its own; the last passes 643 assertions.
  • rx masks for 30 specs across the other headers (ipv4, tcp, ipv6, ptp, mrp, oam, coap, IFH, ...) are byte-identical to master.
  • Fuzz dictionary entries and argc-frame corpus seeds for the new headers. The fuzzers themselves were not run.

Not covered: MST BPDUs (802.1Q clause 14.6, version 3).

Igor Gudich added 4 commits October 20, 2026 15:04
Frames carrying an 802.2 LLC header are not Ethernet II: the type/length
field holds a length, not an EtherType. Without an llc header the only
way to express one is to hand-compute the length and pass the whole
LLC payload as an opaque hex blob:

  eth ... et 0x0026 data hex 4242030000...

which cannot be matched per field and has to be recomputed by hand
whenever anything above it changes size.

llc_fill_defaults sums its own 3 bytes plus every header above it and
writes the result into the "et" field of the header below, following
ipv4_fill_defaults for the summation and ef-icmp.c for reaching one
step down the stack. That covers a VLAN tag between eth and llc too,
since the tag carries the same field name last.

Nothing was needed to stop eth_fill_defaults from clobbering the
length: frame_to_buf runs frame_fill_defaults from the top of the stack
downwards, so by the time ether_type_fill_defaults looks at "et" the
value is already set and it returns early. An explicit "et" on the
command line still wins, for the same reason.

A computed length above 1500 is refused rather than written. 802.3
reads 1536 and up as an EtherType, so "llc stp data repeat 1500 0"
would otherwise emit 0x0602 and a receiver would misparse it, and past
65535 the value would not fit parse_bytes(buf, 2) at all. An explicit
"et" still wins, so a test can build the malformed frame on purpose.

DSAP, SSAP and control default to the STP SAPs (0x42/0x42/0x03), the
only current user.
IEEE 802.1D-1998 clause 9.3 Bridge Protocol Data Units, on top of the
llc header added in the previous commit:

  ef hex eth dmac 01:80:c2:00:00:00 smac 00:11:22:33:44:55 \
         llc stp root_prio 0x8000 tc 1

Configuration and TCN are two header templates rather than one header
with the trailing fields suppressed. The shrink-the-header trick in
ef-sv.c and ef-mld.c drops a trailing optional part of one message; a
TCN is a different message whose only content is the four-byte common
part, and suppressing 31 of 35 fields would leave every config field
name accepted on a TCN command line and silently ignored. The MRP
precedent of one template per message type is the better fit.

Each bridge identifier is split into its priority and MAC halves
(root_prio/root_mac, bridge_prio/bridge_mac) so either can be asserted
on its own.

The four timer fields are exposed as their raw 16-bit wire value rather
than converted to seconds, so a test can write the exact octets; the
unit is in each field's help text. Their defaults, and those of the
bridge identifiers, describe a root bridge advertising the table 8-3
recommended timers.

tc and tcack are 1-bit fields aliased onto bit 0 and bit 7 of flags via
explicit bit_offset, so either the whole byte or a single flag can be
set. Under rx matching the two compose: "flags ign tc 1" masks the byte
down to bit 0 and still requires it set.
Only bit 0 (tc) and bit 7 (tcack) were named. The six bits between them
are what distinguishes the rapid protocol from classic STP -- they carry
the proposal/agreement handshake of 802.1Q clause 13.35 and the sender's
port role -- and none of that could be expressed except by spelling out
the whole byte:

  ef hex eth llc stp flags 0x3e

which says nothing about which bits matter. The eight fields of 802.1Q
table 13-5 are now all named: proposal, role, learning, forwarding and
agreement alongside the existing two.

role is two bits wide rather than two more one-bit aliases, since the
pair is a single enumerated value: 0 unknown, 1 alternate or backup,
2 root, 3 designated.

Every alias carries rx_match_skip so that it only enters the receive
mask when it is named. Without it an alias is masked in by default, and
"flags ign" cannot narrow the byte: it already produced a mask of 0x81
rather than the 0x01 its own test comment described, and with six more
aliases would have produced 0xff, making the "ign" useless. The
assertion is corrected to the value the comment always claimed.

rx_match_skip alone would make "tc ign" a no-op: an ignored field
writes nothing to the mask, so the flags byte around it would still be
compared in full and a BPDU differing only in tc rejected. The aliases
are therefore marked as overlays, and an overlay given "ign" (tracked
by a new rx_match_ign) writes zeros over its bits once the rest of the
mask is built. "tc ign" compares every flag bit but tc, and "flags ign
agreement 1" still compares agreement alone.

Only overlays do this. IFH headers overlap fields too, but as
alternative encodings of the same bits, and there an ignored field must
not erase the mask of one that is compared: "ifh-oc1 ign b1-rew-op 0
b0-masq-port ign" keeps b1-rew-op compared in full. Every header without
overlay fields gets the mask it had before, byte for byte.
The rapid protocol's own BPDU could not be built. Setting "ver 2" on the
stp header changed the version octet and nothing else, leaving a 35-octet
body where 802.1Q clause 14.4 requires 36 -- the configuration BPDU's 35
followed by the Version 1 Length, transmitted as 0. The receive rule in
that clause is version 2, type 0x02 and 36 or more octets, so what the
stp header produced was discarded as malformed by anything implementing
it:

  ef hex eth dmac 01:80:c2:00:00:00 smac 00:11:22:33:44:55 \
         llc stp_rst role 3 proposal 1 learning 1 forwarding 1

A third template rather than a longer HDR_STP, for two reasons. A
permanently 36-byte HDR_STP would make every configuration BPDU
malformed instead. Growing it on demand from a frame_fill_defaults hook
is not available either: a hook can shrink a header, which is what
ef-sv.c does to drop a trailing TLV, but frame_mask_to_buf() never runs
those hooks, so the value buf and the mask buf would disagree on the
header's size and rx matching would write past the shorter one. The
existing argument for keeping stp and stp_tcn apart therefore carries
over unchanged, and the MRP precedent of one template per message type
now holds for three.

The field list is duplicated rather than shared, following ef-mrp.c and
ef-oam.c, where the common fields of four and five templates are spelled
out in each. A shared array cannot work here regardless: def_offset()
assigns bit_offset per header, so one array behind two templates would
have the second pass overwrite the first's offsets.

ver and type default to 2 and 0x02, and v1_len to 0; everything else
carries the same defaults as the configuration BPDU, so the two differ
on the wire only where the standard says they should.
@igudich
igudich requested a review from jeso-mchp October 20, 2026 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant