Skip to content

Commit ff79036

Browse files
simonmorleySimon Morley
andauthored
feat: per-batch scanner lock for concurrent discovery (#2)
* feat: hold scanner lock per-batch instead of per-scan in discover_bpf Previously the scanner mutex was held for the entire batch-send phase, serializing all concurrent discovery requests. Now each batch acquires and releases the lock independently, with a per-request stealth profile clone instead of mutating shared state. Concurrent scans interleave their batches rather than fully serializing. * fix: remove passthrough mode — BPF-only, no degraded operation Passthrough mode silently ran with raw socket TX when BPF had issues, producing discovery results without timing data. Remove the config option entirely so limpet either starts with full BPF or fails hard. --------- Co-authored-by: Simon Morley <simon.morley+debbie@mac.com>
1 parent 7e7d15a commit ff79036

2 files changed

Lines changed: 16 additions & 73 deletions

File tree

src/cli/mod.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -178,18 +178,12 @@ use crate::ScanResult;
178178
/// On non-Linux, returns the engine directly (connect-only for dev).
179179
#[cfg(target_os = "linux")]
180180
fn create_engine(interface: Option<String>) -> Result<Engine, String> {
181-
Engine::new(ScanEngineConfig {
182-
interface,
183-
passthrough: false,
184-
})
181+
Engine::new(ScanEngineConfig { interface })
185182
}
186183

187184
#[cfg(not(target_os = "linux"))]
188185
fn create_engine(interface: Option<String>) -> Result<Engine, String> {
189-
Ok(Engine::new(ScanEngineConfig {
190-
interface,
191-
passthrough: false,
192-
}))
186+
Ok(Engine::new(ScanEngineConfig { interface }))
193187
}
194188

195189
// ─────────────────────────────────────────────────────────────────────────────

src/engine.rs

Lines changed: 14 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ use crate::{PortState, ScanResult, ScannedPort, TimingBackend, TimingRequest, Ti
2222
pub struct ScanEngineConfig {
2323
/// Network interface for XDP (None = auto-detect from /proc/net/route).
2424
pub interface: Option<String>,
25-
/// When true, use raw socket TX only (no AF_XDP redirect).
26-
/// BPF timestamps packets but XDP_PASS lets them through to the kernel.
27-
pub passthrough: bool,
2825
}
2926

3027
/// Scanning engine. Shared via `Arc<Engine>` between discovery and timing loops.
@@ -44,8 +41,6 @@ pub struct ScanEngine {
4441
scanner: Arc<Mutex<SynScanner>>,
4542
interface: String,
4643
backend: TimingBackend,
47-
#[allow(dead_code)]
48-
passthrough: bool,
4944
}
5045

5146
// ─────────────────────────────────────────────────────────────────────────────
@@ -108,10 +103,6 @@ impl Engine {
108103
/// - SYN scanner creation fails (no IPv4 on interface, raw socket error)
109104
#[cfg(target_os = "linux")]
110105
pub fn new(config: ScanEngineConfig) -> Result<Self, String> {
111-
if config.passthrough {
112-
tracing::info!("passthrough mode: using raw socket TX, no AF_XDP redirect");
113-
}
114-
115106
let (backend, collector) = crate::timing::detect_timing_backend(&config.interface)
116107
.map_err(|e| {
117108
format!(
@@ -127,12 +118,11 @@ impl Engine {
127118
tracing::info!(
128119
backend = %backend,
129120
interface = %iface,
130-
passthrough = config.passthrough,
131121
"BPF timing backend initialised"
132122
);
133123

134124
let bpf = Arc::new(Mutex::new(collector));
135-
let scanner = Self::create_scanner(&iface, &bpf, config.passthrough).ok_or_else(|| {
125+
let scanner = Self::create_scanner(&iface, &bpf).ok_or_else(|| {
136126
format!(
137127
"SYN scanner creation failed on interface '{iface}'. \
138128
Check that the interface has an IPv4 address and raw socket permissions."
@@ -144,7 +134,6 @@ impl Engine {
144134
scanner,
145135
interface: iface,
146136
backend,
147-
passthrough: config.passthrough,
148137
}))
149138
}
150139

@@ -158,18 +147,13 @@ impl Engine {
158147
}
159148
}
160149

161-
/// Create a `SynScanner` with the appropriate sender.
162-
///
163-
/// In passthrough mode, uses `RawSocketSender` only (no AF_XDP redirect).
164-
/// Otherwise creates a `HybridSender` and registers its AF_XDP fd in xsk_map.
150+
/// Create a `SynScanner` with raw socket TX + BPF timestamps.
165151
#[cfg(target_os = "linux")]
166152
fn create_scanner(
167153
iface: &str,
168-
bpf: &Arc<Mutex<BpfTimingCollector>>,
169-
passthrough: bool,
154+
_bpf: &Arc<Mutex<BpfTimingCollector>>,
170155
) -> Option<Arc<Mutex<SynScanner>>> {
171156
use crate::scanner::afxdp_sender::AfXdpSend;
172-
use crate::scanner::hybrid_sender::HybridSender;
173157
use crate::scanner::raw_socket_sender::RawSocketSender;
174158
use crate::scanner::syn_sender::interface_source_ip;
175159

@@ -181,39 +165,11 @@ impl Engine {
181165
}
182166
};
183167

184-
let xdp_sender: Box<dyn AfXdpSend> = if passthrough {
185-
match RawSocketSender::new(src_ip, Some(iface)) {
186-
Ok(s) => Box::new(s),
187-
Err(e) => {
188-
tracing::error!(error = %e, "raw socket sender failed");
189-
return None;
190-
}
191-
}
192-
} else {
193-
match HybridSender::new(iface, 0, src_ip) {
194-
Ok(sender) => {
195-
// Register AF_XDP socket in BPF xsk_map.
196-
// try_lock() is safe here — called at init, no contention.
197-
if let Ok(bpf_guard) = bpf.try_lock() {
198-
if let Err(e) = bpf_guard.register_xsk_fd(sender.fd()) {
199-
tracing::warn!(error = %e, "xsk_map registration failed");
200-
}
201-
}
202-
Box::new(sender)
203-
}
204-
Err(e) => {
205-
tracing::warn!(
206-
error = %e,
207-
"hybrid sender unavailable — falling back to raw socket TX"
208-
);
209-
match RawSocketSender::new(src_ip, Some(iface)) {
210-
Ok(s) => Box::new(s),
211-
Err(e) => {
212-
tracing::error!(error = %e, "raw socket fallback also failed");
213-
return None;
214-
}
215-
}
216-
}
168+
let xdp_sender: Box<dyn AfXdpSend> = match RawSocketSender::new(src_ip, Some(iface)) {
169+
Ok(s) => Box::new(s),
170+
Err(e) => {
171+
tracing::error!(error = %e, "raw socket sender failed");
172+
return None;
217173
}
218174
};
219175

@@ -331,21 +287,19 @@ impl ScanEngine {
331287
let timeout = Duration::from_millis(timeout_ms as u64);
332288
let target_ip_u32 = u32::from_be_bytes(target_ip.octets());
333289

334-
// Apply per-request pacing to the shared scanner. We hold the lock
335-
// for the entire batch-send phase, so no concurrent access sees the
336-
// changed profile. Restored to Aggressive after sending.
337-
let mut scanner_guard = self.scanner.lock().await;
338-
let original_profile = scanner_guard.profile().clone();
290+
// Build per-request stealth profile (no shared state mutation).
291+
// Scanner lock is held per-batch, not per-scan, so concurrent
292+
// discovery requests can interleave their batches.
339293
let mut scan_profile = StealthProfile::linux_6x_default();
340294
request.pacing.apply_to(&mut scan_profile);
341-
scanner_guard.set_profile(scan_profile);
342295

343296
let mut all_probes = Vec::new();
344297
for batch in ports.chunks(batch_size) {
298+
let mut scanner_guard = self.scanner.lock().await;
299+
scanner_guard.set_profile(scan_profile.clone());
345300
let result = match scanner_guard.send_syn_batch(target_ip, batch) {
346301
Ok(r) => r,
347302
Err(e) => {
348-
scanner_guard.set_profile(original_profile);
349303
drop(scanner_guard);
350304
return ScanResult {
351305
request_id: request.request_id,
@@ -363,12 +317,9 @@ impl ScanEngine {
363317

364318
// Drain AF_XDP RX ring between batches to prevent overflow
365319
scanner_guard.poll_rx(0);
320+
drop(scanner_guard);
366321
}
367322

368-
// Restore Aggressive profile for timing probes
369-
scanner_guard.set_profile(original_profile);
370-
drop(scanner_guard);
371-
372323
// Early-return polling: check every 5ms if all probes have responses.
373324
// Falls back to full timeout as the deadline.
374325
{
@@ -736,7 +687,6 @@ mod tests {
736687
// Either way, it must not panic.
737688
let result = Engine::new(ScanEngineConfig {
738689
interface: None,
739-
passthrough: true,
740690
});
741691
match result {
742692
Ok(engine) => {
@@ -757,7 +707,6 @@ mod tests {
757707
fn test_engine_new_returns_connect_only_on_non_linux() {
758708
let engine = Engine::new(ScanEngineConfig {
759709
interface: None,
760-
passthrough: true,
761710
});
762711
assert_eq!(engine.backend_str(), "connect");
763712
}

0 commit comments

Comments
 (0)