Skip to content

feat: v3.2.0 — real UDP socket + new L2 raw-Ethernet transport - #133

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

feat: v3.2.0 — real UDP socket + new L2 raw-Ethernet transport#133
SoundMatt merged 1 commit into
mainfrom
feat/l2-transport

Conversation

@SoundMatt

Copy link
Copy Markdown
Owner

Summary

This crate's transport layer had two real gaps, confirmed by direct inspection rather than assumption:

  1. No raw-Ethernet/L2 transport existed at all — the same gap every other RCP-family repo (go-RCP, cpp-RCP, c-RCP) has.
  2. No real UDP transport existed eithersrc/udp.rs's UdpSocket trait had no implementation over a real OS socket; only the in-process EchoUdp/QueuedUdpSocket test doubles existed, and src/bin/rcp.rs's own prior doc comment admitted this plainly.

TC18 §10.1 names both a layer-2 EtherType (0x22F0) and UDP/IP encapsulation ("described in Annex J", of the base IEEE 1722-2016 standard) as legal transports. This PR builds both as permanent, first-class, equally-supported options, closing all three real gaps at once. This is the first real network I/O this crate has ever shipped for RCP — a bigger milestone than the L2 work alone.

1. Real, Annex J-conformant UDP transport (src/udp.rs)

  • StdUdpSocket: a real UdpSocket implementation over a bound std::net::UdpSocket, corrected to IEEE 1722-2016 Annex J framing from the start (no legacy UDP wire format to preserve).
  • Every send_to prepends, and every recv_from strips, a 4-byte big-endian encapsulation sequence number (encode_annex_j_udp_payload/decode_annex_j_udp_payload) — a monotonic per-socket counter, with no invented receiver-side semantics (e.g. loss detection) beyond "increases".
  • New ANNEX_J_CONTROL_PORT (17221, StdUdpSocket::new_default_port's default) and ANNEX_J_CONTINUOUS_PORT (17220, named but unused — RCP is control-plane traffic).

Provenance, stated honestly: this crate has no access to the paywalled IEEE 1722-2016 standard text. The port numbers and the sequence-number field are taken from two independent public secondary sources — a Wireshark issue tracker discussion of the real Annex J framing, and the COVESA Open1722 open-source reference implementation's Avtp_Udp_t header struct (include/avtp/Udp.h, BSD-3-Clause) — flagged as such in the code, not presented with false certainty.

2. New L2 (raw Ethernet) transport, Linux only (src/l2.rs)

  • Mirrors udp.rs's own UdpSocket/UdpTransport abstraction one wire layer down: encode_ethernet_frame/decode_ethernet_frame (dest MAC + src MAC + EtherType 0x22F0 + AVTPDU directly — no encapsulation sequence number; that field is Annex J/UDP-specific and has no L2 counterpart), an L2Socket trait, an L2Transport client (send_acf_abb/send_acf_gbb, mirroring UdpTransport).
  • target_os = "linux" only: RawEthernetSocket, a real AF_PACKET/SOCK_RAW production L2Socket that reads its own interface's MAC via getifaddrs rather than requiring the caller to supply one (destination MAC is still caller-supplied — multicast-MAC derivation is a base-IEEE-1722 algorithm this crate doesn't have).
  • Every other target gets a same-named stub whose bind always returns a clear Err, not a silent no-op, so the type is referenceable unconditionally.
  • Server-side L2 dispatch (an L2RcServer mirroring UdpRcServer) is explicitly out of scope — flagged as a deliberate follow-up in l2.rs's own doc comment, not bundled in silently.

A flagged judgment call: this crate is #![forbid(unsafe_code)] crate-wide, which rules out a direct libc socket()/bind()/sendto()/recvfrom() implementation (would need unsafe extern "C" calls in this crate's own source; forbid cannot be locally overridden — E0453). RawEthernetSocket is instead built on the nix crate (new target_os = "linux"-only dependency), whose socket/bind/sendto/recvfrom/setsockopt/getifaddrs are all safe Rust fns — unsafe lives inside nix's own crate, never this one's. Confirmed against nix 0.31's published API before writing the module, not assumed.

3. CLI wiring (src/bin/rcp.rs)

New serve --udp <bind-ip> [--port <n>] [--stream <hex>] [--max-requests <n>] command — the first rust-rcp command backed by a real OS socket instead of an in-process RcServer invoked directly. Binds a real StdUdpSocket and runs UdpRcServer (previously only ever exercised against mock sockets in this crate's own unit tests) against it. The module doc comment's prior "no concrete UdpSocket implementation over a real OS socket" note is corrected. discover/register/endpoint remain deliberately ephemeral/in-process (unchanged, pre-existing, already-flagged limitation) — serve is additive, not a replacement.

4. Docs / requirements

  • ROADMAP.md intentionally not touched — post-v1.0.0 fix-pass PRs (v2.0.0, v3.0.0, v3.1.0) only ever touched CHANGELOG.md, not ROADMAP.md; verified against those PRs' actual diffs before following suit.
  • CHANGELOG.md: new ## v3.2.0 entry.
  • docs/PUBLIC_API.txt: regenerated via cargo +nightly public-api --simplifiedpurely additive diff, confirmed via scripts/api-snapshot-check.sh.
  • docs/SEMVER.md: l2 added to the Tier 2 ("transport bridges") list alongside udp.
  • .fusa-reqs.json: REQ-UDP-012REQ-UDP-014, REQ-L2-001REQ-L2-008, REQ-CLI-010 (12 new requirements, 564/564 traced).

MINOR version bump (3.1.0 → 3.2.0): every new item is additive pub surface, nothing existing changed shape.

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

All of the following were run directly, not assumed:

  • macOS host (rustc/clippy 1.97.1) and a Linux container (Rust 1.90, to actually exercise the target_os = "linux" nix-based code, which macOS can't compile): cargo build --all-targets, cargo test --all-targets (+ --release), cargo fmt --all -- --check, cargo clippy --all-targets --all-features -- -D warnings — all clean on both platforms for every file this PR touches. (Two pre-existing clippy::const_is_empty findings in unrelated src/lib.rs test code were independently confirmed present on origin/main too, with the identical Linux toolchain — a pre-existing toolchain-version issue, not something this PR introduced or fixed.)
  • Real veth round trip: created a real veth0/veth1 pair (ip link add veth0 type veth peer name veth1) inside the Linux container and ran the #[ignore]d real_raw_ethernet_socket_round_trips_a_frame_over_a_veth_pair test directly against it — a real RawEthernetSocket frame round-tripped byte-for-byte over a real (virtual) Ethernet link. Exercised both as a bare binary invocation and via sudo, matching exactly what the new l2-veth CI job does.
  • Real loopback UDP round trip: std_udp_socket_round_trips_over_real_loopback_socket and a full end-to-end test (std_udp_socket_and_udp_rc_server_serve_a_real_discovery_request_end_to_end) compose a real StdUdpSocket client against a real StdUdpSocket + UdpRcServer server over real loopback sockets — not mocks.
  • Wire bytes inspected directly: a dedicated test binds a bypass std::net::UdpSocket receiver (not StdUdpSocket) to inspect the raw encapsulated bytes and confirm the sequence-number counter actually increments 0, 1, 2 on the wire.
  • .fusa-reqs.json gap check: scripts/fusa-gap-check.sh564/564 (100%) requirements fully traced.
  • HARA / cyber gap checks: scripts/hara_asil_check.py and scripts/cyber-gap-check.sh both clean, unaffected by this change.
  • Coverage: cargo llvm-cov --all-targets --fail-under-lines 9094.76% overall (exit 0), comfortably above the CI gate. (l2.rs itself sits at ~70% in the non-privileged run since RawEthernetSocket's real-socket code path only runs under the #[ignore]d veth test — expected and by design, mirroring why that test is #[ignore]d in the first place.)
  • New l2-veth CI job logic validated directly: reproduced the job's exact cargo test --lib --no-run --message-format=jsonjq binary-path extraction → sudo <bin> ... --exact --ignored pipeline by hand inside the container against the real veth pair; it worked end-to-end before being committed to ci.yml.

Not merging

Per standing instruction, I am not merging this PR — leaving it open for review.

This crate's transport layer had two real gaps, confirmed by direct
inspection rather than assumption: no raw-Ethernet/L2 transport existed at
all (the same gap every other RCP-family repo — go-RCP, cpp-RCP, c-RCP —
has), and src/udp.rs's UdpSocket trait had no implementation over a real
OS socket either — only the in-process EchoUdp/QueuedUdpSocket test
doubles, and src/bin/rcp.rs's own prior doc comment admitted this
plainly. TC18 §10.1 names both a layer-2 EtherType (0x22F0) and UDP/IP
encapsulation ("described in Annex J", of the base IEEE 1722-2016
standard) as legal transports; this builds both as permanent, first-class,
equally-supported options, closing all three real gaps at once. This is
the first real network I/O this crate has ever shipped for RCP.

- src/udp.rs gains StdUdpSocket, a real UdpSocket implementation over a
  bound std::net::UdpSocket, corrected to IEEE 1722-2016 Annex J framing
  from the start. Every send_to prepends, and every recv_from strips, a
  4-byte big-endian encapsulation sequence number
  (encode_annex_j_udp_payload/decode_annex_j_udp_payload) — a monotonic
  per-socket counter with no invented receiver-side semantics beyond
  that. New ANNEX_J_CONTROL_PORT (17221, the default) and
  ANNEX_J_CONTINUOUS_PORT (17220) constants. Provenance: this crate has
  no access to the paywalled IEEE 1722-2016 standard text — the port
  numbers and sequence-number field are taken from two independent
  public secondary sources (a Wireshark issue tracker discussion, and
  the COVESA Open1722 reference implementation's Avtp_Udp_t header
  struct), flagged as such rather than presented with false certainty.

- New src/l2.rs — a raw-Ethernet (layer 2) transport, Linux only,
  mirroring udp.rs's own UdpSocket/UdpTransport abstraction one wire
  layer down: encode_ethernet_frame/decode_ethernet_frame (dest MAC + src
  MAC + EtherType 0x22F0 + AVTPDU directly, no encapsulation sequence
  number — that field is Annex J/UDP-specific), an L2Socket trait, an
  L2Transport client, and — target_os = "linux" only — RawEthernetSocket,
  a real AF_PACKET/SOCK_RAW production L2Socket that reads its own
  interface's MAC via getifaddrs rather than requiring the caller to
  supply one. Every other target gets a same-named stub whose bind always
  returns a clear Err rather than silently no-op-ing.

- This crate is #![forbid(unsafe_code)] crate-wide, which rules out a
  direct libc socket()/bind()/sendto()/recvfrom() implementation (would
  need unsafe extern "C" calls; forbid cannot be locally overridden).
  RawEthernetSocket is instead built on the nix crate (new
  target_os = "linux"-only dependency), whose socket/bind/sendto/recvfrom/
  setsockopt/getifaddrs are all safe Rust fns — unsafe lives inside nix's
  own crate, never this one's. Flagged in src/l2.rs's own module doc
  comment as a deliberate judgment call.

- src/bin/rcp.rs gains a new `serve --udp <bind-ip> [--port <n>]
  [--stream <hex>] [--max-requests <n>]` command — the first rust-rcp
  command backed by a real OS socket instead of an in-process RcServer
  invoked directly: it binds a real StdUdpSocket and runs UdpRcServer
  (previously only ever exercised against mock sockets) against it. The
  module doc comment's prior "no concrete UdpSocket implementation over a
  real OS socket" note is corrected accordingly.

- New tests: pure byte-manipulation round trips for both Annex J
  encapsulation and Ethernet framing (no socket/privileges), mock-backed
  L2Transport/UdpTransport request/response tests, real loopback
  StdUdpSocket round trips (including a real end-to-end
  StdUdpSocket+UdpRcServer discovery request), and a new Linux-only CI job
  (l2-veth) that creates a real veth0/veth1 pair and runs a real
  RawEthernetSocket frame round trip under sudo.

MINOR release: StdUdpSocket, the Annex J constants/functions, and the
entire new l2 module are new pub items only. docs/PUBLIC_API.txt
regenerated (purely additive diff); .fusa-reqs.json gains
REQ-UDP-012..014, REQ-L2-001..008, REQ-CLI-010 (564/564 traced).

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