Merge branch 'maint'

This commit is contained in:
Johnathan Corgan
2026-08-09 13:41:03 +00:00
12 changed files with 412 additions and 12 deletions
+49
View File
@@ -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
+43
View File
@@ -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 |
+1 -1
View File
@@ -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 |
+1 -1
View File
@@ -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. |
+1 -1
View File
@@ -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 |
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+72
View File
@@ -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());
+2
View File
@@ -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
View File
@@ -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]