NetOtter · developer documentation

NetOtter Code Map

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.

Modules

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

Main flows

The paths that matter most, function by function. Solid arrows run in order; dotted arrows are calls made inside the step they leave from.

1. Startup and shutdown

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()"]

2. A tick: from the wire to the packet list

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()"]

3. Showing packets: l and i

In 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()"]

4. Saving and loading .nto files

Loading 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

5. Anomaly detection

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()"]

Files

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.

Headers

Function 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.

Start from: