mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-10 00:26:59 +00:00
Merge branch 'maint'
This commit is contained in:
@@ -93,6 +93,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- macOS: `peers.allow`, `peers.deny`, and the `hosts` file are now read
|
||||
from `/usr/local/etc/fips/`, matching the install layout the macOS
|
||||
packaging ships (`packaging/macos/`). The default-path constants were
|
||||
hardcoded to `/etc/fips/...` with only a `#[cfg(unix)]` / `#[cfg(windows)]`
|
||||
split, so on macOS the daemon looked in a directory that does not exist:
|
||||
`load_file` / `load_hosts_file` hit their `NotFound` no-op arm and silently
|
||||
returned an empty ACL / empty host map. A populated `peers.deny` therefore
|
||||
reported `effective_mode: "default_open"` and `enforcement_active: false`
|
||||
via `fipsctl acl show`, and host-file aliases went unloaded, with no error
|
||||
or warning. The default constants now follow the platform's packaging —
|
||||
`/usr/local/etc/fips/` on macOS, `/etc/fips/` on Linux and other Unix
|
||||
for the ACL files, and `/etc/fips/` on Linux and `%ProgramData%\fips\`
|
||||
on Windows for the hosts file — and are pinned by platform-gated unit
|
||||
tests so the layout cannot silently drift again. At startup the daemon
|
||||
warns once if any of these files exist at the old `/etc/fips/` location
|
||||
but not at the current default. Linux and Windows behavior is unchanged.
|
||||
**macOS users with existing files in `/etc/fips/` should move them to
|
||||
`/usr/local/etc/fips/`.**
|
||||
|
||||
- macOS: `fipsctl keygen` now writes `fips.key` / `fips.pub` to
|
||||
`/usr/local/etc/fips/` by default, matching the install layout the macOS
|
||||
packaging ships. The default output directory was hardcoded to
|
||||
`/etc/fips` for all Unix, but the daemon derives its identity key paths
|
||||
from the config file's directory — `/usr/local/etc/fips/fips.yaml` on
|
||||
macOS — so a generated identity landed where the daemon never reads it
|
||||
and the node silently kept an ephemeral identity. Linux and other Unix
|
||||
keep `/etc/fips`, Windows is unchanged, and the values are pinned by
|
||||
platform-gated unit tests.
|
||||
|
||||
- macOS: the system-wide config search path now includes
|
||||
`/usr/local/etc/fips/fips.yaml` in addition to `/etc/fips/fips.yaml`,
|
||||
matching the install layout the macOS packaging ships. Previously only
|
||||
`/etc/fips/fips.yaml` was probed, so a bare `fips` run without `--config`
|
||||
skipped the installed config and derived identity key paths from a
|
||||
non-existent directory. `/etc/fips/fips.yaml` is still probed first so
|
||||
existing installs keep working. Both the macOS entry in the search path
|
||||
and the directory `fipsctl keygen` writes to read the shared
|
||||
`SYSTEM_CONFIG_DIR` constant, so the two cannot drift apart. The
|
||||
launchd-installed daemon was unaffected (it always passes `--config`).
|
||||
Linux and Windows behavior is unchanged. Because the daemon derives the
|
||||
identity key directory from whichever config file loaded last, a macOS host
|
||||
carrying `fips.yaml` at both locations would have resolved `fips.key` to the
|
||||
new directory, found none, and under `persistent` generated a fresh
|
||||
identity — silently changing its npub, routing address and mesh IPv6. The
|
||||
daemon now adopts a key stranded at `/etc/fips/fips.key` and warns to move
|
||||
it, instead of generating one. The fallback is confined to keys resolved
|
||||
from the system config directory, so a run using `./fips.yaml` or a user
|
||||
config is never redirected to a system key.
|
||||
|
||||
- Nostr NAT traversal no longer breaks after the host suspends. The traversal
|
||||
clock cached a Unix timestamp once at startup and advanced it with a
|
||||
monotonic `Instant`, which does not tick while a machine is asleep, so after
|
||||
|
||||
@@ -72,6 +72,49 @@ present after the first successful daemon start. If the daemon never
|
||||
came up cleanly (config error, permission problem), the key files
|
||||
will be missing.
|
||||
|
||||
### macOS note
|
||||
|
||||
The macOS package (`.pkg`) installs config and keys under
|
||||
`/usr/local/etc/fips/` instead of `/etc/fips/`. The paths above become:
|
||||
|
||||
| Linux / other Unix | macOS |
|
||||
| --- | --- |
|
||||
| `/etc/fips/fips.yaml` | `/usr/local/etc/fips/fips.yaml` |
|
||||
| `/etc/fips/fips.key` | `/usr/local/etc/fips/fips.key` |
|
||||
| `/etc/fips/fips.pub` | `/usr/local/etc/fips/fips.pub` |
|
||||
|
||||
`fipsctl keygen` writes to `/usr/local/etc/fips/` by default on macOS.
|
||||
The daemon still probes `/etc/fips/fips.yaml` as a fallback (so an
|
||||
existing install is not broken by an upgrade), but the macOS packaging
|
||||
only installs files under `/usr/local/etc/fips/`.
|
||||
|
||||
If you have files in `/etc/fips/` from a manual install, move them:
|
||||
|
||||
```sh
|
||||
sudo mv /etc/fips/fips.yaml /usr/local/etc/fips/fips.yaml
|
||||
sudo mv /etc/fips/fips.key /usr/local/etc/fips/fips.key
|
||||
sudo mv /etc/fips/fips.pub /usr/local/etc/fips/fips.pub
|
||||
sudo mv /etc/fips/peers.allow /usr/local/etc/fips/peers.allow 2>/dev/null || true
|
||||
sudo mv /etc/fips/peers.deny /usr/local/etc/fips/peers.deny 2>/dev/null || true
|
||||
sudo mv /etc/fips/hosts /usr/local/etc/fips/hosts 2>/dev/null || true
|
||||
```
|
||||
|
||||
The daemon logs a warning at startup if any of `peers.allow`, `peers.deny`,
|
||||
or `hosts` exist at the old `/etc/fips/` path but not at
|
||||
`/usr/local/etc/fips/`. `fips.yaml` is deliberately not included: both
|
||||
directories stay on the config search path, so a config file left at
|
||||
`/etc/fips/` is still read and is not a stranded file.
|
||||
|
||||
`fips.key` is handled differently again, because losing it is worse than
|
||||
not reading it. The daemon derives the key directory from whichever config
|
||||
file loaded last, so a host carrying `fips.yaml` at both locations resolves
|
||||
the key to `/usr/local/etc/fips/`. If no key is there and one exists at
|
||||
`/etc/fips/fips.key`, the daemon **uses the old key and warns**, rather than
|
||||
generating a new identity: a fresh keypair would change the node's npub,
|
||||
routing address and mesh IPv6 with no migration path. Move the key when you
|
||||
see that warning; the fallback exists to make the upgrade survivable, not
|
||||
to be relied on.
|
||||
|
||||
### File layout and permissions
|
||||
|
||||
| Path | Mode | Owner | Contents |
|
||||
|
||||
@@ -66,7 +66,7 @@ highest-priority value wins.
|
||||
|
||||
| Priority | Path | Purpose |
|
||||
| -------- | ---- | ------- |
|
||||
| 1 | `/etc/fips/fips.yaml` | System-wide defaults |
|
||||
| 1 | `/usr/local/etc/fips/fips.yaml` (macOS), `/etc/fips/fips.yaml` (other Unix) | System-wide defaults |
|
||||
| 2 | `~/.config/fips/fips.yaml` | User preferences |
|
||||
| 3 | `~/.fips.yaml` | Legacy user config |
|
||||
| 4 | `./fips.yaml` | Deployment-specific overrides |
|
||||
|
||||
@@ -89,7 +89,7 @@ daemon.
|
||||
|
||||
| Flag | Argument | Default | Description |
|
||||
| ---- | -------- | ------- | ----------- |
|
||||
| `-d`, `--dir` | `DIR` | `/etc/fips` (Unix), `%APPDATA%\fips` (Windows) | Output directory for `fips.key` and `fips.pub`. |
|
||||
| `-d`, `--dir` | `DIR` | `/usr/local/etc/fips` (macOS), `/etc/fips` (other Unix), `%APPDATA%\fips` (Windows) | Output directory for `fips.key` and `fips.pub`. Matches the directory the platform's packaging installs config into, which is where the daemon derives the key paths from. |
|
||||
| `-f`, `--force` | — | off | Overwrite an existing `fips.key`. |
|
||||
| `-s`, `--stdout` | — | off | Print `nsec` then `npub` to stdout instead of writing files. |
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ locations, lowest to highest priority:
|
||||
|
||||
| Priority | Path | Purpose |
|
||||
|----------|------|---------|
|
||||
| 1 (lowest) | `/etc/fips/fips.yaml` | System-wide defaults |
|
||||
| 1 (lowest) | `/usr/local/etc/fips/fips.yaml` (macOS), `/etc/fips/fips.yaml` (other Unix) | System-wide defaults |
|
||||
| 2 | `~/.config/fips/fips.yaml` | User preferences |
|
||||
| 3 | `~/.fips.yaml` | Legacy user config |
|
||||
| 4 (highest) | `./fips.yaml` | Deployment-specific overrides |
|
||||
|
||||
@@ -111,6 +111,11 @@ async fn run_daemon(
|
||||
}
|
||||
}
|
||||
|
||||
// The hosts/ACL defaults on macOS moved from /etc/fips to
|
||||
// /usr/local/etc/fips; flag files stranded at the old location.
|
||||
#[cfg(target_os = "macos")]
|
||||
fips::node::warn_on_legacy_config_paths();
|
||||
|
||||
// Identity provisioning: config nsec > key file > generate ephemeral
|
||||
let resolved = match resolve_identity(&config, &loaded_paths) {
|
||||
Ok(r) => r,
|
||||
|
||||
+18
-1
@@ -324,10 +324,13 @@ fn print_response(value: &serde_json::Value) {
|
||||
}
|
||||
|
||||
/// Default directory for keygen output.
|
||||
///
|
||||
/// Must match the platform's config dir, since the daemon derives key
|
||||
/// paths from the config file's location.
|
||||
fn default_key_dir() -> PathBuf {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
PathBuf::from("/etc/fips")
|
||||
PathBuf::from(fips::config::SYSTEM_CONFIG_DIR)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
@@ -652,6 +655,20 @@ fn sparkline(values: &[f64], min: f64, max: f64) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// macOS packaging ships config under /usr/local/etc/fips/.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn test_default_key_dir_follows_macos_packaging_layout() {
|
||||
assert_eq!(default_key_dir(), PathBuf::from("/usr/local/etc/fips"));
|
||||
}
|
||||
|
||||
// Non-macOS Unix keeps the historic /etc/fips/ location.
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
#[test]
|
||||
fn test_default_key_dir_keeps_etc_fips_layout() {
|
||||
assert_eq!(default_key_dir(), PathBuf::from("/etc/fips"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acl_show_command_name() {
|
||||
assert_eq!(AclCommands::Show.command_name(), "show_acl");
|
||||
|
||||
+176
-6
@@ -3,7 +3,8 @@
|
||||
//! Loads configuration from YAML files with a cascading priority system:
|
||||
//! 1. `./fips.yaml` (current directory - highest priority)
|
||||
//! 2. `~/.config/fips/fips.yaml` (user config directory)
|
||||
//! 3. `/etc/fips/fips.yaml` (system - lowest priority)
|
||||
//! 3. `/etc/fips/fips.yaml` (system - lowest priority; on macOS
|
||||
//! `/usr/local/etc/fips/fips.yaml` is also probed, after `/etc/fips/`)
|
||||
//!
|
||||
//! Values from higher priority files override those from lower priority files.
|
||||
//!
|
||||
@@ -47,6 +48,15 @@ pub use transport::{
|
||||
/// Default config filename.
|
||||
const CONFIG_FILENAME: &str = "fips.yaml";
|
||||
|
||||
/// System-wide config directory, following the platform's packaging layout
|
||||
/// (`/usr/local/etc/fips` on macOS, `/etc/fips` otherwise). The daemon
|
||||
/// derives identity key paths from the config file's location, so anything
|
||||
/// that reads or writes config-adjacent files should use this one constant.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const SYSTEM_CONFIG_DIR: &str = "/usr/local/etc/fips";
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub const SYSTEM_CONFIG_DIR: &str = "/etc/fips";
|
||||
|
||||
/// Default key filename, placed alongside the config file.
|
||||
const KEY_FILENAME: &str = "fips.key";
|
||||
|
||||
@@ -83,6 +93,42 @@ pub fn key_file_path(config_path: &Path) -> PathBuf {
|
||||
.join(KEY_FILENAME)
|
||||
}
|
||||
|
||||
/// Legacy system config directory, from before the platform-packaging move.
|
||||
///
|
||||
/// Equal to [`SYSTEM_CONFIG_DIR`] everywhere except where packaging installs
|
||||
/// outside `/etc`, which makes [`legacy_key_fallback`] inert on those
|
||||
/// platforms without needing a `cfg` of its own.
|
||||
const LEGACY_SYSTEM_CONFIG_DIR: &str = "/etc/fips";
|
||||
|
||||
/// Find an identity key stranded at the legacy system config directory.
|
||||
///
|
||||
/// Adding a second system config directory to the search path moves the
|
||||
/// directory `resolve_identity` derives the key path from, because that
|
||||
/// directory comes from whichever config file loaded last. A node that
|
||||
/// carries a config at both locations would otherwise find no key at the new
|
||||
/// one and, under `persistent`, generate a fresh identity — silently changing
|
||||
/// its npub, routing address and mesh IPv6, none of which has a migration
|
||||
/// path.
|
||||
///
|
||||
/// Returns the legacy key only when all of these hold, which confines the
|
||||
/// fallback to exactly that regression:
|
||||
///
|
||||
/// - the two directories actually differ, so this is inert on Linux
|
||||
/// - no key exists at `key_path`
|
||||
/// - `key_path` sits in `system_dir`, so an operator using `./fips.yaml` or a
|
||||
/// user config is never redirected to a system key
|
||||
/// - a key does exist at `legacy_dir`
|
||||
fn legacy_key_fallback(key_path: &Path, system_dir: &Path, legacy_dir: &Path) -> Option<PathBuf> {
|
||||
if system_dir == legacy_dir || key_path.exists() {
|
||||
return None;
|
||||
}
|
||||
if key_path.parent() != Some(system_dir) {
|
||||
return None;
|
||||
}
|
||||
let legacy = legacy_dir.join(KEY_FILENAME);
|
||||
legacy.exists().then_some(legacy)
|
||||
}
|
||||
|
||||
/// Derive the public key file path from a config file path.
|
||||
pub fn pub_file_path(config_path: &Path) -> PathBuf {
|
||||
config_path
|
||||
@@ -307,7 +353,31 @@ pub fn resolve_identity(
|
||||
});
|
||||
}
|
||||
|
||||
// No key file yet — generate and persist
|
||||
// No key at the resolved location. Before generating a new identity,
|
||||
// check whether one is stranded at the legacy system config directory:
|
||||
// generating here would silently change the node's npub, routing
|
||||
// address and mesh IPv6.
|
||||
if let Some(legacy) = legacy_key_fallback(
|
||||
&key_path,
|
||||
Path::new(SYSTEM_CONFIG_DIR),
|
||||
Path::new(LEGACY_SYSTEM_CONFIG_DIR),
|
||||
) {
|
||||
let nsec = read_key_file(&legacy)?;
|
||||
let identity = Identity::from_secret_str(&nsec)?;
|
||||
tracing::warn!(
|
||||
legacy = %legacy.display(),
|
||||
current = %key_path.display(),
|
||||
"Identity key found at the legacy path but not at the current default; \
|
||||
using it so the node keeps its identity — move it to the current path"
|
||||
);
|
||||
let _ = write_pub_file(&pub_path, &identity.npub());
|
||||
return Ok(ResolvedIdentity {
|
||||
nsec,
|
||||
source: IdentitySource::KeyFile(legacy),
|
||||
});
|
||||
}
|
||||
|
||||
// No key file anywhere — generate and persist
|
||||
let identity = Identity::generate();
|
||||
let nsec = encode_nsec(&identity.keypair().secret_key());
|
||||
let npub = identity.npub();
|
||||
@@ -454,7 +524,8 @@ impl Config {
|
||||
/// Load configuration from the standard search paths.
|
||||
///
|
||||
/// Files are loaded in reverse priority order and merged:
|
||||
/// 1. `/etc/fips/fips.yaml` (loaded first, lowest priority)
|
||||
/// 1. `/etc/fips/fips.yaml` (and `/usr/local/etc/fips/fips.yaml` on macOS;
|
||||
/// loaded first, lowest priority)
|
||||
/// 2. `~/.config/fips/fips.yaml` (user config)
|
||||
/// 3. `./fips.yaml` (loaded last, highest priority)
|
||||
///
|
||||
@@ -549,9 +620,18 @@ impl Config {
|
||||
pub fn search_paths() -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
// System config (lowest priority)
|
||||
// System config — /etc/fips is always probed so existing installs
|
||||
// keep working after an upgrade.
|
||||
paths.push(PathBuf::from("/etc/fips").join(CONFIG_FILENAME));
|
||||
|
||||
// macOS packaging installs config under /usr/local/etc/fips
|
||||
// (Homebrew-style prefix); probe it after /etc/fips so the
|
||||
// packaged file wins over a stale /etc/fips leftover. Read from
|
||||
// SYSTEM_CONFIG_DIR rather than a second literal, so this path and
|
||||
// the directory `fipsctl keygen` writes into cannot drift apart.
|
||||
#[cfg(target_os = "macos")]
|
||||
paths.push(PathBuf::from(SYSTEM_CONFIG_DIR).join(CONFIG_FILENAME));
|
||||
|
||||
// User config directory
|
||||
if let Some(config_dir) = dirs::config_dir() {
|
||||
paths.push(config_dir.join("fips").join(CONFIG_FILENAME));
|
||||
@@ -1057,13 +1137,103 @@ node:
|
||||
// Should include current directory
|
||||
assert!(paths.iter().any(|p| p.ends_with("fips.yaml")));
|
||||
|
||||
// Should include /etc/fips on Unix
|
||||
#[cfg(unix)]
|
||||
// Should always include /etc/fips as a system config path
|
||||
assert!(
|
||||
paths
|
||||
.iter()
|
||||
.any(|p| p.starts_with("/etc/fips") && p.ends_with("fips.yaml"))
|
||||
);
|
||||
|
||||
// macOS should also include /usr/local/etc/fips
|
||||
#[cfg(target_os = "macos")]
|
||||
assert!(
|
||||
paths
|
||||
.iter()
|
||||
.any(|p| p.starts_with("/usr/local/etc/fips") && p.ends_with("fips.yaml"))
|
||||
);
|
||||
}
|
||||
|
||||
// --- legacy identity-key fallback ---
|
||||
//
|
||||
// Guards the regression the second system config directory introduces:
|
||||
// the key directory follows whichever config file loaded last, so a node
|
||||
// carrying config at both locations would find no key at the new one and
|
||||
// generate a fresh identity. These drive `legacy_key_fallback` with real
|
||||
// directories under a temp root rather than asserting on the constants,
|
||||
// and they run on every platform because the function takes both
|
||||
// directories as arguments.
|
||||
|
||||
fn write_stub_key(dir: &Path) -> PathBuf {
|
||||
std::fs::create_dir_all(dir).unwrap();
|
||||
let p = dir.join(KEY_FILENAME);
|
||||
std::fs::write(&p, "nsec1stub\n").unwrap();
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_key_is_adopted_when_the_new_location_has_none() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let legacy = root.path().join("etc/fips");
|
||||
let system = root.path().join("usr/local/etc/fips");
|
||||
std::fs::create_dir_all(&system).unwrap();
|
||||
let legacy_key = write_stub_key(&legacy);
|
||||
|
||||
let key_path = system.join(KEY_FILENAME);
|
||||
assert_eq!(
|
||||
legacy_key_fallback(&key_path, &system, &legacy),
|
||||
Some(legacy_key),
|
||||
"a key stranded at the legacy path must be adopted, not regenerated"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_key_at_the_new_location_wins_over_the_legacy_one() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let legacy = root.path().join("etc/fips");
|
||||
let system = root.path().join("usr/local/etc/fips");
|
||||
write_stub_key(&legacy);
|
||||
let key_path = write_stub_key(&system);
|
||||
|
||||
assert_eq!(legacy_key_fallback(&key_path, &system, &legacy), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_fallback_when_the_two_directories_are_the_same() {
|
||||
// The Linux case: nothing moved, so the fallback must be inert even
|
||||
// though a key exists at that one directory.
|
||||
let root = TempDir::new().unwrap();
|
||||
let dir = root.path().join("etc/fips");
|
||||
write_stub_key(&dir);
|
||||
let absent = dir.join("nonexistent").join(KEY_FILENAME);
|
||||
|
||||
assert_eq!(legacy_key_fallback(&absent, &dir, &dir), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_fallback_for_a_config_outside_the_system_directory() {
|
||||
// An operator running with ./fips.yaml or a user config must never be
|
||||
// silently redirected to a system key.
|
||||
let root = TempDir::new().unwrap();
|
||||
let legacy = root.path().join("etc/fips");
|
||||
let system = root.path().join("usr/local/etc/fips");
|
||||
let elsewhere = root.path().join("home/someone");
|
||||
std::fs::create_dir_all(&elsewhere).unwrap();
|
||||
write_stub_key(&legacy);
|
||||
|
||||
let key_path = elsewhere.join(KEY_FILENAME);
|
||||
assert_eq!(legacy_key_fallback(&key_path, &system, &legacy), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_fallback_when_the_legacy_location_is_empty_too() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let legacy = root.path().join("etc/fips");
|
||||
let system = root.path().join("usr/local/etc/fips");
|
||||
std::fs::create_dir_all(&legacy).unwrap();
|
||||
std::fs::create_dir_all(&system).unwrap();
|
||||
|
||||
let key_path = system.join(KEY_FILENAME);
|
||||
assert_eq!(legacy_key_fallback(&key_path, &system, &legacy), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+16
-1
@@ -2442,11 +2442,26 @@ mod tests {
|
||||
/// runtime state (no peers, links, sessions, transports, or cache
|
||||
/// entries). This keeps every per-element list empty and every
|
||||
/// scalar deterministic modulo `VOLATILE_KEYS`.
|
||||
///
|
||||
/// The peer ACL is isolated from the host filesystem: the reloader is
|
||||
/// repointed at non-existent paths under the process temp dir, so the
|
||||
/// snapshot is stable regardless of whether an operator has edited the
|
||||
/// real `peers.allow` / `peers.deny` on the dev/CI machine. Without this,
|
||||
/// `show_acl` would reflect the host's live ACL state (e.g. a populated
|
||||
/// `peers.deny` flips `effective_mode` from `default_open` to
|
||||
/// `denylist`), making the snapshot machine-dependent.
|
||||
fn build_test_node() -> Node {
|
||||
let identity =
|
||||
Identity::from_secret_bytes(&TEST_SEED).expect("test seed is a valid secret key");
|
||||
let config = Config::new();
|
||||
Node::with_identity(identity, config).expect("default config is valid")
|
||||
let mut node = Node::with_identity(identity, config).expect("default config is valid");
|
||||
let nonce = std::process::id();
|
||||
let tmp = std::env::temp_dir();
|
||||
node.isolate_peer_acl_for_test(
|
||||
tmp.join(format!("fips-snapshot-test-{nonce}.allow")),
|
||||
tmp.join(format!("fips-snapshot-test-{nonce}.deny")),
|
||||
);
|
||||
node
|
||||
}
|
||||
|
||||
/// Recursively walk a JSON value, replacing the value of any key
|
||||
|
||||
@@ -23,11 +23,52 @@ use std::time::SystemTime;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
/// Default path for the peer allow list.
|
||||
///
|
||||
/// On macOS the install layout (see `packaging/macos/`) ships config under
|
||||
/// `/usr/local/etc/fips/` rather than `/etc/fips/`; the default follows the
|
||||
/// platform's packaging so the daemon reads the file the operator was told
|
||||
/// to edit. Linux and other Unix keep the historic `/etc/fips/` location.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const DEFAULT_PEERS_ALLOW_PATH: &str = "/usr/local/etc/fips/peers.allow";
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub const DEFAULT_PEERS_ALLOW_PATH: &str = "/etc/fips/peers.allow";
|
||||
|
||||
/// Default path for the peer deny list.
|
||||
///
|
||||
/// See [`DEFAULT_PEERS_ALLOW_PATH`] for the macOS `/usr/local/etc/fips/`
|
||||
/// rationale.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const DEFAULT_PEERS_DENY_PATH: &str = "/usr/local/etc/fips/peers.deny";
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub const DEFAULT_PEERS_DENY_PATH: &str = "/etc/fips/peers.deny";
|
||||
|
||||
/// Warn about config files stranded at the pre-move default location.
|
||||
///
|
||||
/// The macOS defaults for `hosts`, `peers.allow` and `peers.deny` moved
|
||||
/// from `/etc/fips` to `/usr/local/etc/fips`, the directory the macOS
|
||||
/// packaging actually populates. The old location is no longer read by
|
||||
/// the default path constants, and a `peers.deny` silently left behind
|
||||
/// there would fail open (a missing deny list is not an error), so surface
|
||||
/// the situation loudly once at startup.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn warn_on_legacy_config_paths() {
|
||||
for (current, name) in [
|
||||
(crate::upper::hosts::DEFAULT_HOSTS_PATH, "hosts"),
|
||||
(DEFAULT_PEERS_ALLOW_PATH, "peers.allow"),
|
||||
(DEFAULT_PEERS_DENY_PATH, "peers.deny"),
|
||||
] {
|
||||
let legacy = format!("/etc/fips/{name}");
|
||||
if std::path::Path::new(&legacy).exists() && !std::path::Path::new(current).exists() {
|
||||
warn!(
|
||||
legacy = %legacy,
|
||||
current = %current,
|
||||
"Config file found at legacy path but not at the current default; \
|
||||
it is no longer read — move it to the current path"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of evaluating a peer against the ACL.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PeerAclDecision {
|
||||
@@ -451,6 +492,16 @@ impl Node {
|
||||
decision
|
||||
)))
|
||||
}
|
||||
|
||||
/// Test-only: replace the peer-ACL reloader with one that reads from
|
||||
/// the given paths, isolating the node from the host's real
|
||||
/// `peers.allow` / `peers.deny` files. Used by snapshot tests
|
||||
/// that must be deterministic regardless of whether an operator has
|
||||
/// edited the system ACL files on the dev/CI machine.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn isolate_peer_acl_for_test(&mut self, allow: PathBuf, deny: PathBuf) {
|
||||
self.peer_acl = PeerAclReloader::with_paths(allow, deny);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -487,6 +538,27 @@ mod tests {
|
||||
acl
|
||||
}
|
||||
|
||||
// Guard against the macOS path regression: the install layout
|
||||
// (`packaging/macos/`) ships config under `/usr/local/etc/fips/`, so the
|
||||
// default ACL paths must follow it, or `peers.allow`/`peers.deny` are
|
||||
// silently unread on macOS (see the `NotFound` no-op in `load_file`).
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn test_default_acl_paths_follow_macos_packaging_layout() {
|
||||
assert_eq!(DEFAULT_PEERS_ALLOW_PATH, "/usr/local/etc/fips/peers.allow");
|
||||
assert_eq!(DEFAULT_PEERS_DENY_PATH, "/usr/local/etc/fips/peers.deny");
|
||||
}
|
||||
|
||||
// Non-macOS Unix/Linux keeps the historic `/etc/fips/` location; this
|
||||
// runs on the Linux CI matrix and pins the value so a future refactor
|
||||
// can't silently drift it.
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
#[test]
|
||||
fn test_default_acl_paths_keep_etc_fips_layout() {
|
||||
assert_eq!(DEFAULT_PEERS_ALLOW_PATH, "/etc/fips/peers.allow");
|
||||
assert_eq!(DEFAULT_PEERS_DENY_PATH, "/etc/fips/peers.deny");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acl_decision_allowed_and_display() {
|
||||
assert!(PeerAclDecision::AllowList.allowed());
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
//! Bloom filters, coordinate caches, transports, links, and peers.
|
||||
|
||||
pub(crate) mod acl;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use acl::warn_on_legacy_config_paths;
|
||||
mod bloom;
|
||||
pub(crate) mod context;
|
||||
mod dataplane;
|
||||
|
||||
+28
-1
@@ -17,7 +17,15 @@ use std::time::SystemTime;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
/// Default path for the FIPS hosts file.
|
||||
#[cfg(unix)]
|
||||
///
|
||||
/// On macOS the install layout (see `packaging/macos/`) ships config under
|
||||
/// `/usr/local/etc/fips/`, not `/etc/fips/`; the default follows the
|
||||
/// platform's packaging so aliases edited by the operator are actually
|
||||
/// loaded. Linux and other Unix keep the historic `/etc/fips/` location;
|
||||
/// Windows uses `%ProgramData%`.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const DEFAULT_HOSTS_PATH: &str = "/usr/local/etc/fips/hosts";
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
pub const DEFAULT_HOSTS_PATH: &str = "/etc/fips/hosts";
|
||||
#[cfg(windows)]
|
||||
pub const DEFAULT_HOSTS_PATH: &str = r"C:\ProgramData\fips\hosts";
|
||||
@@ -311,6 +319,25 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::Identity;
|
||||
|
||||
// --- default path tests ---
|
||||
|
||||
// Guard against the macOS path regression: the install layout
|
||||
// (`packaging/macos/`) ships config under `/usr/local/etc/fips/`, so the
|
||||
// default hosts path must follow it, or host-file aliases are silently
|
||||
// unloaded on macOS (see the `NotFound` no-op in `load_hosts_file`).
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn test_default_hosts_path_follows_macos_packaging_layout() {
|
||||
assert_eq!(DEFAULT_HOSTS_PATH, "/usr/local/etc/fips/hosts");
|
||||
}
|
||||
|
||||
// Non-macOS Unix/Linux keeps the historic `/etc/fips/` location.
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
#[test]
|
||||
fn test_default_hosts_path_keeps_etc_fips_layout() {
|
||||
assert_eq!(DEFAULT_HOSTS_PATH, "/etc/fips/hosts");
|
||||
}
|
||||
|
||||
// --- validate_hostname tests ---
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user