Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions ebpftracer/ebpf.go

Large diffs are not rendered by default.

49 changes: 47 additions & 2 deletions ebpftracer/ebpf/l7/http.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
static __always_inline
int is_http_request(char *buf) {
char b[16];
if (bpf_probe_read_str(&b, sizeof(b), (void *)buf) < 16) {
if (bpf_probe_read(&b, sizeof(b), (void *)buf)) {
return 0;
}
if (b[0] == 'G' && b[1] == 'E' && b[2] == 'T') {
Expand Down Expand Up @@ -35,7 +35,7 @@ int is_http_request(char *buf) {
static __always_inline
int is_http_response(char *buf, __s32 *status) {
char b[16];
if (bpf_probe_read_str(&b, sizeof(b), (void *)buf) < 16) {
if (bpf_probe_read(&b, sizeof(b), (void *)buf)) {
return 0;
}
if (b[0] != 'H' || b[1] != 'T' || b[2] != 'T' || b[3] != 'P' || b[4] != '/') {
Expand All @@ -59,3 +59,48 @@ int is_http_response(char *buf, __s32 *status) {
*status = (b[9]-'0')*100 + (b[10]-'0')*10 + (b[11]-'0');
return 1;
}

static __always_inline
int is_http_response_partial(char *buf, __u64 size, __u8 partial) {
// If this is a continuation of a partial response
if (partial) {
return 1; // Assume it's part of the ongoing HTTP response
}

// Check if we have enough data for a complete HTTP response
if (size < 4) {
return 2; // Mark as partial, need more data
}

__s32 status;
if (!is_http_response(buf, &status)) {
return 0; // Not an HTTP response
}

// Look for end of headers (double CRLF)
char pattern[4] = {'\r', '\n', '\r', '\n'};
__u64 header_end = 0;

#pragma unroll
for (int i = 0; i < MAX_PAYLOAD_SIZE - 4 && i < (int)size - 4; i++) {
char check[4];
if (bpf_probe_read(check, 4, buf + i)) {
break;
}
if (check[0] == pattern[0] && check[1] == pattern[1] &&
check[2] == pattern[2] && check[3] == pattern[3]) {
header_end = i + 4;
break;
}
}
Comment on lines +84 to +95

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This loop to find the end of headers (\r\n\r\n) calls bpf_probe_read for 4 bytes on every iteration. bpf_probe_read is a relatively expensive call, and this PR is focused on performance. A more performant approach would be to read byte-by-byte and use a state machine to find the pattern. This would reduce the overhead of bpf_probe_read calls inside the loop.

    char c;
    int state = 0;
    #pragma unroll
    for (int i = 0; i < MAX_PAYLOAD_SIZE && i < (int)size; i++) {
        if (bpf_probe_read(&c, sizeof(c), buf + i)) {
            break;
        }
        if (c == pattern[state]) {
            state++;
        } else {
            state = (c == pattern[0] ? 1 : 0);
        }

        if (state == 4) {
            header_end = i + 1;
            break;
        }
    }


if (header_end == 0) {
return 2; // Headers incomplete, need more data
}

// Check Content-Length header for completeness
// For now, assume single packet responses are complete
// TODO: Parse Content-Length header for exact validation

return 1; // Complete response
}
52 changes: 52 additions & 0 deletions ebpftracer/ebpf/l7/http2.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#define HTTP2_CLIENT_INITIATED_STREAM(stream_id) (stream_id & 0x01000000) // big-endian (network byte order) odd number
#define HTTP2_SETTINGS_FRAME 0x4

#define bpf_read(src, dst) bpf_probe_read(&dst, sizeof(dst), src)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This bpf_read macro is problematic because it discards the return value of bpf_probe_read. If bpf_probe_read fails, this error is ignored, and subsequent code will operate on uninitialized data. This is a critical bug that can lead to incorrect behavior.

Please remove this macro and use bpf_probe_read directly with proper error checking at all call sites (lines 64, 68, 71).

Example of correct usage:

    __u32 frame_length;
    if (bpf_probe_read(&frame_length, sizeof(frame_length), buf)) {
        return 0; // Or other error code
    }


static __always_inline
int is_client_preface(char *buf, __u64 size, __u8 method) {
if (method != METHOD_HTTP2_CLIENT_FRAMES || size < 24) {
Expand Down Expand Up @@ -38,4 +40,54 @@ int looks_like_http2_frame(char *buf, __u64 size, __u8 method) {
return is_server_preface(frame_type, stream_id, method);
}
return 1;
}

static __always_inline
int is_http2_response_partial(char *buf, __u64 size, __u8 partial) {
// If this is a continuation of a partial response
if (partial) {
return 1; // Continue collecting HTTP/2 frames
}

// Need at least 9 bytes for HTTP/2 frame header
if (size < 9) {
return 2; // Partial, need more data
}

// Check if this looks like HTTP/2 frames
if (!looks_like_http2_frame(buf, size, METHOD_HTTP2_SERVER_FRAMES)) {
return 0; // Not HTTP/2
}

// Parse frame header to check completeness
__u32 frame_length;
bpf_read(buf, frame_length);
frame_length = bpf_htonl(frame_length) >> 8; // Get 24-bit length

__u8 frame_type;
bpf_read(buf + 3, frame_type);

__u8 flags;
bpf_read(buf + 4, flags);

// Check if we have the complete frame
if (size < frame_length + 9) {
return 2; // Incomplete frame, need more data
}

// For DATA frames (0x0), check if END_STREAM flag (0x1) is set
if (frame_type == 0x0) { // DATA frame
if (!(flags & 0x1)) { // END_STREAM flag not set
return 2; // More DATA frames expected
}
}

// For HEADERS frames (0x1), check if END_HEADERS flag (0x4) is set
if (frame_type == 0x1) { // HEADERS frame
if (!(flags & 0x4)) { // END_HEADERS flag not set
return 2; // More HEADERS/CONTINUATION frames expected
}
}

return 1; // Complete response or frame sequence
}
124 changes: 95 additions & 29 deletions ebpftracer/ebpf/l7/l7.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
})
#define COPY_PAYLOAD(dst, size, src) ({ \
TRUNCATE_PAYLOAD_SIZE(size); \
if (bpf_probe_read(dst, size, src)) { \
if (size > 0 && bpf_probe_read(dst, size, src)) { \
return 0; \
} \
})
Expand Down Expand Up @@ -137,6 +137,24 @@ struct user_msghdr {
__u32 msg_flags;
};

static inline __attribute__((__always_inline__))
void init_l7_event(struct l7_event *e) {
// Only initialize critical fields, payload/response arrays are overwritten anyway
e->fd = 0;
e->connection_timestamp = 0;
e->pid = 0;
e->status = STATUS_UNKNOWN;
e->duration = 0;
e->protocol = PROTOCOL_UNKNOWN;
e->method = METHOD_UNKNOWN;
e->padding = 0;
e->statement_id = 0;
e->payload_size = 0;
e->response_size = 0;
// Skip zeroing payload arrays - they're overwritten by COPY_PAYLOAD anyway
// This saves ~10,000 instructions per event
}

static inline __attribute__((__always_inline__))
void send_event(void *ctx, struct l7_event *e, struct connection_id cid, struct connection *conn) {
e->connection_timestamp = conn->timestamp;
Expand Down Expand Up @@ -222,14 +240,38 @@ int trace_enter_write(void *ctx, __u64 fd, __u16 is_tls, char *buf, __u64 size,
k.is_tls = is_tls;
k.stream_id = -1;

if (is_http_request(payload)) {
// Fast path: Check most common protocols first
// Use first bytes to quickly eliminate protocols
char first_4[4];
if (size >= 4 && !bpf_probe_read(first_4, 4, payload)) {
// HTTP: GET, POST, PUT, HEAD, DELETE, OPTIONS, PATCH
if ((first_4[0] == 'G' && first_4[1] == 'E' && first_4[2] == 'T') ||
(first_4[0] == 'P' && first_4[1] == 'O' && first_4[2] == 'S') ||
(first_4[0] == 'P' && first_4[1] == 'U' && first_4[2] == 'T') ||
(first_4[0] == 'H' && first_4[1] == 'E' && first_4[2] == 'A') ||
(first_4[0] == 'D' && first_4[1] == 'E' && first_4[2] == 'L') ||
(first_4[0] == 'O' && first_4[1] == 'P' && first_4[2] == 'T') ||
(first_4[0] == 'P' && first_4[1] == 'A' && first_4[2] == 'T') ||
(first_4[0] == 'C' && first_4[1] == 'O' && first_4[2] == 'N')) {
req->protocol = PROTOCOL_HTTP;
} else if (first_4[0] == 'P' && first_4[1] == 'R' && first_4[2] == 'I' && first_4[3] == ' ') {
// HTTP/2: Check for connection preface or frame header
if (looks_like_http2_frame(payload, size, METHOD_HTTP2_CLIENT_FRAMES)) {
req->protocol = PROTOCOL_HTTP2;
}
}
}

// If fast path didn't detect protocol, try full detection
if (req->protocol == PROTOCOL_UNKNOWN && is_http_request(payload)) {
req->protocol = PROTOCOL_HTTP;
} else if (is_postgres_query(payload, size, &req->request_type)) {
} else if (req->protocol == PROTOCOL_UNKNOWN && is_postgres_query(payload, size, &req->request_type)) {
if (req->request_type == POSTGRES_FRAME_CLOSE) {
struct l7_event *e = bpf_map_lookup_elem(&l7_event_heap, &zero);
if (!e) {
return 0;
}
init_l7_event(e);
e->protocol = PROTOCOL_POSTGRES;
e->method = METHOD_STATEMENT_CLOSE;
e->payload_size = size;
Expand All @@ -238,16 +280,17 @@ int trace_enter_write(void *ctx, __u64 fd, __u16 is_tls, char *buf, __u64 size,
return 0;
}
req->protocol = PROTOCOL_POSTGRES;
} else if (is_redis_query(payload, size)) {
} else if (req->protocol == PROTOCOL_UNKNOWN && is_redis_query(payload, size)) {
req->protocol = PROTOCOL_REDIS;
} else if (is_memcached_query(payload, size)) {
} else if (req->protocol == PROTOCOL_UNKNOWN && is_memcached_query(payload, size)) {
req->protocol = PROTOCOL_MEMCACHED;
} else if (is_mysql_query(payload, size, &req->request_type)) {
} else if (req->protocol == PROTOCOL_UNKNOWN && is_mysql_query(payload, size, &req->request_type)) {
if (req->request_type == MYSQL_COM_STMT_CLOSE) {
struct l7_event *e = bpf_map_lookup_elem(&l7_event_heap, &zero);
if (!e) {
return 0;
}
init_l7_event(e);
e->protocol = PROTOCOL_MYSQL;
e->method = METHOD_STATEMENT_CLOSE;
e->payload_size = size;
Expand All @@ -256,53 +299,56 @@ int trace_enter_write(void *ctx, __u64 fd, __u16 is_tls, char *buf, __u64 size,
return 0;
}
req->protocol = PROTOCOL_MYSQL;
} else if (is_mongo_query(payload, size)) {
} else if (req->protocol == PROTOCOL_UNKNOWN && is_mongo_query(payload, size)) {
req->protocol = PROTOCOL_MONGO;
} else if (is_rabbitmq_produce(payload, size)) {
} else if (req->protocol == PROTOCOL_UNKNOWN && is_rabbitmq_produce(payload, size)) {
struct l7_event *e = bpf_map_lookup_elem(&l7_event_heap, &zero);
if (!e) {
return 0;
}
init_l7_event(e);
e->protocol = PROTOCOL_RABBITMQ;
e->method = METHOD_PRODUCE;
send_event(ctx, e, cid, conn);
return 0;
} else if (nats_method(payload, size) == METHOD_PRODUCE) {
} else if (req->protocol == PROTOCOL_UNKNOWN && nats_method(payload, size) == METHOD_PRODUCE) {
struct l7_event *e = bpf_map_lookup_elem(&l7_event_heap, &zero);
if (!e) {
return 0;
}
init_l7_event(e);
e->protocol = PROTOCOL_NATS;
e->method = METHOD_PRODUCE;
send_event(ctx, e, cid, conn);
return 0;
} else if (is_cassandra_request(payload, size, &k.stream_id)) {
} else if (req->protocol == PROTOCOL_UNKNOWN && is_cassandra_request(payload, size, &k.stream_id)) {
req->protocol = PROTOCOL_CASSANDRA;
} else if (looks_like_http2_frame(payload, size, METHOD_HTTP2_CLIENT_FRAMES)) {
} else if (req->protocol == PROTOCOL_UNKNOWN && looks_like_http2_frame(payload, size, METHOD_HTTP2_CLIENT_FRAMES)) {
struct l7_event *e = bpf_map_lookup_elem(&l7_event_heap, &zero);
if (!e) {
return 0;
}
init_l7_event(e);
e->protocol = PROTOCOL_HTTP2;
e->method = METHOD_HTTP2_CLIENT_FRAMES;
e->duration = bpf_ktime_get_ns();
e->payload_size = size;
COPY_PAYLOAD(e->payload, size, payload);
send_event(ctx, e, cid, conn);
return 0;
} else if (is_clickhouse_query(payload, size)) {
} else if (req->protocol == PROTOCOL_UNKNOWN && is_clickhouse_query(payload, size)) {
req->protocol = PROTOCOL_CLICKHOUSE;
} else if (is_zk_request(payload, total_size)) {
} else if (req->protocol == PROTOCOL_UNKNOWN && is_zk_request(payload, total_size)) {
req->protocol = PROTOCOL_ZOOKEEPER;
} else if (is_kafka_request(payload, size, &req->request_id)) {
} else if (req->protocol == PROTOCOL_UNKNOWN && is_kafka_request(payload, size, &req->request_id)) {
req->protocol = PROTOCOL_KAFKA;
struct l7_request *prev_req = bpf_map_lookup_elem(&active_l7_requests, &k);
if (prev_req && prev_req->protocol == PROTOCOL_KAFKA) {
req->ns = prev_req->ns;
}
} else if (is_dubbo2_request(payload, size)) {
} else if (req->protocol == PROTOCOL_UNKNOWN && is_dubbo2_request(payload, size)) {
req->protocol = PROTOCOL_DUBBO2;
} else if (is_dns_request(payload, size, &k.stream_id)) {
} else if (req->protocol == PROTOCOL_UNKNOWN && is_dns_request(payload, size, &k.stream_id)) {
req->protocol = PROTOCOL_DNS;
}

Expand Down Expand Up @@ -393,11 +439,7 @@ int trace_exit_read(void *ctx, __u64 id, __u32 pid, __u16 is_tls, long int ret)
if (!e) {
return 0;
}
e->protocol = PROTOCOL_UNKNOWN;
e->status = STATUS_UNKNOWN;
e->method = METHOD_UNKNOWN;
e->statement_id = 0;
e->payload_size = 0;
init_l7_event(e);
e->response_size = ret;
COPY_PAYLOAD(e->response, ret, payload);
if (is_rabbitmq_consume(payload, ret)) {
Expand Down Expand Up @@ -435,13 +477,21 @@ int trace_exit_read(void *ctx, __u64 id, __u32 pid, __u16 is_tls, long int ret)
}
response = 1;
} else if (looks_like_http2_frame(payload, ret, METHOD_HTTP2_SERVER_FRAMES)) {
e->protocol = PROTOCOL_HTTP2;
e->method = METHOD_HTTP2_SERVER_FRAMES;
e->duration = bpf_ktime_get_ns();
e->payload_size = ret;
COPY_PAYLOAD(e->payload, ret, payload);
send_event(ctx, e, cid, conn);
return 0;
// Check if there's a matching HTTP/2 request for gRPC handling
req = bpf_map_lookup_elem(&active_l7_requests, &k);
if (req && req->protocol == PROTOCOL_HTTP2) {
// Handle as part of HTTP/2 response sequence
response = 1;
} else {
// Send standalone HTTP/2 frame (non-gRPC)
e->protocol = PROTOCOL_HTTP2;
e->method = METHOD_HTTP2_SERVER_FRAMES;
e->duration = bpf_ktime_get_ns();
e->payload_size = ret;
COPY_PAYLOAD(e->payload, ret, payload);
send_event(ctx, e, cid, conn);
return 0;
}
} else {
return 0;
}
Expand All @@ -451,7 +501,14 @@ int trace_exit_read(void *ctx, __u64 id, __u32 pid, __u16 is_tls, long int ret)
e->payload_size = req->payload_size;
COPY_PAYLOAD(e->payload, req->payload_size, req->payload);
if (e->protocol == PROTOCOL_HTTP) {
response = is_http_response(payload, &e->status);
response = is_http_response_partial(payload, ret, req->partial);
if (response == 2) { // partial
req->partial = 1;
return 0; // keeping the query in the map
}
if (response == 1) {
is_http_response(payload, &e->status); // Get status code
}
Comment on lines 503 to +511

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the case of a complete HTTP response (response == 1), is_http_response is called to get the status code. However, is_http_response_partial already calls is_http_response internally. This results in is_http_response being called twice for every complete response, which is inefficient.

To fix this, you can modify is_http_response_partial in ebpftracer/ebpf/l7/http.c to accept a pointer to the status variable and populate it directly. This avoids the redundant function call.

Example modification in http.c:

// in http.c
int is_http_response_partial(char *buf, __u64 size, __u8 partial, __s32 *status) {
    // ...
    if (!is_http_response(buf, status)) { // pass status pointer
        return 0;
    }
    // ...
}

Then you can update the call site here.

    if (e->protocol == PROTOCOL_HTTP) {
        response = is_http_response_partial(payload, ret, req->partial, &e->status);
        if (response == 2) { // partial
            req->partial = 1;
            return 0; // keeping the query in the map
        }
    }

} else if (e->protocol == PROTOCOL_POSTGRES) {
response = is_postgres_response(payload, ret, &e->status);
if (req->request_type == POSTGRES_FRAME_PARSE) {
Expand Down Expand Up @@ -485,6 +542,15 @@ int trace_exit_read(void *ctx, __u64 id, __u32 pid, __u16 is_tls, long int ret)
req->partial = 1;
return 0; // keeping the query in the map
}
} else if (e->protocol == PROTOCOL_HTTP2) {
response = is_http2_response_partial(payload, ret, req->partial);
if (response == 2) { // partial
req->partial = 1;
return 0; // keeping the query in the map
}
if (response == 1) {
e->method = METHOD_HTTP2_SERVER_FRAMES;
}
} else if (e->protocol == PROTOCOL_DUBBO2) {
response = is_dubbo2_response(payload, &e->status);
}
Expand Down
Loading
Loading