mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-12 01:27:32 +00:00
fipstop: add "Listening on fips0" panel to Node tab
Surfaces local services reachable from the mesh, paired with their
current `inet fips` baseline filter classification. Lands to the
right of the existing TUN section in the Traffic block.
A new daemon control query `show_listening_sockets` returns IPv6
listeners bound to either `::` (wildcard) or the node's fd00::/8
address, each classified as Accept / Drop / Unknown / NoFirewall
against the running inbound chain. fipstop renders the result as a
table beside the Traffic counters: Accept rows in default White,
Drop / Unknown in DarkGray, a yellow banner above the table when
`fips-firewall.service` is inactive, and a trailing `*` on
wildcard binds to remind the operator the bind is not
fips0-specific.
Daemon side:
- `src/control/listening.rs` walks `/proc/net/tcp6` and
`/proc/net/udp6` via the procfs crate (LISTEN state for TCP,
wildcard remote for UDP), filters to fips0-reachable binds, and
resolves inodes to PID / comm via `/proc/<pid>/fd`.
- `src/control/firewall_state.rs` shells out to
`nft -j list table inet fips` and walks the inbound chain.
Recognises canonical accepts (`tcp/udp dport N accept`,
`dport { ... } accept`, `dport A-B accept`), the iifname-scoping
line, conntrack and icmpv6 lines (skipped). Any rule with
unrecognised matchers (saddr filters, jumps, daddr filters) or
non-terminal verdicts forces Unknown classification for the
ports it references. Eleven unit tests cover the classification
logic; the listening enumerator carries a /proc-parsing test of
its own.
- `show_listening_sockets` emits
`{fips0_addr, firewall_active, sockets[]}` with per-row
`{proto, local_addr, port, pid, process, filter, wildcard_bind}`.
fipstop side:
- `src/bin/fipstop/ui/dashboard.rs` splits the Traffic block into
a 50/50 horizontal layout; the existing TUN + Forwarded panel
occupies the left half.
- `src/bin/fipstop/ui/listening.rs` renders the right half.
- `main.rs` fetches the new query each tick when the Node tab is
active. Errors are non-fatal: an old daemon without the query
leaves the payload at None and the panel renders "loading...".
`Cargo.toml` gains `procfs = "0.18"` on the Linux target. IPv4
listeners are not enumerated — fips0 is IPv6-only.
Folded in: revert the default-socket lookup from writability-probe
back to existence-based selection. The previous tempfile-probe on
`/run/fips` silently steered fipstop / fipsctl onto an XDG path
the daemon never bound for any user in the `fips` group whose
shell session had not yet picked up the supplementary group (no
re-login after `usermod -aG`). `XDG_RUNTIME_DIR` is set on every
modern systemd-managed user session, so this hit the common case.
The kernel checks actual group membership at `connect(2)`, so a
user who genuinely cannot connect now gets a clear `EACCES`
rather than a silent path mismatch. Drops the now-unused
`is_writable_dir` helper. `XDG_RUNTIME_DIR` existence validation
is preserved.
Documentation:
- `docs/reference/cli-fipstop.md` — Node-tab row updated, new
"Listening on fips0 panel" section.
- `docs/reference/control-socket.md` — `show_listening_sockets`
added to the read-only queries table.
- `docs/how-to/enable-mesh-firewall.md` — new "Verify with
fipstop" section.
- `docs/tutorials/host-a-service.md` — fipstop callouts at
Steps 3, 5, 6 + Troubleshooting bullet + wildcard-bind reminder
under "What you've learned".
- `CHANGELOG.md` — new bullet under `Added / Operator Tooling`,
resolver `Fixed` entry rewritten to describe the
existence-based final shape.
This commit is contained in:
+29
-42
@@ -93,33 +93,30 @@ pub fn pub_file_path(config_path: &Path) -> PathBuf {
|
||||
/// Resolve a default Unix-socket path under the canonical order:
|
||||
/// `/run/fips/<filename>` → `$XDG_RUNTIME_DIR/fips/<filename>` → `/tmp/fips-<filename>`.
|
||||
///
|
||||
/// `/run/fips` is the packaged convention (`root:fips 0770` directory created
|
||||
/// by the daemon at bind time). `XDG_RUNTIME_DIR` covers non-root dev runs
|
||||
/// where `/run/fips` does not exist or is not writable. `/tmp` is the
|
||||
/// last-resort fallback.
|
||||
/// `/run/fips` is the packaged convention (`root:fips 0770` directory
|
||||
/// created by the daemon at bind time, or by the postinst script).
|
||||
/// `XDG_RUNTIME_DIR` covers dev runs where `/run/fips` does not exist.
|
||||
/// `/tmp` is the last-resort fallback.
|
||||
///
|
||||
/// Hardening notes:
|
||||
/// - `/run/fips` is accepted only if the directory exists and is writable by
|
||||
/// the current process. `create_dir_all` reporting `Ok(())` is *not*
|
||||
/// sufficient: it returns `Ok` for an existing root-owned dir that we
|
||||
/// cannot write to, which would silently steer a non-root daemon onto a
|
||||
/// path that fails at bind time. Writability is probed via tempfile create
|
||||
/// rather than mode bits so ACLs and group membership (the dir is
|
||||
/// `root:fips 0770`) are honored.
|
||||
/// - `XDG_RUNTIME_DIR` is validated as an existing directory before being
|
||||
/// used; a stale post-logout value (after `pam_systemd` reaps the dir) is
|
||||
/// treated as missing.
|
||||
/// Selection is by *existence*, not writability. A fips-group member
|
||||
/// whose shell session has not picked up the supplementary group (no
|
||||
/// re-login after `usermod -aG fips`) cannot tempfile-probe a
|
||||
/// `root:fips 0770` directory but can still connect to a socket inside
|
||||
/// it once the kernel checks the actual group at `connect(2)` time —
|
||||
/// and even where the user genuinely cannot connect, surfacing an
|
||||
/// `EACCES` from the socket call is clearer than silently steering
|
||||
/// fipstop / fipsctl to a path the daemon never bound. The daemon's
|
||||
/// own bind code (`ControlSocket::bind`) creates `/run/fips` if it is
|
||||
/// missing, so the resolver does not need to materialize the directory
|
||||
/// itself.
|
||||
///
|
||||
/// `XDG_RUNTIME_DIR` is validated as an existing directory before being
|
||||
/// used; a stale post-logout value (after `pam_systemd` reaps the dir)
|
||||
/// is treated as missing.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn resolve_default_socket(filename: &str) -> String {
|
||||
// 1. /run/fips — accept only if the directory exists and is writable.
|
||||
let run_fips = Path::new("/run/fips");
|
||||
if run_fips.is_dir() && is_writable_dir(run_fips) {
|
||||
return format!("/run/fips/{filename}");
|
||||
}
|
||||
// Also accept /run/fips if we can create it (covers the first-boot
|
||||
// daemon-as-root case before the directory has been materialized). The
|
||||
// actual chown happens at bind time.
|
||||
if std::fs::create_dir_all(run_fips).is_ok() && is_writable_dir(run_fips) {
|
||||
// 1. /run/fips — preferred whenever the directory exists.
|
||||
if Path::new("/run/fips").is_dir() {
|
||||
return format!("/run/fips/{filename}");
|
||||
}
|
||||
|
||||
@@ -137,21 +134,6 @@ pub(crate) fn resolve_default_socket(filename: &str) -> String {
|
||||
format!("/tmp/fips-{filename}")
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn is_writable_dir(path: &Path) -> bool {
|
||||
// Probe via tempfile creation rather than mode bits: mode-bit checks miss
|
||||
// ACLs and group-membership effects (the /run/fips dir is `root:fips
|
||||
// 0770` and the daemon may run as a user that's in the `fips` group).
|
||||
let probe = path.join(format!(".fips-write-probe-{}", std::process::id()));
|
||||
match std::fs::File::create(&probe) {
|
||||
Ok(_) => {
|
||||
let _ = std::fs::remove_file(&probe);
|
||||
true
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Default control socket path for fipsctl / fipstop.
|
||||
///
|
||||
/// On Unix, delegates to [`resolve_default_socket`] for the canonical
|
||||
@@ -1547,8 +1529,11 @@ peers:
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_resolve_default_socket_xdg_when_no_run_fips() {
|
||||
// With /run/fips unwritable (non-root tests) and XDG_RUNTIME_DIR
|
||||
// pointing at an existing directory, the resolver picks XDG.
|
||||
// With /run/fips absent and XDG_RUNTIME_DIR pointing at an
|
||||
// existing directory, the resolver picks XDG. On test hosts where
|
||||
// /run/fips happens to exist (a real fips deployment), the
|
||||
// resolver legitimately picks /run/fips and skips XDG entirely;
|
||||
// both outcomes are accepted below.
|
||||
let _g = ENV_MUTEX.lock().unwrap();
|
||||
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
@@ -1584,7 +1569,9 @@ peers:
|
||||
#[test]
|
||||
fn test_resolve_default_socket_tmp_when_xdg_invalid() {
|
||||
// With XDG_RUNTIME_DIR pointing at a non-existent directory and
|
||||
// /run/fips unwritable, the resolver falls through to /tmp.
|
||||
// /run/fips absent, the resolver falls through to /tmp. On hosts
|
||||
// where /run/fips exists, the resolver legitimately picks it
|
||||
// first; both outcomes are accepted below.
|
||||
let _g = ENV_MUTEX.lock().unwrap();
|
||||
|
||||
let prev_xdg = std::env::var("XDG_RUNTIME_DIR").ok();
|
||||
|
||||
Reference in New Issue
Block a user