Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 11 additions & 17 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ license = "MIT OR Apache-2.0"
name = "netflow_generator"
repository = "https://github.com/mikemiles-dev/netflow_generator/"
readme = "README.md"
version = "0.2.3"
version = "0.2.4"

[dependencies]
netflow_parser = "0.7.0"
netflow_parser = "0.8.0"
serde_yaml = "0.9"
serde = { version = "1.0", features = ["derive"] }
clap = { version = "4.5", features = ["derive", "cargo"] }
Expand Down
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,65 @@ A flexible NetFlow packet generator written in Rust that supports NetFlow v5, v7
- **Configurable Destination**: Override destination IP and port via CLI
- **Validation**: Automatic validation of configuration files

## Network Behavior

This generator mimics real NetFlow exporter behavior by using a **fixed source port (default: 2056)** for UDP transmissions. This is critical for proper operation with NetFlow collectors that implement RFC-compliant scoping:

- **Real routers** use consistent source ports (not ephemeral ports) for NetFlow exports
- **RFC 7011 (IPFIX)** and **RFC 3954 (NetFlow v9)** specify that collectors should key template caches on `(source_address, observation_domain_id)` or `(source_address, source_id)`
- Using ephemeral ports would cause each packet to appear as a new source, leading to template collisions and parsing errors
- The default source port of **2056** avoids conflicts with NetFlow collectors typically running on port 2055
- You can customize the source port using the `--source-port` option

This ensures compatibility with collectors using `AutoScopedParser`, `RouterScopedParser`, or similar RFC-compliant implementations.

### Sequence Number Tracking (NetFlow v9 and IPFIX)

In continuous mode, the generator properly tracks sequence numbers across iterations to mimic real router behavior:

- **NetFlow v9**: Sequence numbers are tracked per `source_id` (default: 1)
- **IPFIX**: Sequence numbers are tracked per `observation_domain_id` (default: 1)
- Sequence numbers increment with each packet sent from the same exporter
- This prevents sequence number collisions that parsers would detect as errors
- Each exporter (identified by `source IP:port + source_id/observation_domain_id`) maintains its own sequence counter

**Example behavior in continuous mode:**
```
Iteration 1: V9 seq=0, IPFIX seq=0
Iteration 2: V9 seq=2, IPFIX seq=2 (assuming 2 packets per iteration)
Iteration 3: V9 seq=4, IPFIX seq=4
...
```

If you configure multiple exporters with different `source_id` or `observation_domain_id` values, each will maintain independent sequence counters.

### Testing Locally

The generator uses a fixed source port (default: **2056**) to mimic real router behavior. When testing locally:

```bash
# Terminal 1: Start a listener on the standard NetFlow port
nc -ul 127.0.0.1 2055

# Terminal 2: Send to that port (source will be 2056, dest will be 2055)
netflow_generator --verbose --once

# Or using cargo run
cargo run -- --verbose --once
```

**If you need to use a different source port** (e.g., if port 2056 is in use):

```bash
# Use a custom source port
netflow_generator --source-port 9996 --dest 127.0.0.1:2055 --verbose --once

# Or test with both custom source and destination
netflow_generator --source-port 9996 --dest 127.0.0.1:9995 --verbose --once
```

Note: Source and destination ports must be different when testing on the same machine.

## Installation

### Download Pre-built Binaries
Expand Down Expand Up @@ -221,6 +280,7 @@ Options:
-o, --output <FILE> Save packets to pcap file instead of sending via UDP
-v, --verbose Enable verbose output
-t, --threads <NUMBER> Number of threads for parallel packet generation (default: 4)
-s, --source-port <PORT> Source port for UDP transmission (default: 2056)
-i, --interval [SECONDS] Send flows every N seconds (default: 2)
Continuous mode is the default behavior
--once Send flows once and exit (disables continuous mode)
Expand Down Expand Up @@ -623,7 +683,7 @@ The project is organized into several modules:

## Dependencies

- `netflow_parser` (0.7.0) - NetFlow packet structures
- `netflow_parser` (0.8.0) - NetFlow packet structures
- `serde_yaml` (0.9) - YAML parsing
- `serde` (1.0) - Serialization framework
- `clap` (4.5) - CLI argument parsing
Expand Down
20 changes: 20 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
# 0.2.5
* **Fix**: NetFlow v9 and IPFIX sequence numbers now properly increment across iterations in continuous mode
- Previous behavior reset sequence numbers to 0 on each iteration, causing parsers to detect collisions
- Sequence numbers are now tracked per exporter (source_id for V9, observation_domain_id for IPFIX)
- Each exporter maintains independent sequence counters that increment across iterations
- Prevents "sequence number collision" errors in RFC-compliant parsers
- Single-shot mode (--once) behavior unchanged - still starts from 0 each run
* **Fix**: Removed parallel processing (rayon) for V9/IPFIX flows to maintain proper sequence number ordering
- V5 and V7 flows are processed sequentially for consistency
- Sequential processing ensures correct sequence number tracking
* **Fix**: UDP socket now uses fixed source port 2056 instead of ephemeral ports
- Matches real NetFlow exporter behavior where routers use consistent source ports
- Fixes template collision issues with RFC-compliant collectors (AutoScopedParser, RouterScopedParser)
- RFC 7011 (IPFIX) and RFC 3954 (NetFlow v9) specify scoping by (source_address, observation_domain_id/source_id)
- Previous ephemeral port behavior caused each packet to be treated as a different source
- Port 2056 avoids conflicts with NetFlow collectors typically running on port 2055
* **Dependency**: Updated netflow_parser from 0.7.0 to 0.8.0
* **Documentation**: Added "Sequence Number Tracking" section to README explaining behavior in continuous mode
* **Documentation**: Added "Network Behavior" section to README explaining fixed source port rationale

# 0.2.3
* Bump release for cargo publish and README updates.

Expand Down
9 changes: 9 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,13 @@ pub struct Cli {
/// many flows, but will use more CPU and memory.
#[arg(short = 't', long, default_value = "4")]
pub threads: usize,

/// Source port for UDP transmission (default: 2056)
///
/// Real NetFlow exporters use a consistent source port to ensure
/// proper template scoping in collectors. The default of 2056
/// avoids conflicts with NetFlow collectors typically running on 2055.
/// Must be different from the destination port when testing locally.
#[arg(short = 's', long, value_name = "PORT", default_value = "2056")]
pub source_port: u16,
}
45 changes: 36 additions & 9 deletions src/generator/ipfix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,24 @@ use std::time::{SystemTime, UNIX_EPOCH};

/// Build IPFIX packets from configuration
/// Generates proper template and data flowsets
pub fn build_ipfix_packets(config: IPFixConfig) -> Result<Vec<Vec<u8>>> {
///
/// # Arguments
/// * `config` - IPFIX configuration
/// * `override_sequence_number` - Optional sequence number to use (overrides config value)
/// * `send_templates` - Whether to include template packets (for periodic refresh)
///
/// # Returns
/// * `(packets, next_sequence_number)` - Generated packets and the next sequence number to use
pub fn build_ipfix_packets(
config: IPFixConfig,
override_sequence_number: Option<u32>,
send_templates: bool,
) -> Result<(Vec<Vec<u8>>, u32)> {
let mut packets = Vec::new();

// Get header values
let (export_time, mut sequence_number, observation_domain_id) = get_header_values(&config)?;
let (export_time, mut sequence_number, observation_domain_id) =
get_header_values(&config, override_sequence_number)?;

// Separate templates and data flowsets
let mut templates = Vec::new();
Expand All @@ -34,16 +47,17 @@ pub fn build_ipfix_packets(config: IPFixConfig) -> Result<Vec<Vec<u8>>> {
}
}

// Generate template packet if we have templates
if !templates.is_empty() {
// Generate template packet if we have templates AND send_templates is true
// Per RFC 7011: Template packets (Template Sets) do NOT increment the sequence number
if !templates.is_empty() && send_templates {
let template_packet = build_template_packet(
export_time,
sequence_number,
observation_domain_id,
&templates,
)?;
packets.push(template_packet);
sequence_number += 1;
// No sequence increment for template packets
}

// Generate data packets
Expand All @@ -69,7 +83,14 @@ pub fn build_ipfix_packets(config: IPFixConfig) -> Result<Vec<Vec<u8>>> {
&records,
)?;
packets.push(data_packet);
sequence_number += 1;

// Per RFC 7011: Sequence number increments by the number of data records
let num_records = u32::try_from(records.len()).map_err(|_| {
NetflowError::Generation("Too many records (max 4294967295)".to_string())
})?;
sequence_number = sequence_number
.checked_add(num_records)
.ok_or_else(|| NetflowError::Generation("Sequence number overflow".to_string()))?;
}

if packets.is_empty() {
Expand All @@ -78,10 +99,13 @@ pub fn build_ipfix_packets(config: IPFixConfig) -> Result<Vec<Vec<u8>>> {
));
}

Ok(packets)
Ok((packets, sequence_number))
}

fn get_header_values(config: &IPFixConfig) -> Result<(u32, u32, u32)> {
fn get_header_values(
config: &IPFixConfig,
override_sequence_number: Option<u32>,
) -> Result<(u32, u32, u32)> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| NetflowError::Generation(format!("Failed to get system time: {}", e)))?;
Expand All @@ -93,7 +117,10 @@ fn get_header_values(config: &IPFixConfig) -> Result<(u32, u32, u32)> {
u32::try_from(now.as_secs()).unwrap_or(u32::MAX)
};

let sequence_number = if let Some(ref h) = config.header {
// Use override if provided, otherwise use config value, otherwise default to 0
let sequence_number = if let Some(override_seq) = override_sequence_number {
override_seq
} else if let Some(ref h) = config.header {
h.sequence_number.unwrap_or(0)
} else {
0
Expand Down
48 changes: 41 additions & 7 deletions src/generator/samples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,16 @@ pub fn sample_v7_config() -> V7Config {
/// Generate sample V9 configuration
/// Represents HTTP traffic: 192.168.10.5:48921 -> 93.184.216.34:80
pub fn sample_v9_config() -> V9Config {
use crate::config::schema::V9Header;
use serde_yaml::Value;

V9Config {
header: None, // Use defaults
header: Some(V9Header {
sys_up_time: Some(360000),
unix_secs: None,
sequence_number: None,
source_id: Some(1), // V9 uses source_id=1
}),
flowsets: vec![
// Template definition
V9FlowSet::Template {
Expand Down Expand Up @@ -149,10 +155,15 @@ pub fn sample_v9_config() -> V9Config {
/// Generate sample IPFIX configuration
/// Represents SSH session: 172.20.0.100:50122 -> 198.51.100.10:22
pub fn sample_ipfix_config() -> IPFixConfig {
use crate::config::schema::IPFixHeader;
use serde_yaml::Value;

IPFixConfig {
header: None, // Use defaults
header: Some(IPFixHeader {
export_time: None,
sequence_number: None,
observation_domain_id: Some(2), // IPFIX uses observation_domain_id=2 to avoid collision with V9
}),
flowsets: vec![
// Template definition
IPFixFlowSet::Template {
Expand Down Expand Up @@ -228,13 +239,25 @@ pub fn sample_ipfix_config() -> IPFixConfig {
}
}

/// Generate all sample packets
pub fn generate_all_samples() -> Result<Vec<Vec<u8>>> {
/// Generate all sample packets with sequence number tracking
///
/// # Arguments
/// * `v9_seq` - Current V9 sequence number (will be incremented)
/// * `ipfix_seq` - Current IPFIX sequence number (will be incremented)
/// * `send_templates` - Whether to include template packets (for periodic refresh)
///
/// # Returns
/// * `(packets, next_v9_seq, next_ipfix_seq)` - Generated packets and updated sequence numbers
pub fn generate_all_samples_with_seq(
v9_seq: u32,
ipfix_seq: u32,
send_templates: bool,
) -> Result<(Vec<Vec<u8>>, u32, u32)> {
let mut packets = Vec::new();

// V5 sample
let v5_config = sample_v5_config();
let v5_packet = crate::generator::v5::build_v5_packet(v5_config)?;
let v5_packet = crate::generator::v5::build_v5_packet(v5_config, None)?;
packets.push(v5_packet);

// V7 sample
Expand All @@ -244,13 +267,24 @@ pub fn generate_all_samples() -> Result<Vec<Vec<u8>>> {

// V9 sample (may return multiple packets)
let v9_config = sample_v9_config();
let v9_packets = crate::generator::v9::build_v9_packets(v9_config)?;
let (v9_packets, next_v9_seq) =
crate::generator::v9::build_v9_packets(v9_config, Some(v9_seq), send_templates)?;
packets.extend(v9_packets);

// IPFIX sample (may return multiple packets)
let ipfix_config = sample_ipfix_config();
let ipfix_packets = crate::generator::ipfix::build_ipfix_packets(ipfix_config)?;
let (ipfix_packets, next_ipfix_seq) = crate::generator::ipfix::build_ipfix_packets(
ipfix_config,
Some(ipfix_seq),
send_templates,
)?;
packets.extend(ipfix_packets);

Ok((packets, next_v9_seq, next_ipfix_seq))
}

/// Generate all sample packets (legacy function for backwards compatibility)
pub fn generate_all_samples() -> Result<Vec<Vec<u8>>> {
let (packets, _, _) = generate_all_samples_with_seq(0, 0, true)?;
Ok(packets)
}
Loading