1mod matchers;
2use matchers::cache_sizes;
3#[cfg(feature = "http-p0f-request")]
4use matchers::HttpRequestMatchResult;
5
6use crate::error::HuginnNetError;
7use crate::output::FingerprintResult;
8use crate::process::ObservablePackage;
9#[cfg(feature = "http-p0f-response")]
10use huginn_net_http::http::HttpDiagnosis;
11use huginn_net_http::http_process::{FlowKey, HttpProcessors, TcpFlow};
12#[cfg(feature = "http-p0f-request")]
13use huginn_net_http::output::HttpRequestOutput;
14#[cfg(feature = "http-p0f-response")]
15use huginn_net_http::output::HttpResponseOutput;
16#[cfg(feature = "tcp-mtu")]
17use huginn_net_tcp::output::MTUOutput;
18#[cfg(feature = "tcp-syn-ack")]
19use huginn_net_tcp::output::SynAckTCPOutput;
20#[cfg(feature = "tcp-syn")]
21use huginn_net_tcp::output::SynTCPOutput;
22#[cfg(feature = "tcp-uptime")]
23use huginn_net_tcp::output::{UptimeOutput, UptimeRole};
24use huginn_net_tcp::ConnectionTracker;
25use huginn_net_tls::output::TlsClientOutput;
26use pcap_file::pcap::PcapReader;
27use pnet::datalink::{self, Channel, Config};
28use std::fs::File;
29use std::sync::atomic::{AtomicBool, Ordering};
30use std::sync::mpsc::Sender;
31use std::sync::Arc;
32use tracing::{debug, error};
33use ttl_cache::TtlCache;
34
35#[cfg(feature = "db")]
36use huginn_net_db::Database;
37
38#[cfg(feature = "http-p0f-request")]
39pub use huginn_net_http::observable::ObservableHttpRequest;
40#[cfg(feature = "http-p0f-response")]
41pub use huginn_net_http::observable::ObservableHttpResponse;
42pub use huginn_net_tcp::observable::ObservableTcp;
43use huginn_net_tcp::FilterConfig;
44pub use huginn_net_tls::ObservableTlsClient;
45
46#[derive(Debug, Clone)]
48pub struct AnalysisConfig {
49 pub http_enabled: bool,
51 pub tcp_enabled: bool,
53 pub tls_enabled: bool,
55 pub matcher_enabled: bool,
57}
58
59impl Default for AnalysisConfig {
60 fn default() -> Self {
61 Self { http_enabled: true, tcp_enabled: true, tls_enabled: true, matcher_enabled: true }
62 }
63}
64
65pub struct HuginnNet<'a> {
112 #[cfg(feature = "db")]
113 pub tcp_matcher: Option<huginn_net_db::TcpSignatureMatcher<'a>>,
114 #[cfg(feature = "db")]
115 pub http_matcher: Option<huginn_net_db::HttpSignatureMatcher<'a>>,
116 connection_tracker: ConnectionTracker,
117 http_flows: TtlCache<FlowKey, TcpFlow>,
118 http_processors: HttpProcessors,
119 pub(crate) config: AnalysisConfig,
120 filter_config: Option<FilterConfig>,
121 #[cfg(not(feature = "db"))]
122 _lifetime: std::marker::PhantomData<&'a ()>,
123}
124
125impl<'a> HuginnNet<'a> {
126 #[cfg(feature = "db")]
146 pub fn new(
147 database: Option<&'a Database>,
148 max_connections: usize,
149 config: Option<AnalysisConfig>,
150 ) -> Result<Self, HuginnNetError> {
151 let config = config.unwrap_or_default();
152
153 if config.matcher_enabled
154 && (config.tcp_enabled || config.http_enabled)
155 && database.is_none()
156 {
157 return Err(HuginnNetError::MissConfiguration(
158 "Database is required when matcher is enabled".to_string(),
159 ));
160 }
161
162 let tcp_matcher = if config.matcher_enabled && config.tcp_enabled {
163 database.map(|db| huginn_net_db::TcpSignatureMatcher::new(&db.tcp))
164 } else {
165 None
166 };
167
168 let http_matcher = if config.matcher_enabled && config.http_enabled {
169 database.map(|db| huginn_net_db::HttpSignatureMatcher::new(&db.http))
170 } else {
171 None
172 };
173
174 let (connection_tracker_size, http_flows_size) = cache_sizes(&config, max_connections);
175
176 Ok(Self {
177 tcp_matcher,
178 http_matcher,
179 connection_tracker: ConnectionTracker::new(connection_tracker_size),
180 http_flows: TtlCache::new(http_flows_size),
181 http_processors: HttpProcessors::new(),
182 config,
183 filter_config: None,
184 })
185 }
186
187 #[cfg(not(feature = "db"))]
199 pub fn new_observable(
200 max_connections: usize,
201 config: Option<AnalysisConfig>,
202 ) -> Result<Self, HuginnNetError> {
203 let config = config.unwrap_or_default();
204 let (connection_tracker_size, http_flows_size) = cache_sizes(&config, max_connections);
205
206 Ok(Self {
207 connection_tracker: ConnectionTracker::new(connection_tracker_size),
208 http_flows: TtlCache::new(http_flows_size),
209 http_processors: HttpProcessors::new(),
210 config,
211 filter_config: None,
212 _lifetime: std::marker::PhantomData,
213 })
214 }
215
216 pub fn with_filter(mut self, filter: FilterConfig) -> Self {
227 self.filter_config = Some(filter);
228 self
229 }
230
231 fn process_with<F>(
232 &mut self,
233 mut packet_fn: F,
234 sender: Sender<FingerprintResult>,
235 cancel_signal: Option<Arc<AtomicBool>>,
236 ) -> Result<(), HuginnNetError>
237 where
238 F: FnMut() -> Option<Result<Vec<u8>, HuginnNetError>>,
239 {
240 while let Some(packet_result) = packet_fn() {
241 if let Some(ref cancel) = cancel_signal {
242 if cancel.load(Ordering::Relaxed) {
243 debug!("Cancellation signal received, stopping packet processing");
244 break;
245 }
246 }
247
248 match packet_result {
249 Ok(packet) => {
250 if let Some(ref filter) = self.filter_config {
251 if !huginn_net_tcp::raw_filter::apply(&packet, filter) {
252 debug!("Filtered out packet before parsing");
253 continue;
254 }
255 }
256
257 let output = self.analyze_tcp(&packet);
258 if sender.send(output).is_err() {
259 error!("Receiver dropped, stopping packet processing");
260 break;
261 }
262 }
263 Err(e) => {
264 error!("Failed to read packet: {}", e);
265 }
266 }
267 }
268 Ok(())
269 }
270
271 pub fn analyze_network(
283 &mut self,
284 interface_name: &str,
285 sender: Sender<FingerprintResult>,
286 cancel_signal: Option<Arc<AtomicBool>>,
287 ) -> Result<(), HuginnNetError> {
288 let interfaces = datalink::interfaces();
289 let interface = interfaces
290 .into_iter()
291 .find(|iface| iface.name == interface_name)
292 .ok_or_else(|| {
293 HuginnNetError::MissConfiguration(format!(
294 "Could not find network interface: {interface_name}"
295 ))
296 })?;
297
298 debug!("Using network interface: {}", interface.name);
299
300 let config = Config { promiscuous: true, ..Config::default() };
301
302 let (_tx, mut rx) = match datalink::channel(&interface, config) {
303 Ok(Channel::Ethernet(tx, rx)) => (tx, rx),
304 Ok(_) => {
305 return Err(HuginnNetError::MissConfiguration("Unhandled channel type".to_string()))
306 }
307 Err(e) => {
308 return Err(HuginnNetError::MissConfiguration(format!(
309 "Unable to create channel: {e}"
310 )))
311 }
312 };
313
314 self.process_with(
315 move || match rx.next() {
316 Ok(packet) => Some(Ok(packet.to_vec())),
317 Err(e) => Some(Err(HuginnNetError::MissConfiguration(format!(
318 "Error receiving packet: {e}"
319 )))),
320 },
321 sender,
322 cancel_signal,
323 )
324 }
325
326 pub fn analyze_pcap(
336 &mut self,
337 pcap_path: &str,
338 sender: Sender<FingerprintResult>,
339 cancel_signal: Option<Arc<AtomicBool>>,
340 ) -> Result<(), HuginnNetError> {
341 let file = File::open(pcap_path).map_err(|e| {
342 HuginnNetError::MissConfiguration(format!("Failed to open PCAP file: {e}"))
343 })?;
344 let mut pcap_reader = PcapReader::new(file).map_err(|e| {
345 HuginnNetError::MissConfiguration(format!("Failed to create PCAP reader: {e}"))
346 })?;
347
348 self.process_with(
349 move || match pcap_reader.next_packet() {
350 Some(Ok(packet)) => Some(Ok(packet.data.to_vec())),
351 Some(Err(e)) => Some(Err(HuginnNetError::MissConfiguration(format!(
352 "Error reading PCAP packet: {e}"
353 )))),
354 None => None,
355 },
356 sender,
357 cancel_signal,
358 )
359 }
360
361 pub fn analyze_tcp(&mut self, packet: &[u8]) -> FingerprintResult {
369 match ObservablePackage::extract(
370 packet,
371 &mut self.connection_tracker,
372 &mut self.http_flows,
373 &self.http_processors,
374 &self.config,
375 ) {
376 Ok(observable_package) => {
377 #[cfg(feature = "tcp-mtu")]
378 let mtu: Option<MTUOutput> = observable_package.mtu.map(|observable_mtu| {
379 let link_quality = self.match_mtu(&observable_mtu.value);
380
381 MTUOutput {
382 source: huginn_net_tcp::output::IpPort::new(
383 observable_package.source.ip,
384 observable_package.source.port,
385 ),
386 destination: huginn_net_tcp::output::IpPort::new(
387 observable_package.destination.ip,
388 observable_package.destination.port,
389 ),
390 link: link_quality,
391 mtu: observable_mtu.value,
392 }
393 });
394
395 #[cfg(feature = "tcp-syn")]
396 let syn: Option<SynTCPOutput> =
397 observable_package.tcp_request.map(|observable_tcp| {
398 let os_quality = self.match_tcp_request(&observable_tcp);
399
400 SynTCPOutput {
401 source: huginn_net_tcp::output::IpPort::new(
402 observable_package.source.ip,
403 observable_package.source.port,
404 ),
405 destination: huginn_net_tcp::output::IpPort::new(
406 observable_package.destination.ip,
407 observable_package.destination.port,
408 ),
409 os_matched: os_quality,
410 sig: observable_tcp,
411 }
412 });
413
414 #[cfg(feature = "tcp-syn-ack")]
415 let syn_ack: Option<SynAckTCPOutput> =
416 observable_package.tcp_response.map(|observable_tcp| {
417 let os_quality = self.match_tcp_response(&observable_tcp);
418
419 SynAckTCPOutput {
420 source: huginn_net_tcp::output::IpPort::new(
421 observable_package.source.ip,
422 observable_package.source.port,
423 ),
424 destination: huginn_net_tcp::output::IpPort::new(
425 observable_package.destination.ip,
426 observable_package.destination.port,
427 ),
428 os_matched: os_quality,
429 sig: observable_tcp,
430 }
431 });
432
433 #[cfg(feature = "tcp-uptime")]
434 let client_uptime: Option<UptimeOutput> =
435 observable_package.client_uptime.map(|update| UptimeOutput {
436 source: huginn_net_tcp::output::IpPort::new(
437 observable_package.source.ip,
438 observable_package.source.port,
439 ),
440 destination: huginn_net_tcp::output::IpPort::new(
441 observable_package.destination.ip,
442 observable_package.destination.port,
443 ),
444 role: UptimeRole::Client,
445 days: update.days,
446 hours: update.hours,
447 min: update.min,
448 up_mod_days: update.up_mod_days,
449 freq: update.freq,
450 });
451
452 #[cfg(feature = "tcp-uptime")]
453 let server_uptime: Option<UptimeOutput> =
454 observable_package.server_uptime.map(|update| UptimeOutput {
455 source: huginn_net_tcp::output::IpPort::new(
456 observable_package.source.ip,
457 observable_package.source.port,
458 ),
459 destination: huginn_net_tcp::output::IpPort::new(
460 observable_package.destination.ip,
461 observable_package.destination.port,
462 ),
463 role: UptimeRole::Server,
464 days: update.days,
465 hours: update.hours,
466 min: update.min,
467 up_mod_days: update.up_mod_days,
468 freq: update.freq,
469 });
470
471 #[cfg(feature = "http-p0f-request")]
472 let http_request: Option<HttpRequestOutput> =
473 observable_package
474 .http_request
475 .map(|observable_http_request| {
476 let HttpRequestMatchResult { browser_quality, http_diagnosis } =
477 self.match_http_request(&observable_http_request);
478
479 HttpRequestOutput {
480 source: huginn_net_http::output::IpPort::new(
481 observable_package.source.ip,
482 observable_package.source.port,
483 ),
484 destination: huginn_net_http::output::IpPort::new(
485 observable_package.destination.ip,
486 observable_package.destination.port,
487 ),
488 lang: observable_http_request.lang.clone(),
489 browser_matched: browser_quality,
490 diagnosis: http_diagnosis,
491 sig: observable_http_request,
492 }
493 });
494
495 #[cfg(feature = "http-p0f-response")]
496 let http_response: Option<HttpResponseOutput> = observable_package
497 .http_response
498 .map(|observable_http_response| {
499 let web_server_quality =
500 self.match_http_response(&observable_http_response);
501
502 HttpResponseOutput {
503 source: huginn_net_http::output::IpPort::new(
504 observable_package.source.ip,
505 observable_package.source.port,
506 ),
507 destination: huginn_net_http::output::IpPort::new(
508 observable_package.destination.ip,
509 observable_package.destination.port,
510 ),
511 web_server_matched: web_server_quality,
512 diagnosis: HttpDiagnosis::None,
513 sig: observable_http_response,
514 }
515 });
516
517 let tls_client: Option<TlsClientOutput> =
518 observable_package
519 .tls_client
520 .map(|observable_tls| TlsClientOutput {
521 source: huginn_net_tls::output::IpPort::new(
522 observable_package.source.ip,
523 observable_package.source.port,
524 ),
525 destination: huginn_net_tls::output::IpPort::new(
526 observable_package.destination.ip,
527 observable_package.destination.port,
528 ),
529 sig: observable_tls,
530 });
531
532 FingerprintResult {
533 #[cfg(feature = "tcp-syn")]
534 tcp_syn: syn,
535 #[cfg(feature = "tcp-syn-ack")]
536 tcp_syn_ack: syn_ack,
537 #[cfg(feature = "tcp-mtu")]
538 tcp_mtu: mtu,
539 #[cfg(feature = "tcp-uptime")]
540 tcp_client_uptime: client_uptime,
541 #[cfg(feature = "tcp-uptime")]
542 tcp_server_uptime: server_uptime,
543 #[cfg(feature = "http-p0f-request")]
544 http_request,
545 #[cfg(feature = "http-p0f-response")]
546 http_response,
547 tls_client,
548 }
549 }
550 Err(error) => {
551 debug!("Fail to process signature: {}", error);
552 FingerprintResult {
553 #[cfg(feature = "tcp-syn")]
554 tcp_syn: None,
555 #[cfg(feature = "tcp-syn-ack")]
556 tcp_syn_ack: None,
557 #[cfg(feature = "tcp-mtu")]
558 tcp_mtu: None,
559 #[cfg(feature = "tcp-uptime")]
560 tcp_client_uptime: None,
561 #[cfg(feature = "tcp-uptime")]
562 tcp_server_uptime: None,
563 #[cfg(feature = "http-p0f-request")]
564 http_request: None,
565 #[cfg(feature = "http-p0f-response")]
566 http_response: None,
567 tls_client: None,
568 }
569 }
570 }
571 }
572}