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:
@ -53,6 +53,8 @@ type ConnectionFlow struct {
|
||||
IPMeta api.IPMeta
|
||||
TCPMeta api.TCPMeta
|
||||
HelloBuffer []byte
|
||||
NextSeq uint32 // Expected next TCP sequence number for reassembly
|
||||
SeqInit bool // Whether NextSeq has been initialized
|
||||
}
|
||||
|
||||
// ParserImpl implements the api.Parser interface for TLS parsing
|
||||
@ -264,25 +266,39 @@ func (p *ParserImpl) Process(pkt api.RawPacket) (*api.TLSClientHello, error) {
|
||||
return nil, nil // Source IP is excluded
|
||||
}
|
||||
|
||||
key := flowKey(srcIP, srcPort, dstIP, dstPort)
|
||||
|
||||
// Handle SYN packets: create flow and store IP/TCP metadata from SYN
|
||||
// SYN is the only packet that carries TCP options (MSS, WindowScale, SACK, etc.)
|
||||
if tcp.SYN && !tcp.ACK {
|
||||
flow := p.getOrCreateFlow(key, srcIP, srcPort, dstIP, dstPort, ipMeta, tcpMeta)
|
||||
if flow != nil {
|
||||
flow.mu.Lock()
|
||||
// SYN consumes 1 sequence number, so data starts at Seq+1
|
||||
flow.NextSeq = tcp.Seq + 1
|
||||
flow.SeqInit = true
|
||||
flow.mu.Unlock()
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Get TCP payload (TLS data)
|
||||
payload := tcp.Payload
|
||||
if len(payload) == 0 {
|
||||
return nil, nil // No payload
|
||||
return nil, nil // No payload (ACK, FIN, etc.)
|
||||
}
|
||||
|
||||
key := flowKey(srcIP, srcPort, dstIP, dstPort)
|
||||
|
||||
// Check if flow exists before acquiring write lock
|
||||
p.mu.RLock()
|
||||
flow, flowExists := p.flows[key]
|
||||
_, flowExists := p.flows[key]
|
||||
p.mu.RUnlock()
|
||||
|
||||
// Early exit for non-ClientHello first packet
|
||||
// Early exit for non-ClientHello first packet (no SYN seen, no TLS handshake)
|
||||
if !flowExists && payload[0] != 22 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
flow = p.getOrCreateFlow(key, srcIP, srcPort, dstIP, dstPort, ipMeta, tcpMeta)
|
||||
flow := p.getOrCreateFlow(key, srcIP, srcPort, dstIP, dstPort, ipMeta, tcpMeta)
|
||||
if flow == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@ -296,6 +312,26 @@ func (p *ParserImpl) Process(pkt api.RawPacket) (*api.TLSClientHello, error) {
|
||||
return nil, nil // Already processed this flow
|
||||
}
|
||||
|
||||
// TCP sequence tracking: detect retransmissions and maintain order
|
||||
seq := tcp.Seq
|
||||
if flow.SeqInit {
|
||||
if seq < flow.NextSeq {
|
||||
// Retransmission — skip duplicate data
|
||||
return nil, nil
|
||||
}
|
||||
if seq > flow.NextSeq && flow.State == WAIT_CLIENT_HELLO {
|
||||
// Gap detected — missing segment, drop this flow
|
||||
p.mu.Lock()
|
||||
delete(p.flows, key)
|
||||
p.mu.Unlock()
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Update expected next sequence number
|
||||
flow.NextSeq = seq + uint32(len(payload))
|
||||
flow.SeqInit = true
|
||||
|
||||
// Check if this is a TLS ClientHello
|
||||
clientHello, err := parseClientHello(payload)
|
||||
if err != nil {
|
||||
@ -313,14 +349,15 @@ func (p *ParserImpl) Process(pkt api.RawPacket) (*api.TLSClientHello, error) {
|
||||
// Generate ConnID from flow key
|
||||
connID := key
|
||||
|
||||
// Use flow metadata (captured from SYN) for accurate IP/TCP fingerprinting
|
||||
ch := &api.TLSClientHello{
|
||||
SrcIP: srcIP,
|
||||
SrcPort: srcPort,
|
||||
DstIP: dstIP,
|
||||
DstPort: dstPort,
|
||||
Payload: clientHello,
|
||||
IPMeta: ipMeta,
|
||||
TCPMeta: tcpMeta,
|
||||
IPMeta: flow.IPMeta,
|
||||
TCPMeta: flow.TCPMeta,
|
||||
ConnID: connID,
|
||||
SNI: extInfo.SNI,
|
||||
ALPN: joinStringSlice(extInfo.ALPN, ","),
|
||||
@ -366,14 +403,15 @@ func (p *ParserImpl) Process(pkt api.RawPacket) (*api.TLSClientHello, error) {
|
||||
// Generate ConnID from flow key
|
||||
connID := key
|
||||
|
||||
// Use flow metadata (captured from SYN) for accurate IP/TCP fingerprinting
|
||||
ch := &api.TLSClientHello{
|
||||
SrcIP: srcIP,
|
||||
SrcPort: srcPort,
|
||||
DstIP: dstIP,
|
||||
DstPort: dstPort,
|
||||
Payload: clientHello,
|
||||
IPMeta: ipMeta,
|
||||
TCPMeta: tcpMeta,
|
||||
IPMeta: flow.IPMeta,
|
||||
TCPMeta: flow.TCPMeta,
|
||||
ConnID: connID,
|
||||
SNI: extInfo.SNI,
|
||||
ALPN: joinStringSlice(extInfo.ALPN, ","),
|
||||
@ -587,6 +625,12 @@ func extractTLSExtensions(payload []byte) (*TLSExtensionInfo, error) {
|
||||
|
||||
// Use tlsfingerprint to parse ALPN and TLS version
|
||||
fp, err := tlsfingerprint.ParseClientHello(payload)
|
||||
if err != nil {
|
||||
// Retry with sanitized payload (handles truncated/malformed extensions)
|
||||
if sanitized := sanitizeTLSRecord(payload); sanitized != nil {
|
||||
fp, err = tlsfingerprint.ParseClientHello(sanitized)
|
||||
}
|
||||
}
|
||||
if err == nil && fp != nil {
|
||||
// Extract ALPN protocols
|
||||
if len(fp.ALPNProtocols) > 0 {
|
||||
@ -757,3 +801,84 @@ func joinStringSlice(slice []string, sep string) string {
|
||||
}
|
||||
return strings.Join(slice, sep)
|
||||
}
|
||||
|
||||
// sanitizeTLSRecord attempts to fix a TLS ClientHello with truncated extensions
|
||||
// by adjusting lengths to cover only complete extensions. Returns a corrected
|
||||
// copy of the record, or nil if no fix is needed or possible.
|
||||
func sanitizeTLSRecord(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 version + random + session ID + cipher suites + compression methods
|
||||
offset := 2 + 32
|
||||
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
|
||||
if len(hello) < offset+1 {
|
||||
return nil
|
||||
}
|
||||
offset += 1 + int(hello[offset]) // compression methods
|
||||
if len(hello) < offset+2 {
|
||||
return nil
|
||||
}
|
||||
extLenOffset := offset
|
||||
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 to find the last complete one
|
||||
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 // truncated extension
|
||||
}
|
||||
pos += 4 + extBodyLen
|
||||
validLen = pos
|
||||
}
|
||||
|
||||
if validLen == declaredExtLen {
|
||||
return nil // no truncation, nothing to fix
|
||||
}
|
||||
|
||||
fixed := make([]byte, len(data))
|
||||
copy(fixed, data)
|
||||
diff := declaredExtLen - validLen
|
||||
extLenAbs := 5 + 4 + extLenOffset
|
||||
binary.BigEndian.PutUint16(fixed[extLenAbs:], uint16(validLen))
|
||||
newHelloLen := helloLen - diff
|
||||
fixed[5+1] = byte(newHelloLen >> 16)
|
||||
fixed[5+2] = byte(newHelloLen >> 8)
|
||||
fixed[5+3] = byte(newHelloLen)
|
||||
newRecordLen := recordLen - diff
|
||||
binary.BigEndian.PutUint16(fixed[3:5], uint16(newRecordLen))
|
||||
return fixed[:5+newRecordLen]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user