passivetcp_rs/
lib.rs

1// ============================================================================
2// DEPRECATION WARNING
3// ============================================================================
4#![deprecated(
5    since = "1.3.1",
6    note = "This crate has been renamed to 'huginn-net'. Please use 'huginn-net' instead. See https://crates.io/crates/huginn-net for migration details."
7)]
8
9// ============================================================================
10// CORE IMPORTS (database, errors, results - always required)
11// ============================================================================
12use crate::db::{Database, Label};
13use crate::fingerprint_result::{FingerprintResult, OSQualityMatched};
14
15// ============================================================================
16// TCP PROTOCOL IMPORTS (base protocol)
17// ============================================================================
18use crate::fingerprint_result::{
19    MTUOutput, OperativeSystem, SynAckTCPOutput, SynTCPOutput, UptimeOutput,
20};
21use crate::uptime::{Connection, SynData};
22pub use tcp::Ttl;
23
24// ============================================================================
25// HTTP PROTOCOL IMPORTS (depends on TCP)
26// ============================================================================
27use crate::fingerprint_result::{
28    Browser, BrowserQualityMatched, HttpRequestOutput, HttpResponseOutput, WebServer,
29    WebServerQualityMatched,
30};
31use crate::http::{HttpDiagnosis, Signature};
32use crate::http_process::{FlowKey, TcpFlow};
33
34// ============================================================================
35// TLS PROTOCOL IMPORTS (depends on TCP)
36// ============================================================================
37use crate::fingerprint_result::TlsClientOutput;
38
39// ============================================================================
40// SHARED PROCESSING IMPORTS (used across protocols)
41// ============================================================================
42use crate::process::ObservablePackage;
43use crate::signature_matcher::SignatureMatcher;
44
45// ============================================================================
46// OBSERVABLE SIGNALS EXPORTS (conditional in future)
47// ============================================================================
48pub use observable_signals::{
49    ObservableHttpRequest,  // HTTP signals
50    ObservableHttpResponse, // HTTP signals
51    ObservableTcp,          // TCP signals
52    ObservableTlsClient,    // TLS signals
53};
54
55// ============================================================================
56// EXTERNAL CRATE IMPORTS
57// ============================================================================
58use pcap_file::pcap::PcapReader;
59use pnet::datalink;
60use pnet::datalink::Config;
61use std::error::Error;
62use std::fs::File;
63use std::sync::mpsc::Sender;
64use tracing::{debug, error};
65use ttl_cache::TtlCache;
66
67// ============================================================================
68// CORE MODULES (always required - database, matching, errors, results)
69// ============================================================================
70pub mod db;
71pub mod db_matching_trait;
72pub mod db_parse;
73mod display;
74pub mod error;
75pub mod fingerprint_result;
76
77// ============================================================================
78// TCP PROTOCOL MODULES (base protocol - required by HTTP and TLS)
79// ============================================================================
80pub mod mtu;
81mod observable_tcp_signals_matching;
82pub mod tcp;
83pub mod tcp_process;
84pub mod ttl;
85pub mod uptime;
86pub mod window_size;
87
88// ============================================================================
89// HTTP PROTOCOL MODULES (depends on TCP)
90// ============================================================================
91pub mod http;
92pub mod http_languages;
93pub mod http_process;
94mod observable_http_signals_matching;
95
96// ============================================================================
97// TLS PROTOCOL MODULES (depends on TCP)
98// ============================================================================
99pub mod tls;
100pub mod tls_process;
101
102// ============================================================================
103// SHARED PROCESSING MODULES (used by multiple protocols)
104// ============================================================================
105pub mod ip_options;
106pub mod observable_signals;
107pub mod process;
108pub mod signature_matcher;
109
110/// Configuration for protocol analysis
111#[derive(Debug, Clone)]
112pub struct AnalysisConfig {
113    pub http_enabled: bool,
114    pub tcp_enabled: bool,
115    pub tls_enabled: bool,
116}
117
118impl Default for AnalysisConfig {
119    fn default() -> Self {
120        Self {
121            http_enabled: true,
122            tcp_enabled: true,
123            tls_enabled: true,
124        }
125    }
126}
127
128/// A passive TCP fingerprinting engine inspired by `p0f` with JA4 TLS client fingerprinting.
129///
130/// The `PassiveTcp` struct acts as the core component of the library, handling TCP packet
131/// analysis and matching signatures using a database of known fingerprints, plus JA4 TLS
132/// TLS client analysis following the official FoxIO specification.
133pub struct PassiveTcp<'a> {
134    pub matcher: Option<SignatureMatcher<'a>>,
135    tcp_cache: TtlCache<Connection, SynData>,
136    http_cache: TtlCache<FlowKey, TcpFlow>,
137    config: AnalysisConfig,
138}
139
140impl<'a> PassiveTcp<'a> {
141    /// Creates a new instance of `PassiveTcp`.
142    ///
143    /// # Parameters
144    /// - `database`: Optional reference to the database containing known TCP/Http signatures from p0f.
145    ///   Required if HTTP or TCP analysis is enabled. Not needed for TLS-only analysis.
146    /// - `cache_capacity`: The maximum number of connections to maintain in the TTL cache.
147    /// - `config`: Optional configuration specifying which protocols to analyze. If None, uses default (all enabled).
148    ///
149    /// # Returns
150    /// A new `PassiveTcp` instance initialized with the given database, cache capacity, and configuration.
151    pub fn new(
152        database: Option<&'a Database>,
153        cache_capacity: usize,
154        config: Option<AnalysisConfig>,
155    ) -> Self {
156        let config = config.unwrap_or_default();
157
158        let matcher = if config.tcp_enabled || config.http_enabled {
159            database.map(SignatureMatcher::new)
160        } else {
161            None
162        };
163
164        let tcp_cache_size = if config.tcp_enabled {
165            cache_capacity
166        } else {
167            0
168        };
169        let http_cache_size = if config.http_enabled {
170            cache_capacity
171        } else {
172            0
173        };
174
175        Self {
176            matcher,
177            tcp_cache: TtlCache::new(tcp_cache_size),
178            http_cache: TtlCache::new(http_cache_size),
179            config,
180        }
181    }
182
183    fn process_with<F>(
184        &mut self,
185        mut packet_fn: F,
186        sender: Sender<FingerprintResult>,
187    ) -> Result<(), Box<dyn Error>>
188    where
189        F: FnMut() -> Option<Result<Vec<u8>, Box<dyn Error>>>,
190    {
191        while let Some(packet_result) = packet_fn() {
192            match packet_result {
193                Ok(packet) => {
194                    let output = self.analyze_tcp(&packet);
195                    if sender.send(output).is_err() {
196                        error!("Receiver dropped, stopping packet processing");
197                        break;
198                    }
199                }
200                Err(e) => {
201                    error!("Failed to read packet: {}", e);
202                }
203            }
204        }
205        Ok(())
206    }
207
208    /// Captures and analyzes packets on the specified network interface.
209    ///
210    /// Sends `FingerprintResult` through the provided channel.
211    ///
212    /// # Parameters
213    /// - `interface_name`: The name of the network interface to analyze.
214    /// - `sender`: A `Sender` to send `FingerprintResult` objects back to the caller.
215    ///
216    /// # Panics
217    /// - If the network interface cannot be found or a channel cannot be created.
218    pub fn analyze_network(
219        &mut self,
220        interface_name: &str,
221        sender: Sender<FingerprintResult>,
222    ) -> Result<(), Box<dyn Error>> {
223        let interfaces = datalink::interfaces();
224        let interface = interfaces
225            .into_iter()
226            .find(|iface| iface.name == interface_name)
227            .ok_or_else(|| format!("Could not find network interface: {}", interface_name))?;
228
229        debug!("Using network interface: {}", interface.name);
230
231        let config = Config {
232            promiscuous: true,
233            ..Config::default()
234        };
235
236        let (_tx, mut rx) = match datalink::channel(&interface, config) {
237            Ok(datalink::Channel::Ethernet(tx, rx)) => (tx, rx),
238            Ok(_) => return Err("Unhandled channel type".into()),
239            Err(e) => return Err(format!("Unable to create channel: {}", e).into()),
240        };
241
242        self.process_with(
243            move || match rx.next() {
244                Ok(packet) => Some(Ok(packet.to_vec())),
245                Err(e) => Some(Err(e.into())),
246            },
247            sender,
248        )
249    }
250
251    /// Analyzes packets from a PCAP file.
252    ///
253    /// # Parameters
254    /// - `pcap_path`: The path to the PCAP file to analyze.
255    /// - `sender`: A `Sender` to send `FingerprintResult` objects back to the caller.
256    ///
257    /// # Panics
258    /// - If the PCAP file cannot be opened or read.
259    pub fn analyze_pcap(
260        &mut self,
261        pcap_path: &str,
262        sender: Sender<FingerprintResult>,
263    ) -> Result<(), Box<dyn Error>> {
264        let file = File::open(pcap_path)?;
265        let mut pcap_reader = PcapReader::new(file)?;
266
267        self.process_with(
268            move || match pcap_reader.next_packet() {
269                Some(Ok(packet)) => Some(Ok(packet.data.to_vec())),
270                Some(Err(e)) => Some(Err(e.into())),
271                None => None,
272            },
273            sender,
274        )
275    }
276
277    /// Analyzes a TCP packet and returns a `FingerprintResult` object.
278    ///
279    /// # Parameters
280    /// - `packet`: A reference to the TCP packet to analyze.
281    ///
282    /// # Returns
283    /// A `FingerprintResult` object containing the analysis results.
284    pub fn analyze_tcp(&mut self, packet: &[u8]) -> FingerprintResult {
285        match ObservablePackage::extract(
286            packet,
287            &mut self.tcp_cache,
288            &mut self.http_cache,
289            &self.config,
290        ) {
291            Ok(observable_package) => {
292                let (syn, syn_ack, mtu, uptime, http_request, http_response, tls_client) = {
293                    let mtu: Option<MTUOutput> =
294                        observable_package.mtu.and_then(|observable_mtu| {
295                            self.matcher.as_ref().and_then(|matcher| {
296                                matcher.matching_by_mtu(&observable_mtu.value).map(
297                                    |(link, _mtu_result)| MTUOutput {
298                                        source: observable_package.source.clone(),
299                                        destination: observable_package.destination.clone(),
300                                        link: link.clone(),
301                                        mtu: observable_mtu.value,
302                                    },
303                                )
304                            })
305                        });
306
307                    let syn: Option<SynTCPOutput> =
308                        observable_package
309                            .tcp_request
310                            .map(|observable_tcp| SynTCPOutput {
311                                source: observable_package.source.clone(),
312                                destination: observable_package.destination.clone(),
313                                os_matched: self
314                                    .matcher
315                                    .as_ref()
316                                    .and_then(|matcher| {
317                                        matcher.matching_by_tcp_request(&observable_tcp)
318                                    })
319                                    .map(|(label, _signature, quality)| OSQualityMatched {
320                                        os: OperativeSystem::from(label),
321                                        quality,
322                                    }),
323                                sig: observable_tcp,
324                            });
325
326                    let syn_ack: Option<SynAckTCPOutput> =
327                        observable_package
328                            .tcp_response
329                            .map(|observable_tcp| SynAckTCPOutput {
330                                source: observable_package.source.clone(),
331                                destination: observable_package.destination.clone(),
332                                os_matched: self
333                                    .matcher
334                                    .as_ref()
335                                    .and_then(|matcher| {
336                                        matcher.matching_by_tcp_response(&observable_tcp)
337                                    })
338                                    .map(|(label, _signature, quality)| OSQualityMatched {
339                                        os: OperativeSystem::from(label),
340                                        quality,
341                                    }),
342                                sig: observable_tcp,
343                            });
344
345                    let uptime: Option<UptimeOutput> =
346                        observable_package.uptime.map(|update| UptimeOutput {
347                            source: observable_package.source.clone(),
348                            destination: observable_package.destination.clone(),
349                            days: update.days,
350                            hours: update.hours,
351                            min: update.min,
352                            up_mod_days: update.up_mod_days,
353                            freq: update.freq,
354                        });
355
356                    let http_request: Option<HttpRequestOutput> = observable_package
357                        .http_request
358                        .map(|observable_http_request| {
359                            let signature_matcher: Option<(&Label, &Signature, f32)> =
360                                self.matcher.as_ref().and_then(|matcher| {
361                                    matcher.matching_by_http_request(&observable_http_request)
362                                });
363
364                            let ua_matcher: Option<(&String, &Option<String>)> =
365                                observable_http_request.user_agent.clone().and_then(|ua| {
366                                    self.matcher
367                                        .as_ref()
368                                        .and_then(|matcher| matcher.matching_by_user_agent(ua))
369                                });
370
371                            let http_diagnosis = http_process::get_diagnostic(
372                                observable_http_request.user_agent.clone(),
373                                ua_matcher,
374                                signature_matcher.map(|(label, _signature, _quality)| label),
375                            );
376
377                            HttpRequestOutput {
378                                source: observable_package.source.clone(),
379                                destination: observable_package.destination.clone(),
380                                lang: observable_http_request.lang.clone(),
381                                browser_matched: signature_matcher.map(
382                                    |(label, _signature, quality)| BrowserQualityMatched {
383                                        browser: Browser::from(label),
384                                        quality,
385                                    },
386                                ),
387                                diagnosis: http_diagnosis,
388                                sig: observable_http_request,
389                            }
390                        });
391
392                    let http_response: Option<HttpResponseOutput> = observable_package
393                        .http_response
394                        .map(|observable_http_response| HttpResponseOutput {
395                            source: observable_package.source.clone(),
396                            destination: observable_package.destination.clone(),
397                            web_server_matched: self.matcher.as_ref().and_then(|matcher| {
398                                matcher
399                                    .matching_by_http_response(&observable_http_response)
400                                    .map(|(label, _signature, quality)| WebServerQualityMatched {
401                                        web_server: WebServer::from(label),
402                                        quality,
403                                    })
404                            }),
405                            diagnosis: HttpDiagnosis::None,
406                            sig: observable_http_response,
407                        });
408
409                    let tls_client: Option<TlsClientOutput> =
410                        observable_package
411                            .tls_client
412                            .map(|observable_tls| TlsClientOutput {
413                                source: observable_package.source.clone(),
414                                destination: observable_package.destination.clone(),
415                                sig: observable_tls,
416                            });
417
418                    (
419                        syn,
420                        syn_ack,
421                        mtu,
422                        uptime,
423                        http_request,
424                        http_response,
425                        tls_client,
426                    )
427                };
428
429                FingerprintResult {
430                    syn,
431                    syn_ack,
432                    mtu,
433                    uptime,
434                    http_request,
435                    http_response,
436                    tls_client,
437                }
438            }
439            Err(error) => {
440                debug!("Fail to process signature: {}", error);
441                FingerprintResult {
442                    syn: None,
443                    syn_ack: None,
444                    mtu: None,
445                    uptime: None,
446                    http_request: None,
447                    http_response: None,
448                    tls_client: None,
449                }
450            }
451        }
452    }
453}