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}