1#![deny(unused_imports)]
2
3use std::net::IpAddr;
31
32use base64::prelude::*;
33use sha1::{Digest, Sha1};
34
35#[cfg(feature = "serde")]
36use serde::{
37 de::{Deserialize, Deserializer, Visitor},
38 ser::{Serialize, Serializer},
39};
40
41#[inline(always)]
42fn serialize_ip(ip: IpAddr) -> Vec<u8> {
43 match ip {
44 IpAddr::V4(v4) => v4.octets().to_vec(),
45 IpAddr::V6(v6) => v6.octets().to_vec(),
46 }
47}
48
49#[repr(u16)]
50enum IcmpType {
51 EchoReply = 0,
52 Echo = 8,
53 RtrAdvert = 9,
54 RtrSolicit = 10,
55 Tstamp = 13,
56 TstampReply = 14,
57 Info = 15,
58 InfoReply = 16,
59 Mask = 17,
60 MaskReply = 18,
61}
62
63fn icmp4_port_equivalent(p1: u16, p2: u16) -> (u16, u16, bool) {
64 match p1 {
65 t if t == IcmpType::Echo as u16 => (t, IcmpType::EchoReply as u16, false),
66 t if t == IcmpType::EchoReply as u16 => (t, IcmpType::Echo as u16, false),
67 t if t == IcmpType::Tstamp as u16 => (t, IcmpType::TstampReply as u16, false),
68 t if t == IcmpType::TstampReply as u16 => (t, IcmpType::Tstamp as u16, false),
69 t if t == IcmpType::Info as u16 => (t, IcmpType::InfoReply as u16, false),
70 t if t == IcmpType::InfoReply as u16 => (t, IcmpType::Info as u16, false),
71 t if t == IcmpType::RtrSolicit as u16 => (t, IcmpType::RtrAdvert as u16, false),
72 t if t == IcmpType::RtrAdvert as u16 => (t, IcmpType::RtrSolicit as u16, false),
73 t if t == IcmpType::Mask as u16 => (t, IcmpType::MaskReply as u16, false),
74 t if t == IcmpType::MaskReply as u16 => (t, IcmpType::Mask as u16, false),
75 _ => (p1, p2, true),
76 }
77}
78
79#[repr(u16)]
80enum Icmp6Type {
81 EchoRequest = 128,
82 EchoReply = 129,
83 MldListenerQuery = 130,
84 MldListenerReport = 131,
85 NdRouterSolicit = 133,
86 NdRouterAdvert = 134,
87 NdNeighborSolicit = 135,
88 NdNeighborAdvert = 136,
89 WruRequest = 139,
90 WruReply = 140,
91 HaadRequest = 144,
92 HaadReply = 145,
93}
94
95fn icmp6_port_equivalent(p1: u16, p2: u16) -> (u16, u16, bool) {
96 match p1 {
97 t if t == Icmp6Type::EchoRequest as u16 => (t, Icmp6Type::EchoReply as u16, false),
98 t if t == Icmp6Type::EchoReply as u16 => (t, Icmp6Type::EchoRequest as u16, false),
99 t if t == Icmp6Type::MldListenerQuery as u16 => {
100 (t, Icmp6Type::MldListenerReport as u16, false)
101 }
102 t if t == Icmp6Type::MldListenerReport as u16 => {
103 (t, Icmp6Type::MldListenerQuery as u16, false)
104 }
105 t if t == Icmp6Type::NdRouterSolicit as u16 => (t, Icmp6Type::NdRouterAdvert as u16, false),
106 t if t == Icmp6Type::NdRouterAdvert as u16 => (t, Icmp6Type::NdRouterSolicit as u16, false),
107 t if t == Icmp6Type::NdNeighborSolicit as u16 => {
108 (t, Icmp6Type::NdNeighborAdvert as u16, false)
109 }
110 t if t == Icmp6Type::NdNeighborAdvert as u16 => {
111 (t, Icmp6Type::NdNeighborSolicit as u16, false)
112 }
113 t if t == Icmp6Type::WruRequest as u16 => (t, Icmp6Type::WruReply as u16, false),
114 t if t == Icmp6Type::WruReply as u16 => (t, Icmp6Type::WruRequest as u16, false),
115 t if t == Icmp6Type::HaadRequest as u16 => (t, Icmp6Type::HaadReply as u16, false),
116 t if t == Icmp6Type::HaadReply as u16 => (t, Icmp6Type::HaadRequest as u16, false),
117 _ => (p1, p2, true),
118 }
119}
120
121#[derive(Debug, Clone, Copy, Hash)]
123#[repr(u8)]
124pub enum Protocol {
125 ICMP ,
126 TCP ,
127 UDP ,
128 ICMP6 ,
129 SCTP ,
130 Other(u8),
131}
132
133impl From<Protocol> for u8{
134 fn from(value: Protocol) -> u8 {
135 match value {
136 Protocol::ICMP => 1,
137 Protocol::TCP => 6,
138 Protocol::UDP => 17,
139 Protocol::ICMP6 => 58,
140 Protocol::SCTP => 132,
141 Protocol::Other(o) => o,
142 }
143 }
144}
145
146impl From<u8> for Protocol{
147 fn from(value: u8) -> Self{
148 match value {
149 v if v == u8::from(Self::ICMP) => Self::ICMP,
150 v if v == u8::from(Self::TCP) => Self::TCP,
151 v if v == u8::from(Self::UDP) => Self::UDP,
152 v if v == u8::from(Self::ICMP6) => Self::ICMP6,
153 v if v == u8::from(Self::SCTP) => Self::SCTP,
154 _=> Self::Other(value)
155 }
156 }
157}
158
159impl Protocol {
160 #[inline]
173 pub fn into_flow(self, src_ip: IpAddr, src_port: u16, dst_ip: IpAddr, dst_port:u16) -> Flow{
174 Flow::new(self, src_ip, src_port, dst_ip, dst_port)
175 }
176}
177
178
179#[derive(Hash, PartialEq)]
181pub enum CommunityId {
182 V1([u8; 20]),
183}
184
185#[cfg(feature = "serde")]
186impl Serialize for CommunityId{
187 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
188 where
189 S: Serializer {
190 serializer.serialize_str(&self.base64())
191 }
192}
193
194#[cfg(feature = "serde")]
195impl<'de> Deserialize<'de> for CommunityId {
196 fn deserialize<D>(deserializer: D) -> Result<CommunityId, D::Error>
197 where
198 D: Deserializer<'de>,
199 {
200 struct CommunityIdVisitor;
201
202 impl<'de> Visitor<'de> for CommunityIdVisitor {
203 type Value = CommunityId;
204
205 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
206 formatter.write_str("expecting a community-id base64 encoded")
207 }
208
209 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
210 where
211 E: serde::de::Error,
212 {
213 let (version, encoded) = v.split_once(':').ok_or(E::custom("wrong community id format"))?;
214
215 match version {
216 "1" => {
217 let v = BASE64_STANDARD.decode(encoded).map_err(E::custom)?;
218 let mut data = [0u8;20];
219 if data.len() != v.len() {
220 return Err(E::custom("data must be 20 bytes long"));
221 }
222 data.copy_from_slice(&v);
223 Ok(CommunityId::V1(data))
224 }
225 _=> Err(E::custom(format!("unknown community-id version: {}", version)))
226 }
227 }
228 }
229
230 deserializer.deserialize_string(CommunityIdVisitor)
231 }
232}
233
234impl std::fmt::Debug for CommunityId{
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 write!(f, "{}", self.hexdigest())
237 }
238}
239
240impl CommunityId {
241 #[inline(always)]
243 pub fn base64(&self) -> String {
244 match self {
245 Self::V1(data) => format!("1:{}", BASE64_STANDARD.encode(data)),
246 }
247 }
248
249 #[inline(always)]
262 pub fn hexdigest(&self) -> String {
263 match self {
264 Self::V1(data) =>
265 format!("1:{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", data[0],data[1],data[2],data[3],data[4],data[5],data[6],data[7],data[8],data[9],data[10],data[11],data[12],data[13],data[14],data[15],data[16],data[17],data[18],data[19])
266 }
267 }
268}
269
270#[derive(Debug, Clone, Copy, Hash)]
272pub struct Flow {
273 proto: Protocol,
274 src_ip: IpAddr,
275 src_port: Option<u16>,
276 dst_ip: IpAddr,
277 dst_port: Option<u16>,
278 one_way: bool,
279}
280
281impl Flow {
282 #[inline(always)]
283 fn make(
284 proto: Protocol,
285 src_ip: IpAddr,
286 src_port: Option<u16>,
287 dst_ip: IpAddr,
288 dst_port: Option<u16>,
289 ) -> Self {
290 if let (Some(src_port), Some(dst_port)) = (src_port,dst_port){
291 let (src_port, dst_port, one_way) = match proto {
292 Protocol::ICMP => icmp4_port_equivalent(src_port, dst_port),
293 Protocol::ICMP6 => icmp6_port_equivalent(src_port, dst_port),
294 _ => (src_port, dst_port, false),
295 };
296
297 Self {
298 proto,
299 src_ip,
300 src_port: Some(src_port),
301 dst_ip,
302 dst_port: Some(dst_port),
303 one_way,
304 }
305 } else {
306 Self {
307 proto,
308 src_ip,
309 src_port: None,
310 dst_ip,
311 dst_port: None,
312 one_way: false,
313 }
314
315 }
316
317 }
318
319 #[inline]
321 pub fn new(
322 proto: Protocol,
323 src_ip: IpAddr,
324 src_port: u16,
325 dst_ip: IpAddr,
326 dst_port: u16,
327 ) -> Self {
328 Self::make(proto, src_ip, Some(src_port), dst_ip, Some(dst_port))
329 }
330
331 #[inline]
333 pub fn partial(
334 proto: Protocol,
335 src_ip: IpAddr,
336 dst_ip: IpAddr,
337 ) -> Self {
338 Self::make(proto, src_ip, None, dst_ip, None)
339 }
340
341 #[inline(always)]
342 fn order(&self) -> (IpAddr, Option<u16>, IpAddr, Option<u16>) {
343 if self.one_way {
344 (self.src_ip, self.src_port, self.dst_ip, self.dst_port)
345 } else if (self.src_ip, self.src_port) > (self.dst_ip, self.dst_port) {
346 (self.dst_ip, self.dst_port, self.src_ip, self.src_port)
347 } else {
348 (self.src_ip, self.src_port, self.dst_ip, self.dst_port)
349 }
350 }
351
352 #[inline]
354 pub fn community_id_v1(&self, seed: u16) -> CommunityId {
355 let (src_ip, src_port, dst_ip, dst_port) = self.order();
357
358 let mut hasher = Sha1::new();
359
360 hasher.update(seed.to_be_bytes());
362 hasher.update(serialize_ip(src_ip));
364 hasher.update(serialize_ip(dst_ip));
366 hasher.update([u8::from(self.proto)]);
368 hasher.update([0]);
370
371 if let (Some(src_port), Some(dst_port)) = (src_port,dst_port){
373 hasher.update(src_port.to_be_bytes());
375 hasher.update(dst_port.to_be_bytes());
377 }
378
379 CommunityId::V1(hasher.finalize().into())
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use std::str::FromStr;
386
387 use super::*;
388
389 macro_rules! flow {
390 ($proto:expr, $src_ip:literal, $src_port:literal, $dst_ip:literal, $dst_port:literal) => {
391 Flow::new(
392 $proto,
393 IpAddr::from_str($src_ip).unwrap(),
394 $src_port,
395 IpAddr::from_str($dst_ip).unwrap(),
396 $dst_port,
397 )
398 };
399 }
400
401 #[test]
402 fn tcp_reorder() {
403 let f = flow!(Protocol::TCP, "192.168.1.42", 42, "192.168.1.42", 41);
404
405 assert_eq!(
406 "1:eRcf7I/xocOxnYo5pbJBV5NhVm0=",
407 f.community_id_v1(0).base64()
408 );
409
410 let f = flow!(Protocol::TCP, "192.168.1.42", 41, "192.168.1.42", 42);
411 assert_eq!(
412 "1:eRcf7I/xocOxnYo5pbJBV5NhVm0=",
413 f.community_id_v1(0).base64()
414 );
415 }
416
417 #[test]
418 fn tcp_test() {
419 let f = Flow::new(
420 Protocol::TCP,
421 IpAddr::from_str("192.168.1.10").unwrap(),
422 12345,
423 IpAddr::from_str("192.168.1.20").unwrap(),
424 80,
425 );
426
427 assert_eq!(
428 "1:To62PWNVuiriSZDHqB4YZp+VAYM=",
429 f.community_id_v1(0).base64()
430 );
431
432 assert_eq!(
433 "1:4e8eb63d6355ba2ae24990c7a81e18669f950183",
434 f.community_id_v1(0).hexdigest()
435 );
436 }
437
438 #[test]
439 fn test_icmp() {
440 assert_eq!(
441 "1:X0snYXpgwiv9TZtqg64sgzUn6Dk=",
442 flow!(Protocol::ICMP, "192.168.0.89", 8, "192.168.0.1", 0)
443 .community_id_v1(0)
444 .base64()
445 );
446
447 assert_eq!(
448 "1:X0snYXpgwiv9TZtqg64sgzUn6Dk=",
449 flow!(Protocol::ICMP, "192.168.0.1", 0, "192.168.0.89", 8)
450 .community_id_v1(0)
451 .base64()
452 );
453
454 assert_eq!(
455 "1:3o2RFccXzUgjl7zDpqmY7yJi8rI=",
456 flow!(Protocol::ICMP, "192.168.0.89", 20, "192.168.0.1", 0)
457 .community_id_v1(0)
458 .base64()
459 );
460
461 assert_eq!(
462 "1:tz/fHIDUHs19NkixVVoOZywde+I=",
463 flow!(Protocol::ICMP, "192.168.0.89", 20, "192.168.0.1", 1)
464 .community_id_v1(0)
465 .base64()
466 );
467
468 assert_eq!(
469 "1:X0snYXpgwiv9TZtqg64sgzUn6Dk=",
470 flow!(Protocol::ICMP, "192.168.0.1", 0, "192.168.0.89", 20)
471 .community_id_v1(0)
472 .base64()
473 );
474 }
475
476 #[test]
477 fn test_icmp6() {
478 assert_eq!(
479 "1:dGHyGvjMfljg6Bppwm3bg0LO8TY=",
480 flow!(
481 Protocol::ICMP6,
482 "fe80::200:86ff:fe05:80da",
483 135,
484 "fe80::260:97ff:fe07:69ea",
485 0
486 )
487 .community_id_v1(0)
488 .base64()
489 );
490
491 assert_eq!(
492 "1:dGHyGvjMfljg6Bppwm3bg0LO8TY=",
493 flow!(
494 Protocol::ICMP6,
495 "fe80::260:97ff:fe07:69ea",
496 136,
497 "fe80::200:86ff:fe05:80da",
498 0
499 )
500 .community_id_v1(0)
501 .base64()
502 );
503
504 assert_eq!(
505 "1:NdobDX8PQNJbAyfkWxhtL2Pqp5w=",
506 flow!(
507 Protocol::ICMP6,
508 "3ffe:507:0:1:260:97ff:fe07:69ea",
509 3,
510 "3ffe:507:0:1:200:86ff:fe05:80da",
511 0
512 )
513 .community_id_v1(0)
514 .base64()
515 );
516
517 assert_eq!(
518 "1:/OGBt9BN1ofenrmSPWYicpij2Vc=",
519 flow!(
520 Protocol::ICMP6,
521 "3ffe:507:0:1:200:86ff:fe05:80da",
522 3,
523 "3ffe:507:0:1:260:97ff:fe07:69ea",
524 0
525 )
526 .community_id_v1(0)
527 .base64()
528 );
529 }
530
531 #[test]
532 fn test_serde(){
533 let f = flow!(Protocol::TCP, "192.168.1.42", 41, "192.168.1.42", 42);
534
535 assert_eq!(
536 r#""1:eRcf7I/xocOxnYo5pbJBV5NhVm0=""#,
537 serde_json::to_string(&f.community_id_v1(0)).unwrap()
538 );
539
540 assert_eq!(
541 f.community_id_v1(0),
542 serde_json::from_str(r#""1:eRcf7I/xocOxnYo5pbJBV5NhVm0=""#).unwrap()
543 );
544 }
545}