feat(cli): auto-NAT + privilege drop + Windows named-pipe admin

Three v2-hardening features in aura-cli, one pass:

- nat::NatGuard: RAII auto-config of IP forwarding + MASQUERADE on server
  startup. Linux (sysctl ip_forward + iptables -t nat MASQUERADE) and
  macOS (sysctl ip.forwarding + pfctl with /tmp/aura-nat.conf). dry_run
  works on every platform (logs "would run: ..."). Reverts everything in
  Drop. New [server.nat] {auto, egress_iface, dry_run}; absent section =
  back-compat no-op. Removes v1's "manual NAT/forwarding" step.
- privdrop::drop_to_user: drop euid/gid after binding TUN + privileged
  ports. Linux setresuid/setresgid, macOS setgid+setuid (permanent drop),
  Windows no-op with warning. New [server] / [client] run_as = "..."
  (optional). Skipped with info-log if already non-root.
- admin: split transport into cfg(unix) Unix-socket and cfg(windows) Tokio
  named-pipe modules sharing one JSON-line serve/request flow over
  AsyncRead/AsyncWrite. DEFAULT_SOCKET = "/tmp/aura-admin.sock" on Unix,
  r"\\.\pipe\aura-admin" on Windows. Removes v1's "admin Unix-only".

Deps: nix 0.29 user feature under [target.'cfg(unix)'.dependencies] (cli-
local, not workspace). Workspace: 155 tests passed (+13), clippy -D warnings
clean, fmt clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xah30
2026-05-27 02:09:38 +03:00
parent 821f7711e7
commit c6f0d7af9b
14 changed files with 1214 additions and 73 deletions
+135
View File
@@ -0,0 +1,135 @@
//! Privilege drop: switch the process's effective + real + saved UID/GID to a non-root user after
//! all privileged startup work is done (binding the TUN, binding low ports, configuring NAT).
//!
//! v1 spec from project notes:
//!
//! * Linux uses `setresgid(g,g,g)` + `setresuid(u,u,u)` (the full BSD-incompatible API; this also
//! wipes the saved set-uid so the process can never `setuid(0)` back).
//! * macOS does not expose `setresuid` in its BSD ABI — `nix` 0.29 provides `setgid` / `setuid`
//! instead, which on macOS perform a permanent drop when the calling process is root.
//! * Windows is a no-op (named pipes + service accounts cover the analogous use case there).
//!
//! The drop is **best-effort**:
//!
//! * If the current euid is not 0 (e.g. dev running `cargo test` as themselves), [`drop_to_user`]
//! logs an info line and returns `Ok` without changing anything.
//! * If the named user does not exist or the syscalls fail, the error bubbles up so `aura server`
//! exits rather than silently continuing as root.
//!
//! Callers **must** invoke [`drop_to_user`] after every privileged operation completes; doing it
//! earlier means the TUN open or NAT command may fail with EPERM.
use anyhow::Result;
#[cfg(unix)]
use anyhow::{anyhow, Context};
/// Drop privileges to `username`. See module docs for platform behaviour.
///
/// Returns `Ok(())` on:
/// * a successful drop (Linux/macOS, called as root),
/// * a no-op on Windows,
/// * a no-op when already running as a non-root user (Linux/macOS).
///
/// Returns `Err` only on Linux/macOS, when the user lookup or the syscalls themselves fail.
#[cfg(unix)]
pub fn drop_to_user(username: &str) -> Result<()> {
use nix::unistd::{getuid, User};
if !getuid().is_root() {
tracing::info!(
target: "aura::privdrop",
user = username,
"privilege drop skipped: already running as a non-root user"
);
return Ok(());
}
let user = User::from_name(username)
.with_context(|| format!("looking up user '{username}'"))?
.ok_or_else(|| anyhow!("user '{username}' not found in passwd database"))?;
let uid = user.uid;
let gid = user.gid;
// Order matters: drop GID first while we still have root, then UID. Doing UID first would
// leave us as a non-root user that cannot setgid anymore.
drop_uid_gid(uid, gid)?;
tracing::info!(
target: "aura::privdrop",
user = username,
uid = uid.as_raw(),
gid = gid.as_raw(),
"dropped privileges"
);
Ok(())
}
/// Windows stub: there is no analogous "drop to user" syscall sequence on Windows in v1. The
/// server is expected to be run as a configured service account.
#[cfg(windows)]
pub fn drop_to_user(username: &str) -> Result<()> {
tracing::warn!(
target: "aura::privdrop",
user = username,
"privilege drop not implemented on Windows; run aura server as a low-privilege service account instead"
);
Ok(())
}
#[cfg(target_os = "linux")]
fn drop_uid_gid(uid: nix::unistd::Uid, gid: nix::unistd::Gid) -> Result<()> {
use nix::unistd::{setresgid, setresuid};
// Full triple-drop on Linux: real + effective + saved. This guarantees the process cannot
// regain root via setuid(0).
setresgid(gid, gid, gid).with_context(|| format!("setresgid({})", gid.as_raw()))?;
setresuid(uid, uid, uid).with_context(|| format!("setresuid({})", uid.as_raw()))?;
Ok(())
}
#[cfg(target_os = "macos")]
fn drop_uid_gid(uid: nix::unistd::Uid, gid: nix::unistd::Gid) -> Result<()> {
use nix::unistd::{setgid, setuid};
// macOS does not expose setresuid in its BSD ABI. setgid/setuid perform a permanent drop
// when invoked as root: the kernel zeroes the saved set-uid alongside the real and effective
// ids, so this is just as strong as setresuid here.
setgid(gid).with_context(|| format!("setgid({})", gid.as_raw()))?;
setuid(uid).with_context(|| format!("setuid({})", uid.as_raw()))?;
Ok(())
}
// Other unix targets (BSDs, etc.) — fall back to setgid/setuid which exist everywhere POSIX.
#[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
fn drop_uid_gid(uid: nix::unistd::Uid, gid: nix::unistd::Gid) -> Result<()> {
use nix::unistd::{setgid, setuid};
setgid(gid).with_context(|| format!("setgid({})", gid.as_raw()))?;
setuid(uid).with_context(|| format!("setuid({})", uid.as_raw()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
/// On a developer/CI host this test runs as a non-root user, so drop_to_user must be a
/// no-op (no panic, no error, no actual privilege change). Verifies the early-return path.
#[test]
#[cfg(unix)]
fn no_op_when_already_non_root() {
// Use "nobody" as the requested user — it exists on every Unix CI image but we should
// never actually call setuid on it because we are not root.
let res = drop_to_user("nobody");
// If running as root (unlikely in CI), the test still completes successfully — the call
// would actually drop us. Either way, the function returns Ok.
assert!(
res.is_ok(),
"drop_to_user should return Ok on a non-root host, got: {res:?}"
);
}
#[test]
#[cfg(windows)]
fn windows_is_a_noop() {
// Windows: always Ok, regardless of username.
assert!(drop_to_user("any").is_ok());
}
}