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.
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
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.
| 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 4For 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:4739The 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 three sysctls declaratively in
/etc/sysctl.d/, add --no-sysctl to EXPORTER_OPTS, and then add
ProtectKernelTunables=yes to a unit override.
- Linux (Netlink netfilter API required)
CAP_NET_ADMIN(or root) for netlink socket setup and buffer/socket options
On startup the exporter sets three sysctls: nf_conntrack_events=1, without
which there are no events to export at all; nf_conntrack_acct=1, without which
every counter is zero; and nf_conntrack_timestamp=1, without which a flow's
start and end times are unknown. Failing to enable events is fatal; failing to
enable accounting or timestamping only warns. Use --no-sysctl to leave all
three 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 or nf_conntrack_timestamp does not fix up the
entries that already exist. Both counters and timestamps live in extension areas
that are attached when a conntrack entry is created, so every flow already
tracked when the sysctl was flipped stays without them for the rest of its life:
it reports zero counters, and its flow times fall back to when the exporter saw
the event. New entries get both 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.
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 -vUnder systemd, prefer Type=simple without --daemon and let journald capture
stderr, rather than the built-in supervisor.
-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, minimum256).--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--daemonmode, append log output here instead of discarding it.--no-sysctl: do not touch thenf_conntracksysctls.
The effective configuration is logged at startup, including the record size and how many records fit in a message.
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 | IPFIX record (8B / 4B counters) | v9 record (8B / 4B) | Carries |
|---|---|---|---|---|
full |
16 | 74 B / 58 B | 66 B / 50 B | Both NAT directions and both counter directions |
nat-source |
14 | 68 B / 52 B | 60 B / 44 B | Source translation only; drops postNATDestination* |
flow-only |
11 | 61 B / 45 B | 53 B / 37 B | No NAT information at all — pre-NAT five-tuple, counters and timestamps only |
The two protocols differ in size only because of the flow timestamps: 8 bytes each under IPFIX, 4 each under NetFlow v9.
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.
Both protocols carry the same values in the same order. They differ in how the elements are identified, and — for the two timestamps alone — in what the value actually measures.
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.
Fields 15 and 16 come from the conntrack timestamp extension
(nf_conntrack_timestamp), which records when the kernel first saw the flow and
when it tore it down. The two protocols carry different quantities here:
- IPFIX carries absolute milliseconds since the Unix epoch, which is what
flowStartMilliseconds/flowEndMillisecondsare defined as. Nothing else is needed to interpret them. - NetFlow v9 has no absolute-time element in its field table.
FIRST_SWITCHEDandLAST_SWITCHEDare milliseconds counted from the same zero as the header'ssysUpTime, so a collector has to combine them withsysUpTimeand the header'sunix_secsto get a wall-clock time. Two consequences follow: the 32-bit field wraps after 49.7 days of exporter uptime, and a flow that started before the exporter did — every session that outlives a restart — has no representation at all, so it reports0. Prefer IPFIX if flow times matter.
A CREATE event has not ended, so it reports the instant it began as both start and end. On a DELETE both are the kernel's own timestamps.
Without nf_conntrack_timestamp the kernel attaches no timestamp to a conntrack,
and both fields fall back to when the exporter observed the event. That is later
than the truth by however long the flow had been running, but it is a real
instant rather than 1970, so a collector can still place the record.
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.
My flow timestamps are all "now", or every FIRST_SWITCHED is 0. Conntrack
timestamping is off, so the records fall back to the observation time — look for
conntrack timestamping unavailable in the log. Same causes as zero counters
above, and the same catch: entries created before nf_conntrack_timestamp=1 took
effect never get a timestamp. Under NetFlow v9 a FIRST_SWITCHED of 0 has a
second, unrelated cause: the flow genuinely started before the exporter did, and
v9 cannot express that. IPFIX reports the true start in both cases.
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, 68 B per IPFIX record instead of 74.
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.
- 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
--collectornames. 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.
Building from source, packaging, benchmarks and the release process are in DEVELOPMENT.md.
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.

