feat(sniffer): IP-ID sequence classifier (random/incremental/zero/constant)

Adds a per-source-IP rolling sample buffer (deque, maxlen=8) for IP-ID
values seen on attacker SYNs and a stdlib-only classifier in
decnet/sniffer/seq_class.py. Each new SYN appends ip.id and re-classifies
the buffer; the result is logged on tcp_syn_fingerprint events alongside
sample count.

The dedup key now folds in ipid_class so a transition from 'unknown' to
a definitive verdict emits exactly one fresh event instead of being
suppressed by the old (os|options) key. Profiler rollup carries the
latest non-'unknown' label into attacker.tcp_fingerprint.

UI surfaces it as a colour-coded tag in the TCP STACK panel: random
neutral, incremental amber, zero/constant green (the strong signal).
This commit is contained in:
2026-04-26 20:28:32 -04:00
parent b0b08754d0
commit 0e40cc8ae1
6 changed files with 204 additions and 3 deletions

View File

@@ -22,6 +22,7 @@ interface AttackerBehavior {
tos?: number | null;
dscp?: number | null;
ecn?: number | null;
ipid_class?: string | null;
} | null;
retransmit_count: number;
behavior_class: string | null;
@@ -145,6 +146,18 @@ const HashRow: React.FC<{ label: string; value?: string | null }> = ({ label, va
);
};
// Random ISN/IP-ID is the modern default; non-random patterns are
// fingerprinting gold (legacy stacks, custom raw-socket tools).
const seqClassColor = (cls: string): string | undefined => {
switch (cls) {
case 'random': return undefined; // neutral, expected
case 'incremental': return '#e5c07b'; // amber — uncommon
case 'zero':
case 'constant': return '#98c379'; // green — strong signal
default: return undefined;
}
};
const Tag: React.FC<{ children: React.ReactNode; color?: string }> = ({ children, color }) => (
<span style={{
fontSize: '0.7rem', padding: '2px 8px', letterSpacing: '1px',
@@ -755,6 +768,9 @@ const TcpStackBlock: React.FC<{ b: AttackerBehavior }> = ({ b }) => {
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{fp.has_sack && <Tag>SACK</Tag>}
{fp.has_timestamps && <Tag>TS</Tag>}
{fp.ipid_class && fp.ipid_class !== 'unknown' && (
<Tag color={seqClassColor(fp.ipid_class)}>IPID:{fp.ipid_class.toUpperCase()}</Tag>
)}
</div>
{fp.options_sig && (
<div>