Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -4932,6 +4932,114 @@
"level": "HLR",
"asil": "ASIL-B",
"verificationMethod": "test"
},
{
"id": "REQ-UDP-012",
"title": "Annex J UDP encapsulation sequence number is prepended/stripped correctly",
"text": "encode_annex_j_udp_payload(seq, avtpdu) prepends seq as 4 big-endian octets before avtpdu with no other framing; decode_annex_j_udp_payload(buf) is its exact inverse, returning (seq, avtpdu_bytes) for any buf of at least 4 bytes and Err(RcpError::ShortFrame) for fewer than 4 bytes; ANNEX_J_CONTROL_PORT (17221) and ANNEX_J_CONTINUOUS_PORT (17220) are distinct constants documented as taken from public secondary sources (a Wireshark issue tracker discussion and the COVESA Open1722 reference implementation), not the paywalled IEEE 1722-2016 primary standard text",
"standard": "iso26262",
"level": "HLR",
"asil": "ASIL-B",
"verificationMethod": "test"
},
{
"id": "REQ-UDP-013",
"title": "StdUdpSocket::send_to encapsulates outgoing datagrams with a monotonically increasing sequence number",
"text": "StdUdpSocket is a real UdpSocket implementation over a bound std::net::UdpSocket; StdUdpSocket::bind/new_default_port construct it against a real OS socket, and every StdUdpSocket::send_to call prepends the current value of a per-instance monotonically increasing u32 counter (starting at 0) via encode_annex_j_udp_payload before writing to the real socket, with no receiver-side semantics (e.g. loss detection) invented or implied for that counter beyond monotonic increase",
"standard": "iso26262",
"level": "HLR",
"asil": "ASIL-B",
"verificationMethod": "test"
},
{
"id": "REQ-UDP-014",
"title": "StdUdpSocket::recv_from strips the Annex J sequence number and maps a real socket timeout to RcpError::Timeout",
"text": "StdUdpSocket::recv_from applies timeout via the real socket's SO_RCVTIMEO (None blocks indefinitely), strips the leading 4-byte Annex J sequence number from whatever datagram is received via decode_annex_j_udp_payload before returning the remaining AVTPDU bytes to the caller, and maps a real OS-level receive timeout (WouldBlock/TimedOut) to Err(RcpError::Timeout) rather than propagating the raw std::io::Error",
"standard": "iso26262",
"level": "HLR",
"asil": "ASIL-B",
"verificationMethod": "test"
},
{
"id": "REQ-L2-001",
"title": "ETHERTYPE_AVTP names the real IEEE 1722 EtherType and is placed big-endian in the Ethernet header",
"text": "ETHERTYPE_AVTP is 0x22F0, matching TC18 \u00a710.1 (\u201can AVTPDU is marked by an EtherType value of 0x22F0\u201d); encode_ethernet_frame writes it as bytes 12-13 of the frame in big-endian order, and decode_ethernet_frame reads bytes 12-13 the same way when validating a frame's EtherType",
"standard": "iso26262",
"level": "HLR",
"asil": "ASIL-B",
"verificationMethod": "test"
},
{
"id": "REQ-L2-002",
"title": "encode_ethernet_frame/decode_ethernet_frame round-trip a raw Ethernet II frame with no encapsulation sequence number",
"text": "encode_ethernet_frame(dest_mac, src_mac, avtpdu) produces dest_mac (6 bytes) || src_mac (6 bytes) || ETHERTYPE_AVTP (2 bytes, big-endian) || avtpdu directly, with no additional framing (unlike crate::udp::encode_annex_j_udp_payload's 4-byte sequence number, which has no L2 counterpart); decode_ethernet_frame is its exact inverse for any well-formed input, returns Err(RcpError::ShortFrame) for fewer than 14 bytes, and returns Err(RcpError::Other(_)) (not a panic) when the EtherType field is not ETHERTYPE_AVTP",
"standard": "iso26262",
"level": "HLR",
"asil": "ASIL-B",
"verificationMethod": "test"
},
{
"id": "REQ-L2-003",
"title": "L2Socket trait mirrors UdpSocket's testable-abstraction shape for MAC-addressed raw Ethernet frames",
"text": "L2Socket::send(frame) and L2Socket::recv(timeout) operate on already-framed encode_ethernet_frame/decode_ethernet_frame bytes, the same already-framed-bytes-in-bytes-out contract crate::udp::UdpSocket documents, letting L2Transport be tested against a mock L2Socket with no real socket involved",
"standard": "iso26262",
"level": "HLR",
"asil": "ASIL-B",
"verificationMethod": "test"
},
{
"id": "REQ-L2-004",
"title": "L2Transport is constructed with a local StreamId, an L2Socket, a caller-supplied destination MAC, and this transport's own source MAC",
"text": "L2Transport::new(local_stream, socket, dest_mac, src_mac) stores all four; L2Transport::local_stream/dest_mac/src_mac each return exactly the value passed to the constructor; dest_mac may be unicast or multicast and is always a caller input, since this crate does not derive or allocate a multicast MAC of its own",
"standard": "iso26262",
"level": "HLR",
"asil": "ASIL-B",
"verificationMethod": "test"
},
{
"id": "REQ-L2-005",
"title": "L2Transport::send_acf_abb/send_acf_gbb reject a zero timeout immediately, matching UdpTransport's own discipline",
"text": "L2Transport::send_acf_abb and L2Transport::send_acf_gbb both return Err(RcpError::Timeout) immediately, without ever calling the underlying L2Socket, when passed Some(Duration::ZERO), mirroring crate::udp::UdpTransport::send_acf_abb's own zero-timeout short-circuit",
"standard": "iso26262",
"level": "HLR",
"asil": "ASIL-B",
"verificationMethod": "test"
},
{
"id": "REQ-L2-006",
"title": "L2Transport::send_acf_abb/send_acf_gbb build an NTSCF/AVTPDU frame, wrap it as a raw Ethernet frame, and verify the response echoes byte_bus_id",
"text": "L2Transport::send_acf_abb/send_acf_gbb encode the request via crate::acf::encode_acf_abb/encode_acf_gbb, wrap it in an NTSCF frame via crate::avtp::encode_ntscf_frame under this transport's local_stream, wrap that in a raw Ethernet frame via encode_ethernet_frame addressed to dest_mac from src_mac, send it, decode the received response through decode_ethernet_frame/crate::avtp::decode_ntscf_frame/crate::acf::decode_acf_abb (or decode_acf_gbb), and reject it via crate::acf::verify_echo_back if the response's byte_bus_id does not match the request's",
"standard": "iso26262",
"level": "HLR",
"asil": "ASIL-B",
"verificationMethod": "test"
},
{
"id": "REQ-L2-007",
"title": "RawEthernetSocket (Linux) binds an AF_PACKET/SOCK_RAW socket to a named interface, reading that interface's own MAC rather than requiring the caller to supply one",
"text": "On target_os = \"linux\", RawEthernetSocket::bind(interface_name) opens a real AF_PACKET/SOCK_RAW socket (requiring CAP_NET_RAW or root) and binds it to interface_name, reading that interface's own link-layer address via getifaddrs for RawEthernetSocket::mac() rather than accepting a caller-supplied MAC; RawEthernetSocket::send/recv implement L2Socket over that real socket and round-trip a real Ethernet frame byte-for-byte over a veth pair",
"standard": "iso26262",
"level": "HLR",
"asil": "ASIL-B",
"verificationMethod": "test"
},
{
"id": "REQ-L2-008",
"title": "RawEthernetSocket exists and fails explicitly, never silently, on every non-Linux target",
"text": "On any target where target_os != \"linux\", crate::l2::RawEthernetSocket still exists as a type (so downstream code can reference it unconditionally) but RawEthernetSocket::bind always returns Err(RcpError::Other(_)) explaining that AF_PACKET raw sockets are a Linux-specific facility, rather than silently no-op-ing or panicking",
"standard": "iso26262",
"level": "HLR",
"asil": "ASIL-B",
"verificationMethod": "test"
},
{
"id": "REQ-CLI-010",
"title": "serve --udp <bind-ip> [--port <n>] [--stream <hex>] [--max-requests <n>] runs UdpRcServer over a real StdUdpSocket",
"text": "The CLI must, when invoked with serve --udp <bind-ip>, bind a real rcp::udp::StdUdpSocket to <bind-ip>:--port (default rcp::udp::ANNEX_J_CONTROL_PORT) and dispatch inbound requests through rcp::udp::UdpRcServer::serve_one against a fresh RcServer, stopping after --max-requests requests have been served if given (default: unbounded, until a fatal socket error), exiting 3 on a bind or fatal serve error",
"standard": "iso26262",
"level": "HLR",
"asil": "ASIL-B",
"verificationMethod": "test"
}
]
}
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,36 @@ jobs:
- name: test release
run: cargo test --all-targets --release --locked

# ── L2 (raw Ethernet) real veth round trip (Linux only) ───────────────────
# `src/l2.rs`'s RawEthernetSocket needs CAP_NET_RAW (or root) to open an
# AF_PACKET/SOCK_RAW socket at all — not exercisable by the normal `test`
# job above, which is why the round-trip test itself is #[ignore]d by
# default. This job creates a real veth0/veth1 pair and runs that one
# test under sudo against it, proving a real frame round-trips
# byte-for-byte over a real (virtual) Ethernet link — not just that the
# framing/trait logic type-checks.
l2-veth:
name: L2 raw-Ethernet veth round trip (Linux)
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: create veth0/veth1 pair
run: |
sudo ip link add veth0 type veth peer name veth1
sudo ip link set veth0 up
sudo ip link set veth1 up
- name: build l2 tests
run: cargo test --lib --no-run --locked --message-format=json > build.json
- name: run real_raw_ethernet_socket_round_trips_a_frame_over_a_veth_pair under sudo
run: |
bin=$(jq -r 'select(.profile.test == true and (.target.name == "rcp")) | .filenames[]' build.json | head -n1)
test -n "$bin"
sudo "$bin" l2::tests::real_raw_ethernet_socket_round_trips_a_frame_over_a_veth_pair \
--exact --ignored --nocapture

# ── Coverage ──────────────────────────────────────────────────────────────
coverage:
name: Coverage (llvm-cov)
Expand Down
119 changes: 119 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,125 @@ the roadmap milestone that produced them (see `ROADMAP.md`), since this
crate's `Cargo.toml` version does not move until the OPEN Alliance TC18
core replacement reaches `v1.0.0`.

## v3.2.0 (2026-07-31 real UDP socket + new L2 raw-Ethernet transport) — closed

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 item builds both as
permanent, first-class, equally-supported options, closing all three real
gaps rather than just one — this is the first real network I/O this crate
has ever shipped for RCP.

- **rust-RCP-NET-01 (feature):** `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 (there was 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 per-`StdUdpSocket` monotonically
increasing counter with no invented receiver-side semantics (e.g. loss
detection) beyond that. New constants `ANNEX_J_CONTROL_PORT` (17221,
the applicable port for RCP's control-plane request/response/
acknowledgement traffic, and `StdUdpSocket::new_default_port`'s
default) and `ANNEX_J_CONTINUOUS_PORT` (17220, streaming traffic, named
but unused). **Provenance note**, stated once here and referenced from
every touchpoint in code: 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 instead
— 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,
<https://github.com/COVESA/Open1722>) — and are flagged as such rather
than presented with false certainty. New `REQ-UDP-012`/`REQ-UDP-013`/
`REQ-UDP-014`.
- **rust-RCP-NET-02 (feature):** new `src/l2.rs` — a raw-Ethernet (layer
2) transport, Linux only, mirroring `src/udp.rs`'s own
`UdpSocket`/`UdpTransport` abstraction one wire layer down:
[`encode_ethernet_frame`]/[`decode_ethernet_frame`] (destination MAC +
source MAC + EtherType `0x22F0` big-endian + the AVTPDU bytes directly
— no encapsulation sequence number; that field is Annex J/UDP-specific
and has no L2 counterpart), an [`L2Socket`] trait mirroring `UdpSocket`
(`SocketAddr` replaced by a raw `[u8; 6]` MAC), [`L2Transport`]
mirroring `UdpTransport`'s `send_acf_abb`/`send_acf_gbb` client shape,
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 (a caller-supplied destination MAC is still required —
multicast-MAC derivation is a base-IEEE-1722 algorithm this crate does
not have). Every other target gets a same-named stub whose `bind`
always returns a clear `Err` rather than silently no-op-ing, so the
type can be referenced unconditionally. Server-side L2 dispatch (an
`L2RcServer` mirroring `UdpRcServer`) is out of scope for this item —
flagged as a deliberate follow-up, not bundled in silently; this item's
server-facing wiring is `UdpRcServer` run over `StdUdpSocket` (see
rust-RCP-NET-03 below). New `REQ-L2-001` through `REQ-L2-008`.
- **A flagged judgment call — `nix`, not raw `libc` `unsafe` syscalls:**
this crate is `#![forbid(unsafe_code)]` crate-wide, and `forbid` cannot
be locally overridden (E0453) — `src/capi.rs`'s own doc comment already
named this rule as the reason this crate has never built a raw-pointer
FFI boundary. A direct `libc` `socket()`/`bind()`/`sendto()`/
`recvfrom()` implementation would require `unsafe extern "C"` calls in
this crate's own source, which is not available at all here, not a
style choice. `RawEthernetSocket` is instead built on the `nix` crate
(`target_os = "linux"`-only dependency, new to `Cargo.toml`), whose
`socket`/`bind`/`sendto`/`recvfrom`/`setsockopt`/`getifaddrs` functions
are all safe Rust `fn`s — `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. `nix` is a narrowly-scoped
POSIX-bindings crate, not a heavyweight packet-crafting framework like
`pnet`, matching this item's own minimal-footprint intent.
- **rust-RCP-NET-03 (feature):** `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 in this crate's own unit tests) against it. `discover`/
`register`/`endpoint` remain deliberately ephemeral/in-process, per
this file's own pre-existing "Provenance note" (unchanged by this
item); `serve` is a new, additive, real-network-facing command, not a
replacement for them. The module doc comment's prior "no concrete
`rcp::udp::UdpSocket` implementation over a real OS socket" note is
updated accordingly. New `REQ-CLI-010`.
- **Tests, no privileges/Linux required:** pure byte-manipulation round
trips for both the Annex J encapsulation
(`annex_j_encode_decode_round_trips`, short-buffer rejection) and the
Ethernet frame encode/decode (`ethernet_frame_encode_decode_round_trips`,
short-frame/wrong-EtherType rejection), plus mock-socket-backed
`L2Transport`/`UdpTransport` request/response tests (`EchoL2`/`QueuedL2`,
the `L2Socket` analogs of `udp`'s own `EchoUdp`/`QueuedUdpSocket`) — all
run everywhere, no real socket involved.
- **Tests, real sockets:** a real loopback `StdUdpSocket` round trip
(`std_udp_socket_round_trips_over_real_loopback_socket`), a test
proving the encapsulation sequence number actually increments on the
wire by inspecting raw bytes with a bypass `std::net::UdpSocket`, a
real receive-timeout test, and a new end-to-end test composing a real
`StdUdpSocket` client against a real `StdUdpSocket` + `UdpRcServer`
server over real loopback sockets
(`std_udp_socket_and_udp_rc_server_serve_a_real_discovery_request_end_to_end`)
— all run in the normal cross-platform `test` CI job (ubuntu/macos/
windows), no privileges required.
- **New Linux-only CI job (`l2-veth`):** creates a real `veth0`/`veth1`
pair under `sudo`, then runs a `#[cfg(target_os = "linux")]`,
`#[ignore]`d-by-default test
(`real_raw_ethernet_socket_round_trips_a_frame_over_a_veth_pair`) with
`-- --ignored`, proving a real `RawEthernetSocket` frame round-trips
byte-for-byte over a real (virtual) Ethernet link — not just that the
framing/trait logic type-checks.
- This is a MINOR (additive, non-breaking) release: `StdUdpSocket`,
`ANNEX_J_CONTROL_PORT`/`ANNEX_J_CONTINUOUS_PORT`,
`encode_annex_j_udp_payload`/`decode_annex_j_udp_payload`, and the
entire new `l2` module are new `pub` items only — no existing item
changed shape. `docs/PUBLIC_API.txt` is regenerated accordingly (purely
additive diff) per `docs/SEMVER.md`; `.fusa-reqs.json` gains
`REQ-UDP-012`-`REQ-UDP-014`, `REQ-L2-001`-`REQ-L2-008`, and
`REQ-CLI-010` (564/564 traced).

## v3.1.0 (2026-07-31 E2E CRC trailer wire-order fix) — closed

While independently verifying `v3.0.0`'s `acf` wire-format rework byte-for-
Expand Down
31 changes: 30 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading