From f03994d594a6cd949815ef7e9ceea53bea53cee5 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Tue, 25 Nov 2025 16:55:57 -0500 Subject: [PATCH 1/4] add more safety checks for http2 detection --- pkg/ebpf/common/http2grpc_transform.go | 50 +++- pkg/ebpf/common/http2grpc_transform_test.go | 303 +++++++++++++++++--- 2 files changed, 300 insertions(+), 53 deletions(-) diff --git a/pkg/ebpf/common/http2grpc_transform.go b/pkg/ebpf/common/http2grpc_transform.go index 7cd020ce6b..bb1cc5697e 100644 --- a/pkg/ebpf/common/http2grpc_transform.go +++ b/pkg/ebpf/common/http2grpc_transform.go @@ -7,6 +7,7 @@ import ( "bytes" "encoding/binary" "errors" + "regexp" "strconv" "strings" "unsafe" @@ -34,6 +35,9 @@ const ( const initialHeaderTableSize = 4096 +var validPath = regexp.MustCompile(`^[A-Za-z0-9\-/._~]+$`) +var validContentType = regexp.MustCompile(`^[A-Za-z\-/\+]+$`) + type h2Connection struct { hdec *bhpack.Decoder hdecRet *bhpack.Decoder @@ -84,13 +88,41 @@ func protocolIsGRPC(activeGRPCConnections *lru.Cache[uint64, h2Connection], conn var commonHDec = bhpack.NewDecoder(0, nil) +func isHTTPOp(op string) bool { + return op == "GET" || op == "POST" || op == "PATCH" || op == "DELETE" || op == "OPTIONS" || op == "HEAD" +} + +func handleHeaderField(hf *bhpack.HeaderField) bool { + hfKey := strings.ToLower(hf.Name) + switch hfKey { + case ":method": + val := strings.ToUpper(hf.Value) + if isHTTPOp(val) { + return true + } + case ":path": + val := hf.Value + if pos := strings.Index(val, "?"); pos >= 0 { + val = val[:pos] + } + if validPath.MatchString(val) { + return true + } + case "content-type": + val := hf.Value + if validContentType.MatchString(val) { + return true + } + } + + return false +} + func knownFrameKeys(fr *http2.Framer, hf *http2.HeadersFrame) bool { - known := false + knownCount := 0 commonHDec.SetEmitFunc(func(hf bhpack.HeaderField) { - hfKey := strings.ToLower(hf.Name) - switch hfKey { - case ":method", ":path", "content-type", ":status", "grpc-status": - known = true + if handleHeaderField(&hf) { + knownCount++ } }) // Lose reference to MetaHeadersFrame: @@ -116,7 +148,7 @@ func knownFrameKeys(fr *http2.Framer, hf *http2.HeadersFrame) bool { frag = cf.HeaderBlockFragment() } - return known + return knownCount > 1 } func readMetaFrame(parseContext *EBPFParseContext, connID uint64, fr *http2.Framer, hf *http2.HeadersFrame) (string, string, string, bool) { @@ -358,8 +390,10 @@ func http2FromBuffers(parseContext *EBPFParseContext, event *BPFHTTP2Info) (requ if ff, ok := f.(*http2.HeadersFrame); ok { rok := false method, path, contentType, ok := readMetaFrame(parseContext, connID, framer, ff) - - if path == "" { + if pos := strings.Index(path, "?"); pos >= 0 { + path = path[:pos] + } + if path == "" || !validPath.MatchString(path) { path = "*" } diff --git a/pkg/ebpf/common/http2grpc_transform_test.go b/pkg/ebpf/common/http2grpc_transform_test.go index 5f56998294..806d6246bf 100644 --- a/pkg/ebpf/common/http2grpc_transform_test.go +++ b/pkg/ebpf/common/http2grpc_transform_test.go @@ -8,81 +8,94 @@ import ( "github.com/stretchr/testify/assert" "golang.org/x/net/http2" + + "go.opentelemetry.io/obi/pkg/internal/ebpf/bhpack" ) func TestHTTP2QuickDetection(t *testing.T) { tests := []struct { - name string - input []byte - inputLen int - expected bool + name string + input []byte + inputLen int + expected bool + expectedQuick bool }{ { - name: "Status instead of start", - input: []byte{0, 0, 29, 1, 4, 0, 0, 1, 101, 136, 224, 223, 222, 221, 97, 150, 223, 105, 126, 148, 19, 106, 101, 182, 165, 4, 1, 52, 160, 92, 184, 23, 174, 1, 197, 49, 104, 223, 0, 0, 44, 0, 0, 0, 0, 1, 101, 1, 0, 0, 0, 39, 31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 18, 98, 11, 14, 113, 12, 241, 116, 150, 98, 206, 79, 75, 83, 98, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - inputLen: 100, - expected: true, + name: "Status instead of start", + input: []byte{0, 0, 29, 1, 4, 0, 0, 1, 101, 136, 224, 223, 222, 221, 97, 150, 223, 105, 126, 148, 19, 106, 101, 182, 165, 4, 1, 52, 160, 92, 184, 23, 174, 1, 197, 49, 104, 223, 0, 0, 44, 0, 0, 0, 0, 1, 101, 1, 0, 0, 0, 39, 31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 18, 98, 11, 14, 113, 12, 241, 116, 150, 98, 206, 79, 75, 83, 98, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + inputLen: 100, + expectedQuick: true, + expected: false, }, { - name: "Empty", - input: []byte{}, - inputLen: 100, - expected: false, + name: "Empty", + input: []byte{}, + inputLen: 100, + expectedQuick: false, + expected: false, }, { - name: "Short", - input: []byte{0, 0, 70, 1, 4}, - inputLen: 3, - expected: false, + name: "Short", + input: []byte{0, 0, 70, 1, 4}, + inputLen: 3, + expectedQuick: false, + expected: false, }, { - name: "Regular HTTP2/gRPC Frame", - input: []byte{0, 0, 70, 1, 4, 0, 0, 0, 19, 204, 131, 4, 147, 96, 233, 45, 18, 22, 147, 175, 12, 155, 139, 103, 115, 16, 172, 98, 42, 97, 145, 31, 134, 126, 167, 0, 22, 16, 7, 36, 140, 179, 27, 50, 202, 25, 101, 105, 182, 93, 33, 66, 211, 97, 41, 64, 0, 182, 66, 44, 219, 242, 186, 217, 2, 203, 196, 3, 143, 182, 209, 86, 0, 127, 203, 202, 201, 200, 199, 0, 0, 5, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - inputLen: 10000, - expected: true, + name: "Regular HTTP2/gRPC Frame", + input: []byte{0, 0, 70, 1, 4, 0, 0, 0, 19, 204, 131, 4, 147, 96, 233, 45, 18, 22, 147, 175, 12, 155, 139, 103, 115, 16, 172, 98, 42, 97, 145, 31, 134, 126, 167, 0, 22, 16, 7, 36, 140, 179, 27, 50, 202, 25, 101, 105, 182, 93, 33, 66, 211, 97, 41, 64, 0, 182, 66, 44, 219, 242, 186, 217, 2, 203, 196, 3, 143, 182, 209, 86, 0, 127, 203, 202, 201, 200, 199, 0, 0, 5, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + inputLen: 10000, + expectedQuick: true, + expected: true, }, { - name: "Reset frame before HTTP2/gRPC Frame", - input: []byte{0, 0, 4, 3, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 70, 1, 4, 0, 0, 0, 21, 205, 131, 4, 147, 96, 233, 45, 18, 22, 147, 175, 12, 155, 139, 103, 115, 16, 172, 98, 42, 97, 145, 31, 134, 126, 167, 0, 22, 44, 99, 27, 33, 124, 174, 72, 228, 109, 129, 233, 27, 125, 246, 133, 44, 101, 28, 111, 70, 32, 178, 85, 163, 108, 97, 149, 199, 99, 121, 169, 90, 149, 225, 188, 176, 3, 204, 203, 202, 201, 200, 0, 0, 5, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - inputLen: 10000, - expected: true, + name: "Reset frame before HTTP2/gRPC Frame", + input: []byte{0, 0, 4, 3, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 70, 1, 4, 0, 0, 0, 21, 205, 131, 4, 147, 96, 233, 45, 18, 22, 147, 175, 12, 155, 139, 103, 115, 16, 172, 98, 42, 97, 145, 31, 134, 126, 167, 0, 22, 44, 99, 27, 33, 124, 174, 72, 228, 109, 129, 233, 27, 125, 246, 133, 44, 101, 28, 111, 70, 32, 178, 85, 163, 108, 97, 149, 199, 99, 121, 169, 90, 149, 225, 188, 176, 3, 204, 203, 202, 201, 200, 0, 0, 5, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + inputLen: 10000, + expectedQuick: true, + expected: true, }, { - name: "Too short of input len, but enough to parse the reset frame", - input: []byte{0, 0, 4, 3, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 70, 1, 4, 0, 0, 0, 21, 205, 131, 4, 147, 96, 233, 45, 18, 22, 147, 175, 12, 155, 139, 103, 115, 16, 172, 98, 42, 97, 145, 31, 134, 126, 167, 0, 22, 44, 99, 27, 33, 124, 174, 72, 228, 109, 129, 233, 27, 125, 246, 133, 44, 101, 28, 111, 70, 32, 178, 85, 163, 108, 97, 149, 199, 99, 121, 169, 90, 149, 225, 188, 176, 3, 204, 203, 202, 201, 200, 0, 0, 5, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - inputLen: frameHeaderLen + 2, - expected: false, + name: "Too short of input len, but enough to parse the reset frame", + input: []byte{0, 0, 4, 3, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 70, 1, 4, 0, 0, 0, 21, 205, 131, 4, 147, 96, 233, 45, 18, 22, 147, 175, 12, 155, 139, 103, 115, 16, 172, 98, 42, 97, 145, 31, 134, 126, 167, 0, 22, 44, 99, 27, 33, 124, 174, 72, 228, 109, 129, 233, 27, 125, 246, 133, 44, 101, 28, 111, 70, 32, 178, 85, 163, 108, 97, 149, 199, 99, 121, 169, 90, 149, 225, 188, 176, 3, 204, 203, 202, 201, 200, 0, 0, 5, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + inputLen: frameHeaderLen + 2, + expectedQuick: false, + expected: false, }, { - name: "Kafka frame instead of HTTP2", - input: []byte{0, 0, 0, 1, 0, 0, 0, 7, 0, 0, 0, 2, 0, 6, 115, 97, 114, 97, 109, 97, 255, 255, 255, 255, 0, 0, 39, 16, 0, 0, 0, 1, 0, 9, 105, 109, 112, 111, 114, 116, 97, 110, 116, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 72}, - inputLen: 10000, - expected: false, + name: "Kafka frame instead of HTTP2", + input: []byte{0, 0, 0, 1, 0, 0, 0, 7, 0, 0, 0, 2, 0, 6, 115, 97, 114, 97, 109, 97, 255, 255, 255, 255, 0, 0, 39, 16, 0, 0, 0, 1, 0, 9, 105, 109, 112, 111, 114, 116, 97, 110, 116, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 72}, + inputLen: 10000, + expectedQuick: false, + expected: false, }, { - name: "No headers frame (manually tweaked the type to fail)", - input: []byte{0, 0, 4, 3, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 70, 2, 4, 0, 0, 0, 21, 205, 131, 4, 147, 96, 233, 45, 18, 22, 147, 175, 12, 155, 139, 103, 115, 16, 172, 98, 42, 97, 145, 31, 134, 126, 167, 0, 22, 44, 99, 27, 33, 124, 174, 72, 228, 109, 129, 233, 27, 125, 246, 133, 44, 101, 28, 111, 70, 32, 178, 85, 163, 108, 97, 149, 199, 99, 121, 169, 90, 149, 225, 188, 176, 3, 204, 203, 202, 201, 200, 0, 0, 5, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - inputLen: 10000, - expected: false, + name: "No headers frame (manually tweaked the type to fail)", + input: []byte{0, 0, 4, 3, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 70, 2, 4, 0, 0, 0, 21, 205, 131, 4, 147, 96, 233, 45, 18, 22, 147, 175, 12, 155, 139, 103, 115, 16, 172, 98, 42, 97, 145, 31, 134, 126, 167, 0, 22, 44, 99, 27, 33, 124, 174, 72, 228, 109, 129, 233, 27, 125, 246, 133, 44, 101, 28, 111, 70, 32, 178, 85, 163, 108, 97, 149, 199, 99, 121, 169, 90, 149, 225, 188, 176, 3, 204, 203, 202, 201, 200, 0, 0, 5, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + inputLen: 10000, + expectedQuick: false, + expected: false, }, { - name: "Truncated frame, len should be 70 of the second frame", - input: []byte{0, 0, 4, 3, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 70, 2, 4, 0, 0, 0, 21, 205, 131}, - inputLen: 10000, - expected: false, + name: "Truncated frame, len should be 70 of the second frame", + input: []byte{0, 0, 4, 3, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 70, 2, 4, 0, 0, 0, 21, 205, 131}, + inputLen: 10000, + expectedQuick: false, + expected: false, }, { - name: "HTTP2/gRPC Header Frame", - input: []byte{0x0, 0x0, 0x7f, 0x1, 0x0, 0x0, 0x0, 0x6, 0x11, 0x83, 0x86, 0x10, 0x5, 0x3a, 0x70, 0x61, 0x74, 0x68, 0x10, 0x2f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x10, 0xa, 0x3a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x11, 0x34, 0x33, 0x2e, 0x31, 0x33, 0x35, 0x2e, 0x38, 0x34, 0x2e, 0x31, 0x33, 0x3a, 0x39, 0x38, 0x34, 0x38, 0x10, 0xc, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x10, 0xa, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0xe, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x31, 0x2e, 0x36, 0x39, 0x2e, 0x32, 0x10, 0x2, 0x74, 0x65, 0x8, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x73, 0x0, 0x0, 0x32, 0x9, 0x4, 0x0, 0x0, 0x6, 0x11, 0x10, 0x14, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x4, 0x67, 0x7a, 0x69, 0x70, 0x10, 0xc, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x8, 0x32, 0x39, 0x39, 0x39, 0x39, 0x31, 0x34, 0x75}, - inputLen: 10000, - expected: true, + name: "HTTP2/gRPC Header Frame", + input: []byte{0x0, 0x0, 0x7f, 0x1, 0x0, 0x0, 0x0, 0x6, 0x11, 0x83, 0x86, 0x10, 0x5, 0x3a, 0x70, 0x61, 0x74, 0x68, 0x10, 0x2f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x10, 0xa, 0x3a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x11, 0x34, 0x33, 0x2e, 0x31, 0x33, 0x35, 0x2e, 0x38, 0x34, 0x2e, 0x31, 0x33, 0x3a, 0x39, 0x38, 0x34, 0x38, 0x10, 0xc, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x10, 0xa, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0xe, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x67, 0x6f, 0x2f, 0x31, 0x2e, 0x36, 0x39, 0x2e, 0x32, 0x10, 0x2, 0x74, 0x65, 0x8, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x73, 0x0, 0x0, 0x32, 0x9, 0x4, 0x0, 0x0, 0x6, 0x11, 0x10, 0x14, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x4, 0x67, 0x7a, 0x69, 0x70, 0x10, 0xc, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x8, 0x32, 0x39, 0x39, 0x39, 0x39, 0x31, 0x34, 0x75}, + inputLen: 10000, + expectedQuick: true, + expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { res := isLikelyHTTP2(tt.input, tt.inputLen) - assert.Equal(t, tt.expected, res) + assert.Equal(t, tt.expectedQuick, res) res1 := isHTTP2(tt.input, tt.inputLen) assert.Equal(t, tt.expected, res1) }) @@ -314,3 +327,203 @@ func makeBPFHTTP2InfoNewRequest(buf, rbuf []byte, length int) BPFHTTP2Info { return info } + +func TestHandleHeaderField(t *testing.T) { + tests := []struct { + name string + hf *bhpack.HeaderField + expected bool + }{ + // Valid :method values + { + name: "Valid method GET", + hf: &bhpack.HeaderField{Name: ":method", Value: "get"}, + expected: true, + }, + { + name: "Valid method POST", + hf: &bhpack.HeaderField{Name: ":method", Value: "post"}, + expected: true, + }, + { + name: "Valid method PATCH", + hf: &bhpack.HeaderField{Name: ":method", Value: "patch"}, + expected: true, + }, + { + name: "Valid method DELETE", + hf: &bhpack.HeaderField{Name: ":method", Value: "delete"}, + expected: true, + }, + { + name: "Valid method OPTIONS", + hf: &bhpack.HeaderField{Name: ":method", Value: "options"}, + expected: true, + }, + { + name: "Valid method HEAD", + hf: &bhpack.HeaderField{Name: ":method", Value: "head"}, + expected: true, + }, + { + name: "Invalid method PUT", + hf: &bhpack.HeaderField{Name: ":method", Value: "PUT"}, + expected: false, + }, + { + name: "Invalid method TRACE", + hf: &bhpack.HeaderField{Name: ":method", Value: "TRACE"}, + expected: false, + }, + { + name: "Invalid method CONNECT", + hf: &bhpack.HeaderField{Name: ":method", Value: "CONNECT"}, + expected: false, + }, + { + name: "Invalid method arbitrary", + hf: &bhpack.HeaderField{Name: ":method", Value: "CUSTOM"}, + expected: false, + }, + + // Valid :path values + { + name: "Valid path simple", + hf: &bhpack.HeaderField{Name: ":path", Value: "/api/users"}, + expected: true, + }, + { + name: "Valid path with numbers", + hf: &bhpack.HeaderField{Name: ":path", Value: "/api/users/123"}, + expected: true, + }, + { + name: "Valid path with hyphens", + hf: &bhpack.HeaderField{Name: ":path", Value: "/api/user-service"}, + expected: true, + }, + { + name: "Valid path with dots", + hf: &bhpack.HeaderField{Name: ":path", Value: "/api/v1.0/users"}, + expected: true, + }, + { + name: "Valid path with dots and params separator", + hf: &bhpack.HeaderField{Name: ":path", Value: "/api/v1.0/users?hello=world&test=2"}, + expected: true, + }, + { + name: "Valid path with underscores", + hf: &bhpack.HeaderField{Name: ":path", Value: "/api/user_service"}, + expected: true, + }, + { + name: "Valid path with tildes", + hf: &bhpack.HeaderField{Name: ":path", Value: "/~username/files"}, + expected: true, + }, + { + name: "Invalid path with query", + hf: &bhpack.HeaderField{Name: ":path", Value: "/api/users?id=123"}, + expected: true, + }, + { + name: "Invalid path with special chars", + hf: &bhpack.HeaderField{Name: ":path", Value: "/api/users/!@#"}, + expected: false, + }, + { + name: "Invalid path with spaces", + hf: &bhpack.HeaderField{Name: ":path", Value: "/api/user service"}, + expected: false, + }, + + // Valid content-type values + { + name: "Valid content-type grpc", + hf: &bhpack.HeaderField{Name: "content-type", Value: "application/grpc"}, + expected: true, + }, + { + name: "Valid content-type grpc+proto", + hf: &bhpack.HeaderField{Name: "content-type", Value: "application/grpc+proto"}, + expected: true, + }, + { + name: "Valid content-type grpc+json", + hf: &bhpack.HeaderField{Name: "content-type", Value: "application/grpc+json"}, + expected: true, + }, + { + name: "Valid content-type json", + hf: &bhpack.HeaderField{Name: "content-type", Value: "application/json"}, + expected: true, + }, + { + name: "Valid content-type xml", + hf: &bhpack.HeaderField{Name: "content-type", Value: "application/xml"}, + expected: true, + }, + { + name: "Valid content-type with hyphen", + hf: &bhpack.HeaderField{Name: "content-type", Value: "application/x-protobuf"}, + expected: true, + }, + { + name: "Invalid content-type with charset", + hf: &bhpack.HeaderField{Name: "content-type", Value: "application/json; charset=utf-8"}, + expected: false, + }, + { + name: "Invalid content-type with spaces", + hf: &bhpack.HeaderField{Name: "content-type", Value: "application / json"}, + expected: false, + }, + { + name: "Invalid content-type with numbers", + hf: &bhpack.HeaderField{Name: "content-type", Value: "application/grpc123"}, + expected: false, + }, + + // Other headers (should return false) + { + name: "Unknown header", + hf: &bhpack.HeaderField{Name: "user-agent", Value: "grpc-go/1.69.2"}, + expected: false, + }, + { + name: "Empty header name", + hf: &bhpack.HeaderField{Name: "", Value: "value"}, + expected: false, + }, + { + name: "Empty header value", + hf: &bhpack.HeaderField{Name: ":method", Value: ""}, + expected: false, + }, + + // Case sensitivity tests + { + name: "Method name uppercase :METHOD", + hf: &bhpack.HeaderField{Name: ":METHOD", Value: "GET"}, + expected: true, + }, + { + name: "Content-Type uppercase", + hf: &bhpack.HeaderField{Name: "Content-Type", Value: "application/grpc"}, + expected: true, + }, + { + name: "Mixed case path header", + hf: &bhpack.HeaderField{Name: ":Path", Value: "/api/users"}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := handleHeaderField(tt.hf) + assert.Equal(t, tt.expected, result, "Expected %v for %s:%s", tt.expected, tt.hf.Name, tt.hf.Value) + }) + } +} From 9ff7a6256cc2494e366e0c64a548377ed2440ec1 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Tue, 25 Nov 2025 16:57:18 -0500 Subject: [PATCH 2/4] lint --- pkg/ebpf/common/http2grpc_transform.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/ebpf/common/http2grpc_transform.go b/pkg/ebpf/common/http2grpc_transform.go index bb1cc5697e..f374cf0f5c 100644 --- a/pkg/ebpf/common/http2grpc_transform.go +++ b/pkg/ebpf/common/http2grpc_transform.go @@ -35,8 +35,10 @@ const ( const initialHeaderTableSize = 4096 -var validPath = regexp.MustCompile(`^[A-Za-z0-9\-/._~]+$`) -var validContentType = regexp.MustCompile(`^[A-Za-z\-/\+]+$`) +var ( + validPath = regexp.MustCompile(`^[A-Za-z0-9\-/._~]+$`) + validContentType = regexp.MustCompile(`^[A-Za-z\-/\+]+$`) +) type h2Connection struct { hdec *bhpack.Decoder From 783c2f6f79914ff5583c092b05164fadb0da1bea Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Tue, 25 Nov 2025 17:02:42 -0500 Subject: [PATCH 3/4] restore grpc-status --- pkg/ebpf/common/http2grpc_transform.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/ebpf/common/http2grpc_transform.go b/pkg/ebpf/common/http2grpc_transform.go index f374cf0f5c..fafa5ffd67 100644 --- a/pkg/ebpf/common/http2grpc_transform.go +++ b/pkg/ebpf/common/http2grpc_transform.go @@ -115,6 +115,8 @@ func handleHeaderField(hf *bhpack.HeaderField) bool { if validContentType.MatchString(val) { return true } + case "grpc-status": + return true } return false From b26b7540a5ef1f1890aca6cc1401660dd959dc47 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Wed, 26 Nov 2025 12:08:09 -0500 Subject: [PATCH 4/4] improve detection, more tests --- pkg/ebpf/common/http2grpc_transform.go | 7 +++++++ pkg/ebpf/common/http2grpc_transform_test.go | 21 +++++++++++++++++++++ pkg/ebpf/common/tcp_detect_transform.go | 6 ++++++ 3 files changed, 34 insertions(+) diff --git a/pkg/ebpf/common/http2grpc_transform.go b/pkg/ebpf/common/http2grpc_transform.go index fafa5ffd67..0d8b2adf2f 100644 --- a/pkg/ebpf/common/http2grpc_transform.go +++ b/pkg/ebpf/common/http2grpc_transform.go @@ -102,6 +102,13 @@ func handleHeaderField(hf *bhpack.HeaderField) bool { if isHTTPOp(val) { return true } + case ":scheme": + val := strings.ToUpper(hf.Value) + if val == "HTTP" { + return true + } + case "traceparent": + return true case ":path": val := hf.Value if pos := strings.Index(val, "?"); pos >= 0 { diff --git a/pkg/ebpf/common/http2grpc_transform_test.go b/pkg/ebpf/common/http2grpc_transform_test.go index 806d6246bf..d49c9b1599 100644 --- a/pkg/ebpf/common/http2grpc_transform_test.go +++ b/pkg/ebpf/common/http2grpc_transform_test.go @@ -20,6 +20,20 @@ func TestHTTP2QuickDetection(t *testing.T) { expected bool expectedQuick bool }{ + { + name: "Test no :path, but has :scheme", + input: []byte{0, 0, 77, 1, 4, 0, 0, 7, 35, 195, 194, 131, 134, 193, 192, 191, 190, 0, 11, 116, 114, 97, 99, 101, 112, 97, 114, 101, 110, 116, 55, 48, 48, 45, 52, 53, 53, 49, 102, 50, 57, 102, 101, 102, 54, 97, 50, 57, 56, 51, 52, 51, 48, 102, 98, 101, 49, 101, 53, 101, 99, 99, 101, 100, 55, 54, 45, 55, 52, 102, 49, 48, 55, 98, 98, 52, 55, 98, 53, 52, 57, 57, 54, 45, 48, 49, 0, 0, 4, 8, 0, 0, 0, 7, 35, 0, 0, 0, 5, 0, 0, 5, 0, 1, 0, 0, 7, 35, 0, 0, 0, 0, 0, 0, 0, 4, 8, 0, 0, 0, 0, 0, 0, 0, 20, 12}, + inputLen: 10000, + expected: true, + expectedQuick: true, + }, + { + name: "Test no :path, but has :scheme and traceparent", + input: []byte{0, 0, 134, 1, 4, 0, 0, 7, 15, 195, 194, 131, 134, 193, 192, 191, 190, 0, 11, 116, 114, 97, 99, 101, 112, 97, 114, 101, 110, 116, 55, 48, 48, 45, 50, 50, 98, 100, 57, 52, 52, 99, 50, 98, 50, 52, 97, 52, 102, 98, 52, 55, 102, 102, 101, 56, 98, 102, 57, 97, 100, 51, 54, 52, 57, 53, 45, 50, 50, 102, 53, 56, 98, 55, 53, 100, 100, 50, 99, 51, 55, 98, 101, 45, 48, 49, 0, 7, 98, 97, 103, 103, 97, 103, 101, 47, 115, 101, 115, 115, 105, 111, 110, 46, 105, 100, 61, 98, 50, 54, 49, 101, 51, 98, 101, 45, 102, 55, 102, 55, 45, 52, 54, 99, 55, 45, 98, 99, 55, 100, 45, 98, 99, 100, 97, 100, 101, 48, 102, 57, 97, 54, 102, 0, 0, 4, 8, 0, 0, 0, 7, 15, 0, 0, 0, 5, 0, 0, 5, 0, 1, 0, 0, 7, 15, 0, 0, 0, 0, 0, 0, 0, 4, 8, 0, 0, 0, 0, 0, 0, 0, 20, 12}, + inputLen: 10000, + expected: true, + expectedQuick: true, + }, { name: "Status instead of start", input: []byte{0, 0, 29, 1, 4, 0, 0, 1, 101, 136, 224, 223, 222, 221, 97, 150, 223, 105, 126, 148, 19, 106, 101, 182, 165, 4, 1, 52, 160, 92, 184, 23, 174, 1, 197, 49, 104, 223, 0, 0, 44, 0, 0, 0, 0, 1, 101, 1, 0, 0, 0, 39, 31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 18, 98, 11, 14, 113, 12, 241, 116, 150, 98, 206, 79, 75, 83, 98, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, @@ -90,6 +104,13 @@ func TestHTTP2QuickDetection(t *testing.T) { expectedQuick: true, expected: true, }, + { + name: "gRPC proper frame", + input: []byte{0, 0, 113, 1, 4, 0, 0, 0, 33, 218, 131, 4, 154, 96, 233, 45, 18, 22, 147, 175, 122, 114, 147, 169, 237, 78, 226, 217, 220, 196, 43, 26, 232, 25, 11, 170, 201, 11, 103, 134, 126, 167, 0, 22, 33, 75, 27, 66, 40, 218, 125, 217, 6, 251, 236, 198, 240, 192, 32, 145, 240, 189, 35, 77, 137, 233, 86, 109, 231, 70, 243, 79, 21, 240, 62, 217, 89, 3, 139, 0, 63, 127, 1, 161, 65, 80, 131, 30, 165, 205, 36, 17, 137, 192, 149, 152, 202, 180, 174, 202, 234, 205, 56, 71, 86, 140, 142, 200, 180, 100, 144, 114, 20, 18, 190, 55, 37, 217, 216, 215, 214, 213, 0, 0, 170, 0, 0, 0, 0, 0, 33, 0, 0, 0, 0, 165, 10, 36, 98, 50, 54, 49, 101, 51, 98, 101, 45, 102, 55, 102, 55, 45, 52, 54, 99, 55, 45, 98, 99, 55, 100, 45, 98, 99, 100, 97, 100, 101, 48, 102, 57, 97, 54, 102, 18, 3, 66, 82, 76, 26, 68, 10, 25, 49, 54, 48, 48, 32, 65, 109, 112, 104, 105, 116, 104, 101, 97, 116, 114, 101, 32, 80, 97, 114, 107, 119, 97, 121, 18, 13, 77, 111, 117, 110, 116, 97, 105, 110, 32, 86, 105, 101, 119, 26, 2, 67, 65, 34, 13, 85, 110, 105, 116, 101, 100, 32, 83, 116, 97, 116, 101, 115, 42, 5, 57, 52, 48, 52, 51, 42, 19, 115, 111, 109, 101, 111}, + inputLen: 10000, + expected: true, + expectedQuick: true, + }, } for _, tt := range tests { diff --git a/pkg/ebpf/common/tcp_detect_transform.go b/pkg/ebpf/common/tcp_detect_transform.go index 9e34af102a..e02bfb3897 100644 --- a/pkg/ebpf/common/tcp_detect_transform.go +++ b/pkg/ebpf/common/tcp_detect_transform.go @@ -136,6 +136,7 @@ func ReadTCPRequestIntoSpan(parseCtx *EBPFParseContext, cfg *config.EBPFTracer, if isHTTP2(requestBuffer, int(event.Len)) || isHTTP2(responseBuffer, int(event.RespLen)) { evCopy := *event MisclassifiedEvents <- MisclassifiedEvent{EventType: EventTypeKHTTP2, TCPInfo: &evCopy} + return request.Span{}, true, nil // ignore for now, next event will be parsed } else { k, ignore, err := ProcessPossibleKafkaEvent(event, requestBuffer, responseBuffer, parseCtx.kafkaTopicUUIDToName) if ignore { @@ -147,6 +148,11 @@ func ReadTCPRequestIntoSpan(parseCtx *EBPFParseContext, cfg *config.EBPFTracer, } } + if cfg.ProtocolDebug { + fmt.Printf("![>] %v\n", requestBuffer) + fmt.Printf("![<] %v\n", responseBuffer) + } + return request.Span{}, true, nil // ignore if we couldn't parse it }