Skip to main content

pistol/
os.rs

1/* Remote OS Detection */
2#[cfg(feature = "os")]
3use chrono::DateTime;
4#[cfg(feature = "os")]
5use chrono::Local;
6#[cfg(feature = "os")]
7use prettytable::row;
8#[cfg(feature = "os")]
9use prettytable::Cell;
10#[cfg(feature = "os")]
11use prettytable::Row;
12#[cfg(feature = "os")]
13use prettytable::Table;
14#[cfg(feature = "os")]
15use serde::Deserialize;
16#[cfg(feature = "os")]
17use serde::Serialize;
18#[cfg(feature = "os")]
19use std::collections::BTreeMap;
20#[cfg(feature = "os")]
21use std::fmt;
22#[cfg(feature = "os")]
23use std::io::Cursor;
24#[cfg(feature = "os")]
25use std::io::Read;
26#[cfg(feature = "os")]
27use std::net::IpAddr;
28#[cfg(feature = "os")]
29use std::sync::mpsc::channel;
30#[cfg(feature = "os")]
31use std::time::Duration;
32#[cfg(feature = "os")]
33use std::time::Instant;
34#[cfg(feature = "os")]
35use tracing::debug;
36#[cfg(feature = "os")]
37use tracing::warn;
38#[cfg(feature = "os")]
39use zip::ZipArchive;
40
41#[cfg(feature = "os")]
42use crate::error::PistolError;
43#[cfg(feature = "os")]
44use crate::layer::infer_addr;
45#[cfg(feature = "os")]
46use crate::os::dbparser::NmapOSDB;
47#[cfg(feature = "os")]
48use crate::os::osscan::os_probe_thread;
49#[cfg(feature = "os")]
50use crate::os::osscan::Fingerprint;
51#[cfg(feature = "os")]
52use crate::os::osscan6::os_probe_thread6;
53#[cfg(feature = "os")]
54use crate::os::osscan6::Fingerprint6;
55#[cfg(feature = "os")]
56use crate::utils::get_threads_pool;
57#[cfg(feature = "os")]
58use crate::utils::num_threads_check;
59#[cfg(feature = "os")]
60use crate::utils::time_sec_to_string;
61#[cfg(feature = "os")]
62use crate::Target;
63
64#[cfg(feature = "os")]
65pub mod dbparser;
66#[cfg(feature = "os")]
67pub mod operator;
68#[cfg(feature = "os")]
69pub mod operator6;
70#[cfg(feature = "os")]
71pub mod osscan;
72#[cfg(feature = "os")]
73pub mod osscan6;
74#[cfg(feature = "os")]
75pub mod packet;
76#[cfg(feature = "os")]
77pub mod packet6;
78#[cfg(feature = "os")]
79pub mod rr;
80
81#[cfg(feature = "os")]
82#[derive(Debug, Clone)]
83pub struct OsInfo {
84    pub name: String,
85    pub class: Vec<String>,
86    pub cpe: Vec<String>,
87    pub score: usize,
88    pub total: usize,
89    pub db: NmapOSDB,
90}
91
92#[cfg(feature = "os")]
93#[derive(Debug, Clone)]
94pub struct OsInfo6 {
95    pub name: String,
96    pub class: String,
97    pub cpe: String,
98    pub score: f64,
99    pub label: usize,
100}
101
102#[cfg(feature = "os")]
103#[derive(Debug, Clone)]
104pub struct OsDetect4 {
105    pub addr: IpAddr,
106    pub origin: Option<String>,
107    pub alive: bool,
108    pub fingerprint: Fingerprint,
109    pub detects: Vec<OsInfo>,
110    pub cost: Duration,
111}
112
113#[cfg(feature = "os")]
114#[derive(Debug, Clone)]
115pub struct OsDetect6 {
116    pub addr: IpAddr,
117    pub origin: Option<String>,
118    pub alive: bool,
119    pub fingerprint: Fingerprint6,
120    pub detects: Vec<OsInfo6>,
121    pub cost: Duration,
122}
123
124#[cfg(feature = "os")]
125#[derive(Debug, Clone)]
126pub enum OsDetect {
127    V4(OsDetect4),
128    V6(OsDetect6),
129}
130
131#[cfg(feature = "os")]
132impl OsDetect {
133    pub fn addr(&self) -> IpAddr {
134        match self {
135            OsDetect::V4(ipv4) => ipv4.addr,
136            OsDetect::V6(ipv6) => ipv6.addr,
137        }
138    }
139}
140
141#[cfg(feature = "os")]
142#[derive(Debug, Clone)]
143pub struct PistolOsDetects {
144    pub os_detects: Vec<OsDetect>,
145    pub start_time: DateTime<Local>,
146    pub end_time: DateTime<Local>,
147}
148
149#[cfg(feature = "os")]
150impl PistolOsDetects {
151    pub fn new() -> PistolOsDetects {
152        PistolOsDetects {
153            os_detects: Vec::new(),
154            start_time: Local::now(),
155            end_time: Local::now(),
156        }
157    }
158    pub fn value(&self) -> Vec<OsDetect> {
159        self.os_detects.clone()
160    }
161    pub fn finish(&mut self, os_detects: Vec<OsDetect>) {
162        self.end_time = Local::now();
163        self.os_detects = os_detects;
164    }
165}
166
167#[cfg(feature = "os")]
168impl fmt::Display for PistolOsDetects {
169    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
170        let mut table = Table::new();
171        table.add_row(Row::new(vec![Cell::new("OS Detect Results")
172            .style_spec("c")
173            .with_hspan(7)]));
174
175        table.add_row(
176            row![c -> "id", c -> "addr", c -> "rank", c -> "score", c -> "os", c -> "cpe", c -> "time cost"],
177        );
178
179        // sorted
180        let mut btm_addr: BTreeMap<IpAddr, OsDetect> = BTreeMap::new();
181        for detect in &self.os_detects {
182            btm_addr.insert(detect.addr(), detect.clone());
183        }
184
185        let mut total_cost = 0.0;
186        let mut id = 1;
187        for (addr, detect) in btm_addr {
188            match detect {
189                OsDetect::V4(o) => {
190                    let time_cost_str = time_sec_to_string(o.cost);
191                    total_cost += o.cost.as_secs_f64();
192                    if o.alive {
193                        if o.detects.len() > 0 {
194                            for (i, os_info) in o.detects.iter().enumerate() {
195                                let addr_str = match &o.origin {
196                                    Some(origin) => format!("{}({})", addr, origin),
197                                    None => format!("{}", addr),
198                                };
199
200                                let rank_str = format!("#{}", i + 1);
201                                let score_str = format!("{}/{}", os_info.score, os_info.total);
202                                let os_details = &os_info.name;
203                                let os_cpe = os_info.cpe.join("|");
204                                table.add_row(row![c -> id, c -> addr_str, c -> rank_str, c -> score_str, c -> os_details, c -> os_cpe, c -> time_cost_str]);
205                                id += 1;
206                            }
207                        } else {
208                            table.add_row(Row::new(vec![
209                                Cell::new("there are no matching results, which is very uncommon and is maybe a bug").with_hspan(7),
210                            ]));
211                        }
212                    } else {
213                        let addr_str = match o.origin {
214                            Some(origin) => format!("{}({})", addr, origin),
215                            None => format!("{}", addr),
216                        };
217
218                        let rank_str = format!("#{}", 1);
219                        table.add_row(row![c -> id, c -> addr_str, c -> rank_str, c -> "0/0", c -> "target dead", c -> "", c -> time_cost_str]);
220                        id += 1;
221                    }
222                }
223                OsDetect::V6(o) => {
224                    let time_cost_str = time_sec_to_string(o.cost);
225                    total_cost += o.cost.as_secs_f64();
226                    if o.alive {
227                        if o.detects.len() > 0 {
228                            for (i, os_info6) in o.detects.iter().enumerate() {
229                                let addr_str = match &o.origin {
230                                    Some(origin) => format!("{}({})", addr, origin),
231                                    None => format!("{}", addr),
232                                };
233
234                                let number_str = format!("#{}", i + 1);
235                                let score_str = format!("{:.1}", os_info6.score);
236                                let os_str = &os_info6.name;
237                                let os_cpe = &os_info6.cpe;
238                                table.add_row(row![c -> id, c -> addr_str, c -> number_str, c -> score_str, c -> os_str, c -> os_cpe, c -> time_cost_str]);
239                                id += 1;
240                            }
241                        } else {
242                            table.add_row(Row::new(vec![Cell::new(
243                                "no results, usually caused by a novelty value greater than 15.0",
244                            )
245                            .with_hspan(7)]));
246                        }
247                    } else {
248                        let addr_str = match o.origin {
249                            Some(origin) => format!("{}({})", addr, origin),
250                            None => format!("{}", addr),
251                        };
252
253                        let rank_str = format!("#{}", 1);
254                        table.add_row(row![c -> id, c -> addr_str, c -> rank_str, c -> "0.0", c -> "target dead", c -> "", c -> time_cost_str]);
255                        id += 1;
256                    }
257                }
258            }
259        }
260        let avg_cost = total_cost / self.os_detects.len() as f64;
261        let summary = format!(
262            "total used time: {:.3}s, avg time cost: {:.3}s",
263            total_cost, avg_cost,
264        );
265        table.add_row(Row::new(vec![Cell::new(&summary).with_hspan(7)]));
266        write!(f, "{}", table)
267    }
268}
269
270#[cfg(feature = "os")]
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct NmapJsonParameters {
273    pub name: String,
274    pub value: Vec<f64>,
275}
276
277#[cfg(feature = "os")]
278#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct CPE {
280    pub name: String,
281    pub osclass: Vec<Vec<String>>,
282    pub cpe: Vec<String>,
283}
284
285#[cfg(feature = "os")]
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct Linear {
288    pub infolist: Vec<String>,
289    pub w: Vec<Vec<f64>>,
290    pub scale: Vec<Vec<f64>>,
291    pub mean: Vec<Vec<f64>>,
292    pub variance: Vec<Vec<f64>>,
293    pub cpe: Vec<CPE>,
294}
295
296#[cfg(feature = "os")]
297fn gen_linear() -> Result<Linear, PistolError> {
298    let variance_json_data = include_str!("./db/nmap-os-db-ipv6/variance.json");
299    let variance_json: Vec<NmapJsonParameters> = serde_json::from_str(variance_json_data)?;
300
301    let mut infolist = Vec::new();
302    let mut variance = Vec::new();
303    for v in variance_json {
304        variance.push(v.value);
305        infolist.push(v.name);
306    }
307    assert_eq!(infolist.len(), 92);
308    assert_eq!(variance.len(), 92);
309
310    let mean_json_data = include_str!("./db/nmap-os-db-ipv6/mean.json");
311    let mean_json: Vec<NmapJsonParameters> = serde_json::from_str(mean_json_data)?;
312    let mut mean = Vec::new();
313    for m in mean_json {
314        mean.push(m.value);
315    }
316    assert_eq!(mean.len(), 92);
317
318    let scale_json_data = include_str!("./db/nmap-os-db-ipv6/scale.json"); // static
319    let scale_json: Vec<NmapJsonParameters> = serde_json::from_str(scale_json_data)?;
320    let mut scale: Vec<Vec<f64>> = Vec::new();
321    for s in scale_json {
322        scale.push(s.value)
323    }
324    assert_eq!(scale.len(), 695);
325
326    let w_json_data = include_str!("./db/nmap-os-db-ipv6/w.json"); // static
327    let w_json: Vec<NmapJsonParameters> = serde_json::from_str(w_json_data)?;
328    assert_eq!(w_json.len(), 695);
329
330    let mut w = Vec::new();
331    // [695, 92] => [92, 695]
332    for i in 0..w_json[0].value.len() {
333        let mut tmp = Vec::new();
334        for x in &w_json {
335            tmp.push(x.value[i]);
336        }
337        w.push(tmp);
338    }
339
340    let cpe_json_data = include_str!("./db/nmap-os-db-ipv6/cpe.json"); // static
341    let cpe: Vec<CPE> = serde_json::from_str(cpe_json_data)?;
342    assert_eq!(cpe.len(), 92);
343
344    let linear = Linear {
345        infolist,
346        scale,
347        w,
348        mean,
349        variance,
350        cpe,
351    };
352    Ok(linear)
353}
354
355#[cfg(feature = "os")]
356fn get_nmap_os_db() -> Result<Vec<NmapOSDB>, PistolError> {
357    let data = include_bytes!("./db/nmap-os-db.zip");
358    let reader = Cursor::new(data);
359    let mut archive = ZipArchive::new(reader)?;
360
361    if archive.len() > 0 {
362        let mut file = archive.by_index(0)?;
363        let mut contents = String::new();
364        file.read_to_string(&mut contents)?;
365        let ret: Vec<NmapOSDB> = serde_json::from_str(&contents)?;
366        Ok(ret)
367    } else {
368        Err(PistolError::ZipEmptyError)
369    }
370}
371
372/// Detect target machine OS on IPv4 and IPv6.
373#[cfg(feature = "os")]
374pub fn os_detect(
375    targets: &[Target],
376    num_threads: Option<usize>,
377    src_addr: Option<IpAddr>,
378    top_k: usize,
379    timeout: Option<Duration>,
380) -> Result<PistolOsDetects, PistolError> {
381    let num_threads = match num_threads {
382        Some(t) => t,
383        None => {
384            let num_threads = targets.len();
385            let num_threads = num_threads_check(num_threads);
386            num_threads
387        }
388    };
389
390    let (tx, rx) = channel();
391    let pool = get_threads_pool(num_threads);
392    let mut recv_size = 0;
393    let mut ret = PistolOsDetects::new();
394    for target in targets {
395        let dst_addr = target.addr;
396        let tx = tx.clone();
397        recv_size += 1;
398        let ia = match infer_addr(dst_addr, src_addr)? {
399            Some(ia) => ia,
400            None => return Err(PistolError::CanNotFoundSourceAddress),
401        };
402        match dst_addr {
403            IpAddr::V4(_) => {
404                let dst_ports = target.ports.clone();
405                let (dst_ipv4, src_ipv4) = ia.ipv4_addr()?;
406                let nmap_os_db = get_nmap_os_db()?;
407                debug!("ipv4 nmap os db parse finish");
408                let origin = target.origin.clone();
409                pool.execute(move || {
410                    let start_time = Instant::now();
411                    let detect_rets = if dst_ports.len() >= 3 {
412                        let dst_open_tcp_port = dst_ports[0];
413                        let dst_closed_tcp_port = dst_ports[1];
414                        let dst_closed_udp_port = dst_ports[2];
415                        let os_detect_ret = os_probe_thread(
416                            dst_ipv4,
417                            dst_open_tcp_port,
418                            dst_closed_tcp_port,
419                            dst_closed_udp_port,
420                            src_ipv4,
421                            nmap_os_db,
422                            top_k,
423                            timeout,
424                        );
425                        os_detect_ret
426                    } else {
427                        Err(PistolError::OSDetectPortsNotEnough)
428                    };
429                    let od = match detect_rets {
430                        Ok((fingerprint, detects)) => {
431                            let o = OsDetect4 {
432                                addr: dst_addr,
433                                origin: origin.clone(),
434                                alive: true,
435                                fingerprint,
436                                detects,
437                                cost: start_time.elapsed(),
438                            };
439                            let od = OsDetect::V4(o);
440                            Ok(od)
441                        }
442                        Err(e) => Err(e),
443                    };
444                    let _ = tx.send((dst_addr, origin.clone(), od, start_time));
445                });
446            }
447            IpAddr::V6(_) => {
448                let dst_ports = target.ports.clone();
449                let (dst_ipv6, src_ipv6) = ia.ipv6_addr()?;
450                let linear = gen_linear()?;
451                debug!("ipv6 gen linear parse finish");
452                let origin = target.origin.clone();
453                pool.execute(move || {
454                    let start_time = Instant::now();
455                    let detect_rets = if dst_ports.len() >= 3 {
456                        let dst_open_tcp_port = dst_ports[0];
457                        let dst_closed_tcp_port = dst_ports[1];
458                        let dst_closed_udp_port = dst_ports[2];
459
460                        let os_detect_ret = os_probe_thread6(
461                            dst_ipv6,
462                            dst_open_tcp_port,
463                            dst_closed_tcp_port,
464                            dst_closed_udp_port,
465                            src_ipv6,
466                            top_k,
467                            linear,
468                            timeout,
469                        );
470                        os_detect_ret
471                    } else {
472                        Err(PistolError::OSDetectPortsNotEnough)
473                    };
474                    let od = match detect_rets {
475                        Ok((fingerprint, detects)) => {
476                            let o = OsDetect6 {
477                                addr: dst_addr,
478                                origin: origin.clone(),
479                                alive: true,
480                                fingerprint,
481                                detects,
482                                cost: start_time.elapsed(),
483                            };
484                            let od = OsDetect::V6(o);
485                            Ok(od)
486                        }
487                        Err(e) => Err(e),
488                    };
489                    let _ = tx.send((dst_addr, origin.clone(), od, start_time));
490                });
491            }
492        }
493    }
494
495    let iter = rx.into_iter().take(recv_size);
496    let mut os_detects = Vec::new();
497    for (addr, origin, r, start_time) in iter {
498        match r {
499            Ok(od) => {
500                os_detects.push(od);
501            }
502            Err(e) => {
503                warn!("os probe error: {}", e);
504                match addr {
505                    IpAddr::V4(_) => {
506                        let o = OsDetect4 {
507                            addr,
508                            origin,
509                            alive: false,
510                            fingerprint: Fingerprint::empty(),
511                            detects: Vec::new(),
512                            cost: start_time.elapsed(),
513                        };
514                        let od = OsDetect::V4(o);
515                        os_detects.push(od);
516                    }
517                    IpAddr::V6(_) => {
518                        let o = OsDetect6 {
519                            addr,
520                            origin,
521                            alive: false,
522                            fingerprint: Fingerprint6::empty(),
523                            detects: Vec::new(),
524                            cost: start_time.elapsed(),
525                        };
526                        let od = OsDetect::V6(o);
527                        os_detects.push(od);
528                    }
529                }
530            }
531        }
532    }
533    ret.finish(os_detects);
534    Ok(ret)
535}
536
537#[cfg(feature = "os")]
538pub fn os_detect_raw(
539    dst_addr: IpAddr,
540    dst_open_tcp_port: u16,
541    dst_closed_tcp_port: u16,
542    dst_closed_udp_port: u16,
543    src_addr: Option<IpAddr>,
544    top_k: usize,
545    timeout: Option<Duration>,
546) -> Result<OsDetect, PistolError> {
547    let start_time = Instant::now();
548    let ia = match infer_addr(dst_addr, src_addr)? {
549        Some(ia) => ia,
550        None => return Err(PistolError::CanNotFoundSourceAddress),
551    };
552    match dst_addr {
553        IpAddr::V4(_) => {
554            let (dst_ipv4, src_ipv4) = ia.ipv4_addr()?;
555            let nmap_os_db = get_nmap_os_db()?;
556            debug!("ipv4 nmap os db parse finish");
557            let nmap_os_db = nmap_os_db.to_vec();
558            match os_probe_thread(
559                dst_ipv4,
560                dst_open_tcp_port,
561                dst_closed_tcp_port,
562                dst_closed_udp_port,
563                src_ipv4,
564                nmap_os_db,
565                top_k,
566                timeout,
567            ) {
568                Ok((fingerprint, detects)) => {
569                    let o = OsDetect4 {
570                        addr: dst_addr,
571                        origin: None,
572                        alive: true,
573                        fingerprint,
574                        detects,
575                        cost: start_time.elapsed(),
576                    };
577                    let od = OsDetect::V4(o);
578                    Ok(od)
579                }
580                Err(e) => {
581                    warn!("os probe error: {}", e);
582                    let o = OsDetect4 {
583                        addr: dst_addr,
584                        origin: None,
585                        alive: false,
586                        fingerprint: Fingerprint::empty(),
587                        detects: Vec::new(),
588                        cost: start_time.elapsed(),
589                    };
590                    let od = OsDetect::V4(o);
591                    Ok(od)
592                }
593            }
594        }
595        IpAddr::V6(_) => {
596            let (dst_ipv6, src_ipv6) = ia.ipv6_addr()?;
597            let linear = gen_linear()?;
598            debug!("ipv6 gen linear parse finish");
599            match os_probe_thread6(
600                dst_ipv6,
601                dst_open_tcp_port,
602                dst_closed_tcp_port,
603                dst_closed_udp_port,
604                src_ipv6,
605                top_k,
606                linear,
607                timeout,
608            ) {
609                Ok((fingerprint, detects)) => {
610                    let o = OsDetect6 {
611                        addr: dst_addr,
612                        origin: None,
613                        alive: true,
614                        fingerprint,
615                        detects,
616                        cost: start_time.elapsed(),
617                    };
618                    let od = OsDetect::V6(o);
619                    Ok(od)
620                }
621                Err(e) => {
622                    warn!("os probe error: {}", e);
623                    let o = OsDetect6 {
624                        addr: dst_addr,
625                        origin: None,
626                        alive: false,
627                        fingerprint: Fingerprint6::empty(),
628                        detects: Vec::new(),
629                        cost: start_time.elapsed(),
630                    };
631                    let od = OsDetect::V6(o);
632                    Ok(od)
633                }
634            }
635        }
636    }
637}
638
639#[cfg(feature = "os")]
640#[cfg(test)]
641mod tests {
642    use super::*;
643    use crate::PistolLogger;
644    use crate::PistolRunner;
645    use crate::Target;
646    use std::net::Ipv4Addr;
647    use std::net::Ipv6Addr;
648    use std::str::FromStr;
649    #[test]
650    fn test_os_detect_windows_server_2025() {
651        /*
652        -- nmap
653        SCAN(V=7.95%E=4%D=8/18%OT=3389%CT=%CU=%PV=Y%DS=1%DC=D%G=N%M=000C29%TM=68A2EE8D%P=x86_64-pc-linux-gnu)
654        SEQ(SP=106%GCD=1%ISR=108%TI=I%TS=A)
655        OPS(O1=M5B4NW0ST11%O2=M5B4NW0ST11%O3=M5B4NW0NNT11%O4=M5B4NW0ST11%O5=M5B4NW0ST11%O6=M5B4ST11)
656        WIN(W1=FA00%W2=FA00%W3=FA00%W4=FA00%W5=FA00%W6=FA00)
657        ECN(R=Y%DF=Y%TG=80%W=FA00%O=M5B4NW0NNS%CC=Y%Q=)
658        T1(R=Y%DF=Y%TG=80%S=O%A=S+%F=AS%RD=0%Q=)
659        T2(R=N)
660        T3(R=N)
661        T4(R=N)
662        U1(R=N)
663        IE(R=N)
664
665        -- pistol
666        SCAN(V=pistol_4.0.16%D=8/19%OT=3389%CT=8765%CU=9876PV=Y%DS=1%DC=D%G=Y%M=0C29%TM=68A41EB0%P=RUST)
667        SEQ(SP=114%GCD=2%ISR=113%TI=I%TS=A)
668        OPS(O1=M5B4NW0ST11%O2=M5B4NW0ST11%O3=M5B4NW0NNT11%O4=M5B4NW0ST11%O5=M5B4NW0ST11%O6=M5B4ST11)
669        WIN(W1=FA00%W2=FA00%W3=FA00%W4=FA00%W5=FA00%W6=FA00)
670        ECN(R=Y%DF=Y%TG=80%W=FA00%O=M5B4NW0NNS%CC=Y%Q=)
671        T1(R=Y%DF=Y%TG=80%S=O%A=S+%F=AS%RD=0%Q=)
672        T2(R=N)
673        T3(R=N)
674        T4(R=N)
675        T5(R=N)
676        T6(R=N)
677        T7(R=N)
678        U1(R=N%UN=0)
679        IE(R=N)
680        */
681
682        let _pr = PistolRunner::init(
683            PistolLogger::Debug,
684            Some(String::from("os_detect.pcapng")),
685            None, // use default value
686        )
687        .unwrap();
688
689        let src_addr = None;
690        let dst_closed_tcp_port = 8765;
691        let dst_closed_udp_port = 9876;
692
693        // let addr1 = IpAddr::V4(Ipv4Addr::new(192, 168, 5, 5));
694        // let dst_open_tcp_port = 22;
695        // let addr1 = IpAddr::V4(Ipv4Addr::new(192, 168, 5, 6));
696        // let dst_open_tcp_port = 22;
697        let addr1 = IpAddr::V4(Ipv4Addr::new(192, 168, 5, 128));
698        let dst_open_tcp_port = 3389;
699        let target1 = Target::new(
700            addr1,
701            Some(vec![
702                dst_open_tcp_port,
703                dst_closed_tcp_port,
704                dst_closed_udp_port,
705            ]),
706        );
707
708        let timeout = Some(Duration::from_secs_f64(1.0));
709        let top_k = 3;
710        let num_threads = Some(8);
711        let ret = os_detect(&[target1], num_threads, src_addr, top_k, timeout).unwrap();
712        println!("{}", ret);
713
714        let detects = ret.value();
715        for od in detects {
716            match od {
717                OsDetect::V4(r) => {
718                    println!("{}", r.fingerprint);
719                }
720                _ => (),
721            }
722        }
723    }
724
725    #[test]
726    fn test_os_detect_centos_7() {
727        /*
728        -- nmap
729        SCAN(V=7.95%E=4%D=8/19%OT=22%CT=%CU=%PV=Y%DS=1%DC=D%G=N%M=000C29%TM=68A3D962%P=x86_64-pc-linux-gnu)
730        SEQ(SP=105%GCD=1%ISR=10B%TI=Z%TS=A)
731        SEQ(SP=FF%GCD=1%ISR=10C%TI=Z%II=I%TS=A)
732        OPS(O1=M5B4ST11NW7%O2=M5B4ST11NW7%O3=M5B4NNT11NW7%O4=M5B4ST11NW7%O5=M5B4ST11NW7%O6=M5B4ST11)
733        WIN(W1=7120%W2=7120%W3=7120%W4=7120%W5=7120%W6=7120)
734        ECN(R=Y%DF=Y%TG=40%W=7210%O=M5B4NNSNW7%CC=Y%Q=)
735        T1(R=Y%DF=Y%TG=40%S=O%A=S+%F=AS%RD=0%Q=)
736        T2(R=N)
737        T3(R=N)
738        T4(R=Y%DF=Y%TG=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)
739        U1(R=N)
740        IE(R=Y%DFI=N%TG=40%CD=S)
741
742        -- pistol
743        SCAN(V=pistol_4.0.16%D=8/19%OT=22%CT=8765%CU=9876PV=Y%DS=1%DC=D%G=Y%M=0C29%TM=68A3F024%P=RUST)
744        SEQ(SP=114%GCD=1%ISR=112%TI=Z%TS=A)
745        OPS(O1=M5B4ST11NW7%O2=M5B4ST11NW7%O3=M5B4NNT11NW7%O4=M5B4ST11NW7%O5=M5B4ST11NW7%O6=M5B4ST11)
746        WIN(W1=7120%W2=7120%W3=7120%W4=7120%W5=7120%W6=7120)
747        ECN(R=Y%DF=Y%T=40%W=7210%O=M5B4NNSNW7%CC=Y%Q=)
748        T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)
749        T2(R=N)
750        T3(R=N)
751        T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)
752        T5(R=N)
753        T6(R=N)
754        T7(R=N)
755        U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)
756        IE(R=Y%DFI=N%T=40%CD=S)
757        */
758
759        let _pr = PistolRunner::init(
760            PistolLogger::Debug,
761            Some(String::from("os_detect.pcapng")),
762            None, // use default value
763        )
764        .unwrap();
765
766        let src_addr = None;
767        let dst_closed_tcp_port = 8765;
768        let dst_closed_udp_port = 9876;
769
770        let addr1 = IpAddr::V4(Ipv4Addr::new(192, 168, 5, 129));
771        let dst_open_tcp_port = 22;
772        let target1 = Target::new(
773            addr1,
774            Some(vec![
775                dst_open_tcp_port,
776                dst_closed_tcp_port,
777                dst_closed_udp_port,
778            ]),
779        );
780
781        let timeout = Some(Duration::from_secs_f64(1.0));
782        let top_k = 3;
783        let num_threads = Some(8);
784        let ret = os_detect(&[target1], num_threads, src_addr, top_k, timeout).unwrap();
785        println!("{}", ret);
786
787        let detects = ret.value();
788        for od in detects {
789            match od {
790                OsDetect::V4(r) => {
791                    println!("{}", r.fingerprint);
792                }
793                _ => (),
794            }
795        }
796    }
797    #[test]
798    fn test_os_detect6_centos_7() {
799        /*
800        -- nmap
801        SCAN(V=7.95%E=6%D=8/19%OT=22%CT=%CU=43325%PV=N%DS=1%DC=D%G=Y%M=000C29%TM=68A4363F%P=x86_64-pc-linux-gnu)
802        S1(P=6000{4}280640XX{32}0016c7a2c2c785dcac3c69aea0126f90d6fc0000020405a00402080a01c245aeff{4}01030307%ST=0.07947%RT=0.080582)
803        S2(P=6000{4}280640XX{32}0016c7a3a6b3dd08ac3c69afa0126f909b7e0000020405a00402080a01c24612ff{4}01030307%ST=0.180041%RT=0.180935)
804        S3(P=6000{4}280640XX{32}0016c7a4e5cd8492ac3c69b0a0126f90b7750000020405a00101080a01c24676ff{4}01030307%ST=0.279382%RT=0.280225)
805        S4(P=6000{4}280640XX{32}0016c7a54200f6b5ac3c69b1a0126f90e5b80000020405a00402080a01c246daff{4}01030307%ST=0.379506%RT=0.380849)
806        S5(P=6000{4}280640XX{32}0016c7a6a9fe8bd7ac3c69b2a0126f90e8320000020405a00402080a01c2473eff{4}01030307%ST=0.47953%RT=0.480549)
807        S6(P=6000{4}240640XX{32}0016c7a76d2a8034ac3c69b390126f9044520000020405a00402080a01c247a2ff{4}%ST=0.579882%RT=0.581002)
808        IE1(P=6000{4}803a40XX{32}81093eafabcd00{122}%ST=0.61657%RT=0.61718)
809        IE2(P=6000{4}583a40XX{32}0401400a00{3}386001234500280032XX{32}3c00010400{4}2b00010400{12}3a00010400{4}8000402fabcd0001%ST=0.666876%RT=0.668181)
810        NS(P=6000{4}183affXX{32}880009df4000{3}XX{16}%ST=0.719135%RT=0.720038)
811        U1(P=6000{3}01643a40XX{32}0101d53200{4}6001234501341139XX{32}c733a93d01348fec43{300}%ST=0.766442%RT=0.767309)
812        TECN(P=6000{4}200640XX{32}0016c7a8d45582a8ac3c69b48052708035e80000020405a00101040201030307%ST=0.815959%RT=0.816861)
813        T4(P=6000{4}140640XX{32}0016c7aba5421c1400{4}5004000093090000%ST=0.964467%RT=0.966983)
814        EXTRA(FL=12345)
815
816        -- pistol
817        SCAN(V=pistol_4.0.16%E=6%D=8/19%OT=22%CT=8765%CU=9876PV=Y%DS=1%DC=D%G=Y%M=0C29%TM=68A44A76%P=RUST)
818        S1(P=600{2}028640fe800{3}0XX{32}%ST=0.000578%RT=0.025954)
819        S2(P=600{2}028640fe800{3}0XX{32}%ST=0.100911%RT=0.121613)
820        S3(P=600{2}028640fe800{3}0XX{32}%ST=0.201081%RT=0.221879)
821        S4(P=600{2}028640fe800{3}0XX{32}%ST=0.304505%RT=0.325537)
822        S5(P=600{2}028640fe800{3}0XX{32}%ST=0.404598%RT=0.417561)
823        S6(P=600{2}024640fe800{3}0XX{30}%ST=0.504899%RT=0.525877)
824        IE1(P=600{2}0803a40fe800{3}0XX{32}00{44}%ST=0.605082%RT=0.649687)
825        IE2(P=600{2}0583a40fe800{3}0XX{32}78e837a77371778c3c01400{2}2b01400{6}3a01400{2}800402fabcd01%ST=0.649706%RT=0.665666)
826        NS(P=600{2}0183afffe800{3}0XX{24}%ST=3.285946%RT=3.309598)
827        U1(P=600{2}1643a40fe800{3}0XX{32}78e837a77371778c84932694134553643{300}%ST=3.309699%RT=3.330450)
828        TECN(P=600{2}020640fe800{3}0XX{28}%ST=3.330488%RT=3.357514)
829        T4(P=600{2}014640fe800{3}0XX{22}%ST=3.558762%RT=3.578006)
830        */
831
832        let _pr = PistolRunner::init(
833            PistolLogger::Debug,
834            Some(String::from("os_detect6.pcapng")),
835            None, // use default value
836        )
837        .unwrap();
838
839        let src_addr = None;
840        let dst_open_tcp_port = 22;
841        let dst_closed_tcp_port = 8765;
842        let dst_closed_udp_port = 9876;
843        let addr1 = Ipv6Addr::from_str("fe80::78e8:37a7:7371:778c").unwrap();
844        let target1 = Target::new(
845            addr1.into(),
846            Some(vec![
847                dst_open_tcp_port,
848                dst_closed_tcp_port,
849                dst_closed_udp_port,
850            ]),
851        );
852
853        let timeout = Some(Duration::from_secs_f64(0.5));
854        let top_k = 3;
855        let num_threads = Some(8);
856        let ret = os_detect(&[target1], num_threads, src_addr, top_k, timeout).unwrap();
857        println!("{}", ret);
858        // let fingerprint = ret.
859    }
860
861    #[test]
862    fn test_os_detect6_windows_server_2025() {
863        /*
864        -- nmap
865        SCAN(V=7.95%E=6%D=8/19%OT=3389%CT=%CU=%PV=N%DS=1%DC=D%G=Y%M=000C29%TM=68A43D15%P=x86_64-pc-linux-gnu)
866        S1(P=60029ebf00280640XX{32}0d3d99c6689af490e73b9337a012fa0052090000020405a0010303000402080a0132496eff{4}%ST=0.021405%RT=0.022599)
867        S2(P=600bef1e00280640XX{32}0d3d99c7576e06a5e73b9338a012fa0050bb0000020405a0010303000402080a013249d2ff{4}%ST=0.121547%RT=0.123118)
868        S3(P=60085d5d00280640XX{32}0d3d99c86fb65611e73b9339a012fa00eba10000020405a0010303000101080a01324a36ff{4}%ST=0.221461%RT=0.222517)
869        S4(P=600377f700280640XX{32}0d3d99c924b05409e73b933aa012fa0035490000020405a0010303000402080a01324a9aff{4}%ST=0.321468%RT=0.322621)
870        S5(P=6002f85a00280640XX{32}0d3d99ca782ca544e73b933ba012fa00902a0000020405a0010303000402080a01324aff{5}%ST=0.422417%RT=0.423493)
871        S6(P=60073a4400240640XX{32}0d3d99cb694a6beae73b933c9012fa00ec080000020405a00402080a01324b62ff{4}%ST=0.521465%RT=0.522136)
872        NS(P=6000{4}203affXX{32}88002df86000{3}XX{16}0201000c29fd51d5%ST=0.665484%RT=0.667043)
873        TECN(P=6024d63100200640XX{32}0d3d99ccedaa00fae73b933d8052fa0031f50000020405a00103030001010402%ST=0.765114%RT=0.766728)
874        EXTRA(FL=12345)
875
876        -- pistol
877        SCAN(V=pistol_4.0.16%E=6%D=8/20%OT=3389%CT=8765%CU=9876PV=Y%DS=1%DC=D%G=Y%M=0C29%TM=68A527C2%P=RUST)
878        S1(P=60c7220028680fe800{3}0XX{32}%ST=0.000624%RT=0.009970)
879        S2(P=60a8e56028680fe800{3}0XX{32}%ST=0.100868%RT=0.122395)
880        S3(P=60c67e3028680fe800{3}0XX{32}%ST=0.201178%RT=0.218521)
881        S4(P=609ca85028680fe800{3}0XX{32}%ST=0.301444%RT=0.321933)
882        S5(P=609a75c028680fe800{3}0XX{32}%ST=0.401585%RT=0.418401)
883        S6(P=6024033024680fe800{3}0XX{30}%ST=0.501854%RT=0.518154)
884        NS(P=600{2}0203afffe800{3}0XX{28}%ST=8.470275%RT=8.498444)
885        TECN(P=602447e6020680fe800{3}0XX{28}%ST=11.138252%RT=11.166179)
886        EXTRA(FL=12345)
887        */
888
889        let _pr = PistolRunner::init(
890            PistolLogger::Debug,
891            Some(String::from("os_detect6.pcapng")),
892            None, // use default value
893        )
894        .unwrap();
895
896        let src_addr = None;
897        let dst_open_tcp_port = 3389;
898        let dst_closed_tcp_port = 8765;
899        let dst_closed_udp_port = 9876;
900        let addr1 = Ipv6Addr::from_str("fe80::5dd9:4cd2:82ac:d35").unwrap();
901        let target1 = Target::new(
902            addr1.into(),
903            Some(vec![
904                dst_open_tcp_port,
905                dst_closed_tcp_port,
906                dst_closed_udp_port,
907            ]),
908        );
909
910        let timeout = Some(Duration::from_secs_f64(0.5));
911        let top_k = 3;
912        let num_threads = Some(8);
913        let ret = os_detect(&[target1], num_threads, src_addr, top_k, timeout).unwrap();
914        println!("{}", ret);
915        // let fingerprint = ret.
916    }
917    #[test]
918    fn test_os_detect_raw() {
919        let addr1 = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2));
920        let src_addr = None;
921        let dst_open_tcp_port = 22;
922        let dst_closed_tcp_port = 8765;
923        let dst_closed_udp_port = 9876;
924        let timeout = Some(Duration::new(1, 0));
925        let top_k = 3;
926        let ret = os_detect_raw(
927            addr1,
928            dst_open_tcp_port,
929            dst_closed_tcp_port,
930            dst_closed_udp_port,
931            src_addr,
932            top_k,
933            timeout,
934        )
935        .unwrap();
936        println!("{:?}", ret);
937    }
938    #[test]
939    fn test_os_detect6_raw() {
940        let addr1 = IpAddr::V6(Ipv6Addr::new(
941            0xfe80, 0, 0, 0, 0x0020c, 0x29ff, 0xfe2c, 0x09e4,
942        ));
943        let src_addr = None;
944        let dst_open_tcp_port = 22;
945        let dst_closed_tcp_port = 8765;
946        let dst_closed_udp_port = 9876;
947        let timeout = Some(Duration::new(1, 0));
948        let top_k = 3;
949        let ret = os_detect_raw(
950            addr1,
951            dst_open_tcp_port,
952            dst_closed_tcp_port,
953            dst_closed_udp_port,
954            src_addr,
955            top_k,
956            timeout,
957        )
958        .unwrap();
959        println!("{:?}", ret);
960    }
961    #[test]
962    fn test_compare_with_nmap() {
963        let nmap_fingerprint_format = |input: &str| -> String {
964            let output = input.replace("OS:", "");
965            let output = output.replace("\n", "");
966            let output = output.replace(")", ")\n");
967            output
968        };
969
970        let nmap_output = "
971OS:SCAN(V=7.93%E=4%D=11/13%OT=22%CT=1%CU=39775%PV=Y%DS=1%DC=D%G=Y%M=000C29%
972OS:TM=67341E99%P=x86_64-pc-linux-gnu)SEQ(SP=108%GCD=1%ISR=10A%TI=Z%CI=Z%II=
973OS:I%TS=A)OPS(O1=M5B4ST11NW7%O2=M5B4ST11NW7%O3=M5B4NNT11NW7%O4=M5B4ST11NW7%
974OS:O5=M5B4ST11NW7%O6=M5B4ST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W
975OS:6=FE88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M5B4NNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=
976OS:O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD
977OS:=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0
978OS:%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1
979OS:(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI
980OS:=N%T=40%CD=S)";
981
982        let p = nmap_fingerprint_format(&nmap_output);
983        println!("{}", p);
984    }
985}