Summary
Since the flow-accounting backend was migrated from pmacct to the ipt_NETFLOW kernel module (T75), set system flow-accounting vrf <name> no longer affects where NetFlow export packets are sent. A collector that is only reachable inside a VRF never receives any flows when the export is configured with source-address instead of source-interface. Configurations that worked on the pmacct backend — and configurations produced automatically by the flow-accounting 2-to-3 migration script — commit without error but silently stop exporting after the upgrade.
Environment
VyOS 1.5 rolling, any build that includes the pmacct → ipt_NETFLOW migration (T75, commit e992fb4ec, merged late August 2025). A NetFlow collector reachable only through a non-default VRF (typical out-of-band management design).
Steps to reproduce
Given an OOB interface bond0.500 (address 10.0.0.64/xx) enslaved to VRF OOB-Management, and a collector 10.0.0.24 reachable only through that VRF:
set system flow-accounting enable-egress set system flow-accounting netflow engine-id '1064' set system flow-accounting netflow interface 'bond0.500' set system flow-accounting netflow server 10.0.0.24 port '2055' set system flow-accounting netflow server 10.0.0.24 source-address '10.0.0.64' set system flow-accounting vrf 'OOB-Management' commit
Expected result
NetFlow datagrams are exported to 10.0.0.24:2055, sourced from 10.0.0.64, routed via the OOB-Management VRF — the same behaviour the pmacct backend provided (see T3981).
Actual result
No datagrams ever reach the collector. The generated /etc/modprobe.d/ipt_NETFLOW.conf contains:
options ipt_NETFLOW destination=10.0.0.24:2055@10.0.0.64
@10.0.0.64 only binds the export socket's source address. It does not bind the socket to the VRF, so the route lookup for 10.0.0.24 happens in the default routing table, which has no path into OOB-Management. The packets are silently dropped. commit reports no error, and the configured vrf OOB-Management is ignored entirely.
Workaround
Replace the per-server source-address with a source-interface enslaved to the VRF:
delete system flow-accounting netflow server 10.0.0.24 source-address set system flow-accounting netflow server 10.0.0.24 source-interface bond0.500
This renders destination=10.0.0.24:2055%bond0.500. The %interface suffix makes the module set sk_bound_dev_if to that interface (SO_BINDTODEVICE); as the interface is in the VRF, l3mdev then routes the export via the VRF table.
Root cause
- On the pmacct backend, VRF binding was applied at the process level: the uacctd systemd override launched the daemon as ip vrf exec <vrf> /usr/sbin/uacctd ..., so every export socket landed in the VRF regardless of source address.
- ipt_NETFLOW emits from the kernel — there is no daemon to wrap. The new backend never uses the vrf node for the export path; vrf is referenced only in verify() to validate that source-address exists within the VRF. Nothing binds the module's export socket to the VRF.
- The only per-server option that influences the bind device is source-interface (rendered as %ifname). source-address (rendered as @addr) can only set the source IP — it cannot select a VRF.
Module reference (aabc/ipt-netflow, ipt_NETFLOW.c)
- Destination grammar per collector: ip[:port][@srcaddr][%device]. @srcaddr is parsed immediately after the port; %device is parsed after @srcaddr (SEPARATORS = " ,;\t\n", so @ and % are not separators and the order is fixed).
- %device sets sk->sk_bound_dev_if = dev->ifindex (SO_BINDTODEVICE), then sk_dst_reset(). Binding to a VRF master device is exactly what ip vrf exec did.
- @srcaddr binds the socket to the source address only.
- sdev[IFNAMSIZ] — device name is capped at 15 characters.
Proposed fix (most complete)
Make the global vrf setting mean "export all NetFlow out of this VRF" by filling the module's bind-device slot with the VRF device when a per-server source-interface is not given. This reproduces the pre-migration ip vrf exec behaviour, requires no CLI change, and makes existing/migrated configs work again.
Rendering rule per server (note the required order: @address before %device):
destination = collector:port [ @<source-address> ] [ %<bind-device> ]
bind-device = server.source_interface if set
else flow.vrf if set
else (omitted)
source-address = server.source_address if set, else (omitted)Resulting destination by case:
- vrf X + server source-address A → collector:port@A%X
- vrf X alone → collector:port%X
- vrf X + server source-interface I (I in X) → collector:port%I
- no vrf + server source-address A → collector:port@A (unchanged)
- no vrf + server source-interface I → collector:port%I (unchanged)
Principle: the %device slot holds the most specific egress binding available (source-interface over vrf); @address independently pins the source IP.
Additional changes:
- Fix token order (latent bug). The current template emits %device@address, which the module cannot parse when both are present (it reads @ before %). Emit @address%device regardless. This is harmless today only because verify() makes the two mutually exclusive per server; the VRF change introduces the combined case.
- Validation. Keep the source-address/source-interface mutual exclusion and the "address must be assigned within the VRF" check. Add: if vrf is set and a server uses source-interface, the interface must be enslaved to that VRF (vyos.utils.network.get_interface_vrf); otherwise raise a clear error, since such an interface would silently escape the VRF.
- Tests and docs. Add smoketest cases asserting the rendered destination (via get_module_data(...)['parameters']['destination']) for vrf-only, vrf + source-address, and vrf + source-interface. Document in vyos-documentation that vrf binds the NetFlow export to that VRF, source-interface overrides the bind device and must be in the VRF, and source-address sets the source IP.
Upgrade impact
No CLI syntax change and no migration is required; existing configurations continue to parse unchanged. The fix does, however, change the runtime behaviour of configurations that set vrf (hence classified as a behaviour change, not "perfectly compatible"):
- Intended effect. Exports that are currently broken — vrf set together with a per-server source-address, or with no per-server source, and a collector reachable only inside the VRF — start working. This includes every configuration the flow-accounting 2-to-3 migration produced.
- Regression risk to note. A configuration that sets vrf X but whose collector is actually reachable via the default routing table works today only because vrf is currently ignored. After the fix the export socket is bound into VRF X, so it will break if the collector is not reachable there. This was arguably a misconfiguration (the operator explicitly asked for VRF X), but the observable behaviour of an untouched config changes.
- Stricter validation. The added check — a source-interface used together with vrf must be enslaved to that VRF — can reject a configuration that previously committed. It surfaces the same silent VRF-escape problem at commit time instead of at runtime.