From cddc11b310d65e2d80739ebe2c539070c46d1dbd Mon Sep 17 00:00:00 2001 From: Matus Fabian Date: Wed, 1 Oct 2025 12:40:29 -0400 Subject: [PATCH] http: http/3 framing layer DATA, HEADERS, SETTINGS and GOAWAY frames parsing and serailizing Type: feature Change-Id: Id6e8ea3fe010292c73b4604496f8394c1ddba2c7 Signed-off-by: Matus Fabian --- src/plugins/http/CMakeLists.txt | 1 + src/plugins/http/http.h | 34 ++++++-- src/plugins/http/http3/frame.c | 162 ++++++++++++++++++++++++++++++++++++++ src/plugins/http/http3/frame.h | 107 +++++++++++++++++++++++++ src/plugins/http/http3/http3.h | 49 +++++++++++- src/plugins/http/test/http_test.c | 130 ++++++++++++++++++++++++++++++ 6 files changed, 474 insertions(+), 9 deletions(-) create mode 100644 src/plugins/http/http3/frame.c create mode 100644 src/plugins/http/http3/frame.h diff --git a/src/plugins/http/CMakeLists.txt b/src/plugins/http/CMakeLists.txt index 694d4fdcea6..4e5fc195b56 100644 --- a/src/plugins/http/CMakeLists.txt +++ b/src/plugins/http/CMakeLists.txt @@ -16,6 +16,7 @@ add_vpp_plugin(http http2/hpack.c http2/http2.c http2/frame.c + http3/frame.c http3/qpack.c http.c http_buffer.c diff --git a/src/plugins/http/http.h b/src/plugins/http/http.h index dca7069ce19..50400095f4e 100644 --- a/src/plugins/http/http.h +++ b/src/plugins/http/http.h @@ -1526,6 +1526,8 @@ http_parse_masque_host_port (u8 *path, u32 path_len, } #define HTTP_INVALID_VARINT ((u64) ~0) +#define HTTP_VARINT_MAX 0x3FFFFFFFFFFFFFFF +#define HTTP_VARINT_MAX_LEN 8 #define HTTP_CAPSULE_HEADER_MAX_SIZE 8 #define HTTP_UDP_PROXY_DATAGRAM_CAPSULE_OVERHEAD 5 #define HTTP_UDP_PAYLOAD_MAX_LEN 65527 @@ -1541,7 +1543,7 @@ typedef enum http_capsule_type_ /* variable-length integer (RFC9000 section 16) */ always_inline u64 -_http_decode_varint (u8 **pos, u8 *end) +http_decode_varint (u8 **pos, u8 *end) { u8 first_byte, bytes_left, *p; u64 value; @@ -1578,10 +1580,10 @@ _http_decode_varint (u8 **pos, u8 *end) } always_inline u8 * -_http_encode_varint (u8 *dst, u64 value) +http_encode_varint (u8 *dst, u64 value) { - ASSERT (value <= 0x3FFFFFFFFFFFFFFF); - if (value <= 0x3f) + ASSERT (value <= HTTP_VARINT_MAX); + if (value <= 0x3F) { *dst++ = (u8) value; return dst; @@ -1614,6 +1616,22 @@ _http_encode_varint (u8 *dst, u64 value) } } +always_inline u8 +http_varint_len (u64 value) +{ + if (value > 0x3F) + { + if (value > 0x3FFF) + { + if (value > 0x3FFFFFFF) + return 8; + return 4; + } + return 2; + } + return 1; +} + always_inline int _http_parse_capsule (u8 *data, u64 len, u64 *type, u8 *value_offset, u64 *value_len) @@ -1622,7 +1640,7 @@ _http_parse_capsule (u8 *data, u64 len, u64 *type, u8 *value_offset, u8 *p = data; u8 *end = data + len; - capsule_type = _http_decode_varint (&p, end); + capsule_type = http_decode_varint (&p, end); if (capsule_type == HTTP_INVALID_VARINT) { clib_warning ("failed to parse capsule type"); @@ -1635,7 +1653,7 @@ _http_parse_capsule (u8 *data, u64 len, u64 *type, u8 *value_offset, return -1; } - capsule_value_len = _http_decode_varint (&p, end); + capsule_value_len = http_decode_varint (&p, end); if (capsule_value_len == HTTP_INVALID_VARINT) { clib_warning ("failed to parse capsule length"); @@ -1691,7 +1709,7 @@ http_decap_udp_payload_datagram (u8 *data, u64 len, u8 *payload_offset, } /* context ID field should be zero (RFC9298 section 4) */ - context_id = _http_decode_varint (&p, end); + context_id = http_decode_varint (&p, end); if (context_id != 0) { *payload_len = value_len + value_offset; @@ -1729,7 +1747,7 @@ http_encap_udp_payload_datagram (u8 *buf, u64 payload_len) *buf++ = HTTP_CAPSULE_TYPE_DATAGRAM; /* capsule length */ - buf = _http_encode_varint (buf, payload_len + 1); + buf = http_encode_varint (buf, payload_len + 1); /* context ID */ *buf++ = 0; diff --git a/src/plugins/http/http3/frame.c b/src/plugins/http/http3/frame.c new file mode 100644 index 00000000000..b7e8a61cdc3 --- /dev/null +++ b/src/plugins/http/http3/frame.c @@ -0,0 +1,162 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright(c) 2025 Cisco Systems, Inc. + */ + +#include + +/* + * RFC9114 section 7.1 + * + * HTTP/3 Frame Format { + * Type (i), + * Length (i), + * Frame Payload (..), + * } + */ + +__clib_export http3_error_t +http3_frame_header_read (u8 *src, u64 src_len, http3_stream_type_t stream_type, + http3_frame_header_t *fh) +{ + u8 *p = src; + u8 *end = src + src_len; + + /* parse frame header */ + fh->type = http_decode_varint (&p, end); + if (fh->type == HTTP_INVALID_VARINT || p == end) + return HTTP3_ERROR_INCOMPLETE; + fh->length = http_decode_varint (&p, end); + if (fh->length == HTTP_INVALID_VARINT) + return HTTP3_ERROR_INCOMPLETE; + fh->payload = p; + + /* validate if received on correct stream type */ + switch (fh->type) + { +#define _(value, label, ctrl_stream, req_stream, push_stream) \ + case HTTP3_FRAME_TYPE_##label: \ + { \ + switch (stream_type) \ + { \ + case HTTP3_STREAM_TYPE_CONTROL: \ + if (!ctrl_stream) \ + return HTTP3_ERROR_FRAME_UNEXPECTED; \ + break; \ + case HTTP3_STREAM_TYPE_PUSH: \ + if (!push_stream) \ + return HTTP3_ERROR_FRAME_UNEXPECTED; \ + break; \ + case HTTP3_STREAM_TYPE_REQUEST: \ + if (!req_stream) \ + return HTTP3_ERROR_FRAME_UNEXPECTED; \ + break; \ + default: \ + ASSERT (0); \ + break; \ + } \ + } \ + break; + foreach_http3_frame_type +#undef _ + default : + /* ignore unknown frame type */ + break; + } + return HTTP3_ERROR_NO_ERROR; +} + +__clib_export http3_error_t +http3_frame_goaway_read (u8 *payload, u64 len, u64 *stream_or_push_id) +{ + u8 *p = payload; + u8 *end = payload + len; + + if (len == 0) + return HTTP3_ERROR_FRAME_ERROR; + + *stream_or_push_id = http_decode_varint (&p, end); + if (*stream_or_push_id == HTTP_INVALID_VARINT || p != end) + return HTTP3_ERROR_FRAME_ERROR; + + return HTTP3_ERROR_NO_ERROR; +} + +__clib_export void +http3_frame_goaway_write (u64 stream_or_push_id, u8 **dst) +{ + u8 *p; + u8 payload_len = http_varint_len (stream_or_push_id); + + vec_add2 (*dst, p, 2 + payload_len); + *p++ = (u8) HTTP3_FRAME_TYPE_GOAWAY; + *p++ = payload_len; + p = http_encode_varint (p, stream_or_push_id); +} + +__clib_export http3_error_t +http3_frame_settings_read (u8 *payload, u64 len, + http3_conn_settings_t *settings) +{ + u8 *p = payload; + u8 *end = payload + len; + u64 identifier, value; + + if (len == 0) + return HTTP3_ERROR_NO_ERROR; + + while (p != end) + { + identifier = http_decode_varint (&p, end); + if (identifier == HTTP_INVALID_VARINT || p == end) + return HTTP3_ERROR_FRAME_ERROR; + value = http_decode_varint (&p, end); + if (value == HTTP_INVALID_VARINT) + return HTTP3_ERROR_FRAME_ERROR; + switch (identifier) + { +#define _(v, label, member, min, max, default_value, server, client) \ + case HTTP3_SETTINGS_##label: \ + if (!(value >= min && value <= max)) \ + return HTTP3_ERROR_SETTINGS_ERROR; \ + settings->member = value; \ + break; + foreach_http3_settings +#undef _ + default : + /* ignore unknown or unsupported identifier */ + break; + } + } + + return HTTP3_ERROR_NO_ERROR; +} + +__clib_export void +http3_frame_settings_write (http3_conn_settings_t *settings, u8 **dst) +{ + u64 payload_len = 0; + u8 *p; + +#define _(v, label, member, min, max, default_value, server, client) \ + if (settings->member != default_value) \ + payload_len += http_varint_len (settings->member) + \ + http_varint_len (HTTP3_SETTINGS_##label); + foreach_http3_settings +#undef _ + + vec_add2 (*dst, p, 1 + http_varint_len (payload_len) + payload_len); + *p++ = (u8) HTTP3_FRAME_TYPE_SETTINGS; + p = http_encode_varint (p, payload_len); + + if (payload_len == 0) + return; + +#define _(v, label, member, min, max, default_value, server, client) \ + if (settings->member != default_value) \ + { \ + p = http_encode_varint (p, HTTP3_SETTINGS_##label); \ + p = http_encode_varint (p, settings->member); \ + } + foreach_http3_settings +#undef _ +} diff --git a/src/plugins/http/http3/frame.h b/src/plugins/http/http3/frame.h new file mode 100644 index 00000000000..e7df7ba5c52 --- /dev/null +++ b/src/plugins/http/http3/frame.h @@ -0,0 +1,107 @@ +/* SPDX-License-Identifier: Apache-2.0 + * Copyright(c) 2025 Cisco Systems, Inc. + */ + +#ifndef SRC_PLUGINS_HTTP_HTTP3_FRAME_H_ +#define SRC_PLUGINS_HTTP_HTTP3_FRAME_H_ + +#include + +#define HTTP3_FRAME_HEADER_MAX_LEN (HTTP_VARINT_MAX_LEN * 2) + +/* value, label, ctrl_stream, req_stream, push_stream */ +#define foreach_http3_frame_type \ + _ (0x00, DATA, 0, 1, 1) \ + _ (0x01, HEADERS, 0, 1, 1) \ + _ (0x03, CANCEL_PUSH, 1, 0, 0) \ + _ (0x04, SETTINGS, 1, 0, 0) \ + _ (0x05, PUSH_PROMISE, 0, 1, 0) \ + _ (0x07, GOAWAY, 1, 0, 0) \ + _ (0x0D, MAX_PUSH_ID, 1, 0, 0) + +typedef enum +{ +#define _(value, label, ctrl_stream, req_stream, push_stream) \ + HTTP3_FRAME_TYPE_##label = value, + foreach_http3_frame_type +#undef _ +} http3_frame_type_t; + +typedef struct +{ + u64 type; + u64 length; + u8 *payload; +} http3_frame_header_t; + +/** + * Parse frame header + * + * @param src Pointer to the beginning of the frame + * @param src_len Length of data available for parsing + * @param stream_type Current stream type + * @param fh Parsed frame header + * + * @return @c HTTP3_ERROR_NO_ERROR on success + */ +http3_error_t http3_frame_header_read (u8 *src, u64 src_len, + http3_stream_type_t stream_type, + http3_frame_header_t *fh); + +/** + * Write frame header + * @param type Frame type + * @param length Frame payload length + * @param dst Buffer pointer where frame header will be written + * + * @return Frame header length + */ +always_inline u8 +http3_frame_header_write (http3_frame_type_t type, u64 length, u8 *dst) +{ + *dst++ = (u8) type; + http_encode_varint (dst, length); + return http_varint_len (length) + 1; +} + +/** + * Parse GOAWAY frame payload + * + * @param payload Payload to parse + * @param len Payload length + * @param stream_or_push_id Parsed stream ID or push ID + * + * @return @c HTTP3_ERROR_NO_ERROR on success + */ +http3_error_t http3_frame_goaway_read (u8 *payload, u64 len, + u64 *stream_or_push_id); + +/** + * Write GOAWAY frame to the end of given vector + * + * @param stream_or_push_id Stream ID or push ID + * @param dst Vector where GOAWAY frame will be written + */ +void http3_frame_goaway_write (u64 stream_or_push_id, u8 **dst); + +/** + * Parse SETTINGS frame payload + * + * @param payload Payload to parse + * @param len Payload length + * @param settings HTTP/3 settings where parsed values are stored + * + * @return @c HTTP3_ERROR_NO_ERROR on success + */ +http3_error_t http3_frame_settings_read (u8 *payload, u64 len, + http3_conn_settings_t *settings); + +/** + * Write SETTINGS frame to the end of given vector + * + * @param settings HTTP/3 settings used for payload + * @param dst Vector where SETTINGS frame will be written + */ +void http3_frame_settings_write (http3_conn_settings_t *settings, u8 **dst); + +#endif /* SRC_PLUGINS_HTTP_HTTP3_FRAME_H_ */ diff --git a/src/plugins/http/http3/http3.h b/src/plugins/http/http3/http3.h index 5557e3c1a90..85787806132 100644 --- a/src/plugins/http/http3/http3.h +++ b/src/plugins/http/http3/http3.h @@ -7,6 +7,7 @@ #include #include +#include #define foreach_http3_errors \ _ (NO_ERROR, "NO_ERROR", 0x0100) \ @@ -28,7 +29,9 @@ _ (VERSION_FALLBACK, "VERSION_FALLBACK", 0x0110) \ _ (QPACK_DECOMPRESSION_FAILED, "QPACK_DECOMPRESSION_FAILED", 0x0200) \ _ (QPACK_ENCODER_STREAM_ERROR, "QPACK_ENCODER_STREAM_ERROR", 0x0201) \ - _ (QPACK_DECODER_STREAM_ERROR, "QPACK_DECODER_STREAM_ERROR", 0x0202) + _ (QPACK_DECODER_STREAM_ERROR, "QPACK_DECODER_STREAM_ERROR", 0x0202) \ + _ (INCOMPLETE, "INCOMPLETE", -1) +/* NOTE: negative values are for internal use only */ typedef enum { @@ -56,4 +59,48 @@ format_http3_error (u8 *s, va_list *va) return format (s, "%s", t); } +#define HTTP3_SETTING_VALUE_UNLIMITED (((u64) 1) << 62) + +/* value, label, member, min, max, default_value, server, client */ +#define foreach_http3_settings \ + _ (0x01, QPACK_MAX_TABLE_CAPACITY, qpack_max_table_capacity, 0, \ + HTTP_VARINT_MAX, 0, 1, 1) \ + _ (0x06, MAX_FIELD_SECTION_SIZE, max_field_section_size, 0, \ + HTTP_VARINT_MAX, HTTP3_SETTING_VALUE_UNLIMITED, 1, 1) \ + _ (0x07, QPACK_BLOCKED_STREAMS, qpack_blocked_streams, 0, HTTP_VARINT_MAX, \ + 0, 1, 1) \ + _ (0x08, ENABLE_CONNECT_PROTOCOL, enable_connect_protocol, 0, 1, 0, 1, 0) \ + _ (0x33, H3_DATAGRAM, h3_datagram, 0, 1, 0, 1, 1) + +typedef enum +{ +#define _(value, label, member, min, max, default_value, server, client) \ + HTTP3_SETTINGS_##label = value, + foreach_http3_settings +#undef _ +} http3_settings_t; + +typedef struct +{ +#define _(value, label, member, min, max, default_value, server, client) \ + u64 member; + foreach_http3_settings +#undef _ +} http3_conn_settings_t; + +static const http3_conn_settings_t http3_default_conn_settings = { +#define _(value, label, member, min, max, default_value, server, client) \ + default_value, + foreach_http3_settings +#undef _ +}; + +typedef u64 http3_stream_type_t; + +#define HTTP3_STREAM_TYPE_CONTROL 0x00 +#define HTTP3_STREAM_TYPE_PUSH 0x01 +#define HTTP3_STREAM_TYPE_ENCODER 0x02 +#define HTTP3_STREAM_TYPE_DECODER 0x03 +#define HTTP3_STREAM_TYPE_REQUEST (((u64) 1) << 62) /* internal use only */ + #endif /* SRC_PLUGINS_HTTP_HTTP3_H_ */ diff --git a/src/plugins/http/test/http_test.c b/src/plugins/http/test/http_test.c index 3d61b0b7a31..bd79dde65e5 100644 --- a/src/plugins/http/test/http_test.c +++ b/src/plugins/http/test/http_test.c @@ -10,6 +10,7 @@ #include #include #include +#include #define HTTP_TEST_I(_cond, _comment, _args...) \ ({ \ @@ -1895,6 +1896,131 @@ http_test_qpack (vlib_main_t *vm) return 0; } +static int +http_test_h3_frame (vlib_main_t *vm) +{ + vlib_cli_output (vm, "http3_frame_header_read"); + + static http3_error_t (*_http3_frame_header_read) ( + u8 * src, u64 src_len, http3_stream_type_t stream_type, + http3_frame_header_t * fh); + _http3_frame_header_read = + vlib_get_plugin_symbol ("http_plugin.so", "http3_frame_header_read"); + + http3_error_t rv; + http3_frame_header_t fh = {}; + u8 data[] = { + 0x00, 0x09, 0x6e, 0x6f, 0x74, 0x20, 0x66, 0x6f, 0x75, 0x6e, 0x64, + }; + rv = _http3_frame_header_read (data, sizeof (data), + HTTP3_STREAM_TYPE_REQUEST, &fh); + HTTP_TEST ((rv == HTTP3_ERROR_NO_ERROR && fh.type == HTTP3_FRAME_TYPE_DATA && + fh.length == 9 && fh.payload[0] == data[2]), + "frame identified as DATA"); + + rv = _http3_frame_header_read (data, 1, HTTP3_STREAM_TYPE_REQUEST, &fh); + HTTP_TEST ((rv == HTTP3_ERROR_INCOMPLETE), "frame is incomplete (rv=%d)", + rv); + + rv = _http3_frame_header_read (data, sizeof (data), + HTTP3_STREAM_TYPE_CONTROL, &fh); + HTTP_TEST ((rv == HTTP3_ERROR_FRAME_UNEXPECTED), + "frame is on wrong stream (rv=0x%04x)", rv); + + vlib_cli_output (vm, "http3_frame_header_write"); + + u8 header_buf[HTTP3_FRAME_HEADER_MAX_LEN] = {}; + u8 header_len = + http3_frame_header_write (HTTP3_FRAME_TYPE_DATA, 9, header_buf); + HTTP_TEST ((header_len == 2 && !memcmp (header_buf, data, header_len)), + "DATA frame header written: %U", format_http_bytes, header_buf, + header_len); + + vlib_cli_output (vm, "http3_frame_goaway_read"); + + static http3_error_t (*_http3_frame_goaway_read) (u8 * payload, u64 len, + u64 * stream_or_push_id); + _http3_frame_goaway_read = + vlib_get_plugin_symbol ("http_plugin.so", "http3_frame_goaway_read"); + + u8 goaway[] = { + 0x07, 0x02, 0x7B, 0xBD, 0xFE, + }; + u64 stream_id; + rv = _http3_frame_header_read (goaway, 4, HTTP3_STREAM_TYPE_CONTROL, &fh); + HTTP_TEST ((rv == HTTP3_ERROR_NO_ERROR && + fh.type == HTTP3_FRAME_TYPE_GOAWAY && fh.length == 2 && + fh.payload[0] == goaway[2]), + "frame identified as GOAWAY"); + rv = _http3_frame_goaway_read (fh.payload, fh.length, &stream_id); + HTTP_TEST ((rv == HTTP3_ERROR_NO_ERROR && stream_id == 15293), + "GOAWAY stream ID %lu (rv=0x%04x)", stream_id, rv); + + rv = _http3_frame_goaway_read (fh.payload, 1, &stream_id); + HTTP_TEST ((rv == HTTP3_ERROR_FRAME_ERROR), + "GOAWAY frame payload is corrupted (rv=0x%04x)", rv); + + rv = _http3_frame_goaway_read (fh.payload, 3, &stream_id); + HTTP_TEST ((rv == HTTP3_ERROR_FRAME_ERROR), + "GOAWAY frame payload has extra bytes (rv=0x%04x)", rv); + + vlib_cli_output (vm, "http3_frame_goaway_write"); + static void (*_http3_frame_goaway_write) (u64 stream_or_push_id, u8 * *dst); + _http3_frame_goaway_write = + vlib_get_plugin_symbol ("http_plugin.so", "http3_frame_goaway_write"); + + u8 *buf = 0; + _http3_frame_goaway_write (15293, &buf); + HTTP_TEST ((vec_len (buf) == 4 && !memcmp (buf, goaway, 4)), + "GOAWAY frame written: %U", format_hex_bytes, buf, vec_len (buf)); + vec_reset_length (buf); + + vlib_cli_output (vm, "http3_frame_settings_read"); + + static http3_error_t (*_http3_frame_settings_read) ( + u8 * payload, u64 len, http3_conn_settings_t * settings); + + _http3_frame_settings_read = + vlib_get_plugin_symbol ("http_plugin.so", "http3_frame_settings_read"); + + http3_conn_settings_t h3_settings = {}; + u8 settings[] = { + 0x04, 0x04, 0x07, 0x05, 0x08, 0x01, 0x33, 0x02, + }; + rv = _http3_frame_header_read (settings, 6, HTTP3_STREAM_TYPE_CONTROL, &fh); + HTTP_TEST ((rv == HTTP3_ERROR_NO_ERROR && + fh.type == HTTP3_FRAME_TYPE_SETTINGS && fh.length == 4 && + fh.payload[0] == settings[2]), + "frame identified as SETTINGS"); + rv = _http3_frame_settings_read (fh.payload, fh.length, &h3_settings); + HTTP_TEST ( + (rv == HTTP3_ERROR_NO_ERROR && h3_settings.qpack_blocked_streams == 5 && + h3_settings.enable_connect_protocol == 1 && + h3_settings.h3_datagram == 0 && h3_settings.max_field_section_size == 0 && + h3_settings.qpack_max_table_capacity == 0), + "SETTINGS frame payload parsed (rv=0x%04x)", rv); + rv = _http3_frame_settings_read (fh.payload, 6, &h3_settings); + HTTP_TEST ((rv == HTTP3_ERROR_SETTINGS_ERROR), + "invalid setting value (rv=0x%04x)", rv); + + vlib_cli_output (vm, "http3_frame_settings_write"); + + static void (*_http3_frame_settings_write) (http3_conn_settings_t * settings, + u8 * *dst); + _http3_frame_settings_write = + vlib_get_plugin_symbol ("http_plugin.so", "http3_frame_settings_write"); + h3_settings = http3_default_conn_settings; + h3_settings.enable_connect_protocol = 1; + h3_settings.qpack_blocked_streams = 5; + _http3_frame_settings_write (&h3_settings, &buf); + HTTP_TEST ((vec_len (buf) == 6 && !memcmp (buf, settings, 6)), + "SETTINGS frame written: %U", format_hex_bytes, buf, + vec_len (buf)); + + vec_free (buf); + return 0; +} + static clib_error_t * test_http_command_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command_t *cmd) @@ -1918,6 +2044,8 @@ test_http_command_fn (vlib_main_t *vm, unformat_input_t *input, res = http_test_h2_frame (vm); else if (unformat (input, "qpack")) res = http_test_qpack (vm); + else if (unformat (input, "h3-frame")) + res = http_test_h3_frame (vm); else if (unformat (input, "all")) { if ((res = http_test_parse_authority (vm))) @@ -1936,6 +2064,8 @@ test_http_command_fn (vlib_main_t *vm, unformat_input_t *input, goto done; if ((res = http_test_qpack (vm))) goto done; + if ((res = http_test_h3_frame (vm))) + goto done; } else break; -- 2.16.6