rust_network_scanner/
os_fingerprint.rs

1//! OS Fingerprinting module for network scanning v2.0
2//!
3//! Provides operating system detection based on TCP/IP stack analysis.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Operating system categories
9#[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/// Windows version detection
21#[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/// Linux distribution detection
32#[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/// OS fingerprint result
43#[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    /// Get human-readable OS name
56    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    /// Check if detection is reliable
69    pub fn is_reliable(&self) -> bool {
70        self.confidence >= 0.7
71    }
72}
73
74/// OS fingerprint signatures database
75pub struct OSSignatures {
76    signatures: HashMap<String, (OperatingSystem, f32)>,
77}
78
79impl OSSignatures {
80    /// Create a new signature database with defaults
81    pub fn new() -> Self {
82        let mut signatures = HashMap::new();
83
84        // Windows signatures (TTL 128)
85        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        // Linux signatures (TTL 64)
95        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        // macOS signatures (TTL 64)
105        signatures.insert(
106            "ttl:64:win:65535:sack".to_string(),
107            (OperatingSystem::MacOS("Unknown".to_string()), 0.7),
108        );
109
110        // Cisco signatures (TTL 255)
111        signatures.insert(
112            "ttl:255:cisco".to_string(),
113            (OperatingSystem::Cisco, 0.9),
114        );
115
116        Self { signatures }
117    }
118
119    /// Look up a signature
120    pub fn lookup(&self, fingerprint: &str) -> Option<&(OperatingSystem, f32)> {
121        self.signatures.get(fingerprint)
122    }
123
124    /// Add a custom signature
125    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
136/// OS detector for network scanning
137pub struct OSDetector {
138    signatures: OSSignatures,
139}
140
141impl OSDetector {
142    /// Create a new OS detector
143    pub fn new() -> Self {
144        Self {
145            signatures: OSSignatures::new(),
146        }
147    }
148
149    /// Detect OS from TTL value
150    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    /// Detect OS from TCP parameters
170    pub fn detect_from_tcp(&self, ttl: u8, window_size: u16, mss: Option<u16>) -> OSFingerprint {
171        // Build fingerprint string
172        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        // Look up in signature database
187        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        // Fall back to TTL-based detection
200        self.detect_from_ttl(ttl)
201    }
202
203    /// Detect OS from service banner
204    pub fn detect_from_banner(&self, banner: &str) -> Option<OSFingerprint> {
205        let banner_lower = banner.to_lowercase();
206
207        // Windows detection
208        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        // Linux detection
231        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    /// Combine multiple detection methods
271    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        // Try banner detection first (most reliable)
279        if let Some(b) = banner {
280            if let Some(fp) = self.detect_from_banner(b) {
281                return fp;
282            }
283        }
284
285        // Try TCP-based detection
286        if let (Some(t), Some(w)) = (ttl, window_size) {
287            return self.detect_from_tcp(t, w, mss);
288        }
289
290        // Fall back to TTL-only detection
291        if let Some(t) = ttl {
292            return self.detect_from_ttl(t);
293        }
294
295        // Unknown
296        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}