3 Save Format
Watersrc-stack edited this page 2026-10-22 23:53:48 +02:00
This file contains ambiguous Unicode characters

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 no version >= ... 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 / -F toggle on top, and -A overrides 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.
  • Budget: packets go through packets_add(), so a file past the RAM budget loads up to it.

Changing the format

  1. Change put_* and get_* together, and the description in save_format.h.
  2. Bump SAVE_FORMAT_VERSION in netotter.h.
  3. Decide whether older files must still be read. Today's answer is no; adding support means version branches in the loader.
  4. 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.