stamparm/maltrail · warning

well-formed hello parses

Error message

well-formed hello parses

What it means

A test `expect` on `parse_client_hello`: the test feeds a fixed hex-encoded TLS ClientHello record and asserts the parser returns `Some`, panicking with 'well-formed hello parses' otherwise. Since the input is hand-verified well-formed, a panic means the TLS ClientHello parser regressed — it now rejects a record it must accept.

Solutions

  1. Dump the parse failure point: make `parse_client_hello` return a detailed error (or add temp tracing) and find which field/extension guard rejects r1.
  2. Check recently added bounds/length guards — the r1 record's extensions end at a specific offset; an off-by-one in extension-length handling commonly rejects it.
  3. Verify the `Vec::from_hex("1603...")` string is intact (record type 0x16, length 0x86) and was not truncated during edits.
  4. Add unit tests per parse sub-step (record header, handshake header, extensions) so regressions point at the failing stage instead of a bare expect.

Example fix

// before
let ch1 = parse_client_hello(&r1).expect("well-formed hello parses");
// after (diagnose on failure)
let ch1 = parse_client_hello(&r1)
    .unwrap_or_else(|| panic!("parse_client_hello rejected well-formed hello: {:02x?}", &r1[..48]));
Defensive patterns

Strategy: validation

Validate before calling

// sanity-check the fixture before parsing
assert_eq!(&r1[..1], &0x16u8, "not a handshake record");
let ch1 = parse_client_hello(&r1).expect("well-formed hello parses");

Try / catch

let ch1 = parse_client_hello(&r1)
    .unwrap_or_else(|| panic!("parse failed for {:02x?}", &r1[..64]));

Prevention

When it happens

Trigger: Running `ja3_ja4_match_the_python_reference_implementation` after parser changes makes `parse_client_hello(&r1)` return `None` for hello 1 (TLS 1.3-style stack with SNI, ALPN, supported_versions, no GREASE). Any newly added strictness — extension bounds checks, version parsing, GREASE handling, record-layer length checks — that misfires on this input triggers the panic.

Common situations: Refactoring the TLS parser's offset/length arithmetic; tightening bounds guards after fuzz fixes; changing JA3/JA4 assembly so an earlier parse step fails; input hex accidentally truncated in a copy-paste edit.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

Thrown at sensor/src/protocols/tls.rs:508

        // an SNI list whose first entry is not host_name(0) must not yield a bogus name
        let mut srv = vec![0x02, 0x00, 0x03];
        srv.extend_from_slice(b"abc");
        let mut lst = (srv.len() as u16).to_be_bytes().to_vec();
        lst.extend_from_slice(&srv);
        assert_eq!(parse_sni_extension(&lst, true), Ok(None));
    }

    /// Vectors generated by `core/tls_intel.py:parse_client_hello()` over these exact bytes -
    /// the cross-language contract the trail matching depends on. Regenerate with:
    ///
    ///   python3 -c "import sys; sys.path.insert(0,'.'); \
    ///     from core.tls_intel import parse_client_hello as p; \
    ///     h=bytes.fromhex('<R1HEX>'); o=p(h); print(o['sni'],o['ja3'],o['ja4'])"
    #[test]
    fn ja3_ja4_match_the_python_reference_implementation() {
        // hello 1: TLS 1.3-ish stack, SNI + ALPN + supported_versions, no GREASE
        let r1 = Vec::from_hex("1603010086010000820303111111111111111111111111111111111111111111111111111111111111111100000c13011302c02bc02f009c009e0100004d000a00080006001d00170018000d000800060403080404010010000e000c02683208687474702f312e31002b000504030403030000001600140000116d61696c2e6576696c2e6578616d706c65");
        let ch1 = parse_client_hello(&r1).expect("well-formed hello parses");
        assert_eq!(ch1.sni.as_deref(), Some("mail.evil.example"));
        assert_eq!(ch1.ja3, "d190f828263095de150a20b19136e314");
        assert_eq!(ch1.ja4, "t13d0605h2_72b63408b255_beb9f91c6f80");

        // hello 2: GREASE ciphers/extensions dropped from the fingerprint, EC point formats
        // carried into JA3's last field, no SNI ('i'), no ALPN ("00"), legacy version only
        let r2 = Vec::from_hex("16030100500100004c030322222222222222222222222222222222222222222222222222222222222222220000081a1a13013a3ac02f0100001b000b0003020001000a000600041a1a001d000d0006000408040401");
        let ch2 = parse_client_hello(&r2).expect("well-formed hello parses");
        assert_eq!(ch2.sni, None);
        assert_eq!(ch2.ja3, "a20735de562085796a564839bc8368cc");
        assert_eq!(ch2.ja4, "t12i020300_c1929292aa6b_7c9dbb57f4ec");

        // and the SNI-only helper still agrees with the full parse on both
        assert_eq!(client_hello_sni(&r1), ch1.sni);
        assert_eq!(client_hello_sni(&r2), None);
    }

    #[test]

View on GitHub (pinned to 77cfb06d76)