feat(net): PIM/MSDP Watch (new) + IGMP Watch v5 (package) + OSI visib… · PierreGode/Ragnar@fe5b2aa · GitHub
Skip to content

Commit fe5b2aa

Browse files
committed
feat(net): PIM/MSDP Watch (new) + IGMP Watch v5 (package) + OSI visibility matrix refresh
PIM / MSDP Watch — new passive multicast-routing-control-plane scanner. Covers PIM-SM/ DM/BiDir/SSM, RP discovery (BSR / Cisco Auto-RP / Embedded-RP / Anycast-RP) and MSDP (RFC 3618). IP proto 103, multicast in the clear. Flags attack (forged-Hello DR hijack, rogue Candidate-RP/BSR, Register & MSDP-SA injection, Assert contention, floods) and posture/structural (IPsec-AH protection loss, SSM violations, Embedded-RP/Auto-RP anomalies, bad-checksum/malformed). 88 codes (37 posture + 51 attack), NO CVEs. RPF is a router-internal RIB decision, covered by 3 observable heuristics (PMW-060/081/068), never guessed. Engine vendored verbatim at python/pimwatch.py (+ fixtures + selftest); do_pim_watch adapter via a python/-on-sys.path guard using the engine's in-capture learn-then-arm (no persisted baseline). Verdict attack->attack, notice+/warn/critical posture->anomaly, info-only->clean. Dual-stack: PIM v4+v6 (EH chains walked), Auto-RP + MSDP IPv4-only (v6 equivalents are Embedded-RP + Anycast-RP on the PIM side). Route /api/net/pim-watch; UI card in L3 (count 14->15); new 'pim' selftest suite (8 scenarios incl. the vendored engine's 179-check suite + Scapy pcap e2e). Live-validated in a sealed netns (bad-checksum PIM replay -> PMW-003 off the real tcpdump path). IGMP Watch v5 — the standalone igmpwatch/ package (python3 -m igmpwatch; sibling to the in-app do_igmp_watch) updated to v5: restructured pipeline (new main.py/models.py/ alerting.py, alert.py removed), expanded detector matrix, same 3 IGMP/MLD CVEs the in-app watcher already detects (CVE-2019-5608/2025-50681/2026-53275). NO CVE-index change — gen_cve_list gains an igmpwatch/ -> 'IGMP / MLD Watch' owner (+ selftest.py) so the package's CVE strings dedupe to the existing owner. selftest 123/123 (harness scapy-compat fix: v3 builders now carry the IGMPv3() base layer that supplies the type byte on current scapy, where a bare IGMPv3mr(...) serialized type 0x00). docs/igmpwatch.md refreshed. OSI visibility matrix refreshed — web/images/osi/matrix_full.jpg replaced with the current module set (RADIUS at L7, RIP + PIM/MSDP at L3); the 7 per-layer band crops regenerated from it; matrix cache-bust bumped across all 16 references. JS cache-bust bumped. Aggregate do_routing_selftest 58 suites, all green.
1 parent a944eb1 commit fe5b2aa

35 files changed

Lines changed: 8051 additions & 1408 deletions

‎docs/igmpwatch.md‎

Lines changed: 245 additions & 94 deletions
Large diffs are not rendered by default.

‎docs/nettools.md‎

Lines changed: 46 additions & 1 deletion

‎docs/releases.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
## Releases
44

5+
### 2026-10-17
6+
7+
#### [#969](https://github.com/PierreGode/Ragnar/pull/969) — feat(net): PIM/MSDP Watch (new) + IGMP Watch v5 (package) + OSI visibility matrix refresh
8+
*branch `feature/pim-msdp-igmp-v5`*
9+
10+
- **New passive watcher: PIM / MSDP** — the multicast routing control plane. Covers PIM-SM (RFC 7761), PIM-DM (RFC 3973), BiDir (RFC 5015), SSM (RFC 4607), RP discovery via BSR (RFC 5059) / Cisco Auto-RP (UDP/496) / Embedded-RP (RFC 3956) / Anycast-RP (RFC 4610), and inter-domain MSDP (RFC 3618, TCP/639). Rides IP proto 103, multicast in the clear (`224.0.0.13` / `ff02::d`) with no auth beyond optional IPsec AH. Flags **attack** (forged Hello DR-hijack, rogue Candidate-RP/BSR, PIM Register & MSDP SA injection, Assert contention, Join/Prune/SA floods) and **posture/structural** (IPsec-AH protection loss, SSM-range violations, Embedded-RP/Auto-RP anomalies, bad-checksum/truncated/malformed messages). **88 codes** (37 posture + 51 attack), **no CVEs** (protocol-abuse engine, not a version scanner). RPF (a router-internal RIB decision) is covered by three observable heuristics (`PMW-060`/`PMW-081`/`PMW-068`), never guessed at. Engine vendored verbatim at `python/pimwatch.py` (self-contained stdlib; scapy only on its own live path) + `python/pimwatch_fixtures.py` / `python/pimwatch_selftest.py`; new `do_pim_watch` adapter in `network_diagnostics.py` via a `python/`-on-`sys.path` guard, using the engine's in-capture learn-then-arm (no persisted baseline). Verdicts map attack→`attack`, warn/critical posture→`anomaly` (existing tokens). **Dual-stack by protocol pairing:** PIM is v4+v6 (extension-header chains walked); Auto-RP + MSDP are IPv4-only (their v6 equivalents are Embedded-RP and Anycast-RP, on the PIM side). Route `GET /api/net/pim-watch`; UI card in the L3 tab (count 14→15); new `pim` self-test suite (8 scenarios incl. the vendored engine's own 179-check suite + a Scapy pcap e2e leg). Live-validated in a sealed netns (bad-checksum PIM replay → `PMW-003` off the real tcpdump path).
11+
- **IGMP Watch v5** — the standalone `igmpwatch/` package (invoked `python3 -m igmpwatch`; sibling to the in-app `do_igmp_watch`) updated to v5: restructured pipeline (new `main.py` / `models.py` / `alerting.py`, `alert.py` removed), expanded flood/anomaly/recon/policy/posture detector matrix, and the same three IGMP/MLD CVEs the in-app watcher already detects (`CVE-2019-5608` / `CVE-2025-50681` / `CVE-2026-53275` → `IGMPW-003/001/002`). **No CVE-index change** — `gen_cve_list` gains an `igmpwatch/` → *IGMP / MLD Watch* owner so the package's CVE strings dedupe to the existing owner rather than double-count. Self-test **123/123** (a scapy-version compatibility fix in the harness: v3 report/query builders now carry the `IGMPv3()` base layer that supplies the type byte on current scapy, where a bare `IGMPv3mr(...)` serialized type `0x00`). Docs refreshed at [docs/igmpwatch.md](igmpwatch.md).
12+
- **OSI visibility matrix refreshed** — `web/images/osi/matrix_full.jpg` replaced with the current module set (now including RADIUS at L7 and RIP + PIM/MSDP at L3), and the seven per-layer band crops (`matrix_l1…l7.jpg`) regenerated from it; matrix cache-bust bumped across all 16 references.
13+
- Aggregate `do_routing_selftest` **58 suites**, all green.
14+
515
### 2026-10-16
616

717
#### [#968](https://github.com/PierreGode/Ragnar/pull/968) — feat(net): RADIUS Watch — passive RADIUS/EAP scanner (BlastRADIUS + FreeRADIUS CVEs)

‎igmpwatch.yaml‎

Lines changed: 108 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,112 @@
1-
# igmpwatch configuration. All keys optional; unset keys use built-in defaults.
2-
iface: eth0
3-
mode: learn # learn | enforce
4-
dedup_window: 60 # seconds to collapse repeat alerts
5-
db: /var/lib/igmpwatch/igmpwatch.db
6-
7-
detectors:
8-
window_s: 10
9-
multicast_storm_count: 100
10-
report_storm_count: 40
11-
query_storm_count: 10
12-
leave_storm_count: 30
13-
flap_count: 6
14-
group_scan_count: 20
15-
querier_contention_sources: 3
16-
learn_window_s: 180
17-
sensitive_groups: [] # e.g. ['239.10.0.0', '232.1.1.1'] — any unlisted join alerts
18-
allowlist: {} # e.g. {'aa:bb:cc:dd:ee:ff': ['239.1.1.1']}
19-
ssm_allowed: {} # e.g. {'232.1.1.1': ['10.0.0.1']}
1+
# igmpwatch configuration -- passive IGMP snooping security monitor
2+
# All keys are optional; anything omitted falls back to built-in defaults.
3+
4+
interface: eth0 # SPAN/mirror destination NIC (RX-only)
5+
db_path: /var/lib/igmpwatch/igmpwatch.db
6+
log_path: /var/log/igmpwatch/events.jsonl
7+
membership_timeout: 300 # s before an un-refreshed join ages out
8+
learning_period: 180 # s to baseline querier + membership before enforcing
9+
10+
# Protocol-conformance posture (not CVEs): non-conformant behaviour that
11+
# weakens the multicast security model, from legacy gear or deliberate downgrade.
12+
posture:
13+
flag_ssm_version_mismatch: true # IGMPW-P01 / RFC 4604 - pre-v3 membership in SSM range
14+
15+
# Passive detection of documented CVE trigger shapes. A hit means a packet
16+
# matching the published malformation crossed the segment -- NOT that the target
17+
# is unpatched or that exploitation succeeded. Each rule runs on BOTH families.
18+
cve:
19+
flag_invalid_record_type: true # IGMPW-001 / CVE-2025-50681 (7.5) igmpproxy
20+
flag_query_source_overrun: true # IGMPW-002 / CVE-2026-53275 (8.8) Linux MLD UAF
21+
flag_fragmented_membership: true # IGMPW-003 / CVE-2019-5608 (9.8) FreeBSD MLDv2
22+
23+
mld:
24+
enable: true # also decode IPv6 MLD (ICMPv6) alongside IGMP
25+
26+
capture_filter: "" # optional BPF override; empty = auto (ip proto 2 [+ icmp6])
27+
28+
flood:
29+
window: 10 # sliding-window seconds
30+
report_rate: 40 # reports / source / window
31+
leave_rate: 30 # leaves / source / window
32+
query_rate: 8 # queries / source / window
33+
global_rate: 300 # total IGMP msgs / window (segment-wide storm)
34+
flap_transitions: 8 # join<->leave toggles / (host,group) / window
35+
36+
anomaly:
37+
require_router_alert: true # flag v2/v3 queries missing IP Router Alert
38+
require_ttl_1: true # IGMP must be TTL 1 (link-local)
39+
flag_downgrade: true # v1/v2 query after v3 established (SSM bypass)
40+
41+
recon:
42+
scan_distinct_groups: 6 # group-specific queries to N groups...
43+
scan_window: 15 # ...within this many seconds -> scan
44+
alert_nonquerier_query: true # general query from a non-elected querier
45+
46+
policy:
47+
mode: learn # learn | enforce
48+
allow_link_local: true # 224.0.0.0/24 joins (mDNS/LLMNR/etc) are normal
49+
flag_unknown_group: true # first-seen group -> INFO (baseline)
50+
51+
benign_groups: [] # extra always-allowed groups
52+
# groups where ANY unlisted join is alerted, even in learn mode:
53+
sensitive_groups: []
54+
# - 239.100.0.0/24 # e.g. IPTV / market-data / OT feed
55+
56+
allow: [] # host -> permitted groups allowlist
57+
# - mac: "aa:bb:cc:*" # MAC prefix (wildcard) or exact
58+
# groups: ["239.10.0.0/16", "224.0.0.251"]
59+
# - ip: "10.20.30.40"
60+
# groups: ["232.1.0.0/16"]
61+
62+
ssm_source_allow: [] # optional allowed SSM (232/8) source CIDRs
63+
# - 198.51.100.0/24
64+
65+
alerting:
66+
cooldown: 60 # s to suppress duplicate (rule,host,group)
67+
min_severity: LOW # INFO | LOW | MEDIUM | HIGH | CRITICAL
68+
stdout: true
2069

2170
dataplane:
22-
interval: 5
23-
mcast_storm_pps: 1000
24-
mcast_ratio: 0.6
25-
mcast_ratio_floor_pps: 100
26-
flood_no_members_pps: 50
71+
enable: true # sample NIC counters, cross-ref vs joined groups
72+
interval: 5 # s between /sys/class/net/<iface>/statistics reads
73+
mcast_pps: 2000 # received multicast pps over this -> storm (HIGH)
74+
mcast_pps_floor: 150 # multicast pps while NO data groups joined -> flood
75+
members_floor: 1 # "no members" = fewer than this many active data groups
76+
rx_drop_pps: 100 # rx_dropped delta/s over this -> capture loss (MED)
77+
mcast_ratio: 0.9 # multicast fraction of rx over this -> alert (MED)
78+
ratio_min_pps: 500 # only evaluate the ratio above this total rx pps
2779

80+
# Out-of-band management-plane poll of the ACCESS SWITCH for per-interface and
81+
# per-group multicast state. Not segment traffic: sends nothing on the mirror,
82+
# no IGMP, no joins. Off by default -- needs creds + a reachable mgmt IP.
2883
snmp:
29-
enable: false # OFF by default (credentialed, out-of-band)
30-
host: null # switch management IP
31-
community: public
32-
interval: 30
33-
iface_mcast_storm_pps: 1000
34-
monitor_ifaces: [] # ifIndex whitelist ([] = all)
35-
census_not_forwarded: false
36-
cache_capabilities: true
37-
capability_strikes: 3
38-
capability_ttl: 3600
84+
enable: false
85+
host: "" # switch management IP / hostname
86+
port: 161
87+
version: "2c" # "1" | "2c" | "3"
88+
community: public # v1/v2c
89+
# v3 (used when version: "3"):
90+
v3_user: ""
91+
v3_level: authPriv # noAuthNoPriv | authNoPriv | authPriv
92+
v3_auth_protocol: SHA
93+
v3_auth_key: ""
94+
v3_priv_protocol: AES
95+
v3_priv_key: ""
96+
timeout: 2
97+
retries: 1
98+
interval: 30 # s between polls (heavier than sysfs)
99+
snmp_bin_dir: "" # optional dir holding snmpbulkwalk
100+
iface_mcast_pps: 3000 # per-interface in/out multicast pps -> storm (HIGH)
101+
monitor_ifaces: [] # optional ifName/ifIndex whitelist (empty = all)
102+
cross_reference: true # compare switch group table vs igmpwatch census
103+
cache_capabilities: true # remember which walks each switch answers
104+
capability_ttl: 3600 # s before re-checking a "known empty" group table
105+
capability_strikes: 3 # consecutive empty walks before marking unsupported
106+
flag_unsubscribed_forwarding: true # switch forwards a group we never saw join (HIGH)
107+
flag_census_not_forwarded: false # we saw a join the switch doesn't list (noisier)
108+
# OID overrides for non-standard / L2-only platforms:
109+
# if_name_oid: 1.3.6.1.2.1.31.1.1.1.1
110+
# if_in_mcast_oid: 1.3.6.1.2.1.31.1.1.1.2
111+
# if_out_mcast_oid: 1.3.6.1.2.1.31.1.1.1.4
112+
# group_table_oid: 1.3.6.1.2.1.85.1.2.1.4

‎igmpwatch/__init__.py‎

Lines changed: 2 additions & 0 deletions

0 commit comments

Comments
 (0)