communityid/
lib.rs

1#![deny(unused_imports)]
2
3//! This crate provides a practical implementation of the [Community ID 
4//! standard](https://github.com/corelight/community-id-spec) for network
5//! flow hashing.
6//! 
7//! # Features
8//! 
9//! * `serde`: when enabled implements `serde::Serialize` and `serde::Deserialize` traits
10//! 
11//! # Example
12//! 
13//! ```
14//! use communityid::{Protocol, Flow};
15//! use std::net::Ipv4Addr;
16//! 
17//! let f = Flow::new(Protocol::UDP, Ipv4Addr::new(192,168,1,42).into(), 4242, Ipv4Addr::new(8,8,8,8).into(), 53);
18//! let f2 = Flow::new(Protocol::UDP,  Ipv4Addr::new(8,8,8,8).into(), 53, Ipv4Addr::new(192,168,1,42).into(), 4242);
19//! 
20//! // community-id can be base64 encoded
21//! assert_eq!(f.community_id_v1(0).base64(), "1:vTdrngJjlP5eZ9mw9JtnKyn99KM=");
22//! 
23//! // community-id can be hex encoded
24//! assert_eq!(f2.community_id_v1(0).hexdigest(), "1:bd376b9e026394fe5e67d9b0f49b672b29fdf4a3");
25//! 
26//! // we can test equality between two community-ids
27//! assert_eq!(f.community_id_v1(0), f2.community_id_v1(0));
28//! ``` 
29
30use 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/// Enumeration holding the supported protocols by the community-id standard
122#[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    /// Converts a protocol into a [Flow]
161    /// 
162    /// Example
163    /// 
164    /// ```
165    /// use communityid::{Protocol};
166    /// use std::net::Ipv4Addr;
167    /// 
168    /// let f = Protocol::UDP.into_flow(Ipv4Addr::new(192,168,1,42).into(), 4242, Ipv4Addr::new(8,8,8,8).into(), 53);
169    /// 
170    /// assert_eq!(f.community_id_v1(0).base64(), "1:vTdrngJjlP5eZ9mw9JtnKyn99KM=");
171    /// ```
172    #[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/// Enumeration representing a community-id
180#[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    /// Encodes the current community-id in its base64 representation
242    #[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    /// Encodes the current community-id in its hexadecimal digest representation
250    /// 
251    /// Example
252    /// 
253    /// ```
254    /// use communityid::{Protocol, Flow};
255    /// use std::net::Ipv4Addr;
256    /// 
257    /// let f = Flow::new(Protocol::UDP, Ipv4Addr::new(192,168,1,42).into(), 4242, Ipv4Addr::new(8,8,8,8).into(), 53);
258    /// 
259    /// assert_eq!(f.community_id_v1(0).hexdigest(), "1:bd376b9e026394fe5e67d9b0f49b672b29fdf4a3");
260    /// ```
261    #[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/// Structure representing a network flow
271#[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    /// Creates a new flow from parameters
320    #[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    /// Creates a partial flow (without port information) from source and destination [IpAddr] 
332    #[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    /// Computes the [CommunityId] corresponding to that Flow
353    #[inline]
354    pub fn community_id_v1(&self, seed: u16) -> CommunityId {
355        // swap addresses and ports if necessary to ensure consistency.
356        let (src_ip, src_port, dst_ip, dst_port) = self.order();
357
358        let mut hasher = Sha1::new();
359        
360        // seed
361        hasher.update(seed.to_be_bytes());
362        // src ip
363        hasher.update(serialize_ip(src_ip));
364        // dest ip
365        hasher.update(serialize_ip(dst_ip));
366        // protocol
367        hasher.update([u8::from(self.proto)]);
368        // padding
369        hasher.update([0]);
370        
371        // if both the ports are specified
372        if let (Some(src_port), Some(dst_port)) = (src_port,dst_port){
373            // src port be
374            hasher.update(src_port.to_be_bytes());
375            // dst port be
376            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}