1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
//! ARP watch — L2 visibility + ARP-spoof / binding-change detection.
//!
//! Two surfaces on one Monitor, both from the `arp` feature:
//!
//! 1. **Raw ARP feed** — `.on_arp(...)` prints every parsed `ArpMessage`:
//! who-has requests, is-at replies, gratuitous announcements, RARP.
//! 2. **Anomalies** — `.on_arp_anomaly(...)` raises the security signal:
//! `SpoofSuspected` (a gratuitous reply whose target MAC ≠ sender MAC —
//! classic cache poisoning) and `BindingChanged` (a known IP now claims
//! a different MAC — failover or MITM). Each is also pushed to the
//! anomaly sink so it lands in your normal alert pipeline.
//!
//! ARP is L2 (no 5-tuple), but the kernel prefilter still narrows precisely:
//! arming an ARP hook adds an `EtherType(0x0806)` term, so a pure-ARP monitor
//! sheds non-ARP traffic in-kernel, and an ARP+IP monitor captures the union.
//!
//! Run:
//!
//! ```sh
//! cargo run --example monitor_arp_watch \
//! --features "monitor" -- eth0 300
//! ```
//!
//! Arguments: `<iface>` (default `lo`) `<seconds>` (default 300).
//!
//! Smoke test on a real segment: `arping -U <your-ip>` sends a gratuitous
//! ARP; a tool like `arpspoof` (dsniff) triggers `SpoofSuspected`.
use std::time::Duration;
use netring::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let iface = std::env::args().nth(1).unwrap_or_else(|| "lo".into());
let dur_secs: u64 = std::env::args()
.nth(2)
.and_then(|s| s.parse().ok())
.unwrap_or(300);
eprintln!("monitor_arp_watch: watching ARP on {iface} for {dur_secs}s");
eprintln!(" press Ctrl-C to stop early");
Monitor::builder()
.interface(&iface)
// The gateway's virtual MAC is trusted — a VRRP failover that moves
// it to the backup router must not page anyone. (Example value.)
// .arp_allow("10.0.0.1".parse()?, MacAddr([0x00, 0x00, 0x5e, 0x00, 0x01, 0x01]))
.arp_warmup(Duration::from_secs(3))
.on_arp(|m, _ctx| {
// Raw feed: one line per ARP message.
let kind = if m.is_likely_spoof() {
" [SPOOF?]"
} else if m.is_gratuitous() {
" [gratuitous]"
} else {
""
};
println!(
"arp {:<12} {} is-at {:?} → {}{}",
m.oper.as_str(),
m.sender_ip,
m.sender,
m.target_ip,
kind,
);
Ok(())
})
.on_arp_anomaly(|a, ctx| {
// Security signal: log + push to the anomaly sink.
eprintln!(
"!! {} — {} claims {:?}{}",
a.kind.as_str(),
a.ip(),
a.mac(),
a.prior_mac
.map(|p| format!(" (was {p:?})"))
.unwrap_or_default(),
);
ctx.emit(a.kind.as_str(), a.kind.severity())
.with("ip", a.ip().to_string())
.with("mac", format!("{:?}", a.mac()))
.emit();
Ok(())
})
.sink(StdoutSink::default())
.build()?
.run_for(Duration::from_secs(dur_secs))
.await?;
eprintln!("monitor_arp_watch: done");
Ok(())
}