release: version 1.1.15 - Fix ALPN detection for malformed TLS extensions
Some checks failed
Build RPM Package / Build RPM Packages (CentOS 7, Rocky 8/9/10) (push) Has been cancelled
Some checks failed
Build RPM Package / Build RPM Packages (CentOS 7, Rocky 8/9/10) (push) Has been cancelled
- FIX: ALPN (tls_alpn) not appearing in logs for packets with truncated extensions - Add sanitizeTLSRecord fallback in extractTLSExtensions (tlsparse/parser.go) - Mirrors sanitization already present in fingerprint/engine.go - ALPN now correctly extracted even when ParseClientHello fails on raw payload - Bump version to 1.1.15 in main.go and packaging/rpm/ja4sentinel.spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@ -2,6 +2,7 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"ja4sentinel/api"
|
||||
@ -29,8 +30,14 @@ func (e *EngineImpl) FromClientHello(ch api.TLSClientHello) (*api.Fingerprints,
|
||||
// Parse the ClientHello using tlsfingerprint
|
||||
fp, err := tlsfingerprint.ParseClientHello(ch.Payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse ClientHello from %s:%d -> %s:%d (conn_id=%s, payload_len=%d): %w",
|
||||
ch.SrcIP, ch.SrcPort, ch.DstIP, ch.DstPort, ch.ConnID, len(ch.Payload), err)
|
||||
// Try to sanitize truncated extensions and retry
|
||||
if sanitized := sanitizeClientHelloExtensions(ch.Payload); sanitized != nil {
|
||||
fp, err = tlsfingerprint.ParseClientHello(sanitized)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse ClientHello from %s:%d -> %s:%d (conn_id=%s, payload_len=%d): %w",
|
||||
ch.SrcIP, ch.SrcPort, ch.DstIP, ch.DstPort, ch.ConnID, len(ch.Payload), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate JA4 fingerprint
|
||||
@ -67,3 +74,93 @@ func extractJA4Hash(ja4 string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// sanitizeClientHelloExtensions fixes ClientHellos with truncated extension data
|
||||
// by adjusting the extensions length to include only complete extensions.
|
||||
// Returns a corrected copy, or nil if the payload cannot be fixed.
|
||||
func sanitizeClientHelloExtensions(data []byte) []byte {
|
||||
if len(data) < 5 || data[0] != 0x16 {
|
||||
return nil
|
||||
}
|
||||
recordLen := int(data[3])<<8 | int(data[4])
|
||||
if len(data) < 5+recordLen {
|
||||
return nil
|
||||
}
|
||||
payload := data[5 : 5+recordLen]
|
||||
if len(payload) < 4 || payload[0] != 0x01 {
|
||||
return nil
|
||||
}
|
||||
helloLen := int(payload[1])<<16 | int(payload[2])<<8 | int(payload[3])
|
||||
if len(payload) < 4+helloLen {
|
||||
return nil
|
||||
}
|
||||
hello := payload[4 : 4+helloLen]
|
||||
|
||||
// Skip through ClientHello fields to reach extensions
|
||||
offset := 2 + 32 // version + random
|
||||
if len(hello) < offset+1 {
|
||||
return nil
|
||||
}
|
||||
offset += 1 + int(hello[offset]) // session ID
|
||||
if len(hello) < offset+2 {
|
||||
return nil
|
||||
}
|
||||
csLen := int(hello[offset])<<8 | int(hello[offset+1])
|
||||
offset += 2 + csLen // cipher suites
|
||||
if len(hello) < offset+1 {
|
||||
return nil
|
||||
}
|
||||
offset += 1 + int(hello[offset]) // compression methods
|
||||
if len(hello) < offset+2 {
|
||||
return nil
|
||||
}
|
||||
extLenOffset := offset // position of extensions length field
|
||||
declaredExtLen := int(hello[offset])<<8 | int(hello[offset+1])
|
||||
offset += 2
|
||||
extStart := offset
|
||||
|
||||
if len(hello) < extStart+declaredExtLen {
|
||||
return nil
|
||||
}
|
||||
extData := hello[extStart : extStart+declaredExtLen]
|
||||
|
||||
// Walk extensions, find how many complete ones exist
|
||||
validLen := 0
|
||||
pos := 0
|
||||
for pos < len(extData) {
|
||||
if pos+4 > len(extData) {
|
||||
break
|
||||
}
|
||||
extBodyLen := int(extData[pos+2])<<8 | int(extData[pos+3])
|
||||
if pos+4+extBodyLen > len(extData) {
|
||||
break // this extension is truncated
|
||||
}
|
||||
pos += 4 + extBodyLen
|
||||
validLen = pos
|
||||
}
|
||||
|
||||
if validLen == declaredExtLen {
|
||||
return nil // no truncation found, nothing to fix
|
||||
}
|
||||
|
||||
// Build a corrected copy with adjusted extensions length
|
||||
fixed := make([]byte, len(data))
|
||||
copy(fixed, data)
|
||||
|
||||
// Absolute offset of extensions length field within data
|
||||
extLenAbs := 5 + 4 + extLenOffset
|
||||
diff := declaredExtLen - validLen
|
||||
|
||||
// Update extensions length
|
||||
binary.BigEndian.PutUint16(fixed[extLenAbs:], uint16(validLen))
|
||||
// Update ClientHello handshake length
|
||||
newHelloLen := helloLen - diff
|
||||
fixed[5+1] = byte(newHelloLen >> 16)
|
||||
fixed[5+2] = byte(newHelloLen >> 8)
|
||||
fixed[5+3] = byte(newHelloLen)
|
||||
// Update TLS record length
|
||||
newRecordLen := recordLen - diff
|
||||
binary.BigEndian.PutUint16(fixed[3:5], uint16(newRecordLen))
|
||||
|
||||
return fixed[:5+newRecordLen]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user