stamparm/maltrail · error

capture error on worker

Error message

capture error on worker {} ({e})

What it means

This error is logged when the libpcap packet-capture read call inside the worker loop returns Err(e). In offline (pcap file replay) mode it means the capture file could not be read at all (or hit a malformed/truncated region); if zero packets were read the worker fails with WorkerError::Capture instead of silently reporting a successful replay of zero packets. In live-capture mode it means the capture handle hit an error (e.g. interface disappeared); only after LIVE_CAPTURE_ERROR_LIMIT consecutive errors does it become fatal.

Solutions

  1. Validate the capture file with tshark -r file or capinfos before replaying; fix the source file (e.g. re-merge with mergecap -F pcap so all interfaces share one link type).
  2. Check the log for '(no packets could be read)': if absent, packets were already processed and the error is only a truncated tail — treat the run's events as valid.
  3. For live captures, verify interface name and permissions (capabilities/root, or configure the ambient-capture capability); the failure becomes fatal only after repeated consecutive errors, so check whether LIVE_CAPTURE_ERROR_LIMIT was hit.
  4. Ensure the pcapng was written completely before replay; use the final, non-growing file.

Example fix

// before
sensor replay merged.pcapng   # exits 0, received=0, 'capture error on worker 0'
// after
mergecap -w merged.pcap merged1.pcapng merged2.pcapng  # single link type / pcap format
sensor replay merged.pcap    # packets parsed, events produced
Defensive patterns

Strategy: validation

Validate before calling

import subprocess, sys
def validate_capture(path: str) -> bool:
    """Ensure libpcap/tshark can read the capture before replaying."""
    r = subprocess.run(["capinfos", path], capture_output=True, text=True)
    if r.returncode != 0:
        print(f"unreadable capture: {r.stderr}", file=sys.stderr)
        return False
    if int(r.stdout.split('Number of packets:')[1].split()[0]) == 0:
        print('capture has zero packets', file=sys.stderr)
        return False
    return True

assert validate_capture('merged.pcapng'), 'fix capture file before replay'

Type guard

fn is_readable_capture(path: &str) -> bool {
    pcap::Capture::from_file(path)
        .map(|mut c| c.next_packet().is_ok())
        .unwrap_or(false)
}

Try / catch

match sensor.run_all() {
    Ok(report) if report.received > 0 => { /* success */ }
    Ok(report) => eprintln!("replay succeeded but 0 packets read — check capture file"),
    Err(sensor::Error::Capture(msg)) => eprintln!("capture failed: {msg}"),
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Calling run_all/run with an offline pcap/pcapng file that libpcap cannot read — e.g. a mergecap pcapng whose interfaces have different link types — or a truncated/corrupt capture file; or a live capture whose interface errors out (device gone, permission/privilege loss, BPF/capture handle failure).

Common situations: Replaying a mergecap-produced pcapng with mismatched per-interface link types; a capture file cut off mid-write (truncated tail after some packets); running a live capture and the NIC is removed or link state changes; insufficient privileges causing the capture handle to degrade.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of stamparm/maltrail@77cfb06d76 (2026-09-13). Data as JSON: /api/errors/44865da8522b8663. Report an issue: GitHub.

Appendix: source

Thrown at sensor/src/worker.rs:225

                                handle = next;
                                label = next_label;
                                file_started = Instant::now();
                                file_packets = 0;
                                file_events = st.sink.events;
                                datalink = handle.datalink();
                                poll_fd = handle.selectable_fd();
                                continue;
                            }
                            None => {
                                outcome = Ok(WorkerExit::OfflineEof);
                                fatal = true;
                            }
                        }
                    }
                    break;
                }
                Err(e) => {
                    crate::output::log_error(&format!("capture error on worker {} ({e})", ctx.id), true);
                    if offline {
                        // A capture error is NOT a clean end of file. If it happened before a
                        // single packet was read, the capture could not be read at all, and
                        // reporting a successful replay of zero packets is the offline version
                        // of the silent blind spot Gate 1.1 fixed: the analyst sees "no
                        // detections" when the truth is "your file was never parsed".
                        //
                        // Found by the shadow harness on a `mergecap` output — libpcap refuses a
                        // pcapng whose interfaces have different link types, and the sensor
                        // replayed it to "success" with received=0.
                        //
                        // After packets HAVE been read this is a truncated tail: the events found
                        // so far are real and worth keeping, so the run still succeeds and the
                        // error stands in the log.
                        if st.metrics.packets_received == 0 {
                            outcome = Err(WorkerError::Capture(format!("{e} (no packets could be read)")));
                        } else {
                            outcome = Ok(WorkerExit::OfflineEof);

View on GitHub (pinned to 77cfb06d76)