Which file holds which logic, and which function calls which. The call edges come from GCC's -fcallgraph-info on both the Linux and the Windows (MinGW) builds, so they are what the compiler saw, plus the calls made through the net_parsers[] table. The rest of the documentation is in the wiki.
One box per source folder. An arrow means at least one function of a module calls one of the other. Every module also uses Utils (my_write, my_putstr) and the static tables, left out to keep the picture readable. Core code reaches the OS only through common_lib.h; exactly one backend is compiled in.
flowchart TB entry["Entry point"] commands["Interactive commands"] parsers["Parsers"] display["Display"] filters["Filters"] analysis["Analysis"] anomaly["Anomaly detection"] save["Save files"] helpers["Packet helpers"] linux["Linux backend"] windows["Windows backend"] analysis --> display analysis --> filters analysis --> linux analysis --> parsers analysis --> windows anomaly --> analysis anomaly --> display anomaly --> filters anomaly --> linux anomaly --> windows commands --> analysis commands --> anomaly commands --> display commands --> filters commands --> helpers commands --> linux commands --> parsers commands --> save commands --> windows display --> analysis display --> anomaly display --> linux display --> parsers display --> windows entry --> analysis entry --> anomaly entry --> commands entry --> display entry --> filters entry --> helpers entry --> linux entry --> parsers entry --> save entry --> windows filters --> analysis filters --> anomaly filters --> display filters --> linux filters --> windows helpers --> anomaly helpers --> display helpers --> filters helpers --> linux helpers --> windows parsers --> analysis parsers --> anomaly parsers --> helpers parsers --> linux parsers --> save parsers --> windows save --> analysis save --> filters save --> helpers
The paths that matter most, function by function. Solid arrows run in order; dotted arrows are calls made inside the step they leave from.
netotter() in src/entry_point/netotter.c. A failing step skips straight to the cleanup and returns 1.
flowchart TD
main["main()"] --> nt["netotter()"]
nt --> nn["netotter_new()"]
nn --> pa["parse_args()<br/>one flag at a time; -I / -O opened here,<br/>-T read by anom_trusted_load()"]
pa --> ic["init_common_struct()<br/>the backend lists the interfaces"]
ic --> lr["local_refresh()<br/>own addresses, automatic 'me' renamings"]
lr --> rc["reverse_cache_load()<br/>stored reverse DNS answers"]
rc --> q{"-l given?"}
q -- yes --> ifd["if_tab_display()"]
q -- no --> si["store_init()<br/>the payload spool"]
si --> lp["load_packets() when -I"]
lp --> fa["filters_apply_args()<br/>-f / -F"]
fa --> ef["enable_from_list() when -i"]
ef --> st["start_program()<br/>anomaly_load_summary() after -I"]
st --> m{"-c given?"}
m -- yes --> cli["run_cli()<br/>at Ctrl-C: the list, anomaly_stats(), anomaly_report()"]
m -- no --> ri["run_interactive()"]
cli --> sp["save_packets() when -O / -o"]
ri --> sp
sp --> ct["common_lib_terminate()"]
ifd --> ct
ct --> nd["netotter_delete()"]
run_cli() runs the same chain (and stops when the RAM budget is full), sleeping with my_usleep() between ticks instead of waiting for a key, and sends no reverse DNS lookups.
flowchart TD ri["run_interactive() loop"] --> sw["stdin_wait()"] sw -- "a key" --> hk["handle_key()<br/>one command"] sw -- "timeout" --> tk["tick()"] tk --> rf["raw_to_f()"] rf --> at["anomaly_tick()<br/>new warnings and critical anomalies"] at --> cf["capture_full_check()<br/>stops capture past PACKETS_RAM_MAX"] cf --> lr["local_refresh()"] lr --> rt["reverse_tick()"] tk -.-> nwt["nw_tick()<br/>per enabled interface, kernel drops reported"] nwt -.-> rec["TPACKET_V3 ring (nw_ring_read) or recvmsg /<br/>pcap_next_ex: one queue_t per frame, with its capture time"] rf -.-> pp["process_packet()<br/>per frame"] pp -.-> pl["parse_link()"] pp -.-> np["net_parsers[ ]<br/>parse_ipv4() · parse_ipv6() · parse_arp()"] pp -.-> pt["parse_transport()"] pp -.-> l7["parse_l7()<br/>DNS · ICMP · HTTP facts, classify_payload()"] pp -.-> hl["hosts_learn()<br/>DNS · TLS SNI · QUIC Initial"] pp -.-> sp["store_payload()<br/>compressed, to the spool"] rf -.-> pa["packets_add()<br/>RAM budget, the list, then anomalies_packet()"] rf -.-> qp["queue_purge()"]
l and iIn the classic view my_write() goes straight to stdout and view_end() has nothing to rewrite.
flowchart TD hk["handle_key() 'l' or 'i'"] --> lc["list_command() / info_command()"] lc --> vb["view_begin()<br/>analysis view: port names, hostnames, output to memory"] vb --> dp["l: display_packets() → filters_match() → print_packet_line()<br/>(anomaly tags in the analysis view)<br/>i: store_read_payload() → print_packet_info()<br/>(application facts, anomaly_print_packet())"] dp --> mw["my_write()<br/>stdout or memory buffer"] mw --> ve["view_end()<br/>renames_write()"] ve --> qr["reverse_queue_lookups()<br/>online and analysis view only"] qr -.-> rt["next tick: reverse_tick() → send_query()"] rt -.-> ha["handle_answer()<br/>reverse_answer_add()"]
.nto filesLoading only happens at startup (-I); saving at exit (-O / -o) or with the O / o commands. Payloads are copied as they are stored, never recompressed nor decompressed.
flowchart TD
subgraph SAVE["Save"]
s1["O / o → save_command() → save_to_file()"] --> s2["save_open()<br/>adds .nto, mode 0600"]
s2 --> s3["save_packets()"]
x["-O / -o at exit"] --> s3
s3 --> s4["write_save()<br/>header, filters, renamings, own addresses, names"]
s4 --> s5["put_record() per packet<br/>headers, capture time, application facts, payload class"]
end
subgraph LOAD["Load"]
l1["-I → load_packets()"] --> l2["read_file()"]
l2 --> l3["header: NOTR magic, format v16 only, version strings"]
l3 --> l4["sections: filters, renamings, own addresses,<br/>reverse DNS answers, hostnames"]
l4 --> l5["get_record() per packet → packets_add()<br/>anomalies rebuilt from the records"]
l5 --> l6["anomaly_load_summary()"]
end
Every packet added to the list, captured or loaded, goes through anomalies_packet() (src/anomaly/). "Now" is the newest packet's capture time, never the wall clock, so a -I load finds the same anomalies as the live capture.
flowchart TD
pa["packets_add()"] --> ap["anomalies_packet()"]
ap -.-> se["anom_attempts_settle()<br/>when the time moves: unanswered attempts fail"]
ap --> ip{"IP packet?"}
ip -- no --> arp["anom_spike_packet()<br/>anom_arp_packet(): MAC changes, conflicts"]
ip -- yes --> fl["anom_spike_packet() · flow_of()<br/>hosts, flows both ways"]
fl -.-> nf["new flow: anom_detect_flow()<br/>floods, bursts, brute force, sweeps;<br/>anom_attempt_queue() for scans"]
fl -.-> ea["established: anom_detect_answered()<br/>foreign hosts, direct-IP checks"]
fl -.-> cc["anom_check_class()<br/>payload class vs port"]
fl -.-> dq["DNS query: anom_check_tunnel() · anom_check_ptr()"]
fl -.-> dl["anom_detect_l7()<br/>NXDOMAIN, SERVFAIL, unreachables, HTTP errors"]
nf --> ar["anomaly_raise()"]
ea --> ar
cc --> ar
dq --> ar
dl --> ar
arp --> ar
ar -.-> tk["after each tick: anomaly_tick()<br/>anom_direct_settle(), live alerts"]
ar -.-> sw["s: stats_command() → anomaly_stats()<br/>w: warnings_command() → anomaly_report()"]
Every C file in the build, grouped by folder, with the description from its header comment, then its functions in source order. Bold chips are callable from other files, the rest are static; struck-through chips are never called. Click any chip to open it in the explorer.
Pick a function to see its source comment, who calls it and what it calls. Open the arrows to walk further up or down the call tree. Calls into the OS backend show both the Linux and the Windows function.