1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub enum OperatingSystem {
11 Windows(WindowsVersion),
12 Linux(LinuxDistro),
13 MacOS(String),
14 BSD(String),
15 Cisco,
16 Juniper,
17 Unknown,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub enum WindowsVersion {
23 Windows10,
24 Windows11,
25 WindowsServer2016,
26 WindowsServer2019,
27 WindowsServer2022,
28 Unknown,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33pub enum LinuxDistro {
34 Ubuntu,
35 Debian,
36 CentOS,
37 RedHat,
38 Alpine,
39 Unknown,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct OSFingerprint {
45 pub os: OperatingSystem,
46 pub confidence: f32,
47 pub ttl: Option<u8>,
48 pub window_size: Option<u16>,
49 pub mss: Option<u16>,
50 pub tcp_options: Vec<String>,
51 pub raw_fingerprint: String,
52}
53
54impl OSFingerprint {
55 pub fn os_name(&self) -> String {
57 match &self.os {
58 OperatingSystem::Windows(v) => format!("Windows ({:?})", v),
59 OperatingSystem::Linux(d) => format!("Linux ({:?})", d),
60 OperatingSystem::MacOS(v) => format!("macOS {}", v),
61 OperatingSystem::BSD(v) => format!("BSD {}", v),
62 OperatingSystem::Cisco => "Cisco IOS".to_string(),
63 OperatingSystem::Juniper => "Juniper JUNOS".to_string(),
64 OperatingSystem::Unknown => "Unknown".to_string(),
65 }
66 }
67
68 pub fn is_reliable(&self) -> bool {
70 self.confidence >= 0.7
71 }
72}
73
74pub struct OSSignatures {
76 signatures: HashMap<String, (OperatingSystem, f32)>,
77}
78
79impl OSSignatures {
80 pub fn new() -> Self {
82 let mut signatures = HashMap::new();
83
84 signatures.insert(
86 "ttl:128:win:65535".to_string(),
87 (OperatingSystem::Windows(WindowsVersion::Windows10), 0.85),
88 );
89 signatures.insert(
90 "ttl:128:win:8192".to_string(),
91 (OperatingSystem::Windows(WindowsVersion::WindowsServer2019), 0.8),
92 );
93
94 signatures.insert(
96 "ttl:64:mss:1460".to_string(),
97 (OperatingSystem::Linux(LinuxDistro::Ubuntu), 0.75),
98 );
99 signatures.insert(
100 "ttl:64:mss:1380".to_string(),
101 (OperatingSystem::Linux(LinuxDistro::CentOS), 0.7),
102 );
103
104 signatures.insert(
106 "ttl:64:win:65535:sack".to_string(),
107 (OperatingSystem::MacOS("Unknown".to_string()), 0.7),
108 );
109
110 signatures.insert(
112 "ttl:255:cisco".to_string(),
113 (OperatingSystem::Cisco, 0.9),
114 );
115
116 Self { signatures }
117 }
118
119 pub fn lookup(&self, fingerprint: &str) -> Option<&(OperatingSystem, f32)> {
121 self.signatures.get(fingerprint)
122 }
123
124 pub fn add_signature(&mut self, fingerprint: String, os: OperatingSystem, confidence: f32) {
126 self.signatures.insert(fingerprint, (os, confidence));
127 }
128}
129
130impl Default for OSSignatures {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136pub struct OSDetector {
138 signatures: OSSignatures,
139}
140
141impl OSDetector {
142 pub fn new() -> Self {
144 Self {
145 signatures: OSSignatures::new(),
146 }
147 }
148
149 pub fn detect_from_ttl(&self, ttl: u8) -> OSFingerprint {
151 let (os, confidence) = match ttl {
152 128 => (OperatingSystem::Windows(WindowsVersion::Unknown), 0.7),
153 64 => (OperatingSystem::Linux(LinuxDistro::Unknown), 0.6),
154 255 => (OperatingSystem::Cisco, 0.8),
155 _ => (OperatingSystem::Unknown, 0.3),
156 };
157
158 OSFingerprint {
159 os,
160 confidence,
161 ttl: Some(ttl),
162 window_size: None,
163 mss: None,
164 tcp_options: Vec::new(),
165 raw_fingerprint: format!("ttl:{}", ttl),
166 }
167 }
168
169 pub fn detect_from_tcp(&self, ttl: u8, window_size: u16, mss: Option<u16>) -> OSFingerprint {
171 let mut fingerprint_parts = vec![format!("ttl:{}", ttl)];
173
174 if window_size == 65535 {
175 fingerprint_parts.push("win:65535".to_string());
176 } else if window_size == 8192 {
177 fingerprint_parts.push("win:8192".to_string());
178 }
179
180 if let Some(mss_val) = mss {
181 fingerprint_parts.push(format!("mss:{}", mss_val));
182 }
183
184 let fingerprint = fingerprint_parts.join(":");
185
186 if let Some((os, confidence)) = self.signatures.lookup(&fingerprint) {
188 return OSFingerprint {
189 os: os.clone(),
190 confidence: *confidence,
191 ttl: Some(ttl),
192 window_size: Some(window_size),
193 mss,
194 tcp_options: Vec::new(),
195 raw_fingerprint: fingerprint,
196 };
197 }
198
199 self.detect_from_ttl(ttl)
201 }
202
203 pub fn detect_from_banner(&self, banner: &str) -> Option<OSFingerprint> {
205 let banner_lower = banner.to_lowercase();
206
207 if banner_lower.contains("windows") || banner_lower.contains("microsoft") {
209 let version = if banner_lower.contains("2022") {
210 WindowsVersion::WindowsServer2022
211 } else if banner_lower.contains("2019") {
212 WindowsVersion::WindowsServer2019
213 } else if banner_lower.contains("2016") {
214 WindowsVersion::WindowsServer2016
215 } else {
216 WindowsVersion::Unknown
217 };
218
219 return Some(OSFingerprint {
220 os: OperatingSystem::Windows(version),
221 confidence: 0.9,
222 ttl: None,
223 window_size: None,
224 mss: None,
225 tcp_options: Vec::new(),
226 raw_fingerprint: format!("banner:{}", &banner[..banner.len().min(50)]),
227 });
228 }
229
230 if banner_lower.contains("ubuntu") {
232 return Some(OSFingerprint {
233 os: OperatingSystem::Linux(LinuxDistro::Ubuntu),
234 confidence: 0.95,
235 ttl: None,
236 window_size: None,
237 mss: None,
238 tcp_options: Vec::new(),
239 raw_fingerprint: "banner:ubuntu".to_string(),
240 });
241 }
242
243 if banner_lower.contains("debian") {
244 return Some(OSFingerprint {
245 os: OperatingSystem::Linux(LinuxDistro::Debian),
246 confidence: 0.95,
247 ttl: None,
248 window_size: None,
249 mss: None,
250 tcp_options: Vec::new(),
251 raw_fingerprint: "banner:debian".to_string(),
252 });
253 }
254
255 if banner_lower.contains("centos") || banner_lower.contains("red hat") {
256 return Some(OSFingerprint {
257 os: OperatingSystem::Linux(LinuxDistro::CentOS),
258 confidence: 0.9,
259 ttl: None,
260 window_size: None,
261 mss: None,
262 tcp_options: Vec::new(),
263 raw_fingerprint: "banner:centos".to_string(),
264 });
265 }
266
267 None
268 }
269
270 pub fn detect_combined(
272 &self,
273 ttl: Option<u8>,
274 window_size: Option<u16>,
275 mss: Option<u16>,
276 banner: Option<&str>,
277 ) -> OSFingerprint {
278 if let Some(b) = banner {
280 if let Some(fp) = self.detect_from_banner(b) {
281 return fp;
282 }
283 }
284
285 if let (Some(t), Some(w)) = (ttl, window_size) {
287 return self.detect_from_tcp(t, w, mss);
288 }
289
290 if let Some(t) = ttl {
292 return self.detect_from_ttl(t);
293 }
294
295 OSFingerprint {
297 os: OperatingSystem::Unknown,
298 confidence: 0.0,
299 ttl: None,
300 window_size: None,
301 mss: None,
302 tcp_options: Vec::new(),
303 raw_fingerprint: "unknown".to_string(),
304 }
305 }
306}
307
308impl Default for OSDetector {
309 fn default() -> Self {
310 Self::new()
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_ttl_detection() {
320 let detector = OSDetector::new();
321
322 let windows = detector.detect_from_ttl(128);
323 assert!(matches!(windows.os, OperatingSystem::Windows(_)));
324
325 let linux = detector.detect_from_ttl(64);
326 assert!(matches!(linux.os, OperatingSystem::Linux(_)));
327
328 let cisco = detector.detect_from_ttl(255);
329 assert!(matches!(cisco.os, OperatingSystem::Cisco));
330 }
331
332 #[test]
333 fn test_banner_detection() {
334 let detector = OSDetector::new();
335
336 let ubuntu = detector.detect_from_banner("Ubuntu 22.04 LTS").unwrap();
337 assert!(matches!(ubuntu.os, OperatingSystem::Linux(LinuxDistro::Ubuntu)));
338 assert!(ubuntu.confidence >= 0.9);
339
340 let windows = detector.detect_from_banner("Microsoft Windows Server 2019").unwrap();
341 assert!(matches!(windows.os, OperatingSystem::Windows(_)));
342 }
343
344 #[test]
345 fn test_combined_detection() {
346 let detector = OSDetector::new();
347
348 let result = detector.detect_combined(
349 Some(64),
350 Some(65535),
351 Some(1460),
352 Some("OpenSSH 8.2p1 Ubuntu"),
353 );
354
355 assert!(matches!(result.os, OperatingSystem::Linux(LinuxDistro::Ubuntu)));
356 assert!(result.confidence >= 0.9);
357 }
358
359 #[test]
360 fn test_os_name() {
361 let fp = OSFingerprint {
362 os: OperatingSystem::Windows(WindowsVersion::WindowsServer2022),
363 confidence: 0.9,
364 ttl: Some(128),
365 window_size: None,
366 mss: None,
367 tcp_options: Vec::new(),
368 raw_fingerprint: "test".to_string(),
369 };
370
371 assert!(fp.os_name().contains("Windows"));
372 }
373
374 #[test]
375 fn test_reliability() {
376 let reliable = OSFingerprint {
377 os: OperatingSystem::Linux(LinuxDistro::Ubuntu),
378 confidence: 0.9,
379 ttl: None,
380 window_size: None,
381 mss: None,
382 tcp_options: Vec::new(),
383 raw_fingerprint: "test".to_string(),
384 };
385
386 let unreliable = OSFingerprint {
387 confidence: 0.5,
388 ..reliable.clone()
389 };
390
391 assert!(reliable.is_reliable());
392 assert!(!unreliable.is_reliable());
393 }
394}