1 Filters and Analysis Internals
Watersrc-stack edited this page 2026-10-11 14:31:31 +02:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Filters and analysis internals

Display filters

Headers: includes/filters.h. Code: src/filters/.

File Job
filters.c The active list and its toggles: filter_apply(), filters_apply_args(), filters_remove_at(), filters_clear().
filter_print.c The Active filters (OR): ... lines.
filter_match.c filters_match(), one helper per filter type.
filter_parse.c Names and values: strict dotted IPv4, IPv6 with at most one ::, port 1–65535, protocol and EtherType names.
local.c The machine's own addresses, for me.

The name tables (filter_type_names[], filter_shortcuts[]) are in src/static_data/static_filter.c. They also produce the Available filters: line and the unknown-filter error, so a new filter shows up everywhere once it's in the table.

Representation

netotter_t.filters is a list of filter_t: a FILTER_TYPE and a value union, zeroed before it's filled so two filters compare with memcmp. The union holds an IPv4 or IPv6 address, a port, an interface name (FK_IFACE, up to 15 characters, or all), a protocol number (FK_PROTO) or an EtherType (FK_ETYPE).

Toggles

filter_apply():

  • A single filter is added when absent and removed when present.
  • A shortcut (ipv4, ipv6, port, me) adds whichever of its two filters is missing. It removes both only when both are active.

Matching

filters_match(filters, mode, local, packet):

  • No filter: everything is shown.
  • FM_OR: a packet is shown when any filter matches.
  • FM_AND: a packet is shown when every filter matches. This is naive on purpose: a shortcut in AND mode needs source and destination to match.
  • ARP: packet_ipv4() makes ARP packets answer the IPv4 and me filters, with the sender IP as source and the target IP as destination.
  • proto: compares the IPv4 protocol, or the IPv6 next header or the protocol after the extension headers (ipv6->upper).
  • etype: compares net_proto. llc is stored as 0 and matches every value up to 1500.

Filters only affect display: display_packets() (the l list and the CLI list) and -o / o saves. Capture and i cover every packet.

Own addresses: me

netotter_t.local is a list of local_addr_t (interface name, family, address, saved, renamed):

  • Filling: the backend's local_addresses_refresh() fills it (getifaddrs() on Linux, GetAdaptersAddresses on Windows).
  • Refreshing: local_refresh() runs at startup and then every 30 s (LOCAL_REFRESH_S) after the ticks.
  • Only growing: addresses are only ever added. A temporary IPv6 address that expired still belongs to this machine in older packets.
  • From a file: on -I, saved addresses are merged with local_add(..., saved = 1). An address that is also live stays live, and M marks the others (from the file).

The analysis view's output

includes/analysis.h, src/analysis/view.c, src/analysis/renames.c, libs/utils/my_write.c.

Display code never calls write(). It calls my_write(fd, ...):

  • with a normal fd, that's write();
  • with OUT_MEMORY, it appends to a growable buffer.

my_putstr / PSTR, my_putunbr / PNBR, everything in src/display/ and filter_print.c go through it.

flowchart LR
  vb["view_begin()"] -->|classic| fd1["fd 1: written as it comes"]
  vb -->|analysis| mem["OUT_MEMORY buffer"]
  mem --> ve["view_end()"]
  ve --> rw["renames_write()<br/>longest match at each position"]
  rw --> out["stdout"]

l and i are wrapped in view_begin() / view_end():

  • Classic view (or no renaming): output goes to fd 1.
  • Analysis view: view_begin() turns on port names (display_port_names(1)) and hostnames (display_hosts()), and sends output to memory. view_end() takes the text (my_out_take()) and prints it through renames_write(), then turns them off again.

A memory buffer was chosen over the alternatives because the code must build on Linux and Windows. A pipe blocks at 64 KiB, and Windows has no named pipes. tmpfile() fails at the drive root on some Windows runtimes.

Renamings

netotter_t.renames is a list of rename_t (from, to, kind). renames_write() scans the text and, at each position, replaces the longest matching from.

Kind Made by Matches Saved
RN_TEXT n <text>,<name> (rename_set()) any plain substring yes
RN_IPV4 / RN_IPV6 local_sync_renames(): <address> -> me whole addresses only (address_char() checks the neighbouring characters) no

Automatic renamings:

  • Whole addresses: 192.0.2.10 doesn't hit 192.0.2.103, and ::1 doesn't hit fe80::1. An IPv4 address may be followed by : (its port).
  • User text wins: rename_set_auto() never replaces a user renaming of the same text. Typing a renaming for an automatic one turns it into RN_TEXT.
  • When they're made: local_sync_renames() runs when local_refresh() adds addresses and after load_local(). local_addr_t.renamed marks the done ones, so an n <address> removal lasts for the session.

Port names

port_names[] (src/static_data/static_display.c) is a range table: {low, high, name, app}.

  • Order: the first match wins, so single ports go before the ranges that contain them.
  • Width: name is exactly 5 characters, the width of the longest port number, so endpoint columns stay aligned.
  • Apps: app is the precise application (Minecraft (Java)), printed on i's Application line, or NULL for plain protocols.
  • Ephemeral ports: game ranges stay below 32768, so random client ports aren't mislabelled.

Port names are printed before the renamings apply, so a renaming can rename them.