Sign inSign up

mjaksn/nettail

By mjaksn

•Updated 1 minute ago

A NetFlow and IPFIX collector that prints to the console or web terminal with hostnames

Image
0

3.7K

mjaksn/nettail repository overview

⁠nettail

CI Release PyPI GHCR Docker Hub License: MIT

A NetFlow and IPFIX collector that prints flow records to the console in a readable table, annotating addresses with hostnames where it can find them. tail -f for your network.

Built for pointing a UniFi Dream Machine Pro at a workstation and actually seeing what the network is doing. Works with any exporter that speaks NetFlow v5, NetFlow v9, or IPFIX (v10).

Two dependencies, both of them halves of this program that grew up and left: netflume⁠ reads the wire, and lanname⁠ turns an address into a hostname. Neither has dependencies of its own, so pip install nettail brings in three pure Python packages and nothing else.

TIME         EXPORTER        PROTO  SOURCE                                     DESTINATION                                 PKTS    BYTES     DUR  FLAGS
13:40:03.000 10.0.0.1        TCP    192.168.1.42:51234 (macbook-pro)         ↑ 140.82.114.4:443/https (github)               23     4.1K   4.90s  ...AP.SF
13:40:03.000 10.0.0.1        UDP    192.168.1.77:5353/mdns (hue)             ⇄ 224.0.0.251:5353/mdns                          2     180B   4.90s
13:39:56.103 10.0.0.1        TCP    10.0.1.5:44321 (nas)                     ↑ 104.244.42.1:443/https (twitter-edge)        412    57.5K  12.50s  ...AP...
13:40:08.453 10.0.0.1        TCP    1.1.1.1:853/domain-s                     ↓ 10.0.1.5:39012 (nas)                           4     320B   0.15s  ......S.

⁠Contents


⁠Requirements

Neither has dependencies of its own, so this brings in three pure Python packages and nothing else.

pip install nettail

Or, from a checkout, either of:

pip install -e .        # the nettail command, pointed at the working tree
pip install lanname netflume && python -m nettail

Binding to the default port 2055 does not require root on Linux, since it is above 1024. You only need elevated privileges if you choose a port below 1024.


⁠Quick start

# Listen on the default port
nettail

# Pick a port and only show flows that touch the internet
nettail --port 2055 --external-only

# Passive name resolution plus your own static mappings
nettail --resolve dns --hosts ./lan-hosts

# Dump every decoded field under each flow
nettail --verbose

# Spell out each template an exporter sends, without the field lines
nettail --templates

# Machine readable, one object per line
nettail --json > flows.jsonl

# Or to a file of its own, with the table still on the screen
nettail --json flows.jsonl

# Keep thirty days of flow history rather than fourteen, for the browser to
# scroll back into and to replay a tab that was in the background
nettail --flow-retention-days 30

# Or keep no flow history at all
nettail --flow-store off

Press Ctrl-C to stop. A summary prints on exit with datagram counts, template statistics, name resolution hit rates, and the top ten external addresses by volume.


⁠Configuring the UDM Pro

In the UniFi Network application:

Settings > CyberSecure > Traffic Logging

On some firmware versions this lives under Settings > System > Integrations instead. Look for a section named NetFlow or NetFlow (IPFIX).

SettingValue
NetFlow (IPFIX)Enabled
Collector addressIP of the machine running this script
Collector port2055
SamplingOff
Networks / VLANsSelect every network you want visibility into
⁠Sampling must be off

This matters more than anything else on the page. Sampled export throws away most flows before they leave the router. A malware beacon is a handful of small flows per hour, and at 1:1024 sampling you will never see it. Sampling is fine for bandwidth graphs and useless for security monitoring.

If your firmware does not let you disable sampling, the flow data is not trustworthy for detection work and you should look at a mirror port with Zeek instead.

You do not have to take the router's word for it. The collector reads the sampling rate an exporter advertises and says so on stderr the first time it sees one:

10.0.0.1 reports 1-in-1000 sampling. The byte and packet counts shown are a sample,
so real traffic is roughly 1000x higher. Turn sampling off at the exporter for true
counts.

The rate is repeated in a Sampling section of the exit summary. It is read from whichever form the exporter uses: samplingInterval or samplerRandomInterval on v9, samplingPacketInterval with samplingPacketSpace or samplingSize out of samplingPopulation on IPFIX, and the sampling field of the header on v5. Nothing is printed when an exporter reports no sampling, or a rate of one in one.

If an exporter later reports that sampling is off, meaning an interval of one or a selection that skips nothing, the remembered rate is dropped and a line says the counts are complete again. An option record that says nothing about sampling, such as one carrying interface names, never changes what is remembered.

Detection depends on the exporter actually sending options data. Silence is not proof that sampling is off, so check the UniFi UI as well. On v5 there are no options records at all: sampling is read from the header, and an exporter that stops sampling simply clears the field, which is reported as nothing rather than as a change.

⁠Local traffic may never arrive

Selecting every network in the table above reads like a request for visibility into all of them. On a UDM Pro it is not what you get. Traffic to and from the internet arrives. Traffic between two local hosts may not, and the two reasons for that are worth telling apart, because only one of them is surprising.

Two hosts on the same VLAN never produce a flow record at all. They talk through a switch and the gateway routes nothing between them, so there is nothing for it to account. This is not a fault and no setting changes it. A flow exporter running on the router cannot see traffic that never reaches the router, and every tool reading NetFlow from a gateway has this property, this one included.

Two hosts on different VLANs are routed by the gateway and may still be missing. This is the surprising one. Traffic between separate subnets has to pass through the UDM Pro, and a tracert or traceroute between the hosts shows it as a hop, so the router demonstrably handles the packets. They can still be absent from the export, with every network selected.

Two mechanisms would account for that and they cannot be told apart from outside the box. The routing may be offloaded to hardware along a path the flow accounting does not sit on, or the accounting may be scoped to the internet-facing path by design. The consequence is the same either way, which is why the distinction matters less than it looks: neither this collector nor anything in the export configuration changes it.

This is behaviour observed on a UDM Pro rather than anything Ubiquiti documents, so establish it on your own hardware before concluding. Three checks, in order of effort:

  1. tracert or traceroute between the two hosts. The UDM's address as a hop means it routes them, and the rest of this section applies. A switch address instead means a layer 3 switch is doing the routing and the gateway has nothing to report, which is a different problem with a different answer.
  2. Insights > Flows in the UniFi Network application. Sessions listed there and absent from your export mean the data exists on the box and only the export scope is holding it back. That is the version worth raising with Ubiquiti, being a demonstrable gap rather than a suspicion.
  3. This collector's own exit summary. Compare flows decoded under Summary with flows under External traffic. Two equal figures mean every flow that arrived had a public endpoint, so nothing internal is being exported at all.

If it is east-west visibility you need, a gateway is the wrong vantage point for it. Mirror the VLANs you care about to a capture host at the switch and read packets rather than flow records, which is the same advice as for sampled export and for a different reason.

⁠Other notes
  • Make sure a host firewall on the collector is not dropping inbound UDP on your chosen port. sudo ufw allow 2055/udp or the nftables equivalent.
  • If flows appear with the UDM's own WAN address as the source, you are seeing post-NAT records and have lost internal host attribution. Check which networks are selected in the export configuration.

⁠Command line options

usage: nettail [-h] [--version] [--config FILE | --save-config [FILE]]
               [--bind BIND] [--port PORT] [--external-only] [--names]
               [--macs] [--verbose] [--templates] [--json [FILE]]
               [--flow-store [FILE]] [--flow-retention-days DAYS]
               [--flow-prune-every SECONDS] [--flow-commit-every SECONDS]
               [--colour WHEN] [--no-color] [--header-every HEADER_EVERY]
               [--sticky-header] [--hide-status] [--no-supplemental-services]
               [--web] [--web-port PORT] [--web-bind ADDR] [--web-host NAME]
               [--web-token TOKEN] [--web-colour WHEN] [--web-readonly]
               [--web-detail-refresh SECONDS] [--size-scale-max BYTES |
               --size-scale-dynamic] [--size-scale-window FLOWS] [--country]
               [--country-db FILE] [--update-country-db]
               [--country-style {auto,flag,code}] [--resolve {off,dns,all}]
               [--hosts FILE] [--resolve-public] [--fqdn]
               [--resolve-workers RESOLVE_WORKERS]
               [--resolve-timeout RESOLVE_TIMEOUT]
⁠General
OptionDefaultDescription
--versionPrint the version and exit
--config FILEsearchedRead settings from this file instead of looking for one. See Settings file⁠
--save-config [FILE]~/.nettail/nettail.confWrite what this run would have used to a file and exit without collecting anything
--bind BIND0.0.0.0Address to bind the UDP socket to
--port PORT2055UDP port to listen on. 0 has the system pick a free one, and the startup line names the port it picked
--external-onlyoffOnly display flows where the source or destination is a public IP. Everything is still counted in the summary
--namesoffShow a host by its name in place of its address, where a name is known. The n key turns it off and on while running
--macsoffShow hardware addresses on a line under each flow, on the exporters that send them. The p key turns it off and on while running
--verboseoffPrint every decoded field on an indented line under each flow, and report datagrams that could not be decoded. The v key moves the same setting mid-run
--templatesoffSpell out each template the first time an exporter sends it, and note in one line each time a template is sent again. v9 and IPFIX only; v5 carries no templates. The t key moves the same setting mid-run
--json [FILE]offEmit one JSON object per flow. On its own, or given -, the objects go to stdout in place of the table. Given a path they are appended to that file instead, and the table, the keys and the browser view carry on as if the flag were not there
--flow-store [FILE]onWhere to keep the local SQLite history of shown flows. By default, and on its own, it writes under a per-user data directory (%APPDATA%\nettail on Windows, ~/Library/Application Support/nettail on macOS, and $XDG_DATA_HOME/nettail or ~/.local/share/nettail elsewhere); given a path it writes there instead, and off keeps no history. The history is what a browser scrolls back into and what a backgrounded tab is replayed from when it comes back. A store that cannot be opened stops the run and says so
--flow-retention-days DAYS14How long to keep rows in the durable flow store. At least one day, and a value of 14 keeps the last two weeks unless you change it
--flow-prune-every SECONDS43200How often old rows are pruned from the SQLite store. This is 12 hours by default, and it is a background maintenance pass rather than a freeze on the collector
--flow-commit-every SECONDS1How often buffered writes are committed to the file. Lower values make the history more current, while higher values allow a little more batching
--colour WHENautoWhen to use ANSI colour on this terminal: auto, always or never. Under auto a terminal gets colour and a redirected stream does not, and NO_COLOR in the environment turns it off. The browser view has its own switch, --web-colour, and is not decided by this one. --color is accepted too
--no-coloroffThe same as --colour never, and like it, about this terminal
--header-every N40Reprint the column header every N lines. 0 disables repeats
--sticky-headeroffPin the column header to the top row of the window. The k key moves the same setting mid-run. See below
--hide-statusoffTurn off the two-line status bar at the foot of the window, which is shown by default whenever output is going to a terminal. The b key toggles it while the collector runs. See The status bar⁠
--no-supplemental-servicesoffName ports from the system services database alone. Without it, a short list shipped with this program fills in the ports the system does not know. See Service names⁠
⁠Web interface

All off unless --web is given. See The web interface⁠.

OptionDefaultDescription
--weboffAlso serve the display to a browser
--web-port PORT2056Port for the web interface. 0 has the system pick a free one, and the printed URL names the port it picked
--web-bind ADDR127.0.0.1Address for the web interface. Anything other than loopback exposes this network's traffic over cleartext HTTP, and is warned about at startup
--web-host NAMEnoneA name the view answers to. Under the loopback default it is added beside localhost; under another --web-bind, which otherwise answers to any name, it restricts the view to the names given. May be repeated
--web-colour WHENonColour in the browser view: on or off. A browser is a colour-capable reader whatever stdout is, so a redirected run does not take the colour out of it. --web-color is accepted too
--web-token TOKENrandomUse this token in the URL instead of a fresh random one, so a bookmark survives a restart. Taken from NETTAIL_WEB_TOKEN in the environment when the flag is not given, which is how an installed service receives one without it appearing in ps
--web-readonlyoffServe the display but accept no keys from the browser
--web-detail-refresh SECONDS5How often the flow details dialog asks the collector for its figures again. 0 leaves them still until Refresh is pressed. See Clicking a flow⁠
⁠Sticky header

--sticky-header keeps the column header on the top row while flows scroll underneath it, so you never lose track of which column is which. It works by setting a VT100 scroll region (DECSTBM) covering every row but the first, then letting output scroll inside that region as usual.

The k key moves the same setting while the collector runs, in both directions, which is worth having for the first item below: pin the header while you read the columns, let it go when you want the scrollback back. Letting it go leaves the flows where they are, and pinning it again scrolls one row rather than clearing the screen, so an hour of history is not the price of pressing a key. In a window too short to hold a header the key says so and changes nothing, as the b key does for the bar.

Two things to know before turning it on:

  • You lose scrollback. Most terminals discard lines that scroll out of a margin region instead of pushing them into the scrollback buffer, so you can only see what is currently on screen. If you want to scroll back through past flows, leave the flag off and stay with --header-every, or keep the display and write the flows to a file with --json flows.jsonl.
  • It needs a real terminal. On Windows the script enables virtual terminal processing automatically, which covers Windows Terminal and modern conhost. If stdout is redirected to a file or a pipe, or the window is too short, the flag prints a notice and falls back to --header-every. The header alone needs six rows; with the status bar up, which is the default, it needs eight.

The header is redrawn when the window is resized (the size is re-measured every 16 flow lines), and the scroll region is released on Ctrl-C so the summary and your shell prompt are not trapped inside it. --header-every is ignored while the header is pinned, since the repeats would be redundant.

⁠Flow size colour
OptionDefaultDescription
--size-scale-max BYTES100KTop of the BYTES colour scale. Accepts a plain byte count or a K, M, G, T suffix (powers of 1024, matching the column)
--size-scale-dynamicoffRe-range the scale to the largest flow seen so far instead of a fixed top. Mutually exclusive with --size-scale-max
--size-scale-window FLOWSoffScope the dynamic scale to the last N flows instead of the whole run. Implies --size-scale-dynamic, so it cannot be combined with --size-scale-max

See Size colour scale⁠ for what the colours mean.

⁠Country marking options
OptionDefaultDescription
--countryoffMark every public address with the country it is in. Implied by --country-db
--country-db FILEsearchedThe database to read. Without it, the first of /etc/nettail/country.mmdb, /usr/share/GeoIP/GeoLite2-Country.mmdb and /var/lib/GeoIP/GeoLite2-Country.mmdb that exists, and a few more besides
--update-country-dboffFetch DB-IP's free country database, put it where the next run will read it, and exit without collecting anything. Replaces the database that is there, or fetches a first one where there is none. With --country-db it refreshes that file instead of searching
--country-style {auto,flag,code}autoHow this terminal is shown a country: flag for the emoji, code for the two letters, auto for the letters wherever a flag is known not to be drawn. The browser is sent the flag whatever this says, and draws it or not by its own fonts

See Country marking⁠ for where the database comes from and what is marked.

⁠Hostname resolution options
OptionDefaultDescription
--resolve MODEalloff, dns, or all. See below
--hosts FILEnoneStatic mappings in /etc/hosts format. Repeatable
--resolve-publicoffAlso reverse-resolve public addresses via PTR
--fqdnoffShow nas.lan instead of nas
--resolve-workers N4Background lookup threads
--resolve-timeout SEC1.0Budget for the mDNS and NetBIOS probes together, mDNS taking at most half

⁠Settings file

Everything the collector takes on the command line it takes in a file instead, under the same name without its leading dashes. There is no second list of what can be set: the file is read against the same parser the command line is, so an option that exists is settable, and one that does not is reported as an unknown key.

# nettail.conf
port = 2055
external-only = true
web = true
web-bind = 127.0.0.1
flow-retention-days = 30
resolve = dns
hosts =
    /etc/hosts.lan
    /etc/hosts.iot

A [nettail] header is allowed and is not needed: there is one section and nowhere else for a setting to be, so a file that opens with port = 2055 is read as though it had one. A key may be written with dashes as the flag is or with underscores as Python spells it, and an option with two spellings answers to both: colour and color are one setting, as they are one flag. Writing one option twice under two of its names is reported rather than silently resolved, and the first is the one used. A switch takes true or false, and yes, no, on, off, 1 and 0 besides. An option that may be repeated takes one value a line, indented under the key. A comment is a line beginning with # or ;, and a % is a % rather than anything clever.

The command line wins. What the file says becomes the default and anything typed overrides it, so a config file is where the settings you always want live and the command line is for today. What neither says falls back to what the program shipped with, which is how a default this program changes later still reaches you.

The one exception is an option that may be repeated, --hosts and --web-host. Typing one of those adds to what the file listed rather than replacing it, because that is what repeatable means everywhere else here. A run that wants none of them wants --config pointed at a file that lists none.

The durable flow store is a different shape of option. It is on unless something turns it off, so a file needs to say nothing to have one; flow-store = /var/lib/nettail/flows.sqlite3 writes there instead of in the default per-user path, and flow-store = off keeps no history at all. false and no mean off too, since a file reads them as switches, and true means the default path, as --flow-store on its own does. The retention and cadence settings are ordinary values, so flow-retention-days = 30, flow-prune-every = 3600, and flow-commit-every = 2.5 all work exactly as the flags do.

Options that are alternatives win the same way, and it is worth saying because they are the one place where winning means the file's setting is dropped rather than replaced. With size-scale-max in your file, --size-scale-dynamic on the command line gives you a dynamic scale and the file's fixed top is set aside for that run, exactly as though it had not been there. The same goes for --size-scale-window, which rules out a fixed top too. Neither is an error: you asked for one of a pair of alternatives and got it.

There is a second thing worth knowing before you rely on it: a switch turned on in a file cannot be turned off from the command line, because switches here have no --no- form. Colour is not quite an exception and is worth spelling out, since it is the setting most likely to be in a file: colour is a choice rather than a switch, so colour = never in a file still yields to --colour always like any other value. no-color = true is a switch, so it yields to nothing, and it beats --colour always besides, because --no-color means --colour never however it arrived. Set colour, not no-color, in a file you want to argue with later. Anything else, and the answer is a different file.

⁠Where it is looked for

The first of these that exists is read, and only that one. They are not merged: a setting that comes from two files at once is a sett

Tag summary

Content type

Image

Digest

sha256:c1085b138…

Size

48.7 MB

Last updated

1 minute ago

docker pull mjaksn/nettail