OS backends
C has no interfaces, so NetOtter uses a header: includes/common_lib.h declares the functions every backend implements, and the Makefile compiles exactly one backend (libs/linux/ or libs/windows/). Core code includes common_lib.h only, never a backend header, and keeps every OS-specific header (poll.h, sys/socket.h, windows.h...) and every OS-specific field (like st_ino) out of src/. make windows checks that the core still builds for the other system.
The contract
lib_common_t carries the backend's name (lib_name: Linux / Windows, also written in save files), its interface list, and an opaque internal_struct for the backend's own state.
| Function | Job |
|---|---|
init_common_struct() |
Starts the backend and lists the interfaces, all disabled. |
common_lib_terminate() |
Closes every capture and the resolver socket, frees everything. |
tick() |
Appends the frames waiting on every enabled interface to the queue, without blocking. |
toggle_ifname() / get_ifname_state() |
Starts / stops capture on interface id, or reads its state. |
disable_all() |
Stops every interface; returns how many were running. |
stdin_wait() |
Waits up to n ms for a typed line: 1 ready, 0 timeout, -1 error. |
local_addresses_refresh() |
Appends the machine's current addresses that aren't known yet (never removes one). |
resolver_open/send/recv/close() |
The UDP socket to the system DNS server, for reverse lookups. |
random_bytes() |
Unpredictable bytes, for DNS query ids. |
same_file() |
Whether two open files are the same file (-I vs -O). |
my_usleep() |
Sleeps n µs (usleep is obsolete in POSIX). |
cache_file_path() |
Where a cache file lives, optionally creating the folders. |
term_colors() |
Whether stdout shows ANSI colours. |
spool_open/write_at/read_at/close() |
The temporary payload file. |
Some functions take no lib_common_t, because they run before the backend starts or after it stops: same_file, my_usleep, cache_file_path, term_colors and the spool functions.
Interface ids are positions in the backend's current list, so they shift when an interface appears or disappears.
Linux (libs/linux/)
| File | What |
|---|---|
linux_lib.c |
Interfaces from if_nameindex(), Wi-Fi detection through a wireless-extensions ioctl; same_file (st_dev / st_ino), my_usleep (nanosleep), term_colors (a tty with a TERM other than dumb), cache_file_path ($XDG_CACHE_HOME/netotter/ when absolute, else ~/.cache/netotter/, folders created 0700). |
linux_common_functions.c |
The contract functions. |
linux_network.c |
One AF_PACKET / SOCK_RAW / ETH_P_ALL socket per enabled interface, restricted to it with bind() on its sll_ifindex (SO_BINDTODEVICE doesn't filter packet sockets). nw_tick reads with recvfrom(MSG_DONTWAIT) until EAGAIN; sll_hatype gives the link type. |
linux_addresses.c |
Own addresses from getifaddrs(). |
linux_input.c |
stdin_wait with poll(). |
linux_resolver.c |
Non-blocking UDP socket connected to the first nameserver of /etc/resolv.conf; random_bytes with getrandom(). |
linux_spool.c |
An unnamed file (O_TMPFILE, falling back to mkstemp + unlink), pread / pwrite. |
Capturing needs cap_net_raw. Ethernet, Wi-Fi (outside monitor mode) and lo all deliver Ethernet frames. Tunnels (tun, WireGuard) deliver raw IP. On lo, each packet is seen twice (out and in).
Windows (libs/windows/)
| File | What |
|---|---|
windows_pcap.c / .h |
Loads Npcap's wpcap.dll at runtime and fills a table of function pointers (npcap_t). |
windows_adapters.c |
GetAdaptersAddresses, short names, descriptions. |
windows_lib.c |
The interface list: Npcap's devices, named after their Windows adapters. |
windows_common_functions.c |
The contract functions. |
windows_network.c |
One Npcap capture handle per enabled interface. |
windows_addresses.c |
Own addresses from GetAdaptersAddresses. |
windows_input.c |
stdin_wait for consoles and pipes. |
windows_resolver.c |
Winsock UDP socket to the first DNS server from GetNetworkParams (IPv4 only); random_bytes with BCryptGenRandom. |
windows_spool.c |
FILE_FLAG_DELETE_ON_CLOSE file, offsets through OVERLAPPED. |
Npcap, loaded at runtime
The library is loaded from System32\Npcap\wpcap.dll (with LOAD_WITH_ALTERED_SEARCH_PATH), then, for Npcap's WinPcap-compatible mode, wpcap.dll from System32 only. NetOtter never loads it from its own folder or the current directory, so a planted DLL can't be picked up.
Without Npcap, set_interfaces prints a notice and returns an empty list, and NetOtter still works on files. The build needs no Npcap SDK: windows_pcap.h declares the few libpcap types used. Npcap's license forbids redistributing it without an OEM license, so it must never be bundled.
GetProcAddress results are cast through void (*)(void). Under C23 (GCC 16's default), FARPROC's () means (void), and a direct cast breaks -Werror.
Interface names
Windows adapter names are GUIDs. NetOtter uses ConvertInterfaceLuidToNameA names instead (ethernet_32769, wireless_32768, loopback_0, cut to 15 characters). They have no spaces, so they fit me filters and save files. They're matched to Npcap's \Device\NPF_{GUID} by the GUID in AdapterName.
- A device without an adapter is
npf_<position>. -laddsFriendlyName: Descriptionafter each name.- Own addresses use the same names, so
me wireless_32768works.
Capture
- Opening:
pcap_createwith a 65536-byte snapshot length, no promiscuous mode (like Linux), immediate mode and an 8 MiB kernel buffer (it holds frames while a typed line pauses the ticks). Then non-blockingpcap_next_ex, up to 500 frames per tick. - Link types:
DLT_EN10MB(Ethernet, and Wi-Fi outside monitor mode) isLINK_ETH.DLT_NULL/DLT_LOOP(loopback, 4-byte family header stripped) and the raw-IP DLTs areLINK_RAW_IP.- Anything else is
LINK_UNK.
- Loopback: packets show up once.
- Errors:
PCAP_ERROR_PERM_DENIEDmeans Npcap was installed for administrators only.
Console input
A console has no poll(). stdin_wait checks every 20 ms for a key press that types a character (or, for a pipe, PeekNamedPipe). It returns at the first character, then read() takes the rest of the line. The console only echoes while a read() waits, so ticks pause while a line is typed.
Testing under Wine
make windows then wine ./NetOtter.exe -I capture.nto -c | tr -d '\r' gives the same output as Linux (Windows ends lines with \r\n). Limits:
- Wine's
wpcap.dllwraps the host libpcap: interfaces are listed, but opening them fails (Wine has nocap_net_raw). WINEDLLOVERRIDES="wpcap=d"simulates a machine without Npcap.- Wine can't
PeekNamedPipea Unix pipe (error 50), so piped commands run no ticks between them. - Wine's
LOCALAPPDATAis its own (C:\users\<user>\AppData\Local).
Adding a backend function
- Declare it in
common_lib.hwith a comment saying what it returns on each system. - Implement it in both
libs/linux/andlibs/windows/. A new file goes inADDSRCS. - Check both builds:
make re && make windows.
User guide
- Overview
- Installation
- Getting started
- Command-line options
- Interactive commands
- Display filters
- Analysis view
- Reverse DNS and privacy
- Anomaly detection
- Save files
- Memory and limits
- Troubleshooting
Developer documentation
- Overview
- Building
- Architecture
- OS backends
- Packet parsing
- Packet storage
- Save format
- Hostname learning
- Reverse DNS internals
- Filters and analysis internals
- Anomaly lab
- Coding conventions
- Function explorer
Learning