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
- 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.
- 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.
- Verify the `Vec::from_hex("1603...")` string is intact (record type 0x16, length 0x86) and was not truncated during edits.
- 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
- Make parse_client_hello return a detailed error (offset + reason) during development
- Add sub-step unit tests: record header, handshake header, each extension
- Re-run fingerprint fixtures after any parser strictness change
- Verify test hex fixtures are not truncated when edited
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)