Skip to content

Commit

Permalink
[USM] extract fix frame from filtering (#20528)
Browse files Browse the repository at this point in the history
* usm: http2: Change PROG_HTTP2 to PROG_HTTP2_FRAME_FILTER

In this PR we'll add a new hook point, so for clarity it is best to have meaningful names
which represent better the usage of the tail-calls

* usm: http2: Introduce first frame handler

The new tail call intended to run a the first tail call for http2 (instead of frames_filter).
The goal will be to read the first frame, with consideration to remainder from previous packet
and to reduce the complexity of frames_filter, that will lead into processing more frames

* usm: http2: Extract first-frame-handler into a function

As a preliminary step, extracting the relevant code into a separated function to ease future modifications

* usm: http2: Move tcp termination handling to socket__http2_handle_first_frame

The first tail call should handle it, as there is no need to call further tail-calls for cleanup.
Also, less code in socket__http2_filter, means less complexity for the verifier and higher chance
to process more frames.

* usm: http2: Move initialization of iteration_value to socket__http2_handle_first_frame

The first frame might be interesting, so we need to move the initialization into socket__http2_handle_first_frame for that case.
Thus in socket__http2_filter we just need to get the pointer

* usm: http2: socket__http2_handle_first_frame is now responsbile for extracting first frame

Changed socket__http2_handle_first_frame so it will extract the first frame, and clear the frame_state if consumed.
We also override the data_off field cached in dispatcher_arguments map, so socket__http2_filter will have the correct
offset of the next frame to read from.

* usm: Added documentation for some consts
  • Loading branch information
guyarb authored Nov 2, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent d38192e commit 108d837
Showing 5 changed files with 133 additions and 68 deletions.
3 changes: 2 additions & 1 deletion pkg/network/ebpf/c/protocols/classification/defs.h
Original file line number Diff line number Diff line change
@@ -133,7 +133,8 @@ typedef enum {
typedef enum {
PROG_UNKNOWN = 0,
PROG_HTTP,
PROG_HTTP2,
PROG_HTTP2_HANDLE_FIRST_FRAME,
PROG_HTTP2_FRAME_FILTER,
PROG_HTTP2_FRAME_PARSER,
PROG_KAFKA,
PROG_GRPC,
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ __maybe_unused static __always_inline protocol_prog_t protocol_to_program(protoc
case PROTOCOL_HTTP:
return PROG_HTTP;
case PROTOCOL_HTTP2:
return PROG_HTTP2;
return PROG_HTTP2_HANDLE_FIRST_FRAME;
case PROTOCOL_KAFKA:
return PROG_KAFKA;
default:
158 changes: 104 additions & 54 deletions pkg/network/ebpf/c/protocols/http2/decoding.h
Original file line number Diff line number Diff line change
@@ -399,30 +399,23 @@ static __always_inline void reset_frame(struct http2_frame *out) {
out->flags = 0;
}

static __always_inline __u8 find_relevant_headers(struct __sk_buff *skb, skb_info_t *skb_info, http2_frame_with_offset *frames_array, frame_header_remainder_t *frame_state) {
bool is_headers_or_rst_frame, is_data_end_of_stream;
__u8 interesting_frame_index = 0;
struct http2_frame current_frame = {};

// Filter preface.
skip_preface(skb, skb_info);

static __always_inline bool get_first_frame(struct __sk_buff *skb, skb_info_t *skb_info, frame_header_remainder_t *frame_state, struct http2_frame *current_frame) {
// No state, try reading a frame.
if (frame_state == NULL) {
// Checking we have enough bytes in the packet to read a frame header.
if (skb_info->data_off + HTTP2_FRAME_HEADER_SIZE > skb_info->data_end) {
// Not enough bytes, cannot read frame, so we have 0 interesting frames in that packet.
return 0;
return false;
}

// Reading frame, and ensuring the frame is valid.
bpf_skb_load_bytes(skb, skb_info->data_off, (char *)&current_frame, HTTP2_FRAME_HEADER_SIZE);
bpf_skb_load_bytes(skb, skb_info->data_off, (char *)current_frame, HTTP2_FRAME_HEADER_SIZE);
skb_info->data_off += HTTP2_FRAME_HEADER_SIZE;
if (!format_http2_frame_header(&current_frame)) {
if (!format_http2_frame_header(current_frame)) {
// Frame is not valid, so we have 0 interesting frames in that packet.
return 0;
return false;
}
goto valid_frame;
return true;
}

// Getting here means we have a frame state from the previous packets.
@@ -438,24 +431,24 @@ static __always_inline __u8 find_relevant_headers(struct __sk_buff *skb, skb_inf

// Frame-header-remainder.
if (frame_state->header_length > 0) {
fix_header_frame(skb, skb_info, (char*)&current_frame, frame_state);
if (format_http2_frame_header(&current_frame)) {
fix_header_frame(skb, skb_info, (char*)current_frame, frame_state);
if (format_http2_frame_header(current_frame)) {
skb_info->data_off += frame_state->remainder;
frame_state->remainder = 0;
goto valid_frame;
return true;
}

// We couldn't read frame header using the remainder.
return 0;
return false;
}

// Checking if we can read a frame header.
if (skb_info->data_off + HTTP2_FRAME_HEADER_SIZE <= skb_info->data_end) {
bpf_skb_load_bytes(skb, skb_info->data_off, (char *)&current_frame, HTTP2_FRAME_HEADER_SIZE);
if (format_http2_frame_header(&current_frame)) {
bpf_skb_load_bytes(skb, skb_info->data_off, (char *)current_frame, HTTP2_FRAME_HEADER_SIZE);
if (format_http2_frame_header(current_frame)) {
// We successfully read a valid frame.
skb_info->data_off += HTTP2_FRAME_HEADER_SIZE;
goto valid_frame;
return true;
}
}

@@ -465,60 +458,72 @@ static __always_inline __u8 find_relevant_headers(struct __sk_buff *skb, skb_inf
// The remainders "ends" the current packet. No interesting frames were found.
if (skb_info->data_off == skb_info->data_end) {
frame_state->remainder = 0;
return 0;
return false;
}
reset_frame(&current_frame);
bpf_skb_load_bytes(skb, skb_info->data_off, (char *)&current_frame, HTTP2_FRAME_HEADER_SIZE);
if (format_http2_frame_header(&current_frame)) {
reset_frame(current_frame);
bpf_skb_load_bytes(skb, skb_info->data_off, (char *)current_frame, HTTP2_FRAME_HEADER_SIZE);
if (format_http2_frame_header(current_frame)) {
frame_state->remainder = 0;
skb_info->data_off += HTTP2_FRAME_HEADER_SIZE;
goto valid_frame;
return true;
}
}
// still not valid / does not have a remainder - abort.
return 0;
return false;
}

valid_frame:
static __always_inline __u8 find_relevant_headers(struct __sk_buff *skb, skb_info_t *skb_info, http2_frame_with_offset *frames_array, __u8 original_index) {
bool is_headers_or_rst_frame, is_data_end_of_stream;
__u8 interesting_frame_index = 0;
struct http2_frame current_frame = {};
if (original_index == 1) {
interesting_frame_index = 1;
}

#pragma unroll(HTTP2_MAX_FRAMES_TO_FILTER)
for (__u32 iteration = 0; iteration < HTTP2_MAX_FRAMES_TO_FILTER; ++iteration) {
// END_STREAM can appear only in Headers and Data frames.
// Check out https://datatracker.ietf.org/doc/html/rfc7540#section-6.1 for data frame, and
// https://datatracker.ietf.org/doc/html/rfc7540#section-6.2 for headers frame.
is_headers_or_rst_frame = current_frame.type == kHeadersFrame || current_frame.type == kRSTStreamFrame;
is_data_end_of_stream = ((current_frame.flags & HTTP2_END_OF_STREAM) == HTTP2_END_OF_STREAM) && (current_frame.type == kDataFrame);
if (is_headers_or_rst_frame || is_data_end_of_stream) {
frames_array[interesting_frame_index].frame = current_frame;
frames_array[interesting_frame_index].offset = skb_info->data_off;
interesting_frame_index++;
}
skb_info->data_off += current_frame.length;

// Checking we can read HTTP2_FRAME_HEADER_SIZE from the skb.
if (skb_info->data_off + HTTP2_FRAME_HEADER_SIZE > skb_info->data_end) {
break;
}
if (interesting_frame_index >= HTTP2_MAX_FRAMES_ITERATIONS) {
break;
}

bpf_skb_load_bytes(skb, skb_info->data_off, (char *)&current_frame, HTTP2_FRAME_HEADER_SIZE);
skb_info->data_off += HTTP2_FRAME_HEADER_SIZE;
if (!format_http2_frame_header(&current_frame)) {
break;
}

// END_STREAM can appear only in Headers and Data frames.
// Check out https://datatracker.ietf.org/doc/html/rfc7540#section-6.1 for data frame, and
// https://datatracker.ietf.org/doc/html/rfc7540#section-6.2 for headers frame.
is_headers_or_rst_frame = current_frame.type == kHeadersFrame || current_frame.type == kRSTStreamFrame;
is_data_end_of_stream = ((current_frame.flags & HTTP2_END_OF_STREAM) == HTTP2_END_OF_STREAM) && (current_frame.type == kDataFrame);
if (interesting_frame_index < HTTP2_MAX_FRAMES_ITERATIONS && (is_headers_or_rst_frame || is_data_end_of_stream)) {
frames_array[interesting_frame_index].frame = current_frame;
frames_array[interesting_frame_index].offset = skb_info->data_off;
interesting_frame_index++;
}
skb_info->data_off += current_frame.length;
}

return interesting_frame_index;
}

SEC("socket/http2_filter")
int socket__http2_filter(struct __sk_buff *skb) {
SEC("socket/http2_handle_first_frame")
int socket__http2_handle_first_frame(struct __sk_buff *skb) {
const __u32 zero = 0;
struct http2_frame current_frame = {};

dispatcher_arguments_t dispatcher_args_copy;
bpf_memset(&dispatcher_args_copy, 0, sizeof(dispatcher_arguments_t));
if (!fetch_dispatching_arguments(&dispatcher_args_copy.tup, &dispatcher_args_copy.skb_info)) {
return 0;
// We're not calling fetch_dispatching_arguments as, we need to modify the `data_off` field of skb_info, so
// the next prog will start to read from the next valid frame.
dispatcher_arguments_t *args = bpf_map_lookup_elem(&dispatcher_arguments, &zero);
if (args == NULL) {
return false;
}
bpf_memcpy(&dispatcher_args_copy.tup, &args->tup, sizeof(conn_tuple_t));
bpf_memcpy(&dispatcher_args_copy.skb_info, &args->skb_info, sizeof(skb_info_t));

// If we detected a tcp termination we should stop processing the packet, and clear its dynamic table by deleting the counter.
if (is_tcp_termination(&dispatcher_args_copy.skb_info)) {
@@ -531,6 +536,56 @@ int socket__http2_filter(struct __sk_buff *skb) {
return 0;
}

// A single packet can contain multiple HTTP/2 frames, due to instruction limitations we have divided the
// processing into multiple tail calls, where each tail call process a single frame. We must have context when
// we are processing the frames, for example, to know how many bytes have we read in the packet, or it we reached
// to the maximum number of frames we can process. For that we are checking if the iteration context already exists.
// If not, creating a new one to be used for further processing
http2_tail_call_state_t *iteration_value = bpf_map_lookup_elem(&http2_frames_to_process, &zero);
if (iteration_value == NULL) {
return 0;
}
iteration_value->frames_count = 0;
iteration_value->iteration = 0;

// Filter preface.
skip_preface(skb, &dispatcher_args_copy.skb_info);

frame_header_remainder_t *frame_state = bpf_map_lookup_elem(&http2_remainder, &dispatcher_args_copy.tup);

if (!get_first_frame(skb, &dispatcher_args_copy.skb_info, frame_state, &current_frame)) {
return 0;
}

// If we have a state and we consumed it, then delete it.
if (frame_state != NULL && frame_state->remainder == 0) {
bpf_map_delete_elem(&http2_remainder, &dispatcher_args_copy.tup);
}

bool is_headers_or_rst_frame = current_frame.type == kHeadersFrame || current_frame.type == kRSTStreamFrame;
bool is_data_end_of_stream = ((current_frame.flags & HTTP2_END_OF_STREAM) == HTTP2_END_OF_STREAM) && (current_frame.type == kDataFrame);
if (is_headers_or_rst_frame || is_data_end_of_stream) {
iteration_value->frames_array[0].frame = current_frame;
iteration_value->frames_array[0].offset = dispatcher_args_copy.skb_info.data_off;
iteration_value->frames_count = 1;
}
dispatcher_args_copy.skb_info.data_off += current_frame.length;
// Overriding the data_off field of the cached skb_info. The next prog will start from the offset of the next valid
// frame.
args->skb_info.data_off = dispatcher_args_copy.skb_info.data_off;

bpf_tail_call_compat(skb, &protocols_progs, PROG_HTTP2_FRAME_FILTER);
return 0;
}

SEC("socket/http2_filter")
int socket__http2_filter(struct __sk_buff *skb) {
dispatcher_arguments_t dispatcher_args_copy;
bpf_memset(&dispatcher_args_copy, 0, sizeof(dispatcher_arguments_t));
if (!fetch_dispatching_arguments(&dispatcher_args_copy.tup, &dispatcher_args_copy.skb_info)) {
return 0;
}

const __u32 zero = 0;

// A single packet can contain multiple HTTP/2 frames, due to instruction limitations we have divided the
@@ -542,20 +597,15 @@ int socket__http2_filter(struct __sk_buff *skb) {
if (iteration_value == NULL) {
return 0;
}
bpf_memset(iteration_value->frames_array, 0, HTTP2_MAX_FRAMES_ITERATIONS * sizeof(http2_frame_with_offset));

// Some functions might change and override fields in dispatcher_args_copy.skb_info. Since it is used as a key
// in a map, we cannot allow it to be modified. Thus, having a local copy of skb_info.
skb_info_t local_skb_info = dispatcher_args_copy.skb_info;

frame_header_remainder_t *frame_state = bpf_map_lookup_elem(&http2_remainder, &dispatcher_args_copy.tup);
// The verifier cannot tell if `iteration_value->frames_count` is 0 or 1, so we have to help it. The value is
// 1 if we have found an interesting frame in `socket__http2_handle_first_frame`, otherwise it is 0.
// filter frames
iteration_value->frames_count = find_relevant_headers(skb, &local_skb_info, iteration_value->frames_array, frame_state);

// if we have a state and we consumed it, then delete it.
if (frame_state != NULL && frame_state->remainder == 0) {
bpf_map_delete_elem(&http2_remainder, &dispatcher_args_copy.tup);
}
iteration_value->frames_count = find_relevant_headers(skb, &local_skb_info, iteration_value->frames_array, iteration_value->frames_count);

frame_header_remainder_t new_frame_state = {0};
if (local_skb_info.data_off > local_skb_info.data_end) {
12 changes: 9 additions & 3 deletions pkg/network/protocols/ebpf.go
Original file line number Diff line number Diff line change
@@ -28,10 +28,16 @@ const (
type ProgramType C.protocol_prog_t

const (
ProgramHTTP ProgramType = C.PROG_HTTP
ProgramHTTP2 ProgramType = C.PROG_HTTP2
// ProgramHTTP is the Golang representation of the C.PROG_HTTP enum
ProgramHTTP ProgramType = C.PROG_HTTP
// ProgramHTTP2HandleFirstFrame is the Golang representation of the C.PROG_HTTP2_HANDLE_FIRST_FRAME enum
ProgramHTTP2HandleFirstFrame ProgramType = C.PROG_HTTP2_HANDLE_FIRST_FRAME
// ProgramHTTP2FrameFilter is the Golang representation of the C.PROG_HTTP2_HANDLE_FRAME enum
ProgramHTTP2FrameFilter ProgramType = C.PROG_HTTP2_FRAME_FILTER
// ProgramHTTP2FrameParser is the Golang representation of the C.PROG_HTTP2_FRAME_PARSER enum
ProgramHTTP2FrameParser ProgramType = C.PROG_HTTP2_FRAME_PARSER
ProgramKafka ProgramType = C.PROG_KAFKA
// ProgramKafka is the Golang representation of the C.PROG_KAFKA enum
ProgramKafka ProgramType = C.PROG_KAFKA
)

func Application(protoNum uint8) ProtocolType {
26 changes: 17 additions & 9 deletions pkg/network/protocols/http2/protocol.go
Original file line number Diff line number Diff line change
@@ -37,14 +37,15 @@ type protocol struct {
}

const (
inFlightMap = "http2_in_flight"
dynamicTable = "http2_dynamic_table"
dynamicTableCounter = "http2_dynamic_counter_table"
http2IterationsTable = "http2_iterations"
staticTable = "http2_static_table"
filterTailCall = "socket__http2_filter"
parserTailCall = "socket__http2_frames_parser"
eventStream = "http2"
inFlightMap = "http2_in_flight"
dynamicTable = "http2_dynamic_table"
dynamicTableCounter = "http2_dynamic_counter_table"
http2IterationsTable = "http2_iterations"
staticTable = "http2_static_table"
firstFrameHandlerTailCall = "socket__http2_handle_first_frame"
filterTailCall = "socket__http2_filter"
parserTailCall = "socket__http2_frames_parser"
eventStream = "http2"
)

var Spec = &protocols.ProtocolSpec{
@@ -81,7 +82,14 @@ var Spec = &protocols.ProtocolSpec{
TailCalls: []manager.TailCallRoute{
{
ProgArrayName: protocols.ProtocolDispatcherProgramsMap,
Key: uint32(protocols.ProgramHTTP2),
Key: uint32(protocols.ProgramHTTP2HandleFirstFrame),
ProbeIdentificationPair: manager.ProbeIdentificationPair{
EBPFFuncName: firstFrameHandlerTailCall,
},
},
{
ProgArrayName: protocols.ProtocolDispatcherProgramsMap,
Key: uint32(protocols.ProgramHTTP2FrameFilter),
ProbeIdentificationPair: manager.ProbeIdentificationPair{
EBPFFuncName: filterTailCall,
},

0 comments on commit 108d837

Please sign in to comment.