Table of Contents
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.
Save format (.nto, v17)
The code is in src/save/, and the layout is also described at the top of includes/save_format.h:
| File | What it does |
|---|---|
save_io.c |
Big-endian write and read cursors, write_all(), read_file(). |
save_record.c |
One packet record, both ways. |
save_sections.c |
The filters, renamings and own addresses. |
save_names.c |
The reverse DNS answers and learned hostnames. |
save_write.c |
Saving. |
save_load.c |
Loading. |
Every multi-byte number is big-endian. Strings have no '\0': they're ended by ~ or preceded by their length. All sections are mandatory, in this order.
Layout
Header
| Bytes | Field |
|---|---|
| 4 | magic NOTR |
| 1 | format version: SAVE_FORMAT_VERSION, 16 |
| n + 1 | program version (NETOTTER_VERSION, e.g. r1.03.00), ended by ~ |
| n + 1 | backend name (Linux / Windows), ended by ~ |
| 4 | packet count |
Both strings must be printable, or the file is rejected as corrupted. That's why the program version can't contain ~.
Sections
1. Active filters
| Bytes | Field |
|---|---|
| 2 | filter count |
| 1 | mode: 0 = OR, 1 = AND |
| per filter: 1 | type: the FILTER_TYPE number (0 ipv4src, 1 ipv4dest, 2 ipv6src, 3 ipv6dest, 4 portsrc, 5 portdest, 6 mesrc, 7 medest, 8 proto, 9 etype, 10 macsrc, 11 macdest, 12 ifname, 13 payloadmin, 14 payloadmax, 15 ttlmin, 16 ttlmax, 17 anomaly) |
| per filter | value: IPv4 (4), IPv6 (16), port (u16), interface name (u8 length + text, me* and ifname), protocol (u8), EtherType (u16), MAC (6), size (u16), TTL (u8), anomaly kind (u8, 0xFF = all) |
2. Renamings (the user's only)
| Bytes | Field |
|---|---|
| 2 | count |
| per renaming | u16 length + text, u16 length + new name |
3. Own addresses
| Bytes | Field |
|---|---|
| 2 | count |
| per address | u8 length + interface name, u8 v6 flag, 4 or 16 address bytes |
4. Reverse DNS answers and 5. learned hostnames
| Bytes | Field |
|---|---|
| 2 | count |
| per name | u8 v6 flag, 4 or 16 address bytes, u8 name length, name |
A name length of 0 in an answer means the server said there's no name. Learned hostnames always have 1 to 255 characters.
Both sections hold only the names of the saved packets' addresses: never the whole cache or host map. Before writing, mark_packet() stamps the answers and hosts of each saved packet with a save mark.
Packet records
The count from the header, each:
| Bytes | Field |
|---|---|
| 2 | interface id |
| 1 | interface kind (PACKET_SRC) |
| 1 + n | interface name |
| 1 | link type: 0 Ethernet, 1 raw IP, 2 unknown |
| 1 | layers present: SAVE_HAS_* bits |
| 2 | net_proto (EtherType) |
| 1 | transport type: 0 UDP, 1 TCP, 2 none |
| 2 + 2 | source and destination ports |
| 8 | capture time: microseconds since the Unix epoch (UTC) |
| 1 | payload class: what the application data looks like (enum PAYLOAD_CLASS: 0 none, 1 TLS, 2 SSH, 3 HTTP, 4 DNS, 5 QUIC, 6 NTP, 7 SMTP, 8 FTP, 9 a 220 greeting, 10 POP3, 11 IMAP, 12 RDP, 13 SMB, 14 BitTorrent, 15 text, 16 random-looking, 17 binary) |
if SAVE_HAS_ETH (0x01) |
6 destination MAC, 6 source MAC, u16 EtherType |
if SAVE_HAS_IPV4 (0x02) |
u8 header length, u16 total length, u16 fragment offset, u8 TTL, u8 protocol, 4 source, 4 destination |
if SAVE_HAS_IPV6 (0x10) |
u8 traffic class, u32 flow label, u16 payload length, u8 next header, u8 hop limit, 16 source, 16 destination, u8 upper protocol, u16 extension length |
if SAVE_HAS_ARP (0x20) |
u16 operation, 6 sender MAC, 4 sender IP, 6 target MAC, 4 target IP |
if SAVE_HAS_UDP (0x04) |
u16 source port, u16 destination port, u16 length, u16 checksum |
if SAVE_HAS_TCP (0x08) |
u16 ports ×2, u32 sequence, u32 acknowledgement, u8 header length, u8 flags, u16 window, u16 checksum, u16 urgent |
if SAVE_HAS_MCAST (0x40) |
IGMP / MLD: u8 v6, u8 version, u8 type, u16 max response, u16 checksum, group (4 or 16), u16 sources, u16 records, u8 kept, then per kept record u8 type, group, u16 sources |
if SAVE_HAS_L7 (0x80) |
application facts, u8 kind then: DNS (1) u16 id, u8 QR << 7 | rcode, u16 question type, u16 answer count, u8 name length, u8 longest label, u32 name hash, u32 domain hash; ICMP (2) u8 type, u8 code, u8 flags (1: quotes a packet, 2: IPv6), then if it quotes one: u8 protocol, source and destination (4 or 16 each), u16 ports ×2; HTTP request (3) u8 method; HTTP response (4) u16 status; DHCP (5) u8 message type (2 offer, 5 ack), the address it gives (4), u8 prefix length of the subnet mask (1 to 32), router (4, 0.0.0.0 if none) |
| 2 | payload length (the real one) |
| 2 | stored length |
| 1 | flags: 0x01 = compressed |
| stored length | the stored bytes, copied as they are |
Example
A capture from the anomaly lab (tools/lab/samples/errors-dns.nto), saved again with -A -F ipv4src 192.0.2.10, portsrc 53,, -o: only the answers of the lab's DNS server are kept, so the first record is an NXDOMAIN with its application layer.
00000000: 4e4f 5452 1172 312e 3033 2e30 307e 4c69 NOTR.r1.03.00~Li
00000010: 6e75 787e 0000 0023 0002 0100 c000 020a nux~...#........
00000020: 0400 3500 0000 0502 6c6f 007f 0000 0104 ..5.....lo......
00000030: 6574 6830 00c0 0002 fa02 6c6f 0100 0000 eth0......lo....
00000040: 0000 0000 0000 0000 0000 0000 0104 6574 ..............et
00000050: 6830 0120 010d b800 0000 0100 0000 0000 h0. ............
00000060: 0002 5004 6574 6830 01fe 8000 0000 0000 ..P.eth0........
00000070: 0002 005e fffe 0053 fa00 0000 0100 c000 ...^...S........
00000080: 020a 0f77 7777 2e65 7861 6d70 6c65 2e63 ...www.example.c
00000090: 6f6d 0002 0204 6574 6830 0087 0800 0000 om....eth0......
000000a0: 3595 7500 065d c186 7d26 8004 0000 5e00 5.u..]..}&....^.
000000b0: 5320 0000 5e00 5310 0800 1400 3e00 0040 S ..^.S.....>..@
000000c0: 11c0 0002 0ac0 0002 1400 3595 7500 2a84 ..........5.u.*.
000000d0: 5a01 de67 8300 0100 0010 0ccf f7ad 55cf Z..g..........U.
000000e0: f7ad 5500 2a00 2a00 0035 9575 002a 845a ..U.*.*..5.u.*.Z
000000f0: de67 8183 0001 0000 0000 0000 0c71 3172 .g...........q1r
00000100: 3270 7a63 7870 7135 3603 636f 6d00 0001 2pzcxpq56.com...
| Offset | Bytes | Meaning |
|---|---|---|
00 |
4e 4f 54 52 |
NOTR |
04 |
11 |
format v17 |
05 |
72 31 ... 7e |
r1.03.00~ |
0e |
4c 69 6e 75 78 7e |
Linux~ |
14 |
00 00 00 23 |
35 packets |
18 |
00 02 01 |
2 filters, AND mode: |
1b |
00 c0 00 02 0a |
ipv4src 192.0.2.10 |
20 |
04 00 35 |
portsrc 53 |
23 |
00 00 |
no renaming |
25 |
00 05 |
5 own addresses: |
27 |
02 6c 6f 00 7f 00 00 01 |
lo, IPv4, 127.0.0.1 |
2f |
04 65 74 68 30 00 c0 00 02 fa |
eth0, IPv4, 192.0.2.250 |
| ... | lo ::1, eth0 2001:db8:0:1::250, eth0 fe80::200:5eff:fe00:53fa |
|
79 |
00 00 |
no reverse DNS answer |
7b |
00 01 |
1 hostname: |
7d |
00 c0 00 02 0a 0f www.example.com |
192.0.2.10 is www.example.com |
92 |
00 02 02 04 eth0 |
record 1: interface 2, kind 2, eth0 |
9a |
00 87 |
Ethernet link; layers 0x87 = Ethernet + IPv4 + UDP + L7 |
9c |
08 00 00 00 35 95 75 |
IPv4, UDP, ports 53 -> 38261 |
a3 |
00 06 5d c1 86 7d 26 80 |
captured at 1791935626684032 µs: 2026-10-13 23:53:46.684032 UTC |
ab |
04 |
payload class 4: DNS |
ac |
00 00 5e 00 53 20 00 00 5e 00 53 10 08 00 |
Ethernet: to 00:00:5e:00:53:20, from 00:00:5e:00:53:10 |
ba |
14 00 3e 00 00 40 11 c0 00 02 0a c0 00 02 14 |
IPv4: 20-byte header, 62 bytes, TTL 64, UDP, 192.0.2.10 -> 192.0.2.20 |
c9 |
00 35 95 75 00 2a 84 5a |
UDP: ports, length 42, checksum |
d1 |
01 de 67 83 00 01 00 00 10 0c cf f7 ad 55 cf f7 ad 55 |
DNS: id 56935, response (0x80) NXDOMAIN (3), question A, 0 answers, a 16-char name whose longest label has 12, its hash and its domain's (the same: the name is a domain, q1r2pzcxpq56.com) |
e3 |
00 2a 00 2a 00 |
payload 42 bytes, 42 stored, not compressed |
e8 |
00 35 95 75 ... |
the payload: UDP header, then the DNS message |
Loading rules
load_packets() (save_load.c) reads the whole file, then:
- Type: the file must be a regular file (a FIFO is refused instead of blocking) and start with
NOTR. - Version: only the current format, v17, is read. Anything else gives
unsupported save format vN (this NetOtter reads v17 only). This was chosen for release r1, and kept since: a clean base rather than compatibility code, so the loader has noversion >= ...branches. v14 added each packet's capture time, v15 the application facts (SAVE_HAS_L7), v16 the payload class, v17 the DHCP facts. - Bounds: every read is bounds-checked (
save_rbuf_t.err). A file cut short, or with bytes after the last record, is corrupted. - Payloads: they're added as they are (
store_add()), never decompressed. - Sections:
- The filters become active (
filters_append, no toggling);-f/-Ftoggle on top, and-Aoverrides the mode. - The renamings are restored (
rename_set). - The own addresses are merged as saved ones (
local_add(..., saved = 1)); an address that's still live stays live. - The answers enter the store as
RA_SAVE, never written to the cache file. - The hostnames enter the host map as passive names.
- The filters become active (
- Budget: packets go through
packets_add(), so a file past the RAM budget loads up to it.
Changing the format
- Change
put_*andget_*together, and the description insave_format.h. - Bump
SAVE_FORMAT_VERSIONinnetotter.h. - Decide whether older files must still be read. Today's answer is no; adding support means version branches in the loader.
- New filter types go at the end of
enum FILTER_TYPE, because types are saved by number.
Files are opened with O_BINARY, which is 0 on Linux. Without it, Windows would translate \r\n and stop reading at the first 0x1A byte.
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