Changelog
All notable changes to this project are documented in this file. Format follows Keep a Changelog; the project uses Semantic Versioning.
Unreleased
Added
- The desktop app scans for devices on startup, so the device list is populated without any action and “Scan again” reads as a repeat rather than the only way to get a list at all.
- The desktop app remembers the “Advanced connection” values (peer identifier
and PSK path) between runs, restoring them on the next launch. They are stored
only after a successful connection, so a typo never becomes the new default.
On macOS the file is
~/Library/Application Support/com.rosterloh.NetprovApp/connection.conf; elsewhere$XDG_CONFIG_HOME/netprov/connection.conf, falling back to~/.config/netprov/connection.conf. It records the path to the PSK, never key bytes, so it is not a secret. Writes are best-effort: an unwritable home means the values are not remembered, not that connecting fails. - The desktop app accepts
--key-path <PATH>(short-k, matching thenetprovCLI) to override the PSK path. Resolution order is the flag, then the stored value, then/etc/netprov/key.
Changed
- The desktop app’s bundle identifier is now
com.rosterloh.NetprovAppvia a newcrates/app/Dioxus.toml, replacing thecom.example.NetprovAppthatdxderives by default. macOS keys per-app Bluetooth consent and the Application Support directory by bundle id, so upgrading re-prompts for Bluetooth permission once and orphans anyconnection.confwritten under the old identifier.
Fixed
BleClient::set_timeis now bounded by a 5 s deadline in both BLE backends. It was the one unbounded await left on the connect path, so a peer that accepted the Current Time write and never answered it hung the caller forever: the desktop app sat on “Connecting securely…” with no timeout, no error and no way out but force-quit, becauseconnect_devicecallsset_timebetween authenticating and loading the snapshot and discards its result.netprov set-timeagainst a real Raspberry Pi 5 reproduced it at 102 s and counting, whilenetprov list— which never callsset_time— worked against the same device, which is what isolated it.netprovd’stimedatedSetTimecall is bounded by the same 5 s deadline. It runs inside the CurrentTime GATT write closure, andSetTimeis polkit-gated: on a headless device with no authentication agent the call can block instead of being denied, so BlueZ never sends the ATT write response. That is the peer-side half of the hang above, and it needs a redeployednetprovdto take effect. The timeout error names polkit, since that is the likely cause and it is otherwise invisible.
Documentation
docs/src/guides/desktop-app.mdnow points atdx serverather thancargo run -p netprov-app --features desktop. A plain Cargo binary does not resolveasset!()paths, so it starts with no stylesheet at all; onlydx(or the bundle it produces) renders the app as intended.
1.1.1 — 2026-08-09
Added
netprovdexposes the standard Bluetooth SIG Current Time Service (0x1805/0x2A2B), so an already-authenticated client can set a clockless device’s system clock (and, viatimedated, its RTC). Reads are unauthenticated; writes require an authenticated session and are clamped to a plausible window.netprov-sdk’sBleClient::set_timeand the desktop app’s automatic post-auth sync make this transparent;netprov set-timeexposes it from the CLI. (#32)
Fixed
connect_wifino longer accumulates duplicate802-11-wirelessprofiles when retrying a mistyped password: an existing profile for the SSID is now looked up and updated/reactivated instead of always callingAddAndActivateConnection. It also polls the resultingActiveConnection’s state until activated/deactivated, so a bad passphrase surfaces as an error instead of a premature success. (#46)
Security
- Further sandboxed
packaging/netprovd.service: an emptyCapabilityBoundingSet/AmbientCapabilities(the daemon only proxies over the NetworkManager and BlueZ D-Bus APIs, never opening a raw network or Bluetooth socket itself),RestrictAddressFamilies=AF_UNIX,MemoryDenyWriteExecute,ProtectClock,ProtectHostname,ProtectKernelLogs,ProtectProc=invisible,RestrictSUIDSGID, andUMask=0077. Also switchedRequires=/Restart=on-failuretoBindsTo=/Restart=alwaysso a clean stop or restart ofNetworkManager.service/bluetooth.servicenow bringsnetprovdback up instead of leaving it down. (#24)
1.1.0 — 2026-08-01
Post-release hardening pass over the BLE auth and transport path, found during a codebase review.
Added
- macOS (and Windows) BLE support for the client and desktop app, via a second
SDK transport backend built on
btleplug.netprov-sdk’sblefeature now picksblueron Linux andbtleplug(CoreBluetooth / WinRT) elsewhere, socargo build -p netprov-app --features desktopworks on a Mac.netprovdis unchanged and remains Linux-only. (#31) netprov ble-scan— lists nearby netprov devices with the identifier to pass to--ble-peer. Requires no PSK and no connection.netprov-sdk/netprov-clientfeatureble-btleplug, forcing the portable backend on Linux so the macOS code path is covered by Linux CI.NETPROV_BLE_MAX_FRAGMENToverrides the per-frame BLE value length, for platforms that do not report a negotiated ATT MTU.- CI: a macOS job building and testing the client, SDK and desktop app.
Changed
- BLE peers are identified by an opaque
PeerIdrather than a BD_ADDR. CoreBluetooth never discloses peer MAC addresses, so--ble-peerand the app’s peer field now accept a platform handle or the advertised device name; a BD_ADDR still works wherever the platform provides one. netprov_sdk::parse_peer_addressis replaced byparse_peer_id, andBleDevice.address: bluer::AddressbyBleDevice.id: PeerIdplus an optionaladdress: Option<String>.
Fixed
- macOS:
--ble-peer <name>stopped resolving after the first connection. CoreBluetooth replaces its cachedlocal_namewith the peer’s GATT device name (a Pi reports its hostname), so the nameble-scanprinted no longer matched. Scans now prefer the advertised name, and both names resolve. - macOS: every request timed out after a successful session. CoreBluetooth
caches a characteristic’s notify state across connections and answers
subscribefrom that cache without writing the CCCD, sonetprovdnever saw a new subscription and dropped each response withdropping notify frame for inactive peer. The client now clears that state when it connects, which also covers sessions the server ended (a rejected PSK drops the link, leaving nothing to unsubscribe over). - A rejected PSK reported
Device disconnectedon macOS. The server errors theAuthResponsewrite and drops the link, and CoreBluetooth discards the ATT error behind the disconnect, so the client now names the likely cause. - macOS:
ble-scanandconnectintermittently found nothing when run shortly after a previous connection. The client never disconnected, and CoreBluetooth keeps the peripheral connected in the system daemon past process exit; a connected peripheral stops advertising. The CLI now disconnects on exit, including on failure. - A peer refusing an operation for insufficient link security is reported as
such, instead of surfacing as
Device disconnected(CoreBluetooth reports the ATT error and drops the link ~2s later) or being misreported as a wrong PSK. - BLE session ordering: sensitive characteristics are no longer reachable before authentication completes.
- Link-layer encryption enforced on ChallengeNonce, AuthResponse, and
Request/Response characteristics (
encrypt_read/encrypt_writeincrates/server/src/ble/gatt.rs). - MTU-aware fragmentation now respects the negotiated BLE MTU instead of a fixed size.
- Reassembly is gated on authentication, partial-message buffers are bounded, and a peer-session handle leak on disconnect is fixed.
- BLE service/characteristic UUIDs de-duplicated to a single source of truth.
Changed
- Failed-auth rate limiter moved from a per-peer-only tier to an additional global tier, bounding aggregate brute-force attempts across peers.
Added
netprovdinitiates bonding itself when a peer subscribes. The sensitive characteristics need an encrypted link, but a central only discovers that by failing a read: CoreBluetooth does raise its pairing prompt at that point, yet the read has already errored and the link is torn down seconds later, and it never retries. Requesting the bond at subscribe puts the prompt up before the first encrypted read, so a first-time connection can complete instead of having to fail once to provoke pairing.
Security
-
The BLE auth handshake is now mutual, and the protocol version is 2. Previously only the client proved knowledge of the PSK; the server proved nothing, so a peer that merely advertised the netprov service UUID could have collected the Wi-Fi credentials a client sent next.
AuthResponsenow carries the client’s nonce alongside its tag, and the server answers with a tag of its own that the client verifies before issuing any request. The two tags are domain-separated (netprov-auth-client-v2/-server-v2) so neither can be replayed as the other. v1 peers are not wire-compatible. -
Sensitive characteristics ask for
encrypt_*rather thanencrypt_authenticated_*. The authenticated variants are BlueZ’sBT_SECURITY_HIGHand require an MITM-protected LTK, whichnetprovdcannot negotiate: it runs headless and registers aNoInputNoOutputagent, so BlueZ can only do Just Works. The mismatch made every read of a sensitive characteristic fail with “Encryption is insufficient” and the link drop moments later — the BLE transport could not complete authentication on any platform without an out-of-bandbluetoothctlpairing. MITM protection is now provided by the mutual handshake above, at the only layer that holds a shared secret. -
Session::submit_authconsumes the pending nonce before the rate-limit check, so a nonce issued just before lockout no longer survives the whole lockout window. (#19) -
packaging/netprovd.servicenow setsEnvironment=NETPROV_PRODUCTION=1, so the shipped daemon refuses to start on a missing/unreadable/etc/netprov/keyinstead of silently falling back to the public embedded dev key.serve-tcp(dev/loopback) is unaffected. (#10)
1.0.0 — 2026-04-24
First tagged release. Implements the v1 goals.
Added
Protocol (netprov-protocol crate)
- CBOR wire format with a 4 KiB message ceiling enforced at both ends.
- Request/Response envelope with
request_idcorrelation and aResult-shaped response payload. - Seven v1 operations:
ListInterfaces,GetIpConfig,WifiStatus,WifiScan,SetDhcp,SetStaticIpv4,ConnectWifi. - Fragment/reassemble framing for BLE MTUs up to 512 bytes (
[u16 request_id] [u16 seq] [u8 flags]header, FIN bit). BoundedString(512-byte cap) to keep error payloads within frame limits.- HMAC-SHA256 helpers with constant-time verification via
subtle. - Property test for
fragment ∘ reassemble = idacross arbitrary payloads and MTUs.
Server (netprov-server crate, netprovd binary)
NetworkFacadetrait with two implementations:MockFacade(defaultmockfeature) — in-memory state, fault injection, used by all session-layer and loopback tests.NmrsFacade(live-nmfeature) — real NetworkManager vianmrs+ rawzbus. Implements all seven ops.
- Per-connection
Sessionstate machine: challenge/response HMAC auth, single-use nonces, authenticated dispatch. - Per-peer failed-auth rate limiter (5 failures / 60s → 10 min lockout, configurable; mockable clock).
- BLE GATT server (
live-blefeature) overbluer0.17: one primary service with Info, ChallengeNonce, AuthResponse, and Request/Response (write + notify) characteristics.run_ble_serverhandles adapter setup, advertising, per-peerPeerSession, and fragment fan-out. - Static-IPv4 validator: prefix range, loopback/multicast/broadcast rejection, gateway-in-subnet check, DNS sanity.
- Key loading priority:
$NETPROV_KEY_PATH→/etc/netprov/key(mode enforced) → embedded dev key.$NETPROV_PRODUCTION=1disables the dev-key fallback. - Startup banner and periodic WARN loop when the embedded dev key is active.
netprovd keygen [--install]subcommand — generates a 32-byte PSK, prints base64 + QR code, optionally installs to/etc/netprov/keyat mode 0600.netprovd serve-tcpsubcommand — loopback TCP transport for dev (drivesMockFacade).netprovd serve-blesubcommand (live-ble) — production BLE GATT server (drivesNmrsFacade) with sd-notifyType=notifyreadiness signalling.tracing-journaldrouting when the journald socket is reachable; stderr fmt layer as fallback on dev/non-systemd hosts.
Client (netprov-client crate, netprov binary)
- Transport-agnostic
Client<IO>over anyAsyncRead + AsyncWrite. - BLE connector (
blefeature) that discovers the service by UUID, reads the Info + ChallengeNonce characteristics, computes and writes the HMAC tag, then fragments and exchanges Request/Response frames over the notify stream. - Full CLI parity with the protocol:
list,ip <iface>,wifi-status,wifi-scan,wifi-connect,set-dhcp,set-static. --endpointfor TCP transport,--ble-peer <BD_ADDR>for BLE.$NETPROV_KEY_PATHand$NETPROV_ENDPOINTenv fallbacks.
Packaging
cargo-debmetadata producing a singlenetprov_1.0.0-1_<arch>.debcontaining both binaries, the systemd unit, and the README.- Debian maintainer scripts:
postinst(creates/etc/netprovat 0700 and prints key-install guidance — never auto-starts the service);prerm(stops the unit);postrm(removes/etc/netprovonpurgeonly). packaging/netprovd.service—Type=notify,NoNewPrivileges,ProtectSystem=strict,ProtectHome,PrivateTmp, system-call filter restricted to@system-serviceminus@mount @obsolete @privileged @reboot @swap,ReadWritePaths=/etc/netprov.
CI
- GitHub Actions matrix across
ubuntu-latest(amd64) andubuntu-24.04-arm(aarch64). Runs fmt, clippy in three feature configurations (default,live-nm,live-ble), the default test suite, and a release build underlive-ble. Separatedebjob builds and uploads per-arch.debartifacts gated on thetestjob passing.
Documentation
- Design spec.
- Part 1 (core) and Part 2 (BLE + systemd + deb) implementation plans.
- README with architecture diagram, install and dev quickstarts, build matrix, testing tier table.
- Two-box BLE smoke-test runbook, now at
docs/src/guides/ble-smoke-test.md.
Security
- Application-layer HMAC-SHA256 challenge/response defeats active MitM during BLE pairing regardless of Just Works vs. secure pairing at the link layer.
- Per-peer-MAC rate limiter bounds brute-force attempts.
- Single-use nonces — consumed by the first
AuthResponsewrite on each connection. - Key file at
/etc/netprov/keyrefused if any group or world permission bit is set; no silent fallback to the dev key on misconfigured perms. - Embedded dev key is flagged as INSECURE at startup and every 60 seconds thereafter.
- Static-IP input validation runs at the facade boundary before any NetworkManager call.
Non-goals (deferred)
The v1 release deliberately excludes:
- IPv6 configuration.
- Enterprise Wi-Fi (WPA2-EAP / 802.1X).
- Destructive operations (reboot, interface disable, forget connection).
- Mobile and desktop clients.
- Apply-with-rollback for write operations.
- Running as a non-root user with
CAP_NET_ADMIN+ polkit. - Split
netprov/netprovdpackages.