Skip to main content

netgauze_flow_pkt/
ipfix.rs

1// Copyright (C) 2023-present The NetGauze Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12// implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use chrono::{DateTime, Utc};
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19
20use crate::ie::Field;
21use crate::{DataSetId, FieldSpecifier};
22
23pub const IPFIX_VERSION: u16 = 10;
24
25/// A value of 2 is reserved for Template Sets
26pub(crate) const IPFIX_TEMPLATE_SET_ID: u16 = 2;
27
28/// A value of 3 is reserved for Options Template Sets
29pub(crate) const IPFIX_OPTIONS_TEMPLATE_SET_ID: u16 = 3;
30
31/// Simpler template that is used to decode data records
32#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
33#[cfg_attr(feature = "fuzz", derive(arbitrary::Arbitrary))]
34pub struct DecodingTemplate {
35    pub scope_fields_specs: Box<[FieldSpecifier]>,
36    pub fields_specs: Box<[FieldSpecifier]>,
37
38    /// Number of Data Records processed using this template
39    pub processed_count: u64,
40}
41
42impl DecodingTemplate {
43    pub const fn new(
44        scope_fields_specs: Box<[FieldSpecifier]>,
45        fields_specs: Box<[FieldSpecifier]>,
46    ) -> Self {
47        Self {
48            scope_fields_specs,
49            fields_specs,
50            processed_count: 0,
51        }
52    }
53
54    /// Increment Data Record count by one
55    pub const fn increment_processed_count(&mut self) {
56        self.processed_count = self.processed_count.wrapping_add(1);
57    }
58
59    /// Get the current processed Data Record count
60    pub const fn processed_count(&self) -> u64 {
61        self.processed_count
62    }
63
64    /// Get the current processed Data Record count and reset the value to zero
65    pub const fn reset_processed_count(&mut self) -> u64 {
66        let prev = self.processed_count;
67        self.processed_count = 0;
68        prev
69    }
70}
71
72/// Cache to store templates needed for decoding data packets
73pub type TemplatesMap = HashMap<u16, DecodingTemplate>;
74
75/// IP Flow Information Export (IPFIX) v10 Packet.
76///
77/// ```text
78///  +--------+--------------------------------------------------------+
79///  |        | +----------+ +---------+     +-----------+ +---------+ |
80///  |Message | | Template | | Data    |     | Options   | | Data    | |
81///  | Header | | Set      | | Set     | ... | Template  | | Set     | |
82///  |        | |          | |         |     | Set       | |         | |
83///  |        | +----------+ +---------+     +-----------+ +---------+ |
84///  +--------+--------------------------------------------------------+
85/// ```
86/// ```text
87/// 0                   1                   2                   3
88///  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
89/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
90/// |       Version Number          |            Length             |
91/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
92/// |                           Export Time                         |
93/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
94/// |                       Sequence Number                         |
95/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
96/// |                    Observation Domain ID                      |
97/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
98/// ```
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
100#[cfg_attr(feature = "fuzz", derive(arbitrary::Arbitrary))]
101pub struct IpfixPacket {
102    version: u16,
103    #[cfg_attr(feature = "fuzz", arbitrary(with = crate::arbitrary_datetime))]
104    export_time: DateTime<Utc>,
105    sequence_number: u32,
106    observation_domain_id: u32,
107    sets: Box<[Set]>,
108}
109
110impl IpfixPacket {
111    pub const fn new(
112        export_time: DateTime<Utc>,
113        sequence_number: u32,
114        observation_domain_id: u32,
115        sets: Box<[Set]>,
116    ) -> Self {
117        Self {
118            version: IPFIX_VERSION,
119            export_time,
120            sequence_number,
121            observation_domain_id,
122            sets,
123        }
124    }
125
126    /// IPFIX Protocol version
127    pub const fn version(&self) -> u16 {
128        self.version
129    }
130
131    /// Time at which the IPFIX Message Header leaves the Exporter.
132    ///
133    /// Note: The exporter is sending this value at a seconds granularity as
134    /// UNIX epoch.
135    pub const fn export_time(&self) -> DateTime<Utc> {
136        self.export_time
137    }
138
139    /// Incremental sequence counter modulo 2^32 of all IPFIX Data Records sent
140    /// in the current stream from the current Observation Domain by the
141    /// Exporting Process.
142    ///
143    /// Note: Template and Options Template Records do not increase the Sequence
144    /// Number.
145    pub const fn sequence_number(&self) -> u32 {
146        self.sequence_number
147    }
148
149    /// A 32-bit identifier of the Observation Domain that is locally unique to
150    /// the Exporting Process.
151    pub const fn observation_domain_id(&self) -> u32 {
152        self.observation_domain_id
153    }
154
155    /// The IPFIX payload is a vector of [Set].
156    pub const fn sets(&self) -> &[Set] {
157        &self.sets
158    }
159
160    /// Consume the packet and return the owned sets
161    pub fn into_sets(self) -> Box<[Set]> {
162        self.sets
163    }
164
165    /// Add fields to all data records in the packet
166    pub fn with_fields_added(self, add_fields: &[Field]) -> Self {
167        let sets = self
168            .sets
169            .into_vec()
170            .into_iter()
171            .map(|set| set.with_fields_added(add_fields))
172            .collect::<Box<[_]>>();
173
174        Self {
175            version: self.version,
176            export_time: self.export_time,
177            sequence_number: self.sequence_number,
178            observation_domain_id: self.observation_domain_id,
179            sets,
180        }
181    }
182
183    /// Add scope fields to all data records in the packet
184    pub fn with_scope_fields_added(self, add_scope_fields: &[Field]) -> Self {
185        let sets = self
186            .sets
187            .into_vec()
188            .into_iter()
189            .map(|set| set.with_scope_fields_added(add_scope_fields))
190            .collect::<Box<[_]>>();
191
192        Self {
193            version: self.version,
194            export_time: self.export_time,
195            sequence_number: self.sequence_number,
196            observation_domain_id: self.observation_domain_id,
197            sets,
198        }
199    }
200}
201
202/// Every Set contains a common header. The Sets can be any of these three
203/// possible types: Data Set, Template Set, or Options Template Set.
204///
205/// ```text
206///  0                   1                   2                   3
207///  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
208/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
209/// |          Set ID               |          Length               |
210/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
211/// ```
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
213#[cfg_attr(feature = "fuzz", derive(arbitrary::Arbitrary))]
214pub enum Set {
215    Template(Box<[TemplateRecord]>),
216    OptionsTemplate(Box<[OptionsTemplateRecord]>),
217    Data {
218        id: DataSetId,
219        records: Box<[DataRecord]>,
220    },
221}
222
223impl Set {
224    pub const fn id(&self) -> u16 {
225        match self {
226            Self::Template(_) => IPFIX_TEMPLATE_SET_ID,
227            Self::OptionsTemplate(_) => IPFIX_OPTIONS_TEMPLATE_SET_ID,
228            Self::Data { id, records: _ } => id.0,
229        }
230    }
231
232    /// Add fields to all data records in this set
233    pub fn with_fields_added(self, add_fields: &[Field]) -> Self {
234        match self {
235            Set::Data { id, records } => {
236                let modified_records = records
237                    .into_vec()
238                    .into_iter()
239                    .map(|record| record.with_fields_added(add_fields))
240                    .collect::<Box<[_]>>();
241
242                Set::Data {
243                    id,
244                    records: modified_records,
245                }
246            }
247            other => other,
248        }
249    }
250
251    /// Add scope fields to all data records in this set
252    pub fn with_scope_fields_added(self, add_scope_fields: &[Field]) -> Self {
253        match self {
254            Set::Data { id, records } => {
255                let modified_records = records
256                    .into_vec()
257                    .into_iter()
258                    .map(|record| record.with_scope_fields_added(add_scope_fields))
259                    .collect::<Box<[_]>>();
260
261                Set::Data {
262                    id,
263                    records: modified_records,
264                }
265            }
266            other => other,
267        }
268    }
269}
270
271/// Template Records allow the Collecting Process to process
272/// IPFIX Messages without necessarily knowing the interpretation of all
273/// Data Records. A Template Record contains any combination of IANA-
274/// assigned and/or enterprise-specific Information Element identifiers.
275///
276/// ```text
277/// +--------------------------------------------------+
278/// | Template Record Header                           |
279/// +--------------------------------------------------+
280/// | Field Specifier                                  |
281/// +--------------------------------------------------+
282/// | Field Specifier                                  |
283/// +--------------------------------------------------+
284///  ...
285/// +--------------------------------------------------+
286/// | Field Specifier                                  |
287/// +--------------------------------------------------+
288/// ```
289///
290/// The format of the Template Record Header is
291/// ```text
292///  0                   1                   2                   3
293///  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
294/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
295/// |      Template ID (> 255)      |         Field Count           |
296/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
297/// ```
298#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
299#[cfg_attr(feature = "fuzz", derive(arbitrary::Arbitrary))]
300pub struct TemplateRecord {
301    id: u16,
302    field_specifiers: Box<[FieldSpecifier]>,
303}
304
305impl TemplateRecord {
306    pub const fn new(id: u16, field_specifiers: Box<[FieldSpecifier]>) -> Self {
307        Self {
308            id,
309            field_specifiers,
310        }
311    }
312
313    /// Each Template Record is given a unique Template ID in the range 256 to
314    /// 65535. TODO (AH): do we need to check for template IDs < 256,
315    /// see [RFC 7011](https://www.rfc-editor.org/rfc/rfc7011#section-3.4.1)
316    pub const fn id(&self) -> u16 {
317        self.id
318    }
319
320    /// List of [`FieldSpecifier`] defined in the template.
321    pub const fn field_specifiers(&self) -> &[FieldSpecifier] {
322        &self.field_specifiers
323    }
324}
325
326/// An Options Template Record contains any combination of IANA-assigned
327/// and/or enterprise-specific Information Element identifiers.
328/// ```text
329/// +--------------------------------------------------+
330/// | Options Template Record Header                   |
331/// +--------------------------------------------------+
332/// | Field Specifier                                  |
333/// +--------------------------------------------------+
334/// | Field Specifier                                  |
335/// +--------------------------------------------------+
336///  ...
337/// +--------------------------------------------------+
338/// | Field Specifier                                  |
339/// +--------------------------------------------------+
340/// ```
341///
342///
343/// The format of the Options Template Record Header:
344/// ```text
345///  0                   1                   2                   3
346///  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
347/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
348/// |         Template ID (> 255)   |         Field Count           |
349/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
350/// |      Scope Field Count        |
351/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
352/// ```
353///
354/// An The example in Figure shows an Options Template Set with mixed
355/// IANA-assigned and enterprise-specific Information Elements.
356/// ```text
357///  0                   1                   2                   3
358///  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
359/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
360/// |          Set ID = 3           |          Length               |
361/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
362/// |         Template ID = 258     |         Field Count = N + M   |
363/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
364/// |     Scope Field Count = N     |0|  Scope 1 Infor. Element id. |
365/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
366/// |     Scope 1 Field Length      |0|  Scope 2 Infor. Element id. |
367/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
368/// |     Scope 2 Field Length      |             ...               |
369/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
370/// |            ...                |1|  Scope N Infor. Element id. |
371/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
372/// |     Scope N Field Length      |   Scope N Enterprise Number  ...
373/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
374/// ..  Scope N Enterprise Number   |1| Option 1 Infor. Element id. |
375/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
376/// |    Option 1 Field Length      |  Option 1 Enterprise Number  ...
377/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
378/// .. Option 1 Enterprise Number   |              ...              |
379/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
380/// |             ...               |0| Option M Infor. Element id. |
381/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
382/// |     Option M Field Length     |      Padding (optional)       |
383/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
384/// ```
385#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
386#[cfg_attr(feature = "fuzz", derive(arbitrary::Arbitrary))]
387pub struct OptionsTemplateRecord {
388    id: u16,
389    scope_field_specifiers: Box<[FieldSpecifier]>,
390    field_specifiers: Box<[FieldSpecifier]>,
391}
392
393impl OptionsTemplateRecord {
394    pub const fn new(
395        id: u16,
396        scope_field_specifiers: Box<[FieldSpecifier]>,
397        field_specifiers: Box<[FieldSpecifier]>,
398    ) -> Self {
399        Self {
400            id,
401            scope_field_specifiers,
402            field_specifiers,
403        }
404    }
405
406    pub const fn id(&self) -> u16 {
407        self.id
408    }
409
410    pub const fn scope_field_specifiers(&self) -> &[FieldSpecifier] {
411        &self.scope_field_specifiers
412    }
413
414    pub const fn field_specifiers(&self) -> &[FieldSpecifier] {
415        &self.field_specifiers
416    }
417}
418
419#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
420#[cfg_attr(feature = "fuzz", derive(arbitrary::Arbitrary))]
421pub struct DataRecord {
422    scope_fields: Box<[Field]>,
423    fields: Box<[Field]>,
424}
425
426impl DataRecord {
427    pub const fn new(scope_fields: Box<[Field]>, fields: Box<[Field]>) -> Self {
428        Self {
429            scope_fields,
430            fields,
431        }
432    }
433
434    pub const fn scope_fields(&self) -> &[Field] {
435        &self.scope_fields
436    }
437
438    pub const fn fields(&self) -> &[Field] {
439        &self.fields
440    }
441
442    /// Deconstruct the data record into its parts
443    pub fn into_parts(self) -> (Box<[Field]>, Box<[Field]>) {
444        (self.scope_fields, self.fields)
445    }
446
447    /// Add multiple fields
448    pub fn with_fields_added(self, add_fields: &[Field]) -> Self {
449        let mut fields = self.fields.into_vec();
450        fields.extend_from_slice(add_fields);
451
452        Self {
453            scope_fields: self.scope_fields,
454            fields: fields.into_boxed_slice(),
455        }
456    }
457
458    /// Add multiple scope fields
459    pub fn with_scope_fields_added(self, add_scope_fields: &[Field]) -> Self {
460        let mut scope_fields = self.scope_fields.into_vec();
461        scope_fields.extend_from_slice(add_scope_fields);
462
463        Self {
464            scope_fields: scope_fields.into_boxed_slice(),
465            fields: self.fields,
466        }
467    }
468}
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473    use crate::ie;
474    use chrono::TimeZone;
475
476    #[test]
477    fn test_ipfix_packet() {
478        let export_time = Utc.with_ymd_and_hms(2024, 6, 20, 14, 0, 0).unwrap();
479        let sequence_number = 0;
480        let observation_domain_id = 0;
481        let sets = [
482            Set::Template(Box::new([TemplateRecord::new(
483                256,
484                Box::new([
485                    FieldSpecifier::new(ie::IE::octetDeltaCount, 4).unwrap(),
486                    FieldSpecifier::new(ie::IE::tcpDestinationPort, 2).unwrap(),
487                ]),
488            )])),
489            Set::OptionsTemplate(Box::new([OptionsTemplateRecord::new(
490                258,
491                Box::new([FieldSpecifier::new(ie::IE::egressVRFID, 4).unwrap()]),
492                Box::new([FieldSpecifier::new(ie::IE::interfaceName, 255).unwrap()]),
493            )])),
494            Set::Data {
495                id: DataSetId::new(256).unwrap(),
496                records: Box::new([DataRecord::new(
497                    Box::new([Field::octetDeltaCount(189)]),
498                    Box::new([Field::tcpDestinationPort(8080)]),
499                )]),
500            },
501        ];
502        let packet = IpfixPacket::new(
503            export_time,
504            sequence_number,
505            observation_domain_id,
506            Box::new(sets.clone()),
507        );
508        assert_eq!(packet.version(), IPFIX_VERSION);
509        assert_eq!(packet.export_time(), export_time);
510        assert_eq!(packet.sequence_number(), sequence_number);
511        assert_eq!(packet.observation_domain_id(), observation_domain_id);
512        assert_eq!(packet.sets(), &sets);
513    }
514
515    #[test]
516    fn test_template_record() {
517        let template = TemplateRecord::new(
518            256,
519            Box::new([
520                FieldSpecifier::new(ie::IE::octetDeltaCount, 4).unwrap(),
521                FieldSpecifier::new(ie::IE::tcpDestinationPort, 2).unwrap(),
522            ]),
523        );
524        assert_eq!(template.id(), 256);
525        assert_eq!(
526            template.field_specifiers(),
527            &[
528                FieldSpecifier::new(ie::IE::octetDeltaCount, 4).unwrap(),
529                FieldSpecifier::new(ie::IE::tcpDestinationPort, 2).unwrap(),
530            ]
531        );
532    }
533
534    #[test]
535    fn test_options_template_record() {
536        let template = OptionsTemplateRecord::new(
537            258,
538            Box::new([FieldSpecifier::new(ie::IE::egressVRFID, 4).unwrap()]),
539            Box::new([FieldSpecifier::new(ie::IE::interfaceName, 255).unwrap()]),
540        );
541        assert_eq!(template.id(), 258);
542        assert_eq!(
543            template.scope_field_specifiers(),
544            &[FieldSpecifier::new(ie::IE::egressVRFID, 4).unwrap()]
545        );
546        assert_eq!(
547            template.field_specifiers(),
548            &[FieldSpecifier::new(ie::IE::interfaceName, 255).unwrap()]
549        );
550    }
551
552    #[test]
553    fn test_data_record() {
554        let record = DataRecord::new(
555            Box::new([Field::octetDeltaCount(189)]),
556            Box::new([Field::tcpDestinationPort(8080)]),
557        );
558        assert_eq!(record.scope_fields(), &[Field::octetDeltaCount(189)]);
559        assert_eq!(record.fields(), &[Field::tcpDestinationPort(8080)]);
560    }
561
562    #[test]
563    fn test_set() {
564        let template = TemplateRecord::new(
565            256,
566            Box::new([
567                FieldSpecifier::new(ie::IE::octetDeltaCount, 4).unwrap(),
568                FieldSpecifier::new(ie::IE::tcpDestinationPort, 2).unwrap(),
569            ]),
570        );
571        let options_template = OptionsTemplateRecord::new(
572            258,
573            Box::new([FieldSpecifier::new(ie::IE::egressVRFID, 4).unwrap()]),
574            Box::new([FieldSpecifier::new(ie::IE::interfaceName, 255).unwrap()]),
575        );
576        let data = DataRecord::new(
577            Box::new([Field::octetDeltaCount(189)]),
578            Box::new([Field::tcpDestinationPort(8080)]),
579        );
580        let sets = [
581            Set::Template(Box::new([template.clone()])),
582            Set::OptionsTemplate(Box::new([options_template.clone()])),
583            Set::Data {
584                id: DataSetId::new(256).unwrap(),
585                records: Box::new([data.clone()]),
586            },
587        ];
588        assert_eq!(sets[0].id(), IPFIX_TEMPLATE_SET_ID);
589        assert_eq!(sets[1].id(), IPFIX_OPTIONS_TEMPLATE_SET_ID);
590        assert_eq!(sets[2].id(), 256);
591    }
592
593    #[test]
594    fn test_data_record_with_fields_added() {
595        let original_record = DataRecord::new(
596            Box::new([Field::octetDeltaCount(189)]),
597            Box::new([Field::tcpDestinationPort(8080)]),
598        );
599
600        let modified_record = original_record.with_fields_added(&[
601            Field::packetDeltaCount(10),
602            Field::sourceIPv4Address([192, 168, 1, 1].into()),
603        ]);
604
605        let expected_record = DataRecord::new(
606            Box::new([Field::octetDeltaCount(189)]),
607            Box::new([
608                Field::tcpDestinationPort(8080),
609                Field::packetDeltaCount(10),
610                Field::sourceIPv4Address([192, 168, 1, 1].into()),
611            ]),
612        );
613
614        assert_eq!(modified_record, expected_record);
615    }
616
617    #[test]
618    fn test_data_record_with_scope_fields_added() {
619        let original_record = DataRecord::new(
620            Box::new([Field::octetDeltaCount(189)]),
621            Box::new([Field::tcpDestinationPort(8080)]),
622        );
623
624        let modified_record = original_record
625            .with_scope_fields_added(&[Field::egressVRFID(42), Field::ingressVRFID(24)]);
626
627        let expected_record = DataRecord::new(
628            Box::new([
629                Field::octetDeltaCount(189),
630                Field::egressVRFID(42),
631                Field::ingressVRFID(24),
632            ]),
633            Box::new([Field::tcpDestinationPort(8080)]),
634        );
635
636        assert_eq!(modified_record, expected_record);
637    }
638
639    #[test]
640    fn test_set_with_fields_added() {
641        let data_record1 = DataRecord::new(
642            Box::new([Field::octetDeltaCount(100)]),
643            Box::new([Field::tcpDestinationPort(80)]),
644        );
645        let data_record2 = DataRecord::new(
646            Box::new([Field::octetDeltaCount(200)]),
647            Box::new([Field::tcpDestinationPort(443)]),
648        );
649
650        let data_set = Set::Data {
651            id: DataSetId::new(256).unwrap(),
652            records: Box::new([data_record1, data_record2]),
653        };
654
655        let modified_set = data_set.with_fields_added(&[Field::packetDeltaCount(5)]);
656
657        let expected_set = Set::Data {
658            id: DataSetId::new(256).unwrap(),
659            records: Box::new([
660                DataRecord::new(
661                    Box::new([Field::octetDeltaCount(100)]),
662                    Box::new([Field::tcpDestinationPort(80), Field::packetDeltaCount(5)]),
663                ),
664                DataRecord::new(
665                    Box::new([Field::octetDeltaCount(200)]),
666                    Box::new([Field::tcpDestinationPort(443), Field::packetDeltaCount(5)]),
667                ),
668            ]),
669        };
670
671        assert_eq!(modified_set, expected_set);
672    }
673
674    #[test]
675    fn test_set_with_scope_fields_added() {
676        let data_record1 = DataRecord::new(
677            Box::new([Field::octetDeltaCount(100)]),
678            Box::new([Field::tcpDestinationPort(80)]),
679        );
680        let data_record2 = DataRecord::new(
681            Box::new([Field::octetDeltaCount(200)]),
682            Box::new([Field::tcpDestinationPort(443)]),
683        );
684
685        let data_set = Set::Data {
686            id: DataSetId::new(256).unwrap(),
687            records: Box::new([data_record1, data_record2]),
688        };
689
690        let modified_set = data_set.with_scope_fields_added(&[Field::egressVRFID(42)]);
691
692        let expected_set = Set::Data {
693            id: DataSetId::new(256).unwrap(),
694            records: Box::new([
695                DataRecord::new(
696                    Box::new([Field::octetDeltaCount(100), Field::egressVRFID(42)]),
697                    Box::new([Field::tcpDestinationPort(80)]),
698                ),
699                DataRecord::new(
700                    Box::new([Field::octetDeltaCount(200), Field::egressVRFID(42)]),
701                    Box::new([Field::tcpDestinationPort(443)]),
702                ),
703            ]),
704        };
705
706        assert_eq!(modified_set, expected_set);
707    }
708
709    #[test]
710    fn test_ipfix_packet_with_fields_added() {
711        let export_time = Utc.with_ymd_and_hms(2024, 6, 20, 14, 0, 0).unwrap();
712
713        let original_packet = IpfixPacket::new(
714            export_time,
715            0,
716            0,
717            Box::new([
718                Set::Data {
719                    id: DataSetId::new(256).unwrap(),
720                    records: Box::new([DataRecord::new(
721                        Box::new([Field::octetDeltaCount(100)]),
722                        Box::new([Field::tcpDestinationPort(80)]),
723                    )]),
724                },
725                Set::Template(Box::new([TemplateRecord::new(
726                    256,
727                    Box::new([
728                        FieldSpecifier::new(ie::IE::octetDeltaCount, 4).unwrap(),
729                        FieldSpecifier::new(ie::IE::tcpDestinationPort, 2).unwrap(),
730                    ]),
731                )])),
732            ]),
733        );
734
735        let modified_packet = original_packet.with_fields_added(&[Field::packetDeltaCount(5)]);
736
737        let expected_packet = IpfixPacket::new(
738            export_time,
739            0,
740            0,
741            Box::new([
742                Set::Data {
743                    id: DataSetId::new(256).unwrap(),
744                    records: Box::new([DataRecord::new(
745                        Box::new([Field::octetDeltaCount(100)]),
746                        Box::new([Field::tcpDestinationPort(80), Field::packetDeltaCount(5)]),
747                    )]),
748                },
749                Set::Template(Box::new([TemplateRecord::new(
750                    256,
751                    Box::new([
752                        FieldSpecifier::new(ie::IE::octetDeltaCount, 4).unwrap(),
753                        FieldSpecifier::new(ie::IE::tcpDestinationPort, 2).unwrap(),
754                    ]),
755                )])),
756            ]),
757        );
758
759        assert_eq!(modified_packet, expected_packet);
760    }
761
762    #[test]
763    fn test_ipfix_packet_with_scope_fields_added() {
764        let export_time = Utc.with_ymd_and_hms(2024, 6, 20, 14, 0, 0).unwrap();
765        let data_record = DataRecord::new(
766            Box::new([Field::octetDeltaCount(100)]),
767            Box::new([Field::tcpDestinationPort(80)]),
768        );
769
770        let original_packet = IpfixPacket::new(
771            export_time,
772            0,
773            0,
774            Box::new([Set::Data {
775                id: DataSetId::new(256).unwrap(),
776                records: Box::new([data_record]),
777            }]),
778        );
779
780        let modified_packet = original_packet.with_scope_fields_added(&[Field::egressVRFID(42)]);
781
782        let expected_packet = IpfixPacket::new(
783            export_time,
784            0,
785            0,
786            Box::new([Set::Data {
787                id: DataSetId::new(256).unwrap(),
788                records: Box::new([DataRecord::new(
789                    Box::new([Field::octetDeltaCount(100), Field::egressVRFID(42)]),
790                    Box::new([Field::tcpDestinationPort(80)]),
791                )]),
792            }]),
793        );
794
795        assert_eq!(modified_packet, expected_packet);
796    }
797}