stamparm/maltrail · warning · QuicParseError

packet too short for header-protection sample

Error message

packet too short for header-protection sample

What it means

During QUIC Initial SNI extraction, the code needs a 16-byte header-protection sample starting at sample_offset (packet-number offset + 4). The packet's payload `p` is too short to supply those 16 bytes, so _extract_sni_impl raises QuicParseError.

Solutions

  1. Increase capture snapshot size (e.g. tcpdump -s0) so UDP payloads aren't truncated
  2. Catch QuicParseError in extract_sni_from_quic_initial callers and skip the packet
  3. Check length bounds after reading each varint before computing sample_offset
  4. Filter to only valid QUIC Initial long-header packets before parsing

Example fix

// before
sni = extract_sni_from_quic_initial(udp_payload)
// after
try:
    sni = extract_sni_from_quic_initial(udp_payload)
except QuicParseError:
    sni = None  # malformed/short packet
Defensive patterns

Strategy: try-catch

Validate before calling

def quic_initial_plausible(udp_payload):
    return udp_payload and len(udp_payload) > 6 and (udp_payload[0] & 0x80) and len(udp_payload) >= 1200  # typical Initial size

Try / catch

try:
    sni = extract_sni_from_quic_initial(payload)
except QuicParseError:
    sni = None  # skip malformed packet

Prevention

When it happens

Trigger: UDP payload shorter than the required varint-decoded offsets allow; coalesced/fragmented packets; crafted or corrupt QUIC Initial packets; non-Initial packets routed into the parser.

Common situations: Capturing truncated packets (snapshot length too small in tcpdump), feeding TCP-reassembled or padded junk into extract_sni_from_quic_initial, fuzz/malicious traffic.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of stamparm/maltrail@77cfb06d76 (2026-09-13). Data as JSON: /api/errors/981f1b1f132cb3e8. Report an issue: GitHub.

Appendix: source

Thrown at core/quic_sni.py:288

    off = 5
    dcid_len = p[off]; off += 1
    dcid = bytes(udp_payload[off:off + dcid_len]); off += dcid_len
    scid_len = p[off]; off += 1
    off += scid_len
    # long header packet type must be Initial
    if ver_kind == 1:
        if (first & 0x30) != 0x00:
            return None
    else:
        if (first & 0x30) != 0x10:                            # v2 remaps Initial to 0b01
            return None
    token_len, off = _read_varint(udp_payload, off)
    off += token_len
    length, off = _read_varint(udp_payload, off)             # length of (pn + payload)
    pn_offset = off
    sample_offset = pn_offset + 4
    if sample_offset + 16 > len(p):
        raise QuicParseError("packet too short for header-protection sample")

    key, iv, hp = derive_client_initial_keys(dcid, ver_kind)

    sample = bytes(udp_payload[sample_offset:sample_offset + 16])
    mask = _b(aes_ecb_block(hp, sample))

    first_unmasked = first ^ (mask[0] & 0x0F)
    pn_len = (first_unmasked & 0x03) + 1
    pn_bytes = bytearray(udp_payload[pn_offset:pn_offset + pn_len])
    for i in range(pn_len):
        pn_bytes[i] ^= mask[1 + i]
    packet_number = 0
    for bb in pn_bytes:
        packet_number = (packet_number << 8) | bb

    payload_offset = pn_offset + pn_len
    payload_len = length - pn_len
    ciphertext = bytes(udp_payload[payload_offset:payload_offset + payload_len])

View on GitHub (pinned to 77cfb06d76)