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.
Packet parsing
raw_to_f() (src/parsers/raw_to_f.c) turns the raw frames of the queue into packets (fpacket_t), one at a time, through process_packet(). The Learning section explains each protocol's layout.
The packet: fpacket_t
Defined in includes/netotter_types.h. A packet owns one structure per decoded header, and fpacket_delete() frees them all.
| Fields | Layer |
|---|---|
if_id, ifname, type_src |
where it was captured |
link, eth |
link layer: LINK_ETH with its Ethernet header, or LINK_RAW_IP |
net_proto, net_type, ipv4 / ipv6 / arp |
network layer: the EtherType, and the header matching net_type |
ptype, tcph / udph, port_s, port_d |
transport layer (T_TCP, T_UDP or T_UNK) |
mcast |
IGMP / MLD (ptype stays T_UNK: they have no ports) |
payload, payload_len, l4_off |
everything after the network header, while parsing |
ts_us |
capture time, microseconds since the Unix epoch (from the kernel ring on Linux, Npcap on Windows) |
l7 |
DNS, ICMP and HTTP facts (l7_info_t), NULL for anything else |
stored_len, stored_flags, stored_off, stored |
the payload once stored (see Packet storage) |
The layers
flowchart TD
pl["parse_link()"] -->|EtherType| fnp["find_net_parser()<br/>net_parsers[]"]
fnp --> ip4["parse_ipv4()"]
fnp --> ip6["parse_ipv6()<br/>+ skip_ipv6_extensions()"]
fnp --> arp["parse_arp()"]
fnp -->|"none / failed"| unk["NET_UNK: payload = after the link header"]
ip4 -->|"l4_proto"| pt["parse_transport()"]
ip6 -->|"l4_proto"| pt
pt --> tcp["parse_tcph()"]
pt --> udp["parse_udph()"]
pt --> igmp["parse_igmp() (IPv4, proto 2)"]
pt --> mld["parse_mld() (ICMPv6 130/131/132/143)"]
pt --> l7["parse_l7(): DNS, ICMP, HTTP"]
Application facts: parse_l7()
src/parsers/parse_l7.c runs right after the transport layer, while the payload is still plain. It fills fpacket->l7, which is saved with the packet, so the anomaly detection works on a loaded capture without decompressing anything:
- DNS (UDP or TCP port 53): id, response flag, rcode, answer count, the first question's type, and for its name: length, longest label, and FNV-1a hashes of the name and of its last two labels. Those hashes count distinct names and domains (random names, PTR bursts) without keeping the names.
- ICMP / ICMPv6: type and code, and for an error message, the packet it's about: protocol, addresses, ports. A port unreachable then refuses the UDP connection it names.
- HTTP (any TCP port): a request method (
GET,POST...) or a status line (HTTP/1.x NNN) at the start of a segment.
The l line shows them as tags: [DNS A?], [DNS NXDOMAIN], [DNS A 1 answer], [port unreachable 192.0.2.10:514/udp], [echo request], [GET], [HTTP 404]. i adds a DNS, ICMP or HTTP line; for DNS, it reads the question name again from the decompressed payload.
Link: parse_link()
- Ethernet (
LINK_ETH):parse_eth()reads the 14-byte header intoethand setsnet_prototo its EtherType. - Raw IP (
LINK_RAW_IP, VPN tunnels): there is no link header, so the EtherType is guessed from the IP version nibble (4 →0x0800, 6 →0x86dd).
Values of 1500 and below aren't EtherTypes but 802.3 lengths, shown as LLC.
Network: net_parsers[]
src/static_data/static_nw.c maps EtherType → parser:
static_net_parsers const net_parsers[] = {
{ETYPE_IPV4, parse_ipv4},
{ETYPE_IPV6, parse_ipv6},
{ETYPE_ARP, parse_arp},
};
A parser has the net_parser_t signature:
int parse_xxx(fpacket_t *fpacket, const uint8_t *data, int len, net_result_t *res);
It decodes its header into the packet, sets net_type, and fills res:
hdr_len: the header's length;payload_len: the bytes after it;l4_proto: the next protocol's number, or -1 when there's none.
It returns 0 on success, or 1 when the header can't be decoded, and then it changes nothing. With no parser, or a failed one, the frame is still kept, as NET_UNK.
Details per parser:
parse_ipv4- Honours the header length field (IHL), so options are skipped, though not decoded.
- Cuts the payload at the IP total length, because short frames get padded to 60 bytes.
- Only the first fragment carries the transport header:
l4_protois -1 for the others.
parse_ipv6- Reads the fixed 40-byte header.
skip_ipv6_extensions()then walks up to 8 Hop-by-Hop, Routing and Destination Options headers. It setsipv6->upperto the protocol after them andipv6->ext_lento their length, and stops at a Fragment header or anything cut short.- The payload still starts right after the fixed header;
fpacket->l4_off = ext_lenis where the transport header begins inside it.
parse_arp: IPv4-over-Ethernet ARP only: operation, sender and target MAC and IP.
Transport: parse_transport()
It dispatches on the protocol number, reading at payload + l4_off:
parse_tcph()- Reads the fixed header fields.
- Honours the data offset, so options are counted in the header length (
TCP header 40 bytesfor a SYN), though not decoded. - Sets
port_s/port_d.
parse_udph(): reads the 8-byte header and sets the ports.parse_igmp()/parse_mld()- Fill one
mcast_header_t: version, type, max response time, group. - Keep the first 16 records of v3 / v2 reports, with the announced record count kept exact.
- Fill one
Hostnames: hosts_learn()
The last reader of the plain payload: DNS answers, TLS ClientHello SNI and QUIC Initial packets. See Hostname learning. After it, store_payload() stores the payload and frees it.
Reading bytes
Multi-byte fields are big-endian. Use read_u16() / read_u32() (declared in network.h), never a cast like *(uint16_t *)p: a cast reads the bytes in the machine's order (wrong on x86) and may be misaligned.
Every parser checks lengths before reading: the frame comes from the network, and anyone can send anything.
Adding a network protocol (VLAN, RARP...)
- Type: add a
NET_*value, a header struct, and its pointer infpacket_t(netotter_types.h). Free it infpacket_delete(). - Parser: write
parse_xxx()with thenet_parser_tsignature, and add a row tonet_parsers[]. - Display: show it in the
lline (print_ip,src/display/display_list.c) and in theidetails (print_ip_info,display_info.c). - Save format: add a
SAVE_HAS_*bit and handle it input_headers()/get_headers()(src/save/save_record.c). This changes the.ntolayout, so bumpSAVE_FORMAT_VERSION; see Save format. - Filters, if useful:
etypealready matches any EtherType. - Build: add any new file to
SRCS, then runmake re(a struct changed) andmake windows.
Decoded so far
| Layer | Protocols |
|---|---|
| Link | Ethernet II, 802.3 (shown as LLC), raw IP |
| Network | IPv4 (options skipped), IPv6 (with extension headers), ARP (IPv4 over Ethernet) |
| Transport | TCP (options skipped), UDP, IGMP v1–v3, MLD v1–v2 |
| Application, for names | DNS answers, TLS ClientHello, QUIC v1 / v2 Initial packets |
| Application, for the anomaly detection | DNS header and question, ICMP / ICMPv6 type and quoted packet, HTTP request method and response status |
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