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 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}