netflow_parser/
netflow_common.rs

1use std::net::IpAddr;
2
3use crate::NetflowPacket;
4use crate::protocol::ProtocolTypes;
5use crate::static_versions::{v5::V5, v7::V7};
6use crate::variable_versions::data_number::FieldValue;
7use crate::variable_versions::ipfix_lookup::{IANAIPFixField, IPFixField};
8use crate::variable_versions::v9_lookup::V9Field;
9use crate::variable_versions::{
10    ipfix::{FlowSetBody as IPFixFlowSetBody, IPFix},
11    v9::{FlowSetBody as V9FlowSetBody, V9},
12};
13
14#[derive(Debug)]
15pub enum NetflowCommonError {
16    UnknownVersion(NetflowPacket),
17}
18
19/// Generic configuration for mapping fields to NetflowCommonFlowSet fields.
20/// Each field can have a primary and optional fallback field type.
21#[derive(Debug, Clone)]
22pub struct FieldMapping<T> {
23    /// Primary field to search for
24    pub primary: T,
25    /// Optional fallback field if primary is not found
26    pub fallback: Option<T>,
27}
28
29impl<T> FieldMapping<T> {
30    /// Create a new field mapping with only a primary field
31    pub fn new(primary: T) -> Self {
32        Self {
33            primary,
34            fallback: None,
35        }
36    }
37
38    /// Create a new field mapping with a primary and fallback field
39    pub fn with_fallback(primary: T, fallback: T) -> Self {
40        Self {
41            primary,
42            fallback: Some(fallback),
43        }
44    }
45}
46
47/// Type alias for V9 field mapping configuration
48pub type V9FieldMapping = FieldMapping<V9Field>;
49
50/// Type alias for IPFix field mapping configuration
51pub type IPFixFieldMapping = FieldMapping<IPFixField>;
52
53/// Configuration for V9 field mappings used when converting V9 to NetflowCommon.
54///
55/// This allows customization of which V9 fields map to which NetflowCommonFlowSet fields.
56/// By default, standard IANA field mappings are used.
57///
58/// # Example
59///
60/// ```rust
61/// use netflow_parser::netflow_common::V9FieldMappingConfig;
62/// use netflow_parser::variable_versions::v9_lookup::V9Field;
63///
64/// // Use default mappings
65/// let config = V9FieldMappingConfig::default();
66///
67/// // Or customize specific fields
68/// let mut config = V9FieldMappingConfig::default();
69/// config.src_addr.primary = V9Field::Ipv6SrcAddr;  // Prefer IPv6
70/// config.src_addr.fallback = Some(V9Field::Ipv4SrcAddr);  // Fall back to IPv4
71/// ```
72#[derive(Debug, Clone)]
73pub struct V9FieldMappingConfig {
74    /// Mapping for source address field
75    pub src_addr: V9FieldMapping,
76    /// Mapping for destination address field
77    pub dst_addr: V9FieldMapping,
78    /// Mapping for source port field
79    pub src_port: V9FieldMapping,
80    /// Mapping for destination port field
81    pub dst_port: V9FieldMapping,
82    /// Mapping for protocol field
83    pub protocol: V9FieldMapping,
84    /// Mapping for first seen timestamp field
85    pub first_seen: V9FieldMapping,
86    /// Mapping for last seen timestamp field
87    pub last_seen: V9FieldMapping,
88    /// Mapping for source MAC address field
89    pub src_mac: V9FieldMapping,
90    /// Mapping for destination MAC address field
91    pub dst_mac: V9FieldMapping,
92}
93
94impl Default for V9FieldMappingConfig {
95    fn default() -> Self {
96        Self {
97            src_addr: V9FieldMapping::with_fallback(V9Field::Ipv4SrcAddr, V9Field::Ipv6SrcAddr),
98            dst_addr: V9FieldMapping::with_fallback(V9Field::Ipv4DstAddr, V9Field::Ipv6DstAddr),
99            src_port: V9FieldMapping::new(V9Field::L4SrcPort),
100            dst_port: V9FieldMapping::new(V9Field::L4DstPort),
101            protocol: V9FieldMapping::new(V9Field::Protocol),
102            first_seen: V9FieldMapping::new(V9Field::FirstSwitched),
103            last_seen: V9FieldMapping::new(V9Field::LastSwitched),
104            src_mac: V9FieldMapping::new(V9Field::InSrcMac),
105            dst_mac: V9FieldMapping::new(V9Field::InDstMac),
106        }
107    }
108}
109
110/// Configuration for IPFIX field mappings used when converting IPFIX to NetflowCommon.
111///
112/// This allows customization of which IPFIX fields map to which NetflowCommonFlowSet fields.
113/// By default, standard IANA field mappings are used.
114///
115/// # Example
116///
117/// ```rust
118/// use netflow_parser::netflow_common::IPFixFieldMappingConfig;
119/// use netflow_parser::variable_versions::ipfix_lookup::{IPFixField, IANAIPFixField};
120///
121/// // Use default mappings
122/// let config = IPFixFieldMappingConfig::default();
123///
124/// // Or customize specific fields
125/// let mut config = IPFixFieldMappingConfig::default();
126/// config.src_addr.primary = IPFixField::IANA(IANAIPFixField::SourceIpv6address);  // Prefer IPv6
127/// config.src_addr.fallback = Some(IPFixField::IANA(IANAIPFixField::SourceIpv4address));  // Fall back to IPv4
128/// ```
129#[derive(Debug, Clone)]
130pub struct IPFixFieldMappingConfig {
131    /// Mapping for source address field
132    pub src_addr: IPFixFieldMapping,
133    /// Mapping for destination address field
134    pub dst_addr: IPFixFieldMapping,
135    /// Mapping for source port field
136    pub src_port: IPFixFieldMapping,
137    /// Mapping for destination port field
138    pub dst_port: IPFixFieldMapping,
139    /// Mapping for protocol field
140    pub protocol: IPFixFieldMapping,
141    /// Mapping for first seen timestamp field
142    pub first_seen: IPFixFieldMapping,
143    /// Mapping for last seen timestamp field
144    pub last_seen: IPFixFieldMapping,
145    /// Mapping for source MAC address field
146    pub src_mac: IPFixFieldMapping,
147    /// Mapping for destination MAC address field
148    pub dst_mac: IPFixFieldMapping,
149}
150
151impl Default for IPFixFieldMappingConfig {
152    fn default() -> Self {
153        Self {
154            src_addr: IPFixFieldMapping::with_fallback(
155                IPFixField::IANA(IANAIPFixField::SourceIpv4address),
156                IPFixField::IANA(IANAIPFixField::SourceIpv6address),
157            ),
158            dst_addr: IPFixFieldMapping::with_fallback(
159                IPFixField::IANA(IANAIPFixField::DestinationIpv4address),
160                IPFixField::IANA(IANAIPFixField::DestinationIpv6address),
161            ),
162            src_port: IPFixFieldMapping::new(IPFixField::IANA(
163                IANAIPFixField::SourceTransportPort,
164            )),
165            dst_port: IPFixFieldMapping::new(IPFixField::IANA(
166                IANAIPFixField::DestinationTransportPort,
167            )),
168            protocol: IPFixFieldMapping::new(IPFixField::IANA(
169                IANAIPFixField::ProtocolIdentifier,
170            )),
171            first_seen: IPFixFieldMapping::new(IPFixField::IANA(
172                IANAIPFixField::FlowStartSysUpTime,
173            )),
174            last_seen: IPFixFieldMapping::new(IPFixField::IANA(
175                IANAIPFixField::FlowEndSysUpTime,
176            )),
177            src_mac: IPFixFieldMapping::new(IPFixField::IANA(IANAIPFixField::SourceMacaddress)),
178            dst_mac: IPFixFieldMapping::new(IPFixField::IANA(
179                IANAIPFixField::DestinationMacaddress,
180            )),
181        }
182    }
183}
184
185#[derive(Debug, Default)]
186/// Common structure for Netflow
187pub struct NetflowCommon {
188    pub version: u16,
189    pub timestamp: u32,
190    pub flowsets: Vec<NetflowCommonFlowSet>,
191}
192
193impl TryFrom<&NetflowPacket> for NetflowCommon {
194    type Error = NetflowCommonError;
195
196    fn try_from(value: &NetflowPacket) -> Result<Self, NetflowCommonError> {
197        match value {
198            NetflowPacket::V5(v5) => Ok(v5.into()),
199            NetflowPacket::V7(v7) => Ok(v7.into()),
200            NetflowPacket::V9(v9) => Ok(v9.into()),
201            NetflowPacket::IPFix(ipfix) => Ok(ipfix.into()),
202        }
203    }
204}
205
206#[derive(Debug, Default)]
207/// Common flow set structure for Netflow
208pub struct NetflowCommonFlowSet {
209    /// Source IP address
210    pub src_addr: Option<IpAddr>,
211    /// Destination IP address
212    pub dst_addr: Option<IpAddr>,
213    /// TCP/UDP source port number or equivalent
214    pub src_port: Option<u16>,
215    /// TCP/UDP destination port number or equivalent
216    pub dst_port: Option<u16>,
217    /// Number of IP protocol type (for example, TCP = 6; UDP = 17)
218    pub protocol_number: Option<u8>,
219    /// IP protocol type itself
220    pub protocol_type: Option<ProtocolTypes>,
221    /// Duration of the flow first
222    pub first_seen: Option<u32>,
223    /// Duration of the flow last
224    pub last_seen: Option<u32>,
225    /// Source MAC address
226    pub src_mac: Option<String>,
227    /// Destination MAC address
228    pub dst_mac: Option<String>,
229}
230
231impl From<&V5> for NetflowCommon {
232    fn from(value: &V5) -> Self {
233        // Convert V5 to NetflowCommon
234        NetflowCommon {
235            version: value.header.version,
236            timestamp: value.header.sys_up_time,
237            flowsets: value
238                .flowsets
239                .iter()
240                .map(|set| NetflowCommonFlowSet {
241                    src_addr: Some(set.src_addr.into()),
242                    dst_addr: Some(set.dst_addr.into()),
243                    src_port: Some(set.src_port),
244                    dst_port: Some(set.dst_port),
245                    protocol_number: Some(set.protocol_number),
246                    protocol_type: Some(set.protocol_type),
247                    first_seen: Some(set.first),
248                    last_seen: Some(set.last),
249                    src_mac: None,
250                    dst_mac: None,
251                })
252                .collect(),
253        }
254    }
255}
256
257impl From<&V7> for NetflowCommon {
258    fn from(value: &V7) -> Self {
259        // Convert V7 to NetflowCommon
260        NetflowCommon {
261            version: value.header.version,
262            timestamp: value.header.sys_up_time,
263            flowsets: value
264                .flowsets
265                .iter()
266                .map(|set| NetflowCommonFlowSet {
267                    src_addr: Some(set.src_addr.into()),
268                    dst_addr: Some(set.dst_addr.into()),
269                    src_port: Some(set.src_port),
270                    dst_port: Some(set.dst_port),
271                    protocol_number: Some(set.protocol_number),
272                    protocol_type: Some(set.protocol_type),
273                    first_seen: Some(set.first),
274                    last_seen: Some(set.last),
275                    src_mac: None,
276                    dst_mac: None,
277                })
278                .collect(),
279        }
280    }
281}
282
283/// Helper structure to store all found V9 fields in a single pass
284#[derive(Copy, Clone)]
285struct V9FieldCache<'a> {
286    src_addr_v4: Option<&'a FieldValue>,
287    src_addr_v6: Option<&'a FieldValue>,
288    dst_addr_v4: Option<&'a FieldValue>,
289    dst_addr_v6: Option<&'a FieldValue>,
290    src_port: Option<&'a FieldValue>,
291    dst_port: Option<&'a FieldValue>,
292    protocol: Option<&'a FieldValue>,
293    first_seen: Option<&'a FieldValue>,
294    last_seen: Option<&'a FieldValue>,
295    src_mac: Option<&'a FieldValue>,
296    dst_mac: Option<&'a FieldValue>,
297}
298
299impl<'a> V9FieldCache<'a> {
300    fn from_fields(fields: &'a [(V9Field, FieldValue)]) -> Self {
301        let mut cache = Self {
302            src_addr_v4: None,
303            src_addr_v6: None,
304            dst_addr_v4: None,
305            dst_addr_v6: None,
306            src_port: None,
307            dst_port: None,
308            protocol: None,
309            first_seen: None,
310            last_seen: None,
311            src_mac: None,
312            dst_mac: None,
313        };
314
315        // Single pass through all fields
316        for (field_type, field_value) in fields {
317            match field_type {
318                V9Field::Ipv4SrcAddr => cache.src_addr_v4 = Some(field_value),
319                V9Field::Ipv6SrcAddr => cache.src_addr_v6 = Some(field_value),
320                V9Field::Ipv4DstAddr => cache.dst_addr_v4 = Some(field_value),
321                V9Field::Ipv6DstAddr => cache.dst_addr_v6 = Some(field_value),
322                V9Field::L4SrcPort => cache.src_port = Some(field_value),
323                V9Field::L4DstPort => cache.dst_port = Some(field_value),
324                V9Field::Protocol => cache.protocol = Some(field_value),
325                V9Field::FirstSwitched => cache.first_seen = Some(field_value),
326                V9Field::LastSwitched => cache.last_seen = Some(field_value),
327                V9Field::InSrcMac => cache.src_mac = Some(field_value),
328                V9Field::InDstMac => cache.dst_mac = Some(field_value),
329                _ => {} // Ignore other fields
330            }
331        }
332
333        cache
334    }
335}
336
337/// Macro to create NetflowCommonFlowSet from a cache structure with separate v4/v6 fields
338macro_rules! create_common_flowset_with_ip_versions {
339    ($cache:expr, $src_v4:ident, $src_v6:ident, $dst_v4:ident, $dst_v6:ident) => {
340        NetflowCommonFlowSet {
341            src_addr: $cache
342                .$src_v4
343                .or($cache.$src_v6)
344                .and_then(|v| v.try_into().ok()),
345            dst_addr: $cache
346                .$dst_v4
347                .or($cache.$dst_v6)
348                .and_then(|v| v.try_into().ok()),
349            src_port: $cache.src_port.and_then(|v| v.try_into().ok()),
350            dst_port: $cache.dst_port.and_then(|v| v.try_into().ok()),
351            protocol_number: $cache.protocol.and_then(|v| v.try_into().ok()),
352            protocol_type: $cache.protocol.and_then(|v| {
353                v.try_into()
354                    .ok()
355                    .map(|proto: u8| ProtocolTypes::from(proto))
356            }),
357            first_seen: $cache.first_seen.and_then(|v| v.try_into().ok()),
358            last_seen: $cache.last_seen.and_then(|v| v.try_into().ok()),
359            src_mac: $cache.src_mac.and_then(|v| v.try_into().ok()),
360            dst_mac: $cache.dst_mac.and_then(|v| v.try_into().ok()),
361        }
362    };
363}
364
365/// Macro to create NetflowCommonFlowSet from a cache structure
366macro_rules! create_common_flowset {
367    ($cache:expr) => {
368        NetflowCommonFlowSet {
369            src_addr: $cache.src_addr.and_then(|v| v.try_into().ok()),
370            dst_addr: $cache.dst_addr.and_then(|v| v.try_into().ok()),
371            src_port: $cache.src_port.and_then(|v| v.try_into().ok()),
372            dst_port: $cache.dst_port.and_then(|v| v.try_into().ok()),
373            protocol_number: $cache.protocol.and_then(|v| v.try_into().ok()),
374            protocol_type: $cache.protocol.and_then(|v| {
375                v.try_into()
376                    .ok()
377                    .map(|proto: u8| ProtocolTypes::from(proto))
378            }),
379            first_seen: $cache.first_seen.and_then(|v| v.try_into().ok()),
380            last_seen: $cache.last_seen.and_then(|v| v.try_into().ok()),
381            src_mac: $cache.src_mac.and_then(|v| v.try_into().ok()),
382            dst_mac: $cache.dst_mac.and_then(|v| v.try_into().ok()),
383        }
384    };
385}
386
387/// Macro to generate field cache checking logic for config-based caches
388macro_rules! check_field_mapping {
389    ($field_type:expr, $field_value:expr, $cache:expr, $config:expr, $field_name:ident) => {
390        if *$field_type == $config.$field_name.primary {
391            $cache.$field_name = Some($field_value);
392        } else if Some(*$field_type) == $config.$field_name.fallback
393            && $cache.$field_name.is_none()
394        {
395            $cache.$field_name = Some($field_value);
396        }
397    };
398}
399
400/// Helper structure to cache V9 field lookups with custom mapping in a single pass
401#[derive(Copy, Clone)]
402struct V9ConfigFieldCache<'a> {
403    src_addr: Option<&'a FieldValue>,
404    dst_addr: Option<&'a FieldValue>,
405    src_port: Option<&'a FieldValue>,
406    dst_port: Option<&'a FieldValue>,
407    protocol: Option<&'a FieldValue>,
408    first_seen: Option<&'a FieldValue>,
409    last_seen: Option<&'a FieldValue>,
410    src_mac: Option<&'a FieldValue>,
411    dst_mac: Option<&'a FieldValue>,
412}
413
414impl<'a> V9ConfigFieldCache<'a> {
415    fn from_fields_with_config(
416        fields: &'a [(V9Field, FieldValue)],
417        config: &V9FieldMappingConfig,
418    ) -> Self {
419        let mut cache = Self {
420            src_addr: None,
421            dst_addr: None,
422            src_port: None,
423            dst_port: None,
424            protocol: None,
425            first_seen: None,
426            last_seen: None,
427            src_mac: None,
428            dst_mac: None,
429        };
430
431        // Single pass through all fields, collecting based on config
432        for (field_type, field_value) in fields {
433            check_field_mapping!(field_type, field_value, cache, config, src_addr);
434            check_field_mapping!(field_type, field_value, cache, config, dst_addr);
435            check_field_mapping!(field_type, field_value, cache, config, src_port);
436            check_field_mapping!(field_type, field_value, cache, config, dst_port);
437            check_field_mapping!(field_type, field_value, cache, config, protocol);
438            check_field_mapping!(field_type, field_value, cache, config, first_seen);
439            check_field_mapping!(field_type, field_value, cache, config, last_seen);
440            check_field_mapping!(field_type, field_value, cache, config, src_mac);
441            check_field_mapping!(field_type, field_value, cache, config, dst_mac);
442        }
443
444        cache
445    }
446}
447
448impl NetflowCommon {
449    /// Convert a V9 packet to NetflowCommon using a custom field mapping configuration.
450    ///
451    /// This allows you to specify which V9 fields should be used for each
452    /// NetflowCommonFlowSet field, including fallback fields.
453    ///
454    /// # Example
455    ///
456    /// ```rust
457    /// use netflow_parser::netflow_common::V9FieldMappingConfig;
458    /// use netflow_parser::variable_versions::v9_lookup::V9Field;
459    ///
460    /// // Use custom configuration that prefers IPv6
461    /// let mut config = V9FieldMappingConfig::default();
462    /// config.src_addr.primary = V9Field::Ipv6SrcAddr;
463    /// config.src_addr.fallback = Some(V9Field::Ipv4SrcAddr);
464    ///
465    /// // Then use: NetflowCommon::from_v9_with_config(&v9, &config);
466    /// ```
467    pub fn from_v9_with_config(value: &V9, config: &V9FieldMappingConfig) -> Self {
468        let mut flowsets = vec![];
469
470        for flowset in &value.flowsets {
471            if let V9FlowSetBody::Data(data) = &flowset.body {
472                for data_field in &data.fields {
473                    // Single pass through fields to collect all values with config
474                    let cache = V9ConfigFieldCache::from_fields_with_config(data_field, config);
475                    flowsets.push(create_common_flowset!(cache));
476                }
477            }
478        }
479
480        NetflowCommon {
481            version: value.header.version,
482            timestamp: value.header.sys_up_time,
483            flowsets,
484        }
485    }
486}
487
488impl From<&V9> for NetflowCommon {
489    fn from(value: &V9) -> Self {
490        // Convert V9 to NetflowCommon using default configuration with single-pass field lookup
491        let mut flowsets = vec![];
492
493        for flowset in &value.flowsets {
494            if let V9FlowSetBody::Data(data) = &flowset.body {
495                for data_field in &data.fields {
496                    // Single pass through fields to collect all values
497                    let cache = V9FieldCache::from_fields(data_field);
498                    flowsets.push(create_common_flowset_with_ip_versions!(
499                        cache,
500                        src_addr_v4,
501                        src_addr_v6,
502                        dst_addr_v4,
503                        dst_addr_v6
504                    ));
505                }
506            }
507        }
508
509        NetflowCommon {
510            version: value.header.version,
511            timestamp: value.header.sys_up_time,
512            flowsets,
513        }
514    }
515}
516
517/// Helper structure to store all found IPFIX fields in a single pass
518#[derive(Copy, Clone)]
519struct IPFixFieldCache<'a> {
520    src_addr_v4: Option<&'a FieldValue>,
521    src_addr_v6: Option<&'a FieldValue>,
522    dst_addr_v4: Option<&'a FieldValue>,
523    dst_addr_v6: Option<&'a FieldValue>,
524    src_port: Option<&'a FieldValue>,
525    dst_port: Option<&'a FieldValue>,
526    protocol: Option<&'a FieldValue>,
527    first_seen: Option<&'a FieldValue>,
528    last_seen: Option<&'a FieldValue>,
529    src_mac: Option<&'a FieldValue>,
530    dst_mac: Option<&'a FieldValue>,
531}
532
533impl<'a> IPFixFieldCache<'a> {
534    fn from_fields(fields: &'a [(IPFixField, FieldValue)]) -> Self {
535        let mut cache = Self {
536            src_addr_v4: None,
537            src_addr_v6: None,
538            dst_addr_v4: None,
539            dst_addr_v6: None,
540            src_port: None,
541            dst_port: None,
542            protocol: None,
543            first_seen: None,
544            last_seen: None,
545            src_mac: None,
546            dst_mac: None,
547        };
548
549        // Single pass through all fields
550        for (field_type, field_value) in fields {
551            match field_type {
552                IPFixField::IANA(IANAIPFixField::SourceIpv4address) => {
553                    cache.src_addr_v4 = Some(field_value)
554                }
555                IPFixField::IANA(IANAIPFixField::SourceIpv6address) => {
556                    cache.src_addr_v6 = Some(field_value)
557                }
558                IPFixField::IANA(IANAIPFixField::DestinationIpv4address) => {
559                    cache.dst_addr_v4 = Some(field_value)
560                }
561                IPFixField::IANA(IANAIPFixField::DestinationIpv6address) => {
562                    cache.dst_addr_v6 = Some(field_value)
563                }
564                IPFixField::IANA(IANAIPFixField::SourceTransportPort) => {
565                    cache.src_port = Some(field_value)
566                }
567                IPFixField::IANA(IANAIPFixField::DestinationTransportPort) => {
568                    cache.dst_port = Some(field_value)
569                }
570                IPFixField::IANA(IANAIPFixField::ProtocolIdentifier) => {
571                    cache.protocol = Some(field_value)
572                }
573                IPFixField::IANA(IANAIPFixField::FlowStartSysUpTime) => {
574                    cache.first_seen = Some(field_value)
575                }
576                IPFixField::IANA(IANAIPFixField::FlowEndSysUpTime) => {
577                    cache.last_seen = Some(field_value)
578                }
579                IPFixField::IANA(IANAIPFixField::SourceMacaddress) => {
580                    cache.src_mac = Some(field_value)
581                }
582                IPFixField::IANA(IANAIPFixField::DestinationMacaddress) => {
583                    cache.dst_mac = Some(field_value)
584                }
585                _ => {} // Ignore other fields
586            }
587        }
588
589        cache
590    }
591}
592
593/// Macro to check field mapping for types that need as_ref() for fallback comparison
594macro_rules! check_field_mapping_ref {
595    ($field_type:expr, $field_value:expr, $cache:expr, $config:expr, $field_name:ident) => {
596        if *$field_type == $config.$field_name.primary {
597            $cache.$field_name = Some($field_value);
598        } else if $config.$field_name.fallback.as_ref() == Some($field_type)
599            && $cache.$field_name.is_none()
600        {
601            $cache.$field_name = Some($field_value);
602        }
603    };
604}
605
606/// Helper structure to cache IPFIX field lookups with custom mapping in a single pass
607#[derive(Copy, Clone)]
608struct IPFixConfigFieldCache<'a> {
609    src_addr: Option<&'a FieldValue>,
610    dst_addr: Option<&'a FieldValue>,
611    src_port: Option<&'a FieldValue>,
612    dst_port: Option<&'a FieldValue>,
613    protocol: Option<&'a FieldValue>,
614    first_seen: Option<&'a FieldValue>,
615    last_seen: Option<&'a FieldValue>,
616    src_mac: Option<&'a FieldValue>,
617    dst_mac: Option<&'a FieldValue>,
618}
619
620impl<'a> IPFixConfigFieldCache<'a> {
621    fn from_fields_with_config(
622        fields: &'a [(IPFixField, FieldValue)],
623        config: &IPFixFieldMappingConfig,
624    ) -> Self {
625        let mut cache = Self {
626            src_addr: None,
627            dst_addr: None,
628            src_port: None,
629            dst_port: None,
630            protocol: None,
631            first_seen: None,
632            last_seen: None,
633            src_mac: None,
634            dst_mac: None,
635        };
636
637        // Single pass through all fields, collecting based on config
638        for (field_type, field_value) in fields {
639            check_field_mapping_ref!(field_type, field_value, cache, config, src_addr);
640            check_field_mapping_ref!(field_type, field_value, cache, config, dst_addr);
641            check_field_mapping_ref!(field_type, field_value, cache, config, src_port);
642            check_field_mapping_ref!(field_type, field_value, cache, config, dst_port);
643            check_field_mapping_ref!(field_type, field_value, cache, config, protocol);
644            check_field_mapping_ref!(field_type, field_value, cache, config, first_seen);
645            check_field_mapping_ref!(field_type, field_value, cache, config, last_seen);
646            check_field_mapping_ref!(field_type, field_value, cache, config, src_mac);
647            check_field_mapping_ref!(field_type, field_value, cache, config, dst_mac);
648        }
649
650        cache
651    }
652}
653
654impl NetflowCommon {
655    /// Convert an IPFIX packet to NetflowCommon using a custom field mapping configuration.
656    ///
657    /// This allows you to specify which IPFIX fields should be used for each
658    /// NetflowCommonFlowSet field, including fallback fields.
659    ///
660    /// # Example
661    ///
662    /// ```rust
663    /// use netflow_parser::netflow_common::IPFixFieldMappingConfig;
664    /// use netflow_parser::variable_versions::ipfix_lookup::{IPFixField, IANAIPFixField};
665    ///
666    /// // Use custom configuration that prefers IPv6
667    /// let mut config = IPFixFieldMappingConfig::default();
668    /// config.src_addr.primary = IPFixField::IANA(IANAIPFixField::SourceIpv6address);
669    /// config.src_addr.fallback = Some(IPFixField::IANA(IANAIPFixField::SourceIpv4address));
670    ///
671    /// // Then use: NetflowCommon::from_ipfix_with_config(&ipfix, &config);
672    /// ```
673    pub fn from_ipfix_with_config(value: &IPFix, config: &IPFixFieldMappingConfig) -> Self {
674        let mut flowsets = vec![];
675
676        for flowset in &value.flowsets {
677            if let IPFixFlowSetBody::Data(data) = &flowset.body {
678                for data_field in &data.fields {
679                    // Single pass through fields to collect all values with config
680                    let cache =
681                        IPFixConfigFieldCache::from_fields_with_config(data_field, config);
682                    flowsets.push(create_common_flowset!(cache));
683                }
684            }
685        }
686
687        NetflowCommon {
688            version: value.header.version,
689            timestamp: value.header.export_time,
690            flowsets,
691        }
692    }
693}
694
695impl From<&IPFix> for NetflowCommon {
696    fn from(value: &IPFix) -> Self {
697        // Convert IPFix to NetflowCommon with single-pass field lookup
698        let mut flowsets = vec![];
699
700        for flowset in &value.flowsets {
701            if let IPFixFlowSetBody::Data(data) = &flowset.body {
702                for data_field in &data.fields {
703                    // Single pass through fields to collect all values
704                    let cache = IPFixFieldCache::from_fields(data_field);
705                    flowsets.push(create_common_flowset_with_ip_versions!(
706                        cache,
707                        src_addr_v4,
708                        src_addr_v6,
709                        dst_addr_v4,
710                        dst_addr_v6
711                    ));
712                }
713            }
714        }
715
716        NetflowCommon {
717            version: value.header.version,
718            timestamp: value.header.export_time,
719            flowsets,
720        }
721    }
722}
723
724#[cfg(test)]
725mod common_tests {
726    use std::net::{IpAddr, Ipv4Addr};
727
728    use crate::netflow_common::NetflowCommon;
729    use crate::static_versions::v5::{FlowSet as V5FlowSet, Header as V5Header, V5};
730    use crate::static_versions::v7::{FlowSet as V7FlowSet, Header as V7Header, V7};
731    use crate::variable_versions::data_number::{DataNumber, FieldValue};
732    use crate::variable_versions::ipfix::{
733        Data as IPFixData, FlowSet as IPFixFlowSet, FlowSetBody as IPFixFlowSetBody,
734        FlowSetHeader as IPFixFlowSetHeader, Header as IPFixHeader, IPFix,
735    };
736    use crate::variable_versions::ipfix_lookup::{IANAIPFixField, IPFixField};
737    use crate::variable_versions::v9::{
738        Data as V9Data, FlowSet as V9FlowSet, FlowSetBody as V9FlowSetBody,
739        FlowSetHeader as V9FlowSetHeader, Header as V9Header, V9,
740    };
741    use crate::variable_versions::v9_lookup::V9Field;
742
743    #[test]
744    fn it_converts_v5_to_common() {
745        let v5 = V5 {
746            header: V5Header {
747                version: 5,
748                count: 1,
749                sys_up_time: 100,
750                unix_secs: 1609459200,
751                unix_nsecs: 0,
752                flow_sequence: 1,
753                engine_type: 0,
754                engine_id: 0,
755                sampling_interval: 0,
756            },
757            flowsets: vec![V5FlowSet {
758                src_addr: Ipv4Addr::new(192, 168, 1, 1),
759                dst_addr: Ipv4Addr::new(192, 168, 1, 2),
760                src_port: 1234,
761                dst_port: 80,
762                protocol_number: 6,
763                protocol_type: crate::protocol::ProtocolTypes::Tcp,
764                next_hop: Ipv4Addr::new(192, 168, 1, 254),
765                input: 0,
766                output: 0,
767                d_pkts: 10,
768                d_octets: 1000,
769                first: 100,
770                last: 200,
771                pad1: 0,
772                tcp_flags: 0,
773                tos: 0,
774                src_as: 0,
775                dst_as: 0,
776                src_mask: 0,
777                dst_mask: 0,
778                pad2: 0,
779            }],
780        };
781
782        let common: NetflowCommon = NetflowCommon::from(&v5);
783
784        assert_eq!(common.version, 5);
785        assert_eq!(common.timestamp, 100);
786        assert_eq!(common.flowsets.len(), 1);
787        let flowset = &common.flowsets[0];
788        assert_eq!(
789            flowset.src_addr.unwrap(),
790            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))
791        );
792        assert_eq!(
793            flowset.dst_addr.unwrap(),
794            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))
795        );
796        assert_eq!(flowset.src_port.unwrap(), 1234);
797        assert_eq!(flowset.dst_port.unwrap(), 80);
798        assert_eq!(flowset.protocol_number.unwrap(), 6);
799        assert_eq!(
800            flowset.protocol_type.unwrap(),
801            crate::protocol::ProtocolTypes::Tcp
802        );
803        assert_eq!(flowset.first_seen.unwrap(), 100);
804        assert_eq!(flowset.last_seen.unwrap(), 200);
805    }
806
807    #[test]
808    fn it_converts_v7_to_common() {
809        let v7 = V7 {
810            header: V7Header {
811                version: 7,
812                count: 1,
813                sys_up_time: 100,
814                unix_secs: 1609459200,
815                unix_nsecs: 0,
816                flow_sequence: 1,
817                reserved: 0,
818            },
819            flowsets: vec![V7FlowSet {
820                src_addr: Ipv4Addr::new(192, 168, 1, 1),
821                dst_addr: Ipv4Addr::new(192, 168, 1, 2),
822                src_port: 1234,
823                dst_port: 80,
824                protocol_number: 6,
825                protocol_type: crate::protocol::ProtocolTypes::Tcp,
826                next_hop: Ipv4Addr::new(192, 168, 1, 254),
827                input: 0,
828                output: 0,
829                d_pkts: 10,
830                d_octets: 1000,
831                first: 100,
832                last: 200,
833                tcp_flags: 0,
834                tos: 0,
835                src_as: 0,
836                dst_as: 0,
837                src_mask: 0,
838                dst_mask: 0,
839                flags_fields_invalid: 0,
840                flags_fields_valid: 0,
841                router_src: Ipv4Addr::new(192, 168, 1, 254),
842            }],
843        };
844
845        let common: NetflowCommon = NetflowCommon::from(&v7);
846
847        assert_eq!(common.version, 7);
848        assert_eq!(common.timestamp, 100);
849        assert_eq!(common.flowsets.len(), 1);
850        let flowset = &common.flowsets[0];
851        assert_eq!(
852            flowset.src_addr.unwrap(),
853            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))
854        );
855        assert_eq!(
856            flowset.dst_addr.unwrap(),
857            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))
858        );
859        assert_eq!(flowset.src_port.unwrap(), 1234);
860        assert_eq!(flowset.dst_port.unwrap(), 80);
861        assert_eq!(flowset.protocol_number.unwrap(), 6);
862        assert_eq!(
863            flowset.protocol_type.unwrap(),
864            crate::protocol::ProtocolTypes::Tcp
865        );
866        assert_eq!(flowset.first_seen.unwrap(), 100);
867        assert_eq!(flowset.last_seen.unwrap(), 200);
868    }
869
870    #[test]
871    fn it_converts_v9_to_common() {
872        // Test for V9 conversion
873        let v9 = V9 {
874            header: V9Header {
875                version: 9,
876                count: 1,
877                sys_up_time: 100,
878                unix_secs: 1609459200,
879                sequence_number: 1,
880                source_id: 0,
881            },
882            flowsets: vec![V9FlowSet {
883                header: V9FlowSetHeader {
884                    flowset_id: 0,
885                    length: 0,
886                },
887                body: V9FlowSetBody::Data(V9Data {
888                    padding: vec![],
889                    fields: vec![Vec::from([
890                        (
891                            V9Field::Ipv4SrcAddr,
892                            FieldValue::Ip4Addr(Ipv4Addr::new(192, 168, 1, 1)),
893                        ),
894                        (
895                            V9Field::Ipv4DstAddr,
896                            FieldValue::Ip4Addr(Ipv4Addr::new(192, 168, 1, 2)),
897                        ),
898                        (
899                            V9Field::L4SrcPort,
900                            FieldValue::DataNumber(DataNumber::U16(1234)),
901                        ),
902                        (
903                            V9Field::L4DstPort,
904                            FieldValue::DataNumber(DataNumber::U16(80)),
905                        ),
906                        (V9Field::Protocol, FieldValue::DataNumber(DataNumber::U8(6))),
907                        (
908                            V9Field::FirstSwitched,
909                            FieldValue::DataNumber(DataNumber::U32(100)),
910                        ),
911                        (
912                            V9Field::LastSwitched,
913                            FieldValue::DataNumber(DataNumber::U32(200)),
914                        ),
915                        (
916                            V9Field::InSrcMac,
917                            FieldValue::MacAddr("00:00:00:00:00:01".to_string()),
918                        ),
919                        (
920                            V9Field::InDstMac,
921                            FieldValue::MacAddr("00:00:00:00:00:02".to_string()),
922                        ),
923                    ])],
924                }),
925            }],
926        };
927
928        let common: NetflowCommon = NetflowCommon::from(&v9);
929        assert_eq!(common.version, 9);
930        assert_eq!(common.timestamp, 100);
931        assert_eq!(common.flowsets.len(), 1);
932        let flowset = &common.flowsets[0];
933        assert_eq!(
934            flowset.src_addr.unwrap(),
935            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))
936        );
937        assert_eq!(
938            flowset.dst_addr.unwrap(),
939            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))
940        );
941        assert_eq!(flowset.src_port.unwrap(), 1234);
942        assert_eq!(flowset.dst_port.unwrap(), 80);
943        assert_eq!(flowset.protocol_number.unwrap(), 6);
944        assert_eq!(
945            flowset.protocol_type.unwrap(),
946            crate::protocol::ProtocolTypes::Tcp
947        );
948        assert_eq!(flowset.first_seen.unwrap(), 100);
949        assert_eq!(flowset.last_seen.unwrap(), 200);
950        assert_eq!(flowset.src_mac.as_ref().unwrap(), "00:00:00:00:00:01");
951        assert_eq!(flowset.dst_mac.as_ref().unwrap(), "00:00:00:00:00:02");
952    }
953
954    #[test]
955    fn it_converts_ipfix_to_common() {
956        // Test for IPFix conversion
957        let ipfix = IPFix {
958            header: IPFixHeader {
959                version: 10,
960                length: 0,
961                export_time: 100,
962                sequence_number: 1,
963                observation_domain_id: 0,
964            },
965            flowsets: vec![IPFixFlowSet {
966                header: IPFixFlowSetHeader {
967                    header_id: 0,
968                    length: 0,
969                },
970                body: IPFixFlowSetBody::Data(IPFixData {
971                    fields: vec![Vec::from([
972                        (
973                            IPFixField::IANA(IANAIPFixField::SourceIpv4address),
974                            FieldValue::Ip4Addr(Ipv4Addr::new(192, 168, 1, 1)),
975                        ),
976                        (
977                            IPFixField::IANA(IANAIPFixField::DestinationIpv4address),
978                            FieldValue::Ip4Addr(Ipv4Addr::new(192, 168, 1, 2)),
979                        ),
980                        (
981                            IPFixField::IANA(IANAIPFixField::SourceTransportPort),
982                            FieldValue::DataNumber(DataNumber::U16(1234)),
983                        ),
984                        (
985                            IPFixField::IANA(IANAIPFixField::DestinationTransportPort),
986                            FieldValue::DataNumber(DataNumber::U16(80)),
987                        ),
988                        (
989                            IPFixField::IANA(IANAIPFixField::ProtocolIdentifier),
990                            FieldValue::DataNumber(DataNumber::U8(6)),
991                        ),
992                        (
993                            IPFixField::IANA(IANAIPFixField::FlowStartSysUpTime),
994                            FieldValue::DataNumber(DataNumber::U32(100)),
995                        ),
996                        (
997                            IPFixField::IANA(IANAIPFixField::FlowEndSysUpTime),
998                            FieldValue::DataNumber(DataNumber::U32(200)),
999                        ),
1000                        (
1001                            IPFixField::IANA(IANAIPFixField::SourceMacaddress),
1002                            FieldValue::MacAddr("00:00:00:00:00:01".to_string()),
1003                        ),
1004                        (
1005                            IPFixField::IANA(IANAIPFixField::DestinationMacaddress),
1006                            FieldValue::MacAddr("00:00:00:00:00:02".to_string()),
1007                        ),
1008                    ])],
1009                    padding: vec![],
1010                }),
1011            }],
1012        };
1013
1014        let common: NetflowCommon = NetflowCommon::from(&ipfix);
1015        assert_eq!(common.version, 10);
1016        assert_eq!(common.timestamp, 100);
1017        assert_eq!(common.flowsets.len(), 1);
1018        let flowset = &common.flowsets[0];
1019        assert_eq!(
1020            flowset.src_addr.unwrap(),
1021            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))
1022        );
1023        assert_eq!(
1024            flowset.dst_addr.unwrap(),
1025            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))
1026        );
1027        assert_eq!(flowset.src_port.unwrap(), 1234);
1028        assert_eq!(flowset.dst_port.unwrap(), 80);
1029        assert_eq!(flowset.protocol_number.unwrap(), 6);
1030        assert_eq!(
1031            flowset.protocol_type.unwrap(),
1032            crate::protocol::ProtocolTypes::Tcp
1033        );
1034        assert_eq!(flowset.first_seen.unwrap(), 100);
1035        assert_eq!(flowset.last_seen.unwrap(), 200);
1036        assert_eq!(flowset.src_mac.as_ref().unwrap(), "00:00:00:00:00:01");
1037        assert_eq!(flowset.dst_mac.as_ref().unwrap(), "00:00:00:00:00:02");
1038    }
1039
1040    #[test]
1041    fn it_converts_v9_to_common_with_custom_config() {
1042        use crate::netflow_common::V9FieldMappingConfig;
1043        use std::net::Ipv6Addr;
1044
1045        // Create a V9 packet with both IPv4 and IPv6 addresses
1046        let v9 = V9 {
1047            header: V9Header {
1048                version: 9,
1049                count: 1,
1050                sys_up_time: 100,
1051                unix_secs: 1609459200,
1052                sequence_number: 1,
1053                source_id: 0,
1054            },
1055            flowsets: vec![V9FlowSet {
1056                header: V9FlowSetHeader {
1057                    flowset_id: 0,
1058                    length: 0,
1059                },
1060                body: V9FlowSetBody::Data(V9Data {
1061                    padding: vec![],
1062                    fields: vec![Vec::from([
1063                        (
1064                            V9Field::Ipv4SrcAddr,
1065                            FieldValue::Ip4Addr(Ipv4Addr::new(192, 168, 1, 1)),
1066                        ),
1067                        (
1068                            V9Field::Ipv6SrcAddr,
1069                            FieldValue::Ip6Addr(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
1070                        ),
1071                        (
1072                            V9Field::L4SrcPort,
1073                            FieldValue::DataNumber(DataNumber::U16(1234)),
1074                        ),
1075                    ])],
1076                }),
1077            }],
1078        };
1079
1080        // Default config prefers IPv4
1081        let default_config = V9FieldMappingConfig::default();
1082        let common = NetflowCommon::from_v9_with_config(&v9, &default_config);
1083        assert_eq!(
1084            common.flowsets[0].src_addr.unwrap(),
1085            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))
1086        );
1087
1088        // Custom config that prefers IPv6
1089        let mut ipv6_config = V9FieldMappingConfig::default();
1090        ipv6_config.src_addr.primary = V9Field::Ipv6SrcAddr;
1091        ipv6_config.src_addr.fallback = Some(V9Field::Ipv4SrcAddr);
1092
1093        let common_ipv6 = NetflowCommon::from_v9_with_config(&v9, &ipv6_config);
1094        assert_eq!(
1095            common_ipv6.flowsets[0].src_addr.unwrap(),
1096            IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1))
1097        );
1098    }
1099
1100    #[test]
1101    fn it_converts_ipfix_to_common_with_custom_config() {
1102        use crate::netflow_common::IPFixFieldMappingConfig;
1103        use std::net::Ipv6Addr;
1104
1105        // Create an IPFIX packet with both IPv4 and IPv6 addresses
1106        let ipfix = IPFix {
1107            header: IPFixHeader {
1108                version: 10,
1109                length: 0,
1110                export_time: 100,
1111                sequence_number: 1,
1112                observation_domain_id: 0,
1113            },
1114            flowsets: vec![IPFixFlowSet {
1115                header: IPFixFlowSetHeader {
1116                    header_id: 0,
1117                    length: 0,
1118                },
1119                body: IPFixFlowSetBody::Data(IPFixData {
1120                    fields: vec![Vec::from([
1121                        (
1122                            IPFixField::IANA(IANAIPFixField::SourceIpv4address),
1123                            FieldValue::Ip4Addr(Ipv4Addr::new(192, 168, 1, 1)),
1124                        ),
1125                        (
1126                            IPFixField::IANA(IANAIPFixField::SourceIpv6address),
1127                            FieldValue::Ip6Addr(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
1128                        ),
1129                        (
1130                            IPFixField::IANA(IANAIPFixField::SourceTransportPort),
1131                            FieldValue::DataNumber(DataNumber::U16(1234)),
1132                        ),
1133                    ])],
1134                    padding: vec![],
1135                }),
1136            }],
1137        };
1138
1139        // Default config prefers IPv4
1140        let default_config = IPFixFieldMappingConfig::default();
1141        let common = NetflowCommon::from_ipfix_with_config(&ipfix, &default_config);
1142        assert_eq!(
1143            common.flowsets[0].src_addr.unwrap(),
1144            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))
1145        );
1146
1147        // Custom config that prefers IPv6
1148        let mut ipv6_config = IPFixFieldMappingConfig::default();
1149        ipv6_config.src_addr.primary = IPFixField::IANA(IANAIPFixField::SourceIpv6address);
1150        ipv6_config.src_addr.fallback =
1151            Some(IPFixField::IANA(IANAIPFixField::SourceIpv4address));
1152
1153        let common_ipv6 = NetflowCommon::from_ipfix_with_config(&ipfix, &ipv6_config);
1154        assert_eq!(
1155            common_ipv6.flowsets[0].src_addr.unwrap(),
1156            IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1))
1157        );
1158    }
1159
1160    #[test]
1161    fn it_uses_fallback_when_primary_not_found() {
1162        use crate::netflow_common::V9FieldMappingConfig;
1163
1164        // Create a V9 packet with only IPv6 address (no IPv4)
1165        let v9 = V9 {
1166            header: V9Header {
1167                version: 9,
1168                count: 1,
1169                sys_up_time: 100,
1170                unix_secs: 1609459200,
1171                sequence_number: 1,
1172                source_id: 0,
1173            },
1174            flowsets: vec![V9FlowSet {
1175                header: V9FlowSetHeader {
1176                    flowset_id: 0,
1177                    length: 0,
1178                },
1179                body: V9FlowSetBody::Data(V9Data {
1180                    padding: vec![],
1181                    fields: vec![Vec::from([(
1182                        V9Field::Ipv4SrcAddr,
1183                        FieldValue::Ip4Addr(Ipv4Addr::new(10, 0, 0, 1)),
1184                    )])],
1185                }),
1186            }],
1187        };
1188
1189        // Config that prefers IPv6 but falls back to IPv4
1190        let mut config = V9FieldMappingConfig::default();
1191        config.src_addr.primary = V9Field::Ipv6SrcAddr;
1192        config.src_addr.fallback = Some(V9Field::Ipv4SrcAddr);
1193
1194        let common = NetflowCommon::from_v9_with_config(&v9, &config);
1195        // Should fall back to IPv4 since IPv6 is not present
1196        assert_eq!(
1197            common.flowsets[0].src_addr.unwrap(),
1198            IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))
1199        );
1200    }
1201}