BLE-provisioned network configuration for headless embedded Linux. A systemd
service (netprovd) advertises a GATT service that a paired client can use to
list interfaces, read IP configuration, scan Wi-Fi, and set DHCP / static IPv4
/ Wi-Fi credentials. Written in Rust, talks to NetworkManager over D-Bus.
The companion netprov CLI speaks the same protocol over TCP (loopback, for
dev) or BLE (for production).
flowchart TD
Client["BLE client (netprov CLI)"]
NM["NetworkManager (system)"]
subgraph Daemon["netprovd (systemd service, runs as root)"]
direction LR
GATT["BLE GATT Server"]
Session["Auth & Session"]
Facade["Network Facade"]
GATT --> Session --> Facade
end
Client -->|"GATT over LE (Just Works-encrypted link + app-layer HMAC auth)"| GATT
Facade -->|D-Bus| NM
Security posture: the sensitive characteristics (ChallengeNonce, AuthResponse,
Request/Response — everything but Info) require an encrypted link
(encrypt_authenticated_read/encrypt_authenticated_write in
crates/server/src/ble/gatt.rs). Since netprovd runs headless with no display
or keyboard, it registers a no-IO-capability bluer::agent::Agent
(crates/server/src/ble/server.rs), so BlueZ negotiates Just Works pairing on
first access. Just Works establishes encryption but provides no
man-in-the-middle protection by itself; the app-layer HMAC challenge (keyed by
the pre-shared PSK) is what actually authorizes commands, so an active MITM
that completes Just Works pairing still cannot issue commands without the PSK.
The residual risk is that an active MITM present during pairing could
potentially observe traffic before authorization completes (e.g. the
ChallengeNonce). Wi-Fi credentials (Op::ConnectWifi) are only ever
transmitted after the encrypted link and HMAC auth are both established. The
Info characteristic (model + protocol version only, per spec §11) stays
unauthenticated and unencrypted.
Five Rust crates in one workspace:
| Crate | Role |
|---|---|
netprov-protocol |
Wire format: CBOR messages, framing, HMAC auth helpers. Transport-agnostic. |
netprov-server |
netprovd daemon. BLE GATT driver, session state machine, NetworkFacade (mock + nmrs). |
netprov-sdk |
Transport-agnostic ProvisioningClient trait plus BLE/TCP transport implementations, shared by the CLI and desktop app. |
netprov-client |
netprov CLI. Connects over BLE (via --ble-peer) or TCP behind the dev-tcp feature. |
netprov-app |
Dioxus desktop UI, gated behind the desktop feature. |
Builds for amd64 and aarch64 are produced by CI as artifacts. On a target
box:
sudo dpkg -i netprov_<version>_arm64.deb
sudo netprovd keygen --install # generate and install a production PSK
sudo systemctl enable --now netprovd # start at boot + nowThe deb does not auto-start — the admin must install a production key first.
packaging/netprovd.service ships with NETPROV_PRODUCTION=1 set, so the
daemon refuses to start without a real key at /etc/netprov/key rather than
silently falling back to the embedded dev key (the committed PSK under
packaging/, used only by serve-tcp for local dev/loopback tests, where it
logs a loud warning every 60 seconds).
The full request/response surface is exercised end-to-end over TCP against an
in-memory MockFacade:
# one terminal
cp packaging/dev-key.bin /tmp/netprov-key.bin && chmod 600 /tmp/netprov-key.bin
cargo run -p netprov-server --bin netprovd -- serve-tcp --listen 127.0.0.1:9600
# another terminal
cargo run -p netprov-client --features dev-tcp --bin netprov -- \
--key-path /tmp/netprov-key.bin --endpoint 127.0.0.1:9600 listlist, ip <iface>, wifi-status, wifi-scan, wifi-connect, set-dhcp,
set-static all work against the mock.
See packaging/SMOKE-TEST.md for the two-box
hardware-in-the-loop runbook.
cargo test --workspace # default: no BLE, no NM
cargo build -p netprov-server --features live-ble # + BLE GATT server
cargo build -p netprov-server --features live-nm # + real NetworkManager
cargo deb -p netprov-server # build the .deblive-ble implies live-nm (production BLE needs NM). live-nm-destructive
gates the mutating NmrsFacade integration tests that are unsafe to run in
CI.
The Dioxus desktop app is gated behind the desktop feature because it needs
native GTK/WebKit development libraries on Linux. On Ubuntu/Debian:
sudo apt-get install -y \
pkg-config \
libgtk-3-dev \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
libxdo-devThen run the BLE-first desktop app with:
cargo run -p netprov-app --features desktopThe app talks to target devices over BLE; the TCP transport remains a dev/test path for protocol regression coverage.
| Tier | In CI? | Covers |
|---|---|---|
| Unit tests | Yes | Protocol: CBOR round-trips, framing, HMAC, bounded strings. Plus proptests. |
| Session-layer | Yes | Session state machine against MockFacade. |
| Client↔server loopback | Yes | Full request/response surface via tokio::io::duplex. |
| Live NetworkManager | Opt-in | --features live-nm -- --ignored. Requires NM on the system bus. |
| Live BLE e2e | Opt-in | --features live-ble + two BLE-equipped boxes. |
- Design spec
- Part 1 plan — protocol, session, mock facade, loopback
- Part 2 plan — BLE GATT, systemd, NmrsFacade, cargo-deb, CI
MIT OR Apache-2.0.