planarian_backend_netwatch/
parse.rs1use crate::ebpf::{Event, HOOK_GENL, HOOK_NETDEV, HOOK_NFBATCH, HOOK_NFNETLINK, HOOK_RTNETLINK, HOOK_SOCKOPT};
8use crate::model::{Change, ChangeKind, Owner};
9
10const NLM_F_CREATE: u16 = 0x400;
11const NLMSG_HDRLEN: usize = 16;
12
13const RTM_NEWLINK: u16 = 16;
14const RTM_DELLINK: u16 = 17;
15const RTM_NEWROUTE: u16 = 24;
16const RTM_DELROUTE: u16 = 25;
17const RTM_NEWRULE: u16 = 32;
18const RTM_DELRULE: u16 = 33;
19
20const IFLA_IFNAME: u16 = 3;
21const RTA_DST: u16 = 1;
22const RTA_OIF: u16 = 4;
23const RTA_PRIORITY: u16 = 6;
24const RTA_TABLE: u16 = 15;
25const FRA_PRIORITY: u16 = 6;
26const FRA_TABLE: u16 = 15;
27const FR_ACT_TO_TBL: u8 = 1;
28
29const NFNL_SUBSYS_NFTABLES: u16 = 10;
31
32const NFT_READS: [u16; 8] = [1, 4, 7, 10, 13, 16, 19, 23];
42
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub enum Parsed {
45 Nothing,
47 Changes(Vec<Change>),
48 Unreadable(String),
50}
51
52fn u16_at(b: &[u8], o: usize) -> Option<u16> {
53 b.get(o..o + 2).map(|x| u16::from_ne_bytes([x[0], x[1]]))
54}
55
56fn u32_at(b: &[u8], o: usize) -> Option<u32> {
57 b.get(o..o + 4).map(|x| u32::from_ne_bytes([x[0], x[1], x[2], x[3]]))
58}
59
60fn attrs(b: &[u8], from: usize) -> Option<Vec<(u16, &[u8])>> {
63 let mut out = vec![];
64 let mut o = from;
65 while o + 4 <= b.len() {
66 let len = u16_at(b, o)? as usize;
67 let ty = u16_at(b, o + 2)? & 0x3fff;
68 if len < 4 || o + len > b.len() {
69 return None;
70 }
71 out.push((ty, &b[o + 4..o + len]));
72 o += (len + 3) & !3;
73 }
74 Some(out)
75}
76
77fn cstr(b: &[u8]) -> String {
78 String::from_utf8_lossy(b.split(|c| *c == 0).next().unwrap_or(&[])).into_owned()
79}
80
81const RTM_SETLINK: u16 = 19;
82const IFINFOMSG_LEN: usize = 16;
83const IFLA_NET_NS_PID: u16 = 19;
84const IFLA_NET_NS_FD: u16 = 28;
85const IFLA_TARGET_NETNSID: u16 = 46;
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum Scope {
91 Host,
93 Elsewhere,
96 Unknown,
99 Reaches,
104}
105
106pub fn scope(ev: &Event, own: u32) -> Scope {
107 if ev.netns == 0 {
108 return Scope::Unknown;
109 }
110 if ev.netns == own {
111 return Scope::Host;
112 }
113 if ev.hook == HOOK_RTNETLINK && matches!(ev.nlmsg_type(), Some(RTM_NEWLINK | RTM_SETLINK)) {
114 let reaches = |a: &[(u16, &[u8])]| {
115 a.iter().any(|(t, _)| matches!(*t, IFLA_NET_NS_PID | IFLA_NET_NS_FD | IFLA_TARGET_NETNSID))
116 };
117 match attrs(&ev.data, NLMSG_HDRLEN + IFINFOMSG_LEN) {
118 Some(a) if !reaches(&a) && !ev.truncated() => {}
119 _ => return Scope::Reaches,
120 }
121 }
122 Scope::Elsewhere
123}
124
125pub fn take(
129 t: &mut crate::Tracker,
130 ev: &Event,
131 own_netns: u32,
132 unit: impl FnOnce() -> Option<planarian_schema::UnitId>,
133 ifname: &mut dyn FnMut(u32) -> Option<String>,
134) {
135 match scope(ev, own_netns) {
136 Scope::Elsewhere => return,
137 Scope::Unknown => return t.lost(None),
138 Scope::Reaches => return t.lost(unit().as_ref()),
139 Scope::Host => {}
140 }
141 debug_assert_eq!(ev.netns, own_netns, "a non-host event reached the tracker");
145 let unit = unit();
146 let owner = unit.clone().map(Owner::Unit).unwrap_or(Owner::Unattributed);
147 match parse(ev, &owner, ifname) {
148 Parsed::Nothing => {}
149 Parsed::Changes(cs) => cs.iter().for_each(|c| t.apply(c)),
150 Parsed::Unreadable(_) => t.lost(unit.as_ref()),
151 }
152}
153
154pub fn parse(ev: &Event, owner: &Owner, ifname: &mut dyn FnMut(u32) -> Option<String>) -> Parsed {
156 let b = &ev.data;
157 if ev.hook == HOOK_NETDEV {
158 let name = cstr(b);
161 if name.is_empty() {
162 return Parsed::Unreadable("an interface registered without a readable name".into());
163 }
164 return Parsed::Changes(vec![Change {
165 owner: owner.clone(),
166 kind: ChangeKind::Interface,
167 object: name.clone(),
168 iface: Some(name),
169 steers: false,
170 removed: false,
171 }]);
172 }
173 if ev.hook == HOOK_SOCKOPT {
174 let pf = b.first().copied().unwrap_or(0);
177 return Parsed::Changes(vec![Change {
178 owner: owner.clone(),
179 kind: ChangeKind::Firewall,
180 object: format!("legacy tables pf {pf}"),
181 iface: None,
182 steers: true,
183 removed: false,
184 }]);
185 }
186 let (Some(ty), Some(flags)) = (u16_at(b, 4), u16_at(b, 6)) else {
187 return Parsed::Unreadable("no netlink header".into());
188 };
189 let change = |kind, object: String, iface: Option<String>, steers, removed| Change {
190 owner: owner.clone(),
191 kind,
192 object,
193 iface,
194 steers,
195 removed,
196 };
197 match ev.hook {
198 HOOK_RTNETLINK => match ty {
199 RTM_NEWLINK | RTM_DELLINK => {
200 if ty == RTM_NEWLINK && flags & NLM_F_CREATE == 0 {
201 return Parsed::Nothing;
203 }
204 let index = u32_at(b, NLMSG_HDRLEN + 4);
205 let name = attrs(b, NLMSG_HDRLEN + 16)
206 .and_then(|a| a.iter().find(|(t, _)| *t == IFLA_IFNAME).map(|(_, v)| cstr(v)))
207 .or_else(|| index.filter(|i| *i != 0).and_then(ifname));
208 match name {
209 Some(n) => Parsed::Changes(vec![change(
210 ChangeKind::Interface,
211 n.clone(),
212 Some(n),
213 false,
214 ty == RTM_DELLINK,
215 )]),
216 None => Parsed::Unreadable("a link change without a name".into()),
217 }
218 }
219 RTM_NEWROUTE | RTM_DELROUTE => {
220 let (Some(&family), Some(&dst_len), Some(&table)) =
221 (b.get(NLMSG_HDRLEN), b.get(NLMSG_HDRLEN + 1), b.get(NLMSG_HDRLEN + 4))
222 else {
223 return Parsed::Unreadable("a short route message".into());
224 };
225 let Some(a) = attrs(b, NLMSG_HDRLEN + 12) else {
226 return Parsed::Unreadable("a route message past the capture".into());
227 };
228 let get = |t: u16| a.iter().find(|(x, _)| *x == t).map(|(_, v)| *v);
229 let table = get(RTA_TABLE).and_then(|v| u32_at(v, 0)).unwrap_or(table as u32);
230 let dst = get(RTA_DST).map(hex).unwrap_or_default();
231 let oif = get(RTA_OIF).and_then(|v| u32_at(v, 0));
232 let metric = get(RTA_PRIORITY).and_then(|v| u32_at(v, 0)).unwrap_or(0);
233 let iface = oif.and_then(ifname);
234 let steers = dst_len <= 1;
237 let object = format!("route {family} {table} {dst}/{dst_len} oif {} metric {metric}", oif.unwrap_or(0));
240 Parsed::Changes(vec![change(ChangeKind::Route, object, iface, steers, ty == RTM_DELROUTE)])
241 }
242 RTM_NEWRULE | RTM_DELRULE => {
243 let (Some(&family), Some(&dst_len), Some(&src_len), Some(&action)) =
244 (b.get(NLMSG_HDRLEN), b.get(NLMSG_HDRLEN + 1), b.get(NLMSG_HDRLEN + 2), b.get(NLMSG_HDRLEN + 7))
245 else {
246 return Parsed::Unreadable("a short rule message".into());
247 };
248 let Some(a) = attrs(b, NLMSG_HDRLEN + 12) else {
249 return Parsed::Unreadable("a rule message past the capture".into());
250 };
251 let get = |t: u16| a.iter().find(|(x, _)| *x == t).and_then(|(_, v)| u32_at(v, 0));
252 let table = get(FRA_TABLE).unwrap_or(b.get(NLMSG_HDRLEN + 4).copied().unwrap_or(0) as u32);
258 let steers = dst_len == 0 && src_len == 0 && action == FR_ACT_TO_TBL && !matches!(table, 253..=255);
259 let object = format!("rule {family} {} {table}", get(FRA_PRIORITY).unwrap_or(0));
260 Parsed::Changes(vec![change(ChangeKind::Rule, object, None, steers, ty == RTM_DELRULE)])
261 }
262 _ => Parsed::Nothing,
264 },
265 HOOK_NFNETLINK | HOOK_NFBATCH => {
269 if ev.hook == HOOK_NFNETLINK && ty >> 8 != NFNL_SUBSYS_NFTABLES {
270 return Parsed::Nothing;
271 }
272 if ev.hook == HOOK_NFNETLINK && NFT_READS.contains(&(ty & 0xff)) {
273 return Parsed::Nothing;
275 }
276 Parsed::Changes(vec![change(ChangeKind::Firewall, "nftables".into(), None, true, false)])
277 }
278 HOOK_GENL => Parsed::Nothing,
281 h => Parsed::Unreadable(format!("unknown hook {h}")),
282 }
283}
284
285fn hex(b: &[u8]) -> String {
286 b.iter().map(|x| format!("{x:02x}")).collect()
287}