Skip to main content

Overview

The Decoy transform generates duplicate packets with a low TTL value that expire before reaching the destination. These decoy packets confuse DPI systems that attempt to reconstruct traffic, causing them to see corrupted or invalid protocol streams.

Structure

pub struct DecoyTransform {
    params: DecoyParams,
}

Configuration

send_before
bool
default:"false"
When enabled, sends a decoy packet before the real packet. The decoy has low TTL and expires in transit, while the real packet reaches the destination.
send_after
bool
default:"false"
When enabled, sends a decoy packet after the real packet. Both settings can be enabled to send decoys both before and after.
ttl
u8
default:"1"
The TTL (Time To Live) value for decoy packets. Low values (1-3) ensure the decoy expires quickly:
  • 1: Expires at first router
  • 2: Expires at second hop
  • 3: Expires at third hop
probability
f32
default:"0.0"
Probability (0.0 to 1.0) of generating a decoy packet. Values:
  • 0.0: Never generate decoys (disabled)
  • 0.5: 50% chance per packet
  • 1.0: Always generate decoys

Methods

new

pub fn new(params: &DecoyParams) -> Self
Creates a new Decoy transform with the specified parameters.

create_decoy

fn create_decoy(&self, original: &[u8]) -> Option<BytesMut>
Creates a decoy packet from the original packet by:
  1. Copying the original packet
  2. Setting TTL to the configured ttl value
  3. Inverting the IP identification field bytes (XOR with 0xFF)
Returns: Some(decoy) if the packet is valid IPv4, None otherwise.

should_send_decoy

fn should_send_decoy(&self, seed: u64) -> bool
Determines whether to generate a decoy based on the configured probability. Returns: true if a decoy should be generated, false otherwise.

apply

fn apply(&self, ctx: &mut FlowContext<'_>, data: &mut BytesMut) -> Result<TransformResult>
Applies the decoy transform to generate decoy packets. Returns: TransformResult::Fragmented if decoys were generated, TransformResult::Continue otherwise.

Behavior

  • Decoys are only generated if send_before or send_after is enabled
  • Probability check determines if decoy generation happens for each packet
  • Only IPv4 packets can have decoys generated (minimum 20 bytes)
  • When send_before is enabled:
    • The decoy becomes the first packet (modified data buffer)
    • The real packet is emitted as a subsequent packet
  • When send_after is enabled:
    • The real packet stays first
    • The decoy is emitted as a subsequent packet
  • Both modes can be active simultaneously
  • Decoy packets have modified IP ID to make them appear different
  • Low TTL ensures decoys expire before reaching the destination

Example Configuration

Always send decoy before

{
  "decoy": {
    "send_before": true,
    "send_after": false,
    "ttl": 1,
    "probability": 1.0
  }
}
Every packet is preceded by a decoy that expires at the first router.

50% chance of decoy after

{
  "decoy": {
    "send_before": false,
    "send_after": true,
    "ttl": 2,
    "probability": 0.5
  }
}
Half of all packets are followed by a decoy that expires at the second hop.

Decoy sandwich

{
  "decoy": {
    "send_before": true,
    "send_after": true,
    "ttl": 1,
    "probability": 0.3
  }
}
30% of packets are sandwiched between two decoys.

Disabled

{
  "decoy": {
    "send_before": false,
    "send_after": false,
    "ttl": 1,
    "probability": 0.0
  }
}

Code Example

From decoy.rs:206:
#[test]
fn test_decoy_send_before() {
    let params = DecoyParams {
        send_before: true,
        send_after: false,
        ttl: 2,
        probability: 1.0,
    };
    let transform = DecoyTransform::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_packet();

    let result = transform.apply(&mut ctx, &mut data).unwrap();
    assert_eq!(result, TransformResult::Fragmented);
    assert_eq!(ctx.output_packets.len(), 1);
    
    // First packet (in data buffer) is the decoy with TTL=2
    assert_eq!(data[8], 2);
    
    // Second packet (emitted) is the real packet with original TTL=0x40
    assert_eq!(ctx.output_packets[0][8], 0x40);
}

Decoy Creation Logic

From decoy.rs:20:
fn create_decoy(&self, original: &[u8]) -> Option<BytesMut> {
    if original.len() < 20 {
        return None;
    }

    let version = (original[0] >> 4) & 0x0F;
    if version != 4 {
        return None;
    }

    let mut decoy = BytesMut::from(original);
    
    // Set low TTL
    decoy[8] = self.params.ttl;
    
    // Modify IP ID to make decoy different
    if decoy.len() > 5 {
        decoy[4] ^= 0xFF;
        decoy[5] ^= 0xFF;
    }

    Some(decoy)
}
The decoy is identical to the original packet except:
  • TTL is set to a low value
  • IP identification field is inverted

Probability Check

From decoy.rs:42:
fn should_send_decoy(&self, seed: u64) -> bool {
    if self.params.probability <= 0.0 {
        return false;
    }
    if self.params.probability >= 1.0 {
        return true;
    }
    
    let threshold = (self.params.probability * 1000.0) as u64;
    (seed % 1000) < threshold
}
Probability is checked using a threshold: seed % 1000 < (probability * 1000) The seed is generated in apply() using:
let seed = ctx.state.packet_count
    .wrapping_mul(0x1337CAFE)
    .wrapping_add(data.len() as u64);

How Decoys Defeat DPI

  1. Middlebox confusion: DPI systems see the decoy packet and attempt to parse it
  2. Decoy expires: The decoy never reaches the destination due to low TTL
  3. Real packet arrives: The destination receives only the real packet
  4. Desynchronization: The DPI system’s view of the connection differs from reality
  5. Signature failure: Protocol signatures fail to match due to extra/modified packets

Use Cases

  • DPI evasion: Confuse DPI systems that reconstruct TCP streams
  • Middlebox testing: Detect presence of middleboxes along the path
  • Censorship circumvention: Defeat censorship systems that inspect initial packets
  • Protocol obfuscation: Make protocol detection unreliable

Important Notes

  • Decoys only work with IPv4 packets
  • TTL should be set low enough to expire before the destination
  • Network topology affects effective TTL values
  • High decoy rates may trigger rate limiting or anomaly detection
  • Decoys do not reach the destination server, so server-side logging is unaffected

Build docs developers (and LLMs) love