Skip to main content

dope_core/
platform.rs

1use std::error;
2use std::fmt::{self, Display, Formatter};
3use std::io::{Error, ErrorKind};
4
5use crate::backend;
6use crate::io::file::RawMetadata;
7
8pub trait Platform {
9    type Sqe;
10    type Gso;
11    type StatBuf;
12    type TimerSpec;
13    fn entropy() -> std::io::Result<[u64; 2]>;
14    fn parse_meta(raw: &Self::StatBuf) -> RawMetadata;
15    fn snapshot() -> std::io::Result<Snapshot>;
16}
17
18cfg_select! {
19    target_os = "linux" => {
20        use std::fs;
21        use std::mem::{MaybeUninit, size_of};
22
23        use crate::driver::Driver;
24
25        impl Platform for Driver {
26            type Sqe = backend::uring::sqe::Sqe;
27            type Gso = backend::uring::platform::gso::Gso;
28            type StatBuf = libc::statx;
29            type TimerSpec = io_uring::types::Timespec;
30
31            fn entropy() -> std::io::Result<[u64; 2]> {
32                let mut words = MaybeUninit::<[u64; 2]>::uninit();
33                let mut data = words.as_mut_ptr().cast::<u8>();
34                let mut len = size_of::<[u64; 2]>();
35                while len != 0 {
36                    // SAFETY: data/len stay inside the uninitialized words buffer.
37                    let written = unsafe { libc::getrandom(data.cast(), len, 0) };
38                    if written < 0 {
39                        let error = Error::last_os_error();
40                        if error.kind() == ErrorKind::Interrupted {
41                            continue;
42                        }
43                        return Err(error);
44                    }
45                    if written == 0 {
46                        return Err(Error::new(
47                            ErrorKind::UnexpectedEof,
48                            "kernel entropy returned no bytes",
49                        ));
50                    }
51                    let written = written as usize;
52                    // SAFETY: written <= len keeps the cursor inside the buffer.
53                    data = unsafe { data.add(written) };
54                    len -= written;
55                }
56                // SAFETY: the loop wrote every byte of words.
57                Ok(unsafe { words.assume_init() })
58            }
59
60            fn parse_meta(raw: &Self::StatBuf) -> RawMetadata {
61                RawMetadata {
62                    len: raw.stx_size,
63                    modified: Some((raw.stx_mtime.tv_sec, raw.stx_mtime.tv_nsec)),
64                    regular: u32::from(raw.stx_mode) & libc::S_IFMT == libc::S_IFREG,
65                }
66            }
67
68            fn snapshot() -> std::io::Result<Snapshot> {
69                Ok(Snapshot {
70                    syncookies: read_u32("/proc/sys/net/ipv4/tcp_syncookies")? != 0,
71                    max_syn_backlog: read_u32("/proc/sys/net/ipv4/tcp_max_syn_backlog")?,
72                    somaxconn: read_u32("/proc/sys/net/core/somaxconn")?,
73                    rlimit_nofile: OpenFileLimit::get()?.soft(),
74                })
75            }
76        }
77
78        fn read_u32(path: &str) -> std::io::Result<u32> {
79            let value = fs::read_to_string(path)?;
80            value
81                .trim()
82                .parse::<u32>()
83                .map_err(|error| Error::new(ErrorKind::InvalidData, format!("{path}: {error}")))
84        }
85    }
86    _ => {
87        use std::ffi::CStr;
88        use std::mem::{MaybeUninit, size_of};
89        use std::ptr::null_mut;
90
91        use crate::driver::Driver;
92
93        impl Platform for Driver {
94            type Sqe = backend::kqueue::sqe::Sqe;
95            type Gso = backend::kqueue::platform::gso::Gso;
96            type StatBuf = libc::stat;
97            type TimerSpec = backend::kqueue::sqe::TimerSpec;
98
99            fn entropy() -> std::io::Result<[u64; 2]> {
100                let mut words = MaybeUninit::<[u64; 2]>::uninit();
101                loop {
102                    // SAFETY: the pointer/length name the whole words buffer.
103                    if unsafe { libc::getentropy(words.as_mut_ptr().cast(), size_of::<[u64; 2]>()) } == 0 {
104                        // SAFETY: getentropy filled the buffer.
105                        return Ok(unsafe { words.assume_init() });
106                    }
107                    let error = Error::last_os_error();
108                    if error.kind() != ErrorKind::Interrupted {
109                        return Err(error);
110                    }
111                }
112            }
113
114            fn parse_meta(raw: &Self::StatBuf) -> RawMetadata {
115                RawMetadata {
116                    len: u64::try_from(raw.st_size).unwrap_or(0),
117                    modified: u32::try_from(raw.st_mtime_nsec)
118                        .ok()
119                        .filter(|nanos| *nanos < 1_000_000_000)
120                        .map(|nanos| (raw.st_mtime, nanos)),
121                    regular: (raw.st_mode as libc::mode_t) & libc::S_IFMT == libc::S_IFREG,
122                }
123            }
124
125            fn snapshot() -> std::io::Result<Snapshot> {
126                let somaxconn = sysctl_u32(c"kern.ipc.somaxconn")?;
127                Ok(Snapshot {
128                    syncookies: true,
129                    max_syn_backlog: somaxconn,
130                    somaxconn,
131                    rlimit_nofile: OpenFileLimit::get()?.soft(),
132                })
133            }
134        }
135
136        fn sysctl_u32(name: &CStr) -> std::io::Result<u32> {
137            let mut value: libc::c_int = 0;
138            let mut len = size_of::<libc::c_int>();
139            // SAFETY: value/len are live locals sized for the sysctl result.
140            let rc = unsafe {
141                libc::sysctlbyname(
142                    name.as_ptr(),
143                    (&mut value as *mut libc::c_int).cast(),
144                    &mut len,
145                    null_mut(),
146                    0,
147                )
148            };
149            if rc != 0 {
150                return Err(Error::last_os_error());
151            }
152            u32::try_from(value).map_err(|_| Error::new(ErrorKind::InvalidData, "negative sysctl"))
153        }
154    }
155}
156
157const SYN_BACKLOG_FLOOR: u32 = 4096;
158const SOMAXCONN_FLOOR: u32 = 4096;
159
160#[derive(Debug)]
161pub struct Snapshot {
162    pub syncookies: bool,
163    pub max_syn_backlog: u32,
164    pub somaxconn: u32,
165    pub rlimit_nofile: u64,
166}
167
168impl Snapshot {
169    pub fn check_slots(&self, requested_slots: u32) -> Result<(), Mismatch> {
170        if u64::from(requested_slots) > self.rlimit_nofile {
171            return Err(Mismatch::NoFileTooLow {
172                requested: requested_slots,
173                rlimit: self.rlimit_nofile,
174            });
175        }
176        if !self.syncookies && self.max_syn_backlog < SYN_BACKLOG_FLOOR {
177            return Err(Mismatch::SynFloodVulnerable {
178                backlog: self.max_syn_backlog,
179                expected: SYN_BACKLOG_FLOOR,
180            });
181        }
182        if self.somaxconn < SOMAXCONN_FLOOR {
183            return Err(Mismatch::SomaxconnTooLow {
184                requested: SOMAXCONN_FLOOR,
185                kernel: self.somaxconn,
186            });
187        }
188        Ok(())
189    }
190}
191
192#[derive(Debug)]
193pub enum Mismatch {
194    NoFileTooLow { requested: u32, rlimit: u64 },
195    SomaxconnTooLow { requested: u32, kernel: u32 },
196    SynFloodVulnerable { backlog: u32, expected: u32 },
197}
198
199impl Display for Mismatch {
200    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
201        match self {
202            Self::NoFileTooLow { requested, rlimit } => write!(
203                formatter,
204                "RLIMIT_NOFILE ({rlimit}) below configured fixed_file_slots ({requested}); raise the limit or lower the slot count"
205            ),
206            Self::SomaxconnTooLow { requested, kernel } => write!(
207                formatter,
208                "somaxconn ({kernel}) below required floor ({requested}); raise it to {requested}"
209            ),
210            Self::SynFloodVulnerable { backlog, expected } => write!(
211                formatter,
212                "SYN-flood vulnerable: syncookies off and max_syn_backlog ({backlog}) below floor ({expected})"
213            ),
214        }
215    }
216}
217
218impl error::Error for Mismatch {}
219
220impl From<Mismatch> for Error {
221    fn from(mismatch: Mismatch) -> Self {
222        Error::new(ErrorKind::InvalidInput, mismatch)
223    }
224}
225
226pub(crate) struct OpenFileLimit(libc::rlimit);
227
228impl OpenFileLimit {
229    pub(crate) fn get() -> std::io::Result<Self> {
230        let mut limit = libc::rlimit {
231            rlim_cur: 0,
232            rlim_max: 0,
233        };
234        let rc = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) };
235        if rc != 0 {
236            return Err(Error::last_os_error());
237        }
238        Ok(Self(limit))
239    }
240
241    #[allow(clippy::unnecessary_cast)]
242    pub(crate) fn soft(&self) -> u64 {
243        self.0.rlim_cur as u64
244    }
245
246    pub(crate) fn raise(mut self) -> std::io::Result<()> {
247        self.0.rlim_cur = self.0.rlim_max;
248        let rc = unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &self.0) };
249        if rc != 0 {
250            return Err(Error::last_os_error());
251        }
252        Ok(())
253    }
254}