From 1a78b66e5f44afaa468a4e6267145ff23cbc1697 Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Thu, 28 May 2026 15:14:03 +0530 Subject: [PATCH 01/21] Add project README and sequence diagrams Document DoIP server block diagram, supported message types, and build instructions. Include PlantUML diagrams for startup, TCP connection, UDP request, and graceful shutdown flows. Signed-off-by: VinaykumarRS1995 --- .gitignore | 1 + README.md | 145 ++++++++++++++++++++++++++++++ docs/01-startup.puml | 62 +++++++++++++ docs/02-tcp-connection.puml | 62 +++++++++++++ docs/03-udp-request.puml | 36 ++++++++ docs/04-graceful-shutdown.puml | 45 ++++++++++ docs/Graceful Shutdown.svg | 1 + docs/Startup.svg | 1 + docs/TCP Connection Lifecycle.svg | 1 + docs/UDP Request Handling.svg | 1 + 10 files changed, 355 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docs/01-startup.puml create mode 100644 docs/02-tcp-connection.puml create mode 100644 docs/03-udp-request.puml create mode 100644 docs/04-graceful-shutdown.puml create mode 100644 docs/Graceful Shutdown.svg create mode 100644 docs/Startup.svg create mode 100644 docs/TCP Connection Lifecycle.svg create mode 100644 docs/UDP Request Handling.svg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/README.md b/README.md new file mode 100644 index 0000000..37958c3 --- /dev/null +++ b/README.md @@ -0,0 +1,145 @@ + + + +# πŸ”Œ UDS-to-SOVD Proxy + +This repository contains the UDS-to-SOVD Proxy of the [Eclipse OpenSOVD](https://github.com/eclipse-opensovd/uds2sovd-proxy) project. + +In the SOVD (Service-Oriented Vehicle Diagnostics) context, the UDS-to-SOVD Proxy serves as a protocol translation gateway between legacy UDS (Unified Diagnostic Services) based diagnostic tools and the modern SOVD-based diagnostic architecture. + +It accepts UDS requests over DoIP (Diagnostics over IP, [ISO 13400-2](https://www.iso.org/standard/74785.html)), resolves the corresponding SOVD service using the diagnostic description (MDD) of the ECU, and translates them into SOVD REST API calls. The SOVD responses are then encoded back into UDS format and returned to the requesting tool. + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ uds2sovd-proxy β”‚ + β”‚ β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” DoIP β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚Diagnostic│◄────────►│ β”‚ DoIP │──►│ UDS2SOVD │──┼────►│ SOVD β”‚ +β”‚ Tester β”‚ TCP/UDP β”‚ β”‚ Server β”‚ β”‚UDS↔SOVD/RESTβ”‚ β”‚ β”‚ Backend β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ :13400 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +> **Project status:** The UDS2SOVD translation layer currently returns NRC 0x11 (serviceNotSupported) for all diagnostic requests (StubProxy). Real SOVD integration is under development. + +## goals + +- transparent UDS ↔ SOVD protocol translation +- high performance (asynchronous I/O) +- low memory and disk-space consumption +- safe & secure +- fast startup + +## introduction + +The proxy consists of a **DoIP Server** (handles the DoIP wire protocol over TCP :13400 / UDP :13400) and the **UDS2SOVD translation layer** (translates UDS request bytes into SOVD REST API calls using the ECU's MDD diagnostic description). + +**Discovery** happens over UDP β€” testers broadcast vehicle identification requests and the server responds with its VIN, EID, and logical address. **Diagnostics** happen over TCP β€” after a routing activation handshake, the tester sends UDS requests which the server forwards to the UDS2SOVD layer. + +### supported messages + +| Payload Type | Name | Transport | Behavior | +|-------------|------|-----------|----------| +| 0x0001 | VehicleIdentificationRequest | UDP | Announces this entity | +| 0x0002 | VehicleIdentificationByEID | UDP | Responds if EID matches, silent otherwise (ISO Β§7.6.1) | +| 0x0003 | VehicleIdentificationByVIN | UDP | Responds if VIN matches, silent otherwise (ISO Β§7.6.1) | +| 0x4001 | EntityStatusRequest | UDP | Reports node type and capacity | +| 0x0005 | RoutingActivationRequest | TCP | Accepts handshake | +| 0x0007 | AliveCheckRequest | TCP | Confirms connection is live | +| 0x8001 | DiagnosticMessage | TCP | Forwards UDS payload, returns ECU response | + +### usage + +1. Run with defaults (TCP `127.0.0.1:13400`, UDP `0.0.0.0:13400`): + ```sh + cargo run + ``` +2. Or with a TOML config file: + ```sh + cargo run -- path/to/config.toml + ``` +3. Verify with the E2E tester (proxy must be running): + ```sh + cargo run --example doip_tester + ``` + +### configuration + +If no config file is passed, sensible defaults are used: + +| Setting | Default | Description | +|---------|---------|-------------| +| TCP address | `127.0.0.1:13400` | Where TCP clients connect | +| UDP address | `0.0.0.0:13400` | Where UDP broadcasts are received | +| Max connections | `10` | Concurrent TCP sessions | +| Read buffer | `4096` bytes | TCP read chunk size | +| Logical address | `0x0001` | DoIP entity address | +| VIN | `00000000000000000` | Vehicle Identification Number | +| EID | `00:00:00:00:00:00` | Entity ID (MAC address) | +| GID | `00:00:00:00:00:00` | Group ID | + +## building + +### prerequisites + +Rust toolchain β‰₯ 1.85 β€” install via [rustup](https://rustup.rs/). + +### build the executable + +```sh +cargo build --release +``` + +## developing + +### pre commit + +```sh +uv run https://raw.githubusercontent.com/eclipse-opensovd/cicd-workflows/main/run_checks.py +``` + +### codestyle + +see [codestyle](CODESTYLE.md) + +### testing + +#### unit tests + +Unittests are placed in the relevant module as usual in rust: +```rust +... +#[cfg(test)] +mod tests { + ... +} +``` + +Run unit tests with: +```sh +cargo test --locked --lib +``` + +#### integration tests + +Start the proxy, then run the E2E tester: +```sh +cargo run & +cargo run --example doip_tester +``` + +## license + +Apache-2.0 β€” see [LICENSE](LICENSE). + diff --git a/docs/01-startup.puml b/docs/01-startup.puml new file mode 100644 index 0000000..717d6a0 --- /dev/null +++ b/docs/01-startup.puml @@ -0,0 +1,62 @@ +@startuml Startup +title DoIP Server - Startup Flow + + + +participant "Main" as Main +participant "Server" as Server +participant "TCP Service" as Tcp +participant "UDP Service" as Udp + +== Load Configuration == + +Main -> Main: load configuration +||| +note right of Main + CLI config file or defaults +end note + +== Initialize Services == + +Main -> Tcp: create TCP service +||| +Main -> Udp: create UDP service +||| +Main -> Server: create server + +== Start Runtime == + +Main -> Server: start() +||| + +Server -> Tcp: bind TCP listener +alt TCP bind failure + Tcp --> Server: startup error + Server --> Main: startup failed +end +||| + +Server -> Udp: bind UDP socket +alt UDP bind failure + Udp --> Server: startup error + Server --> Main: startup failed +end + +== Running State == + +Server -> Tcp: start connection loop +Server -> Udp: start discovery loop +||| + +note over Tcp, Udp + Services running concurrently +end note + +@enduml \ No newline at end of file diff --git a/docs/02-tcp-connection.puml b/docs/02-tcp-connection.puml new file mode 100644 index 0000000..f5001b2 --- /dev/null +++ b/docs/02-tcp-connection.puml @@ -0,0 +1,62 @@ +@startuml TCP_Connection_Lifecycle +title DoIP Server - TCP Connection Lifecycle + + + +participant "Client" as Client +participant "TCP Service" as Tcp +participant "Session" as Sess +participant "UDS2SOVD" as Proxy + +== Connection Establishment == + +Client -> Tcp: connect (port 13400) + +alt maximum sessions reached +Tcp -> Client: connection rejected +Tcp ->x Client: close connection + +else session accepted +Tcp -> Sess: spawn session +end + +== Request Processing == + +loop until disconnect or failure + + +Client -> Sess: DoIP request + +alt invalid request + Sess -> Client: negative acknowledgment + +else diagnostic message + Sess -> Proxy: forward diagnostic payload + Proxy --> Sess: diagnostic response + Sess -> Client: diagnostic response + +else supported request + Sess -> Client: response +end + +note right of Sess + communication failure + terminates session +end note + + +end + +== Session Cleanup == + +note over Sess + session ends + slot dropped automatically +end note \ No newline at end of file diff --git a/docs/03-udp-request.puml b/docs/03-udp-request.puml new file mode 100644 index 0000000..18bd957 --- /dev/null +++ b/docs/03-udp-request.puml @@ -0,0 +1,36 @@ +@startuml UDP_Request_Handling +title DoIP Server - UDP Discovery + + + +participant "Client" as Client +participant "UDP Service" as Udp + +== Receive Loop == + +loop waiting for datagrams + + Client -> Udp: discovery request (port 13400) + + alt valid identification request + Udp -> Client: vehicle announcement (VIN, EID, address) + else EID/VIN does not match + note right of Udp + silent drop (ISO 7.6.1) + end note + else entity status request + Udp -> Client: node type and capacity + else invalid request + Udp -> Client: negative acknowledgment + end + +end + +@enduml \ No newline at end of file diff --git a/docs/04-graceful-shutdown.puml b/docs/04-graceful-shutdown.puml new file mode 100644 index 0000000..e690910 --- /dev/null +++ b/docs/04-graceful-shutdown.puml @@ -0,0 +1,45 @@ +@startuml Graceful_Shutdown +title DoIP Server - Graceful Shutdown + + + +participant "OS" as OS +participant "main" as Main +participant "Server" as Server +participant "Sessions" as Sess + +== Running == + +Main -> Server: start() + +note over Server + TCP and UDP services + running concurrently +end note + +== Shutdown == + +OS -> Main: SIGINT (Ctrl+C) + +Main --> Server: server future dropped + +note over Server + TCP listener dropped (stops accepting) + UDP socket dropped (stops receiving) +end note + +Main --> Sess: runtime exits + +note over Sess + all tasks cancelled + session resources released automatically +end note + +@enduml \ No newline at end of file diff --git a/docs/Graceful Shutdown.svg b/docs/Graceful Shutdown.svg new file mode 100644 index 0000000..2acf5a5 --- /dev/null +++ b/docs/Graceful Shutdown.svg @@ -0,0 +1 @@ +Graceful ShutdownOSServerTcpUdpSession.s.OSOSServerServerTcpTcpUdpUdpSession(s)Session(s)Running concurrently via try_join!SIGINT (ctrl+c)tokio::select! → ctrl_c() branch winsLogs: "Received shutdown signal, stopping server"start() returns Ok(())[drop] tcp.start() future cancelled[drop] udp.start() future cancelledTcpListener dropped → stops acceptingSpawned sessions continue untiltokio runtime shuts down → tasks cancelled[runtime drop] tasks cancelled \ No newline at end of file diff --git a/docs/Startup.svg b/docs/Startup.svg new file mode 100644 index 0000000..8e6ce13 --- /dev/null +++ b/docs/Startup.svg @@ -0,0 +1 @@ +DoIP Server - Startup FlowApplicationServerTCP ServiceUDP ServiceApplicationApplicationServerServerTCP ServiceTCP ServiceUDP ServiceUDP ServiceLoad Configurationload configurationCLI config file or defaultsInitialize Servicescreate TCP servicecreate UDP servicecreate serverStart Runtimestart()bind TCP listeneralt[TCP bind failure]startup errorstartup failedbind UDP socketalt[UDP bind failure]startup errorstartup failedRunning Statestart connection loopstart discovery loopServices running concurrently \ No newline at end of file diff --git a/docs/TCP Connection Lifecycle.svg b/docs/TCP Connection Lifecycle.svg new file mode 100644 index 0000000..31da5fe --- /dev/null +++ b/docs/TCP Connection Lifecycle.svg @@ -0,0 +1 @@ +TCP Connection LifecycleClientTcpSessionManagerSessionFramerDispatcherPayloadHandlerSovdProxyClientClientTcpTcpSessionManagerSessionManagerSessionSessionFramerFramerDispatcherDispatcherPayloadHandlerPayloadHandlerSovdProxySovdProxyTCP connectlog error, continue looptry_acquire()alt[max sessions reached]NoneNACK 0x02 (message too large)close connection[slot available]Some(ConnectionSlot)spawn Session::run(stream, dispatcher, buf_size)loop[I/O loop]stream.read(&mut buf).awaitalt[read returns 0 (client disconnected)]break loop[read error]break loop[read OK (bytes_read > 0)]feed(&buf[..bytes_read])alt[framing error (bad version/too large)]Err(Error)NACK 0x00 (incorrect pattern)continue loop[valid frame]Ok(Frame)dispatch(TcpRequest)alt[unknown payload type]Err(UnknownPayloadType)NACK 0x01 (unknown type)[handler found]handle(req)opt[DiagnosticMessage]forward(uds_bytes)ecu_responseOk(Response)Ok(Response)write responsealt[write fails]Socket broken — return (terminate session)Client disconnects (read returns 0)or read error → break loop[implicit] ConnectionSlot dropped → counter-- \ No newline at end of file diff --git a/docs/UDP Request Handling.svg b/docs/UDP Request Handling.svg new file mode 100644 index 0000000..1afabbf --- /dev/null +++ b/docs/UDP Request Handling.svg @@ -0,0 +1 @@ +UDP Request HandlingClientUdp .mod.rs.Handler .handler.rs.DispatcherPayloadHandlerClientClientUdp (mod.rs)Udp (mod.rs)Handler (handler.rs)Handler (handler.rs)DispatcherDispatcherPayloadHandlerPayloadHandlerUdpSocket::bind(addr)loop[recv loop]broadcast UDP datagram (port 13400)socket.recv_from(&mut buf).awaitalt[recv_from error]log error, continue loop[Ok(bytes_received, src_addr)]handle(&buf[..bytes_received])validate version, inverse version, payload lengthUdpPayloadType::try_from(payload_type_raw)alt[invalid header or unknown payload type]Err(Error)NACK 0x01 (unknown payload type)send_to error is logged, not fatal[valid request]dispatch(UdpRequest)alt[no handler registered]Err(UnknownPayloadType)Err(Error)NACK 0x01[handler found]handle(req)Ok(Response)Ok(Response)Ok(Response)socket.send_to(response, src_addr) [unicast]send_to error is logged, not fatal \ No newline at end of file From 2dbe504c468c67d4719a91170b0e1f8cfbae2eda Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Thu, 28 May 2026 15:14:50 +0530 Subject: [PATCH 02/21] Set up Cargo project with dependencies and module structure Define crate as library and binary. Add tokio, serde, toml, tracing, and thiserror dependencies. Declare top-level modules. Signed-off-by: VinaykumarRS1995 --- Cargo.lock | 832 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 30 ++ src/error.rs | 23 ++ src/lib.rs | 22 ++ 4 files changed, 907 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/error.rs create mode 100644 src/lib.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0445145 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,832 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "uds2sovd" +version = "0.1.0" +dependencies = [ + "serde", + "thiserror", + "tokio", + "toml", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1919a6c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,30 @@ +# Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "uds2sovd" +version = "0.1.0" +edition = "2024" +rust-version = "1.85" +description = "DoIP server (ISO 13400-2) that proxies UDS diagnostics to a SOVD backend" + +[[bin]] +name = "doip-server" +path = "app/main.rs" + +[dependencies] +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +toml = "0.8" +uuid = { version = "1", features = ["v4"] } +tracing = "0.1" +tracing-subscriber = "0.3" +thiserror = "2" diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..e0a26b2 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use crate::doip::error::Error; + +/// Top-level application error. +#[derive(Debug, thiserror::Error)] +pub enum AppError { + #[error(transparent)] + Doip(#[from] Error), + + #[error("io error: {0}")] + Io(#[from] std::io::Error), +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4d5a582 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +//! DoIP server library (ISO 13400-2) β€” proxies UDS diagnostics to a SOVD backend. +//! +//! This crate provides the protocol layer, transport layer, configuration, and +//! proxy interface. The binary entry point lives in `app/main.rs`. + +pub mod config; +pub mod doip; +pub mod error; +pub mod proxy; +pub mod server; From f1ef3602da41a5e7b21f489b582333b37354cee0 Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Thu, 28 May 2026 15:15:40 +0530 Subject: [PATCH 03/21] Add configuration module with pluggable provider trait Introduce ServerConfig for TCP, UDP, and ECU identity settings. Provide InMemoryProvider for defaults and TomlProvider for file-based configuration loading. Signed-off-by: VinaykumarRS1995 --- src/config/defaults.rs | 44 ++++++++++ src/config/mod.rs | 26 ++++++ src/config/provider/in_memory.rs | 46 +++++++++++ src/config/provider/mod.rs | 20 +++++ src/config/provider/toml.rs | 36 ++++++++ src/config/types.rs | 136 +++++++++++++++++++++++++++++++ 6 files changed, 308 insertions(+) create mode 100644 src/config/defaults.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/provider/in_memory.rs create mode 100644 src/config/provider/mod.rs create mode 100644 src/config/provider/toml.rs create mode 100644 src/config/types.rs diff --git a/src/config/defaults.rs b/src/config/defaults.rs new file mode 100644 index 0000000..5ef3f7e --- /dev/null +++ b/src/config/defaults.rs @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use crate::doip::types::{Eid, Gid, LogicalAddress, Vin}; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; + +/// Default TCP listen address (loopback, standard DoIP port 13400). +pub const DEFAULT_TCP_ADDRESS: SocketAddr = + SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 13400)); + +/// Default UDP listen address (all interfaces, standard DoIP port 13400). +pub const DEFAULT_UDP_ADDRESS: SocketAddr = + SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 13400)); + +/// Default maximum number of concurrent TCP connections. +pub const DEFAULT_MAX_CONNECTIONS: usize = 10; + +// TODO: Add DEFAULT_MAX_DATA_SIZE constant for entity status response. + +/// Default TCP read buffer size in bytes. +pub const DEFAULT_READ_BUFFER_SIZE: usize = 4096; + +/// Default DoIP logical address for this server entity. +pub const DEFAULT_LOGICAL_ADDRESS: LogicalAddress = LogicalAddress::new(0x0001); + +/// Default VIN: 17 ASCII zeroes. +/// Must be overridden with the actual vehicle VIN in production. +pub const DEFAULT_VIN: Vin = Vin::new(*b"00000000000000000"); + +/// Default Entity Identifier: all-zero bytes. +/// Should be set to the MAC address of the DoIP network interface. +pub const DEFAULT_EID: Eid = Eid::new([0u8; 6]); + +/// Default Group Identifier: all-zero bytes. +pub const DEFAULT_GID: Gid = Gid::new([0u8; 6]); diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..dbbacba --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +//! Server configuration: types, defaults, and pluggable providers. + +pub mod defaults; +pub mod provider; +pub mod types; + +pub use provider::{InMemoryConfigProvider, TomlConfigProvider}; +pub use types::{EcuConfig, ServerConfig, TcpConfig, UdpConfig}; + +/// Trait for loading server configuration from any source. +pub trait ConfigProvider { + /// Load and return a complete [`ServerConfig`]. + fn load(&self) -> ServerConfig; +} diff --git a/src/config/provider/in_memory.rs b/src/config/provider/in_memory.rs new file mode 100644 index 0000000..1fbcb0f --- /dev/null +++ b/src/config/provider/in_memory.rs @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use crate::config::ConfigProvider; +use crate::config::types::ServerConfig; + +/// Config provider that holds a pre-built [`ServerConfig`] in memory. +/// Use when configuration is constructed programmatically rather than loaded from a file. +pub struct InMemoryConfigProvider { + config: ServerConfig, +} + +impl InMemoryConfigProvider { + /// Wrap an existing config for use as a provider. + pub fn new(config: ServerConfig) -> Self { + Self { config } + } +} + +impl ConfigProvider for InMemoryConfigProvider { + fn load(&self) -> ServerConfig { + self.config.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn load_returns_stored_config() { + let config = ServerConfig::default(); + let provider = InMemoryConfigProvider::new(config); + // load() should return a valid config without panicking + let (_tcp, _udp, _ecu) = provider.load().into_parts(); + } +} diff --git a/src/config/provider/mod.rs b/src/config/provider/mod.rs new file mode 100644 index 0000000..673a2b7 --- /dev/null +++ b/src/config/provider/mod.rs @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +//! Configuration providers β€” load [`ServerConfig`](super::types::ServerConfig) +//! from different sources (in-memory, TOML file). + +pub mod in_memory; +pub mod toml; + +pub use in_memory::InMemoryConfigProvider; +pub use toml::TomlConfigProvider; diff --git a/src/config/provider/toml.rs b/src/config/provider/toml.rs new file mode 100644 index 0000000..e42f054 --- /dev/null +++ b/src/config/provider/toml.rs @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use std::path::PathBuf; + +use crate::config::ConfigProvider; +use crate::config::types::ServerConfig; + +/// Config provider that loads a [`ServerConfig`] from a TOML file. +pub struct TomlConfigProvider { + path: PathBuf, +} + +impl TomlConfigProvider { + pub fn new(path: PathBuf) -> Self { + Self { path } + } +} + +impl ConfigProvider for TomlConfigProvider { + fn load(&self) -> ServerConfig { + let content = std::fs::read_to_string(&self.path) + .unwrap_or_else(|e| panic!("Failed to read config file {:?}: {}", self.path, e)); + toml::from_str(&content) + .unwrap_or_else(|e| panic!("Failed to parse config file {:?}: {}", self.path, e)) + } +} diff --git a/src/config/types.rs b/src/config/types.rs new file mode 100644 index 0000000..b85be96 --- /dev/null +++ b/src/config/types.rs @@ -0,0 +1,136 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use std::net::SocketAddr; + +use serde::Deserialize; + +use super::defaults; +use crate::doip::types::{Eid, Gid, LogicalAddress, Vin}; + +/// Top-level server configuration, split into TCP, UDP, and ECU sections. +#[derive(Debug, Clone, Deserialize, Default)] +pub struct ServerConfig { + tcp: TcpConfig, + udp: UdpConfig, + ecu: EcuConfig, +} + +impl ServerConfig { + /// Destructure into the three sub-configs. + pub fn into_parts(self) -> (TcpConfig, UdpConfig, EcuConfig) { + (self.tcp, self.udp, self.ecu) + } +} + +/// TCP transport settings: listen address, connection limits, buffer size. +#[derive(Debug, Clone, Deserialize)] +pub struct TcpConfig { + address: SocketAddr, + max_connections: usize, + logical_address: LogicalAddress, + read_buffer_size: usize, +} + +impl TcpConfig { + /// TCP listen address (e.g. `127.0.0.1:13400`). + pub fn address(&self) -> SocketAddr { + self.address + } + + /// Maximum number of concurrent TCP sessions. + pub fn max_connections(&self) -> usize { + self.max_connections + } + + /// This entity's DoIP logical address. + pub fn logical_address(&self) -> LogicalAddress { + self.logical_address + } + + /// TCP read buffer size in bytes. + pub fn read_buffer_size(&self) -> usize { + self.read_buffer_size + } +} + +/// UDP transport settings: listen address and logical address. +#[derive(Debug, Clone, Deserialize)] +pub struct UdpConfig { + address: SocketAddr, + logical_address: LogicalAddress, +} + +impl UdpConfig { + /// UDP listen address (e.g. `0.0.0.0:13400`). + pub fn address(&self) -> SocketAddr { + self.address + } + + /// This entity's DoIP logical address. + pub fn logical_address(&self) -> LogicalAddress { + self.logical_address + } +} + +impl Default for TcpConfig { + fn default() -> Self { + Self { + address: defaults::DEFAULT_TCP_ADDRESS, + max_connections: defaults::DEFAULT_MAX_CONNECTIONS, + logical_address: defaults::DEFAULT_LOGICAL_ADDRESS, + read_buffer_size: defaults::DEFAULT_READ_BUFFER_SIZE, + } + } +} + +impl Default for UdpConfig { + fn default() -> Self { + Self { + address: defaults::DEFAULT_UDP_ADDRESS, + logical_address: defaults::DEFAULT_LOGICAL_ADDRESS, + } + } +} + +#[derive(Debug, Clone, Deserialize)] +/// ECU identity settings: VIN, EID, and GID used in vehicle identification responses. +pub struct EcuConfig { + vin: Vin, + eid: Eid, + gid: Gid, +} + +impl EcuConfig { + /// Vehicle Identification Number (17 ASCII characters). + pub fn vin(&self) -> Vin { + self.vin + } + /// Entity Identifier (6 bytes, typically MAC address). + pub fn eid(&self) -> Eid { + self.eid + } + /// Group Identifier (6 bytes). + pub fn gid(&self) -> Gid { + self.gid + } +} + +impl Default for EcuConfig { + fn default() -> Self { + Self { + vin: defaults::DEFAULT_VIN, + eid: defaults::DEFAULT_EID, + gid: defaults::DEFAULT_GID, + } + } +} From 018e678d5166d27ec9c7ca2ad088aa66bb292037 Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Thu, 28 May 2026 15:17:12 +0530 Subject: [PATCH 04/21] Define DoIP protocol data model and message types Implement 8-byte header parsing with version validation. Define TcpPayloadType and UdpPayloadType enums for all supported message types (0x0001-0x8003) per ISO 13400-2. Signed-off-by: VinaykumarRS1995 --- src/doip/constants.rs | 99 ++++++++++++++ src/doip/error.rs | 40 ++++++ src/doip/message.rs | 306 ++++++++++++++++++++++++++++++++++++++++++ src/doip/types.rs | 80 +++++++++++ 4 files changed, 525 insertions(+) create mode 100644 src/doip/constants.rs create mode 100644 src/doip/error.rs create mode 100644 src/doip/message.rs create mode 100644 src/doip/types.rs diff --git a/src/doip/constants.rs b/src/doip/constants.rs new file mode 100644 index 0000000..41bc069 --- /dev/null +++ b/src/doip/constants.rs @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +/// DoIP protocol version byte (byte 0 of the generic header), ISO 13400-2 #7.3. +pub const PROTOCOL_VERSION: u8 = 0xFD; + +/// Inverse of the protocol version byte (byte 1 of the generic header). +/// Must equal `!PROTOCOL_VERSION` for the header to be considered valid. +pub const INVERSE_VERSION: u8 = 0x02; // !0xFD + +/// Size of the DoIP generic header in bytes (ISO 13400-2 #7.3). +pub const HEADER_LEN: usize = 8; + +// RoutingActivation response codes (ISO 13400-2 #9.9, Table 28) + +/// Routing activation successful. +pub const ROUTING_ACTIVATION_CODE_SUCCESS: u8 = 0x10; + +// DiagnosticMessage ACK codes (ISO 13400-2 #9.11, Table 33) + +/// Diagnostic message received and forwarded to the target network. +pub const DIAGNOSTIC_MESSAGE_ACK: u8 = 0x00; + +// VehicleIdentification / VehicleAnnouncement (ISO 13400-2 #7.6.2) + +/// No further action is required from the client. +pub const NO_FURTHER_ACTION: u8 = 0x00; + +// Generic DoIP header NACK codes (ISO 13400-2 Β§9.4, Table 18) + +/// Header fields do not match the expected pattern (bad version or inverse byte). +pub const NACK_INCORRECT_PATTERN: u8 = 0x00; + +/// Payload type is not supported by this entity. +pub const NACK_UNKNOWN_PAYLOAD_TYPE: u8 = 0x01; + +/// Message is too large to be processed. +pub const NACK_MESSAGE_TOO_LARGE: u8 = 0x02; + +/// Server ran out of memory. +pub const NACK_OUT_OF_MEMORY: u8 = 0x03; + +/// Payload length field does not match actual payload size. +pub const NACK_INVALID_PAYLOAD_LENGTH: u8 = 0x04; + +/// Receive buffer size for UDP DoIP datagrams. +/// All ISO 13400-2 defined UDP messages fit within a single Ethernet frame (MTU 1500 bytes). +/// The largest defined message is VehicleAnnouncementResponse at 40 bytes. +pub const UDP_RECV_BUF_SIZE: usize = 1500; + +/// Field lengths (ISO 13400-2) +/// VIN (Vehicle Identification Number) length in bytes. +pub const VIN_LEN: usize = 17; + +/// EID (Entity Identification / MAC address) length in bytes. +pub const EID_LEN: usize = 6; + +// Entity status (ISO 13400-2 Β§7.6.3) + +/// DoIP node type: DoIP gateway (0x00) or DoIP node (0x01). +pub const DOIP_NODE_TYPE: u8 = 0x01; + +/// Entity status response payload length: 1 (node type) + 1 (max TCP) + 1 (current TCP) + 4 (max data size). +pub const ENTITY_STATUS_RESPONSE_LEN: usize = 7; + +// Maximum payload (ISO 13400-2 Β§7.3) + +/// Maximum DoIP payload length accepted by this implementation. +pub const MAX_DOIP_PAYLOAD_LEN: usize = 65_535; + +// Routing Activation (ISO 13400-2 Β§9.9) + +/// Minimum length of a routing activation request payload (bytes). +pub const ROUTING_ACTIVATION_REQUEST_MIN_LEN: usize = 11; + +// Diagnostic Message (ISO 13400-2 Β§9.11) + +/// Minimum diagnostic message payload length: 2 (source addr) + 2 (target addr). +pub const DIAG_MSG_MIN_PAYLOAD_LEN: usize = 4; + +/// Diagnostic message positive ACK header length: 2 (source) + 2 (target) + 1 (ACK code). +pub const DIAG_ACK_HEADER_LEN: usize = 5; + +// UDS response codes (ISO 14229-1) + +/// UDS negative response service ID. +pub const UDS_NEGATIVE_RESPONSE: u8 = 0x7F; + +/// UDS NRC: service not supported. +pub const NRC_SERVICE_NOT_SUPPORTED: u8 = 0x11; diff --git a/src/doip/error.rs b/src/doip/error.rs new file mode 100644 index 0000000..f58366a --- /dev/null +++ b/src/doip/error.rs @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use crate::proxy::SovdProxyError; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("invalid header version: expected 0xFD, got {0:#x}")] + InvalidHeaderVersion(u8), + + #[error("invalid inverse version: expected 0x02, got {0:#x}")] + InvalidInverseVersion(u8), + + #[error("unknown DoIP payload type: {0:#06x}")] + UnknownPayloadType(u16), + + #[error("invalid payload length: declared {declared}, got {actual}")] + InvalidPayloadLength { declared: u32, actual: usize }, + + #[error("payload too short: expected at least {expected} bytes, got {actual}")] + PayloadTooShort { expected: usize, actual: usize }, + + #[error("payload length {0} exceeds maximum allowed size")] + PayloadTooLarge(usize), + + #[error("SOVD proxy error: {0}")] + Proxy(#[from] SovdProxyError), + + #[error("no matching entity for request")] + NoMatch, +} diff --git a/src/doip/message.rs b/src/doip/message.rs new file mode 100644 index 0000000..99765da --- /dev/null +++ b/src/doip/message.rs @@ -0,0 +1,306 @@ +use crate::doip::constants::{ + INVERSE_VERSION, NACK_INCORRECT_PATTERN, NACK_INVALID_PAYLOAD_LENGTH, NACK_MESSAGE_TOO_LARGE, + NACK_UNKNOWN_PAYLOAD_TYPE, PROTOCOL_VERSION, +}; +use crate::doip::error::Error; + +/// Maps a DoIP error to the appropriate generic header NACK code (ISO 13400-2 Table 4). +pub fn nack_code(err: &Error) -> u8 { + match err { + Error::InvalidHeaderVersion(_) | Error::InvalidInverseVersion(_) => NACK_INCORRECT_PATTERN, + Error::UnknownPayloadType(_) => NACK_UNKNOWN_PAYLOAD_TYPE, + Error::PayloadTooLarge(_) => NACK_MESSAGE_TOO_LARGE, + Error::InvalidPayloadLength { .. } | Error::PayloadTooShort { .. } => { + NACK_INVALID_PAYLOAD_LENGTH + } + Error::Proxy(_) => NACK_INCORRECT_PATTERN, + Error::NoMatch => NACK_INCORRECT_PATTERN, + } +} + +// Connection identity + +/// Unique identifier for a TCP session, assigned at accept time. +/// Distinct from the DoIP logical address which is assigned at routing activation. +#[derive(Debug)] +pub struct ConnectionId(uuid::Uuid); + +impl ConnectionId { + /// Generate a new random connection ID. + pub fn new() -> Self { + Self(uuid::Uuid::new_v4()) + } +} + +impl Default for ConnectionId { + fn default() -> Self { + Self::new() + } +} + +impl std::fmt::Display for ConnectionId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +// Payload types + +/// Payload types valid on TCP connections (ISO 13400-2). +/// Compile-time type-safe: a UdpPayloadType value cannot be assigned here. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u16)] +pub enum TcpPayloadType { + GenericDoipHeaderNack = 0x0000, + RoutingActivationRequest = 0x0005, + RoutingActivationResponse = 0x0006, + AliveCheckRequest = 0x0007, + AliveCheckResponse = 0x0008, + DiagnosticMessage = 0x8001, + DiagnosticMessagePositiveAck = 0x8002, + DiagnosticMessageNegativeAck = 0x8003, +} + +impl TryFrom for TcpPayloadType { + type Error = u16; + fn try_from(v: u16) -> Result { + match v { + 0x0000 => Ok(Self::GenericDoipHeaderNack), + 0x0005 => Ok(Self::RoutingActivationRequest), + 0x0006 => Ok(Self::RoutingActivationResponse), + 0x0007 => Ok(Self::AliveCheckRequest), + 0x0008 => Ok(Self::AliveCheckResponse), + 0x8001 => Ok(Self::DiagnosticMessage), + 0x8002 => Ok(Self::DiagnosticMessagePositiveAck), + 0x8003 => Ok(Self::DiagnosticMessageNegativeAck), + other => Err(other), + } + } +} + +/// Payload types valid on UDP (ISO 13400-2). +/// Compile-time type-safe: a TcpPayloadType value cannot be assigned here. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u16)] +pub enum UdpPayloadType { + GenericDoipHeaderNack = 0x0000, + VehicleIdentificationRequest = 0x0001, + VehicleIdentificationRequestWithEid = 0x0002, + VehicleIdentificationRequestWithVin = 0x0003, + VehicleAnnouncementResponse = 0x0004, + DoipEntityStatusRequest = 0x4001, + DoipEntityStatusResponse = 0x4002, +} + +impl TryFrom for UdpPayloadType { + type Error = u16; + fn try_from(v: u16) -> Result { + match v { + 0x0000 => Ok(Self::GenericDoipHeaderNack), + 0x0001 => Ok(Self::VehicleIdentificationRequest), + 0x0002 => Ok(Self::VehicleIdentificationRequestWithEid), + 0x0003 => Ok(Self::VehicleIdentificationRequestWithVin), + 0x0004 => Ok(Self::VehicleAnnouncementResponse), + 0x4001 => Ok(Self::DoipEntityStatusRequest), + 0x4002 => Ok(Self::DoipEntityStatusResponse), + other => Err(other), + } + } +} + +// Transport-typed requests + +/// A request arriving over a TCP connection. +/// The payload type is compile-time restricted to [`TcpPayloadType`] values. +// TODO: Consider unifying TcpRequest/UdpRequest into a generic Request

. +#[derive(Debug)] +pub struct TcpRequest { + payload_type: TcpPayloadType, + payload: Vec, +} + +impl TcpRequest { + /// Create a TCP request from a validated payload type and raw bytes. + pub fn new(payload_type: TcpPayloadType, payload: Vec) -> Self { + Self { + payload_type, + payload, + } + } + + /// The raw payload bytes (no DoIP header). + pub fn payload(&self) -> &[u8] { + &self.payload + } +} + +/// A request arriving over UDP. +/// The payload type is compile-time restricted to [`UdpPayloadType`] values. +pub struct UdpRequest { + payload_type: UdpPayloadType, + payload: Vec, +} + +impl UdpRequest { + /// Create a UDP request from a validated payload type and raw bytes. + pub fn new(payload_type: UdpPayloadType, payload: Vec) -> Self { + Self { + payload_type, + payload, + } + } + + /// The raw payload bytes (no DoIP header). + pub fn payload(&self) -> &[u8] { + &self.payload + } +} + +// Response + +/// DoIP response: payload type + payload bytes. +/// Transport-agnostic β€” the same struct is used for TCP writes and UDP sends. +/// Call `to_bytes()` to get the full on-wire representation including the 8-byte header. +#[derive(Debug)] +pub struct Response { + payload_type: u16, + payload: Vec, +} + +impl Response { + /// Create a response with a raw payload type and payload bytes. + pub fn new(payload_type: u16, payload: Vec) -> Self { + Self { + payload_type, + payload, + } + } + + /// Build a GenericDoipHeaderNack response (ISO 13400-2 Β§9.4). + /// NACK codes: 0x00=incorrect pattern, 0x01=unknown payload type, + /// 0x02=message too large, 0x03=out of memory, 0x04=invalid payload length. + /// + pub fn doip_header_nack(code: u8) -> Self { + Self::new(0x0000, vec![code]) + } + + /// The numeric payload type for this response. + pub fn payload_type(&self) -> u16 { + self.payload_type + } + + /// The raw payload bytes. + pub fn payload(&self) -> &[u8] { + &self.payload + } + + /// Serialise into on-wire bytes: 8-byte DoIP generic header + payload. + pub fn to_bytes(&self) -> Vec { + let len = self.payload().len() as u32; + let mut buf = Vec::with_capacity(crate::doip::constants::HEADER_LEN + self.payload().len()); + buf.push(PROTOCOL_VERSION); + buf.push(INVERSE_VERSION); + buf.extend_from_slice(&self.payload_type().to_be_bytes()); + buf.extend_from_slice(&len.to_be_bytes()); + buf.extend_from_slice(self.payload()); + buf + } +} + +// Payload-type extraction + +/// Implemented by request types so the generic `Dispatcher` can extract the +/// payload type without knowing the concrete request type. +pub trait HasPayloadType { + fn payload_type(&self) -> PayloadType; +} + +impl HasPayloadType for TcpRequest { + fn payload_type(&self) -> TcpPayloadType { + self.payload_type + } +} + +impl HasPayloadType for UdpRequest { + fn payload_type(&self) -> UdpPayloadType { + self.payload_type + } +} + +/// Infallible conversion β€” every enum variant has a defined `u16` value. +impl From for u16 { + fn from(payload_type: TcpPayloadType) -> Self { + payload_type as u16 + } +} + +impl From for u16 { + fn from(payload_type: UdpPayloadType) -> Self { + payload_type as u16 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn nack_response_has_correct_payload_type_and_code() { + let resp = Response::doip_header_nack(0x02); + assert_eq!(resp.payload_type(), 0x0000); + assert_eq!(resp.payload(), &[0x02]); + } + + #[test] + fn response_to_bytes_has_correct_header() { + let resp = Response::new(0x0004, vec![0xAA, 0xBB]); + let bytes = resp.to_bytes(); + assert_eq!(bytes[0], 0xFD); // protocol version + assert_eq!(bytes[1], 0x02); // inverse version + assert_eq!(&bytes[2..4], &0x0004u16.to_be_bytes()); // payload type + assert_eq!(&bytes[4..8], &2u32.to_be_bytes()); // payload length + assert_eq!(&bytes[8..], &[0xAA, 0xBB]); // payload + } + + #[test] + fn tcp_payload_type_try_from_valid() { + assert_eq!( + TcpPayloadType::try_from(0x0005), + Ok(TcpPayloadType::RoutingActivationRequest) + ); + assert_eq!( + TcpPayloadType::try_from(0x8001), + Ok(TcpPayloadType::DiagnosticMessage) + ); + } + + #[test] + fn tcp_payload_type_try_from_invalid() { + assert_eq!(TcpPayloadType::try_from(0xFFFF), Err(0xFFFF)); + } + + #[test] + fn udp_payload_type_try_from_valid() { + assert_eq!( + UdpPayloadType::try_from(0x0001), + Ok(UdpPayloadType::VehicleIdentificationRequest) + ); + assert_eq!( + UdpPayloadType::try_from(0x4001), + Ok(UdpPayloadType::DoipEntityStatusRequest) + ); + } + + #[test] + fn udp_payload_type_try_from_invalid() { + assert_eq!(UdpPayloadType::try_from(0x9999), Err(0x9999)); + } + + #[test] + fn payload_type_into_u16_roundtrip() { + let tcp: u16 = TcpPayloadType::DiagnosticMessage.into(); + assert_eq!(tcp, 0x8001); + let udp: u16 = UdpPayloadType::DoipEntityStatusRequest.into(); + assert_eq!(udp, 0x4001); + } +} diff --git a/src/doip/types.rs b/src/doip/types.rs new file mode 100644 index 0000000..7099999 --- /dev/null +++ b/src/doip/types.rs @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use serde::Deserialize; + +/// DoIP logical address (2-byte big-endian value, ISO 13400-2 #7.3). +/// Identifies a DoIP entity (ECU or DoIP gateway) within the vehicle network. +#[derive(Debug, Clone, Copy, Deserialize)] +pub struct LogicalAddress(u16); + +impl LogicalAddress { + /// Create a new logical address from a raw `u16`. + pub const fn new(addr: u16) -> Self { + Self(addr) + } + /// Serialise as big-endian bytes for on-wire use. + pub fn to_be_bytes(self) -> [u8; 2] { + self.0.to_be_bytes() + } +} + +impl From for LogicalAddress { + fn from(addr: u16) -> Self { + Self(addr) + } +} + +/// Vehicle Identification Number (ISO 3779): 17 ASCII characters. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +pub struct Vin([u8; 17]); + +impl Vin { + /// Create a VIN from a 17-byte array. + pub const fn new(bytes: [u8; 17]) -> Self { + Self(bytes) + } + /// Raw bytes for on-wire serialisation. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +/// Entity Identifier: 6 bytes, typically the MAC address of the DoIP node's +/// network interface (ISO 13400-2 #7.6.2). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +pub struct Eid([u8; 6]); + +impl Eid { + pub const fn new(bytes: [u8; 6]) -> Self { + Self(bytes) + } + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +/// Group Identifier: 6 bytes, used to group DoIP entities that share a subnet +/// (ISO 13400-2 #7.6.2). +#[derive(Debug, Clone, Copy, Deserialize)] +pub struct Gid([u8; 6]); + +impl Gid { + /// Create a GID from a 6-byte array. + pub const fn new(bytes: [u8; 6]) -> Self { + Self(bytes) + } + /// Raw bytes for on-wire serialisation. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} From 602ee2853e7c86875f6659093a45a5ded10578ca Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Thu, 28 May 2026 15:17:19 +0530 Subject: [PATCH 05/21] Add PayloadHandler trait and generic dispatcher Introduce PayloadHandler trait for handler implementations. Build Dispatcher with HashMap-based routing from payload type to handler, generic over transport. Signed-off-by: VinaykumarRS1995 --- src/doip/dispatch.rs | 132 +++++++++++++++++++++++++++++++++++++++++++ src/doip/mod.rs | 60 ++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 src/doip/dispatch.rs create mode 100644 src/doip/mod.rs diff --git a/src/doip/dispatch.rs b/src/doip/dispatch.rs new file mode 100644 index 0000000..32ef38e --- /dev/null +++ b/src/doip/dispatch.rs @@ -0,0 +1,132 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use std::collections::HashMap; +use std::hash::Hash; + +use crate::doip::error::Error; +use crate::doip::message::{ + HasPayloadType, Response, TcpPayloadType, TcpRequest, UdpPayloadType, UdpRequest, +}; + +/// Handler for a single payload type on one transport. +/// +/// The generic parameters enforce transport segregation at compile time: +/// a `PayloadHandler` cannot be registered on +/// a `UdpDispatcher` and vice versa. +pub trait PayloadHandler: Send + Sync { + /// The payload type this handler is registered for. + fn payload_type(&self) -> PayloadType; + /// Process the request and return a response or error. + fn handle(&self, req: Request) -> Result; +} + +/// Generic registry and router for payload-type handlers. +/// +/// Completely protocol-agnostic. The concrete transports bind it to specific +/// payload-type enums via the [`TcpDispatcher`] and [`UdpDispatcher`] aliases. +pub struct Dispatcher +where + PayloadType: Eq + Hash, +{ + handlers: HashMap + Send + Sync>>, +} + +impl Dispatcher +where + PayloadType: Eq + Hash + Into, + Request: HasPayloadType, +{ + /// Create an empty dispatcher with no handlers registered. + pub fn new() -> Self { + Self { + handlers: HashMap::new(), + } + } + + /// Register a handler for its declared payload type. + pub fn register(&mut self, handler: impl PayloadHandler + 'static) { + let payload_type = handler.payload_type(); + self.handlers.insert(payload_type, Box::new(handler)); + } + + /// Route a request to the handler registered for its payload type. + /// Returns `Err(UnknownPayloadType)` if no handler is registered. + pub fn dispatch(&self, req: Request) -> Result { + let payload_type = req.payload_type(); + self.handlers + .get(&payload_type) + .ok_or_else(|| Error::UnknownPayloadType(payload_type.into()))? + .handle(req) + } +} + +impl Default for Dispatcher +where + PayloadType: Eq + Hash + Into, + Request: HasPayloadType, +{ + fn default() -> Self { + Self::new() + } +} + +/// Dispatcher bound to the TCP transport payload types. +pub type TcpDispatcher = Dispatcher; + +/// Dispatcher bound to the UDP transport payload types. +pub type UdpDispatcher = Dispatcher; + +#[cfg(test)] +mod tests { + use super::*; + use crate::doip::error::Error; + use crate::doip::message::{Response, TcpPayloadType, TcpRequest}; + + struct AliveEchoHandler; + + impl PayloadHandler for AliveEchoHandler { + fn payload_type(&self) -> TcpPayloadType { + TcpPayloadType::AliveCheckRequest + } + fn handle(&self, _req: TcpRequest) -> Result { + Ok(Response::new( + TcpPayloadType::AliveCheckResponse as u16, + vec![], + )) + } + } + + fn make_req(pt: TcpPayloadType) -> TcpRequest { + TcpRequest::new(pt, vec![]) + } + + #[test] + fn dispatch_routes_to_registered_handler() { + let mut dispatcher = TcpDispatcher::new(); + dispatcher.register(AliveEchoHandler); + let resp = dispatcher + .dispatch(make_req(TcpPayloadType::AliveCheckRequest)) + .unwrap(); + assert_eq!( + resp.payload_type(), + TcpPayloadType::AliveCheckResponse as u16 + ); + } + + #[test] + fn dispatch_rejects_unknown_type() { + let dispatcher = TcpDispatcher::new(); + let result = dispatcher.dispatch(make_req(TcpPayloadType::DiagnosticMessage)); + assert!(matches!(result, Err(Error::UnknownPayloadType(0x8001)))); + } +} diff --git a/src/doip/mod.rs b/src/doip/mod.rs new file mode 100644 index 0000000..873dd0d --- /dev/null +++ b/src/doip/mod.rs @@ -0,0 +1,60 @@ +// Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 + +pub mod constants; +pub mod dispatch; +pub mod error; +pub mod handlers; +pub mod message; +pub mod types; + +pub use dispatch::{PayloadHandler, TcpDispatcher, UdpDispatcher}; +pub use types::{Eid, Gid, LogicalAddress, Vin}; + +use std::sync::Arc; + +/// Build the TCP dispatcher with all TCP-legal handlers registered. +/// `proxy` is called for every DiagnosticMessage (0x8001). +pub fn tcp_dispatcher( + logical_addr: LogicalAddress, + proxy: Arc, +) -> TcpDispatcher { + use handlers::{AliveCheckHandler, DiagnosticsHandler, RoutingActivationHandler}; + let mut dispatcher = TcpDispatcher::new(); + dispatcher.register(RoutingActivationHandler::new(logical_addr)); + dispatcher.register(AliveCheckHandler::new(logical_addr)); + dispatcher.register(DiagnosticsHandler::new(proxy)); + dispatcher +} + +/// Build the UDP dispatcher with all UDP-legal handlers registered. +pub fn udp_dispatcher(logical_addr: LogicalAddress, vin: Vin, eid: Eid, gid: Gid) -> UdpDispatcher { + use handlers::{ + EntityStatusHandler, IdentifyVehicleByEidHandler, IdentifyVehicleByVinHandler, + IdentifyVehicleHandler, + }; + let mut dispatcher = UdpDispatcher::new(); + dispatcher.register(IdentifyVehicleHandler::new(vin, eid, gid, logical_addr)); + dispatcher.register(IdentifyVehicleByEidHandler::new( + vin, + eid, + gid, + logical_addr, + )); + dispatcher.register(IdentifyVehicleByVinHandler::new( + vin, + eid, + gid, + logical_addr, + )); + dispatcher.register(EntityStatusHandler::new(10, 65_535)); + dispatcher +} From 36d9971a7cff0018e1711c04aca67d3a36ed9384 Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Thu, 28 May 2026 15:17:25 +0530 Subject: [PATCH 06/21] Add SOVD proxy trait with stub and mock implementations Define SovdProxy trait with forward() for UDS byte translation. StubProxy returns NRC 0x11 pending real backend integration. MockProxy echoes input for unit testing. Signed-off-by: VinaykumarRS1995 --- src/proxy/error.rs | 17 ++++++++++++++++ src/proxy/mock.rs | 23 ++++++++++++++++++++++ src/proxy/mod.rs | 41 +++++++++++++++++++++++++++++++++++++++ src/proxy/stub.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 src/proxy/error.rs create mode 100644 src/proxy/mock.rs create mode 100644 src/proxy/mod.rs create mode 100644 src/proxy/stub.rs diff --git a/src/proxy/error.rs b/src/proxy/error.rs new file mode 100644 index 0000000..0c76963 --- /dev/null +++ b/src/proxy/error.rs @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +#[derive(Debug, thiserror::Error)] +pub enum SovdProxyError { + #[error("SOVD returned an invalid UDS response")] + InvalidResponse, +} diff --git a/src/proxy/mock.rs b/src/proxy/mock.rs new file mode 100644 index 0000000..f511092 --- /dev/null +++ b/src/proxy/mock.rs @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use super::{SovdProxy, SovdProxyError}; + +/// Loopback proxy β€” echoes the UDS request bytes as the response. +/// Used in tests only; not intended for production. +pub struct MockProxy; + +impl SovdProxy for MockProxy { + fn forward(&self, uds_request: &[u8]) -> Result, SovdProxyError> { + Ok(uds_request.to_vec()) + } +} diff --git a/src/proxy/mod.rs b/src/proxy/mod.rs new file mode 100644 index 0000000..1a77f43 --- /dev/null +++ b/src/proxy/mod.rs @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +pub mod error; +#[cfg(test)] +pub mod mock; +pub mod stub; + +pub use error::SovdProxyError; + +/// Translates raw UDS request bytes into raw UDS response bytes by forwarding +/// the request to the SOVD diagnostic system. +/// +/// # Contract +/// +/// - `uds_request` contains only UDS service-layer bytes β€” no DoIP framing. +/// - On success the returned `Vec` is the raw UDS response from SOVD. +/// - On failure a [`SovdProxyError`] describes why the proxy could not produce +/// a response. +/// +/// # Implementations +/// +/// | Type | Purpose | +/// | | | +/// | [`stub::StubProxy`] | Returns NRC 0x11 (serviceNotSupported). Use until the real SOVD backend is ready. | +/// | [`mock::MockProxy`] | Loopback β€” echoes the request. Used in unit/integration tests. | +/// +/// The real implementation (provided separately) will forward requests to a +/// SOVD server over the vehicle network. +pub trait SovdProxy: Send + Sync { + fn forward(&self, uds_request: &[u8]) -> Result, SovdProxyError>; +} diff --git a/src/proxy/stub.rs b/src/proxy/stub.rs new file mode 100644 index 0000000..69a8483 --- /dev/null +++ b/src/proxy/stub.rs @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use super::{SovdProxy, SovdProxyError}; + +/// Stub proxy β€” returns UDS Negative Response Code 0x11 (serviceNotSupported) +/// for every request. +/// +/// Replace with the real [`SovdProxy`] implementation once the SOVD backend +/// integration layer is available. +pub struct StubProxy; + +impl SovdProxy for StubProxy { + fn forward(&self, uds_request: &[u8]) -> Result, SovdProxyError> { + if uds_request.is_empty() { + return Err(SovdProxyError::InvalidResponse); + } + // UDS Negative Response: 0x7F 0x11 (serviceNotSupported) + Ok(vec![0x7F, uds_request[0], 0x11]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn stub_returns_nrc_service_not_supported() { + let proxy = StubProxy; + let resp = proxy.forward(&[0x22, 0xF1, 0x90]).unwrap(); + assert_eq!(resp, vec![0x7F, 0x22, 0x11]); + } + + #[test] + fn stub_errors_on_empty_request() { + let proxy = StubProxy; + assert!(proxy.forward(&[]).is_err()); + } +} From d0bc79705dd4928c1d8451ac89b1282df677cbe5 Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Thu, 28 May 2026 15:17:34 +0530 Subject: [PATCH 07/21] Implement UDP handlers for vehicle identification and entity status Add handlers for VehicleIdentification (0x0001-0x0003), VIN request (0x0004), and EntityStatus (0x4001). Vehicle ID handlers share common announcement-building logic. Signed-off-by: VinaykumarRS1995 --- src/doip/handlers/entity_status.rs | 103 ++++++++++++++++ .../handlers/vehicle_identification/common.rs | 73 +++++++++++ .../handlers/vehicle_identification/mod.rs | 22 ++++ .../vehicle_identification/request.rs | 95 ++++++++++++++ .../vehicle_identification/request_by_eid.rs | 116 ++++++++++++++++++ .../vehicle_identification/request_by_vin.rs | 110 +++++++++++++++++ 6 files changed, 519 insertions(+) create mode 100644 src/doip/handlers/entity_status.rs create mode 100644 src/doip/handlers/vehicle_identification/common.rs create mode 100644 src/doip/handlers/vehicle_identification/mod.rs create mode 100644 src/doip/handlers/vehicle_identification/request.rs create mode 100644 src/doip/handlers/vehicle_identification/request_by_eid.rs create mode 100644 src/doip/handlers/vehicle_identification/request_by_vin.rs diff --git a/src/doip/handlers/entity_status.rs b/src/doip/handlers/entity_status.rs new file mode 100644 index 0000000..9837a7f --- /dev/null +++ b/src/doip/handlers/entity_status.rs @@ -0,0 +1,103 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use crate::doip::{ + PayloadHandler, + constants::{DOIP_NODE_TYPE, ENTITY_STATUS_RESPONSE_LEN}, + error::Error, + message::{Response, UdpPayloadType, UdpRequest}, +}; + +// DoipEntityStatusRequest (0x4001) + +/// Handles DoipEntityStatusRequest (ISO 13400-2 Β§7.6.3). +/// Reports node type, max TCP sessions, current sessions, and max data size. +pub struct EntityStatusHandler { + max_connections: u8, + // TODO: Derive max_data_size from config instead of hardcoding at registration. + max_data_size: u32, +} + +impl EntityStatusHandler { + pub fn new(max_connections: u8, max_data_size: u32) -> Self { + Self { + max_connections, + max_data_size, + } + } +} + +impl PayloadHandler for EntityStatusHandler { + fn payload_type(&self) -> UdpPayloadType { + UdpPayloadType::DoipEntityStatusRequest + } + + /// Response payload (7 bytes): + /// [0] node type (0x01 = DoIP node) + /// [1] max concurrent TCP sockets + /// [2] currently open TCP sockets (0 β€” not tracked at this level) + /// [3..7] max data size (u32 big-endian) + fn handle(&self, req: UdpRequest) -> Result { + if !req.payload().is_empty() { + return Err(Error::InvalidPayloadLength { + declared: req.payload().len() as u32, + actual: req.payload().len(), + }); + } + let mut payload = Vec::with_capacity(ENTITY_STATUS_RESPONSE_LEN); + payload.push(DOIP_NODE_TYPE); + payload.push(self.max_connections); + payload.push(0x00); // current sessions β€” not tracked at this level + payload.extend_from_slice(&self.max_data_size.to_be_bytes()); + Ok(Response::new( + UdpPayloadType::DoipEntityStatusResponse as u16, + payload, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn handle_returns_7_byte_status_response() { + let handler = EntityStatusHandler::new(10, 65_535); + let req = UdpRequest::new(UdpPayloadType::DoipEntityStatusRequest, vec![]); + let resp = handler.handle(req).unwrap(); + + assert_eq!( + resp.payload_type(), + UdpPayloadType::DoipEntityStatusResponse as u16 + ); + assert_eq!(resp.payload().len(), ENTITY_STATUS_RESPONSE_LEN); + assert_eq!(resp.payload()[0], DOIP_NODE_TYPE); + assert_eq!(resp.payload()[1], 10); + assert_eq!(resp.payload()[2], 0x00); + assert_eq!( + u32::from_be_bytes([ + resp.payload()[3], + resp.payload()[4], + resp.payload()[5], + resp.payload()[6] + ]), + 65_535 + ); + } + + #[test] + fn handle_rejects_non_empty_payload() { + let handler = EntityStatusHandler::new(10, 65_535); + let req = UdpRequest::new(UdpPayloadType::DoipEntityStatusRequest, vec![0x01]); + assert!(handler.handle(req).is_err()); + } +} diff --git a/src/doip/handlers/vehicle_identification/common.rs b/src/doip/handlers/vehicle_identification/common.rs new file mode 100644 index 0000000..b330071 --- /dev/null +++ b/src/doip/handlers/vehicle_identification/common.rs @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +//! Shared response builder for Vehicle Identification handlers (ISO 13400-2 Β§7.6.2). + +use crate::doip::{ + constants::NO_FURTHER_ACTION, + message::{Response, UdpPayloadType}, + types::{Eid, Gid, LogicalAddress, Vin}, +}; + +/// 17 (VIN) + 2 (addr) + 6 (EID) + 6 (GID) + 1 (action byte) = 32 +pub(super) const VI_RESPONSE_LEN: usize = 32; + +/// Builds the 32-byte Vehicle Identification Response / Announcement payload. +/// +/// Layout: +/// ```text +/// [0..17] VIN +/// [17..19] logical address (big-endian) +/// [19..25] EID +/// [25..31] GID +/// [31] further action required (0x00 = none) +/// ``` +pub(super) fn create_vi_response( + vin: &Vin, + eid: &Eid, + gid: &Gid, + logical_address: LogicalAddress, +) -> Response { + let mut payload = Vec::with_capacity(VI_RESPONSE_LEN); + payload.extend_from_slice(vin.as_bytes()); + payload.extend_from_slice(&logical_address.to_be_bytes()); + payload.extend_from_slice(eid.as_bytes()); + payload.extend_from_slice(gid.as_bytes()); + payload.push(NO_FURTHER_ACTION); + Response::new(UdpPayloadType::VehicleAnnouncementResponse as u16, payload) +} + +#[cfg(test)] +pub(super) mod fixtures { + use crate::doip::types::{Eid, Gid, LogicalAddress, Vin}; + + /// ISO example VIN (17 ASCII characters, valid format) + pub const TEST_VIN: Vin = Vin::new(*b"1HGBH41JXMN109186"); + + // Sample MAC address used as EID (6 bytes) + pub const TEST_EID: Eid = Eid::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01]); + + /// Group ID (6 bytes, all-zero = no grouping per ISO 13400-2) + pub const TEST_GID: Gid = Gid::new([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + + /// ECU logical address (ISO 13400-2 range 0x0001–0x0DFF) + pub const TEST_ADDR: LogicalAddress = LogicalAddress::new(0x0E01); + + /// EID that does NOT match TEST_EID (broadcast address, clearly different) + pub const NON_MATCHING_EID: Eid = Eid::new([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); + + /// VIN that does NOT match TEST_VIN (valid format, different vehicle) + pub const NON_MATCHING_VIN: Vin = Vin::new(*b"WVWZZZ3CZWE123456"); + + /// Expected response payload length + pub const VI_RESPONSE_LEN: usize = super::VI_RESPONSE_LEN; +} diff --git a/src/doip/handlers/vehicle_identification/mod.rs b/src/doip/handlers/vehicle_identification/mod.rs new file mode 100644 index 0000000..f908f69 --- /dev/null +++ b/src/doip/handlers/vehicle_identification/mod.rs @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +//! Vehicle Identification handlers (ISO 13400-2 Β§7.6). + +mod common; +mod request; +mod request_by_eid; +mod request_by_vin; + +pub use request::IdentifyVehicleHandler; +pub use request_by_eid::IdentifyVehicleByEidHandler; +pub use request_by_vin::IdentifyVehicleByVinHandler; diff --git a/src/doip/handlers/vehicle_identification/request.rs b/src/doip/handlers/vehicle_identification/request.rs new file mode 100644 index 0000000..aa78fdc --- /dev/null +++ b/src/doip/handlers/vehicle_identification/request.rs @@ -0,0 +1,95 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +//! Handler for VehicleIdentificationRequest (0x0001, ISO 13400-2 Β§7.6.1). + +use super::common::create_vi_response; +use crate::doip::{ + PayloadHandler, + error::Error, + message::{Response, UdpPayloadType, UdpRequest}, + types::{Eid, Gid, LogicalAddress, Vin}, +}; + +/// Handles 0x0001 β€” responds to any client unconditionally. +pub struct IdentifyVehicleHandler { + vin: Vin, + eid: Eid, + gid: Gid, + logical_address: LogicalAddress, +} + +impl IdentifyVehicleHandler { + pub fn new(vin: Vin, eid: Eid, gid: Gid, logical_address: LogicalAddress) -> Self { + Self { + vin, + eid, + gid, + logical_address, + } + } +} + +impl PayloadHandler for IdentifyVehicleHandler { + fn payload_type(&self) -> UdpPayloadType { + UdpPayloadType::VehicleIdentificationRequest + } + + fn handle(&self, req: UdpRequest) -> Result { + if !req.payload().is_empty() { + return Err(Error::InvalidPayloadLength { + declared: req.payload().len() as u32, + actual: req.payload().len(), + }); + } + Ok(create_vi_response( + &self.vin, + &self.eid, + &self.gid, + self.logical_address, + )) + } +} + +#[cfg(test)] +mod tests { + use super::super::common::fixtures::*; + use super::*; + + fn handler() -> IdentifyVehicleHandler { + IdentifyVehicleHandler::new(TEST_VIN, TEST_EID, TEST_GID, TEST_ADDR) + } + + #[test] + fn empty_payload_returns_announcement() { + let resp = handler() + .handle(UdpRequest::new( + UdpPayloadType::VehicleIdentificationRequest, + vec![], + )) + .unwrap(); + assert_eq!( + resp.payload_type(), + UdpPayloadType::VehicleAnnouncementResponse as u16 + ); + assert_eq!(resp.payload().len(), VI_RESPONSE_LEN); + } + + #[test] + fn non_empty_payload_returns_error() { + let result = handler().handle(UdpRequest::new( + UdpPayloadType::VehicleIdentificationRequest, + vec![0x01], + )); + assert!(matches!(result, Err(Error::InvalidPayloadLength { .. }))); + } +} diff --git a/src/doip/handlers/vehicle_identification/request_by_eid.rs b/src/doip/handlers/vehicle_identification/request_by_eid.rs new file mode 100644 index 0000000..b060745 --- /dev/null +++ b/src/doip/handlers/vehicle_identification/request_by_eid.rs @@ -0,0 +1,116 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +//! Handler for VehicleIdentificationRequestWithEID (0x0002, ISO 13400-2 Β§7.6.1.1). + +use super::common::create_vi_response; +use crate::doip::{ + PayloadHandler, + constants::EID_LEN, + error::Error, + message::{Response, UdpPayloadType, UdpRequest}, + types::{Eid, Gid, LogicalAddress, Vin}, +}; + +/// Handles 0x0002 β€” responds only if the requested EID matches. +pub struct IdentifyVehicleByEidHandler { + vin: Vin, + eid: Eid, + gid: Gid, + logical_address: LogicalAddress, +} + +impl IdentifyVehicleByEidHandler { + pub fn new(vin: Vin, eid: Eid, gid: Gid, logical_address: LogicalAddress) -> Self { + Self { + vin, + eid, + gid, + logical_address, + } + } +} + +impl PayloadHandler for IdentifyVehicleByEidHandler { + fn payload_type(&self) -> UdpPayloadType { + UdpPayloadType::VehicleIdentificationRequestWithEid + } + + fn handle(&self, req: UdpRequest) -> Result { + if req.payload().len() != EID_LEN { + return Err(Error::PayloadTooShort { + expected: EID_LEN, + actual: req.payload().len(), + }); + } + let requested = Eid::new([ + req.payload()[0], + req.payload()[1], + req.payload()[2], + req.payload()[3], + req.payload()[4], + req.payload()[5], + ]); + if requested != self.eid { + return Err(Error::NoMatch); + } + Ok(create_vi_response( + &self.vin, + &self.eid, + &self.gid, + self.logical_address, + )) + } +} + +#[cfg(test)] +mod tests { + use super::super::common::fixtures::*; + use super::*; + + fn handler() -> IdentifyVehicleByEidHandler { + IdentifyVehicleByEidHandler::new(TEST_VIN, TEST_EID, TEST_GID, TEST_ADDR) + } + + #[test] + fn matching_eid_returns_announcement() { + let req = UdpRequest::new( + UdpPayloadType::VehicleIdentificationRequestWithEid, + TEST_EID.as_bytes().to_vec(), + ); + assert!(handler().handle(req).is_ok()); + } + + #[test] + fn non_matching_eid_returns_no_match() { + let req = UdpRequest::new( + UdpPayloadType::VehicleIdentificationRequestWithEid, + NON_MATCHING_EID.as_bytes().to_vec(), + ); + assert!(matches!(handler().handle(req), Err(Error::NoMatch))); + } + + #[test] + fn wrong_length_returns_error() { + let req = UdpRequest::new( + UdpPayloadType::VehicleIdentificationRequestWithEid, + vec![0x00; 3], // 3 bytes β€” less than required 6 + ); + assert!(matches!( + handler().handle(req), + Err(Error::PayloadTooShort { + expected: 6, + actual: 3 + }) + )); + } +} diff --git a/src/doip/handlers/vehicle_identification/request_by_vin.rs b/src/doip/handlers/vehicle_identification/request_by_vin.rs new file mode 100644 index 0000000..52a3e6c --- /dev/null +++ b/src/doip/handlers/vehicle_identification/request_by_vin.rs @@ -0,0 +1,110 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +//! Handler for VehicleIdentificationRequestWithVIN (0x0003, ISO 13400-2 Β§7.6.1.2). + +use super::common::create_vi_response; +use crate::doip::{ + PayloadHandler, + constants::VIN_LEN, + error::Error, + message::{Response, UdpPayloadType, UdpRequest}, + types::{Eid, Gid, LogicalAddress, Vin}, +}; + +/// Handles 0x0003 β€” responds only if the requested VIN matches. +pub struct IdentifyVehicleByVinHandler { + vin: Vin, + eid: Eid, + gid: Gid, + logical_address: LogicalAddress, +} + +impl IdentifyVehicleByVinHandler { + pub fn new(vin: Vin, eid: Eid, gid: Gid, logical_address: LogicalAddress) -> Self { + Self { + vin, + eid, + gid, + logical_address, + } + } +} + +impl PayloadHandler for IdentifyVehicleByVinHandler { + fn payload_type(&self) -> UdpPayloadType { + UdpPayloadType::VehicleIdentificationRequestWithVin + } + + fn handle(&self, req: UdpRequest) -> Result { + if req.payload().len() != VIN_LEN { + return Err(Error::PayloadTooShort { + expected: VIN_LEN, + actual: req.payload().len(), + }); + } + let mut bytes = [0u8; 17]; + bytes.copy_from_slice(req.payload()); + if Vin::new(bytes) != self.vin { + return Err(Error::NoMatch); + } + Ok(create_vi_response( + &self.vin, + &self.eid, + &self.gid, + self.logical_address, + )) + } +} + +#[cfg(test)] +mod tests { + use super::super::common::fixtures::*; + use super::*; + + fn handler() -> IdentifyVehicleByVinHandler { + IdentifyVehicleByVinHandler::new(TEST_VIN, TEST_EID, TEST_GID, TEST_ADDR) + } + + #[test] + fn matching_vin_returns_announcement() { + let req = UdpRequest::new( + UdpPayloadType::VehicleIdentificationRequestWithVin, + TEST_VIN.as_bytes().to_vec(), + ); + assert!(handler().handle(req).is_ok()); + } + + #[test] + fn non_matching_vin_returns_no_match() { + let req = UdpRequest::new( + UdpPayloadType::VehicleIdentificationRequestWithVin, + NON_MATCHING_VIN.as_bytes().to_vec(), + ); + assert!(matches!(handler().handle(req), Err(Error::NoMatch))); + } + + #[test] + fn wrong_length_returns_error() { + let req = UdpRequest::new( + UdpPayloadType::VehicleIdentificationRequestWithVin, + vec![0x00; 3], // 3 bytes β€” less than required 17 + ); + assert!(matches!( + handler().handle(req), + Err(Error::PayloadTooShort { + expected: 17, + actual: 3 + }) + )); + } +} From 762050bec9d750d0c2014838e783c3c0a4f73992 Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Thu, 28 May 2026 15:17:47 +0530 Subject: [PATCH 08/21] Implement alive check and routing activation TCP handlers Add AliveCheck (0x0007) responding to keep-alive pings. RoutingActivation (0x0005) returns 0x10 as placeholder until full state-machine logic is implemented. Signed-off-by: VinaykumarRS1995 --- src/doip/handlers/alive_check.rs | 93 ++++++++++++++++++++ src/doip/handlers/routing_activation.rs | 109 ++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 src/doip/handlers/alive_check.rs create mode 100644 src/doip/handlers/routing_activation.rs diff --git a/src/doip/handlers/alive_check.rs b/src/doip/handlers/alive_check.rs new file mode 100644 index 0000000..418995e --- /dev/null +++ b/src/doip/handlers/alive_check.rs @@ -0,0 +1,93 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use crate::doip::{ + PayloadHandler, + error::Error, + message::{Response, TcpPayloadType, TcpRequest}, + types::LogicalAddress, +}; + +/// Handles AliveCheckRequest (0x0007, ISO 13400-2 Β§9.7). +/// Responds with this entity's logical address to confirm the connection is live. +pub struct AliveCheckHandler { + logical_address: LogicalAddress, +} + +impl AliveCheckHandler { + pub fn new(logical_address: LogicalAddress) -> Self { + Self { logical_address } + } + + /// Protocol logic (ISO 13400-2 #9.7). + /// AliveCheckResponse payload = server logical address (2 bytes). + fn respond(&self) -> Response { + let mut payload = Vec::with_capacity(2); + payload.extend_from_slice(&self.logical_address.to_be_bytes()); + Response::new(TcpPayloadType::AliveCheckResponse as u16, payload) + } +} + +impl PayloadHandler for AliveCheckHandler { + fn payload_type(&self) -> TcpPayloadType { + TcpPayloadType::AliveCheckRequest + } + fn handle(&self, req: TcpRequest) -> Result { + if !req.payload().is_empty() { + return Err(Error::InvalidPayloadLength { + declared: req.payload().len() as u32, + actual: req.payload().len(), + }); + } + Ok(self.respond()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::doip::types::LogicalAddress; + + #[test] + fn handle_empty_payload_returns_logical_address() { + let handler = AliveCheckHandler::new(LogicalAddress::new(0x0001)); + let req = TcpRequest::new(TcpPayloadType::AliveCheckRequest, vec![]); + let resp = handler.handle(req).unwrap(); + assert_eq!( + resp.payload_type(), + TcpPayloadType::AliveCheckResponse as u16 + ); + assert_eq!(resp.payload(), &[0x00, 0x01]); + } + + #[test] + fn handle_encodes_address_big_endian() { + let handler = AliveCheckHandler::new(LogicalAddress::new(0x1234)); + let req = TcpRequest::new(TcpPayloadType::AliveCheckRequest, vec![]); + let resp = handler.handle(req).unwrap(); + assert_eq!(resp.payload(), &[0x12, 0x34]); + } + + #[test] + fn handle_rejects_non_empty_payload() { + let handler = AliveCheckHandler::new(LogicalAddress::new(0x0001)); + let req = TcpRequest::new(TcpPayloadType::AliveCheckRequest, vec![0xAA]); + let resp = handler.handle(req); + assert!(matches!( + resp, + Err(Error::InvalidPayloadLength { + declared: 1, + actual: 1 + }) + )); + } +} diff --git a/src/doip/handlers/routing_activation.rs b/src/doip/handlers/routing_activation.rs new file mode 100644 index 0000000..3a21cf6 --- /dev/null +++ b/src/doip/handlers/routing_activation.rs @@ -0,0 +1,109 @@ +use crate::doip::{ + PayloadHandler, + constants::{ROUTING_ACTIVATION_CODE_SUCCESS, ROUTING_ACTIVATION_REQUEST_MIN_LEN}, + error::Error, + message::{Response, TcpPayloadType, TcpRequest}, + types::LogicalAddress, +}; + +/// Handles RoutingActivationRequest (0x0005, ISO 13400-2 Β§9.9). +/// Currently always returns success (0x10); state machine is future work. +pub struct RoutingActivationHandler { + server_logical_address: LogicalAddress, +} + +impl RoutingActivationHandler { + pub fn new(server_logical_address: LogicalAddress) -> Self { + Self { + server_logical_address, + } + } + + /// Shared protocol logic (ISO 13400-2 #9.9). + /// Returns a RoutingActivationResponse payload. + fn activate(&self, client_address: u16, _activation_type: u8) -> Response { + // Payload layout (13 bytes): + // [0..2] client logical address + // [2..4] server logical address + // [4] response code: 0x10 = success + // [5..9] reserved ISO (0x00000000) + // [9..13] reserved OEM (0x00000000) + let mut payload = Vec::with_capacity(13); + payload.extend_from_slice(&client_address.to_be_bytes()); + payload.extend_from_slice(&self.server_logical_address.to_be_bytes()); + payload.push(ROUTING_ACTIVATION_CODE_SUCCESS); + payload.extend_from_slice(&[0u8; 4]); // reserved ISO + payload.extend_from_slice(&[0u8; 4]); // reserved OEM + Response::new(TcpPayloadType::RoutingActivationResponse as u16, payload) + } +} + +impl PayloadHandler for RoutingActivationHandler { + fn payload_type(&self) -> TcpPayloadType { + TcpPayloadType::RoutingActivationRequest + } + + fn handle(&self, req: TcpRequest) -> Result { + // Payload layout (11 bytes): source_addr(2) + activation_type(1) + reserved(8) + if req.payload().len() < ROUTING_ACTIVATION_REQUEST_MIN_LEN { + return Err(Error::PayloadTooShort { + expected: ROUTING_ACTIVATION_REQUEST_MIN_LEN, + actual: req.payload().len(), + }); + } + let client_address = u16::from_be_bytes([req.payload()[0], req.payload()[1]]); + let activation_type = req.payload()[2]; + Ok(self.activate(client_address, activation_type)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::doip::types::LogicalAddress; + + fn make_req(payload: Vec) -> TcpRequest { + TcpRequest::new(TcpPayloadType::RoutingActivationRequest, payload) + } + + #[test] + fn handle_valid_request_returns_activation_success() { + let handler = RoutingActivationHandler::new(LogicalAddress::new(0x0001)); + let resp = handler + .handle(make_req(vec![0x00, 0x42, 0x00, 0, 0, 0, 0, 0, 0, 0, 0])) + .unwrap(); + assert_eq!( + resp.payload_type(), + TcpPayloadType::RoutingActivationResponse as u16 + ); + assert_eq!( + resp.payload()[4], 0x10, + "response code must be 0x10 (success)" + ); + // client address echoed back + assert_eq!(&resp.payload()[0..2], &[0x00, 0x42]); + // server address + assert_eq!(&resp.payload()[2..4], &[0x00, 0x01]); + } + + #[test] + fn handle_rejects_short_payload() { + let handler = RoutingActivationHandler::new(LogicalAddress::new(0x0001)); + assert!(matches!( + handler.handle(make_req(vec![0x00])), + Err(Error::PayloadTooShort { .. }) + )); + } + + #[test] + fn handle_rejects_partial_payload() { + let handler = RoutingActivationHandler::new(LogicalAddress::new(0x0001)); + assert!(matches!( + handler.handle(make_req(vec![0x00, 0x42, 0x00])), + Err(Error::PayloadTooShort { + expected: 11, + actual: 3 + }) + )); + } +} From cedafae1ef851d06a5befbc62e11ff898b792f69 Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Thu, 28 May 2026 15:18:06 +0530 Subject: [PATCH 09/21] Implement diagnostic message handler and wire dispatcher factories Add DiagnosticsHandler (0x8001) forwarding UDS bytes via SovdProxy and returning DiagnosticMessagePositiveAck. Wire all handlers into build_tcp_dispatcher() and build_udp_dispatcher(). Signed-off-by: VinaykumarRS1995 --- src/doip/handlers/diagnostics.rs | 108 +++++++++++++++++++++++++++++++ src/doip/handlers/mod.rs | 27 ++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/doip/handlers/diagnostics.rs create mode 100644 src/doip/handlers/mod.rs diff --git a/src/doip/handlers/diagnostics.rs b/src/doip/handlers/diagnostics.rs new file mode 100644 index 0000000..44168c6 --- /dev/null +++ b/src/doip/handlers/diagnostics.rs @@ -0,0 +1,108 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use std::sync::Arc; + +use crate::doip::{ + PayloadHandler, + constants::{DIAG_ACK_HEADER_LEN, DIAG_MSG_MIN_PAYLOAD_LEN, DIAGNOSTIC_MESSAGE_ACK}, + error::Error, + message::{Response, TcpPayloadType, TcpRequest}, +}; +use crate::proxy::SovdProxy; + +/// Handles DiagnosticMessage (0x8001, ISO 13400-2 Β§9.11). +/// Forwards UDS bytes to the SOVD proxy and returns the ECU response. +pub struct DiagnosticsHandler { + proxy: Arc, +} + +impl DiagnosticsHandler { + pub fn new(proxy: Arc) -> Self { + Self { proxy } + } + + /// Protocol logic (ISO 13400-2 #9.11): forward UDS bytes to the SOVD proxy, + /// wrap the response in a DiagnosticMessagePositiveAck. + fn forward(&self, src: u16, tgt: u16, uds: &[u8]) -> Result { + let ecu_response = self.proxy.forward(uds)?; + + // Payload layout: + // [0..2] source address (server β†’ originally tgt) + // [2..4] target address (client β†’ originally src) + // [4] ack code: 0x00 = ACK + // [5..] UDS response data from ECU + let mut payload = Vec::with_capacity(DIAG_ACK_HEADER_LEN + ecu_response.len()); + payload.extend_from_slice(&tgt.to_be_bytes()); // server address + payload.extend_from_slice(&src.to_be_bytes()); // client address + payload.push(DIAGNOSTIC_MESSAGE_ACK); + payload.extend_from_slice(&ecu_response); + Ok(Response::new( + TcpPayloadType::DiagnosticMessagePositiveAck as u16, + payload, + )) + } +} + +impl PayloadHandler for DiagnosticsHandler { + fn payload_type(&self) -> TcpPayloadType { + TcpPayloadType::DiagnosticMessage + } + + fn handle(&self, req: TcpRequest) -> Result { + // Payload layout: source_addr(2) + target_addr(2) + uds_data(N) + if req.payload().len() < DIAG_MSG_MIN_PAYLOAD_LEN { + return Err(Error::PayloadTooShort { + expected: DIAG_MSG_MIN_PAYLOAD_LEN, + actual: req.payload().len(), + }); + } + let src = u16::from_be_bytes([req.payload()[0], req.payload()[1]]); + let tgt = u16::from_be_bytes([req.payload()[2], req.payload()[3]]); + self.forward(src, tgt, &req.payload()[4..]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::proxy::mock::MockProxy; + + #[test] + fn handle_forwards_uds_and_returns_ack() { + let handler = DiagnosticsHandler::new(Arc::new(MockProxy)); + let uds = vec![0x22, 0xF1, 0x90]; // ReadDataByIdentifier + let mut payload = vec![0x00, 0x01, 0x10, 0x00]; // src=0x0001 tgt=0x1000 + payload.extend_from_slice(&uds); + + let resp = handler + .handle(TcpRequest::new(TcpPayloadType::DiagnosticMessage, payload)) + .unwrap(); + + assert_eq!( + resp.payload_type(), + TcpPayloadType::DiagnosticMessagePositiveAck as u16 + ); + assert_eq!(resp.payload()[4], 0x00, "ACK code must be 0x00"); + assert_eq!(&resp.payload()[5..], &uds, "MockProxy echoes UDS bytes"); + } + + #[test] + fn handle_rejects_short_payload() { + let handler = DiagnosticsHandler::new(Arc::new(MockProxy)); + let resp = handler.handle(TcpRequest::new( + TcpPayloadType::DiagnosticMessage, + vec![0x00, 0x01], + )); + assert!(matches!(resp, Err(Error::PayloadTooShort { .. }))); + } +} diff --git a/src/doip/handlers/mod.rs b/src/doip/handlers/mod.rs new file mode 100644 index 0000000..6beb5ae --- /dev/null +++ b/src/doip/handlers/mod.rs @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +//! Payload handlers β€” one per DoIP message type. + +pub mod alive_check; +pub mod diagnostics; +pub mod entity_status; +pub mod routing_activation; +pub mod vehicle_identification; + +pub use alive_check::AliveCheckHandler; +pub use diagnostics::DiagnosticsHandler; +pub use entity_status::EntityStatusHandler; +pub use routing_activation::RoutingActivationHandler; +pub use vehicle_identification::{ + IdentifyVehicleByEidHandler, IdentifyVehicleByVinHandler, IdentifyVehicleHandler, +}; From d8d31d23f7c2448911f08fd42636bbc56599f3f7 Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Thu, 28 May 2026 15:18:21 +0530 Subject: [PATCH 10/21] Implement async TCP/UDP transport with DoIP framing Add TCP listener with per-connection task spawning. DoIP framer reconstructs complete messages from partial reads. UDP uses simple recv-parse-dispatch loop with no framing needed. Signed-off-by: VinaykumarRS1995 --- src/server/tcp/framer.rs | 236 ++++++++++++++++++++++++++++++++++++++ src/server/tcp/mod.rs | 78 +++++++++++++ src/server/udp/handler.rs | 165 ++++++++++++++++++++++++++ src/server/udp/mod.rs | 76 ++++++++++++ 4 files changed, 555 insertions(+) create mode 100644 src/server/tcp/framer.rs create mode 100644 src/server/tcp/mod.rs create mode 100644 src/server/udp/handler.rs create mode 100644 src/server/udp/mod.rs diff --git a/src/server/tcp/framer.rs b/src/server/tcp/framer.rs new file mode 100644 index 0000000..16a9636 --- /dev/null +++ b/src/server/tcp/framer.rs @@ -0,0 +1,236 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use crate::doip::constants::{HEADER_LEN, INVERSE_VERSION, PROTOCOL_VERSION}; +use crate::doip::error::Error; +use crate::doip::message::TcpPayloadType; + +/// A fully validated DoIP frame parsed from the TCP byte stream. +#[derive(Debug)] +pub struct Frame { + payload_type: TcpPayloadType, + payload: Vec, +} + +impl Frame { + /// Consume the frame, returning the payload type and raw bytes. + pub fn into_parts(self) -> (TcpPayloadType, Vec) { + (self.payload_type, self.payload) + } +} + +/// Stateful byte-stream framer. +/// +/// Accumulates raw bytes across multiple reads and emits complete, validated +/// DoIP frames. +pub struct Framer { + buffer: Vec, +} + +impl Framer { + /// Create a new framer with an empty internal buffer. + pub fn new() -> Self { + Self { buffer: Vec::new() } + } + + /// Feed raw bytes in; receive zero or more complete frames (or per-frame errors) out. + /// + /// A framing error on one frame does NOT discard subsequent buffered data. + pub fn feed(&mut self, bytes: &[u8]) -> Vec> { + self.buffer.extend_from_slice(bytes); + let mut frames = Vec::new(); + + loop { + if self.buffer.len() < HEADER_LEN { + break; // not enough bytes for a header yet + } + + // Validate protocol version byte + if self.buffer[0] != PROTOCOL_VERSION { + frames.push(Err(Error::InvalidHeaderVersion(self.buffer[0]))); + self.buffer.drain(..1); // discard one byte and attempt re-sync + continue; + } + + // Validate inverse version byte + if self.buffer[1] != INVERSE_VERSION { + frames.push(Err(Error::InvalidInverseVersion(self.buffer[1]))); + self.buffer.drain(..HEADER_LEN); // discard the bad header + continue; + } + + let payload_type_raw = u16::from_be_bytes([self.buffer[2], self.buffer[3]]); + let payload_len = u32::from_be_bytes([ + self.buffer[4], + self.buffer[5], + self.buffer[6], + self.buffer[7], + ]) as usize; + // ISO 13400-2: max DoIP payload size is 64KB for standard diagnostics. + use crate::doip::constants::MAX_DOIP_PAYLOAD_LEN; + + if payload_len > MAX_DOIP_PAYLOAD_LEN { + frames.push(Err(Error::PayloadTooLarge(payload_len))); + self.buffer.drain(..HEADER_LEN); // Discard the header, can't sync payload we don't have + continue; + } + + let total_len = HEADER_LEN + payload_len; + if self.buffer.len() < total_len { + break; // payload not yet fully received β€” wait for more data + } + + let payload_type = match TcpPayloadType::try_from(payload_type_raw) { + Ok(parsed_type) => parsed_type, + Err(raw) => { + frames.push(Err(Error::UnknownPayloadType(raw))); + self.buffer.drain(..total_len); + continue; + } + }; + + let payload = self.buffer[HEADER_LEN..total_len].to_vec(); + self.buffer.drain(..total_len); + frames.push(Ok(Frame { + payload_type, + payload, + })); + } + + frames + } +} + +impl Default for Framer { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn raw_frame(payload_type: u16, payload: &[u8]) -> Vec { + let mut buf = vec![0xFD, 0x02]; + buf.extend_from_slice(&payload_type.to_be_bytes()); + buf.extend_from_slice(&(payload.len() as u32).to_be_bytes()); + buf.extend_from_slice(payload); + buf + } + + #[test] + fn single_complete_frame_emitted() { + let mut f = Framer::new(); + let raw = raw_frame(0x0007, &[]); // AliveCheckRequest, empty payload + let mut out = f.feed(&raw); + assert_eq!(out.len(), 1); + let (pt, payload) = out.remove(0).unwrap().into_parts(); + assert_eq!(pt, TcpPayloadType::AliveCheckRequest); + assert!(payload.is_empty()); + } + + #[test] + fn header_split_across_two_feeds() { + let mut f = Framer::new(); + let raw = raw_frame(0x0007, &[]); + assert!( + f.feed(&raw[..4]).is_empty(), + "partial header yields no frame" + ); + let out = f.feed(&raw[4..]); + assert_eq!(out.len(), 1); + assert!(out[0].is_ok()); + } + + #[test] + fn payload_split_across_two_feeds() { + let mut f = Framer::new(); + let payload = vec![0xAA, 0xBB, 0xCC, 0xDD]; + let raw = raw_frame(0x0007, &payload); + let mid = raw.len() / 2; + assert!( + f.feed(&raw[..mid]).is_empty(), + "partial payload yields no frame" + ); + let mut out = f.feed(&raw[mid..]); + assert_eq!(out.len(), 1); + let (_, frame_payload) = out.remove(0).unwrap().into_parts(); + assert_eq!(frame_payload, payload); + } + + #[test] + fn two_messages_packed_in_one_feed() { + let mut f = Framer::new(); + let mut raw = raw_frame(0x0007, &[]); // AliveCheckRequest + raw.extend(raw_frame( + 0x0005, + &[0x00, 0x01, 0x00, 0, 0, 0, 0, 0, 0, 0, 0], + )); // RoutingActivation + let out = f.feed(&raw); + assert_eq!(out.len(), 2); + assert!(out[0].is_ok()); + assert!(out[1].is_ok()); + } + + #[test] + fn invalid_protocol_version_returns_error() { + let mut f = Framer::new(); + let mut raw = raw_frame(0x0007, &[]); + raw[0] = 0x01; // corrupt version byte + let out = f.feed(&raw); + assert_eq!(out.len(), 1); + assert!(matches!(out[0], Err(Error::InvalidHeaderVersion(0x01)))); + } + + #[test] + fn unknown_payload_type_returns_error() { + let mut f = Framer::new(); + let raw = raw_frame(0xDEAD, &[]); // not a valid TcpPayloadType + let out = f.feed(&raw); + assert_eq!(out.len(), 1); + assert!(matches!(out[0], Err(Error::UnknownPayloadType(0xDEAD)))); + } + + #[test] + fn good_frame_after_bad_frame_is_recovered() { + let mut f = Framer::new(); + let mut raw = raw_frame(0xDEAD, &[]); // bad frame + raw.extend(raw_frame(0x0007, &[])); // good frame after + let out = f.feed(&raw); + assert_eq!(out.len(), 2); + assert!(out[0].is_err()); + assert!(out[1].is_ok()); + } + + #[test] + fn invalid_inverse_version_returns_error() { + let mut f = Framer::new(); + let mut raw = raw_frame(0x0007, &[]); + raw[1] = 0xAB; // corrupt inverse version byte + let out = f.feed(&raw); + assert_eq!(out.len(), 1); + assert!(matches!(out[0], Err(Error::InvalidInverseVersion(0xAB)))); + } + + #[test] + fn payload_too_large_returns_error() { + let mut f = Framer::new(); + // Header declaring 65536 bytes (exceeds MAX_PAYLOAD_LEN of 65535) + let mut raw = vec![0xFD, 0x02]; + raw.extend_from_slice(&0x0007u16.to_be_bytes()); + raw.extend_from_slice(&65_536u32.to_be_bytes()); + let out = f.feed(&raw); + assert_eq!(out.len(), 1); + assert!(matches!(out[0], Err(Error::PayloadTooLarge(65_536)))); + } +} diff --git a/src/server/tcp/mod.rs b/src/server/tcp/mod.rs new file mode 100644 index 0000000..68f1c64 --- /dev/null +++ b/src/server/tcp/mod.rs @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +//! TCP transport β€” accept loop, session management, and byte-stream framing. + +pub mod framer; +pub mod session; + +use std::io; +use std::sync::Arc; + +use tokio::io::AsyncWriteExt; +use tokio::net::TcpListener; + +use super::Transport; +use crate::config::TcpConfig; +use crate::doip::TcpDispatcher; +use crate::doip::message::Response; +use session::{Session, SessionManager}; + +/// TCP transport: binds a listener and spawns one session per accepted connection. +pub struct Tcp { + config: TcpConfig, + manager: SessionManager, + dispatcher: Arc, +} + +impl Tcp { + /// Create a TCP transport with the given config and dispatcher. + pub fn new(config: TcpConfig, dispatcher: TcpDispatcher) -> Self { + let manager = SessionManager::new(config.max_connections()); + Self { + config, + manager, + dispatcher: Arc::new(dispatcher), + } + } +} + +impl Transport for Tcp { + async fn start(&self) -> Result<(), io::Error> { + let listener = TcpListener::bind(self.config.address()).await?; + tracing::info!(address = %self.config.address(), "TCP server listening"); + + loop { + match listener.accept().await { + Ok((mut stream, peer_addr)) => match self.manager.try_acquire() { + Some(slot) => { + tracing::info!(peer = %peer_addr, id = %slot.id(), "new TCP connection"); + let session = Session::new(slot); + let dispatcher = Arc::clone(&self.dispatcher); + let buf_size = self.config.read_buffer_size(); + tokio::spawn(async move { + session.run(stream, dispatcher, buf_size).await; + }); + } + None => { + tracing::warn!(peer = %peer_addr, "connection rejected: max sessions reached"); + let nack = + Response::doip_header_nack(crate::doip::constants::NACK_OUT_OF_MEMORY); + let _ = AsyncWriteExt::write_all(&mut stream, &nack.to_bytes()).await; + drop(stream); + } + }, + Err(err) => tracing::error!(error = %err, "TCP accept error"), + } + } + } +} diff --git a/src/server/udp/handler.rs b/src/server/udp/handler.rs new file mode 100644 index 0000000..1644132 --- /dev/null +++ b/src/server/udp/handler.rs @@ -0,0 +1,165 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use std::sync::Arc; + +use crate::doip::UdpDispatcher; +use crate::doip::constants::{HEADER_LEN, INVERSE_VERSION, PROTOCOL_VERSION}; +use crate::doip::error::Error; +use crate::doip::message::{Response, UdpPayloadType, UdpRequest}; + +/// Parses and dispatches a single UDP DoIP datagram. +/// +/// UDP is datagram-based β€” each `recv_from` call yields one complete message, +pub(crate) struct Handler { + dispatcher: Arc, +} + +impl Handler { + pub(crate) fn new(dispatcher: Arc) -> Self { + Self { dispatcher } + } + + /// Parse one UDP datagram and dispatch it to the registered handler. + /// + /// Returns the response to send back, or an error if the datagram is malformed + /// or the payload type is unrecognised. + pub(crate) fn handle(&self, data: &[u8]) -> Result { + if data.len() < HEADER_LEN { + return Err(Error::InvalidPayloadLength { + declared: 0, + actual: data.len(), + }); + } + if data[0] != PROTOCOL_VERSION { + return Err(Error::InvalidHeaderVersion(data[0])); + } + if data[1] != INVERSE_VERSION { + return Err(Error::InvalidInverseVersion(data[1])); + } + + let payload_type_raw = u16::from_be_bytes([data[2], data[3]]); + let payload_len = u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize; + + if data.len() != HEADER_LEN + payload_len { + return Err(Error::InvalidPayloadLength { + declared: payload_len as u32, + actual: data.len().saturating_sub(HEADER_LEN), + }); + } + + let payload_type = + UdpPayloadType::try_from(payload_type_raw).map_err(Error::UnknownPayloadType)?; + + let udp_request = UdpRequest::new( + payload_type, + data[HEADER_LEN..HEADER_LEN + payload_len].to_vec(), + ); + + self.dispatcher.dispatch(udp_request) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::doip::UdpDispatcher; + use crate::doip::handlers::vehicle_identification::IdentifyVehicleHandler; + use crate::doip::message::UdpPayloadType; + use crate::doip::types::{Eid, Gid, LogicalAddress, Vin}; + + /// Build a well-formed raw datagram with the given payload type and payload. + fn raw_frame(payload_type: u16, payload: &[u8]) -> Vec { + let mut buf = vec![0xFD, 0x02]; + buf.extend_from_slice(&payload_type.to_be_bytes()); + buf.extend_from_slice(&(payload.len() as u32).to_be_bytes()); + buf.extend_from_slice(payload); + buf + } + + fn empty_handler() -> Handler { + Handler::new(Arc::new(UdpDispatcher::new())) + } + + #[test] + fn too_short_data_returns_error() { + let result = empty_handler().handle(&[0xFD, 0x02, 0x00]); + assert!(matches!(result, Err(Error::InvalidPayloadLength { .. }))); + } + + #[test] + fn bad_protocol_version_returns_error() { + let mut data = raw_frame(0x0001, &[]); + data[0] = 0xAB; + let result = empty_handler().handle(&data); + assert!(matches!(result, Err(Error::InvalidHeaderVersion(0xAB)))); + } + + #[test] + fn bad_inverse_version_returns_error() { + let mut data = raw_frame(0x0001, &[]); + data[1] = 0xAB; + let result = empty_handler().handle(&data); + assert!(matches!(result, Err(Error::InvalidInverseVersion(0xAB)))); + } + + #[test] + fn unknown_payload_type_returns_error() { + let data = raw_frame(0xDEAD, &[]); + let result = empty_handler().handle(&data); + assert!(matches!(result, Err(Error::UnknownPayloadType(0xDEAD)))); + } + + #[test] + fn handle_rejects_truncated_payload() { + // Header declares 4 bytes of payload but the datagram is truncated. + let mut data = raw_frame(0x0001, &[0x00, 0x00, 0x00, 0x00]); + data.truncate(10); + let result = empty_handler().handle(&data); + assert!(matches!(result, Err(Error::InvalidPayloadLength { .. }))); + } + + #[test] + fn handle_rejects_trailing_bytes() { + let mut data = raw_frame(0x0001, &[]); + data.extend_from_slice(&[0xAA, 0xBB]); + let result = empty_handler().handle(&data); + assert!(matches!( + result, + Err(Error::InvalidPayloadLength { + declared: 0, + actual: 2 + }) + )); + } + + #[test] + fn handle_valid_vin_request_returns_announcement() { + let mut dispatcher = UdpDispatcher::new(); + dispatcher.register(IdentifyVehicleHandler::new( + Vin::new(*b"00000000000000000"), + Eid::new([0u8; 6]), + Gid::new([0u8; 6]), + LogicalAddress::new(0x0001), + )); + + let handler = Handler::new(Arc::new(dispatcher)); + let data = raw_frame(UdpPayloadType::VehicleIdentificationRequest as u16, &[]); + let resp = handler.handle(&data).unwrap(); + + assert_eq!( + resp.payload_type(), + UdpPayloadType::VehicleAnnouncementResponse as u16 + ); + assert_eq!(resp.payload().len(), 32); + } +} diff --git a/src/server/udp/mod.rs b/src/server/udp/mod.rs new file mode 100644 index 0000000..e7d3956 --- /dev/null +++ b/src/server/udp/mod.rs @@ -0,0 +1,76 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +//! UDP transport β€” recv loop for DoIP discovery and entity status messages. + +pub mod handler; + +use std::io; +use std::sync::Arc; + +use tokio::net::UdpSocket; + +use super::Transport; +use crate::config::UdpConfig; +use crate::doip::UdpDispatcher; +use crate::doip::constants::UDP_RECV_BUF_SIZE; +use crate::doip::error::Error; +use handler::Handler; + +/// UDP transport: binds a socket and dispatches one datagram at a time. +pub struct Udp { + config: UdpConfig, + handler: Handler, +} + +impl Udp { + /// Create a UDP transport with the given config and dispatcher. + pub fn new(config: UdpConfig, dispatcher: UdpDispatcher) -> Self { + Self { + config, + handler: Handler::new(Arc::new(dispatcher)), + } + } +} + +impl Transport for Udp { + async fn start(&self) -> Result<(), io::Error> { + let socket = UdpSocket::bind(self.config.address()).await?; + tracing::info!(address = %self.config.address(), "UDP server listening"); + + let mut buf = vec![0u8; UDP_RECV_BUF_SIZE]; + loop { + match socket.recv_from(&mut buf).await { + Ok((bytes_received, src_addr)) => { + match self.handler.handle(&buf[..bytes_received]) { + Ok(resp) => { + if let Err(err) = socket.send_to(&resp.to_bytes(), src_addr).await { + tracing::error!(error = %err, peer = %src_addr, "UDP send error"); + } + } + Err(Error::NoMatch) => { + tracing::debug!(peer = %src_addr, "no matching entity, not responding"); + } + Err(err) => { + tracing::warn!(error = %err, peer = %src_addr, "UDP dispatch error"); + let nack = crate::doip::message::Response::doip_header_nack( + crate::doip::message::nack_code(&err), + ); + let _ = socket.send_to(&nack.to_bytes(), src_addr).await; + } + } + } + Err(err) => tracing::error!(error = %err, "UDP recv error"), + } + } + } +} From e265972d5bb7731ca653bbc81aa4f9cfcb25a71d Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Thu, 28 May 2026 15:18:34 +0530 Subject: [PATCH 11/21] Add session manager with RAII connection tracking Enforce max concurrent TCP connections using atomic counter. SessionSlot acts as RAII guard that auto-releases on drop, ensuring cleanup on disconnection or task cancellation. Signed-off-by: VinaykumarRS1995 --- src/server/tcp/session/manager.rs | 109 ++++++++++++++++++++++++++++++ src/server/tcp/session/mod.rs | 106 +++++++++++++++++++++++++++++ src/server/tcp/session/slot.rs | 44 ++++++++++++ 3 files changed, 259 insertions(+) create mode 100644 src/server/tcp/session/manager.rs create mode 100644 src/server/tcp/session/mod.rs create mode 100644 src/server/tcp/session/slot.rs diff --git a/src/server/tcp/session/manager.rs b/src/server/tcp/session/manager.rs new file mode 100644 index 0000000..5b4f37c --- /dev/null +++ b/src/server/tcp/session/manager.rs @@ -0,0 +1,109 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use super::slot::ConnectionSlot; +use crate::doip::message::ConnectionId; + +/// Tracks the number of active sessions and enforces the maximum connection limit. +/// +/// Uses an atomic counter shared with `ConnectionSlot` β€” when a slot is dropped +/// (session ends for any reason), the counter decrements automatically. No polling, +/// no background task, no explicit remove() call needed. +pub struct SessionManager { + max: usize, + active: Arc, +} + +impl SessionManager { + pub fn new(max: usize) -> Self { + Self { + max, + active: Arc::new(AtomicUsize::new(0)), + } + } + + /// Attempt to acquire a connection slot. + /// + /// Returns `Some(ConnectionSlot)` if capacity is available, `None` if the + /// maximum is already reached. The returned slot auto-decrements the counter + /// when dropped. + pub fn try_acquire(&self) -> Option { + self.active + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| { + if current < self.max { + Some(current + 1) + } else { + None + } + }) + .map(|_| { + let id = ConnectionId::new(); + tracing::debug!(id = %id, active = self.active_count() ,"session slot acquired"); + ConnectionSlot::new(id, Arc::clone(&self.active)) + }) + .map_err(|_| { + tracing::warn!(max = self.max, "max sessions reached β€” connection rejected"); + }) + .ok() + } + + /// Number of sessions currently active. + pub fn active_count(&self) -> usize { + self.active.load(Ordering::SeqCst) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn acquire_increments_active_count() { + let mgr = SessionManager::new(2); + assert_eq!(mgr.active_count(), 0); + let _slot = mgr.try_acquire().unwrap(); + assert_eq!(mgr.active_count(), 1); + } + + #[test] + fn drop_slot_decrements_active_count() { + let mgr = SessionManager::new(2); + let slot = mgr.try_acquire().unwrap(); + assert_eq!(mgr.active_count(), 1); + drop(slot); + assert_eq!(mgr.active_count(), 0); + } + + #[test] + fn acquire_fails_at_max_capacity() { + let mgr = SessionManager::new(1); + let _slot = mgr.try_acquire().unwrap(); + assert!(mgr.try_acquire().is_none()); + } + + #[test] + fn acquire_succeeds_after_slot_released() { + let mgr = SessionManager::new(1); + let slot = mgr.try_acquire().unwrap(); + drop(slot); + assert!(mgr.try_acquire().is_some()); + } + + #[test] + fn zero_max_always_rejects() { + let mgr = SessionManager::new(0); + assert!(mgr.try_acquire().is_none()); + } +} diff --git a/src/server/tcp/session/mod.rs b/src/server/tcp/session/mod.rs new file mode 100644 index 0000000..6cd477f --- /dev/null +++ b/src/server/tcp/session/mod.rs @@ -0,0 +1,106 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +//! Per-connection session: owns a [`ConnectionSlot`], drives the I/O loop, +//! and dispatches parsed frames to handlers. + +pub mod manager; +pub mod slot; + +pub use manager::SessionManager; +pub use slot::ConnectionSlot; + +use std::sync::Arc; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; + +use crate::doip::TcpDispatcher; +use crate::doip::message::{Response, TcpRequest}; +use crate::server::tcp::framer::Framer; + +/// Represents an accepted TCP connection. +/// +/// Owns the `ConnectionSlot` (RAII counter decrement on drop). When `run()` completes +/// the slot is dropped, automatically decrementing the active session counter. +pub struct Session { + slot: ConnectionSlot, +} + +impl Session { + /// Create a session that owns the given connection slot. + pub fn new(slot: ConnectionSlot) -> Self { + Self { slot } + } + + /// Drive the session I/O loop. + /// + /// Consumes `self` β€” when this future completes (clean close, read error, or + /// write error) the slot is dropped, automatically decrementing the session counter. + pub(crate) async fn run( + self, + mut stream: TcpStream, + dispatcher: Arc, + buf_size: usize, + ) { + let id = &self.slot.id(); + let mut framer = Framer::new(); + let mut buf = vec![0u8; buf_size]; + + loop { + match stream.read(&mut buf).await { + Ok(0) => { + tracing::info!(id = %id, "client disconnected"); + break; + } + Ok(bytes_read) => { + for frame_result in framer.feed(&buf[..bytes_read]) { + match frame_result { + Ok(frame) => { + // TODO: Consider creating TcpRequest directly from the buffer to avoid the intermediate Frame. + let (payload_type, payload) = frame.into_parts(); + let req = TcpRequest::new(payload_type, payload); + match dispatcher.dispatch(req) { + Ok(resp) => { + if let Err(err) = stream.write_all(&resp.to_bytes()).await { + tracing::error!(id = %id, error = %err, "write error"); + return; + } + } + Err(err) => { + tracing::warn!(id = %id, error = %err, "dispatch error"); + let nack = Response::doip_header_nack( + crate::doip::message::nack_code(&err), + ); + let _ = stream.write_all(&nack.to_bytes()).await; + } + } + } + Err(err) => { + tracing::warn!(id = %id, error = %err, "framing error"); + let nack = Response::doip_header_nack( + crate::doip::message::nack_code(&err), + ); + let _ = stream.write_all(&nack.to_bytes()).await; + } + } + } + } + Err(err) => { + tracing::error!(id = %id, error = %err, "read error"); + break; + } + } + } + // self drops here β†’ slot drops β†’ counter decremented + } +} diff --git a/src/server/tcp/session/slot.rs b/src/server/tcp/session/slot.rs new file mode 100644 index 0000000..6d57cb7 --- /dev/null +++ b/src/server/tcp/session/slot.rs @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use crate::doip::message::ConnectionId; + +/// RAII guard for an accepted session slot. +/// +/// Holds the connection's unique ID and a shared reference to the session +/// counter. When dropped (session thread exits, error, or clean close), the +/// counter is automatically decremented β€” no explicit cleanup required. +pub struct ConnectionSlot { + id: ConnectionId, + counter: Arc, +} + +impl ConnectionSlot { + pub(super) fn new(id: ConnectionId, counter: Arc) -> Self { + Self { id, counter } + } + + /// The unique ID assigned to this connection. + pub fn id(&self) -> &ConnectionId { + &self.id + } +} + +impl Drop for ConnectionSlot { + fn drop(&mut self) { + self.counter.fetch_sub(1, Ordering::SeqCst); + tracing::debug!(id = %self.id, "session slot released"); + } +} From 5c9b43ef06171c9c9627226df032f6f7fb609b5e Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Thu, 28 May 2026 15:18:45 +0530 Subject: [PATCH 12/21] Wire server struct combining TCP and UDP transports Add Server struct that starts both listeners concurrently and coordinates graceful shutdown via cancellation token. Signed-off-by: VinaykumarRS1995 --- src/server/mod.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/server/mod.rs diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..8faa3a0 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,37 @@ +//! Transport layer β€” TCP and UDP servers that speak DoIP on the wire. + +pub mod tcp; +pub mod udp; + +use std::io; +use tcp::Tcp; +use udp::Udp; + +/// Lifecycle interface shared by the TCP and UDP transports. +// Justification: This trait is only used with concrete types (Tcp, Udp), +// never as `dyn Transport`, so object-safety is not required. +#[allow(async_fn_in_trait)] +pub trait Transport: Send + Sync { + async fn start(&self) -> Result<(), io::Error>; +} + +/// Top-level server owning both transports. +/// +/// DoIP is defined as exactly one TCP and one UDP transport (ISO 13400-2). +pub struct Server { + tcp: Tcp, + udp: Udp, +} + +impl Server { + /// Create a server owning both transports. + pub fn new(tcp: Tcp, udp: Udp) -> Self { + Self { tcp, udp } + } + + /// Run both transports concurrently. Shuts down gracefully on SIGINT(SIGTERM will also be handled in the future). + pub async fn start(&self) -> Result<(), io::Error> { + tokio::try_join!(self.tcp.start(), self.udp.start())?; + Ok(()) + } +} From e354655b3a33ca854cdbc0ef538e25bfe8509efd Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Thu, 28 May 2026 15:19:00 +0530 Subject: [PATCH 13/21] Add application entry point and DoIP tester example Wire all layers in main(): load config, build dispatchers, start transports, handle SIGINT shutdown. Include DoIP tester for manual integration testing against a running server. Signed-off-by: VinaykumarRS1995 --- app/main.rs | 57 +++++++ examples/doip_tester.rs | 359 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 416 insertions(+) create mode 100644 app/main.rs create mode 100644 examples/doip_tester.rs diff --git a/app/main.rs b/app/main.rs new file mode 100644 index 0000000..1dcf744 --- /dev/null +++ b/app/main.rs @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use std::sync::Arc; +use uds2sovd::{config, doip, error, proxy, server}; + +use config::{ConfigProvider, InMemoryConfigProvider, ServerConfig, TomlConfigProvider}; +use error::AppError; +use proxy::stub::StubProxy; +use server::Server; +use server::tcp::Tcp; +use server::udp::Udp; + +#[tokio::main] +async fn main() -> Result<(), AppError> { + tracing_subscriber::fmt::init(); + + let config = match std::env::args().nth(1) { + Some(path) => TomlConfigProvider::new(path.into()).load(), + None => InMemoryConfigProvider::new(ServerConfig::default()).load(), + }; + + tracing::info!("Starting DoIP server"); + + // TODO: Replace StubProxy with real UDS-to-SOVD proxy implementation. + let (tcp_config, udp_config, ecu_config) = config.into_parts(); + let tcp_dispatcher = doip::tcp_dispatcher(tcp_config.logical_address(), Arc::new(StubProxy)); + let udp_dispatcher = doip::udp_dispatcher( + udp_config.logical_address(), + ecu_config.vin(), + ecu_config.eid(), + ecu_config.gid(), + ); + + let tcp = Tcp::new(tcp_config, tcp_dispatcher); + let udp = Udp::new(udp_config, udp_dispatcher); + + let server = Server::new(tcp, udp); + + tokio::select! { + result = server.start() => { result?; } + _ = tokio::signal::ctrl_c() => { + tracing::info!("Received shutdown signal, stopping server"); + } + } + + Ok(()) +} diff --git a/examples/doip_tester.rs b/examples/doip_tester.rs new file mode 100644 index 0000000..e9c15dd --- /dev/null +++ b/examples/doip_tester.rs @@ -0,0 +1,359 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +//! DoIP tester β€” exercises the running DoIP server end-to-end. +//! +//! # Usage +//! ```sh +//! # Terminal 1: start the server +//! cargo run +//! +//! # Terminal 2: run the tester +//! cargo run --example doip_tester +//! ``` +//! + +use std::io::{Read, Write}; +use std::net::{TcpStream, UdpSocket}; +use std::time::Duration; + +use uds2sovd::doip::constants::{HEADER_LEN, INVERSE_VERSION, PROTOCOL_VERSION}; + +const SERVER_TCP: &str = "127.0.0.1:13400"; +const SERVER_UDP: &str = "127.0.0.1:13400"; +const TIMEOUT: Duration = Duration::from_secs(2); + +// Helpers + +/// Constructs an 8-byte DoIP generic header followed by the payload. +fn build_frame(payload_type: u16, payload: &[u8]) -> Vec { + let len = payload.len() as u32; + let mut frame = Vec::with_capacity(HEADER_LEN + payload.len()); + frame.push(PROTOCOL_VERSION); + frame.push(INVERSE_VERSION); + frame.extend_from_slice(&payload_type.to_be_bytes()); + frame.extend_from_slice(&len.to_be_bytes()); + frame.extend_from_slice(payload); + frame +} + +/// Sends a UDP frame and returns the parsed (payload_type, payload) from the response. +fn udp_roundtrip(payload_type: u16, payload: &[u8]) -> Result<(u16, Vec), String> { + let socket = UdpSocket::bind("0.0.0.0:0").map_err(|e| format!("bind: {e}"))?; + socket + .set_read_timeout(Some(TIMEOUT)) + .map_err(|e| format!("timeout: {e}"))?; + socket + .send_to(&build_frame(payload_type, payload), SERVER_UDP) + .map_err(|e| format!("send: {e}"))?; + let mut buf = [0u8; 256]; + let n = socket.recv(&mut buf).map_err(|e| format!("recv: {e}"))?; + parse_response(&buf[..n]) +} + +/// Sends raw bytes over UDP and returns the parsed response. +fn udp_raw_roundtrip(raw: &[u8]) -> Result<(u16, Vec), String> { + let socket = UdpSocket::bind("0.0.0.0:0").map_err(|e| format!("bind: {e}"))?; + socket + .set_read_timeout(Some(TIMEOUT)) + .map_err(|e| format!("timeout: {e}"))?; + socket + .send_to(raw, SERVER_UDP) + .map_err(|e| format!("send: {e}"))?; + let mut buf = [0u8; 256]; + let n = socket.recv(&mut buf).map_err(|e| format!("recv: {e}"))?; + parse_response(&buf[..n]) +} + +/// Sends a TCP frame on an existing stream and returns the parsed response. +fn tcp_roundtrip( + stream: &mut TcpStream, + payload_type: u16, + payload: &[u8], +) -> Result<(u16, Vec), String> { + stream + .write_all(&build_frame(payload_type, payload)) + .map_err(|e| format!("write: {e}"))?; + let mut buf = [0u8; 256]; + let n = stream.read(&mut buf).map_err(|e| format!("read: {e}"))?; + parse_response(&buf[..n]) +} + +/// Sends raw bytes on a fresh TCP connection and returns the parsed response. +fn tcp_raw_roundtrip(raw: &[u8]) -> Result<(u16, Vec), String> { + let mut stream = TcpStream::connect(SERVER_TCP).map_err(|e| format!("connect: {e}"))?; + stream + .set_read_timeout(Some(TIMEOUT)) + .map_err(|e| format!("timeout: {e}"))?; + stream.write_all(raw).map_err(|e| format!("write: {e}"))?; + let mut buf = [0u8; 256]; + let n = stream.read(&mut buf).map_err(|e| format!("read: {e}"))?; + parse_response(&buf[..n]) +} + +/// Parses a DoIP response buffer into (payload_type, payload_bytes). +fn parse_response(data: &[u8]) -> Result<(u16, Vec), String> { + if data.len() < HEADER_LEN { + return Err("response too short for header".into()); + } + let payload_type = u16::from_be_bytes([data[2], data[3]]); + let payload_len = u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize; + if data.len() < HEADER_LEN + payload_len { + return Err(format!( + "response truncated: have {}, need {}", + data.len(), + HEADER_LEN + payload_len + )); + } + Ok(( + payload_type, + data[HEADER_LEN..HEADER_LEN + payload_len].to_vec(), + )) +} + +/// Asserts the response payload type matches the expected value. +fn expect_type(actual: u16, expected: u16) -> Result<(), String> { + if actual != expected { + Err(format!( + "expected type 0x{expected:04X}, got 0x{actual:04X}" + )) + } else { + Ok(()) + } +} + +/// Asserts the response is a NACK (0x0000) with the expected code byte. +fn expect_nack(response_type: u16, payload: &[u8], expected_code: u8) -> Result<(), String> { + expect_type(response_type, 0x0000)?; + let actual = payload.first().copied().unwrap_or(0xFF); + if actual != expected_code { + Err(format!( + "expected NACK code 0x{expected_code:02X}, got 0x{actual:02X}" + )) + } else { + Ok(()) + } +} + +// UDP Tests -- + +/// 0x0001 VehicleIdentificationRequest β†’ 0x0004 VehicleAnnouncement (32 bytes). +fn test_udp_vehicle_id() -> Result<(), String> { + let (ptype, payload) = udp_roundtrip(0x0001, &[])?; + expect_type(ptype, 0x0004)?; + if payload.len() < 32 { + return Err(format!("payload {}/32 bytes", payload.len())); + } + let addr = u16::from_be_bytes([payload[17], payload[18]]); + if addr != 0x0001 { + return Err(format!("logical address 0x{addr:04X}, expected 0x0001")); + } + Ok(()) +} + +/// 0x0002 VehicleIdentificationRequestWithEid β†’ 0x0004 VehicleAnnouncement. +fn test_udp_vehicle_id_by_eid() -> Result<(), String> { + let (ptype, payload) = udp_roundtrip(0x0002, &[0x00; 6])?; + expect_type(ptype, 0x0004)?; + if payload.len() < 32 { + return Err(format!("payload {}/32 bytes", payload.len())); + } + Ok(()) +} + +/// 0x0003 VehicleIdentificationRequestWithVin β†’ 0x0004 VehicleAnnouncement. +fn test_udp_vehicle_id_by_vin() -> Result<(), String> { + let (ptype, payload) = udp_roundtrip(0x0003, b"00000000000000000")?; + expect_type(ptype, 0x0004)?; + if payload.len() < 32 { + return Err(format!("payload {}/32 bytes", payload.len())); + } + Ok(()) +} + +/// 0x4001 EntityStatusRequest β†’ 0x4002 EntityStatusResponse (7 bytes). +fn test_udp_entity_status() -> Result<(), String> { + let (ptype, payload) = udp_roundtrip(0x4001, &[])?; + expect_type(ptype, 0x4002)?; + if payload.len() < 7 { + return Err(format!("payload {}/7 bytes", payload.len())); + } + Ok(()) +} + +/// Invalid protocol version (0xFF) over UDP β†’ NACK 0x00 (incorrect pattern). +fn test_udp_invalid_version() -> Result<(), String> { + let mut frame = build_frame(0x0001, &[]); + frame[0] = 0xFF; + let (ptype, payload) = udp_raw_roundtrip(&frame)?; + expect_nack(ptype, &payload, 0x00) +} + +// TCP Tests -- + +/// 0x0005 RoutingActivationRequest β†’ 0x0006 RoutingActivationResponse (code 0x10). +/// Returns the stream for reuse by subsequent TCP tests. +fn test_tcp_routing_activation() -> Result { + let mut stream = TcpStream::connect(SERVER_TCP).map_err(|e| format!("connect: {e}"))?; + stream + .set_read_timeout(Some(TIMEOUT)) + .map_err(|e| format!("timeout: {e}"))?; + let (ptype, resp) = tcp_roundtrip( + &mut stream, + 0x0005, + &[ + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + )?; + expect_type(ptype, 0x0006)?; + if resp[0] != 0x0E || resp[1] != 0x00 { + return Err(format!( + "echoed source 0x{:02X}{:02X}, expected 0x0E00", + resp[0], resp[1] + )); + } + if resp[4] != 0x10 { + return Err(format!("activation code 0x{:02X}, expected 0x10", resp[4])); + } + Ok(stream) +} + +/// 0x0007 AliveCheckRequest β†’ 0x0008 AliveCheckResponse (2-byte logical address). +fn test_tcp_alive_check(stream: &mut TcpStream) -> Result<(), String> { + let (ptype, payload) = tcp_roundtrip(stream, 0x0007, &[])?; + expect_type(ptype, 0x0008)?; + let addr = u16::from_be_bytes([payload[0], payload[1]]); + if addr != 0x0001 { + return Err(format!("logical address 0x{addr:04X}, expected 0x0001")); + } + Ok(()) +} + +/// 0x8001 DiagnosticMessage (TesterPresent 0x3E) β†’ 0x8002 PositiveAck. +fn test_tcp_diagnostic_tester_present(stream: &mut TcpStream) -> Result<(), String> { + let (ptype, _) = tcp_roundtrip(stream, 0x8001, &[0x0E, 0x00, 0x00, 0x01, 0x3E, 0x00])?; + expect_type(ptype, 0x8002) +} + +/// Invalid protocol version (0xFF) over TCP β†’ NACK 0x00 (incorrect pattern). +fn test_tcp_invalid_version() -> Result<(), String> { + let mut frame = build_frame( + 0x0005, + &[ + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + ); + frame[0] = 0xFF; + let (ptype, payload) = tcp_raw_roundtrip(&frame)?; + expect_nack(ptype, &payload, 0x00) +} + +/// Unknown payload type (0xBEEF) over TCP β†’ NACK 0x01 (unknown payload type). +fn test_tcp_unknown_payload_type() -> Result<(), String> { + let (ptype, payload) = tcp_raw_roundtrip(&build_frame(0xBEEF, &[]))?; + expect_nack(ptype, &payload, 0x01) +} + +// Main - + +fn main() { + println!("=== DoIP Tester ===\n"); + let mut passed = 0u32; + let mut failed = 0u32; + + // UDP tests + for (name, test_fn) in [ + ( + "udp_vehicle_id", + test_udp_vehicle_id as fn() -> Result<(), String>, + ), + ("udp_vehicle_id_by_eid", test_udp_vehicle_id_by_eid), + ("udp_vehicle_id_by_vin", test_udp_vehicle_id_by_vin), + ("udp_entity_status", test_udp_entity_status), + ("udp_invalid_version", test_udp_invalid_version), + ] { + match test_fn() { + Ok(()) => { + println!("[PASS] {name}"); + passed += 1; + } + Err(e) => { + println!("[FAIL] {name} β€” {e}"); + failed += 1; + } + } + } + + // TCP happy-path tests (shared connection: routing β†’ alive β†’ diagnostic) + let stream = match test_tcp_routing_activation() { + Ok(s) => { + println!("[PASS] tcp_routing_activation"); + passed += 1; + Some(s) + } + Err(e) => { + println!("[FAIL] tcp_routing_activation β€” {e}"); + failed += 1; + None + } + }; + if let Some(mut s) = stream { + for (name, test_fn) in [ + ( + "tcp_alive_check", + test_tcp_alive_check as fn(&mut TcpStream) -> Result<(), String>, + ), + ( + "tcp_diagnostic_tester_present", + test_tcp_diagnostic_tester_present, + ), + ] { + match test_fn(&mut s) { + Ok(()) => { + println!("[PASS] {name}"); + passed += 1; + } + Err(e) => { + println!("[FAIL] {name} β€” {e}"); + failed += 1; + } + } + } + } else { + println!("[SKIP] tcp_alive_check β€” no TCP connection"); + println!("[SKIP] tcp_diagnostic_tester_present β€” no TCP connection"); + } + + // TCP error tests (separate connections) + for (name, test_fn) in [ + ( + "tcp_invalid_version", + test_tcp_invalid_version as fn() -> Result<(), String>, + ), + ("tcp_unknown_payload_type", test_tcp_unknown_payload_type), + ] { + match test_fn() { + Ok(()) => { + println!("[PASS] {name}"); + passed += 1; + } + Err(e) => { + println!("[FAIL] {name} β€” {e}"); + failed += 1; + } + } + } + + let total = passed + failed; + println!("\n=== {passed}/{total} passed ==="); + std::process::exit(if failed > 0 { 1 } else { 0 }); +} From a0fd1bf0e4794f5a5a63abe84cf79ce54cd4c80a Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Fri, 29 May 2026 19:04:25 +0530 Subject: [PATCH 14/21] Address PR Initial review feedback - Convert to Cargo workspace (lib + app + client) - Narrow tokio features to specific set - Refactor handlers to accept EcuConfig directly - Tighten visibility (pub(super), pub(in crate::...)) - Remove DEFAULT_ prefix in defaults module - Add compile_fail doc test for transport segregation - Add TODO comments for future improvements - Update README (headings, commands, abbreviations) - Add sample-doip-server.toml - Regenerate SVGs from updated puml sources --- .gitignore | 19 ++++ Cargo.lock | 91 ++++++------------- Cargo.toml | 22 +---- README.md | 66 +++++++------- app/Cargo.toml | 27 ++++++ app/main.rs | 7 +- client/Cargo.toml | 24 +++++ {examples => client}/doip_tester.rs | 12 +-- docs/01-startup.puml | 34 ++++--- docs/01-startup.svg | 74 +++++++++++++++ docs/02-tcp-connection.puml | 17 +++- docs/02-tcp-connection.svg | 75 +++++++++++++++ docs/03-udp-request.puml | 5 +- docs/03-udp-request.svg | 47 ++++++++++ docs/04-graceful-shutdown.puml | 45 --------- docs/Graceful Shutdown.svg | 1 - docs/Startup.svg | 1 - docs/TCP Connection Lifecycle.svg | 1 - docs/UDP Request Handling.svg | 1 - sample-doip-server.toml | 41 +++++++++ src/Cargo.toml | 30 ++++++ src/config/defaults.rs | 16 ++-- src/config/provider/mod.rs | 3 +- src/config/provider/toml.rs | 1 + src/config/types.rs | 22 +++-- src/doip/constants.rs | 27 +----- src/doip/dispatch.rs | 17 ++++ src/doip/error.rs | 7 +- src/doip/handlers/alive_check.rs | 8 +- src/doip/handlers/diagnostics.rs | 22 ++--- src/doip/handlers/entity_status.rs | 10 +- src/doip/handlers/routing_activation.rs | 23 ++--- .../handlers/vehicle_identification/common.rs | 21 +++-- .../vehicle_identification/request.rs | 30 +++--- .../vehicle_identification/request_by_eid.rs | 46 ++++------ .../vehicle_identification/request_by_vin.rs | 36 +++----- src/doip/message.rs | 61 ++++++++++--- src/doip/mod.rs | 57 +++++++----- src/doip/types.rs | 7 +- src/lib.rs | 26 +++++- src/main.rs | 11 --- src/proxy/mock.rs | 2 +- src/proxy/mod.rs | 2 +- src/proxy/stub.rs | 6 +- src/server/tcp/mod.rs | 9 +- src/server/tcp/session/manager.rs | 8 +- src/server/tcp/session/mod.rs | 13 +-- src/server/tcp/session/slot.rs | 4 +- src/server/udp/handler.rs | 9 +- src/server/udp/mod.rs | 2 +- 50 files changed, 724 insertions(+), 422 deletions(-) create mode 100644 app/Cargo.toml create mode 100644 client/Cargo.toml rename {examples => client}/doip_tester.rs (98%) create mode 100644 docs/01-startup.svg create mode 100644 docs/02-tcp-connection.svg create mode 100644 docs/03-udp-request.svg delete mode 100644 docs/04-graceful-shutdown.puml delete mode 100644 docs/Graceful Shutdown.svg delete mode 100644 docs/Startup.svg delete mode 100644 docs/TCP Connection Lifecycle.svg delete mode 100644 docs/UDP Request Handling.svg create mode 100644 sample-doip-server.toml create mode 100644 src/Cargo.toml delete mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..4464952 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,20 @@ +# Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +.env +.cache +.gradle +.vscode + +build /target + +compile_commands.json diff --git a/Cargo.lock b/Cargo.lock index 0445145..27f06e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,36 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "doip-client" +version = "0.1.0" +dependencies = [ + "doipserver-lib", +] + +[[package]] +name = "doip-server" +version = "0.1.0" +dependencies = [ + "doipserver-lib", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "doipserver-lib" +version = "0.1.0" +dependencies = [ + "serde", + "thiserror", + "tokio", + "toml", + "tracing", + "tracing-subscriber", + "uuid", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -166,15 +196,6 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" version = "0.4.30" @@ -213,29 +234,6 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - [[package]] name = "pin-project-lite" version = "0.2.17" @@ -276,27 +274,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "semver" version = "1.0.28" @@ -445,7 +428,6 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -562,19 +544,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "uds2sovd" -version = "0.1.0" -dependencies = [ - "serde", - "thiserror", - "tokio", - "toml", - "tracing", - "tracing-subscriber", - "uuid", -] - [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/Cargo.toml b/Cargo.toml index 1919a6c..212c499 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,22 +9,6 @@ # # SPDX-License-Identifier: Apache-2.0 -[package] -name = "uds2sovd" -version = "0.1.0" -edition = "2024" -rust-version = "1.85" -description = "DoIP server (ISO 13400-2) that proxies UDS diagnostics to a SOVD backend" - -[[bin]] -name = "doip-server" -path = "app/main.rs" - -[dependencies] -tokio = { version = "1", features = ["full"] } -serde = { version = "1", features = ["derive"] } -toml = "0.8" -uuid = { version = "1", features = ["v4"] } -tracing = "0.1" -tracing-subscriber = "0.3" -thiserror = "2" +[workspace] +members = ["src", "app", "client"] +resolver = "3" diff --git a/README.md b/README.md index 37958c3..7a791ba 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,11 @@ https://www.apache.org/licenses/LICENSE-2.0 # πŸ”Œ UDS-to-SOVD Proxy -This repository contains the UDS-to-SOVD Proxy of the [Eclipse OpenSOVD](https://github.com/eclipse-opensovd/uds2sovd-proxy) project. +This repository contains the UDS-to-SOVD Proxy of the [Eclipse OpenSOVD](https://github.com/eclipse-opensovd) project. In the SOVD (Service-Oriented Vehicle Diagnostics) context, the UDS-to-SOVD Proxy serves as a protocol translation gateway between legacy UDS (Unified Diagnostic Services) based diagnostic tools and the modern SOVD-based diagnostic architecture. -It accepts UDS requests over DoIP (Diagnostics over IP, [ISO 13400-2](https://www.iso.org/standard/74785.html)), resolves the corresponding SOVD service using the diagnostic description (MDD) of the ECU, and translates them into SOVD REST API calls. The SOVD responses are then encoded back into UDS format and returned to the requesting tool. +It accepts UDS requests over DoIP (Diagnostics over IP, [ISO 13400-2](https://www.iso.org/standard/74785.html)) and forwards them to an SOVD backend. The SOVD responses are then encoded back into UDS format and returned to the requesting tool. ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” @@ -31,23 +31,22 @@ It accepts UDS requests over DoIP (Diagnostics over IP, [ISO 13400-2](https://ww β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` -> **Project status:** The UDS2SOVD translation layer currently returns NRC 0x11 (serviceNotSupported) for all diagnostic requests (StubProxy). Real SOVD integration is under development. -## goals +## Goals - transparent UDS ↔ SOVD protocol translation - high performance (asynchronous I/O) - low memory and disk-space consumption -- safe & secure +- safe and secure - fast startup -## introduction +## Introduction -The proxy consists of a **DoIP Server** (handles the DoIP wire protocol over TCP :13400 / UDP :13400) and the **UDS2SOVD translation layer** (translates UDS request bytes into SOVD REST API calls using the ECU's MDD diagnostic description). +The proxy consists of a **DoIP Server** (handles the DoIP wire protocol over TCP :13400 / UDP :13400) and the **UDS2SOVD translation layer** (forwards UDS request bytes to an SOVD backend via the `SovdProxy` trait). -**Discovery** happens over UDP β€” testers broadcast vehicle identification requests and the server responds with its VIN, EID, and logical address. **Diagnostics** happen over TCP β€” after a routing activation handshake, the tester sends UDS requests which the server forwards to the UDS2SOVD layer. +**Discovery** happens over UDP β€” testers broadcast vehicle identification requests and the server responds with its VIN (Vehicle Identification Number), EID (Entity Identifier), and logical address. **Diagnostics** happen over TCP β€” after a routing activation handshake, the tester sends UDS requests which the server forwards to the UDS2SOVD layer. -### supported messages +### Supported Messages | Payload Type | Name | Transport | Behavior | |-------------|------|-----------|----------| @@ -59,24 +58,24 @@ The proxy consists of a **DoIP Server** (handles the DoIP wire protocol over TCP | 0x0007 | AliveCheckRequest | TCP | Confirms connection is live | | 0x8001 | DiagnosticMessage | TCP | Forwards UDS payload, returns ECU response | -### usage +### Usage 1. Run with defaults (TCP `127.0.0.1:13400`, UDP `0.0.0.0:13400`): ```sh - cargo run + cargo run -p doip-server ``` -2. Or with a TOML config file: +2. Or with a TOML config file (see [`sample-doip-server.toml`](sample-doip-server.toml)): ```sh - cargo run -- path/to/config.toml + cargo run -p doip-server -- ``` 3. Verify with the E2E tester (proxy must be running): ```sh - cargo run --example doip_tester + cargo run -p doip-client ``` -### configuration +### Configuration -If no config file is passed, sensible defaults are used: +If no configuration file is provided, the system will apply default settings: | Setting | Default | Description | |---------|---------|-------------| @@ -86,38 +85,38 @@ If no config file is passed, sensible defaults are used: | Read buffer | `4096` bytes | TCP read chunk size | | Logical address | `0x0001` | DoIP entity address | | VIN | `00000000000000000` | Vehicle Identification Number | -| EID | `00:00:00:00:00:00` | Entity ID (MAC address) | -| GID | `00:00:00:00:00:00` | Group ID | +| EID | `00:00:00:00:00:00` | Entity Identifier (MAC address) | +| GID | `00:00:00:00:00:00` | Group Identifier | -## building +## Building -### prerequisites +### Prerequisites Rust toolchain β‰₯ 1.85 β€” install via [rustup](https://rustup.rs/). -### build the executable +### Build the Executable ```sh cargo build --release ``` -## developing +## Developing -### pre commit +### Pre Commit ```sh uv run https://raw.githubusercontent.com/eclipse-opensovd/cicd-workflows/main/run_checks.py ``` -### codestyle +### Codestyle -see [codestyle](CODESTYLE.md) +See [CODESTYLE.md](CODESTYLE.md). -### testing +### Testing -#### unit tests +#### Unit Tests -Unittests are placed in the relevant module as usual in rust: +Unit tests are placed in the relevant module as usual in Rust: ```rust ... #[cfg(test)] @@ -131,15 +130,14 @@ Run unit tests with: cargo test --locked --lib ``` -#### integration tests +#### Integration Tests -Start the proxy, then run the E2E tester: +Open one terminal and start the proxy. Then open a second terminal and run the E2E tester: ```sh -cargo run & -cargo run --example doip_tester +cargo run -p doip-server +cargo run -p doip-client ``` -## license +## License Apache-2.0 β€” see [LICENSE](LICENSE). - diff --git a/app/Cargo.toml b/app/Cargo.toml new file mode 100644 index 0000000..b0ec6ce --- /dev/null +++ b/app/Cargo.toml @@ -0,0 +1,27 @@ +# Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "doip-server" +version = "0.1.0" +edition = "2024" +rust-version = "1.85" +description = "DoIP server binary β€” wires configuration, transports, and handlers" + +[[bin]] +name = "doip-server" +path = "main.rs" + +[dependencies] +uds2sovd = { path = "../src", package = "doipserver-lib" } +tokio = { version = "1", features = ["net", "io-util", "macros", "rt-multi-thread", "signal"] } +tracing = "0.1" +tracing-subscriber = "0.3" diff --git a/app/main.rs b/app/main.rs index 1dcf744..9e5bdfe 100644 --- a/app/main.rs +++ b/app/main.rs @@ -34,12 +34,7 @@ async fn main() -> Result<(), AppError> { // TODO: Replace StubProxy with real UDS-to-SOVD proxy implementation. let (tcp_config, udp_config, ecu_config) = config.into_parts(); let tcp_dispatcher = doip::tcp_dispatcher(tcp_config.logical_address(), Arc::new(StubProxy)); - let udp_dispatcher = doip::udp_dispatcher( - udp_config.logical_address(), - ecu_config.vin(), - ecu_config.eid(), - ecu_config.gid(), - ); + let udp_dispatcher = doip::udp_dispatcher(udp_config.logical_address(), &ecu_config); let tcp = Tcp::new(tcp_config, tcp_dispatcher); let udp = Udp::new(udp_config, udp_dispatcher); diff --git a/client/Cargo.toml b/client/Cargo.toml new file mode 100644 index 0000000..3116636 --- /dev/null +++ b/client/Cargo.toml @@ -0,0 +1,24 @@ +# Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "doip-client" +version = "0.1.0" +edition = "2024" +rust-version = "1.85" +description = "DoIP tester client β€” exercises the running DoIP server end-to-end" + +[[bin]] +name = "doip-client" +path = "doip_tester.rs" + +[dependencies] +uds2sovd = { path = "../src", package = "doipserver-lib" } diff --git a/examples/doip_tester.rs b/client/doip_tester.rs similarity index 98% rename from examples/doip_tester.rs rename to client/doip_tester.rs index e9c15dd..4a54758 100644 --- a/examples/doip_tester.rs +++ b/client/doip_tester.rs @@ -15,10 +15,10 @@ //! # Usage //! ```sh //! # Terminal 1: start the server -//! cargo run +//! cargo run -p doip-server //! //! # Terminal 2: run the tester -//! cargo run --example doip_tester +//! cargo run -p doip-client //! ``` //! @@ -32,7 +32,7 @@ const SERVER_TCP: &str = "127.0.0.1:13400"; const SERVER_UDP: &str = "127.0.0.1:13400"; const TIMEOUT: Duration = Duration::from_secs(2); -// Helpers +// Helpers /// Constructs an 8-byte DoIP generic header followed by the payload. fn build_frame(payload_type: u16, payload: &[u8]) -> Vec { @@ -144,7 +144,7 @@ fn expect_nack(response_type: u16, payload: &[u8], expected_code: u8) -> Result< } } -// UDP Tests -- +// UDP Tests -- /// 0x0001 VehicleIdentificationRequest β†’ 0x0004 VehicleAnnouncement (32 bytes). fn test_udp_vehicle_id() -> Result<(), String> { @@ -198,7 +198,7 @@ fn test_udp_invalid_version() -> Result<(), String> { expect_nack(ptype, &payload, 0x00) } -// TCP Tests -- +// TCP Tests -- /// 0x0005 RoutingActivationRequest β†’ 0x0006 RoutingActivationResponse (code 0x10). /// Returns the stream for reuse by subsequent TCP tests. @@ -263,7 +263,7 @@ fn test_tcp_unknown_payload_type() -> Result<(), String> { expect_nack(ptype, &payload, 0x01) } -// Main - +// Main - fn main() { println!("=== DoIP Tester ===\n"); diff --git a/docs/01-startup.puml b/docs/01-startup.puml index 717d6a0..2546d3c 100644 --- a/docs/01-startup.puml +++ b/docs/01-startup.puml @@ -1,5 +1,17 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + @startuml Startup -title DoIP Server - Startup Flow +title 01-startup -participant "Main" as Main +participant "DoIP Server App" as DoipServerApp participant "Server" as Server participant "TCP Service" as Tcp participant "UDP Service" as Udp == Load Configuration == -Main -> Main: load configuration +DoipServerApp -> DoipServerApp: load configuration ||| -note right of Main +note right of DoipServerApp CLI config file or defaults end note == Initialize Services == -Main -> Tcp: create TCP service +DoipServerApp -> Tcp: create TCP service ||| -Main -> Udp: create UDP service +DoipServerApp -> Udp: create UDP service ||| -Main -> Server: create server +DoipServerApp -> Server: create server == Start Runtime == -Main -> Server: start() +DoipServerApp -> Server: start() ||| Server -> Tcp: bind TCP listener alt TCP bind failure Tcp --> Server: startup error - Server --> Main: startup failed + Server --> DoipServerApp: startup failed end ||| Server -> Udp: bind UDP socket alt UDP bind failure Udp --> Server: startup error - Server --> Main: startup failed + Server --> DoipServerApp: startup failed end == Running State == @@ -59,4 +71,4 @@ note over Tcp, Udp Services running concurrently end note -@enduml \ No newline at end of file +@enduml diff --git a/docs/01-startup.svg b/docs/01-startup.svg new file mode 100644 index 0000000..0c178d9 --- /dev/null +++ b/docs/01-startup.svg @@ -0,0 +1,74 @@ +01-startupDoIP Server AppDoIP Server AppServerServerTCP ServiceTCP ServiceUDP ServiceUDP ServiceLoad Configurationload configurationCLI config file or defaultsInitialize Servicescreate TCP servicecreate UDP servicecreate serverStart Runtimestart()bind TCP listeneralt[TCP bind failure]startup errorstartup failedbind UDP socketalt[UDP bind failure]startup errorstartup failedRunning Statestart connection loopstart discovery loopServices running concurrently \ No newline at end of file diff --git a/docs/02-tcp-connection.puml b/docs/02-tcp-connection.puml index f5001b2..53ec740 100644 --- a/docs/02-tcp-connection.puml +++ b/docs/02-tcp-connection.puml @@ -1,5 +1,17 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + @startuml TCP_Connection_Lifecycle -title DoIP Server - TCP Connection Lifecycle +title 02-tcp-connection + +participant "Client" as Client +participant "TCP Service" as Tcp +participant "Session" as Sess +participant "UDS2SOVD" as Proxy + +== Connection Establishment == + +Client -> Tcp: connect (port 13400) + +alt maximum sessions reached +Tcp -> Client: connection rejected +Tcp ->x Client: close connection + +else session accepted +Tcp -> Sess: spawn session +end + +== Request Processing == + +loop until disconnect or failure + + +Client -> Sess: DoIP request + +alt invalid request + Sess -> Client: negative acknowledgment + +else diagnostic message + Sess -> Proxy: forward diagnostic payload + Proxy - -> Sess: diagnostic response + Sess -> Client: diagnostic response + +else supported request + Sess -> Client: response +end + +note right of Sess + communication failure + terminates session +end note + + +end + +== Session Cleanup == + +note over Sess + session ends + slot dropped automatically +end note +@enduml + +PlantUML version 1.2020.02(Sun Mar 01 15:52:07 IST 2020) +(GPL source distribution) +Java Runtime: OpenJDK Runtime Environment +JVM: OpenJDK 64-Bit Server VM +Java Version: 21.0.10+7-Ubuntu-124.04 +Operating System: Linux +Default Encoding: UTF-8 +Language: en +Country: null +--> \ No newline at end of file diff --git a/docs/03-udp-request.puml b/docs/03-udp-request.puml index 18bd957..92d6b37 100644 --- a/docs/03-udp-request.puml +++ b/docs/03-udp-request.puml @@ -1,6 +1,5 @@ @startuml UDP_Request_Handling -title DoIP Server - UDP Discovery - +title 03-udp-request + +participant "Client" as Client +participant "UDP Service" as Udp + +== Receive Loop == + +loop waiting for datagrams + + Client -> Udp: discovery request (port 13400) + + alt valid identification request + Udp -> Client: vehicle announcement (VIN, EID, address) + else EID/VIN does not match + note right of Udp + silent drop (ISO 7.6.1) + end note + else entity status request + Udp -> Client: node type and capacity + else invalid request + Udp -> Client: negative acknowledgment + end + +end + +@enduml + +PlantUML version 1.2020.02(Sun Mar 01 15:52:07 IST 2020) +(GPL source distribution) +Java Runtime: OpenJDK Runtime Environment +JVM: OpenJDK 64-Bit Server VM +Java Version: 21.0.10+7-Ubuntu-124.04 +Operating System: Linux +Default Encoding: UTF-8 +Language: en +Country: null +--> \ No newline at end of file diff --git a/docs/04-graceful-shutdown.puml b/docs/04-graceful-shutdown.puml deleted file mode 100644 index e690910..0000000 --- a/docs/04-graceful-shutdown.puml +++ /dev/null @@ -1,45 +0,0 @@ -@startuml Graceful_Shutdown -title DoIP Server - Graceful Shutdown - - - -participant "OS" as OS -participant "main" as Main -participant "Server" as Server -participant "Sessions" as Sess - -== Running == - -Main -> Server: start() - -note over Server - TCP and UDP services - running concurrently -end note - -== Shutdown == - -OS -> Main: SIGINT (Ctrl+C) - -Main --> Server: server future dropped - -note over Server - TCP listener dropped (stops accepting) - UDP socket dropped (stops receiving) -end note - -Main --> Sess: runtime exits - -note over Sess - all tasks cancelled - session resources released automatically -end note - -@enduml \ No newline at end of file diff --git a/docs/Graceful Shutdown.svg b/docs/Graceful Shutdown.svg deleted file mode 100644 index 2acf5a5..0000000 --- a/docs/Graceful Shutdown.svg +++ /dev/null @@ -1 +0,0 @@ -Graceful ShutdownOSServerTcpUdpSession.s.OSOSServerServerTcpTcpUdpUdpSession(s)Session(s)Running concurrently via try_join!SIGINT (ctrl+c)tokio::select! → ctrl_c() branch winsLogs: "Received shutdown signal, stopping server"start() returns Ok(())[drop] tcp.start() future cancelled[drop] udp.start() future cancelledTcpListener dropped → stops acceptingSpawned sessions continue untiltokio runtime shuts down → tasks cancelled[runtime drop] tasks cancelled \ No newline at end of file diff --git a/docs/Startup.svg b/docs/Startup.svg deleted file mode 100644 index 8e6ce13..0000000 --- a/docs/Startup.svg +++ /dev/null @@ -1 +0,0 @@ -DoIP Server - Startup FlowApplicationServerTCP ServiceUDP ServiceApplicationApplicationServerServerTCP ServiceTCP ServiceUDP ServiceUDP ServiceLoad Configurationload configurationCLI config file or defaultsInitialize Servicescreate TCP servicecreate UDP servicecreate serverStart Runtimestart()bind TCP listeneralt[TCP bind failure]startup errorstartup failedbind UDP socketalt[UDP bind failure]startup errorstartup failedRunning Statestart connection loopstart discovery loopServices running concurrently \ No newline at end of file diff --git a/docs/TCP Connection Lifecycle.svg b/docs/TCP Connection Lifecycle.svg deleted file mode 100644 index 31da5fe..0000000 --- a/docs/TCP Connection Lifecycle.svg +++ /dev/null @@ -1 +0,0 @@ -TCP Connection LifecycleClientTcpSessionManagerSessionFramerDispatcherPayloadHandlerSovdProxyClientClientTcpTcpSessionManagerSessionManagerSessionSessionFramerFramerDispatcherDispatcherPayloadHandlerPayloadHandlerSovdProxySovdProxyTCP connectlog error, continue looptry_acquire()alt[max sessions reached]NoneNACK 0x02 (message too large)close connection[slot available]Some(ConnectionSlot)spawn Session::run(stream, dispatcher, buf_size)loop[I/O loop]stream.read(&mut buf).awaitalt[read returns 0 (client disconnected)]break loop[read error]break loop[read OK (bytes_read > 0)]feed(&buf[..bytes_read])alt[framing error (bad version/too large)]Err(Error)NACK 0x00 (incorrect pattern)continue loop[valid frame]Ok(Frame)dispatch(TcpRequest)alt[unknown payload type]Err(UnknownPayloadType)NACK 0x01 (unknown type)[handler found]handle(req)opt[DiagnosticMessage]forward(uds_bytes)ecu_responseOk(Response)Ok(Response)write responsealt[write fails]Socket broken — return (terminate session)Client disconnects (read returns 0)or read error → break loop[implicit] ConnectionSlot dropped → counter-- \ No newline at end of file diff --git a/docs/UDP Request Handling.svg b/docs/UDP Request Handling.svg deleted file mode 100644 index 1afabbf..0000000 --- a/docs/UDP Request Handling.svg +++ /dev/null @@ -1 +0,0 @@ -UDP Request HandlingClientUdp .mod.rs.Handler .handler.rs.DispatcherPayloadHandlerClientClientUdp (mod.rs)Udp (mod.rs)Handler (handler.rs)Handler (handler.rs)DispatcherDispatcherPayloadHandlerPayloadHandlerUdpSocket::bind(addr)loop[recv loop]broadcast UDP datagram (port 13400)socket.recv_from(&mut buf).awaitalt[recv_from error]log error, continue loop[Ok(bytes_received, src_addr)]handle(&buf[..bytes_received])validate version, inverse version, payload lengthUdpPayloadType::try_from(payload_type_raw)alt[invalid header or unknown payload type]Err(Error)NACK 0x01 (unknown payload type)send_to error is logged, not fatal[valid request]dispatch(UdpRequest)alt[no handler registered]Err(UnknownPayloadType)Err(Error)NACK 0x01[handler found]handle(req)Ok(Response)Ok(Response)Ok(Response)socket.send_to(response, src_addr) [unicast]send_to error is logged, not fatal \ No newline at end of file diff --git a/sample-doip-server.toml b/sample-doip-server.toml new file mode 100644 index 0000000..95a816f --- /dev/null +++ b/sample-doip-server.toml @@ -0,0 +1,41 @@ +# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +# Sample DoIP Server Configuration +# Usage: cargo run -p doip-server -- sample-doip-server.toml + +[tcp] +# TCP listener bind address and port (ISO 13400-2 default: 13400) +address = "127.0.0.1:13400" +# Maximum concurrent TCP connections +max_connections = 10 +# DoIP logical address of this entity +logical_address = 1 +# Read buffer size in bytes per connection +read_buffer_size = 4096 + +[udp] +# UDP socket bind address and port (ISO 13400-2 default: 13400) +address = "0.0.0.0:13400" +# DoIP logical address of this entity +logical_address = 1 + +[ecu] +# Vehicle Identification Number β€” 17 ASCII bytes (ISO 3779) +# Each value is the ASCII code of the character: '0' = 48, 'A' = 65, etc. +# Below represents VIN "00000000000000000" +vin = [48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48] +# Entity Identifier β€” 6 bytes, typically the MAC address of the DoIP interface +# Example: MAC 00:00:00:00:00:00 +eid = [0, 0, 0, 0, 0, 0] +# Group Identifier β€” 6 bytes, groups DoIP entities on the same subnet +# Example: GID 00:00:00:00:00:00 +gid = [0, 0, 0, 0, 0, 0] diff --git a/src/Cargo.toml b/src/Cargo.toml new file mode 100644 index 0000000..a8b8626 --- /dev/null +++ b/src/Cargo.toml @@ -0,0 +1,30 @@ +# Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "doipserver-lib" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" +description = "DoIP server library β€” core DoIP protocol implementation, decoupled from transport and application logic" + +[dependencies] +tokio = { version = "1", features = ["net", "io-util", "macros", "rt-multi-thread", "signal"] } +serde = { version = "1", features = ["derive"] } +toml = "0.8" +uuid = { version = "1", features = ["v4"] } +tracing = "0.1" +tracing-subscriber = "0.3" +thiserror = "2" + +[lib] +name = "doipserverlib" +path = "lib.rs" \ No newline at end of file diff --git a/src/config/defaults.rs b/src/config/defaults.rs index 5ef3f7e..7c14c46 100644 --- a/src/config/defaults.rs +++ b/src/config/defaults.rs @@ -14,31 +14,31 @@ use crate::doip::types::{Eid, Gid, LogicalAddress, Vin}; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; /// Default TCP listen address (loopback, standard DoIP port 13400). -pub const DEFAULT_TCP_ADDRESS: SocketAddr = +pub const TCP_ADDRESS: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 13400)); /// Default UDP listen address (all interfaces, standard DoIP port 13400). -pub const DEFAULT_UDP_ADDRESS: SocketAddr = +pub const UDP_ADDRESS: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 13400)); /// Default maximum number of concurrent TCP connections. -pub const DEFAULT_MAX_CONNECTIONS: usize = 10; +pub const MAX_CONNECTIONS: usize = 10; // TODO: Add DEFAULT_MAX_DATA_SIZE constant for entity status response. /// Default TCP read buffer size in bytes. -pub const DEFAULT_READ_BUFFER_SIZE: usize = 4096; +pub const READ_BUFFER_SIZE: usize = 4096; /// Default DoIP logical address for this server entity. -pub const DEFAULT_LOGICAL_ADDRESS: LogicalAddress = LogicalAddress::new(0x0001); +pub const LOGICAL_ADDRESS: LogicalAddress = LogicalAddress::new(0x0001); /// Default VIN: 17 ASCII zeroes. /// Must be overridden with the actual vehicle VIN in production. -pub const DEFAULT_VIN: Vin = Vin::new(*b"00000000000000000"); +pub const VIN: Vin = Vin::new(*b"00000000000000000"); /// Default Entity Identifier: all-zero bytes. /// Should be set to the MAC address of the DoIP network interface. -pub const DEFAULT_EID: Eid = Eid::new([0u8; 6]); +pub const EID: Eid = Eid::new([0u8; 6]); /// Default Group Identifier: all-zero bytes. -pub const DEFAULT_GID: Gid = Gid::new([0u8; 6]); +pub const GID: Gid = Gid::new([0u8; 6]); diff --git a/src/config/provider/mod.rs b/src/config/provider/mod.rs index 673a2b7..9fbdb3d 100644 --- a/src/config/provider/mod.rs +++ b/src/config/provider/mod.rs @@ -15,6 +15,7 @@ pub mod in_memory; pub mod toml; +pub use self::toml as Toml_provider; pub use in_memory::InMemoryConfigProvider; -pub use toml::TomlConfigProvider; +pub use Toml_provider::TomlConfigProvider; \ No newline at end of file diff --git a/src/config/provider/toml.rs b/src/config/provider/toml.rs index e42f054..538c6eb 100644 --- a/src/config/provider/toml.rs +++ b/src/config/provider/toml.rs @@ -27,6 +27,7 @@ impl TomlConfigProvider { } impl ConfigProvider for TomlConfigProvider { + // TODO: Replace panics with proper error propagation (Result) for production use. fn load(&self) -> ServerConfig { let content = std::fs::read_to_string(&self.path) .unwrap_or_else(|e| panic!("Failed to read config file {:?}: {}", self.path, e)); diff --git a/src/config/types.rs b/src/config/types.rs index b85be96..6c76c5d 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -85,10 +85,10 @@ impl UdpConfig { impl Default for TcpConfig { fn default() -> Self { Self { - address: defaults::DEFAULT_TCP_ADDRESS, - max_connections: defaults::DEFAULT_MAX_CONNECTIONS, - logical_address: defaults::DEFAULT_LOGICAL_ADDRESS, - read_buffer_size: defaults::DEFAULT_READ_BUFFER_SIZE, + address: defaults::TCP_ADDRESS, + max_connections: defaults::MAX_CONNECTIONS, + logical_address: defaults::LOGICAL_ADDRESS, + read_buffer_size: defaults::READ_BUFFER_SIZE, } } } @@ -96,8 +96,8 @@ impl Default for TcpConfig { impl Default for UdpConfig { fn default() -> Self { Self { - address: defaults::DEFAULT_UDP_ADDRESS, - logical_address: defaults::DEFAULT_LOGICAL_ADDRESS, + address: defaults::UDP_ADDRESS, + logical_address: defaults::LOGICAL_ADDRESS, } } } @@ -111,6 +111,10 @@ pub struct EcuConfig { } impl EcuConfig { + /// Create a new ECU config from the given identity fields. + pub fn new(vin: Vin, eid: Eid, gid: Gid) -> Self { + Self { vin, eid, gid } + } /// Vehicle Identification Number (17 ASCII characters). pub fn vin(&self) -> Vin { self.vin @@ -128,9 +132,9 @@ impl EcuConfig { impl Default for EcuConfig { fn default() -> Self { Self { - vin: defaults::DEFAULT_VIN, - eid: defaults::DEFAULT_EID, - gid: defaults::DEFAULT_GID, + vin: defaults::VIN, + eid: defaults::EID, + gid: defaults::GID, } } } diff --git a/src/doip/constants.rs b/src/doip/constants.rs index 41bc069..2dc129c 100644 --- a/src/doip/constants.rs +++ b/src/doip/constants.rs @@ -35,23 +35,6 @@ pub const DIAGNOSTIC_MESSAGE_ACK: u8 = 0x00; /// No further action is required from the client. pub const NO_FURTHER_ACTION: u8 = 0x00; -// Generic DoIP header NACK codes (ISO 13400-2 Β§9.4, Table 18) - -/// Header fields do not match the expected pattern (bad version or inverse byte). -pub const NACK_INCORRECT_PATTERN: u8 = 0x00; - -/// Payload type is not supported by this entity. -pub const NACK_UNKNOWN_PAYLOAD_TYPE: u8 = 0x01; - -/// Message is too large to be processed. -pub const NACK_MESSAGE_TOO_LARGE: u8 = 0x02; - -/// Server ran out of memory. -pub const NACK_OUT_OF_MEMORY: u8 = 0x03; - -/// Payload length field does not match actual payload size. -pub const NACK_INVALID_PAYLOAD_LENGTH: u8 = 0x04; - /// Receive buffer size for UDP DoIP datagrams. /// All ISO 13400-2 defined UDP messages fit within a single Ethernet frame (MTU 1500 bytes). /// The largest defined message is VehicleAnnouncementResponse at 40 bytes. @@ -64,7 +47,7 @@ pub const VIN_LEN: usize = 17; /// EID (Entity Identification / MAC address) length in bytes. pub const EID_LEN: usize = 6; -// Entity status (ISO 13400-2 Β§7.6.3) +// Entity status (ISO 13400-2 Β§7.6.3) /// DoIP node type: DoIP gateway (0x00) or DoIP node (0x01). pub const DOIP_NODE_TYPE: u8 = 0x01; @@ -72,17 +55,17 @@ pub const DOIP_NODE_TYPE: u8 = 0x01; /// Entity status response payload length: 1 (node type) + 1 (max TCP) + 1 (current TCP) + 4 (max data size). pub const ENTITY_STATUS_RESPONSE_LEN: usize = 7; -// Maximum payload (ISO 13400-2 Β§7.3) +// Maximum payload (ISO 13400-2 Β§7.3) /// Maximum DoIP payload length accepted by this implementation. pub const MAX_DOIP_PAYLOAD_LEN: usize = 65_535; -// Routing Activation (ISO 13400-2 Β§9.9) +// Routing Activation (ISO 13400-2 Β§9.9) /// Minimum length of a routing activation request payload (bytes). pub const ROUTING_ACTIVATION_REQUEST_MIN_LEN: usize = 11; -// Diagnostic Message (ISO 13400-2 Β§9.11) +// Diagnostic Message (ISO 13400-2 Β§9.11) /// Minimum diagnostic message payload length: 2 (source addr) + 2 (target addr). pub const DIAG_MSG_MIN_PAYLOAD_LEN: usize = 4; @@ -90,7 +73,7 @@ pub const DIAG_MSG_MIN_PAYLOAD_LEN: usize = 4; /// Diagnostic message positive ACK header length: 2 (source) + 2 (target) + 1 (ACK code). pub const DIAG_ACK_HEADER_LEN: usize = 5; -// UDS response codes (ISO 14229-1) +// UDS response codes (ISO 14229-1) /// UDS negative response service ID. pub const UDS_NEGATIVE_RESPONSE: u8 = 0x7F; diff --git a/src/doip/dispatch.rs b/src/doip/dispatch.rs index 32ef38e..d853384 100644 --- a/src/doip/dispatch.rs +++ b/src/doip/dispatch.rs @@ -81,6 +81,23 @@ where } /// Dispatcher bound to the TCP transport payload types. +/// +/// # Transport segregation +/// +/// The generic type parameters prevent registering a handler for the wrong +/// transport at compile time. For example, a TCP handler cannot be registered +/// on a UDP dispatcher: +/// +/// ```compile_fail +/// use uds2sovd::doip::dispatch::UdpDispatcher; +/// use uds2sovd::doip::handlers::AliveCheckHandler; +/// use uds2sovd::doip::types::LogicalAddress; +/// +/// let mut dispatcher = UdpDispatcher::new(); +/// // AliveCheckHandler implements PayloadHandler, +/// // so this will not compile on a UdpDispatcher. +/// dispatcher.register(AliveCheckHandler::new(LogicalAddress::new(0x0001))); +/// ``` pub type TcpDispatcher = Dispatcher; /// Dispatcher bound to the UDP transport payload types. diff --git a/src/doip/error.rs b/src/doip/error.rs index f58366a..7404b33 100644 --- a/src/doip/error.rs +++ b/src/doip/error.rs @@ -35,6 +35,9 @@ pub enum Error { #[error("SOVD proxy error: {0}")] Proxy(#[from] SovdProxyError), - #[error("no matching entity for request")] - NoMatch, + #[error("no matching EID for request")] + EIDNotMatched, + + #[error("no matching VIN for request")] + VinNotMatched, } diff --git a/src/doip/handlers/alive_check.rs b/src/doip/handlers/alive_check.rs index 418995e..1e31d43 100644 --- a/src/doip/handlers/alive_check.rs +++ b/src/doip/handlers/alive_check.rs @@ -41,11 +41,11 @@ impl PayloadHandler for AliveCheckHandler { fn payload_type(&self) -> TcpPayloadType { TcpPayloadType::AliveCheckRequest } - fn handle(&self, req: TcpRequest) -> Result { - if !req.payload().is_empty() { + fn handle(&self, tcp_request: TcpRequest) -> Result { + if !tcp_request.payload().is_empty() { return Err(Error::InvalidPayloadLength { - declared: req.payload().len() as u32, - actual: req.payload().len(), + declared: tcp_request.payload().len() as u32, + actual: tcp_request.payload().len(), }); } Ok(self.respond()) diff --git a/src/doip/handlers/diagnostics.rs b/src/doip/handlers/diagnostics.rs index 44168c6..7c1ef12 100644 --- a/src/doip/handlers/diagnostics.rs +++ b/src/doip/handlers/diagnostics.rs @@ -34,13 +34,13 @@ impl DiagnosticsHandler { /// Protocol logic (ISO 13400-2 #9.11): forward UDS bytes to the SOVD proxy, /// wrap the response in a DiagnosticMessagePositiveAck. fn forward(&self, src: u16, tgt: u16, uds: &[u8]) -> Result { - let ecu_response = self.proxy.forward(uds)?; + let ecu_response = self.proxy.process(uds)?; // Payload layout: - // [0..2] source address (server β†’ originally tgt) - // [2..4] target address (client β†’ originally src) - // [4] ack code: 0x00 = ACK - // [5..] UDS response data from ECU + // [0..2] source address (server β†’ originally tgt) + // [2..4] target address (client β†’ originally src) + // [4] ack code: 0x00 = ACK + // [5..] UDS response data from ECU let mut payload = Vec::with_capacity(DIAG_ACK_HEADER_LEN + ecu_response.len()); payload.extend_from_slice(&tgt.to_be_bytes()); // server address payload.extend_from_slice(&src.to_be_bytes()); // client address @@ -58,17 +58,17 @@ impl PayloadHandler for DiagnosticsHandler { TcpPayloadType::DiagnosticMessage } - fn handle(&self, req: TcpRequest) -> Result { + fn handle(&self, tcp_request: TcpRequest) -> Result { // Payload layout: source_addr(2) + target_addr(2) + uds_data(N) - if req.payload().len() < DIAG_MSG_MIN_PAYLOAD_LEN { + if tcp_request.payload().len() < DIAG_MSG_MIN_PAYLOAD_LEN { return Err(Error::PayloadTooShort { expected: DIAG_MSG_MIN_PAYLOAD_LEN, - actual: req.payload().len(), + actual: tcp_request.payload().len(), }); } - let src = u16::from_be_bytes([req.payload()[0], req.payload()[1]]); - let tgt = u16::from_be_bytes([req.payload()[2], req.payload()[3]]); - self.forward(src, tgt, &req.payload()[4..]) + let source_address = u16::from_be_bytes([tcp_request.payload()[0], tcp_request.payload()[1]]); + let target_address = u16::from_be_bytes([tcp_request.payload()[2], tcp_request.payload()[3]]); + self.forward(source_address, target_address, &tcp_request.payload()[4..]) } } diff --git a/src/doip/handlers/entity_status.rs b/src/doip/handlers/entity_status.rs index 9837a7f..6967068 100644 --- a/src/doip/handlers/entity_status.rs +++ b/src/doip/handlers/entity_status.rs @@ -17,7 +17,7 @@ use crate::doip::{ message::{Response, UdpPayloadType, UdpRequest}, }; -// DoipEntityStatusRequest (0x4001) +// DoipEntityStatusRequest (0x4001) /// Handles DoipEntityStatusRequest (ISO 13400-2 Β§7.6.3). /// Reports node type, max TCP sessions, current sessions, and max data size. @@ -46,11 +46,11 @@ impl PayloadHandler for EntityStatusHandler { /// [1] max concurrent TCP sockets /// [2] currently open TCP sockets (0 β€” not tracked at this level) /// [3..7] max data size (u32 big-endian) - fn handle(&self, req: UdpRequest) -> Result { - if !req.payload().is_empty() { + fn handle(&self, udp_request: UdpRequest) -> Result { + if !udp_request.payload().is_empty() { return Err(Error::InvalidPayloadLength { - declared: req.payload().len() as u32, - actual: req.payload().len(), + declared: udp_request.payload().len() as u32, + actual: udp_request.payload().len(), }); } let mut payload = Vec::with_capacity(ENTITY_STATUS_RESPONSE_LEN); diff --git a/src/doip/handlers/routing_activation.rs b/src/doip/handlers/routing_activation.rs index 3a21cf6..2fd7a9d 100644 --- a/src/doip/handlers/routing_activation.rs +++ b/src/doip/handlers/routing_activation.rs @@ -23,11 +23,11 @@ impl RoutingActivationHandler { /// Returns a RoutingActivationResponse payload. fn activate(&self, client_address: u16, _activation_type: u8) -> Response { // Payload layout (13 bytes): - // [0..2] client logical address - // [2..4] server logical address - // [4] response code: 0x10 = success - // [5..9] reserved ISO (0x00000000) - // [9..13] reserved OEM (0x00000000) + // [0..2] client logical address + // [2..4] server logical address + // [4] response code: 0x10 = success + // [5..9] reserved ISO (0x00000000) + // [9..13] reserved OEM (0x00000000) let mut payload = Vec::with_capacity(13); payload.extend_from_slice(&client_address.to_be_bytes()); payload.extend_from_slice(&self.server_logical_address.to_be_bytes()); @@ -43,16 +43,16 @@ impl PayloadHandler for RoutingActivationHandler { TcpPayloadType::RoutingActivationRequest } - fn handle(&self, req: TcpRequest) -> Result { + fn handle(&self, tcp_request: TcpRequest) -> Result { // Payload layout (11 bytes): source_addr(2) + activation_type(1) + reserved(8) - if req.payload().len() < ROUTING_ACTIVATION_REQUEST_MIN_LEN { + if tcp_request.payload().len() < ROUTING_ACTIVATION_REQUEST_MIN_LEN { return Err(Error::PayloadTooShort { expected: ROUTING_ACTIVATION_REQUEST_MIN_LEN, - actual: req.payload().len(), + actual: tcp_request.payload().len(), }); } - let client_address = u16::from_be_bytes([req.payload()[0], req.payload()[1]]); - let activation_type = req.payload()[2]; + let client_address = u16::from_be_bytes([tcp_request.payload()[0], tcp_request.payload()[1]]); + let activation_type = tcp_request.payload()[2]; Ok(self.activate(client_address, activation_type)) } } @@ -77,7 +77,8 @@ mod tests { TcpPayloadType::RoutingActivationResponse as u16 ); assert_eq!( - resp.payload()[4], 0x10, + resp.payload()[4], + 0x10, "response code must be 0x10 (success)" ); // client address echoed back diff --git a/src/doip/handlers/vehicle_identification/common.rs b/src/doip/handlers/vehicle_identification/common.rs index b330071..146ea29 100644 --- a/src/doip/handlers/vehicle_identification/common.rs +++ b/src/doip/handlers/vehicle_identification/common.rs @@ -12,14 +12,15 @@ //! Shared response builder for Vehicle Identification handlers (ISO 13400-2 Β§7.6.2). +use crate::config::EcuConfig; use crate::doip::{ constants::NO_FURTHER_ACTION, message::{Response, UdpPayloadType}, - types::{Eid, Gid, LogicalAddress, Vin}, + types::LogicalAddress, }; /// 17 (VIN) + 2 (addr) + 6 (EID) + 6 (GID) + 1 (action byte) = 32 -pub(super) const VI_RESPONSE_LEN: usize = 32; +const VI_RESPONSE_LEN: usize = 32; /// Builds the 32-byte Vehicle Identification Response / Announcement payload. /// @@ -32,22 +33,21 @@ pub(super) const VI_RESPONSE_LEN: usize = 32; /// [31] further action required (0x00 = none) /// ``` pub(super) fn create_vi_response( - vin: &Vin, - eid: &Eid, - gid: &Gid, + ecu_config: &EcuConfig, logical_address: LogicalAddress, ) -> Response { let mut payload = Vec::with_capacity(VI_RESPONSE_LEN); - payload.extend_from_slice(vin.as_bytes()); + payload.extend_from_slice(ecu_config.vin().as_bytes()); payload.extend_from_slice(&logical_address.to_be_bytes()); - payload.extend_from_slice(eid.as_bytes()); - payload.extend_from_slice(gid.as_bytes()); + payload.extend_from_slice(ecu_config.eid().as_bytes()); + payload.extend_from_slice(ecu_config.gid().as_bytes()); payload.push(NO_FURTHER_ACTION); Response::new(UdpPayloadType::VehicleAnnouncementResponse as u16, payload) } #[cfg(test)] pub(super) mod fixtures { + use crate::config::EcuConfig; use crate::doip::types::{Eid, Gid, LogicalAddress, Vin}; /// ISO example VIN (17 ASCII characters, valid format) @@ -68,6 +68,11 @@ pub(super) mod fixtures { /// VIN that does NOT match TEST_VIN (valid format, different vehicle) pub const NON_MATCHING_VIN: Vin = Vin::new(*b"WVWZZZ3CZWE123456"); + /// Test ECU config with the above VIN, EID, GID + pub fn test_ecu_config() -> EcuConfig { + EcuConfig::new(TEST_VIN, TEST_EID, TEST_GID) + } + /// Expected response payload length pub const VI_RESPONSE_LEN: usize = super::VI_RESPONSE_LEN; } diff --git a/src/doip/handlers/vehicle_identification/request.rs b/src/doip/handlers/vehicle_identification/request.rs index aa78fdc..02e64d8 100644 --- a/src/doip/handlers/vehicle_identification/request.rs +++ b/src/doip/handlers/vehicle_identification/request.rs @@ -13,27 +13,24 @@ //! Handler for VehicleIdentificationRequest (0x0001, ISO 13400-2 Β§7.6.1). use super::common::create_vi_response; +use crate::config::EcuConfig; use crate::doip::{ PayloadHandler, error::Error, message::{Response, UdpPayloadType, UdpRequest}, - types::{Eid, Gid, LogicalAddress, Vin}, + types::LogicalAddress, }; /// Handles 0x0001 β€” responds to any client unconditionally. pub struct IdentifyVehicleHandler { - vin: Vin, - eid: Eid, - gid: Gid, + ecu_config: EcuConfig, logical_address: LogicalAddress, } impl IdentifyVehicleHandler { - pub fn new(vin: Vin, eid: Eid, gid: Gid, logical_address: LogicalAddress) -> Self { + pub fn new(ecu_config: EcuConfig, logical_address: LogicalAddress) -> Self { Self { - vin, - eid, - gid, + ecu_config, logical_address, } } @@ -44,19 +41,14 @@ impl PayloadHandler for IdentifyVehicleHandler { UdpPayloadType::VehicleIdentificationRequest } - fn handle(&self, req: UdpRequest) -> Result { - if !req.payload().is_empty() { + fn handle(&self, udp_request: UdpRequest) -> Result { + if !udp_request.payload().is_empty() { return Err(Error::InvalidPayloadLength { - declared: req.payload().len() as u32, - actual: req.payload().len(), + declared: udp_request.payload().len() as u32, + actual: udp_request.payload().len(), }); } - Ok(create_vi_response( - &self.vin, - &self.eid, - &self.gid, - self.logical_address, - )) + Ok(create_vi_response(&self.ecu_config, self.logical_address)) } } @@ -66,7 +58,7 @@ mod tests { use super::*; fn handler() -> IdentifyVehicleHandler { - IdentifyVehicleHandler::new(TEST_VIN, TEST_EID, TEST_GID, TEST_ADDR) + IdentifyVehicleHandler::new(test_ecu_config(), TEST_ADDR) } #[test] diff --git a/src/doip/handlers/vehicle_identification/request_by_eid.rs b/src/doip/handlers/vehicle_identification/request_by_eid.rs index b060745..fef05de 100644 --- a/src/doip/handlers/vehicle_identification/request_by_eid.rs +++ b/src/doip/handlers/vehicle_identification/request_by_eid.rs @@ -13,28 +13,25 @@ //! Handler for VehicleIdentificationRequestWithEID (0x0002, ISO 13400-2 Β§7.6.1.1). use super::common::create_vi_response; +use crate::config::EcuConfig; use crate::doip::{ PayloadHandler, constants::EID_LEN, error::Error, message::{Response, UdpPayloadType, UdpRequest}, - types::{Eid, Gid, LogicalAddress, Vin}, + types::{Eid, LogicalAddress}, }; /// Handles 0x0002 β€” responds only if the requested EID matches. pub struct IdentifyVehicleByEidHandler { - vin: Vin, - eid: Eid, - gid: Gid, + ecu_config: EcuConfig, logical_address: LogicalAddress, } impl IdentifyVehicleByEidHandler { - pub fn new(vin: Vin, eid: Eid, gid: Gid, logical_address: LogicalAddress) -> Self { + pub fn new(ecu_config: EcuConfig, logical_address: LogicalAddress) -> Self { Self { - vin, - eid, - gid, + ecu_config, logical_address, } } @@ -45,30 +42,25 @@ impl PayloadHandler for IdentifyVehicleByEidHandler UdpPayloadType::VehicleIdentificationRequestWithEid } - fn handle(&self, req: UdpRequest) -> Result { - if req.payload().len() != EID_LEN { + fn handle(&self, udp_request: UdpRequest) -> Result { + if udp_request.payload().len() != EID_LEN { return Err(Error::PayloadTooShort { expected: EID_LEN, - actual: req.payload().len(), + actual: udp_request.payload().len(), }); } let requested = Eid::new([ - req.payload()[0], - req.payload()[1], - req.payload()[2], - req.payload()[3], - req.payload()[4], - req.payload()[5], + udp_request.payload()[0], + udp_request.payload()[1], + udp_request.payload()[2], + udp_request.payload()[3], + udp_request.payload()[4], + udp_request.payload()[5], ]); - if requested != self.eid { - return Err(Error::NoMatch); + if requested != self.ecu_config.eid() { + return Err(Error::EIDNotMatched); } - Ok(create_vi_response( - &self.vin, - &self.eid, - &self.gid, - self.logical_address, - )) + Ok(create_vi_response(&self.ecu_config, self.logical_address)) } } @@ -78,7 +70,7 @@ mod tests { use super::*; fn handler() -> IdentifyVehicleByEidHandler { - IdentifyVehicleByEidHandler::new(TEST_VIN, TEST_EID, TEST_GID, TEST_ADDR) + IdentifyVehicleByEidHandler::new(test_ecu_config(), TEST_ADDR) } #[test] @@ -96,7 +88,7 @@ mod tests { UdpPayloadType::VehicleIdentificationRequestWithEid, NON_MATCHING_EID.as_bytes().to_vec(), ); - assert!(matches!(handler().handle(req), Err(Error::NoMatch))); + assert!(matches!(handler().handle(req), Err(Error::EIDNotMatched))); } #[test] diff --git a/src/doip/handlers/vehicle_identification/request_by_vin.rs b/src/doip/handlers/vehicle_identification/request_by_vin.rs index 52a3e6c..3e9b473 100644 --- a/src/doip/handlers/vehicle_identification/request_by_vin.rs +++ b/src/doip/handlers/vehicle_identification/request_by_vin.rs @@ -13,28 +13,25 @@ //! Handler for VehicleIdentificationRequestWithVIN (0x0003, ISO 13400-2 Β§7.6.1.2). use super::common::create_vi_response; +use crate::config::EcuConfig; use crate::doip::{ PayloadHandler, constants::VIN_LEN, error::Error, message::{Response, UdpPayloadType, UdpRequest}, - types::{Eid, Gid, LogicalAddress, Vin}, + types::{LogicalAddress, Vin}, }; /// Handles 0x0003 β€” responds only if the requested VIN matches. pub struct IdentifyVehicleByVinHandler { - vin: Vin, - eid: Eid, - gid: Gid, + ecu_config: EcuConfig, logical_address: LogicalAddress, } impl IdentifyVehicleByVinHandler { - pub fn new(vin: Vin, eid: Eid, gid: Gid, logical_address: LogicalAddress) -> Self { + pub fn new(ecu_config: EcuConfig, logical_address: LogicalAddress) -> Self { Self { - vin, - eid, - gid, + ecu_config, logical_address, } } @@ -45,24 +42,19 @@ impl PayloadHandler for IdentifyVehicleByVinHandler UdpPayloadType::VehicleIdentificationRequestWithVin } - fn handle(&self, req: UdpRequest) -> Result { - if req.payload().len() != VIN_LEN { + fn handle(&self, udp_request: UdpRequest) -> Result { + if udp_request.payload().len() != VIN_LEN { return Err(Error::PayloadTooShort { expected: VIN_LEN, - actual: req.payload().len(), + actual: udp_request.payload().len(), }); } let mut bytes = [0u8; 17]; - bytes.copy_from_slice(req.payload()); - if Vin::new(bytes) != self.vin { - return Err(Error::NoMatch); + bytes.copy_from_slice(udp_request.payload()); + if Vin::new(bytes) != self.ecu_config.vin() { + return Err(Error::VinNotMatched); } - Ok(create_vi_response( - &self.vin, - &self.eid, - &self.gid, - self.logical_address, - )) + Ok(create_vi_response(&self.ecu_config, self.logical_address)) } } @@ -72,7 +64,7 @@ mod tests { use super::*; fn handler() -> IdentifyVehicleByVinHandler { - IdentifyVehicleByVinHandler::new(TEST_VIN, TEST_EID, TEST_GID, TEST_ADDR) + IdentifyVehicleByVinHandler::new(test_ecu_config(), TEST_ADDR) } #[test] @@ -90,7 +82,7 @@ mod tests { UdpPayloadType::VehicleIdentificationRequestWithVin, NON_MATCHING_VIN.as_bytes().to_vec(), ); - assert!(matches!(handler().handle(req), Err(Error::NoMatch))); + assert!(matches!(handler().handle(req), Err(Error::VinNotMatched))); } #[test] diff --git a/src/doip/message.rs b/src/doip/message.rs index 99765da..ae3c5f8 100644 --- a/src/doip/message.rs +++ b/src/doip/message.rs @@ -1,20 +1,53 @@ -use crate::doip::constants::{ - INVERSE_VERSION, NACK_INCORRECT_PATTERN, NACK_INVALID_PAYLOAD_LENGTH, NACK_MESSAGE_TOO_LARGE, - NACK_UNKNOWN_PAYLOAD_TYPE, PROTOCOL_VERSION, -}; +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ + +use crate::doip::constants::{INVERSE_VERSION, PROTOCOL_VERSION}; use crate::doip::error::Error; +/// Generic DoIP header NACK codes (ISO 13400-2 Β§9.4, Table 18). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum DoipNackCode { + /// Header fields do not match the expected pattern (bad version or inverse byte). + IncorrectPattern = 0x00, + /// Payload type is not supported by this entity. + UnknownPayloadType = 0x01, + /// Message is too large to be processed. + MessageTooLarge = 0x02, + /// Server ran out of memory. + OutOfMemory = 0x03, + /// Payload length field does not match actual payload size. + InvalidPayloadLength = 0x04, +} + +impl From for u8 { + fn from(code: DoipNackCode) -> Self { + code as u8 + } +} + /// Maps a DoIP error to the appropriate generic header NACK code (ISO 13400-2 Table 4). -pub fn nack_code(err: &Error) -> u8 { +pub fn nack_code(err: &Error) -> DoipNackCode { match err { - Error::InvalidHeaderVersion(_) | Error::InvalidInverseVersion(_) => NACK_INCORRECT_PATTERN, - Error::UnknownPayloadType(_) => NACK_UNKNOWN_PAYLOAD_TYPE, - Error::PayloadTooLarge(_) => NACK_MESSAGE_TOO_LARGE, + Error::InvalidHeaderVersion(_) | Error::InvalidInverseVersion(_) => { + DoipNackCode::IncorrectPattern + } + Error::UnknownPayloadType(_) => DoipNackCode::UnknownPayloadType, + Error::PayloadTooLarge(_) => DoipNackCode::MessageTooLarge, Error::InvalidPayloadLength { .. } | Error::PayloadTooShort { .. } => { - NACK_INVALID_PAYLOAD_LENGTH + DoipNackCode::InvalidPayloadLength } - Error::Proxy(_) => NACK_INCORRECT_PATTERN, - Error::NoMatch => NACK_INCORRECT_PATTERN, + Error::Proxy(_) => DoipNackCode::IncorrectPattern, + Error::EIDNotMatched | Error::VinNotMatched => DoipNackCode::IncorrectPattern, } } @@ -180,8 +213,8 @@ impl Response { /// NACK codes: 0x00=incorrect pattern, 0x01=unknown payload type, /// 0x02=message too large, 0x03=out of memory, 0x04=invalid payload length. /// - pub fn doip_header_nack(code: u8) -> Self { - Self::new(0x0000, vec![code]) + pub fn doip_header_nack(code: DoipNackCode) -> Self { + Self::new(0x0000, vec![u8::from(code)]) } /// The numeric payload type for this response. @@ -246,7 +279,7 @@ mod tests { #[test] fn nack_response_has_correct_payload_type_and_code() { - let resp = Response::doip_header_nack(0x02); + let resp = Response::doip_header_nack(DoipNackCode::MessageTooLarge); assert_eq!(resp.payload_type(), 0x0000); assert_eq!(resp.payload(), &[0x02]); } diff --git a/src/doip/mod.rs b/src/doip/mod.rs index 873dd0d..82ba740 100644 --- a/src/doip/mod.rs +++ b/src/doip/mod.rs @@ -1,13 +1,14 @@ -// Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) -// -// See the NOTICE file(s) distributed with this work for additional -// information regarding copyright ownership. -// -// This program and the accompanying materials are made available under the -// terms of the Apache License Version 2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: Apache-2.0 +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + */ pub mod constants; pub mod dispatch; @@ -21,8 +22,16 @@ pub use types::{Eid, Gid, LogicalAddress, Vin}; use std::sync::Arc; +// TODO: If the vehicle-identification helper scope grows beyond the current +// small set of factory functions, consider grouping them under a zero-sized +// type for better organization and discoverability. + /// Build the TCP dispatcher with all TCP-legal handlers registered. -/// `proxy` is called for every DiagnosticMessage (0x8001). +/// +/// # Parameters +/// logical_addr: This entity's DoIP logical address, used in routing activation +/// and alive check responses. +/// proxy: SOVD backend proxy invoked for every DiagnosticMessage (0x8001). pub fn tcp_dispatcher( logical_addr: LogicalAddress, proxy: Arc, @@ -36,25 +45,23 @@ pub fn tcp_dispatcher( } /// Build the UDP dispatcher with all UDP-legal handlers registered. -pub fn udp_dispatcher(logical_addr: LogicalAddress, vin: Vin, eid: Eid, gid: Gid) -> UdpDispatcher { +/// +/// # Parameters +/// logical_addr: This entity's DoIP logical address included in identification responses. +/// ecu: ECU identity settings (VIN, EID, GID) used in vehicle identification responses. +pub fn udp_dispatcher( + logical_addr: LogicalAddress, + ecu: &crate::config::EcuConfig, +) -> UdpDispatcher { use handlers::{ EntityStatusHandler, IdentifyVehicleByEidHandler, IdentifyVehicleByVinHandler, IdentifyVehicleHandler, }; + let ecu = ecu.clone(); let mut dispatcher = UdpDispatcher::new(); - dispatcher.register(IdentifyVehicleHandler::new(vin, eid, gid, logical_addr)); - dispatcher.register(IdentifyVehicleByEidHandler::new( - vin, - eid, - gid, - logical_addr, - )); - dispatcher.register(IdentifyVehicleByVinHandler::new( - vin, - eid, - gid, - logical_addr, - )); + dispatcher.register(IdentifyVehicleHandler::new(ecu.clone(), logical_addr)); + dispatcher.register(IdentifyVehicleByEidHandler::new(ecu.clone(), logical_addr)); + dispatcher.register(IdentifyVehicleByVinHandler::new(ecu, logical_addr)); dispatcher.register(EntityStatusHandler::new(10, 65_535)); dispatcher } diff --git a/src/doip/types.rs b/src/doip/types.rs index 7099999..b95dfd9 100644 --- a/src/doip/types.rs +++ b/src/doip/types.rs @@ -34,8 +34,9 @@ impl From for LogicalAddress { } } -/// Vehicle Identification Number (ISO 3779): 17 ASCII characters. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +/// Vehicle Identification Number (ISO 3779): 17 ASCII bytes. +/// In TOML, specify as an array of byte values (e.g., `vin = [48, 48, ...]`). +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] pub struct Vin([u8; 17]); impl Vin { @@ -51,7 +52,7 @@ impl Vin { /// Entity Identifier: 6 bytes, typically the MAC address of the DoIP node's /// network interface (ISO 13400-2 #7.6.2). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] pub struct Eid([u8; 6]); impl Eid { diff --git a/src/lib.rs b/src/lib.rs index 4d5a582..8fcc44b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,10 +10,30 @@ * https://www.apache.org/licenses/LICENSE-2.0 */ -//! DoIP server library (ISO 13400-2) β€” proxies UDS diagnostics to a SOVD backend. +//! # uds2sovd β€” DoIP Server Library //! -//! This crate provides the protocol layer, transport layer, configuration, and -//! proxy interface. The binary entry point lives in `app/main.rs`. +//! A DoIP (Diagnostics over Internet Protocol) server library that accepts +//! UDS diagnostic requests from DoIP clients and forwards them to an SOVD backend. +//! +//! ## What this library offers +//! +//! config: Load server settings (bind addresses, ECU identity) from TOML or in-memory. +//! doip: DoIP protocol: message parsing, handlers for vehicle identification, +//! routing activation, alive check, entity status, and diagnostic messages. +//! proxy: Forward UDS bytes to an SOVD backend. Implement the SovdProxy trait +//! for your backend; StubProxy and MockProxy are provided for development and testing. +//! server: TCP/UDP transport layer with concurrent listeners and graceful shutdown. +//! error: Unified error type for protocol and I/O errors. +//! +//! ## How to use +//! +//! 1. Implement the SovdProxy trait for your SOVD backend. +//! 2. Create a config (TOML file or in-memory). +//! 3. Build the server and run it. +//! +//! See `app/main.rs` for a working example and `sample-doip-server.toml` for +//! a reference configuration. + pub mod config; pub mod doip; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index bf8ee3c..0000000 --- a/src/main.rs +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) -// -// See the NOTICE file(s) distributed with this work for additional -// information regarding copyright ownership. -// -// This program and the accompanying materials are made available under the -// terms of the Apache License Version 2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 - -fn main() {} diff --git a/src/proxy/mock.rs b/src/proxy/mock.rs index f511092..10e819b 100644 --- a/src/proxy/mock.rs +++ b/src/proxy/mock.rs @@ -17,7 +17,7 @@ use super::{SovdProxy, SovdProxyError}; pub struct MockProxy; impl SovdProxy for MockProxy { - fn forward(&self, uds_request: &[u8]) -> Result, SovdProxyError> { + fn process(&self, uds_request: &[u8]) -> Result, SovdProxyError> { Ok(uds_request.to_vec()) } } diff --git a/src/proxy/mod.rs b/src/proxy/mod.rs index 1a77f43..e55881d 100644 --- a/src/proxy/mod.rs +++ b/src/proxy/mod.rs @@ -37,5 +37,5 @@ pub use error::SovdProxyError; /// The real implementation (provided separately) will forward requests to a /// SOVD server over the vehicle network. pub trait SovdProxy: Send + Sync { - fn forward(&self, uds_request: &[u8]) -> Result, SovdProxyError>; + fn process(&self, uds_request: &[u8]) -> Result, SovdProxyError>; } diff --git a/src/proxy/stub.rs b/src/proxy/stub.rs index 69a8483..ffb67a7 100644 --- a/src/proxy/stub.rs +++ b/src/proxy/stub.rs @@ -20,7 +20,7 @@ use super::{SovdProxy, SovdProxyError}; pub struct StubProxy; impl SovdProxy for StubProxy { - fn forward(&self, uds_request: &[u8]) -> Result, SovdProxyError> { + fn process(&self, uds_request: &[u8]) -> Result, SovdProxyError> { if uds_request.is_empty() { return Err(SovdProxyError::InvalidResponse); } @@ -36,13 +36,13 @@ mod tests { #[test] fn stub_returns_nrc_service_not_supported() { let proxy = StubProxy; - let resp = proxy.forward(&[0x22, 0xF1, 0x90]).unwrap(); + let resp = proxy.process(&[0x22, 0xF1, 0x90]).unwrap(); assert_eq!(resp, vec![0x7F, 0x22, 0x11]); } #[test] fn stub_errors_on_empty_request() { let proxy = StubProxy; - assert!(proxy.forward(&[]).is_err()); + assert!(proxy.process(&[]).is_err()); } } diff --git a/src/server/tcp/mod.rs b/src/server/tcp/mod.rs index 68f1c64..686ec1f 100644 --- a/src/server/tcp/mod.rs +++ b/src/server/tcp/mod.rs @@ -12,8 +12,8 @@ //! TCP transport β€” accept loop, session management, and byte-stream framing. -pub mod framer; -pub mod session; +mod framer; +mod session; use std::io; use std::sync::Arc; @@ -24,7 +24,7 @@ use tokio::net::TcpListener; use super::Transport; use crate::config::TcpConfig; use crate::doip::TcpDispatcher; -use crate::doip::message::Response; +use crate::doip::message::{DoipNackCode, Response}; use session::{Session, SessionManager}; /// TCP transport: binds a listener and spawns one session per accepted connection. @@ -65,8 +65,7 @@ impl Transport for Tcp { } None => { tracing::warn!(peer = %peer_addr, "connection rejected: max sessions reached"); - let nack = - Response::doip_header_nack(crate::doip::constants::NACK_OUT_OF_MEMORY); + let nack = Response::doip_header_nack(DoipNackCode::OutOfMemory); let _ = AsyncWriteExt::write_all(&mut stream, &nack.to_bytes()).await; drop(stream); } diff --git a/src/server/tcp/session/manager.rs b/src/server/tcp/session/manager.rs index 5b4f37c..fa4fa79 100644 --- a/src/server/tcp/session/manager.rs +++ b/src/server/tcp/session/manager.rs @@ -21,13 +21,13 @@ use crate::doip::message::ConnectionId; /// Uses an atomic counter shared with `ConnectionSlot` β€” when a slot is dropped /// (session ends for any reason), the counter decrements automatically. No polling, /// no background task, no explicit remove() call needed. -pub struct SessionManager { +pub(in crate::server::tcp) struct SessionManager { max: usize, active: Arc, } impl SessionManager { - pub fn new(max: usize) -> Self { + pub(in crate::server::tcp) fn new(max: usize) -> Self { Self { max, active: Arc::new(AtomicUsize::new(0)), @@ -39,7 +39,7 @@ impl SessionManager { /// Returns `Some(ConnectionSlot)` if capacity is available, `None` if the /// maximum is already reached. The returned slot auto-decrements the counter /// when dropped. - pub fn try_acquire(&self) -> Option { + pub(in crate::server::tcp) fn try_acquire(&self) -> Option { self.active .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| { if current < self.max { @@ -60,7 +60,7 @@ impl SessionManager { } /// Number of sessions currently active. - pub fn active_count(&self) -> usize { + pub(in crate::server::tcp) fn active_count(&self) -> usize { self.active.load(Ordering::SeqCst) } } diff --git a/src/server/tcp/session/mod.rs b/src/server/tcp/session/mod.rs index 6cd477f..c0a6f48 100644 --- a/src/server/tcp/session/mod.rs +++ b/src/server/tcp/session/mod.rs @@ -13,11 +13,11 @@ //! Per-connection session: owns a [`ConnectionSlot`], drives the I/O loop, //! and dispatches parsed frames to handlers. -pub mod manager; -pub mod slot; +pub(super) mod manager; +pub(super) mod slot; -pub use manager::SessionManager; -pub use slot::ConnectionSlot; +pub(super) use manager::SessionManager; +pub(super) use slot::ConnectionSlot; use std::sync::Arc; @@ -32,13 +32,13 @@ use crate::server::tcp::framer::Framer; /// /// Owns the `ConnectionSlot` (RAII counter decrement on drop). When `run()` completes /// the slot is dropped, automatically decrementing the active session counter. -pub struct Session { +pub(super) struct Session { slot: ConnectionSlot, } impl Session { /// Create a session that owns the given connection slot. - pub fn new(slot: ConnectionSlot) -> Self { + pub(super) fn new(slot: ConnectionSlot) -> Self { Self { slot } } @@ -97,6 +97,7 @@ impl Session { } Err(err) => { tracing::error!(id = %id, error = %err, "read error"); + // TODO: propagate error to caller instead of silently disconnecting break; } } diff --git a/src/server/tcp/session/slot.rs b/src/server/tcp/session/slot.rs index 6d57cb7..8ef4559 100644 --- a/src/server/tcp/session/slot.rs +++ b/src/server/tcp/session/slot.rs @@ -20,7 +20,7 @@ use crate::doip::message::ConnectionId; /// Holds the connection's unique ID and a shared reference to the session /// counter. When dropped (session thread exits, error, or clean close), the /// counter is automatically decremented β€” no explicit cleanup required. -pub struct ConnectionSlot { +pub(in crate::server::tcp) struct ConnectionSlot { id: ConnectionId, counter: Arc, } @@ -31,7 +31,7 @@ impl ConnectionSlot { } /// The unique ID assigned to this connection. - pub fn id(&self) -> &ConnectionId { + pub(in crate::server::tcp) fn id(&self) -> &ConnectionId { &self.id } } diff --git a/src/server/udp/handler.rs b/src/server/udp/handler.rs index 1644132..2c3ee63 100644 --- a/src/server/udp/handler.rs +++ b/src/server/udp/handler.rs @@ -72,6 +72,7 @@ impl Handler { #[cfg(test)] mod tests { use super::*; + use crate::config::EcuConfig; use crate::doip::UdpDispatcher; use crate::doip::handlers::vehicle_identification::IdentifyVehicleHandler; use crate::doip::message::UdpPayloadType; @@ -146,9 +147,11 @@ mod tests { fn handle_valid_vin_request_returns_announcement() { let mut dispatcher = UdpDispatcher::new(); dispatcher.register(IdentifyVehicleHandler::new( - Vin::new(*b"00000000000000000"), - Eid::new([0u8; 6]), - Gid::new([0u8; 6]), + EcuConfig::new( + Vin::new(*b"00000000000000000"), + Eid::new([0u8; 6]), + Gid::new([0u8; 6]), + ), LogicalAddress::new(0x0001), )); diff --git a/src/server/udp/mod.rs b/src/server/udp/mod.rs index e7d3956..430e267 100644 --- a/src/server/udp/mod.rs +++ b/src/server/udp/mod.rs @@ -57,7 +57,7 @@ impl Transport for Udp { tracing::error!(error = %err, peer = %src_addr, "UDP send error"); } } - Err(Error::NoMatch) => { + Err(Error::EIDNotMatched) | Err(Error::VinNotMatched) => { tracing::debug!(peer = %src_addr, "no matching entity, not responding"); } Err(err) => { From 7e4a10473b96b36b457ff8fca9cfebd4e7df76fc Mon Sep 17 00:00:00 2001 From: VinaykumarRS1995 Date: Wed, 3 Jun 2026 18:06:20 +0530 Subject: [PATCH 15/21] refactor: Rename , Updated comments - Rename common.rs to utils.rs in vehicle identification handlers - Rename InMemoryConfigProvider to DefaultConfigProvider - Fix Toml_provider casing to toml_provider - Move sample-doip-server.toml into app/ - Add ISO references, design decisions, and validation TODOs across codebase Signed-off-by: VinaykumarRS1995 --- README.md | 57 +++++- app/main.rs | 28 +-- .../sample-doip-server.toml | 2 +- client/doip_tester.rs | 20 +-- docs/01-startup.puml | 21 ++- docs/02-tcp-connection.puml | 22 +-- docs/03-udp-request.puml | 11 ++ src/Cargo.toml | 2 +- src/config/defaults.rs | 20 +-- src/config/error.rs | 28 +++ src/config/mod.rs | 27 +-- src/config/provider/default_config.rs | 62 +++++++ src/config/provider/in_memory.rs | 46 ----- src/config/provider/mod.rs | 29 ++- src/config/provider/toml.rs | 32 ++-- src/config/types.rs | 43 +++-- src/doip/constants.rs | 28 +-- src/doip/dispatch.rs | 36 ++-- src/doip/error.rs | 73 ++++++-- src/doip/handlers/alive_check.rs | 38 ++-- src/doip/handlers/diagnostics.rs | 33 ++-- src/doip/handlers/entity_status.rs | 50 +++--- src/doip/handlers/mod.rs | 20 +-- src/doip/handlers/routing_activation.rs | 13 +- .../handlers/vehicle_identification/mod.rs | 28 +-- .../vehicle_identification/request.rs | 38 ++-- .../vehicle_identification/request_by_eid.rs | 48 +++-- .../vehicle_identification/request_by_vin.rs | 45 +++-- .../{common.rs => utils.rs} | 25 +-- src/doip/header.rs | 114 ++++++++++++ src/doip/message.rs | 50 +++--- src/doip/mod.rs | 30 ++-- src/doip/types.rs | 20 +-- src/error.rs | 24 +-- src/lib.rs | 25 ++- src/proxy/error.rs | 20 +-- src/proxy/mock.rs | 20 +-- src/proxy/mod.rs | 31 ++-- src/proxy/stub.rs | 20 +-- src/server/mod.rs | 25 +++ src/server/tcp/framer.rs | 105 +++++++---- src/server/tcp/mod.rs | 29 +-- src/server/tcp/session/manager.rs | 61 ++++--- src/server/tcp/session/mod.rs | 166 +++++++++++++----- src/server/tcp/session/slot.rs | 20 +-- src/server/udp/handler.rs | 45 +++-- src/server/udp/mod.rs | 26 ++- 47 files changed, 1122 insertions(+), 634 deletions(-) rename sample-doip-server.toml => app/sample-doip-server.toml (95%) create mode 100644 src/config/error.rs create mode 100644 src/config/provider/default_config.rs delete mode 100644 src/config/provider/in_memory.rs rename src/doip/handlers/vehicle_identification/{common.rs => utils.rs} (79%) create mode 100644 src/doip/header.rs diff --git a/README.md b/README.md index 7a791ba..044b0d0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ - -# πŸ”Œ UDS-to-SOVD Proxy +# UDS-to-SOVD Proxy This repository contains the UDS-to-SOVD Proxy of the [Eclipse OpenSOVD](https://github.com/eclipse-opensovd) project. @@ -46,6 +45,19 @@ The proxy consists of a **DoIP Server** (handles the DoIP wire protocol over TCP **Discovery** happens over UDP β€” testers broadcast vehicle identification requests and the server responds with its VIN (Vehicle Identification Number), EID (Entity Identifier), and logical address. **Diagnostics** happen over TCP β€” after a routing activation handshake, the tester sends UDS requests which the server forwards to the UDS2SOVD layer. +**Current state:** The SOVD proxy is a stub (returns NRC 0x11 β€” serviceNotSupported). +The DoIP protocol layer is fully functional for the supported message types below. + +## What It Does + +- Accepts **TCP connections** on port 13400 for diagnostic sessions +- Accepts **UDP datagrams** on port 13400 for vehicle discovery +- Parses and validates DoIP headers (ISO 13400-2 Β§7.3) +- Routes messages to type-safe handlers via a generic dispatcher +- Forwards UDS bytes to the `SovdProxy` trait implementation +- Manages concurrent TCP sessions with RAII-based slot tracking +- Supports TOML-based configuration or sensible defaults + ### Supported Messages | Payload Type | Name | Transport | Behavior | @@ -58,6 +70,33 @@ The proxy consists of a **DoIP Server** (handles the DoIP wire protocol over TCP | 0x0007 | AliveCheckRequest | TCP | Confirms connection is live | | 0x8001 | DiagnosticMessage | TCP | Forwards UDS payload, returns ECU response | +## Limitations + +> **Important:** This is an early-stage implementation. The following are known gaps: + +| Limitation | Impact | +|---|---| +| SOVD proxy is a stub | Returns NRC 0x11 for all UDS requests | +| No session lifecycle state machine | Diagnostics accepted without routing activation | +| No NRC 0x78 response-pending | Slow backends will cause tester timeouts | +| No TLS / DoIP security | ISO 13400-3 not implemented | +| No vehicle announcement broadcasting | Server responds to queries only | +| Single logical address | No multi-ECU routing | +| No config validation | Invalid values accepted silently | + +## Future Work + +- Real SOVD backend integration (async HTTP proxy) +- DoIP session lifecycle state machine (ISO 13400-2 Β§9.3) +- UDS NRC 0x78 response-pending for slow backends +- Configuration validation at startup +- TLS support (ISO 13400-3) +- Structured logging with session correlation +- Multi-ECU routing support +- Vehicle announcement broadcasting (periodic + on-connect) +- CLI argument parsing (e.g., clap) +- Integration test harness with simulated DoIP client + ### Usage 1. Run with defaults (TCP `127.0.0.1:13400`, UDP `0.0.0.0:13400`): @@ -138,6 +177,18 @@ cargo run -p doip-server cargo run -p doip-client ``` +Limitations +Important: This is an early-stage implementation. The following are known gaps: + +Limitation Impact +SOVD proxy is a stub Returns NRC 0x11 for all UDS requests +No session lifecycle state machine Diagnostics accepted without routing activation +No NRC 0x78 response-pending Slow backends will cause tester timeouts +No TLS / DoIP security ISO 13400-3 not implemented +No vehicle announcement broadcasting Server responds to queries only +Single logical address No multi-ECU routing +No config validation Invalid values accepted silently + ## License Apache-2.0 β€” see [LICENSE](LICENSE). diff --git a/app/main.rs b/app/main.rs index 9e5bdfe..55eaa8c 100644 --- a/app/main.rs +++ b/app/main.rs @@ -1,19 +1,17 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - */ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 use std::sync::Arc; use uds2sovd::{config, doip, error, proxy, server}; -use config::{ConfigProvider, InMemoryConfigProvider, ServerConfig, TomlConfigProvider}; +use config::{ConfigProvider, DefaultConfigProvider, ServerConfig, TomlConfigProvider}; use error::AppError; use proxy::stub::StubProxy; use server::Server; @@ -25,8 +23,9 @@ async fn main() -> Result<(), AppError> { tracing_subscriber::fmt::init(); let config = match std::env::args().nth(1) { - Some(path) => TomlConfigProvider::new(path.into()).load(), - None => InMemoryConfigProvider::new(ServerConfig::default()).load(), + //TODO : Add CLI arg parsing with clap or similar for better UX and error handling. + Some(path) => TomlConfigProvider::new(path.into()).load()?, + None => DefaultConfigProvider::new(ServerConfig::default()).load()?, }; tracing::info!("Starting DoIP server"); @@ -41,6 +40,7 @@ async fn main() -> Result<(), AppError> { let server = Server::new(tcp, udp); + // As of now shutdown is triggered by Ctrl+C, but this can be extended to support other signals or programmatic shutdown in the future. tokio::select! { result = server.start() => { result?; } _ = tokio::signal::ctrl_c() => { diff --git a/sample-doip-server.toml b/app/sample-doip-server.toml similarity index 95% rename from sample-doip-server.toml rename to app/sample-doip-server.toml index 95a816f..1339494 100644 --- a/sample-doip-server.toml +++ b/app/sample-doip-server.toml @@ -1,4 +1,4 @@ -# Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) # # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. diff --git a/client/doip_tester.rs b/client/doip_tester.rs index 4a54758..1edf15c 100644 --- a/client/doip_tester.rs +++ b/client/doip_tester.rs @@ -1,14 +1,12 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - */ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 //! DoIP tester β€” exercises the running DoIP server end-to-end. //! diff --git a/docs/01-startup.puml b/docs/01-startup.puml index 2546d3c..ae37317 100644 --- a/docs/01-startup.puml +++ b/docs/01-startup.puml @@ -1,14 +1,13 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - */ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 + @startuml Startup title 01-startup diff --git a/docs/02-tcp-connection.puml b/docs/02-tcp-connection.puml index 53ec740..f06c360 100644 --- a/docs/02-tcp-connection.puml +++ b/docs/02-tcp-connection.puml @@ -1,14 +1,14 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - */ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 + + @startuml TCP_Connection_Lifecycle title 02-tcp-connection diff --git a/docs/03-udp-request.puml b/docs/03-udp-request.puml index 92d6b37..2e5fda5 100644 --- a/docs/03-udp-request.puml +++ b/docs/03-udp-request.puml @@ -1,3 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 + + @startuml UDP_Request_Handling title 03-udp-request - -participant "DoIP Server App" as DoipServerApp -participant "Server" as Server -participant "TCP Service" as Tcp -participant "UDP Service" as Udp - -== Load Configuration == - -DoipServerApp -> DoipServerApp: load configuration -||| -note right of DoipServerApp - CLI config file or defaults -end note - -== Initialize Services == - -DoipServerApp -> Tcp: create TCP service -||| -DoipServerApp -> Udp: create UDP service -||| -DoipServerApp -> Server: create server - -== Start Runtime == - -DoipServerApp -> Server: start() -||| - -Server -> Tcp: bind TCP listener -alt TCP bind failure - Tcp - -> Server: startup error - Server - -> DoipServerApp: startup failed -end -||| - -Server -> Udp: bind UDP socket -alt UDP bind failure - Udp - -> Server: startup error - Server - -> DoipServerApp: startup failed -end - -== Running State == - -Server -> Tcp: start connection loop -Server -> Udp: start discovery loop -||| - -note over Tcp, Udp - Services running concurrently -end note - -@enduml - -PlantUML version 1.2020.02(Sun Mar 01 15:52:07 IST 2020) -(GPL source distribution) -Java Runtime: OpenJDK Runtime Environment -JVM: OpenJDK 64-Bit Server VM -Java Version: 21.0.11+10-1-24.04.2-Ubuntu -Operating System: Linux -Default Encoding: UTF-8 -Language: en -Country: null ---> \ No newline at end of file +01-startupDoIP Server AppServerTCP ServiceUDP ServiceDoIP Server AppDoIP Server AppServerServerTCP ServiceTCP ServiceUDP ServiceUDP ServiceLoad Configurationload configurationCLI config file or defaultsInitialize Servicescreate TCP servicecreate UDP servicecreate serverStart Runtimestart()bind TCP listeneralt[TCP bind failure]startup errorstartup failedbind UDP socketalt[UDP bind failure]startup errorstartup failedRunning Statestart connection loopstart discovery loopServices running concurrently \ No newline at end of file diff --git a/docs/02-tcp-connection.svg b/docs/02-tcp-connection.svg index bf3e21c..7da6580 100644 --- a/docs/02-tcp-connection.svg +++ b/docs/02-tcp-connection.svg @@ -1,75 +1 @@ -02-tcp-connectionClientClientTCP ServiceTCP ServiceSessionSessionUDS2SOVDUDS2SOVDConnection Establishmentconnect (port 13400)alt[maximum sessions reached]connection rejectedclose connection[session accepted]spawn sessionRequest Processingloop[until disconnect or failure]DoIP requestalt[invalid request]negative acknowledgment[diagnostic message]forward diagnostic payloaddiagnostic responsediagnostic response[supported request]responsecommunication failureterminates sessionSession Cleanupsession endsslot dropped automatically \ No newline at end of file +02-tcp-connectionClientTCP ServiceSessionUDS2SOVDClientClientTCP ServiceTCP ServiceSessionSessionUDS2SOVDUDS2SOVDConnection Establishmentconnect (port 13400)alt[maximum sessions reached]connection rejectedclose connection[session accepted]spawn sessionRequest Processingloop[until disconnect or failure]DoIP requestalt[invalid request]negative acknowledgment[diagnostic message]forward diagnostic payloaddiagnostic responsediagnostic response[supported request]responsecommunication failureterminates sessionSession Cleanupsession endsslot dropped automatically \ No newline at end of file diff --git a/docs/03-udp-request.svg b/docs/03-udp-request.svg index a65ce42..8977ad8 100644 --- a/docs/03-udp-request.svg +++ b/docs/03-udp-request.svg @@ -1,47 +1 @@ -03-udp-requestClientClientUDP ServiceUDP ServiceReceive Looploop[waiting for datagrams]discovery request (port 13400)alt[valid identification request]vehicle announcement (VIN, EID, address)[EID/VIN does not match]silent drop (ISO 7.6.1)[entity status request]node type and capacity[invalid request]negative acknowledgment \ No newline at end of file +03-udp-requestClientUDP ServiceClientClientUDP ServiceUDP ServiceReceive Looploop[waiting for datagrams]discovery request (port 13400)alt[valid identification request]vehicle announcement (VIN, EID, address)[EID/VIN does not match]silent drop (ISO 7.6.1)[entity status request]node type and capacity[invalid request]negative acknowledgment \ No newline at end of file From 773afb876f04f37977c6d21a1c2abd0663d4f0c0 Mon Sep 17 00:00:00 2001 From: vinayrs Date: Wed, 17 Jun 2026 16:07:00 +0530 Subject: [PATCH 17/21] docs(config): add rustdoc documentation and design rationale --- src/config/defaults.rs | 34 ++++++++--- src/config/error.rs | 19 +++--- src/config/mod.rs | 57 ++++++++++++++++-- src/config/provider/default_config.rs | 32 ++++++++-- src/config/provider/mod.rs | 25 +++++++- src/config/provider/toml.rs | 33 ++++++++++- src/config/types.rs | 51 ++++++++++++---- src/lib.rs | 84 ++++++++++++++++++++++++--- 8 files changed, 288 insertions(+), 47 deletions(-) diff --git a/src/config/defaults.rs b/src/config/defaults.rs index 33174a6..8992992 100644 --- a/src/config/defaults.rs +++ b/src/config/defaults.rs @@ -8,10 +8,19 @@ // terms of the Apache License Version 2.0 which is available at // https://www.apache.org/licenses/LICENSE-2.0 +//! Default configuration values. +//! +//! These constants provide compile-time defaults for transport +//! settings and ECU identity values used when configuration +//! fields are omitted. + use crate::doip::types::{Eid, Gid, LogicalAddress, Vin}; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; -/// Default TCP listen address (loopback, standard DoIP port 13400). +/// Default TCP listen address. +/// +/// Uses the standard DoIP TCP port (`13400`) and binds to +/// the loopback interface. pub const TCP_ADDRESS: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 13400)); @@ -19,7 +28,7 @@ pub const TCP_ADDRESS: SocketAddr = pub const UDP_ADDRESS: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 13400)); -/// Default maximum number of concurrent TCP connections. +/// Default maximum number of concurrent TCP sessions. pub const MAX_CONNECTIONS: usize = 10; // TODO: Add DEFAULT_MAX_DATA_SIZE constant for entity status response. @@ -27,16 +36,25 @@ pub const MAX_CONNECTIONS: usize = 10; /// Default TCP read buffer size in bytes. pub const READ_BUFFER_SIZE: usize = 4096; -/// Default DoIP logical address for this server entity. +/// Default DoIP logical address advertised by this entity. +/// +/// Used in routing activation and diagnostic communication. pub const LOGICAL_ADDRESS: LogicalAddress = LogicalAddress::new(0x0001); -/// Default VIN: 17 ASCII zeroes. -/// Must be overridden with the actual vehicle VIN in production. +/// Default Vehicle Identification Number (VIN). +/// +/// The default value consists of 17 ASCII zero characters and +/// should be replaced with the actual vehicle VIN in production. pub const VIN: Vin = Vin::new(*b"00000000000000000"); -/// Default Entity Identifier: all-zero bytes. -/// Should be set to the MAC address of the DoIP network interface. +/// Default Entity Identifier (EID). +/// +/// The default value is all zeros and should be replaced with +/// a unique identifier, typically derived from the MAC address +/// of the DoIP network interface. pub const EID: Eid = Eid::new([0u8; 6]); -/// Default Group Identifier: all-zero bytes. +/// Default Group Identifier (GID). +/// +/// The default value is all zeros. pub const GID: Gid = Gid::new([0u8; 6]); diff --git a/src/config/error.rs b/src/config/error.rs index 919a93f..7237b9d 100644 --- a/src/config/error.rs +++ b/src/config/error.rs @@ -8,21 +8,26 @@ // terms of the Apache License Version 2.0 which is available at // https://www.apache.org/licenses/LICENSE-2.0 -//! Configuration loading errors. +//! Configuration subsystem error types. //! -//! Separates file I/O errors from TOML parsing errors for clearer -//! error messages to users. +//! These errors represent failures that can occur while loading +//! and deserializing server configuration. +//! +//! Errors are categorized by source to provide clear diagnostics +//! to users and simplify troubleshooting. -/// Errors that can occur during configuration loading. +/// Errors produced while loading server configuration. /// -/// Variants distinguish between: -/// - Missing/unreadable files (`FileRead`) -/// - Malformed TOML syntax or validation failures (`ParseError`) +/// The error variants preserve the original failure source, +/// allowing callers to distinguish between file access failures +/// and configuration deserialization failures. #[derive(Debug, thiserror::Error)] pub enum ConfigError { + ///Failed to read the TOML configuration file. #[error("Failed to read config file: {0}")] FileRead(#[from] std::io::Error), + ///Failed to parse the TOML configuration content. #[error("Failed to parse TOML config: {0}")] ParseError(#[from] toml::de::Error), } diff --git a/src/config/mod.rs b/src/config/mod.rs index 0c755ea..caf3902 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -8,7 +8,32 @@ // terms of the Apache License Version 2.0 which is available at // https://www.apache.org/licenses/LICENSE-2.0 -//! Server configuration: types, defaults, and pluggable providers. +//! Configuration subsystem for the DoIP server. +//! +//! This module defines the configuration model, Configuration loading abstraction, +//! and provider implementations used by the DoIP server. +//! +//! # Design Rationale +//! +//! Configuration loading is separted from configuration usage. The DoIP server consumes a fully constructed [`ServerConfig`] +//! +//! ```text +//! Configuration Source +//! β”‚ +//! β–Ό +//! ConfigProvider +//! β”‚ +//! β–Ό +//! ServerConfig +//! β”‚ +//! β–Ό +//! Server +//! ``` +//! +//! This separation allows the same server implementation to be used with different configuration sources, such as: +//! - TOML files for production deployments +//! - Default configuration for tests and examples +//! - Future configuration sources (environment variables, remote configuration services, etc.) pub mod defaults; pub mod error; @@ -19,9 +44,33 @@ pub use error::ConfigError; pub use provider::{DefaultConfigProvider, TomlConfigProvider}; pub use types::{EcuConfig, ServerConfig, TcpConfig, UdpConfig}; -/// Trait for loading server configuration from any source. +/// # Design Rationale +/// +/// The DoIP server requires a fully validated configuration +/// before startup. By introducing a configuration provider +/// abstraction, the server remains independent of how +/// configuration is obtained. +/// +/// This enables: +/// +/// - TOML-based configuration for production deployments +/// - In-memory configuration for tests +/// - Future configuration sources without modifying server code +/// +/// Abstraction for loading server configuration. +/// +/// Implementations may load configuration from files, +/// Default structures, environment variables, or other +/// configuration backends. +/// +/// The server depends on this trait rather than concrete +/// configuration sources, allowing configuration loading +/// concerns to remain isolated from server startup logic. pub trait ConfigProvider { - /// Load and return a complete [`ServerConfig`]. - /// Returns an error if the configuration cannot be loaded or parsed. + /// Loads and returns a complete [`ServerConfig`]. + /// + /// # Errors + /// + ///Returns [`ConfigError`] if configuration loading fails.. fn load(&self) -> Result; } diff --git a/src/config/provider/default_config.rs b/src/config/provider/default_config.rs index 0ec3336..a9463aa 100644 --- a/src/config/provider/default_config.rs +++ b/src/config/provider/default_config.rs @@ -8,24 +8,48 @@ // terms of the Apache License Version 2.0 which is available at // https://www.apache.org/licenses/LICENSE-2.0 -//use super::super instead of crate::config to avoid circular dependency with config provider during compilation. +//! Default configuration provider. +//! +//! This provider wraps an already constructed [`ServerConfig`] and +//! returns it on every call to [`ConfigProvider::load`]. +//! +//! Unlike [`TomlConfigProvider`](super::toml::TomlConfigProvider), +//! this provider performs no file I/O and does not involve parsing or validation logic. +//! + use super::super::{ConfigError, ConfigProvider}; use crate::config::types::ServerConfig; -/// Config provider that holds a pre-built [`ServerConfig`] in memory. -/// Use when configuration is constructed programmatically rather than loaded from a file. +/// A [`ConfigProvider`] implementation backed by an default_config instance +/// [`ServerConfig`]. +/// +/// This provider is primarily intended for testing, examples, +/// and applications that construct configuration programmatically. +/// +/// # Design Rationale +/// +/// The configuration loading abstraction allows the server +/// to remain independent of configuration sources. +/// +/// `DefaultConfigProvider` exists to support testing and +/// dependency injection without requiring file-system access. pub struct DefaultConfigProvider { config: ServerConfig, } impl DefaultConfigProvider { - /// Wrap an existing config for use as a provider. + /// Creates a provider that always returns the supplied + /// configuration instance. pub fn new(config: ServerConfig) -> Self { Self { config } } } impl ConfigProvider for DefaultConfigProvider { + /// Returns a clone of the stored configuration. + /// + /// This operation cannot fail because the configuration + /// has already been constructed and validated. fn load(&self) -> Result { // Default config is always valid, so this never fails Ok(self.config.clone()) diff --git a/src/config/provider/mod.rs b/src/config/provider/mod.rs index 0585703..1f31a31 100644 --- a/src/config/provider/mod.rs +++ b/src/config/provider/mod.rs @@ -8,8 +8,29 @@ // terms of the Apache License Version 2.0 which is available at // https://www.apache.org/licenses/LICENSE-2.0 -//! Configuration providers β€” load [`ServerConfig`](super::types::ServerConfig) -//! from different sources (default config or TOML file). +//! Configuration loading infra +//! +//! This module provides abstraction and implementations for creating +//! [`ServerConfig`](super::types::ServerConfig) instances from different sources. +//! +//! # Design Rationale +//! +//! Configuration loading is separated from configuration usage. +//! The DoIP server consumes a fully constructed `\[`ServerConfig`] +//! and remains unaware of where configuration data originated. +//! +//! This enables: +//! +//! - TOML-based configuration for production deployments +//! - In-memory configuration for tests +//! - Future configuration sources (environment variables, +//! remote configuration services, etc.) +//! +//! # Implementations +//! +//! - [`TomlConfigProvider`] loads configuration from TOML files. +//! - [`DefaultConfigProvider`] provides an in-memory configuration +//! primarily intended for testing and examples. pub mod default_config; pub mod toml; diff --git a/src/config/provider/toml.rs b/src/config/provider/toml.rs index 75d93ae..1ddbc5b 100644 --- a/src/config/provider/toml.rs +++ b/src/config/provider/toml.rs @@ -8,23 +8,54 @@ // terms of the Apache License Version 2.0 which is available at // https://www.apache.org/licenses/LICENSE-2.0 +//! TOML-backed configuration provider. +//! +//! This provider loads server configuration from a TOML file +//! and deserializes it into a [`ServerConfig`] using Serde. +//! +//! This is the primary configuration provider intended for +//! production deployments. + use std::path::PathBuf; use crate::config::types::ServerConfig; use crate::config::{ConfigError, ConfigProvider}; -/// Config provider that loads a [`ServerConfig`] from a TOML file. +/// A [`ConfigProvider`] implementation that loads configuration +/// from a TOML file. +/// +/// This provider performs file I/O and deserialization on each +/// call to [`ConfigProvider::load`]. +/// +/// # Design Rationale +/// +/// Configuration loading is separated from server startup to +/// keep the server independent of configuration sources. +/// +/// This allows the same server implementation to be used with +/// TOML files, default configuration, or future configuration +/// backends. pub struct TomlConfigProvider { + /// Path to the TOML configuration file. path: PathBuf, } impl TomlConfigProvider { + /// Creates a provider that loads configuration from the specified TOML file. pub fn new(path: PathBuf) -> Self { Self { path } } } impl ConfigProvider for TomlConfigProvider { + ///Loads configuration from the TOML file specified in the provider. + /// + /// # Errors + /// + /// Returns a `ConfigError` if: + /// - The TOML file cannot be read. + /// - The TOML content cannot be deserialized into a `ServerConfig`. + /// fn load(&self) -> Result { let content = std::fs::read_to_string(&self.path)?; let config = toml::from_str(&content)?; diff --git a/src/config/types.rs b/src/config/types.rs index 89b0708..d2dc998 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -8,16 +8,22 @@ // terms of the Apache License Version 2.0 which is available at // https://www.apache.org/licenses/LICENSE-2.0 -use std::net::SocketAddr; - +//! Configuration data model +//! +//! This module defines the runtime configuration used by the DoIP server. +//! +//! Configuration is grouped into TCP, UDP, and ECU sections. +//! +//! Configuration values are deserialized from TOML files via serde and may fall back to compile-time defaults if not specified. use serde::Deserialize; +use std::net::SocketAddr; use super::defaults; use crate::doip::types::{Eid, Gid, LogicalAddress, Vin}; /// Top-level server configuration, split into TCP, UDP, and ECU sections. /// -///Private fields +/// Private fields /// /// Fields are private and accessed via `into_parts()` to: /// - Force explicit destructuring of config sections @@ -28,6 +34,7 @@ use crate::doip::types::{Eid, Gid, LogicalAddress, Vin}; /// /// `#[serde(default)]` allows partial TOML files β€” missing sections use Default. /// Users only specify what they want to change from defaults. + #[derive(Debug, Clone, Deserialize, Default)] #[serde(default)] pub struct ServerConfig { @@ -37,15 +44,24 @@ pub struct ServerConfig { } impl ServerConfig { - /// Destructure into the three sub-configs. + /// Consumes the configuration and returns the individual + /// transport and ECU configuration sections. + /// + /// This encourages explicit ownership transfer and makes + /// configuration usage visible at the call site. pub fn into_parts(self) -> (TcpConfig, UdpConfig, EcuConfig) { (self.tcp, self.udp, self.ecu) } } -/// TCP transport settings: listen address, connection limits, buffer size. +/// TCP transport configuration. /// -/// Using #[serde(default)] allows omitting fields in TOML β€” they'll use Default values. +/// Controls how the DoIP server accepts and manages +/// TCP diagnostic connections. +/// +/// `#[serde(default)]` allows partial TOML files. +/// Missing fields fall back to compile-time defaults. + #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct TcpConfig { @@ -77,9 +93,12 @@ impl TcpConfig { } } -/// UDP transport settings: listen address and logical address. +/// UDP transport configuration. /// -/// Using #[serde(default)] allows omitting fields in TOML β€” they'll use Default values. +/// Controls DoIP vehicle discovery and stateless UDP communication. +/// +/// `#[serde(default)]` allows partial TOML files. +/// Missing fields fall back to compile-time defaults. #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct UdpConfig { @@ -119,9 +138,13 @@ impl Default for UdpConfig { } } -/// ECU identity settings: VIN, EID, and GID used in vehicle identification responses. +/// ECU identity information advertised by the DoIP entity. +/// +/// These values are included in vehicle identification +/// and entity status responses defined by ISO 13400-2. /// -/// Note: No #[serde(default)] here since VIN/EID are required configuration parameters. +/// Note: No `#[serde(default)]` is used because ECU identity +/// values should be explicitly configured. #[derive(Debug, Clone, Deserialize)] pub struct EcuConfig { vin: Vin, @@ -134,7 +157,9 @@ impl EcuConfig { pub fn new(vin: Vin, eid: Eid, gid: Gid) -> Self { Self { vin, eid, gid } } - /// Vehicle Identification Number (17 ASCII characters). + /// Returns the configured Vehicle Identification Number (VIN). + /// + /// VIN is a 17-character vehicle identifier defined by ISO 3779. pub fn vin(&self) -> Vin { self.vin } @@ -142,7 +167,9 @@ impl EcuConfig { pub fn eid(&self) -> Eid { self.eid } - /// Group Identifier (6 bytes). + /// Returns the configured Group Identifier (GID). + /// + /// GID identifies a logical group of DoIP entities. pub fn gid(&self) -> Gid { self.gid } diff --git a/src/lib.rs b/src/lib.rs index 8fae2e0..0af28ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,25 +10,91 @@ //! # uds2sovd β€” DoIP Server Library //! -//! A DoIP (Diagnostics over Internet Protocol) server library that accepts -//! UDS diagnostic requests from DoIP clients and forwards them to an SOVD backend. +//! A DoIP (Diagnostics over Internet Protocol) server library. +//! This crate provides a transport bridge between DoIP clients and an SOVD (Service-Oriented Vehicle Diagnostics) backend. //! //! ## What this library offers //! -//! config: Load server settings (bind addresses, ECU identity) from TOML or in-memory. -//! doip: DoIP protocol: message parsing, handlers for vehicle identification, -//! routing activation, alive check, entity status, and diagnostic messages. -//! proxy: Forward UDS bytes to an SOVD backend. Implement the SovdProxy trait -//! for your backend; StubProxy and MockProxy are provided for development and testing. -//! server: TCP/UDP transport layer with concurrent listeners and graceful shutdown. -//! error: Unified error type for protocol and I/O errors. +//! ## Crate Layout //! +//! | Module | Responsibility| +//! |--------------|----------------| +//! | [`config`] | Configuration structs and providers | +//! | [`doip`] | DoIP protocol handling (message parsing, serialization, etc.)| +//! | [`error`] | Error types and handling utilities | +//! | [`proxy`] | The `SovdProxy` trait and related types for interfacing with the SOVD backend | +//! | [`server`] | The main server implementation, including the dispatcher and message handlers | +//! +//! ## Request Flow +//! +//! ```text +//! How these layers fit together +//! +//! Tester ( doipclient) +//! β”‚ TCP :13400 β”‚ UDP :13400 +//! β–Ό β–Ό +//! TcpTransport UdpTransport +//! β”‚ β”‚ +//! β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +//! β–Ό +//! Dispatcher // This dispatcher routes messages to protocol-specific +//! | handlers based on the DoIP message type +//! β”‚ +//! Message Handlers (Γ—8) +//! β”‚ +//! SovdProxy (trait) +//! β”‚ +//! β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +//! StubProxy RealSovdProxy +//! (NRC 0x11) (SOVD REST API) +//! current future +//! ``` //! ## How to use //! //! 1. Implement the SovdProxy trait for your SOVD backend. //! 2. Create a config (TOML file or in-memory). //! 3. Build the server and run it. //! +//! ## Public API +//! +//! Only six types are publicly exported. Everything else is `pub(crate)` or private. +//! +//! | Type | What it is | +//! |------|------------| +//! | [`server`] | starts and runs the DoIP server, manages TCP and UDP transports, and handles shutdown | +//! | `ServerConfig` | Configuration for the DoIP server | +//! | `ConfigProvider` | Trait for loading configuration from various sources (TOML, environment variables, etc.) | +//! | `DefaultConfigProvider` | A simple ConfigProvider that takes a ServerConfig directly (useful for testing) | +//! | `TomlConfigProvider` | A ConfigProvider that loads configuration from a TOML file | +//! | `SovdProxy` | Trait that defines the interface for forwarding UDS requests to an SOVD backend and returning responses | +//! +//! +//! ## System Boundaries +//! This crate is Responsible for: +//! - TCP and UDP DoIP communication +//! - Parsing and serializing DoIP messages +//! - Routing DoIP requests to handlers +//! - Managing transport-level sessions +//! - Forwarding UDS payloads through SovdProxy +//! +//! This crate is not Responsible for: +//! - UDS service execution +//! - Diagnostic business logic +//! - Security access algorithms +//! +//! ## Current Status +//! +//! Implemented +//! - TCP DoIP Transport with basic message parsing and handling +//! - UDP Vechicle Discovery with basic request handling +//! - Message dispatching based on DoIP message types +//! - Diagnostic message forwarding to a StubProxy that returns NRC 0x11 (Service Not Supported) for all requests +//! +//! Future Work +//! - Full routing activation state machine implementation +//! - Producation SOVD backend integration (e.g., REST API client) +//! - Additional protocol validation +//! //! See `app/main.rs` for a working example and `app/sample-doip-server.toml` for //! a reference configuration From db87feea5217b575460db01736e3f18132c5869d Mon Sep 17 00:00:00 2001 From: vinayrs Date: Fri, 19 Jun 2026 11:35:42 +0530 Subject: [PATCH 18/21] docs: reorganize architecture and sequence diagrams --- docs/01-startup.svg | 1 - docs/02-tcp-connection.svg | 1 - docs/03-udp-request.svg | 1 - .../doip_server_startup.puml} | 26 +-- docs/Sequence_diagram/doip_server_startup.svg | 1 + .../doip_server_tcp_connection.puml} | 22 +-- .../doip_server_tcp_connection.svg | 1 + .../doip_server_udp_request.puml} | 25 ++- .../doip_server_udp_request.svg | 1 + docs/doip_server_architecture.puml | 176 ++++++++++++++++++ docs/doip_server_architecture.svg | 1 + ..._server_architecture_module_structure.puml | 66 +++++++ ...p_server_architecture_module_structure.svg | 1 + 13 files changed, 283 insertions(+), 40 deletions(-) delete mode 100644 docs/01-startup.svg delete mode 100644 docs/02-tcp-connection.svg delete mode 100644 docs/03-udp-request.svg rename docs/{01-startup.puml => Sequence_diagram/doip_server_startup.puml} (69%) create mode 100644 docs/Sequence_diagram/doip_server_startup.svg rename docs/{02-tcp-connection.puml => Sequence_diagram/doip_server_tcp_connection.puml} (67%) create mode 100644 docs/Sequence_diagram/doip_server_tcp_connection.svg rename docs/{03-udp-request.puml => Sequence_diagram/doip_server_udp_request.puml} (58%) create mode 100644 docs/Sequence_diagram/doip_server_udp_request.svg create mode 100644 docs/doip_server_architecture.puml create mode 100644 docs/doip_server_architecture.svg create mode 100644 docs/doip_server_architecture_module_structure.puml create mode 100644 docs/doip_server_architecture_module_structure.svg diff --git a/docs/01-startup.svg b/docs/01-startup.svg deleted file mode 100644 index 7582c52..0000000 --- a/docs/01-startup.svg +++ /dev/null @@ -1 +0,0 @@ -01-startupDoIP Server AppServerTCP ServiceUDP ServiceDoIP Server AppDoIP Server AppServerServerTCP ServiceTCP ServiceUDP ServiceUDP ServiceLoad Configurationload configurationCLI config file or defaultsInitialize Servicescreate TCP servicecreate UDP servicecreate serverStart Runtimestart()bind TCP listeneralt[TCP bind failure]startup errorstartup failedbind UDP socketalt[UDP bind failure]startup errorstartup failedRunning Statestart connection loopstart discovery loopServices running concurrently \ No newline at end of file diff --git a/docs/02-tcp-connection.svg b/docs/02-tcp-connection.svg deleted file mode 100644 index 7da6580..0000000 --- a/docs/02-tcp-connection.svg +++ /dev/null @@ -1 +0,0 @@ -02-tcp-connectionClientTCP ServiceSessionUDS2SOVDClientClientTCP ServiceTCP ServiceSessionSessionUDS2SOVDUDS2SOVDConnection Establishmentconnect (port 13400)alt[maximum sessions reached]connection rejectedclose connection[session accepted]spawn sessionRequest Processingloop[until disconnect or failure]DoIP requestalt[invalid request]negative acknowledgment[diagnostic message]forward diagnostic payloaddiagnostic responsediagnostic response[supported request]responsecommunication failureterminates sessionSession Cleanupsession endsslot dropped automatically \ No newline at end of file diff --git a/docs/03-udp-request.svg b/docs/03-udp-request.svg deleted file mode 100644 index 8977ad8..0000000 --- a/docs/03-udp-request.svg +++ /dev/null @@ -1 +0,0 @@ -03-udp-requestClientUDP ServiceClientClientUDP ServiceUDP ServiceReceive Looploop[waiting for datagrams]discovery request (port 13400)alt[valid identification request]vehicle announcement (VIN, EID, address)[EID/VIN does not match]silent drop (ISO 7.6.1)[entity status request]node type and capacity[invalid request]negative acknowledgment \ No newline at end of file diff --git a/docs/01-startup.puml b/docs/Sequence_diagram/doip_server_startup.puml similarity index 69% rename from docs/01-startup.puml rename to docs/Sequence_diagram/doip_server_startup.puml index ae37317..a90fbb0 100644 --- a/docs/01-startup.puml +++ b/docs/Sequence_diagram/doip_server_startup.puml @@ -1,16 +1,16 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) -// -// See the NOTICE file(s) distributed with this work for additional -// information regarding copyright ownership. -// -// This program and the accompanying materials are made available under the -// terms of the Apache License Version 2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 - - -@startuml Startup -title 01-startup +/' Copyright (c) 2026 Contributors to the Eclipse Foundation + + See the NOTICE file(s) distributed with this work for additional + information regarding copyright ownership. + + This program and the accompanying materials are made available under the + terms of the Apache License Version 2.0 which is available at + + + SPDX-License-Identifier: Apache-2.0 '/ + +@startuml doip_server_startup +title DoIP Server Startup