srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/include/wireframe/privileges.hpp
diff options
context:
space:
mode:
Diffstat (limited to 'include/wireframe/privileges.hpp')
-rw-r--r--include/wireframe/privileges.hpp87
1 files changed, 87 insertions, 0 deletions
diff --git a/include/wireframe/privileges.hpp b/include/wireframe/privileges.hpp
new file mode 100644
index 0000000..69df725
--- /dev/null
+++ b/include/wireframe/privileges.hpp
@@ -0,0 +1,87 @@
+#pragma once
+
+#ifndef _WIN32
+#include <grp.h>
+#include <unistd.h>
+#endif
+
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <optional>
+#include <string>
+
+// After pcap_open_live() succeeds, the process has gotten everything
+// CAP_NET_RAW exists for - running the rest of the program (decoding
+// untrusted packet bytes, an interactive TUI/GUI event loop) as root
+// from that point on is unnecessary exposure, and PLAN.md says as much
+// directly: "Drop privileges immediately after opening the capture
+// handle; use CAP_NET_RAW via file capabilities instead of running as
+// root."
+//
+// The recommended path doesn't need this file at all: run
+// `sudo setcap cap_net_raw+ep <binary>` once, then invoke the binary
+// directly, unprivileged, forever after - CAP_NET_RAW alone is enough
+// for pcap_open_live(), no root required at any point. This exists for
+// the case someone still runs the binary via sudo (out of habit, or
+// because setcap isn't available/permitted in some environments): drop
+// straight back to the invoking user immediately, so the rest of the
+// process's lifetime - including any -w output file, which then ends
+// up owned by that user instead of root - runs unprivileged either way.
+namespace wireframe {
+
+// Drops from root to the user who actually invoked the program, via
+// sudo's SUDO_UID/SUDO_GID (which sudo always sets). A no-op if not
+// currently root, or if SUDO_UID isn't set (e.g. a genuine root login,
+// not sudo - there's no "real" user to drop to in that case).
+//
+// setuid() to a nonzero UID also clears the process's Linux capability
+// sets as a kernel-level side effect, so this covers both "running as
+// root via sudo" and "root's own CAP_NET_RAW" the same way, without a
+// separate libcap dependency.
+//
+// Returns an error message on failure. The drop is safety-critical: a
+// failure here should be treated as fatal by the caller, not silently
+// ignored while the process keeps running as root.
+inline std::optional<std::string> drop_privileges_if_root() {
+#ifdef _WIN32
+ return std::nullopt; // no POSIX privilege model to drop from
+#else
+ if (geteuid() != 0) return std::nullopt; // already unprivileged
+
+ const char* sudo_uid = std::getenv("SUDO_UID");
+ const char* sudo_gid = std::getenv("SUDO_GID");
+ if (sudo_uid == nullptr || sudo_gid == nullptr) {
+ return std::nullopt; // no safe target to drop to
+ }
+
+ uid_t target_uid = static_cast<uid_t>(std::strtoul(sudo_uid, nullptr, 10));
+ gid_t target_gid = static_cast<gid_t>(std::strtoul(sudo_gid, nullptr, 10));
+
+ // Order matters: groups and GID need root to change, so they must
+ // be dropped before UID - once UID is gone, so is the privilege
+ // to change the others.
+ if (setgroups(1, &target_gid) == -1) {
+ return "setgroups failed: " + std::string(std::strerror(errno));
+ }
+ if (setgid(target_gid) == -1) {
+ return "setgid failed: " + std::string(std::strerror(errno));
+ }
+ if (setuid(target_uid) == -1) {
+ return "setuid failed: " + std::string(std::strerror(errno));
+ }
+
+ // Defense in depth (standard advice from setuid-privilege-drop
+ // write-ups): confirm root can't be reclaimed. If the saved-UID
+ // was somehow left at 0, this would succeed and silently undo the
+ // drop - so a *successful* setuid(0) here means something is
+ // wrong, and is treated as the failure case.
+ if (setuid(0) != -1) {
+ return "failed to permanently drop root (setuid(0) unexpectedly succeeded)";
+ }
+
+ return std::nullopt;
+#endif
+}
+
+} // namespace wireframe