Skip to content

feat: v2.21.0 — L2 (native Ethernet) transport + UDP Annex J conformance fix - #99

Merged
SoundMatt merged 3 commits into
mainfrom
feat/l2-transport
Aug 1, 2026
Merged

SoundMatt merged 3 commits into
mainfrom
feat/l2-transport

Conversation

@SoundMatt

Copy link
Copy Markdown
Owner

Summary

ROADMAP.md milestone 65. Adds a real Layer-2 (raw Ethernet) transport alongside a corrected UDP transport — both are permanent, first-class, equally supported options; neither deprecates the other.

Provenance caveat (applies to every Annex J detail below): this codebase has no verified access to the paywalled IEEE 1722-2016 standard text. The Annex J details here rest on two independent public secondary sources: a Wireshark issue tracker discussion of the real Annex J wire text, and the COVESA Open1722 open-source reference implementation's Avtp_Udp_t header (include/avtp/Udp.h, BSD-3-Clause, github.com/COVESA/Open1722). "Annex J conformant" below means conformant with these two sources' reading, not verified against the primary standard.

1. rcp/udp.hpp Annex J conformance fix

  • Both secondary sources agree an Annex J UDP payload begins with a 4-byte big-endian encapsulation sequence number before the AVTPDU — udp::Frame/MultiFrame never had this. New encode_annexj_datagram/decode_annexj_datagram are the pure codec for that envelope; Server/Client now transparently prepend/strip it around the existing AVTPDU encode/decode path. Both track and expose the value they send/receive (Client::last_sent_encap_seq/last_recv_encap_seq, Server::last_recv_encap_seq(client)) for a future caller's own use (e.g. loss detection) — no such detection is implemented, and the field's exact intended semantics are not claimed as verified.
  • Standard destination UDP ports are 17220 (Continuous/streaming) and 17221 (Discrete/control). RCP request/response/acknowledge traffic is control-plane, so udp::kAnnexJControlPort (17221) is now the default port via a defaulted constructor parameter, while the explicit-port constructor form is unchanged for back-compat/testing.

2. rcp/l2.hpp (new): native IEEE 1722-over-Ethernet transport

The transport udp.hpp's own header comment always named as the alternative to Annex J but never implemented, now built:

  • Wire frame: destination MAC (6) + source MAC (6, auto-read from the caller-specified interface via SIOCGIFHWADDR) + EtherType 0x22F0 (2) + the AVTPDU directly — no encapsulation sequence number (that field is Annex J/UDP-specific, not part of native L2 framing).
  • l2::Server/l2::Client are structurally parallel to udp::Server/udp::Client — same method names, same Handler signature, same request/response correlation strategy — with no new Transport interface introduced (this codebase has none today; matching shapes without formal inheritance matches its existing style).
  • Linux-only (AF_PACKET/SOCK_RAW, needs CAP_NET_RAW or root — documented in the header). Every other platform gets the same "compiles cleanly, every operation returns std::errc::function_not_supported" stub udp.hpp's own Windows branch already established as precedent.
  • Deliberate structural deviation from udp.hpp: the pure Ethernet-header/Frame/MultiFrame codec is available unconditionally on every platform (not gated behind the socket-availability guard), specifically so tests/test_l2.cpp can exercise it everywhere with no privileges and no Linux requirement — documented explicitly in the header comment.
  • The caller supplies the destination MAC (unicast or multicast) — this header does not derive or allocate a multicast MAC itself; that mapping (if IEEE 1722 defines one) lives in the base standard, not available here.

3. Tests

  • tests/test_udp.cpp: new pure tests for encode_annexj_datagram/decode_annexj_datagram (round-trip, rejects short buffer, monotonic increment), a real Client/Server integration test proving the sequence number increments across successive requests and the Server observes it, a default-port test, plus the existing raw-socket multi-datagram test updated to speak the new encapsulated wire format.
  • tests/test_l2.cpp (new): pure EthHeader/Frame/MultiFrame/combined-L2-frame codec tests — no socket, runs on every platform (macOS/Windows exercise the stub instead).
  • tests/l2_veth_roundtrip.cpp (new): standalone (non-ctest) program driving a real l2::Server/l2::Client pair over a real Linux interface pair. Built by tests/CMakeLists.txt but deliberately not registered via add_test — needs CAP_NET_RAW/root and real interfaces, neither of which the normal ctest run has or should require.
  • New l2-veth CI job (.github/workflows/ci.yml): creates a real veth pair with sudo, builds l2_veth_roundtrip, and runs it against veth0/veth1, asserting a real frame round-trips byte-for-byte.

4. Docs/requirements

Self-verification (real bytes, not just "tests pass")

  • macOS (native, this environment): full build (Debug + Release, RELAY_BUILD_CLI=ON) and ctest — 53/53 suites pass (up from 52; test_l2 is new), 116 assertions in test_udp, 43 in test_l2 (macOS exercises the L2 stub).
  • Linux (Docker, ubuntu:22.04, GCC 11 and Clang 14, C++17 and C++20): full build clean, ctest 53/53 pass; test_l2 runs the real (non-stub) pure-codec path (40 assertions, no socket).
  • Real veth round trip (Linux container with --cap-add=NET_ADMIN --cap-add=NET_RAW): created a real veth0/veth1 pair, ran l2_veth_roundtrip with an 8-byte payload — exit 0, payload round-tripped byte-for-byte, confirmed clean shutdown, and repeated 3x with no flakiness. This surfaced and fixed a real bug during development: AF_PACKET sockets don't support shutdown() on Linux (silent no-op, unlike udp.hpp's AF_INET sockets), so close() was hanging forever after a successful round trip — fixed with a 200ms SO_RCVTIMEO poll loop (detail::set_recv_timeout).
  • Privilege/graceful-failure check: ran the same harness as a non-root user with no CAP_NET_RAW — failed cleanly with a clear diagnostic and exit code 1, no crash/hang, confirming the documented CAP_NET_RAW/root requirement and the "construct, then check ok()" contract.
  • One pre-existing -Wnonnull GCC warning under -DCMAKE_BUILD_TYPE=Release on this environment's specific GCC 16.1.0 toolchain was traced back and confirmed (via git stash) to already exist on unmodified main, unrelated to this change — not something this PR introduced or needed to fix (no -Werror for GCC/Clang in this repo; only MSVC's /WX and clang-tidy's own --warnings-as-errors gate, neither of which this warning affects).

Not merging — leaving for review per repo process.

…nce fix

ROADMAP.md milestone 65. Two parts, per two independent public secondary
sources (a Wireshark issue tracker discussion of the real Annex J wire
text, and the COVESA Open1722 open-source reference implementation's
Avtp_Udp_t header, github.com/COVESA/Open1722) — this repo has no verified
access to the paywalled IEEE 1722-2016 standard text itself, so neither
part is presented as verified against the primary standard:

- rcp/udp.hpp Annex J conformance fix: an Annex J UDP payload actually
  begins with a 4-byte big-endian encapsulation sequence number before the
  AVTPDU, which udp::Frame/MultiFrame never had — encode_annexj_datagram/
  decode_annexj_datagram add that envelope, and Server/Client now
  transparently prepend/strip it. The standard destination UDP ports are
  17220 (Continuous) and 17221 (Discrete/control); RCP traffic is
  control-plane, so udp::kAnnexJControlPort (17221) is now the default port
  via a defaulted constructor parameter, with the explicit-port form
  unchanged for back-compat/testing.

- rcp/l2.hpp (new): the native IEEE 1722-over-Ethernet transport
  udp.hpp's own header comment always described as the alternative to
  Annex J, now actually built — destination MAC + source MAC (auto-read via
  SIOCGIFHWADDR) + EtherType 0x22F0 + the AVTPDU directly, no encapsulation
  sequence number (that field is Annex J/UDP-specific). l2::Server/
  l2::Client are structurally parallel to udp::Server/udp::Client (same
  method names, same Handler signature) with no new Transport interface.
  Linux-only (AF_PACKET/SOCK_RAW, needs CAP_NET_RAW/root); every other
  platform gets the same function_not_supported stub udp.hpp's own Windows
  branch already established. The pure Ethernet-header/Frame/MultiFrame
  codec is available on every platform unconditionally (unlike udp.hpp's
  own structure) so tests/test_l2.cpp can exercise it with no privileges
  and no Linux requirement.

Neither transport deprecates or is preferred over the other; both are
permanent, equally supported options.

Verification: tests/l2_veth_roundtrip.cpp is a new standalone (non-ctest)
program that drives a real l2::Server/l2::Client pair over a real Linux
veth interface pair, run by the new l2-veth CI job
(.github/workflows/ci.yml), which creates the veth pair with sudo and
asserts a real Ethernet frame's payload round-trips byte-for-byte. Verified
directly in a Linux container during development, including a genuine
correctness fix that verification surfaced: AF_PACKET sockets do not
support shutdown() on Linux (silently a no-op, unlike the AF_INET sockets
rcp/udp.hpp uses), so l2::Server/l2::Client's serve/read threads would
otherwise hang forever in close() after a request/response had already
round-tripped correctly — fixed via a short SO_RCVTIMEO
(detail::set_recv_timeout, 200ms) so those threads periodically recheck
their own closed flag.

Full local build (C++17/C++20, GCC 11/12, Clang 14, plus macOS) and ctest
(53/53 suites, up from 52) pass. kVersion/CMakeLists.txt bump 2.20.0 →
2.21.0.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Frame::sequence_num/MultiFrame::sequence_num are uint16_t, matching
rcp/udp.hpp's identical fields, but avtp::TscfHeader/NtscfHeader's own
sequence_num is the real 8-bit wire field (avtp.hpp's own header
comment: "8 bits, not 16"). udp.hpp's identical assignment happens to
not trip MSVC's /W4 today, but l2.hpp's did, failing the Windows
build. Added the same explicit static_cast<uint8_t> this codebase
already uses elsewhere for intentional narrowing; behavior is
unchanged (encode_tscf_header/encode_ntscf_header already only ever
wrote the low 8 bits either way). Full local suite still 53/53.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
…ER018 false positive)

cpfusa's CYBER018 rule flags any printf/fprintf/sprintf/vprintf call
whose first argument isn't a string literal -- correct for printf's
own signature (format string first), but a false positive against
fprintf's real signature (FILE* stream first, format string second):
every fprintf(stderr, "literal", ...) call in this file was flagged
even though the actual format string was always a literal. Confirmed
by reading cpfusa's own regex source
(src/cyber/cyber.cpp:154-159, v0.18.0) rather than assuming.

No other file in this repo uses fprintf, so there's no established
workaround to match -- switched this file's three fprintf(stderr, ...)
call sites (plus the CHECK macro) to std::cerr, which has no
format-string argument at all and sidesteps the false positive by
construction rather than silencing the finding. Verified against
cpfusa's actual regex (not just assumed fixed) and confirmed the
binary's behavior is unchanged (macOS smoke test: correct message and
exit code on the non-Linux early-return path). Full local suite still
53/53.

The regex bug itself is being filed as a separate issue against
cpp-FuSa (SoundMatt/cpp-FuSa) -- this commit works around it here,
not there.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
@SoundMatt
SoundMatt merged commit d50e51d into main Aug 1, 2026
22 checks passed
@SoundMatt
SoundMatt deleted the feat/l2-transport branch August 1, 2026 01:41
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