Most web traffic is TLS now, which for a passive analyzer means most of the interesting bytes are gone. One field survives: the Server Name Indication in the ClientHello. It is sent in cleartext, in every TLS version, before encryption starts, and it answers the question you usually have - what was this connection to?

Getting it out means parsing four levels of nested, attacker-supplied length fields. It is the most structurally complex hand-rolled parser in packeteer, and the one I trusted least.

The structure

To reach the SNI you walk:

TLS record          length -> handshake message
  handshake message length -> ClientHello body
    ClientHello       fixed fields, then session id, cipher suites,
                      compression methods, each length-prefixed
      extensions block  length -> list of extensions
        extension       type + length -> SNI extension body
          server name list  length -> entries
            entry           type + length -> the host name

Every one of those lengths is a claim by the sender. Six or seven levels depending on how you count, each nested inside the last, and a parser that checks the outer ones and then trusts the inner ones has checked nothing - the inner ones are exactly where a hostile ClientHello would put its lie.

What “bounds-checked at every level” means in practice

Two rules, applied at each nesting step:

  1. The claimed length must fit inside the remaining bytes of the actual buffer, not inside what the enclosing header said it would be.
  2. The claimed length must fit inside its parent’s claimed extent. An extension declaring 400 bytes inside a 40-byte extensions block is rejected at the level that can see both numbers, not later when a read fails.

The second is the one that is easy to leave out, because it does not cause a memory error. Without it you get a parser that reads within its buffer and produces nonsense: an SNI assembled from bytes belonging to some later structure. A wrong host name in an analyzer output is worse than none, because nothing about it looks wrong.

Verifying it against traffic that was not mine

Unit tests for a parser like this are written by the person who wrote the parser, and they encode the same assumptions. So the dissector was checked two other ways.

Real, unsolicited traffic. Capture on wlp1s0 - the actual wireless interface, not loopback, not a synthetic frame written to make the test pass - and pull an SNI out of a genuine ClientHello from whatever the machine happened to be talking to. Traffic nobody constructed for the test is the only kind that can surprise you.

25.7 million fuzz executions. The most of any harness in the project, which is a direct consequence of the nesting: the number of distinct structural shapes to explore grows with depth, so a parser five levels deep needs proportionally more search than a flat one to reach the same confidence. No crashes.

What did not get built

The dissector stops at the ClientHello and the ServerHello’s version and cipher selection. There is no decryption, and there will not be.

That is a scope boundary the project holds everywhere - QUIC stops at cleartext framing with no HKDF or AEAD for Initial packets, SNMP stops before v3’s USM - and it is what makes the SNI extraction worth having. “Here is the server name, read off the wire” is a fact. “Here is the plaintext, assuming the keys you gave me are right” is a different tool with a different threat model, and mixing them into one output would make both less trustworthy.

The transferable bit

If you are writing a parser for anything with nested length prefixes - TLS, ASN.1, a font table, an archive index - the depth is the risk. Each level multiplies the ways the levels above it can be made to lie, and the checks that matter most are the relational ones between a child and its parent, not the absolute ones against the buffer.

And you cannot review your way there. The inputs that break a five-deep parser are not inputs a person thinks to write down.