Skip to main content

Overview

The Header Normalization transform modifies IPv4 and TCP headers to prevent OS fingerprinting and header-based traffic analysis. It can normalize TTL values, TCP window sizes, and randomize IP identification fields.

Structure

pub struct HeaderNormalizationTransform {
    params: HeaderParams,
}

Configuration

normalize_ttl
bool
default:"false"
When enabled, sets all packets’ TTL (Time To Live) field to ttl_value.
ttl_value
u8
default:"64"
The TTL value to set when normalize_ttl is enabled. Common values:
  • 64: Linux/Unix default
  • 128: Windows default
  • 255: Maximum value
normalize_window
bool
default:"false"
When enabled, sets the TCP window size to 65535 (maximum value).
randomize_ip_id
bool
default:"true"
When enabled, randomizes the IP identification field to prevent tracking.

Methods

new

pub fn new(params: &HeaderParams) -> Self
Creates a new Header Normalization transform with the specified parameters.

normalize_ipv4

fn normalize_ipv4(&self, data: &mut BytesMut, seed: u64)
Normalizes IPv4 header fields:
  • Sets TTL to ttl_value if normalize_ttl is enabled
  • Randomizes IP ID if randomize_ip_id is enabled
Note: Only processes IPv4 packets (version field = 4).

normalize_tcp

fn normalize_tcp(&self, data: &mut BytesMut)
Normalizes TCP header fields:
  • Sets window size to 65535 if normalize_window is enabled
Note: Only processes TCP packets (IP protocol = 6).

tcp_offset

fn tcp_offset(&self, data: &[u8]) -> Option<usize>
Calculates the byte offset where the TCP header begins. Returns: Some(offset) if the packet is valid IPv4 TCP, None otherwise.

apply

fn apply(&self, ctx: &mut FlowContext<'_>, data: &mut BytesMut) -> Result<TransformResult>
Applies header normalization to the packet. Returns: TransformResult::Continue

Behavior

  • Only processes packets with valid IPv4 headers (minimum 20 bytes)
  • Skips non-IPv4 packets (e.g., IPv6)
  • TCP normalization only applies to TCP packets (IP protocol = 6)
  • Packets smaller than minimum header size are ignored
  • The transform is enabled when any normalization option is active
  • Uses packet count as seed for randomization: seed = packet_count * 0xDEADBEEF

Example Configuration

Normalize to Linux defaults

{
  "header": {
    "normalize_ttl": true,
    "ttl_value": 64,
    "normalize_window": true,
    "randomize_ip_id": true
  }
}
Makes all traffic appear to originate from a Linux system with standard settings.

Randomize IP ID only

{
  "header": {
    "normalize_ttl": false,
    "normalize_window": false,
    "randomize_ip_id": true
  }
}
Prevents IP ID-based tracking while preserving other header characteristics.

Windows fingerprint

{
  "header": {
    "normalize_ttl": true,
    "ttl_value": 128,
    "normalize_window": true,
    "randomize_ip_id": true
  }
}
Makes traffic appear to originate from a Windows system.

Code Example

From header.rs:161:
#[test]
fn test_normalize_ttl() {
    let params = HeaderParams {
        normalize_ttl: true,
        ttl_value: 128,
        normalize_window: false,
        randomize_ip_id: false,
    };
    let transform = HeaderNormalizationTransform::new(&params);
    
    let key = test_flow_key();
    let mut state = FlowState::new(key);
    let mut ctx = FlowContext::new(&key, &mut state, None);
    let mut data = create_ipv4_header();

    // Original TTL is 0x40 (64)
    assert_eq!(data[8], 0x40);

    transform.apply(&mut ctx, &mut data).unwrap();

    // TTL is now 128
    assert_eq!(data[8], 128);
}

IPv4 Header Normalization

From header.rs:20:
fn normalize_ipv4(&self, data: &mut BytesMut, seed: u64) {
    if data.len() < 20 {
        return; // Packet too small
    }

    // Verify IPv4 version
    let version = (data[0] >> 4) & 0x0F;
    if version != 4 {
        return;
    }

    // Normalize TTL (byte 8)
    if self.params.normalize_ttl {
        data[8] = self.params.ttl_value;
    }

    // Randomize IP ID (bytes 4-5)
    if self.params.randomize_ip_id {
        let new_id = ((seed >> 16) as u16).to_be_bytes();
        data[4] = new_id[0];
        data[5] = new_id[1];
    }
}

TCP Header Normalization

From header.rs:68:
fn normalize_tcp(&self, data: &mut BytesMut) {
    let tcp_offset = match self.tcp_offset(data) {
        Some(offset) => offset,
        None => return,
    };

    if self.params.normalize_window {
        // TCP window is at offset 14 in TCP header
        let window = 65535u16.to_be_bytes();
        data[tcp_offset + 14] = window[0];
        data[tcp_offset + 15] = window[1];
    }
}

Header Field Locations

IPv4 Header (20 bytes minimum)

  • Byte 0: Version and IHL
  • Bytes 4-5: Identification
  • Byte 8: Time To Live (TTL)
  • Byte 9: Protocol

TCP Header (20 bytes minimum, follows IPv4)

  • Bytes 14-15: Window Size (relative to TCP header start)

Use Cases

  • OS fingerprinting evasion: Normalize headers to match a different OS
  • Tracking prevention: Randomize IP ID to prevent packet tracking
  • Fingerprint obfuscation: Make all connections appear identical
  • Protocol normalization: Standardize header values across flows

Limitations

  • Only supports IPv4 (not IPv6)
  • Only normalizes TCP windows (not UDP)
  • Does not recalculate checksums (assumes lower-level handling)
  • Small packets and malformed headers are silently ignored

Build docs developers (and LLMs) love