stamparm/maltrail · error
one of the two matched
Error message
one of the two matched
What it means
This is a Rust `Option::expect` panic inside the TCP SYN handling path. The code matches on two lookups (`dst_port_hit` and `dst_hit`) against the trails database and asserts that at least one of them matched; if both are `None`, the invariant 'one of the two matched' is broken and the process panics. It exists to avoid re-checking an option the author believed was already proven non-empty.
Solutions
- Replace the `expect` with a graceful `None =>` arm that skips emit_ep (and optionally logs) instead of panicking, so a missing trail never kills the sensor thread.
- Verify the caller's selection logic guarantees at least one of `dst_port_hit`/`dst_hit` is `Some`; fix the upstream filter if it can admit packets with no trail match.
- If the trails DB can be reloaded concurrently, snapshot the lookup results (or hold the guard) so both lookups observe the same DB version.
- Add a regression test with a SYN to an IP absent from the trails DB to ensure it is dropped, not panicked on.
Example fix
// before
None => {
let (info, reference) = dst_hit.expect("one of the two matched");
(ep.dst.render().as_str().to_string(), info, reference, TRAIL::IP)
}
// after
None => match dst_hit {
Some((info, reference)) =>
(ep.dst.render().as_str().to_string(), info, reference, TRAIL::IP),
None => return, // no trail matched; drop instead of panicking
} Defensive patterns
Strategy: try-catch
Validate before calling
// Rust has no try/catch; guard before unwrapping
if dst_port_hit.is_none() && dst_hit.is_none() { return; } // skip packet, no panic Type guard
fn trail_present(h: &Option<(Info, Ref)>) -> bool { h.is_some() } Try / catch
// contain panics at the packet-loop boundary
catch_unwind(AssertUnwindSafe(|| handle_syn(st, ep)))
.unwrap_or_else(|_| log::warn!("syn handler panicked; packet dropped")); Prevention
- Never use expect() for conditions that depend on runtime data; use match/if let
- Add tests feeding packets with no matching trails entry
- Keep trail lookup and emission in one consistent DB snapshot
- Replace paired-lookup invariants with a single lookup returning both forms
When it happens
Trigger: A TCP SYN packet whose destination IP and destination IP:port both fail to match any entry in the trails database reaches the `match dst_port_hit` arm with `dst_port_hit == None` and `dst_hit == None`; the `expect` then panics. Any desynchronization between the predicate that decided this packet was a 'hit' and the actual two `db()` lookups (e.g. key normalization differences, concurrent db reload) triggers it.
Common situations: Running a custom/edited trails feed that removed an IP entry between the filtering pass and the match pass; feeding packets whose addresses are rendered differently by `addr_port` vs `render`; races where the trails DB is swapped/reloaded while packets are in flight.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- just probed
- question
- hmac accepts any key length
- settings::init() must run before statics()
- aho-corasick build
AI-assisted analysis of stamparm/maltrail@77cfb06d76 (2026-09-13).
Data as JSON: /api/errors/5f54df6aee4b0b3a.
Report an issue: GitHub.
Appendix: source
Thrown at sensor/src/process.rs:728
if previous == Some(stamp) {
return; // skip bursts
}
st.metrics.trail_lookups += 1;
let dst_hit = st.trails.db().get_ip(ep.dst).map(|v| (v.info.to_string(), v.reference.to_string()));
let dst_port_hit =
st.trails.db().get_ip_port(ep.dst, ep.dst_port).map(|v| (v.info.to_string(), v.reference.to_string()));
if dst_hit.is_some() || dst_port_hit.is_some() {
let previous_logged = st.last_logged_syn.replace(stamp);
if previous_logged != Some(stamp) {
// IPORT iff the matched key is the addr_port form (not the bare IP).
let (trail, info, reference, trail_type) = match dst_port_hit {
Some((info, reference)) => {
(ep.dst.addr_port(ep.dst_port).as_str().to_string(), info, reference, TRAIL::IPORT)
}
None => {
let (info, reference) = dst_hit.expect("one of the two matched");
(ep.dst.render().as_str().to_string(), info, reference, TRAIL::IP)
}
};
let parking_off_web = info.contains("parking site") && !matches!(ep.dst_port, 80 | 443);
if !info.contains("attacker") && !parking_off_web {
emit_ep(st, sec, usec, ep, PROTO::TCP, trail_type, Field::Text(trail), &info, &reference);
}
}
} else if !ep.dst.is_localhost() {
let src_hit = st.trails.db().get_ip(ep.src).map(|v| (v.info.to_string(), v.reference.to_string()));
let src_port_hit =
st.trails.db().get_ip_port(ep.src, ep.src_port).map(|v| (v.info.to_string(), v.reference.to_string()));
if src_hit.is_some() || src_port_hit.is_some() {
let previous_logged = st.last_logged_syn.replace(stamp);
if previous_logged != Some(stamp) {
let (trail, info, reference, trail_type) = match src_port_hit {
Some((info, reference)) => {
(ep.src.addr_port(ep.src_port).as_str().to_string(), info, reference, TRAIL::IPORT)View on GitHub (pinned to 77cfb06d76)