{"record":{"id":"a3d1e72d80891926","repo":"stamparm/maltrail","slug":"well-formed-hello-parses","errorCode":null,"errorMessage":"well-formed hello parses","messagePattern":"well-formed hello parses","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"sensor/src/protocols/tls.rs","lineNumber":508,"sourceCode":"        // an SNI list whose first entry is not host_name(0) must not yield a bogus name\n        let mut srv = vec![0x02, 0x00, 0x03];\n        srv.extend_from_slice(b\"abc\");\n        let mut lst = (srv.len() as u16).to_be_bytes().to_vec();\n        lst.extend_from_slice(&srv);\n        assert_eq!(parse_sni_extension(&lst, true), Ok(None));\n    }\n\n    /// Vectors generated by `core/tls_intel.py:parse_client_hello()` over these exact bytes -\n    /// the cross-language contract the trail matching depends on. Regenerate with:\n    ///\n    ///   python3 -c \"import sys; sys.path.insert(0,'.'); \\\n    ///     from core.tls_intel import parse_client_hello as p; \\\n    ///     h=bytes.fromhex('<R1HEX>'); o=p(h); print(o['sni'],o['ja3'],o['ja4'])\"\n    #[test]\n    fn ja3_ja4_match_the_python_reference_implementation() {\n        // hello 1: TLS 1.3-ish stack, SNI + ALPN + supported_versions, no GREASE\n        let r1 = Vec::from_hex(\"1603010086010000820303111111111111111111111111111111111111111111111111111111111111111100000c13011302c02bc02f009c009e0100004d000a00080006001d00170018000d000800060403080404010010000e000c02683208687474702f312e31002b000504030403030000001600140000116d61696c2e6576696c2e6578616d706c65\");\n        let ch1 = parse_client_hello(&r1).expect(\"well-formed hello parses\");\n        assert_eq!(ch1.sni.as_deref(), Some(\"mail.evil.example\"));\n        assert_eq!(ch1.ja3, \"d190f828263095de150a20b19136e314\");\n        assert_eq!(ch1.ja4, \"t13d0605h2_72b63408b255_beb9f91c6f80\");\n\n        // hello 2: GREASE ciphers/extensions dropped from the fingerprint, EC point formats\n        // carried into JA3's last field, no SNI ('i'), no ALPN (\"00\"), legacy version only\n        let r2 = Vec::from_hex(\"16030100500100004c030322222222222222222222222222222222222222222222222222222222222222220000081a1a13013a3ac02f0100001b000b0003020001000a000600041a1a001d000d0006000408040401\");\n        let ch2 = parse_client_hello(&r2).expect(\"well-formed hello parses\");\n        assert_eq!(ch2.sni, None);\n        assert_eq!(ch2.ja3, \"a20735de562085796a564839bc8368cc\");\n        assert_eq!(ch2.ja4, \"t12i020300_c1929292aa6b_7c9dbb57f4ec\");\n\n        // and the SNI-only helper still agrees with the full parse on both\n        assert_eq!(client_hello_sni(&r1), ch1.sni);\n        assert_eq!(client_hello_sni(&r2), None);\n    }\n\n    #[test]","sourceCodeStart":490,"sourceCodeEnd":526,"githubUrl":"https://github.com/stamparm/maltrail/blob/77cfb06d7606506d101bbcec0786c77166c4255e/sensor/src/protocols/tls.rs#L490-L526","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet ch1 = parse_client_hello(&r1).expect(\"well-formed hello parses\");\n// after (diagnose on failure)\nlet ch1 = parse_client_hello(&r1)\n    .unwrap_or_else(|| panic!(\"parse_client_hello rejected well-formed hello: {:02x?}\", &r1[..48]));","handlingStrategy":"validation","validationCode":"// sanity-check the fixture before parsing\nassert_eq!(&r1[..1], &0x16u8, \"not a handshake record\");\nlet ch1 = parse_client_hello(&r1).expect(\"well-formed hello parses\");","typeGuard":null,"tryCatchPattern":"let ch1 = parse_client_hello(&r1)\n    .unwrap_or_else(|| panic!(\"parse failed for {:02x?}\", &r1[..64]));","preventionTips":["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"],"tags":["rust","test","tls","parser","ja3"],"backgroundTag":"internal-invariant-violation","analyzedSha":"77cfb06d7606506d101bbcec0786c77166c4255e","analyzedAt":"2026-09-13T03:50:16.010Z","contentChangedAt":"2026-09-13T03:50:16.010Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}