Skip to content
sntnsPublic

About

Pure-Rust client for lldpd's control socket (no FFI, no liblldpctl.so)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

rlldpctl

CI Crates.io docs.rs License: MIT

A pure-Rust client for lldpd's control socket: list interfaces and read discovered LLDP neighbors without shelling out to lldpcli or linking liblldpctl.so.

let mut client = rlldpctl::Client::connect()?;
for iface in client.interfaces()? {
    let details = client.interface(&iface.name)?;
    for neighbor in &details.neighbors {
        println!(
            "{}: {} ({:?})",
            iface.name,
            neighbor.chassis.name.as_deref().unwrap_or("?"),
            neighbor.port_id_str(),
        );
    }
}

Run it live against a real daemon with:

cargo run --example show_neighbors [socket-path]
cargo run --example watch_neighbors [socket-path]  # live feed via SUBSCRIBE

An async equivalent, AsyncClient/AsyncSubscription (backed by tokio::net::UnixStream), is available behind the tokio feature:

rlldpctl = { version = "0.1", features = ["tokio"] }
let mut client = rlldpctl::AsyncClient::connect().await?;
for iface in client.interfaces().await? {
    let details = client.interface(&iface.name).await?;
    // ...
}
cargo run --example show_neighbors_async --features tokio [socket-path]
cargo run --example watch_neighbors_async --features tokio [socket-path]

The sync API has no tokio dependency at all with the feature off - this is purely additive, and both share the exact same encode/decode logic (src/wire), only the socket I/O differs.

Why this exists, and why it's fragile

lldpd does not define a stable IPC protocol for lldpcli/liblldpctl to talk to it with. What's actually on the wire is lldpd's own internal C structures (struct lldpd_port, struct lldpd_chassis, ...), memcpy'd with a small pointer-graph-following envelope around them (src/marshal.c and src/ctl.c upstream). This crate reimplements that envelope and mirrors the relevant structs directly (src/wire/raw.rs), rather than linking liblldpctl.so or shelling out to lldpcli.

That means it's coupled to, and only known to work with:

  • A narrow lldpd version window - narrower than you'd expect. Diffing src/lldpd-structs.h across every tag from 0.9.0 (2008-ish) to master shows the framing and marshaling mechanism has been untouched that whole time, but several fields this crate actually reads are recent additions:

    Version Change
    < 1.0.14 no SET_CHASSIS in the hmsg_type enum -> GET_INTERFACE is 5, not 6
    < 1.0.20 no lldpd_port.p_vlan_advertise_pattern
    < 1.0.21 no lldpd_interface.alias / lldpd_hardware.h_ifalias
    master unreleased lldpd_hardware.h_flags_previous, inserted after 1.0.22

    Net effect: this crate is verified against lldpd 1.0.21, 1.0.22, and current master (matching this workspace's Yocto-packaged 1.0.22, plus tracking upstream ahead of its next tag) - not "any 1.0.x". Sending our hardcoded GET_INTERFACE = 6 to anything older than 1.0.14 doesn't even fail loudly: the daemon just answers a different request (GET_DEFAULT_PORT) instead. A struct layout mismatch below 1.0.21 would desync parsing the same way. "Verified" here is not just a claim: CI builds real lldpd 1.0.21, 1.0.22, and master from source and runs this crate against them (tests/real_lldpd.rs, see Testing). The h_flags_previous row above is exactly what that master leg caught in practice - decode_hardware now tries both the pre- and post-h_flags_previous layouts and keeps whichever one actually parses (src/wire/raw.rs's RawHardware vs RawHardwareWithFlagsPrevious), since there's no version negotiation on the wire to tell it which one a given daemon uses. That's a real fix for a real drift, not a guarantee against every future one: if master changes shape again before its next tag, that CI leg will fail again until this crate is updated to match.

  • The build-time feature flags lldpd was compiled with. Several struct fields only exist under ENABLE_DOT1 / ENABLE_DOT3 / ENABLE_LLDPMED / ENABLE_CUSTOM. This crate assumes dot1, dot3, cdp, fdp and lldpmed on and custom off, matching this workspace's lldpd recipe (PACKAGECONFIG ??= "cdp fdp edp sonmp lldpmed dot1 dot3").

  • The host's C ABI - pointer width (handled automatically via #[repr(C)], as long as you build for the same target lldpd runs on) and time_t tracking pointer width (true of the traditional glibc/musl Linux ABI on 32- and 64-bit; not true of a 32-bit "time64" C library).

In short: don't expect this to survive an lldpd upgrade unverified, and don't expect it to work talking to some other machine's lldpd over a different architecture. This is the same trade-off upstream itself accepts between lldpd and liblldpctl.so internally - just without linking that library. If you need a protocol that doesn't have this problem, you want the real liblldpctl C API, or shelling out to lldpcli -f json.

Scope

v1 implements four requests: GET_INTERFACES (list interfaces), GET_INTERFACE (one interface's local info + discovered neighbors), and SUBSCRIBE/NOTIFICATION (a live feed of neighbor changes via Client::subscribe, which returns an Iterator<Item = Result<NeighborChange>>

  • see examples/watch_neighbors.rs; or AsyncClient::subscribe + AsyncSubscription::next_change behind the tokio feature). Subscribing consumes the client: the protocol doesn't allow more GET_INTERFACES/GET_INTERFACE calls on a connection once it starts pushing notifications, so open a second client if you need both. Chassis name/description/capabilities/management addresses and LLDP-MED inventory TLVs are decoded; per-port VLAN/PPVID/PI TLVs are walked (for correct byte accounting) but not yet surfaced in the model. Nothing that changes daemon state (SET_PORT, SET_CONFIG, ...) is implemented. AsyncSubscription is a plain async fn next_change, not a futures::Stream - wrap it with futures::stream::unfold yourself if you want one.

Testing

  • src/wire/decode.rs has unit tests that hand-build wire messages (using #[repr(C)] reinterpretation of the same raw structs the decoder reads, so they don't require a live lldpd) covering interface lists, a neighbor with a chassis and a management address, and the chassis-deduplication path (two neighbor ports sharing one chassis pointer, as lldpd does for a device seen via more than one discovery protocol).

  • src/wire/decode.rs also covers NOTIFICATION decoding: a deleted-neighbor event with no attached port data, and an added-neighbor event with a full chassis.

  • src/subscription.rs unit-tests Subscription's Iterator impl against a socket pair: a clean peer close ends iteration (None), while a well-framed-but-undecodable message surfaces as Some(Err(_)) without ending it.

  • tests/integration.rs drives the public Client API over a real Unix socket against a hand-rolled fake lldpd, independently of the crate's own encoder.

  • src/async_client.rs mirrors the sync transport tests (request, rejection, clean-close) with #[tokio::test], and tests/async_integration.rs mirrors the black-box socket test - both compile to nothing without --features tokio.

  • examples/show_neighbors.rs/examples/watch_neighbors.rs and their _async counterparts talk to a real, running lldpd - not run in CI, but useful for a manual sanity check on real hardware.

  • tests/real_lldpd.rs is the one exception to "no live lldpd required": it runs a real lldpd, not a fake one, and is what backs the version support claim above. CI (.github/workflows/ci.yml's real-lldpd job) builds lldpd from source at 1.0.21, 1.0.22, and master (tests/real_lldpd/Dockerfile, using upstream's own build recipe), starts two independent instances talking over a real veth pair (tests/real_lldpd/entrypoint.sh - not a Docker bridge network, which would silently drop LLDP's reserved multicast destination), and checks that this crate's Client/AsyncClient decode each side's neighbor data to match the peer's real MAC address read straight from the kernel. This test file no-ops outside that container (no root, no real lldpd, no veth needed for a plain local cargo test). To run it yourself:

    docker build --build-arg LLDPD_REF=1.0.22 -t rlldpctl-real-lldpd:1.0.22 \
      -f tests/real_lldpd/Dockerfile .
    docker run --rm --cap-add=NET_ADMIN --cap-add=NET_RAW rlldpctl-real-lldpd:1.0.22

    Swap LLDPD_REF for 1.0.21 or master to check another version.

cargo test
cargo test --features tokio
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --features tokio -- -D warnings
cargo fmt -- --check

License

MIT

About

Pure-Rust client for lldpd's control socket (no FFI, no liblldpctl.so)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages