1use super::flow::{FlowKey, HttpProcessors, TcpFlow};
9use crate::error::HuginnNetHttpError;
10use crate::filter::raw as raw_filter;
11use crate::filter::FilterConfig;
12use crate::matcher_api::HttpMatcher;
13use crate::output::HttpAnalysisResult;
14use crate::parser::hash as packet_hash;
15use crossbeam_channel::{bounded, Sender};
16use std::fmt;
17use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
18use std::sync::{Arc, Mutex};
19use std::thread;
20use tracing::debug;
21use ttl_cache::TtlCache;
22
23pub type SharedHttpMatcher = Arc<dyn HttpMatcher + Send + Sync>;
27
28struct WorkerConfig {
30 batch_size: usize,
31 timeout_ms: u64,
32 max_connections: usize,
33}
34
35pub struct WorkerPool {
37 packet_senders: Arc<Vec<Sender<Vec<u8>>>>,
38 result_sender: Arc<Mutex<Option<std::sync::mpsc::Sender<HttpAnalysisResult>>>>,
39 shutdown_flag: Arc<AtomicBool>,
40 dispatched_count: AtomicU64,
41 dropped_count: AtomicU64,
42 worker_dropped: Vec<Arc<AtomicU64>>,
43 num_workers: std::num::NonZeroUsize,
44 pub batch_size: usize,
45 pub timeout_ms: u64,
46}
47
48#[derive(Debug, Clone)]
50pub struct WorkerStats {
51 pub id: usize,
52 pub queue_size: usize,
53 pub dropped: u64,
54}
55
56#[derive(Debug, Clone)]
58pub struct PoolStats {
59 pub total_dispatched: u64,
60 pub total_dropped: u64,
61 pub workers: Vec<WorkerStats>,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum DispatchResult {
67 Queued,
69 Dropped,
71}
72
73impl WorkerPool {
74 #[allow(clippy::too_many_arguments)]
89 pub fn new(
90 num_workers: usize,
91 queue_size: usize,
92 batch_size: usize,
93 timeout_ms: u64,
94 result_sender: std::sync::mpsc::Sender<HttpAnalysisResult>,
95 matcher: Option<SharedHttpMatcher>,
96 max_connections: usize,
97 filter_config: Option<FilterConfig>,
98 ) -> Result<Arc<Self>, HuginnNetHttpError> {
99 let num_workers_nz = std::num::NonZeroUsize::new(num_workers).ok_or_else(|| {
100 HuginnNetHttpError::Misconfiguration("Worker count must be at least 1".to_string())
101 })?;
102
103 let mut packet_senders = Vec::with_capacity(num_workers);
104 let worker_dropped: Vec<Arc<AtomicU64>> = (0..num_workers)
105 .map(|_| Arc::new(AtomicU64::new(0)))
106 .collect();
107 let shutdown_flag = Arc::new(AtomicBool::new(false));
108
109 for (worker_id, dropped_slot) in worker_dropped.iter().enumerate() {
110 let (tx, rx) = bounded::<Vec<u8>>(queue_size);
111 packet_senders.push(tx);
112
113 let result_sender_clone = result_sender.clone();
114 let matcher_clone = matcher.clone();
115 let shutdown_flag_clone = Arc::clone(&shutdown_flag);
116 let worker_filter = filter_config.clone();
117 let dropped_clone = Arc::clone(dropped_slot);
118
119 thread::Builder::new()
120 .name(format!("http-worker-{worker_id}"))
121 .spawn(move || {
122 Self::worker_loop(
123 worker_id,
124 rx,
125 result_sender_clone,
126 matcher_clone,
127 dropped_clone,
128 shutdown_flag_clone,
129 WorkerConfig { batch_size, timeout_ms, max_connections },
130 worker_filter,
131 )
132 })
133 .map_err(|e| {
134 HuginnNetHttpError::Misconfiguration(format!(
135 "Failed to spawn worker thread {worker_id}: {e}"
136 ))
137 })?;
138 }
139
140 Ok(Arc::new(Self {
141 packet_senders: Arc::new(packet_senders),
142 result_sender: Arc::new(Mutex::new(Some(result_sender))),
143 shutdown_flag,
144 dispatched_count: AtomicU64::new(0),
145 dropped_count: AtomicU64::new(0),
146 worker_dropped,
147 num_workers: num_workers_nz,
148 batch_size,
149 timeout_ms,
150 }))
151 }
152
153 #[allow(clippy::too_many_arguments)]
155 fn worker_loop(
156 worker_id: usize,
157 rx: crossbeam_channel::Receiver<Vec<u8>>,
158 result_sender: std::sync::mpsc::Sender<HttpAnalysisResult>,
159 matcher: Option<SharedHttpMatcher>,
160 dropped: Arc<AtomicU64>,
161 shutdown_flag: Arc<AtomicBool>,
162 config: WorkerConfig,
163 filter_config: Option<FilterConfig>,
164 ) {
165 use crossbeam_channel::RecvTimeoutError;
166 use std::time::Duration;
167
168 debug!("HTTP worker {} started", worker_id);
169
170 let mut http_flows = TtlCache::new(config.max_connections);
171 let http_processors = HttpProcessors::new();
172 let timeout = Duration::from_millis(config.timeout_ms);
173 let mut batch = Vec::with_capacity(config.batch_size);
174
175 loop {
176 if shutdown_flag.load(Ordering::Relaxed) {
177 debug!("HTTP worker {} received shutdown signal", worker_id);
178 break;
179 }
180
181 match rx.recv_timeout(timeout) {
182 Ok(packet) => {
183 batch.push(packet);
184
185 while batch.len() < config.batch_size {
186 match rx.try_recv() {
187 Ok(packet) => batch.push(packet),
188 Err(_) => break,
189 }
190 }
191
192 let matcher_ref: Option<&dyn HttpMatcher> =
193 matcher.as_deref().map(|m| m as &dyn HttpMatcher);
194 for packet in batch.drain(..) {
195 match Self::process_packet(
196 &packet,
197 &mut http_flows,
198 &http_processors,
199 matcher_ref,
200 filter_config.as_ref(),
201 ) {
202 Ok(result) => {
203 if result_sender.send(result).is_err() {
204 debug!("HTTP worker {} result channel closed", worker_id);
205 return;
206 }
207 }
208 Err(_) => {
209 dropped.fetch_add(1, Ordering::Relaxed);
213 }
214 }
215 }
216 }
217 Err(RecvTimeoutError::Timeout) => {
218 if shutdown_flag.load(Ordering::Relaxed) {
219 debug!("HTTP worker {} received shutdown signal", worker_id);
220 break;
221 }
222 continue;
223 }
224 Err(RecvTimeoutError::Disconnected) => {
225 debug!("HTTP worker {} channel disconnected", worker_id);
226 break;
227 }
228 }
229 }
230
231 debug!("HTTP worker {} stopped", worker_id);
232 }
233
234 fn process_packet(
235 packet: &[u8],
236 http_flows: &mut TtlCache<FlowKey, TcpFlow>,
237 http_processors: &HttpProcessors,
238 matcher: Option<&dyn HttpMatcher>,
239 filter: Option<&FilterConfig>,
240 ) -> Result<HttpAnalysisResult, HuginnNetHttpError> {
241 if let Some(filter_cfg) = filter {
242 if !raw_filter::apply(packet, filter_cfg) {
243 debug!("Filtered out packet before parsing");
244 return Ok(HttpAnalysisResult::empty());
245 }
246 }
247
248 use crate::packet_parser::{parse_packet, IpPacket};
249 use crate::process;
250
251 match parse_packet(packet) {
252 IpPacket::Ipv4(ipv4) => {
253 process::process_ipv4_packet(&ipv4, http_flows, http_processors, matcher)
254 }
255 IpPacket::Ipv6(ipv6) => {
256 process::process_ipv6_packet(&ipv6, http_flows, http_processors, matcher)
257 }
258 IpPacket::None => Ok(HttpAnalysisResult::empty()),
259 }
260 }
261
262 pub fn worker_index_for_packet(&self, packet: &[u8]) -> usize {
266 packet_hash::hash_flow(packet, self.num_workers.get())
267 }
268
269 pub fn dispatch(&self, packet: Vec<u8>) -> DispatchResult {
270 if self.shutdown_flag.load(Ordering::Relaxed) {
271 self.dropped_count.fetch_add(1, Ordering::Relaxed);
272 return DispatchResult::Dropped;
273 }
274
275 let worker_id = self.worker_index_for_packet(&packet);
276
277 self.dispatched_count.fetch_add(1, Ordering::Relaxed);
278
279 if let Some(sender) = self.packet_senders.get(worker_id) {
280 match sender.try_send(packet) {
281 Ok(()) => DispatchResult::Queued,
282 Err(_) => {
283 self.dropped_count.fetch_add(1, Ordering::Relaxed);
284 self.worker_dropped[worker_id].fetch_add(1, Ordering::Relaxed);
285 DispatchResult::Dropped
286 }
287 }
288 } else {
289 self.dropped_count.fetch_add(1, Ordering::Relaxed);
290 DispatchResult::Dropped
291 }
292 }
293
294 pub fn stats(&self) -> PoolStats {
295 let workers = self
296 .packet_senders
297 .iter()
298 .enumerate()
299 .map(|(id, sender)| WorkerStats {
300 id,
301 queue_size: sender.len(),
302 dropped: self.worker_dropped[id].load(Ordering::Relaxed),
303 })
304 .collect();
305
306 PoolStats {
307 total_dispatched: self.dispatched_count.load(Ordering::Relaxed),
308 total_dropped: self.dropped_count.load(Ordering::Relaxed),
309 workers,
310 }
311 }
312
313 pub fn shutdown(&self) {
315 self.shutdown_flag.store(true, Ordering::Relaxed);
316
317 if let Ok(mut sender) = self.result_sender.lock() {
318 *sender = None;
319 }
320 }
321}
322
323impl fmt::Display for PoolStats {
324 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325 writeln!(f, "HTTP Worker Pool Statistics:")?;
326 writeln!(f, " Total dispatched: {}", self.total_dispatched)?;
327 writeln!(f, " Total dropped: {}", self.total_dropped)?;
328 writeln!(f, " Workers: {}", self.workers.len())?;
329 for worker in &self.workers {
330 writeln!(f, " {worker}")?;
331 }
332 Ok(())
333 }
334}
335
336impl fmt::Display for WorkerStats {
337 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338 write!(
339 f,
340 "Worker {}: queue_size={}, dropped={}",
341 self.id, self.queue_size, self.dropped
342 )
343 }
344}