Closes#201. Two-part hardening of crates/nunciod/src/grpc.rs, no proto changes.
Fail closed on non-loopback bind addresses.pub async fn serve now parses addr as a SocketAddr and returns the new GrpcServeError::NonLoopbackAddress(_) if parsing fails or ip().is_loopback() is false, before ever calling TcpListener::bind. This is only in serve (the production entry point from main.rs); serve_on_listener / serve_on_listener_with_overrides are untouched since they take an already-bound test listener (127.0.0.1:0).
One consolidated auth-coverage canary test, all_mounted_services_reject_unauthenticated_calls, spawns a test server and dials all eight currently-mounted services -- System, Accounts, Mail, Filters, Export, Audit, Calendar, Contacts -- with no bearer token, asserting Code::Unauthenticated for each. The doc comment on serve_on_listener_with_overrides now tells a future author to update both the add_service chain and this test's service list when adding a service.
The existing serve_fails_closed_when_bind_address_is_invalid test asserted GrpcServeError::Bind for a syntactically-invalid address; that address now fails address validation before ever reaching bind, so it's renamed serve_fails_closed_when_bind_address_is_malformed and now asserts NonLoopbackAddress. Added serve_rejects_a_non_loopback_bind_address_without_binding (covers 0.0.0.0:0 and 10.0.0.5:9420) and serve_accepts_ipv6_loopback_bind_address (covers [::1]:0, using a short tokio::time::timeout to distinguish "still serving" from a real failure, and asserting any failure is Bind, never NonLoopbackAddress, so the test is robust on IPv6-less CI runners).
All existing per-service unauthenticated-call tests are untouched and still pass.
Test plan
cargo fmt --all -- --check -- clean
cargo check-all (clippy --all-targets --workspace -D warnings) -- zero warnings
cargo test-all -- 426 passed, 0 failed, 0 ignored
cargo fmt --all -- --check
(no output -- clean)
cargo check-all
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.51s
cargo test-all
test result: ok. 25 passed; 0 failed; 0 ignored ...
test result: ok. 2 passed; 0 failed; 0 ignored ...
test result: ok. 28 passed; 0 failed; 0 ignored ...
test result: ok. 4 passed; 0 failed; 0 ignored ...
test result: ok. 12 passed; 0 failed; 0 ignored ...
test result: ok. 3 passed; 0 failed; 0 ignored ...
test result: ok. 52 passed; 0 failed; 0 ignored ...
test result: ok. 26 passed; 0 failed; 0 ignored ...
test result: ok. 2 passed; 0 failed; 0 ignored ...
test result: ok. 52 passed; 0 failed; 0 ignored ...
test result: ok. 3 passed; 0 failed; 0 ignored ...
test result: ok. 23 passed; 0 failed; 0 ignored ...
test result: ok. 61 passed; 0 failed; 0 ignored ...
test result: ok. 120 passed; 0 failed; 0 ignored ... <-- nunciod (incl. new canary + bind-hardening tests)
test result: ok. 1 passed; 0 failed; 0 ignored ... <-- accounts_lifecycle_e2e_test
test result: ok. 1 passed; 0 failed; 0 ignored ... <-- calendar_e2e_test
test result: ok. 1 passed; 0 failed; 0 ignored ... <-- contacts_e2e_test
test result: ok. 1 passed; 0 failed; 0 ignored ... <-- daemon_e2e_test
test result: ok. 2 passed; 0 failed; 0 ignored ... <-- filters_triage_e2e_test
test result: ok. 6 passed; 0 failed; 0 ignored ... <-- grpc_auth_test
test result: ok. 1 passed; 0 failed; 0 ignored ... <-- spine_e2e_test
(plus 8 doc-test runs, 0 tests each)
TOTAL: 426 passed; 0 failed; 0 ignored
No out-of-scope bugs found during this work.
## Summary
Closes #201. Two-part hardening of `crates/nunciod/src/grpc.rs`, no proto changes.
1. **Fail closed on non-loopback bind addresses.** `pub async fn serve` now parses `addr` as a `SocketAddr` and returns the new `GrpcServeError::NonLoopbackAddress(_)` if parsing fails or `ip().is_loopback()` is false, *before* ever calling `TcpListener::bind`. This is only in `serve` (the production entry point from `main.rs`); `serve_on_listener` / `serve_on_listener_with_overrides` are untouched since they take an already-bound test listener (`127.0.0.1:0`).
2. **One consolidated auth-coverage canary test**, `all_mounted_services_reject_unauthenticated_calls`, spawns a test server and dials all **eight** currently-mounted services -- **System, Accounts, Mail, Filters, Export, Audit, Calendar, Contacts** -- with no bearer token, asserting `Code::Unauthenticated` for each. The doc comment on `serve_on_listener_with_overrides` now tells a future author to update both the `add_service` chain and this test's service list when adding a service.
The existing `serve_fails_closed_when_bind_address_is_invalid` test asserted `GrpcServeError::Bind` for a syntactically-invalid address; that address now fails address *validation* before ever reaching bind, so it's renamed `serve_fails_closed_when_bind_address_is_malformed` and now asserts `NonLoopbackAddress`. Added `serve_rejects_a_non_loopback_bind_address_without_binding` (covers `0.0.0.0:0` and `10.0.0.5:9420`) and `serve_accepts_ipv6_loopback_bind_address` (covers `[::1]:0`, using a short `tokio::time::timeout` to distinguish "still serving" from a real failure, and asserting any failure is `Bind`, never `NonLoopbackAddress`, so the test is robust on IPv6-less CI runners).
All existing per-service unauthenticated-call tests are untouched and still pass.
## Test plan
- [x] `cargo fmt --all -- --check` -- clean
- [x] `cargo check-all` (clippy `--all-targets --workspace -D warnings`) -- zero warnings
- [x] `cargo test-all` -- **426 passed, 0 failed, 0 ignored**
```
cargo fmt --all -- --check
(no output -- clean)
cargo check-all
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.51s
cargo test-all
test result: ok. 25 passed; 0 failed; 0 ignored ...
test result: ok. 2 passed; 0 failed; 0 ignored ...
test result: ok. 28 passed; 0 failed; 0 ignored ...
test result: ok. 4 passed; 0 failed; 0 ignored ...
test result: ok. 12 passed; 0 failed; 0 ignored ...
test result: ok. 3 passed; 0 failed; 0 ignored ...
test result: ok. 52 passed; 0 failed; 0 ignored ...
test result: ok. 26 passed; 0 failed; 0 ignored ...
test result: ok. 2 passed; 0 failed; 0 ignored ...
test result: ok. 52 passed; 0 failed; 0 ignored ...
test result: ok. 3 passed; 0 failed; 0 ignored ...
test result: ok. 23 passed; 0 failed; 0 ignored ...
test result: ok. 61 passed; 0 failed; 0 ignored ...
test result: ok. 120 passed; 0 failed; 0 ignored ... <-- nunciod (incl. new canary + bind-hardening tests)
test result: ok. 1 passed; 0 failed; 0 ignored ... <-- accounts_lifecycle_e2e_test
test result: ok. 1 passed; 0 failed; 0 ignored ... <-- calendar_e2e_test
test result: ok. 1 passed; 0 failed; 0 ignored ... <-- contacts_e2e_test
test result: ok. 1 passed; 0 failed; 0 ignored ... <-- daemon_e2e_test
test result: ok. 2 passed; 0 failed; 0 ignored ... <-- filters_triage_e2e_test
test result: ok. 6 passed; 0 failed; 0 ignored ... <-- grpc_auth_test
test result: ok. 1 passed; 0 failed; 0 ignored ... <-- spine_e2e_test
(plus 8 doc-test runs, 0 tests each)
TOTAL: 426 passed; 0 failed; 0 ignored
```
No out-of-scope bugs found during this work.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Closes #201. Two-part hardening of
crates/nunciod/src/grpc.rs, no proto changes.pub async fn servenow parsesaddras aSocketAddrand returns the newGrpcServeError::NonLoopbackAddress(_)if parsing fails orip().is_loopback()is false, before ever callingTcpListener::bind. This is only inserve(the production entry point frommain.rs);serve_on_listener/serve_on_listener_with_overridesare untouched since they take an already-bound test listener (127.0.0.1:0).all_mounted_services_reject_unauthenticated_calls, spawns a test server and dials all eight currently-mounted services -- System, Accounts, Mail, Filters, Export, Audit, Calendar, Contacts -- with no bearer token, assertingCode::Unauthenticatedfor each. The doc comment onserve_on_listener_with_overridesnow tells a future author to update both theadd_servicechain and this test's service list when adding a service.The existing
serve_fails_closed_when_bind_address_is_invalidtest assertedGrpcServeError::Bindfor a syntactically-invalid address; that address now fails address validation before ever reaching bind, so it's renamedserve_fails_closed_when_bind_address_is_malformedand now assertsNonLoopbackAddress. Addedserve_rejects_a_non_loopback_bind_address_without_binding(covers0.0.0.0:0and10.0.0.5:9420) andserve_accepts_ipv6_loopback_bind_address(covers[::1]:0, using a shorttokio::time::timeoutto distinguish "still serving" from a real failure, and asserting any failure isBind, neverNonLoopbackAddress, so the test is robust on IPv6-less CI runners).All existing per-service unauthenticated-call tests are untouched and still pass.
Test plan
cargo fmt --all -- --check-- cleancargo check-all(clippy--all-targets --workspace -D warnings) -- zero warningscargo test-all-- 426 passed, 0 failed, 0 ignoredNo out-of-scope bugs found during this work.