Skip to content

Repository files navigation

NatStream

NatStream is a Linux daemon that exports conntrack NAT events as IPFIX (RFC 7011) or NetFlow v9 (RFC 3954) records over UDP.

It listens to netlink conntrack notifications, extracts NAT-relevant flow fields, and sends them as flow records to a configured collector.

NatStream on a live NAT box: 28.7 Gbps in, 28.8 Gbps out, exporter at 0.1% CPU and 2 MB RSS

A live NAT box carrying ~28.7 Gbps in and ~28.8 Gbps out on bond0, exporting every conntrack event to a collector: natstream sits at 0.1% CPU and 2 MB resident on an 88-thread E5-2699, and the whole machine is at 3%.

Upstream: https://github.com/FastNetMon/NatStream

Install (Debian / Ubuntu)

Every release publishes a .deb per target distribution, with a SHA256SUMS alongside them: https://github.com/FastNetMon/NatStream/releases/latest.

To build one yourself, ./build.sh packages the daemon inside a Docker container based on the target distribution — the same script the release workflow runs, so a locally built package is the packaged one. See DEVELOPMENT.md.

The package

Path
/usr/sbin/natstream the daemon
/lib/systemd/system/natstream.service the service unit
/etc/default/natstream configuration (a dpkg conffile)

Installing does not enable or start the service, because it has no useful default collector and would only produce a restart loop. Configure it first:

sudo dpkg -i natstream_0.1.0-1~deb13_amd64.deb
sudoedit /etc/default/natstream        # set COLLECTOR=ip:port
sudo systemctl enable --now natstream.service
journalctl -u natstream -f

/etc/default/natstream holds the collector endpoint and any extra flags:

COLLECTOR=203.0.113.10:4739
EXPORTER_OPTS=--protocol netflow9 --profile nat-source --counter-width 4

For several collectors, use a comma-separated list — systemd substitutes ${COLLECTOR} as a single word, so this reaches the exporter as one value:

COLLECTOR=203.0.113.10:4739,198.51.100.7:4739

The service

The unit runs the daemon in the foreground under Type=simple and lets journald take its output — systemd supervises it, so the daemon's own --daemon supervisor is not used and would only get in the way.

It does not run as root. The exporter needs CAP_NET_ADMIN to join the conntrack netlink group, force its socket buffer sizes, and set the nf_conntrack sysctls — the kernel's net sysctl handler grants CAP_NET_ADMIN holders owner-level access, so no root and no CAP_DAC_OVERRIDE are required. The unit therefore uses DynamicUser=yes with exactly that one capability, plus the usual sandboxing (ProtectHome, PrivateDevices, ProtectProc, RestrictAddressFamilies=AF_NETLINK AF_INET AF_INET6, a @system-service syscall filter, and no new privileges). systemd-analyze security rates it 2.6.

One directive is deliberately absent: ProtectKernelTunables=yes would remount /proc/sys read-only and break the sysctl setup the exporter does at startup. If you would rather lock that down, set the two sysctls declaratively in /etc/sysctl.d/, add --no-sysctl to EXPORTER_OPTS, and then add ProtectKernelTunables=yes to a unit override.

Requirements

  • Linux (Netlink netfilter API required)
  • CAP_NET_ADMIN (or root) for netlink socket setup and buffer/socket options

On startup the exporter sets nf_conntrack_events=1, without which there are no events to export at all, and nf_conntrack_acct=1, without which every counter is zero. Failing to enable events is fatal; failing to enable accounting only warns. Use --no-sysctl to leave both alone on hosts where they are already configured or /proc/sys is read-only. Note that these settings are net-namespace-wide and are not restored on exit.

Enabling nf_conntrack_acct does not fix up the entries that already exist. The accounting counters live in an extension area that is attached when a conntrack entry is created, so every flow already tracked when the sysctl was flipped stays without one and reports zero for the rest of its life. New entries get counters immediately, so the effect fades as the table churns; a long-lived session can keep exporting zeros for hours. conntrack -F clears it at once, at the cost of breaking every established NAT session on the box.

Run

sudo natstream --collector <IP>:<port>

Examples:

# Basic foreground mode
sudo natstream --collector 203.0.113.10:4739

# Override buffers and domain id
sudo natstream \
  --collector 203.0.113.10:4739 \
  --recv-buf 8388608 \
  --send-buf 8388608 \
  --domain-id 100

# The same records to two collectors, as a repeated flag or a comma-separated list
sudo natstream \
  --collector 203.0.113.10:4739 \
  --collector 198.51.100.7:4739
sudo natstream --collector 203.0.113.10:4739,198.51.100.7:4739

# NetFlow v9 to a collector that only decodes the base field registry
sudo natstream \
  --collector 203.0.113.10:2055 \
  --protocol netflow9 \
  --profile flow-only \
  --counter-width 4

# Run with self-supervision, a log file and verbose logs
sudo natstream --collector 203.0.113.10:4739 \
  --daemon --log-file /var/log/natstream.log -v

Under systemd, prefer Type=simple without --daemon and let journald capture stderr, rather than the built-in supervisor.

Command-line options

  • -c, --collector <ip:port> (required): flow collector endpoint. Repeat the flag, or separate endpoints with commas, to export the same records to several collectors. IPv4 and IPv6 endpoints can be mixed.
  • --protocol <ipfix|netflow9>: export protocol (default: ipfix).
  • --profile <full|nat-source|flow-only>: which field set to export (default: full).
  • --counter-width <4|8>: byte and packet counter width (default: 8).
  • --template-id <u16>: template ID to advertise (default: 256, minimum 256).
  • --template-interval <secs>: seconds between template retransmissions (default: 5).
  • --domain-id <u32>: observation domain ID / NetFlow v9 source ID (default: 0).
  • --recv-buf <bytes>: netlink receive buffer size (default: 4194304).
  • --send-buf <bytes>: UDP send buffer size (default: 4194304).
  • -v, --verbose: enable debug logging.
  • --daemon: run as a background supervisor/worker pair with restart on worker crash.
  • --log-file <path>: in --daemon mode, append log output here instead of discarding it.
  • --no-sysctl: do not touch the nf_conntrack sysctls.

The effective configuration is logged at startup, including the record size and how many records fit in a message.

Protocol and profiles

The NAT elements this exporter relies on — natEvent and the four postNAT* elements — are IPFIX registry entries and are not in RFC 3954's NetFlow v9 field table. They are a Cisco NAT Event Logging convention there. Support varies by collector and by collector version, which is what the profiles are for: pick the largest field set your collector actually decodes.

Profile Fields Record (8B / 4B counters) Carries
full 14 58 B / 42 B Both NAT directions and both counter directions
nat-source 12 52 B / 36 B Source translation only; drops postNATDestination*
flow-only 9 45 B / 29 B No NAT information at all — pre-NAT five-tuple and counters only

Rough starting points, worth confirming against your own version: pmacct (nfacctd) and nfdump 1.7+ handle full; ntopng and NEL-aware commercial collectors generally do too. Collectors limited to their base registry need flow-only, which is a real loss of information — it exports the flow but not the translation. Use tshark -d udp.port==<port>,cflow -V to see exactly what a dissector makes of your export before blaming a collector.

--counter-width 4 exists because NetFlow v9's IN_BYTES/OUT_BYTES default to four bytes and some collectors expect exactly that. Conntrack counters are 64-bit, so a value too large for a four-byte field is clamped, not truncated, and the clamp count is reported in the periodic stats line.

Record layout

Both protocols carry the same field values in the same order; they differ only in how the elements are identified.

# Field IPFIX IE NetFlow v9 type Bytes Profiles
1 natEvent 230 230 1 full, nat-source
2 protocolIdentifier 4 4 PROTOCOL 1 all
3 sourceIPv4Address 8 8 IPV4_SRC_ADDR 4 all
4 destinationIPv4Address 12 12 IPV4_DST_ADDR 4 all
5 sourceTransportPort 7 7 L4_SRC_PORT 2 all
6 destinationTransportPort 11 11 L4_DST_PORT 2 all
7 postNATSourceIPv4Address 225 225 4 full, nat-source
8 postNAPTSourceTransportPort 227 227 2 full, nat-source
9 postNATDestinationIPv4Address 226 226 4 full
10 postNAPTDestinationTransportPort 228 228 2 full
11 octetDeltaCount 1 1 IN_BYTES 4 or 8 all
12 packetDeltaCount 2 2 IN_PKTS 4 or 8 all
13 reply octets 1 + PEN 29305 23 OUT_BYTES 4 or 8 all
14 reply packets 2 + PEN 29305 24 OUT_PKTS 4 or 8 all

Fields 7–10 come from the conntrack reply tuple, so SNAT, DNAT and combined translations all report the address and port they actually rewrote; a direction that was not translated simply repeats the original value.

Fields 13 and 14 report the flow's reply direction. Under IPFIX that is an RFC 5103 reverse Information Element, so the collector must handle enterprise-specific field specifiers. NetFlow v9 has no enterprise mechanism, so it uses the dedicated OUT_BYTES/OUT_PKTS types instead. Note these are not interchangeable: IEs 23/24 under IPFIX mean postOctetDeltaCount/postPacketDeltaCount, the forward direction as modified by a middlebox, which is a different quantity.

Counters come from the conntrack accounting counters, so they are zero on CREATE events and hold the flow's totals on DELETE.

FAQ

All my counters are zero. Conntrack accounting is off. The exporter tries to set nf_conntrack_acct=1 at startup but only warns if it cannot — look for conntrack accounting unavailable, counters will be zero in the log. The usual causes are --no-sysctl in EXPORTER_OPTS, ProtectKernelTunables=yes in a unit override, or a read-only /proc/sys. If accounting was enabled only recently, see the note in Requirements: entries created before that never get counters.

Half my flows are zero and the other half are not. That is expected. The exporter subscribes to both the conntrack CREATE and DELETE event groups. A CREATE record is emitted the instant the entry appears, when nothing has been counted yet, so its counters are zero by definition; the flow's totals arrive on the matching DELETE record.

No flows at all. The exporter refuses to start when it cannot enable nf_conntrack_events, so a running daemon has event delivery. Check that nf_conntrack is actually tracking (conntrack -E should print events), that the collector address in COLLECTOR= is reachable over UDP, and the periodic log line for send failures and netlink drops.

My collector shows the forward counters but not the reply ones. Under IPFIX the reply direction is an RFC 5103 reverse Information Element with PEN 29305, and collectors that ignore enterprise-specific field specifiers drop it. Either use a collector that handles the full profile, or export NetFlow v9, where the reply direction is carried as plain OUT_BYTES/OUT_PKTS. See Protocol and profiles.

The post-NAT destination is identical to the destination — can I drop it? On a box that only does source NAT, yes: that is what --profile nat-source is for. It drops fields 9 and 10 and leaves the rest, 52 B per record instead of 58. Those two fields come from the conntrack reply tuple's source, and nothing rewrites the destination under plain SNAT or masquerade, so they repeat fields 4 and 6 exactly. They stop repeating as soon as anything translates the destination — a DNAT port forward, a REDIRECT to a transparent proxy, hairpin NAT, a load-balancing rule — and then they are the only place the collector learns which backend the flow reached. Check with nft list ruleset | grep -Ei 'dnat|redirect' before deciding, and note that the template is fixed at startup: add a port forward later and the translation silently stops being exported. Do not solve the duplication the other way round by keeping only the post-NAT pair — fields 3–6 are the base five-tuple that every collector decodes without any NAT support at all.

Can one instance feed two collectors? Yes — pass --collector more than once, or one comma-separated list, and every message goes to all of them. The records are encoded once and only the send() repeats, so a second collector costs one more syscall per message, not a second copy of the work. Each collector gets its own socket, so a mixed list of IPv4 and IPv6 endpoints is fine and an address that is unreachable at startup fails startup rather than being discovered later in the logs. What one instance cannot do is speak two protocols: the protocol, profile and counter width are fixed for the whole process, so IPFIX to one collector and NetFlow v9 to another still means two instances — and then each is an independent netlink multicast subscriber, so the kernel copies every conntrack event once per instance.

Notes

  • Uses only a small set of dependencies and manual encoding/parsing for hot-path efficiency.
  • Messages are capped to an MTU-safe size of 1472 bytes. NetFlow v9 FlowSets are padded to a 4-byte boundary as RFC 3954 asks.
  • One process speaks one protocol, profile and counter width, to as many collectors as --collector names. Exporting in two different protocols means two instances, and each is a separate netlink multicast subscriber, so the kernel copies every conntrack event once per instance.
  • A send failure is counted per collector, and the periodic stats line names the collectors that failed, since one unreachable destination does not stop the others.
  • Every 10 seconds, any netlink drops, UDP send failures, truncated datagrams and messages from a non-kernel sender are logged.
  • The daemon runs as root throughout; it does not yet drop privileges after opening its sockets.

Development

Building from source, packaging, benchmarks and the release process are in DEVELOPMENT.md.

License

Copyright 2026 FastNetMon LTD.

Licensed under the Apache License, Version 2.0. See LICENSE for the full text and NOTICE for the copyright notice, or https://www.apache.org/licenses/LICENSE-2.0.

About

NatStream is user space NAT events exporter in Netflow v9 and IPFIX formats

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages