Skip to main content

fosr_lib/
structs.rs

1use chrono::{DateTime, FixedOffset};
2use pcap_file::pcap;
3use pnet::util::MacAddr;
4use rand_distr::Uniform;
5use rand_distr::weighted::WeightedIndex;
6use serde::{Deserialize, Serialize};
7use std::cmp::Ordering;
8use std::fmt::{Debug, Display};
9use std::net::Ipv4Addr;
10use std::str::FromStr;
11use std::time::Duration;
12use strum::{Display, EnumIter, EnumString, IntoEnumIterator};
13use thingbuf::Recycle;
14
15/// A general wrapper to pass a seed along with actual data
16#[derive(Debug, Clone)]
17pub struct SeededData<T: Clone> {
18    pub seed: u64,
19    pub data: T,
20}
21
22/// Stage 1 structure
23#[derive(Debug, Clone)]
24pub struct TimePoint {
25    pub unix_time: Duration,
26    pub date_time: DateTime<FixedOffset>,
27}
28
29// Stage 2 and 3 structures
30
31/// A transport protocol
32#[allow(clippy::upper_case_acronyms)]
33#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, EnumString)]
34#[serde(rename_all_fields = "UPPERCASE")]
35#[strum(
36    parse_err_fn = String::from,
37    parse_err_ty = String
38)]
39pub enum L4Proto {
40    #[serde(alias = "tcp")]
41    TCP,
42    #[serde(alias = "udp")]
43    UDP,
44    #[serde(alias = "icmp")]
45    ICMP,
46}
47
48/// Connection states, adapted from Zeek
49/// <https://docs.zeek.org/en/master/scripts/base/protocols/conn/main.zeek.html#field-Conn::Info$conn_state>
50#[allow(clippy::upper_case_acronyms)]
51#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, EnumString)]
52#[strum(
53    parse_err_fn = String::from,
54    parse_err_ty = String
55)]
56pub enum TCPConnState {
57    /// Normal establishment and termination
58    SF,
59    /// Originator sent a SYN followed by a FIN, we never saw a SYN ACK from the responder (hence the connection was “half” open)
60    SH,
61    /// Connection aborted (RST)
62    RST,
63    /// Connection attempt seen, no reply
64    S0,
65    /// Connection attempt rejected
66    REJ,
67    /// For non-TCP communication
68    #[strum(serialize = "none")]
69    NoState,
70}
71
72impl Display for L4Proto {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match self {
75            L4Proto::TCP => write!(f, "TCP"),
76            L4Proto::UDP => write!(f, "UDP"),
77            L4Proto::ICMP => write!(f, "ICMP"),
78        }
79    }
80}
81
82impl L4Proto {
83    pub fn iter() -> [L4Proto; 2] {
84        // TODO: add the other protocols when they are implemented
85        [L4Proto::TCP, L4Proto::UDP] //, L4Proto::ICMP]
86    }
87
88    pub fn get_protocol_number(&self) -> u8 {
89        match &self {
90            L4Proto::TCP => 6,
91            L4Proto::UDP => 17,
92            L4Proto::ICMP => 1,
93        }
94    }
95
96    pub fn wrap(&self, d: FlowData, c: Option<TCPConnState>) -> Flow {
97        match &self {
98            L4Proto::TCP => Flow::TCP(d, c.unwrap_or(TCPConnState::SF)), // FIXME
99            L4Proto::UDP => {
100                // assert!(c.is_none());
101                Flow::UDP(d)
102            }
103            L4Proto::ICMP => {
104                // assert!(c.is_none());
105                Flow::ICMP(d)
106            }
107        }
108    }
109}
110
111#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, Hash, PartialEq, Display, EnumIter)]
112#[allow(clippy::upper_case_acronyms)]
113#[serde(rename_all = "lowercase")]
114/// A list of application layer protocol
115pub enum L7Proto {
116    // Dedicated variants for well-known protocols
117    HTTP,
118    HTTPS,
119    QUIC,
120    SSH,
121    DNS,
122    DHCP,
123    SMTP,
124    Telnet,
125    IMAPS,
126    MQTT,
127    KMS,
128    MulticastDNS,
129    FTP,
130    FTPData,
131    LDAP,
132    NTP,
133    Kerberos,
134    DCERPC,
135    SMB,
136    // TODO complete
137    /// Joker variant for everything else
138    // &'static str costs a little leak (a few bytes) but make this enum
139    // Copy, which is very convenient
140    NonDefault(&'static str),
141}
142
143impl FromStr for L7Proto {
144    type Err = String;
145
146    // TODO: ssl ok pour plusieurs protocoles (idem pour http si on a des sous-usages)
147
148    fn from_str(original_s: &str) -> Result<Self, String> {
149        let binding = original_s.to_uppercase().replace(' ', "");
150        let s = binding.as_str().trim();
151        Ok(if s.contains("HTTPS") || s.contains("SSL") {
152            L7Proto::HTTPS
153        } else if s.contains("HTTP") {
154            // must be after "https"
155            L7Proto::HTTP
156        } else if s.contains("QUIC") {
157            L7Proto::QUIC
158        } else if s.contains("SSH") {
159            L7Proto::SSH
160        } else if s.contains("MULTICASTDNS") {
161            L7Proto::MulticastDNS
162        } else if s.contains("DNS") {
163            // must be after "multicast dns"
164            L7Proto::DNS
165        } else if s.contains("DHCP") {
166            L7Proto::DHCP
167        } else if s.contains("SMTP") {
168            // including SMTPS
169            L7Proto::SMTP
170        } else if s.contains("TELNET") {
171            L7Proto::Telnet
172        } else if s.contains("IMAP") {
173            // including IMAPS
174            L7Proto::IMAPS
175        } else if s.contains("MQTT") {
176            L7Proto::MQTT
177        } else if s.contains("KMS") {
178            L7Proto::KMS
179        } else if s.contains("FTP-DATA") {
180            L7Proto::FTPData
181        } else if s.contains("FTP") {
182            // must be after "ftp"
183            L7Proto::FTP
184        } else if s.contains("LDAP") {
185            L7Proto::LDAP
186        } else if s.contains("NTP") {
187            L7Proto::NTP
188        } else if s.contains("DCE_RPC") || s.contains("DCERPC") {
189            L7Proto::DCERPC
190        } else if s.contains("KRB") || s.contains("KERBEROS") {
191            L7Proto::Kerberos
192        } else if s.contains("SMB") {
193            L7Proto::SMB
194        } else {
195            log::info!("Non-default protocol: {original_s}");
196            L7Proto::NonDefault(String::from(original_s).leak())
197        })
198    }
199}
200
201impl L7Proto {
202    /// All protocol names as strings.
203    pub fn all_names() -> Vec<String> {
204        L7Proto::iter().map(|p| p.to_string()).collect()
205    }
206
207    /// Human-facing label for UI display (e.g. "mDNS", "HTTP").
208    /// Unlike `Display` (lowercase for config use), this preserves capitalization.
209    pub fn ui_label(&self) -> &'static str {
210        match self {
211            L7Proto::HTTP => "HTTP",
212            L7Proto::HTTPS => "HTTPS",
213            L7Proto::QUIC => "QUIC",
214            L7Proto::SSH => "SSH",
215            L7Proto::DNS => "DNS",
216            L7Proto::DHCP => "DHCP",
217            L7Proto::SMTP => "SMTP",
218            L7Proto::Telnet => "Telnet",
219            L7Proto::IMAPS => "IMAPS",
220            L7Proto::MQTT => "MQTT",
221            L7Proto::KMS => "KMS",
222            L7Proto::MulticastDNS => "mDNS",
223            L7Proto::FTP => "FTP",
224            L7Proto::FTPData => "FTP (data)",
225            L7Proto::LDAP => "LDAP",
226            L7Proto::NTP => "NTP",
227            L7Proto::Kerberos => "Kerberos",
228            L7Proto::SMB => "SMB",
229            L7Proto::DCERPC => "DCE/RPC",
230            L7Proto::NonDefault(s) => s,
231        }
232    }
233
234    /// Default source port. Some protocols impose it.
235    pub fn get_default_src_port(&self) -> Port {
236        Port::Random
237    }
238
239    /// Default destination port that is used if a configuration file does not override it
240    pub fn get_default_dst_port(&self) -> Option<Port> {
241        match self {
242            L7Proto::HTTP => Some(Port::Fixed(80)),
243            L7Proto::HTTPS | L7Proto::QUIC => Some(Port::Fixed(443)),
244            L7Proto::SSH => Some(Port::Fixed(22)),
245            L7Proto::DNS => Some(Port::Fixed(53)),
246            L7Proto::DHCP => Some(Port::Fixed(67)),
247            L7Proto::SMTP => Some(Port::Fixed(587)),
248            L7Proto::Telnet => Some(Port::Fixed(23)),
249            L7Proto::IMAPS => Some(Port::Fixed(993)),
250            L7Proto::MQTT => Some(Port::Fixed(1883)),
251            L7Proto::KMS => Some(Port::Fixed(1688)),
252            L7Proto::MulticastDNS => Some(Port::Fixed(5353)),
253            L7Proto::FTP => Some(Port::Fixed(21)),
254            L7Proto::FTPData => Some(Port::Random),
255            L7Proto::LDAP => Some(Port::Fixed(389)), // TODO(pf): this is non encrypted LDAP port
256            L7Proto::NTP => Some(Port::Fixed(123)),
257            L7Proto::Kerberos => Some(Port::Fixed(88)),
258            L7Proto::SMB => Some(Port::Fixed(445)),
259            L7Proto::DCERPC => Some(Port::Fixed(135)),
260            L7Proto::NonDefault(_) => None,
261        }
262    }
263}
264
265#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
266/// A default port
267pub enum Port {
268    /// A fixed port
269    Fixed(u16),
270    /// A random port
271    Random, // TODO: range ? weights ?
272}
273
274#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
275/// A list of application layer protocol with their default port
276pub struct L7ProtoWithPort {
277    proto: L7Proto,
278    port: Port,
279}
280
281impl FromStr for L7ProtoWithPort {
282    type Err = String;
283
284    fn from_str(s: &str) -> Result<Self, Self::Err> {
285        let v: Vec<String> = s.split(':').map(ToString::to_string).collect();
286        assert!(!v.is_empty() && v.len() <= 2);
287        let port: Option<Port> = if v.len() == 2 {
288            if v[1].to_lowercase() == "random" {
289                // the usefulness of "http:random" is debattable
290                Some(Port::Random)
291            } else {
292                Some(Port::Fixed(
293                    v[1].parse::<u16>().expect("Cannot parse the port in {s}"),
294                ))
295            }
296        } else {
297            None
298        };
299        let proto: L7Proto =
300            L7Proto::from_str(v[0].to_uppercase().replace(' ', "").as_str().trim()).unwrap();
301        if port.is_none() && proto.get_default_dst_port().is_none() {
302            Err(format!(
303                "Non-default protocol {s} must include a port number"
304            ))
305        } else {
306            let port = port.unwrap_or_else(|| proto.get_default_dst_port().unwrap());
307            Ok(L7ProtoWithPort { proto, port })
308        }
309    }
310}
311
312impl L7ProtoWithPort {
313    pub fn get_proto(&self) -> L7Proto {
314        self.proto
315    }
316
317    pub fn get_port(&self) -> Port {
318        self.port
319    }
320}
321
322#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, EnumString, Hash)]
323#[strum(ascii_case_insensitive)]
324#[serde(rename_all = "lowercase")]
325/// The OS of an host. By default, assume Linux
326pub enum OS {
327    #[default]
328    Linux,
329    Windows,
330    Router,
331}
332
333impl Display for OS {
334    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
335        match self {
336            OS::Linux => write!(f, "Linux"),
337            OS::Windows => write!(f, "Windows"),
338            OS::Router => write!(f, "Router"),
339        }
340    }
341}
342
343impl OS {
344    pub fn get_initial_ttl(&self) -> u8 {
345        match self {
346            OS::Linux => 64,
347            OS::Windows => 128,
348            OS::Router => 255,
349        }
350    }
351
352    // Source: https://en.wikipedia.org/wiki/Ephemeral_port
353    pub fn get_ephemeral_port_distr(&self) -> Uniform<u16> {
354        match self {
355            OS::Linux | OS::Router => Uniform::new(32768, 60999).unwrap(),
356            OS::Windows => Uniform::new(49152, 65535).unwrap(),
357        }
358    }
359}
360
361/// A wrapper for transport layer flow
362#[allow(clippy::upper_case_acronyms)]
363#[derive(Debug, Clone, Copy)]
364pub enum Flow {
365    TCP(FlowData, TCPConnState),
366    UDP(FlowData),
367    ICMP(FlowData),
368}
369
370impl Flow {
371    pub fn get_data(&self) -> &FlowData {
372        match &self {
373            Flow::TCP(data, _) | Flow::UDP(data) | Flow::ICMP(data) => data,
374        }
375    }
376
377    pub fn get_data_mut(&mut self) -> &mut FlowData {
378        match self {
379            Flow::TCP(data, _) | Flow::UDP(data) | Flow::ICMP(data) => data,
380        }
381    }
382
383    pub fn get_flow_id(&self) -> FlowId {
384        let d = self.get_data();
385        FlowId {
386            protocol: self.get_proto(),
387            src_ip: d.src_ip,
388            dst_ip: d.dst_ip,
389            src_port: d.src_port,
390            dst_port: d.dst_port,
391        }
392    }
393
394    pub fn get_proto(&self) -> L4Proto {
395        match &self {
396            Flow::TCP(_, _) => L4Proto::TCP,
397            Flow::UDP(_) => L4Proto::UDP,
398            Flow::ICMP(_) => L4Proto::ICMP,
399        }
400    }
401}
402
403/// The data of a transport layer flow
404#[derive(Debug, Clone, Copy)]
405pub struct FlowData {
406    // In online mode, the local IP will always be the source
407    pub src_ip: Ipv4Addr,
408    pub dst_ip: Ipv4Addr,
409    pub src_os: OS,
410    pub dst_os: OS,
411    pub src_mac: MacAddr,
412    pub dst_mac: MacAddr,
413    pub src_port: u16,
414    pub dst_port: u16,
415    pub src_ttl: u8,
416    pub dst_ttl: u8,
417    pub packets_count_cluster: usize,
418    pub fwd_packets_count: usize,
419    pub bwd_packets_count: usize,
420    pub timestamp: Duration,
421    pub l7_proto: L7Proto,
422}
423
424impl From<Flow> for FlowData {
425    fn from(f: Flow) -> FlowData {
426        match f {
427            Flow::TCP(data, _) | Flow::UDP(data) | Flow::ICMP(data) => data,
428        }
429    }
430}
431
432// Stage 3 structures
433
434#[derive(Debug, Clone)]
435/// Types of payload in the automata
436pub enum PayloadType {
437    /// No payload
438    Empty,
439    /// Payload is not random and will be replayed
440    Binary(&'static Vec<Vec<u8>>, WeightedIndex<u64>),
441}
442
443pub(crate) trait EdgeType: Debug + Clone {
444    fn get_payload_type(&self) -> &PayloadType;
445    fn get_direction(&self) -> PacketDirection;
446}
447
448// Stage 3 and 4 structures
449
450/// The direction of a packet
451#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
452pub enum PacketDirection {
453    /// client to server
454    Forward,
455    /// server to client
456    Backward,
457}
458
459impl PacketDirection {
460    pub fn into_reverse(self) -> PacketDirection {
461        match self {
462            PacketDirection::Forward => PacketDirection::Backward,
463            PacketDirection::Backward => PacketDirection::Forward,
464        }
465    }
466}
467
468/// The payload of a packet
469#[derive(Debug, Clone)]
470pub enum Payload {
471    /// No payload
472    Empty,
473    /// A replayed payload
474    Binary(&'static Vec<u8>),
475    /// A payload that will be randomly generated
476    Random(usize),
477}
478
479impl Payload {
480    pub fn get_payload_size(&self) -> usize {
481        match &self {
482            Payload::Empty => 0,
483            Payload::Binary(l) => l.len(),
484            Payload::Random(len) => *len,
485        }
486    }
487}
488
489/// A trait for obtaining indicators from a packet
490pub trait PacketInfo: Clone + Debug {
491    #[allow(unused)]
492    fn get_direction(&self) -> PacketDirection;
493    fn get_ts(&self) -> Duration;
494    fn set_ts(&mut self, ts: Duration);
495}
496
497#[derive(Debug, Clone)]
498/// The packets intermediate representation (as output by stage 3)
499pub struct PacketsIR<T: PacketInfo> {
500    pub packets_info: Vec<T>,
501    pub flow: Flow,
502}
503
504// Stage 4 structures
505#[derive(Debug, Clone, Eq, PartialEq)]
506/// A packet, with a timestamp and some data
507pub struct Packet {
508    pub timestamp: Duration,
509    pub data: Vec<u8>,
510}
511
512impl From<pcap::PcapPacket<'_>> for Packet {
513    fn from(p: pcap::PcapPacket<'_>) -> Packet {
514        Packet {
515            timestamp: p.timestamp,
516            data: p.data.into_owned(),
517        }
518    }
519}
520
521impl Packet {
522    pub fn get_mutable_ip_packet(&mut self) -> Option<pnet_packet::ipv4::MutableIpv4Packet<'_>> {
523        let eth_offset = pnet_packet::ethernet::EthernetPacket::minimum_packet_size();
524        let ip_packet = pnet_packet::ipv4::MutableIpv4Packet::new(&mut self.data[eth_offset..])?;
525        Some(ip_packet)
526    }
527}
528
529/// Used for packet ordering before pcap export
530impl Ord for Packet {
531    fn cmp(&self, other: &Self) -> Ordering {
532        if self.timestamp == other.timestamp {
533            self.data.cmp(&other.data) // use data in case both timestamps are equal
534        } else {
535            self.timestamp.cmp(&other.timestamp)
536        }
537    }
538}
539
540impl PartialOrd for Packet {
541    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
542        Some(self.cmp(other))
543    }
544}
545
546#[derive(Debug, Clone)]
547/// A set of packets from the same flow
548pub struct Packets {
549    pub packets: Vec<Packet>,
550    pub directions: Vec<PacketDirection>,
551    pub timestamps: Vec<Duration>,
552    pub flow: Flow,
553}
554
555impl Packets {
556    pub fn clear(&mut self) {
557        self.packets.clear();
558        self.directions.clear();
559        self.timestamps.clear();
560    }
561
562    pub fn reverse(&mut self) {
563        for d in &mut self.directions {
564            *d = d.into_reverse();
565        }
566        let data = self.flow.get_data_mut();
567        (data.src_ip, data.dst_ip) = (data.dst_ip, data.src_ip);
568        (data.src_port, data.dst_port) = (data.dst_port, data.src_port);
569        (data.src_ttl, data.dst_ttl) = (data.dst_ttl, data.src_ttl);
570        (data.fwd_packets_count, data.bwd_packets_count) =
571            (data.bwd_packets_count, data.fwd_packets_count);
572    }
573}
574
575impl Default for Packets {
576    fn default() -> Self {
577        Packets {
578            packets: Vec::with_capacity(150),
579            directions: Vec::with_capacity(150),
580            timestamps: Vec::with_capacity(150),
581            flow: Flow::UDP(FlowData {
582                src_ip: Ipv4Addr::UNSPECIFIED,
583                dst_ip: Ipv4Addr::UNSPECIFIED,
584                src_os: OS::Linux,
585                dst_os: OS::Linux,
586                src_mac: MacAddr::zero(),
587                dst_mac: MacAddr::zero(),
588                src_port: 0,
589                dst_port: 0,
590                src_ttl: 0,
591                dst_ttl: 0,
592                packets_count_cluster: 0,
593                fwd_packets_count: 0,
594                bwd_packets_count: 0,
595                timestamp: Duration::new(0, 0),
596                l7_proto: L7Proto::HTTP,
597            }),
598        }
599    }
600}
601
602/// The recycler used by thingbuf
603pub struct PacketsRecycler {}
604
605impl Recycle<Packets> for PacketsRecycler {
606    // Required methods
607    fn new_element(&self) -> Packets {
608        Packets::default()
609    }
610    fn recycle(&self, element: &mut Packets) {
611        element.clear();
612    }
613}
614
615#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
616/// A 5-uplet typically used to identify a flow
617pub struct FlowId {
618    pub protocol: L4Proto,
619    pub src_ip: Ipv4Addr,
620    pub dst_ip: Ipv4Addr,
621    pub src_port: u16,
622    pub dst_port: u16,
623}
624
625impl FlowId {
626    /// Check whether a given flow is compatible with the current `FlowId`.
627    /// Compatibility is based on matching source IP, destination IP, source port, and destination port.
628    pub fn is_compatible(&self, f: &Flow) -> bool {
629        let d = f.get_data();
630        self.src_ip == d.src_ip
631            && self.dst_ip == d.dst_ip
632            && self.src_port == d.src_port
633            && self.dst_port == d.dst_port
634            && self.protocol == f.get_proto()
635    }
636
637    pub fn normalize(&mut self) {
638        if self.src_ip > self.dst_ip
639            || (self.src_ip == self.dst_ip && self.src_port > self.dst_port)
640        {
641            std::mem::swap(&mut self.src_ip, &mut self.dst_ip);
642            std::mem::swap(&mut self.src_port, &mut self.dst_port);
643        }
644    }
645}