simple_doip 0.6.0

An ISO 13400-2 (DoIP) implementation with a no_std, zero-copy protocol core and optional async client and server
Documentation

simple_doip

An ISO 13400-2 (DoIP) implementation with a no_std, zero-copy protocol core and optional async client and server.

ARCHITECTURE.md describes how the crate is put together — the feature-gated layering, the sans-io framing/decode seam, the error taxonomy, the relationship to automotive-wire-codec, and the known issues and deferred refactors a new maintainer should read before changing anything.

Scope and limitations

The protocol core — framing, message encode/decode — is golden-vector tested against the on-wire format. The async client and server cover the common case, within these bounds:

  • No TLS. Connections are in the clear on TCP_PORT (13400); TCP_TLS_PORT (3496) is defined by ISO 13400-2 and unused here.
  • The server serves one TCP connection at a time. A second client cannot connect while the first is being served.
  • No unsolicited UDP vehicle announcement. A tester learns of an entity only by asking, and identification is answered on the UDP path only, by Server::run_udp_responder on a socket the caller binds and drives.
  • The client requires the peer to acknowledge before it responds. A DiagnosticMessage that arrives while the client is waiting for the acknowledgement is dropped, so a peer that answers first appears never to answer at all.

ARCHITECTURE.md §7 has the mechanics behind each of these, and the deferred work around them.

None of this constrains bare-metal or single-client use.

Quickstart

The protocol core needs no allocator and no I/O: frame a byte buffer with try_frame, then decode the payload with Payload::decode. This block is the doctest on try_frame in src/framer.rs, wrapped in a fn main returning Result so it compiles as pasted (the doctest gets the same Ok(()) from a hidden line instead). That doctest is the canonical, CI-tested version; if the two ever drift, trust the doctest.

use simple_doip::{try_frame, messages::{MessageError, Payload}};

fn main() -> Result<(), MessageError> {
    // A complete DoIP NACK frame: 8-byte header + 1-byte body.
    let buf = [0x02, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03];

    let (frame, consumed) = try_frame(&buf)?.expect("buffer holds a complete frame");
    assert_eq!(consumed, 9);

    let payload = Payload::decode(frame.payload, frame.header.payload_type)?;
    assert!(matches!(payload, Payload::DoIPNack(_)));
    Ok(())
}

Feature flags

The protocol core is no_std and zero-copy by default. Everything that pulls in alloc, std, or an async runtime is opt-in via Cargo features:

Feature Enables Depends on
alloc Allocator-backed helpers
std std-backed I/O and error traits alloc
codec The tokio-util Encoder/Decoder for DoIP frames std, tokio, tokio-util, bytes
client The async DoIP client codec, async-trait, futures
server The async DoIP server codec, async-trait, futures

default = [], so bare-metal / embedded targets should build with default-features = false to keep the crate no_std with no allocator or runtime dependencies.

alloc is what gates messages::OwnedMessage and its owned mirrors of the borrowed message types — the practical reason to enable it is that you need a message to outlive the buffer it was decoded from (e.g. to move it across a queue or task boundary).

For development and testing, enable client and server:

cargo test --features client,server

Examples

  • bare_metal_codec — encode, frame, and decode with no allocator and no I/O. Runs standalone:

    cargo run --example bare_metal_codec --no-default-features
    
  • echo_server and simple_client — a matched pair, not standalone. The server listens on TCP_PORT (13400); the client dials 127.0.0.1:13400. Run each in its own terminal, server first:

    cargo run --example echo_server --features server    # terminal 1
    cargo run --example simple_client --features client   # terminal 2
    

    The client's ConnectorSocket refuses any server_address whose port is not TCP_PORT (13400), returning Error::InvalidPort — pointing a client at a non-standard port requires your own Connector implementation.

    echo_server answers a diagnostic message the way DoIP prescribes: first a positive acknowledgement carrying the received bytes back in its previous-message-data field, then the echo itself as a separate DiagnosticMessage. Both are written into the ResponseWriter the handler is given, which is the shape a real UDS response takes.

    echo_server calls run_server, so it serves TCP only and does not answer UDP discovery probes; see Server::run_udp_responder for that half.

Relationship to automotive-wire-codec

The wire-level primitives — byte-level Decode/Encode, Incomplete, TrailingBytes — come from the automotive-wire-codec crate. simple_doip::wire re-exports what consumers need so that using this crate does not require a direct dependency on automotive-wire-codec. Because those re-exported types appear in this crate's public API (e.g. in MessageError's variants and in every message type's trait impls), automotive-wire-codec's semver is effectively part of simple_doip's own semver: a breaking change in that crate is a breaking change here too.

MSRV

The minimum supported Rust version is 1.88, bound by let-chain syntax used in this crate.

Contributing

Pull requests, bug reports and questions are welcome — see CONTRIBUTING.md. Security reports go through GitHub's private vulnerability reporting; see SECURITY.md.

License

Licensed under either of MIT or Apache-2.0 at your option.