Skip to main content

p0f_rs/
lib.rs

1use std::{
2    array::TryFromSliceError,
3    io::{self, Read as _, Write as _},
4    net::IpAddr,
5    os::unix::net::UnixStream,
6    path::Path,
7    time::Duration,
8};
9
10use chrono::{DateTime, Utc};
11use thiserror::Error;
12
13const REQUEST_MAGIC: u32 = 0x50304601;
14const RESPONSE_MAGIC: u32 = 0x50304602;
15
16const REQUEST_SIZE: usize = 21;
17#[cfg(not(feature = "p0f-mtu"))]
18const RESPONSE_SIZE: usize = 232;
19#[cfg(feature = "p0f-mtu")]
20const RESPONSE_SIZE: usize = 234;
21
22const STR_MAX: usize = 31;
23const STR_SIZE: usize = STR_MAX + 1;
24
25const STATUS_BADQUERY: u32 = 0x00;
26const STATUS_OK: u32 = 0x10;
27const STATUS_NOMATCH: u32 = 0x20;
28
29const ADDRESS_IPV4: u8 = 0x04;
30const ADDRESS_IPV6: u8 = 0x06;
31
32const MATCH_NORMAL: u8 = 0x00;
33const MATCH_FUZZY: u8 = 0x01;
34const MATCH_GENERIC: u8 = 0x02;
35const MATCH_FUZZY_GENERIC: u8 = 0x03;
36
37#[derive(Debug, Error)]
38pub enum Error {
39    #[error("io error: {0}")]
40    Io(#[from] io::Error),
41    #[error("invalid magic")]
42    InvalidMagic,
43    #[error("bad query")]
44    BadQuery,
45    #[error("timestamp out of range: {0}")]
46    TimestampOutOfRange(&'static str),
47    #[error("missing data: {0}")]
48    MissingData(&'static str),
49    #[error("invalid data: {0}")]
50    InvalidData(#[from] TryFromSliceError),
51}
52
53pub struct P0f(UnixStream);
54
55impl P0f {
56    pub fn new<T: AsRef<Path>>(path: T) -> io::Result<Self> {
57        let socket = UnixStream::connect(path)?;
58
59        Ok(P0f(socket))
60    }
61
62    pub fn query<T: Into<IpAddr>>(&mut self, address: T) -> Result<Option<Response>, Error> {
63        let address = address.into();
64
65        let mut request = Vec::with_capacity(REQUEST_SIZE);
66        request.extend_from_slice(&REQUEST_MAGIC.to_ne_bytes());
67
68        match address {
69            IpAddr::V4(address) => {
70                request.push(ADDRESS_IPV4);
71                request.extend_from_slice(&address.octets());
72                request.extend_from_slice(&[0; 12]);
73            }
74            IpAddr::V6(address) => {
75                request.push(ADDRESS_IPV6);
76                request.extend_from_slice(&address.octets());
77            }
78        }
79
80        self.0.write_all(&request)?;
81        let mut response = [0; RESPONSE_SIZE];
82        self.0.read_exact(&mut response)?;
83        let mut response = BufferReader::new(&response);
84
85        let magic = u32::from_ne_bytes(*response.read_array().ok_or(Error::MissingData("magic"))?);
86        if magic != RESPONSE_MAGIC {
87            return Err(Error::InvalidMagic);
88        }
89        let status =
90            u32::from_ne_bytes(*response.read_array().ok_or(Error::MissingData("status"))?);
91        match status {
92            STATUS_BADQUERY => return Err(Error::BadQuery),
93            STATUS_OK => {}
94            STATUS_NOMATCH => return Ok(None),
95            _ => unreachable!(),
96        }
97
98        let first_seen = DateTime::from_timestamp(
99            u32::from_ne_bytes(
100                *response
101                    .read_array()
102                    .ok_or(Error::MissingData("first_seen"))?,
103            ) as i64,
104            0,
105        )
106        .ok_or(Error::TimestampOutOfRange("first_seen"))?;
107        let last_seen = DateTime::from_timestamp(
108            u32::from_ne_bytes(
109                *response
110                    .read_array()
111                    .ok_or(Error::MissingData("last_seen"))?,
112            ) as i64,
113            0,
114        )
115        .ok_or(Error::TimestampOutOfRange("last_seen"))?;
116        let total_conn = u32::from_ne_bytes(
117            *response
118                .read_array()
119                .ok_or(Error::MissingData("total_conn"))?,
120        );
121
122        let uptime_min = match u32::from_ne_bytes(
123            *response
124                .read_array()
125                .ok_or(Error::MissingData("uptime_min"))?,
126        ) {
127            0 => None,
128            uptime => Some(Duration::from_secs(uptime as u64 * 60)),
129        };
130        let up_mod_days = Duration::from_secs(
131            u32::from_ne_bytes(
132                *response
133                    .read_array()
134                    .ok_or(Error::MissingData("up_mod_days"))?,
135            ) as u64
136                * 86400,
137        );
138
139        let last_nat = match u32::from_ne_bytes(
140            *response
141                .read_array()
142                .ok_or(Error::MissingData("last_nat"))?,
143        ) {
144            0 => None,
145            last_nat => Some(
146                DateTime::from_timestamp(last_nat as i64, 0)
147                    .ok_or(Error::TimestampOutOfRange("last_seen"))?,
148            ),
149        };
150
151        let last_chg = match u32::from_ne_bytes(
152            *response
153                .read_array()
154                .ok_or(Error::MissingData("last_chg"))?,
155        ) {
156            0 => None,
157            last_chg => Some(
158                DateTime::from_timestamp(last_chg as i64, 0)
159                    .ok_or(Error::TimestampOutOfRange("last_chg"))?,
160            ),
161        };
162        let distance = match i16::from_ne_bytes(
163            *response
164                .read_array()
165                .ok_or(Error::MissingData("distance"))?,
166        ) {
167            -1 => None,
168            distance => Some(distance),
169        };
170
171        let bad_sw =
172            match u8::from_ne_bytes(*response.read_array().ok_or(Error::MissingData("bad_sw"))?) {
173                0 => None,
174                1 => Some(BadSw::OsDifference),
175                2 => Some(BadSw::OutrightMismatch),
176                d => {
177                    println!("bad_sw: {}", d);
178                    unreachable!();
179                }
180            };
181        let os_match_q = match u8::from_ne_bytes(
182            *response
183                .read_array()
184                .ok_or(Error::MissingData("os_match_q"))?,
185        ) {
186            MATCH_NORMAL => OsMatchQuality::Normal,
187            MATCH_FUZZY => OsMatchQuality::Fuzzy,
188            MATCH_GENERIC => OsMatchQuality::Generic,
189            MATCH_FUZZY_GENERIC => OsMatchQuality::FuzzyGeneric,
190            _ => unreachable!(),
191        };
192
193        let os_name = match response.get_buffer()[0] {
194            0 => None,
195            _ => Some(
196                String::from_utf8_lossy(
197                    &response
198                        .read_array::<STR_SIZE>()
199                        .ok_or(Error::MissingData("os_name"))?[..STR_SIZE],
200                )
201                .trim_end_matches('\0')
202                .to_string(),
203            ),
204        };
205
206        let os_flavor = match response.get_buffer()[0] {
207            0 => None,
208            _ => Some(
209                String::from_utf8_lossy(
210                    &response
211                        .read_array::<STR_SIZE>()
212                        .ok_or(Error::MissingData("os_flavor"))?[..STR_SIZE],
213                )
214                .trim_end_matches('\0')
215                .to_string(),
216            ),
217        };
218
219        let http_name = match response.get_buffer()[0] {
220            0 => None,
221            _ => Some(
222                String::from_utf8_lossy(
223                    &response
224                        .read_array::<STR_SIZE>()
225                        .ok_or(Error::MissingData("http_name"))?[..STR_SIZE],
226                )
227                .trim_end_matches('\0')
228                .to_string(),
229            ),
230        };
231
232        let http_flavor = match response.get_buffer()[0] {
233            0 => None,
234            _ => Some(
235                String::from_utf8_lossy(
236                    &response
237                        .read_array::<STR_SIZE>()
238                        .ok_or(Error::MissingData("http_flavor"))?[..STR_SIZE],
239                )
240                .trim_end_matches('\0')
241                .to_string(),
242            ),
243        };
244
245        #[cfg(feature = "p0f-mtu")]
246        let link_mtu = u16::from_ne_bytes(
247            *response
248                .read_array()
249                .ok_or(Error::MissingData("mtu"))?,
250        );
251
252        let link_type = match response.get_buffer()[0] {
253            0 => None,
254            _ => Some(
255                String::from_utf8_lossy(
256                    &response
257                        .read_array::<STR_SIZE>()
258                        .ok_or(Error::MissingData("link_type"))?[..STR_SIZE],
259                )
260                .trim_end_matches('\0')
261                .to_string(),
262            ),
263        };
264
265        let language = match response.get_buffer()[0] {
266            0 => None,
267            _ => Some(
268                String::from_utf8_lossy(
269                    &response
270                        .read_array::<STR_SIZE>()
271                        .ok_or(Error::MissingData("language"))?[..STR_SIZE],
272                )
273                .trim_end_matches('\0')
274                .to_string(),
275            ),
276        };
277
278        Ok(Some(Response {
279            first_seen,
280            last_seen,
281            total_conn,
282            uptime_min,
283            up_mod_days,
284            last_nat,
285            last_chg,
286            distance,
287            bad_sw,
288            os_match_q,
289            os_name,
290            os_flavor,
291            http_name,
292            http_flavor,
293            #[cfg(feature = "p0f-mtu")]
294            link_mtu,
295            link_type,
296            language,
297        }))
298    }
299}
300
301#[derive(Clone, Debug)]
302#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
303pub struct Response {
304    pub first_seen: DateTime<Utc>,
305    pub last_seen: DateTime<Utc>,
306    pub total_conn: u32,
307    pub uptime_min: Option<Duration>,
308    pub up_mod_days: Duration,
309    pub last_nat: Option<DateTime<Utc>>,
310    pub last_chg: Option<DateTime<Utc>>,
311    pub distance: Option<i16>,
312    pub bad_sw: Option<BadSw>,
313    pub os_match_q: OsMatchQuality,
314    pub os_name: Option<String>,
315    pub os_flavor: Option<String>,
316    pub http_name: Option<String>,
317    pub http_flavor: Option<String>,
318    #[cfg(feature = "p0f-mtu")]
319    pub link_mtu: u16,
320    pub link_type: Option<String>,
321    pub language: Option<String>,
322}
323
324#[derive(Clone, Debug)]
325#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
326pub enum BadSw {
327    OsDifference,
328    OutrightMismatch,
329}
330
331#[derive(Clone, Debug)]
332#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
333pub enum OsMatchQuality {
334    Normal,
335    Fuzzy,
336    Generic,
337    FuzzyGeneric,
338}
339
340struct BufferReader<'a> {
341    buffer: &'a [u8],
342    pos: usize,
343}
344
345impl<'a> BufferReader<'a> {
346    fn new(buffer: &'a [u8]) -> Self {
347        BufferReader { buffer, pos: 0 }
348    }
349
350    fn read_array<const N: usize>(&mut self) -> Option<&'a [u8; N]> {
351        if self.pos + N <= self.buffer.len() {
352            let slice = &self.buffer[self.pos..self.pos + N];
353            self.pos += N;
354            // SAFETY: We know that the slice has exactly N elements
355            Some(slice.try_into().unwrap())
356        } else {
357            None
358        }
359    }
360
361    fn get_buffer(&self) -> &'a [u8] {
362        &self.buffer[self.pos..]
363    }
364}