trippy_privilege/
lib.rs

1//! Discover platform privileges.
2//!
3//! A cross-platform library to discover and manage platform privileges needed
4//! for sending ICMP packets via RAW and `IPPROTO_ICMP` sockets.
5//!
6//! [`Privilege::acquire_privileges`]:
7//!
8//! - On Linux we check if `CAP_NET_RAW` is in the permitted set and if so raise it to the effective
9//!   set
10//! - On other Unix platforms this is a no-op
11//! - On Windows this is a no-op
12//!
13//! [`Privilege::has_privileges`] (obtained via [`Privilege::discover`]):
14//!
15//! - On Linux we check if `CAP_NET_RAW` is in the effective set
16//! - On other Unix platforms we check that the effective user is root
17//! - On Windows we check if the current process has an elevated token
18//!
19//! [`Privilege::needs_privileges`] (obtained via [`Privilege::discover`]):
20//!
21//! - On macOS we do not always need privileges to send ICMP packets as we can use `IPPROTO_ICMP`
22//!   sockets with the `IP_HDRINCL` socket option.
23//! - On Linux we always need privileges to send ICMP packets even though it supports the
24//!   `IPPROTO_ICMP` socket type but not the `IP_HDRINCL` socket option
25//! - On Windows we always need privileges to send ICMP packets
26//!
27//! [`Privilege::drop_privileges`]:
28//!
29//! - On Linux we clear the effective set
30//! - On other Unix platforms this is a no-op
31//! - On Windows this is a no-op
32//!
33//! # Examples
34//!
35//! Acquire the required privileges if we can:
36//!
37//! ```rust
38//! # fn main() -> anyhow::Result<()> {
39//! # use trippy_privilege::Privilege;
40//! let privilege = Privilege::acquire_privileges()?;
41//! if privilege.has_privileges() {
42//!     println!("You have the required privileges for raw sockets");
43//! } else {
44//!     println!("You do not have the required privileges for raw sockets");
45//! }
46//! if privilege.needs_privileges() {
47//!     println!("You always need privileges to send ICMP packets.");
48//! } else {
49//!     println!("You do not always need privileges to send ICMP packets.");
50//! }
51//! # Ok(())
52//! # }
53//! ```
54//!
55//! Discover the current privileges:
56//!
57//! ```rust
58//! # fn main() -> anyhow::Result<()> {
59//! # use trippy_privilege::Privilege;
60//! let privilege = Privilege::discover()?;
61//! if privilege.has_privileges() {
62//!     println!("You have the required privileges for raw sockets");
63//! } else {
64//!     println!("You do not have the required privileges for raw sockets");
65//! }
66//! if privilege.needs_privileges() {
67//!     println!("You always need privileges to send ICMP packets.");
68//! } else {
69//!     println!("You do not always need privileges to send ICMP packets.");
70//! }
71//! # Ok(())
72//! # }
73//! ```
74//!
75//! Drop all privileges:
76//!
77//! ```rust
78//! # fn main() -> anyhow::Result<()> {
79//! # use trippy_privilege::Privilege;
80//! Privilege::drop_privileges()?;
81//! # Ok(())
82//! # }
83//! ```
84
85/// A privilege error result.
86pub type Result<T> = std::result::Result<T, Error>;
87
88/// A privilege error.
89#[derive(thiserror::Error, Debug)]
90pub enum Error {
91    #[cfg(target_os = "linux")]
92    #[error("caps error: {0}")]
93    CapsError(#[from] caps::errors::CapsError),
94    #[cfg(windows)]
95    #[error("OpenProcessToken failed")]
96    OpenProcessTokenError,
97    #[cfg(windows)]
98    #[error("GetTokenInformation failed")]
99    GetTokenInformationError,
100}
101
102/// Run-time platform privilege information.
103#[derive(Debug)]
104pub struct Privilege {
105    has_privileges: bool,
106    needs_privileges: bool,
107}
108
109impl Privilege {
110    /// Discover information about the platform privileges.
111    pub fn discover() -> Result<Self> {
112        let has_privileges = Self::check_has_privileges()?;
113        let needs_privileges = Self::check_needs_privileges();
114        Ok(Self {
115            has_privileges,
116            needs_privileges,
117        })
118    }
119
120    /// Create a new Privilege instance.
121    #[must_use]
122    pub const fn new(has_privileges: bool, needs_privileges: bool) -> Self {
123        Self {
124            has_privileges,
125            needs_privileges,
126        }
127    }
128
129    /// Are we running with the privileges required for raw sockets?
130    #[must_use]
131    pub const fn has_privileges(&self) -> bool {
132        self.has_privileges
133    }
134
135    /// Does our platform always need privileges for `ICMP`?
136    ///
137    /// Specifically, each platform requires privileges unless it supports the `IPPROTO_ICMP` socket
138    /// type which _also_ allows the `IP_HDRINCL` socket option to be set.
139    #[must_use]
140    pub const fn needs_privileges(&self) -> bool {
141        self.needs_privileges
142    }
143
144    // Linux
145
146    #[cfg(target_os = "linux")]
147    /// Acquire privileges, if possible.
148    ///
149    /// Check if `CAP_NET_RAW` is in the permitted set and if so raise it to the effective set.
150    pub fn acquire_privileges() -> Result<Self> {
151        if caps::has_cap(None, caps::CapSet::Permitted, caps::Capability::CAP_NET_RAW)? {
152            caps::raise(None, caps::CapSet::Effective, caps::Capability::CAP_NET_RAW)?;
153        }
154        Self::discover()
155    }
156
157    #[cfg(target_os = "linux")]
158    /// Do we have the required privileges?
159    ///
160    /// Check if `CAP_NET_RAW` is in the effective set.
161    fn check_has_privileges() -> Result<bool> {
162        Ok(caps::has_cap(
163            None,
164            caps::CapSet::Effective,
165            caps::Capability::CAP_NET_RAW,
166        )?)
167    }
168
169    #[cfg(target_os = "linux")]
170    /// Drop all privileges.
171    ///
172    /// Clears the effective set.
173    pub fn drop_privileges() -> Result<()> {
174        caps::clear(None, caps::CapSet::Effective)?;
175        Ok(())
176    }
177
178    // Unix (excl. Linux)
179
180    #[cfg(all(unix, not(target_os = "linux")))]
181    #[allow(clippy::unnecessary_wraps)]
182    /// Acquire privileges, if possible.
183    ///
184    /// This is a no-op on non-Linux unix systems.
185    pub fn acquire_privileges() -> Result<Self> {
186        Self::discover()
187    }
188
189    #[cfg(all(unix, not(target_os = "linux")))]
190    #[allow(clippy::unnecessary_wraps)]
191    /// Do we have the required privileges?
192    ///
193    /// Checks if the effective user is root.
194    fn check_has_privileges() -> Result<bool> {
195        Ok(nix::unistd::Uid::effective().is_root())
196    }
197
198    #[cfg(all(unix, not(target_os = "linux")))]
199    #[allow(clippy::unnecessary_wraps)]
200    /// Drop all privileges.
201    ///
202    /// This is a no-op on non-Linux unix systems.
203    pub const fn drop_privileges() -> Result<()> {
204        Ok(())
205    }
206
207    // Unix (excl. macOS)
208
209    #[cfg(all(unix, not(target_os = "macos")))]
210    /// Does the platform always require privileges?
211    ///
212    /// Whilst Linux supports the `IPPROTO_ICMP` socket type, it does not allow using it with the
213    /// `IP_HDRINCL` socket option and is therefore not supported.  This may be supported in the
214    /// future.
215    ///
216    /// `NetBSD`, `OpenBSD` and `FreeBSD` do not support `IPPROTO_ICMP`.
217    const fn check_needs_privileges() -> bool {
218        true
219    }
220
221    // macOS
222
223    #[cfg(target_os = "macos")]
224    /// Does the platform always require privileges?
225    ///
226    /// `macOS` supports both privileged and unprivileged modes.
227    const fn check_needs_privileges() -> bool {
228        false
229    }
230
231    // Windows
232
233    #[cfg(windows)]
234    #[allow(clippy::unnecessary_wraps)]
235    /// Acquire privileges, if possible.
236    ///
237    /// This is a no-op on `Windows`.
238    pub fn acquire_privileges() -> Result<Self> {
239        Self::discover()
240    }
241
242    #[cfg(windows)]
243    #[allow(clippy::unnecessary_wraps)]
244    /// Do we have the required privileges?
245    ///
246    /// Check if the current process has an elevated token.
247    fn check_has_privileges() -> Result<bool> {
248        macro_rules! syscall {
249            ($p: path, $fn: ident ( $($arg: expr),* $(,)* ) ) => {{
250                #[allow(unsafe_code)]
251                unsafe { paste::paste!(windows_sys::Win32::$p::$fn) ($($arg, )*) }
252            }};
253        }
254
255        /// Window elevated privilege checker.
256        pub struct Privileged {
257            handle: windows_sys::Win32::Foundation::HANDLE,
258        }
259
260        impl Privileged {
261            /// Create a new `ElevationChecker` for the current process.
262            pub fn current_process() -> Result<Self> {
263                use windows_sys::Win32::Security::TOKEN_QUERY;
264                let mut handle: windows_sys::Win32::Foundation::HANDLE = 0;
265                let current_process = syscall!(System::Threading, GetCurrentProcess());
266                let res = syscall!(
267                    System::Threading,
268                    OpenProcessToken(current_process, TOKEN_QUERY, std::ptr::addr_of_mut!(handle))
269                );
270                if res == 0 {
271                    Err(Error::OpenProcessTokenError)
272                } else {
273                    Ok(Self { handle })
274                }
275            }
276
277            /// Check if the current process has elevated privileged.
278            pub fn is_elevated(&self) -> Result<bool> {
279                use windows_sys::Win32::Security::TokenElevation;
280                use windows_sys::Win32::Security::TOKEN_ELEVATION;
281                let mut elevation = TOKEN_ELEVATION { TokenIsElevated: 0 };
282                #[allow(clippy::cast_possible_truncation)]
283                let size = std::mem::size_of::<TOKEN_ELEVATION>() as u32;
284                let mut ret_size = 0u32;
285                let ret = syscall!(
286                    Security,
287                    GetTokenInformation(
288                        self.handle,
289                        TokenElevation,
290                        std::ptr::addr_of_mut!(elevation).cast(),
291                        size,
292                        std::ptr::addr_of_mut!(ret_size),
293                    )
294                );
295                if ret == 0 {
296                    Err(Error::GetTokenInformationError)
297                } else {
298                    Ok(elevation.TokenIsElevated != 0)
299                }
300            }
301        }
302
303        impl Drop for Privileged {
304            fn drop(&mut self) {
305                if self.handle != 0 {
306                    syscall!(Foundation, CloseHandle(self.handle));
307                }
308            }
309        }
310        Privileged::current_process()?.is_elevated()
311    }
312
313    #[cfg(windows)]
314    #[allow(clippy::unnecessary_wraps)]
315    /// Drop all capabilities.
316    ///
317    /// This is a no-op on `Windows`.
318    pub const fn drop_privileges() -> Result<()> {
319        Ok(())
320    }
321
322    #[cfg(target_os = "windows")]
323    /// Does the platform always require privileges?
324    ///
325    /// Privileges are always required on `Windows`.
326    const fn check_needs_privileges() -> bool {
327        true
328    }
329}