stenotype(1)

πŸ’– stenotype(1) β€” the manual page, but make it make sense πŸ’–

okay bestie, sit down, hydrate, because today we're talking about stenotype from the stenographer fam (she's on version 1.0.1, icon behaviour) and honestly? she's that girl. no cap.


✨ NAME ✨

stenotype β€” dump raw packets to disk. that's it. that's the whole job. she dumps packets, she goes home, she doesn't text back. πŸ’…


🌸 SYNOPSIS 🌸

stenotype [-qv?] [--aiops=NUM] [--blocks=NUM] [--count=NUM]
          [--dir=STRING] [--fanout_id=NUM] [--fanout_type=NUM]
          [--fileage_sec=NUM] [--filesize_mb=NUM] [--filter=STRING]
          [--gid=NUM] [--iface=STRING] [--index_nicelevel=NUM] [--no_index]
          [--no_watchdogs] [--preallocate_file_mb=NUM] [--seccomp=STRING]
          [--threads=NUM] [--uid=NUM] [--help] [--usage]

like yeah she's got a lot of flags, she's high maintenance in the most productive way possible 🫢


πŸ¦‹ DESCRIPTION πŸ¦‹

so stenotype is a whole mechanism for dumping raw packets to disk fast as hell. her whole philosophy? keep the interface simple β€” and like, no file rotation, because that's literally left as an exercise for the reader 😭 (yes fr, go build it yourself bestie, you got this).

but simple interface β‰  weak tool. she's very powerful. like, scary powerful. main character energy.

stenotype runs a NIC β†’ disk pipeline that's specifically engineered to push output to disk as fast as physically possible while only ever using the kernel's own built-in mechanisms. no extra middlemen, no drama, no copy-paste layers. periodt.

  1. NIC β†’ RAM: stenotype uses MMAP'd AF_PACKET with 1MB blocks and a high timeout, so it can offload the actual writing of packets β€” and deciding how they're laid out β€” to the kernel. the kernel stuffs every packet it can into 1MB, then taps the userspace process on the shoulder like "hey, there's a block available in the MMAP'd ring buffer ✨". and the best part? it guarantees no overruns (meaning no packet is ever split across the 1MB boundary, thank u) plus nice alignment to memory pages. we love a considerate kernel.
  2. RAM β†’ Disk: since the kernel already handed us one single 1MB block of packets that's all nicely aligned, we can just O_DIRECT write it straight to disk. no extra copying, no kernel buffering, no nonsense. and to keep sequential reads going strong, all disk IO happens asynchronously via io_submit (which, btw, works specifically for O_DIRECT files... joy! πŸŽ€). and because the write is async, we use that waiting-around time to do our own in-memory processing and indexing. multitasking queen πŸ‘‘

there are N async IO operations available, and N is flag-specified (--aiops, we'll get there). once we've used up all N of them, we block until one that's already in use finishes, then reuse it. recycling!! sustainable!! πŸ’š

the whole pipeline, step by step, for my visual learners:

  • kernel gives userspace a 1MB block of packets
  • userspace iterates over packets in block, updates any indexes
  • userspace starts async IO operation to write block to disk
  • after N async IO operations are submitted, we synchronously wait for the least recent one to finish.
  • when an async IO operation finishes, we release the 1MB block back to the kernel to write more packets.

it's giving assembly line but make it packets. obsessed.


πŸ’œ OPTIONS πŸ’œ

  • --aiops=NUM β€” max number of async IO operations. (this is the N from the pipeline lore 🫢)
  • --blocks=NUM β€” total number of blocks to use, and remember: each one is 1MB.
  • --count=NUM β€” total number of packets to read. set it to -1 to read forever and ever and ever (she's committed).
  • --dir=STRING β€” directory to store packet files in. tell her where to drop the goods.
  • --fanout_id=NUM β€” if you're fanning out across processes, set this.
  • --fanout_type=NUM β€” TPACKET_V3 fanout type to fanout packets.
  • --fileage_sec=NUM β€” files older than this many secs get rotated.
  • --filesize_mb=NUM β€” max file size in MB before the file gets rotated.
  • --filter=STRING β€” a BPF compiled filter used to pick which packets get captured. important: this has to be a compiled BPF in hexadecimal, which you can get from a human readable filter expression using the provided compile_bpf.sh script. (yes, hex. she doesn't do vibes-based filtering, sorry bestie 😭)
  • --gid=NUM β€” drop privileges to this group.
  • --iface=STRING β€” interface to read packets from. name the NIC, girl.
  • --index_nicelevel=NUM β€” nice level of indexing threads.
  • --no_index β€” do not compute or write indexes. (skipping the index arc entirely πŸ’€)
  • --no_watchdogs β€” don't start any watchdogs.
  • --preallocate_file_mb=NUM β€” when creating new files, preallocate to this many MB.
  • -q β€” quiet logging. each -q counteracts one -v, it's a whole tug of war πŸͺ’
  • --seccomp=STRING β€” seccomp style, one of 'none', 'trace', 'kill'. pick your fighter.
  • --threads=NUM β€” number of parallel threads to read packets with.
  • --uid=NUM β€” drop privileges to this user.
  • -v β€” verbose logging, and you can give it multiple times. more v's, more yapping πŸ—£οΈ
  • -?, --help β€” give this help list.
  • --usage β€” give a short usage message.
heads up bestie: -q and -v literally cancel each other out. one -v vs one -q is a fair fight. plan accordingly. βš–οΈ

okay and that's the whole page!! no boring parts skipped, every flag accounted for, she ate and left no crumbs πŸ”₯

this was stenographer version 1.0.1, dated 29 January 2026 β€” so like, recent. fresh. relevant. bye bestie, go dump some packets, you're gonna slay πŸ’…βœ¨

πŸ“¦ where this page comes from