http: http/3 framing layer 21/43821/4
authorMatus Fabian <[email protected]>
Wed, 1 Oct 2025 16:40:29 +0000 (12:40 -0400)
committerFlorin Coras <[email protected]>
Thu, 2 Oct 2025 20:40:09 +0000 (20:40 +0000)
DATA, HEADERS, SETTINGS and GOAWAY frames parsing and serailizing

Type: feature

Change-Id: Id6e8ea3fe010292c73b4604496f8394c1ddba2c7
Signed-off-by: Matus Fabian <[email protected]>
src/plugins/http/CMakeLists.txt
src/plugins/http/http.h
src/plugins/http/http3/frame.c [new file with mode: 0644]
src/plugins/http/http3/frame.h [new file with mode: 0644]
src/plugins/http/http3/http3.h
src/plugins/http/test/http_test.c

index 694d4fd..4e5fc19 100644 (file)
@@ -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
index dca7069..5040009 100644 (file)
@@ -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 (file)
index 0000000..b7e8a61
--- /dev/null
@@ -0,0 +1,162 @@
+/* SPDX-License-Identifier: Apache-2.0
+ * Copyright(c) 2025 Cisco Systems, Inc.
+ */
+
+#include <http/http3/frame.h>
+
+/*
+ * 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 (file)
index 0000000..e7df7ba
--- /dev/null
@@ -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 <http/http3/http3.h>
+
+#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_ */
index 5557e3c..8578780 100644 (file)
@@ -7,6 +7,7 @@
 
 #include <vppinfra/format.h>
 #include <vppinfra/types.h>
+#include <http/http.h>
 
 #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_ */
index 3d61b0b..bd79dde 100644 (file)
@@ -10,6 +10,7 @@
 #include <http/http2/hpack.h>
 #include <http/http2/frame.h>
 #include <http/http3/qpack.h>
+#include <http/http3/frame.h>
 
 #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;