feat(profiler): wire p0f-v2 matcher into sniffer_rollup priority chain
The ~30-signature hand-rolled p0f-lite table in decnet/sniffer/p0f.py
misses most real-world attackers (yesterday's SLOW SCAN being a
textbook case — 9 hours of events, 19 hits, os_guess = NULL). The
375-sig vendored p0f v2 DB was already there; this commit actually
calls it.
New resolution chain in sniffer_rollup:
1. Enabled OS-fingerprint providers (p0f-v2 default, via
DECNET_OSFP_PROVIDERS) tried in declared order. Provider with
highest-confidence match across all enabled sources wins.
2. Modal os_guess label from the sniffer's hand-rolled p0f.py.
Kept as fallback because v2's DB predates post-2006 kernels.
3. TTL bucket (linux / windows / embedded). Coarse but never wrong.
Wiring details:
- _match_via_osfp_providers: never raises — factory / provider
failures collapse to None and the chain falls through to the
old modal-label / TTL path. A corrupt .fp file or misconfigured
DECNET_OSFP_PROVIDERS must never wedge a profile rebuild.
- tcp_fp_context tracks whether the LATEST tcp_fp snapshot came
from a passive SYN ('syn' → p0f.fp) or an active prober probe
('synack' → p0fa.fp). Routes to the right sig list.
- initial-TTL normalisation via decnet.sniffer.p0f.initial_ttl.
Observation's TTL may be N hops below the OS's initial; v2
signatures match on the canonical bucket.
Soft-field semantics on Signature.score(): df and total_len are now
skip-checked when the observation is missing them. Sniffer doesn't
currently emit either SD field; a literal-constraint sig
shouldn't hard-reject a match solely because of upstream
incompleteness. Hard fields (window, ttl, options_sig, quirks)
still hard-reject on absent/mismatched input — those are the real
discriminators. Promote df / total_len back to hard the moment the
sniffer starts emitting them.
+2 integration tests on TestSnifferRollup, +2 soft-field tests on
test_signature. Full regression: 166 tests across tests/prober/osfp
+ tests/profiler all green.
This commit is contained in:
@@ -151,29 +151,38 @@ class Signature:
|
||||
|
||||
def score(self, obs: dict[str, Any]) -> Optional[float]:
|
||||
"""Return a confidence in [0, 1] on match, or None if any field
|
||||
rejects the observation."""
|
||||
rejects the observation.
|
||||
|
||||
Soft-field semantics: ``df`` and ``total_len`` are treated as
|
||||
"skip check when observation is missing" — the sniffer doesn't
|
||||
currently emit either, and a literal-constraint sig shouldn't
|
||||
reject a match solely because the observation is upstream-
|
||||
incomplete. Hard fields (``window``, ``ttl``, ``options_sig``,
|
||||
``quirks``) still hard-reject on absent or mismatched input —
|
||||
those are the real discriminators."""
|
||||
mss = obs.get("mss")
|
||||
# Window
|
||||
# Window (hard)
|
||||
if not self.wss.matches(obs.get("window"), mss):
|
||||
return None
|
||||
# TTL — initial-TTL bucket must match exactly. The profiler is
|
||||
# expected to have rounded the observed TTL up to the nearest
|
||||
# bucket already via decnet.sniffer.p0f.initial_ttl.
|
||||
# bucket already via decnet.sniffer.p0f.initial_ttl. (hard)
|
||||
obs_ttl = obs.get("ttl")
|
||||
if obs_ttl is None or obs_ttl != self.ttl:
|
||||
return None
|
||||
# DF (None on the sig side = wildcard)
|
||||
# DF (soft — skip when unknown)
|
||||
if self.df is not None:
|
||||
obs_df = obs.get("df")
|
||||
if obs_df is None or bool(obs_df) != self.df:
|
||||
if obs_df is not None and bool(obs_df) != self.df:
|
||||
return None
|
||||
# Total length
|
||||
if not self.total_len.matches(obs.get("total_len")):
|
||||
# Total length (soft — skip when unknown)
|
||||
obs_total = obs.get("total_len")
|
||||
if obs_total is not None and not self.total_len.matches(obs_total):
|
||||
return None
|
||||
# Options
|
||||
# Options (hard)
|
||||
if not _options_match(self.options, obs.get("options_sig")):
|
||||
return None
|
||||
# Quirks — must match as a set.
|
||||
# Quirks — must match as a set. (hard)
|
||||
obs_quirks = obs.get("quirks") or frozenset()
|
||||
if not isinstance(obs_quirks, frozenset):
|
||||
obs_quirks = frozenset(obs_quirks)
|
||||
|
||||
Reference in New Issue
Block a user