Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ jobs:
run: cargo test --locked --doc --all-features --workspace

integration:
name: NetworkManager WiFi Integration
name: NetworkManager Integration
runs-on: [self-hosted, linux, x64]
# Pull requests wait for a maintainer to approve this environment.
environment:
Expand All @@ -108,10 +108,87 @@ jobs:

- name: Load virtual WiFi radios
run: |
set -euo pipefail

sudo -n modprobe -r mac80211_hwsim || true
mapfile -t interfaces_before < <(
iw dev | awk '$1 == "Interface" { print $2 }' | sort -u
)

sudo -n modprobe mac80211_hwsim radios=2
udevadm settle --timeout=10

mapfile -t interfaces_after < <(
iw dev | awk '$1 == "Interface" { print $2 }' | sort -u
)
mapfile -t hwsim_interfaces < <(
comm -13 \
<(printf '%s\n' "${interfaces_before[@]}") \
<(printf '%s\n' "${interfaces_after[@]}")
)
if (( ${#hwsim_interfaces[@]} != 2 )); then
echo "Expected the hwsim module to create two interfaces, found ${#hwsim_interfaces[@]}" >&2
iw dev >&2 || true
exit 1
fi

printf 'NMRS_HOST_HWSIM_INTERFACES=%s\n' "${hwsim_interfaces[*]}" >> "${GITHUB_ENV}"

- name: Release virtual WiFi radios from host NetworkManager
run: |
set -euo pipefail
export LC_ALL=C

diagnose() {
echo "Host hwsim/NetworkManager diagnostics:" >&2
iw dev >&2 || true
nmcli device status >&2 || true
nmcli general permissions >&2 || true
journalctl -u NetworkManager --no-pager -n 100 >&2 || true
}
trap diagnose ERR

read -r -a hwsim_interfaces <<< "${NMRS_HOST_HWSIM_INTERFACES:?missing hwsim interface list}"
if (( ${#hwsim_interfaces[@]} != 2 )); then
echo "Expected exactly two recorded hwsim interfaces, found ${#hwsim_interfaces[@]}" >&2
exit 1
fi

if [[ ! -S /run/dbus/system_bus_socket ]]; then
echo "Host system D-Bus socket is unavailable" >&2
exit 1
fi

# The self-hosted runner can load hwsim but is not authorized by
# host polkit to control NetworkManager. Use the already-required
# privileged test image to make this narrowly scoped D-Bus change.
docker compose run --build --rm --no-deps \
-e NMRS_HOST_HWSIM_INTERFACES \
-v /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro \
--entrypoint bash \
test-integration \
-euo pipefail -c '
read -r -a hwsim_interfaces <<< "${NMRS_HOST_HWSIM_INTERFACES:?missing hwsim interface list}"
if (( ${#hwsim_interfaces[@]} != 2 )); then
echo "Expected exactly two hwsim interfaces in the helper, found ${#hwsim_interfaces[@]}" >&2
exit 1
fi

for interface in "${hwsim_interfaces[@]}"; do
nmcli device set "${interface}" managed no
done
for interface in "${hwsim_interfaces[@]}"; do
managed="$(nmcli --get-values GENERAL.NM-MANAGED device show "${interface}")"
if [[ "${managed}" != "no" ]]; then
echo "Host NetworkManager still manages ${interface}: NM-MANAGED=${managed}" >&2
exit 1
fi
done
'

- name: Run integration tests with NetworkManager and virtual Ethernet
run: docker compose run --build --rm test-integration

- name: Run integration tests with NetworkManager and virtual WiFi
run: docker compose run --build --rm test-wifi-integration

Expand Down
25 changes: 20 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,41 @@ Internal modules (`core`, `dbus`, `monitoring`, `types`, `util`) are not part of

## Build and test

Requires a running NetworkManager instance (or use the provided Dockerfile).
Library and documentation tests do not require NetworkManager. Environmental
tests must use the isolated Docker harness or an explicit opt-in.

```bash
cargo check # quick compile check
cargo fmt --all -- --check # formatting (default rustfmt, no config file)
cargo clippy --all-targets --all-features -- -D warnings # lints (warnings are errors in CI)
cargo test -p nmrs --lib --all-features # unit tests only
cargo test --doc --all-features --workspace # doc tests
cargo test --all-features --workspace # unit + integration (needs NM + wifi hardware)
cargo test --test integration_test --all-features # integration only
cargo test --all-features --workspace # unit/docs; environmental tests stay ignored
docker compose run --build --rm test-integration # isolated NM settings/agent/WireGuard/wired lifecycle
docker compose run --build --rm test-wifi-integration # CI-equivalent virtual WiFi tests (Linux)
```

Integration tests require wifi hardware or `mac80211_hwsim`:
Integration tests are `#[ignore]` so normal test commands never touch the host
NetworkManager. The WiFi harness requires two `mac80211_hwsim` radios:
```bash
sudo modprobe mac80211_hwsim radios=2
cargo test --test integration_test --all-features
docker compose run --build --rm test-wifi-integration
sudo modprobe -r mac80211_hwsim
```

For a deliberately selected local NetworkManager, the NM-only opt-in is:
```bash
NMRS_REQUIRE_NETWORKMANAGER=1 \
cargo test --test integration_test --all-features \
networkmanager_ -- --ignored --test-threads=1
```

Those tests create, update, and delete saved profiles; exercise a real
NetworkManager-to-agent secret exchange; and activate a native WireGuard
connection. The Docker harness additionally creates an isolated veth pair and
validates wired DHCP activation. Once a capability flag is set, missing
facilities, timeouts, and unexpected errors must fail rather than skip.

## Toolchain

- Edition 2024, resolver 3, stable Rust (MSRV: 1.90.0)
Expand Down
66 changes: 45 additions & 21 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ I'm fairly accepting to all PR's, only with a couple caveats:
**To run or develop nmrs you need:**

- Rust (stable) via `rustup`
- A running `NetworkManager` instance
- Linux and NetworkManager only for environmental integration tests

I also provide a `Dockerfile` you can build if you don't use Linux and use macOS instead.

Expand All @@ -29,7 +29,8 @@ docker compose run --rm test
```

This starts an isolated system D-Bus and NetworkManager instance, waits for it
to be ready, and requires integration tests to connect to it.
to be ready, runs the workspace tests, and executes the NetworkManager profile
CRUD integration contract. It does not use the host system bus.

**To run an interactive shell:**

Expand All @@ -47,7 +48,7 @@ docker run --rm -it -v $(pwd):/app nmrs-lib # mounts local changes
If you decide to run the shell, ensure you run all commands from within the nmrs directory, not root.

```bash
cargo test -p nmrs # run library tests
cargo test -p nmrs --lib # run library unit tests
cargo build -p nmrs # build the library
cargo check # you get the point...
```
Expand All @@ -69,45 +70,68 @@ fix(#24): fixed bug where something was happening

## Tests

All tests must pass before a merge takes place.
All unit, documentation, and applicable environmental tests must pass before a
merge takes place.

### Ensure NetworkManager is running
### Unit and documentation tests

```bash
sudo systemctl start NetworkManager
cargo test --locked --lib --all-features --workspace
cargo test --locked --doc --all-features --workspace
```

### Test everything (unit + integration)
The integration tests are marked `#[ignore]`. A normal `cargo test` compiles
them but does not contact or mutate any NetworkManager instance. Do not remove
that boundary or add tests which silently return success when a required daemon,
device, or access point is missing.

### Isolated NetworkManager integration

```bash
cargo test --all-features
docker compose run --build --rm test-integration
```

### Integration tests
This starts a private system D-Bus and NetworkManager, plus a veth-backed DHCP
network which cannot select Docker's own `eth0`. It validates real saved-profile
creation, decoding, update, deletion, exact direct and unified settings events,
a NetworkManager-routed secret request and reply, native WireGuard activation,
wired discovery, DHCP activation, typed active-connection data, and disconnect
cleanup. The harness sets
`NMRS_REQUIRE_NETWORKMANAGER=1` and `NMRS_REQUIRE_WIRED=1`; once a capability is
declared, missing services and unexpected D-Bus errors fail the test.

These require WiFi hardware. Please make sure you
run this locally before your PR to ensure everything works.
### Deterministic WiFi integration

```bash
cargo test --test integration_test --all-features
```

If you do not have access to WiFi hardware (for whatever odd reason that is), you can do something like this:
The WiFi contract requires two `mac80211_hwsim` radios. The container configures
one as a WPA2 access point, supplies DHCP with dnsmasq, and gives only the other
radio to its private NetworkManager. It asserts discovery, WPA authentication,
network and device callback delivery, DHCP, disconnect, saved-credential
reconnect, forget, and the missing-password error after cleanup.

```bash
sudo modprobe mac80211_hwsim radios=2
cargo test --test integration_test --all-features
docker compose run --build --rm test-wifi-integration
sudo modprobe -r mac80211_hwsim
```

For the same virtual-radio setup used in CI, on a Linux host with Docker:
The WiFi runner sets `NMRS_REQUIRE_WIFI=1`, `NMRS_WIFI_INTERFACE`,
`NMRS_EXPECT_WIFI_SSID`, and `NMRS_WIFI_PASSWORD`. If a declared facility is
missing, the test fails rather than being reported as a pass.

To run the NM-only contracts against a deliberately selected local daemon, opt
in explicitly:

```bash
sudo modprobe mac80211_hwsim radios=2
docker compose run --build --rm test-wifi-integration
sudo modprobe -r mac80211_hwsim
NMRS_REQUIRE_NETWORKMANAGER=1 \
cargo test --test integration_test --all-features \
networkmanager_ -- --ignored --test-threads=1
```

These tests create and delete a NetworkManager profile and register a temporary
secret agent. The wired contract is intentionally available only when its
separate capability and private interface are supplied. Prefer the Docker
harness unless modifying the selected daemon is intentional.

> [!NOTE]
>
> This method only works on linux
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ RUN apt-get update && apt-get install -y \
libdbus-1-dev \
pkg-config \
dbus \
dnsmasq-base \
ethtool \
hostapd \
iproute2 \
iw \
network-manager \
wpasupplicant \
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,15 @@ If something is missing that you'd like to see, please file a PR or issue, addin

Contributions are welcome. Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.

Environmental tests are opt-in and ignored by a normal `cargo test`, so local
test runs never probe the host NetworkManager implicitly. Use
`docker compose run --build --rm test-integration` for isolated settings,
NetworkManager-routed secrets, native WireGuard activation, and veth-backed
wired DHCP lifecycles, or
`test-wifi-integration` with two `mac80211_hwsim` radios for the deterministic
WPA/DHCP and callback-monitor lifecycle. See the contributing guide for the
exact commands.

## Requirements

- **Rust**: 1.90.0+
Expand Down
2 changes: 1 addition & 1 deletion docs/src/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ These indicate issues the user can fix:
|-------|------------|
| `NotFound` | Move closer to the network or check SSID spelling |
| `AuthFailed` | Check password or credentials |
| `MissingPassword` | Provide a non-empty password |
| `MissingPassword` | Provide a non-empty password, or ensure a saved profile exists before requesting its stored PSK |
| `Timeout` | Retry or increase timeout |
| `DhcpFailed` | Check network infrastructure |
| `NoWifiDevice` | Check that a Wi-Fi adapter is installed |
Expand Down
3 changes: 3 additions & 0 deletions docs/src/api/network-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ builder output, prefer
|--------|---------|-------------|
| `connect_wired()` | `Result<()>` | Connect first available Ethernet device |

For this method and the wired device-listing methods below, Ethernet includes
devices that NetworkManager reports as `veth`.

## VPN Methods

| Method | Returns | Description |
Expand Down
7 changes: 6 additions & 1 deletion docs/src/appendix/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@ No. Concurrent connection operations (calling `connect()` from multiple tasks) a

### How do I handle saved connections?

When nmrs connects to a network, NetworkManager saves the profile. On subsequent connections, the saved profile is reused automatically. You don't need to provide credentials again. Use `forget()` to delete a saved profile.
When nmrs connects to a network, NetworkManager saves the profile. To reconnect
with its stored settings, pass `WifiSecurity::Open` or an empty
`WifiSecurity::WpaPsk` password. A non-empty PSK or an EAP configuration is an
explicit fresh-credential request, so nmrs builds a fresh profile instead of
ignoring it. If activation with a stored PSK fails, nmrs returns the error but
keeps the saved profile. Use `forget()` to delete a saved profile intentionally.

## VPN

Expand Down
Loading