Skip to content

Feature gap: address-persistent SNAT pool mode silently degrades to round-robin in userspace-dp #1377

@psaab

Description

@psaab

Gap

The eBPF dataplane implements address-persistent SNAT pool mode (Junos pool-name { address-persistent; }): the pool address selected for a given internal source IP is sticky — re-NAT'd flows from the same internal source always pick the same pool address. The userspace dataplane parses the same pool_addresses snapshot field but its PortAllocator does round-robin only — the addr_persistent semantic is silently lost. This is a CORRECTNESS regression for any deployment relying on sticky external-IP affinity (e.g. SIP, stateful upstream auth tied to client IP).

eBPF implementation (source of truth)

  • bpf/xdp/xdp_policy.c:442-445if (cfg->addr_persistent) ip_idx = ((__u32)src_ip) % cfg->num_ips; (IPv4 sticky path)
  • bpf/xdp/xdp_policy.c:527-535 — same for IPv6: hash last 4 bytes of source for sticky pool index
  • pkg/dataplane/compiler_nat.go:306, 419if natCfg.AddressPersistent { ... } propagates the flag into BPF nat_pool_config
  • bpf/headers/xpf_nat.h (struct nat_pool_config { __u8 addr_persistent; ... })

Userspace-dp gap

userspace-dp/src/nat.rs:107-114 PortAllocator::next_address_index():

pub(crate) fn next_address_index(&self) -> usize {
    if self.counters.is_empty() { return 0; }
    let idx = self.addr_counter.fetch_add(1, Ordering::Relaxed);
    (idx as usize) % self.counters.len()
}

grep -rn 'address.persistent\\|address_persistent\\|AddressPersistent' userspace-dp/src/ — zero matches. grep -rn 'address_persistent\\|AddressPersistent\\|addr_persistent' pkg/dataplane/userspace/snapshot.go — zero matches. The Go-side snapshot does not even carry the flag to Rust.

Recommended fix

  1. Plumb AddressPersistent bool through SourceNATRuleSnapshot in pkg/dataplane/userspace/protocol.go + matching field on the Rust SourceNATRuleSnapshot
  2. In userspace-dp/src/nat.rs::PortAllocator, add next_address_index_for(src_ip: IpAddr) that returns hash(src_ip) % num_ips when sticky, else falls back to round-robin
  3. Add capability gate or test that address-persistent config actually round-trips correctly under userspace mode

Blocker for #1373

This must land before Phase 4 (BPF source removal) of #1373. Today the eBPF dataplane preserves correctness for these configs; once eBPF is removed there is no fallback path and the silent regression becomes permanent.


Refined contract (added 2026-05-17 after triple-review of #1384 and code review of #1385)

See docs/pr/1373-retire-ebpf-dataplane/plan-1377-snat-pools.md for the full implementation contract. New since the original issue body:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions