feat: v3.2.0 — real UDP socket + new L2 raw-Ethernet transport - #133
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This crate's transport layer had two real gaps, confirmed by direct inspection rather than assumption:
go-RCP,cpp-RCP,c-RCP) has.src/udp.rs'sUdpSockettrait had no implementation over a real OS socket; only the in-processEchoUdp/QueuedUdpSockettest doubles existed, andsrc/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 realUdpSocketimplementation over a boundstd::net::UdpSocket, corrected to IEEE 1722-2016 Annex J framing from the start (no legacy UDP wire format to preserve).send_toprepends, and everyrecv_fromstrips, 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".ANNEX_J_CONTROL_PORT(17221,StdUdpSocket::new_default_port's default) andANNEX_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_theader 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)udp.rs's ownUdpSocket/UdpTransportabstraction one wire layer down:encode_ethernet_frame/decode_ethernet_frame(dest MAC + src MAC + EtherType0x22F0+ AVTPDU directly — no encapsulation sequence number; that field is Annex J/UDP-specific and has no L2 counterpart), anL2Sockettrait, anL2Transportclient (send_acf_abb/send_acf_gbb, mirroringUdpTransport).target_os = "linux"only:RawEthernetSocket, a realAF_PACKET/SOCK_RAWproductionL2Socketthat reads its own interface's MAC viagetifaddrsrather 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).bindalways returns a clearErr, not a silent no-op, so the type is referenceable unconditionally.L2RcServermirroringUdpRcServer) is explicitly out of scope — flagged as a deliberate follow-up inl2.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 directlibcsocket()/bind()/sendto()/recvfrom()implementation (would needunsafe extern "C"calls in this crate's own source;forbidcannot be locally overridden — E0453).RawEthernetSocketis instead built on thenixcrate (newtarget_os = "linux"-only dependency), whosesocket/bind/sendto/recvfrom/setsockopt/getifaddrsare all safe Rustfns —unsafelives insidenix's own crate, never this one's. Confirmed againstnix0.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 firstrust-rcpcommand backed by a real OS socket instead of an in-processRcServerinvoked directly. Binds a realStdUdpSocketand runsUdpRcServer(previously only ever exercised against mock sockets in this crate's own unit tests) against it. The module doc comment's prior "no concreteUdpSocketimplementation over a real OS socket" note is corrected.discover/register/endpointremain deliberately ephemeral/in-process (unchanged, pre-existing, already-flagged limitation) —serveis additive, not a replacement.4. Docs / requirements
ROADMAP.mdintentionally not touched — post-v1.0.0fix-pass PRs (v2.0.0,v3.0.0,v3.1.0) only ever touchedCHANGELOG.md, notROADMAP.md; verified against those PRs' actual diffs before following suit.CHANGELOG.md: new## v3.2.0entry.docs/PUBLIC_API.txt: regenerated viacargo +nightly public-api --simplified— purely additive diff, confirmed viascripts/api-snapshot-check.sh.docs/SEMVER.md:l2added to the Tier 2 ("transport bridges") list alongsideudp..fusa-reqs.json:REQ-UDP-012–REQ-UDP-014,REQ-L2-001–REQ-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
pubsurface, nothing existing changed shape.Self-verification (real bytes/frames, not just "tests pass")
All of the following were run directly, not assumed:
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-existingclippy::const_is_emptyfindings in unrelatedsrc/lib.rstest code were independently confirmed present onorigin/maintoo, with the identical Linux toolchain — a pre-existing toolchain-version issue, not something this PR introduced or fixed.)veth0/veth1pair (ip link add veth0 type veth peer name veth1) inside the Linux container and ran the#[ignore]dreal_raw_ethernet_socket_round_trips_a_frame_over_a_veth_pairtest directly against it — a realRawEthernetSocketframe round-tripped byte-for-byte over a real (virtual) Ethernet link. Exercised both as a bare binary invocation and viasudo, matching exactly what the newl2-vethCI job does.std_udp_socket_round_trips_over_real_loopback_socketand a full end-to-end test (std_udp_socket_and_udp_rc_server_serve_a_real_discovery_request_end_to_end) compose a realStdUdpSocketclient against a realStdUdpSocket+UdpRcServerserver over real loopback sockets — not mocks.std::net::UdpSocketreceiver (notStdUdpSocket) to inspect the raw encapsulated bytes and confirm the sequence-number counter actually increments 0, 1, 2 on the wire..fusa-reqs.jsongap check:scripts/fusa-gap-check.sh→564/564 (100%) requirements fully traced.scripts/hara_asil_check.pyandscripts/cyber-gap-check.shboth clean, unaffected by this change.cargo llvm-cov --all-targets --fail-under-lines 90→ 94.76% overall (exit 0), comfortably above the CI gate. (l2.rsitself sits at ~70% in the non-privileged run sinceRawEthernetSocket'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.)l2-vethCI job logic validated directly: reproduced the job's exactcargo test --lib --no-run --message-format=json→jqbinary-path extraction →sudo <bin> ... --exact --ignoredpipeline by hand inside the container against the real veth pair; it worked end-to-end before being committed toci.yml.Not merging
Per standing instruction, I am not merging this PR — leaving it open for review.