All posts
SDV & standards · · 8 min read

A real Zephyr ECU talking to the real Eclipse SOVD adapter, over real CAN

In From UDS to SOVD we described the Classic Diagnostic Adapter (CDA) as the thing that sits between a REST world and a UDS world — and talked about it the way most SOVD content does, in the abstract. This post is the concrete version: we took the real, open-source Eclipse OpenSOVD CDA, built from its own public source, and put a real EDS firmware build on the other end of a real CAN bus. No mock ECU. No mock adapter. We haven't found a published, reproducible writeup of that specific pairing — if you have seen one, we'd genuinely like the link.


The part that sounds hard and isn't: no sudo required

A CAN bus for this needs a vxcan virtual pair, and creating one needs CAP_NET_ADMIN — on a shared or sandboxed machine, that reads like "ask an administrator." It doesn't need one. CAP_NET_ADMIN is a capability, not a login, and a container can be granted it without granting anything else:

docker run --cap-add=NET_ADMIN --cap-add=NET_RAW ...

We checked rather than assumed: a bare ip link add type vxcan on the host fails with Operation not permitted; the identical command inside such a container succeeds. docker itself needs no sudo either, for any account already in the docker group. The entire setup below — both binaries, the virtual CAN link, the whole exchange — runs as an unprivileged user.


What we actually built

Three pieces, two of them untouched third-party software:

The real CDA, compiled from the Eclipse project's own source, pointed at one of its own shipped ODX test fixtures (FLXC1000.mdd, Apache-2.0) — no ISO schema purchase needed for any of this; the fixtures are generated with the MIT-licensed odxtools, not derived from a restricted standard.

Real EDS, specifically examples/basic_ecu built for Zephyr's native_sim board against a real host SocketCAN device rather than Zephyr's in-process loopback — a build configuration that already existed in EDS (#231) and had simply never been pointed at anything outside its own process before.

A vxcan pair joining the two, inside one container, with candump watching the wire so every claim below is a decoded frame, not an inference.


Four real exchanges

Discovery. The CDA's ISO 15765-4 discovery probe — a physical TesterPresent — reached real EDS and got a real positive response. One bug surfaced immediately: basic_ecu only had a functional CAN filter (the broadcast address), not a physical one, which is exactly what a discovery-based tester needs and exactly what the CDA's own config validation refuses to let you route around. One struct field fixed it:

const zephyr_port_cfg_t port_cfg = {
    .can_dev = DIAG_CAN_DEV,
    .physical_rx_id = 0x7E0U,
};

Shipped as EDS#389. After it, discovery held — the SOVD API reported the component "Online", stable, not flapping between discovered and lost the way it does if you only answer the liveness check and nothing else.

A data read. GET .../data/VINDataIdentifier decodes, underneath the HTTP layer, to a genuine multi-frame ISO-TP exchange — First Frame, Flow Control, two Consecutive Frames, 20 bytes — answered by basic_ecu's actual, unmodified VIN handler. Textbook ISO 15765-2, and nobody had to write a line of EDS code to get it.

A SecurityAccess unlock, with real cryptography. This is the one we'd point a skeptic at first. The CDA requested a seed; real EDS returned a genuine 8-byte seed with its documented sequence counter. We computed the key independently — standard AES-128-CMAC (RFC 4493), run against basic_ecu's own documented placeholder key, not assumed or copied from anywhere — and sent it back:

7E0  02 27 03 ...              RequestSeed, sub-function 0x03
7E8  10 0A 67 03 .. .. .. ..   First Frame: 8-byte seed begins
7E0  30 00 00 ...              Flow Control
7E8  21 .. .. .. ..            Consecutive Frame: rest of seed
7E0  06 27 04 a9 5c 89 4a 00   SendKey, sub-function 0x04, computed key
7E8  03 67 04 00               positive response — unlocked

Real EDS accepted it, and held the unlocked state when we checked again afterward. A deliberately wrong key, submitted first by a copy-paste mistake rather than a security test, got a real NRC 53 (invalidKey) rather than any other failure mode — further confirmation that the validation path is live, not stubbed.

A RoutineControl execution. POST .../operations/SelfTest/executions decodes to 31 01 10 01 on the wire; real EDS's dispatcher answered with a genuine positive response, 71 01 10 01 — not an echo, a result from the actual routine table.


What this does not claim

Two of those four exchanges needed something worth disclosing rather than smoothing over. The test fixture's ODX database only declares SecurityAccess and that specific routine ID on configurations basic_ecu doesn't reach by default, so reaching them needed a small, deliberately unshipped local change — a scratch DID value and a scratch routine entry, matched to this one third-party fixture, reverted immediately after and never committed to the real example. The SecurityAccess sub-function pair that worked (0x03/0x04) also happens to be numerically identical between the fixture's own numbering and EDS's, which is a coincidence between two independently-designed schemes, not evidence that any arbitrary level would line up the same way.

So: real crypto, real protocol, real firmware, real third-party adapter — and two of the four results needed a key that happened to fit this one lock. That's a materially different claim from "SecurityAccess interop just works," and we'd rather say so here than have someone discover the gap themselves.

DoIP, the adapter's other transport, we checked too and it's currently blocked — not by a bug, by a real gap. The CDA discovers DoIP gateways exclusively through a UDP broadcast (ISO 13400-2's Vehicle Identification Request/Announcement), with no configuration escape hatch to point it at a gateway directly. EDS's DoIP server has never implemented that side of the exchange — documented, not hidden, in our own DoIP feature matrix. Closing it is real engineering, not a config change, and it's scoped as EDS#392 rather than built on spec.


One more thing worth mentioning: we found a bug in their example too

The CDA's own shipped example config overrides the test fixture's protocol with a value that doesn't match what the fixture's ODX database actually declares — the project's own integration-test suite doesn't apply that same override, so the top-level example disagreed with the project's own ground truth. We filed it; it's now issue #619 on their tracker. Reading someone else's repo closely enough to find something real in it is, consistently, the cheapest way we've found to be useful to a community before asking anything of it.


Try it yourself

git clone https://github.com/Xaloqi/EDS
cd EDS
bash scripts/demo_can_sovd_cda_interop.sh /path/to/your/west-workspace

It builds both binaries from source — the EDS example locally, the CDA from its own pinned public commit — joins them over a vxcan pair inside a Docker container, and runs the VIN read above end to end. No sudo, no purchase, nothing mocked. Needs Docker and a working west workspace; the script checks for both up front and says plainly what's missing rather than failing confusingly partway through.

The demo script is Apache-2.0 in the EDS repo; the EDS runtime is GPL v2. See the script →