http: http/2 starting tcp connection 42/42642/6
authorMatus Fabian <[email protected]>
Wed, 2 Apr 2025 17:44:44 +0000 (13:44 -0400)
committerFlorin Coras <[email protected]>
Mon, 7 Apr 2025 19:39:37 +0000 (19:39 +0000)
Type: feature

Change-Id: Ic731cf0f758f247cd067ca13ec2901c2c01b5065
Signed-off-by: Matus Fabian <[email protected]>
extras/hs-test/http2_test.go [new file with mode: 0644]
extras/hs-test/infra/suite_h2.go [new file with mode: 0644]
src/plugins/http/http.c
src/plugins/http/http2/http2.c
src/plugins/http/http_private.h

diff --git a/extras/hs-test/http2_test.go b/extras/hs-test/http2_test.go
new file mode 100644 (file)
index 0000000..5fa3219
--- /dev/null
@@ -0,0 +1,50 @@
+package main
+
+import (
+       "fmt"
+       "strings"
+       "time"
+
+       . "fd.io/hs-test/infra"
+)
+
+func init() {
+       RegisterH2Tests(Http2TcpGetTest)
+}
+
+func Http2TcpGetTest(s *H2Suite) {
+       vpp := s.Containers.Vpp.VppInstance
+       serverAddress := s.VppAddr()
+       vpp.Vppctl("http cli server")
+       s.Log(vpp.Vppctl("show session verbose 2"))
+       args := fmt.Sprintf("--max-time 10 --noproxy '*' --http2-prior-knowledge http://%s:80/show/version", serverAddress)
+       writeOut, log := s.RunCurlContainer(s.Containers.Curl, args)
+       s.Log(vpp.Vppctl("show session verbose 2"))
+       s.AssertContains(log, "HTTP/2 200")
+       s.AssertContains(writeOut, "<html>", "<html> not found in the result!")
+       s.AssertContains(writeOut, "</html>", "</html> not found in the result!")
+
+       /* test session cleanup */
+       httpStreamCleanupDone := false
+       tcpSessionCleanupDone := false
+       for nTries := 0; nTries < 30; nTries++ {
+               o := vpp.Vppctl("show session verbose 2")
+               if !strings.Contains(o, "[T] "+serverAddress+":80->") {
+                       tcpSessionCleanupDone = true
+               }
+               if !strings.Contains(o, "[H2]") {
+                       httpStreamCleanupDone = true
+               }
+               if httpStreamCleanupDone && tcpSessionCleanupDone {
+                       break
+               }
+               time.Sleep(1 * time.Second)
+       }
+       s.AssertEqual(true, tcpSessionCleanupDone, "TCP session not cleanup")
+       s.AssertEqual(true, httpStreamCleanupDone, "HTTP/2 stream not cleanup")
+
+       /* test server app stop listen */
+       vpp.Vppctl("http cli server listener del")
+       o := vpp.Vppctl("show session verbose proto http")
+       s.AssertNotContains(o, "LISTEN")
+}
diff --git a/extras/hs-test/infra/suite_h2.go b/extras/hs-test/infra/suite_h2.go
new file mode 100644 (file)
index 0000000..54a373e
--- /dev/null
@@ -0,0 +1,90 @@
+package hst
+
+import (
+       "reflect"
+       "runtime"
+       "strings"
+
+       . "github.com/onsi/ginkgo/v2"
+)
+
+var h2Tests = map[string][]func(s *H2Suite){}
+
+type H2Suite struct {
+       HstSuite
+       Interfaces struct {
+               Tap *NetInterface
+       }
+       Containers struct {
+               Vpp  *Container
+               Curl *Container
+       }
+}
+
+func RegisterH2Tests(tests ...func(s *H2Suite)) {
+       h2Tests[getTestFilename()] = tests
+}
+
+func (s *H2Suite) SetupSuite() {
+       s.HstSuite.SetupSuite()
+       s.LoadNetworkTopology("tap")
+       s.LoadContainerTopology("single")
+       s.Interfaces.Tap = s.GetInterfaceByName("htaphost")
+       s.Containers.Vpp = s.GetContainerByName("vpp")
+       s.Containers.Curl = s.GetContainerByName("curl")
+}
+
+func (s *H2Suite) SetupTest() {
+       s.HstSuite.SetupTest()
+
+       // Setup test conditions
+       var sessionConfig Stanza
+       sessionConfig.NewStanza("session").Append("enable").Append("use-app-socket-api")
+
+       vpp, _ := s.Containers.Vpp.newVppInstance(s.Containers.Vpp.AllocatedCpus, sessionConfig)
+
+       s.AssertNil(vpp.Start())
+       s.AssertNil(vpp.CreateTap(s.Interfaces.Tap, 1, 1), "failed to create tap interface")
+
+       if *DryRun {
+               s.LogStartedContainers()
+               s.Skip("Dry run mode = true")
+       }
+}
+
+func (s *H2Suite) TearDownTest() {
+       s.HstSuite.TearDownTest()
+}
+
+func (s *H2Suite) VppAddr() string {
+       return s.Interfaces.Tap.Peer.Ip4AddressString()
+}
+
+var _ = PDescribe("Http2Suite", Ordered, ContinueOnFailure, func() {
+       var s H2Suite
+       BeforeAll(func() {
+               s.SetupSuite()
+       })
+       BeforeEach(func() {
+               s.SetupTest()
+       })
+       AfterAll(func() {
+               s.TearDownSuite()
+       })
+       AfterEach(func() {
+               s.TearDownTest()
+       })
+
+       for filename, tests := range h2Tests {
+               for _, test := range tests {
+                       test := test
+                       pc := reflect.ValueOf(test).Pointer()
+                       funcValue := runtime.FuncForPC(pc)
+                       testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2]
+                       It(testName, func(ctx SpecContext) {
+                               s.Log(testName + ": BEGIN")
+                               test(&s)
+                       }, SpecTimeout(TestTimeout))
+               }
+       }
+})
index 1099f9e..321f001 100644 (file)
@@ -420,6 +420,7 @@ http_ts_accept_callback (session_t *ts)
   http_conn_t *lhc, *hc;
   u32 hc_index, thresh;
   http_conn_handle_t hc_handle;
+  transport_proto_t tp;
 
   ts_listener = listen_session_get_from_handle (ts->listener_handle);
   lhc = http_listener_get (ts_listener->opaque);
@@ -436,8 +437,17 @@ http_ts_accept_callback (session_t *ts)
   hc->state = HTTP_CONN_STATE_ESTABLISHED;
 
   ts->session_state = SESSION_STATE_READY;
-  /* TODO: TLS set by ALPN result, TCP: will decide in http_ts_rx_callback */
-  hc->version = HTTP_VERSION_1;
+  tp = session_get_transport_proto (ts);
+  if (tp == TRANSPORT_PROTO_TLS)
+    {
+      /* TODO: set by ALPN result */
+      hc->version = HTTP_VERSION_1;
+    }
+  else
+    {
+      /* going to decide in http_ts_rx_callback */
+      hc->version = HTTP_VERSION_NA;
+    }
   hc_handle.version = hc->version;
   hc_handle.conn_index = hc_index;
   ts->opaque = hc_handle.as_u32;
@@ -449,7 +459,6 @@ http_ts_accept_callback (session_t *ts)
    * the fifo is small (under 16K) we set the threshold to it's size, meaning
    * a notification will be given when the fifo empties.
    */
-  ts = session_get_from_handle (hc->hc_tc_session_handle);
   thresh = clib_min (svm_fifo_size (ts->tx_fifo), HTTP_FIFO_THRESH);
   svm_fifo_set_deq_thresh (ts->tx_fifo, thresh);
 
@@ -532,6 +541,10 @@ http_ts_disconnect_callback (session_t *ts)
   if (hc->state < HTTP_CONN_STATE_TRANSPORT_CLOSED)
     hc->state = HTTP_CONN_STATE_TRANSPORT_CLOSED;
 
+  /* in case peer close cleartext connection before send something */
+  if (PREDICT_FALSE (hc->version == HTTP_VERSION_NA))
+    return;
+
   http_vfts[hc->version].transport_close_callback (hc);
 }
 
@@ -558,6 +571,8 @@ http_ts_rx_callback (session_t *ts)
 {
   http_conn_t *hc;
   http_conn_handle_t hc_handle;
+  u32 max_deq;
+  u8 *rx_buf;
 
   hc_handle.as_u32 = ts->opaque;
 
@@ -572,7 +587,37 @@ http_ts_rx_callback (session_t *ts)
       return 0;
     }
 
-  /* TODO: if version is unknown */
+  if (hc_handle.version == HTTP_VERSION_NA)
+    {
+      HTTP_DBG (1, "unknown http version");
+      max_deq = svm_fifo_max_dequeue_cons (ts->rx_fifo);
+      if (max_deq >= http2_conn_preface.len)
+       {
+         rx_buf = http_get_rx_buf (hc);
+         svm_fifo_peek (ts->rx_fifo, 0, http2_conn_preface.len, rx_buf);
+         if (memcmp (rx_buf, http2_conn_preface.base,
+                     http2_conn_preface.len) == 0)
+           {
+#if HTTP_2_ENABLE > 0
+             hc->version = HTTP_VERSION_2;
+             http_vfts[hc->version].conn_accept_callback (hc);
+#else
+             svm_fifo_dequeue_drop_all (ts->rx_fifo);
+             http_disconnect_transport (hc);
+             return 0;
+#endif
+           }
+         else
+           hc->version = HTTP_VERSION_1;
+       }
+      else
+       hc->version = HTTP_VERSION_1;
+
+      HTTP_DBG (1, "identified HTTP/%u",
+               hc->version == HTTP_VERSION_1 ? 1 : 2);
+      hc_handle.version = hc->version;
+      ts->opaque = hc_handle.as_u32;
+    }
   http_vfts[hc_handle.version].transport_rx_callback (hc);
 
   if (hc->state == HTTP_CONN_STATE_TRANSPORT_CLOSED)
index e31b3ee..b155a69 100644 (file)
@@ -51,6 +51,24 @@ typedef struct http2_req_
   u32 payload_len;
 } http2_req_t;
 
+#define foreach_http2_conn_flags                                              \
+  _ (EXPECT_PREFACE, "expect-preface")                                        \
+  _ (PREFACE_VERIFIED, "preface-verified")
+
+typedef enum http2_conn_flags_bit_
+{
+#define _(sym, str) HTTP2_CONN_F_BIT_##sym,
+  foreach_http2_conn_flags
+#undef _
+} http2_conn_flags_bit_t;
+
+typedef enum http2_conn_flags_
+{
+#define _(sym, str) HTTP2_CONN_F_##sym = 1 << HTTP2_CONN_F_BIT_##sym,
+  foreach_http2_conn_flags
+#undef _
+} __clib_packed http2_conn_flags_t;
+
 typedef struct http2_conn_ctx_
 {
   http2_conn_settings_t peer_settings;
@@ -255,6 +273,29 @@ http2_stream_close (http2_req_t *req)
     }
 }
 
+always_inline void
+http2_send_server_preface (http_conn_t *hc)
+{
+  u8 *response;
+  http2_main_t *h2m = &http2_main;
+  http2_settings_entry_t *setting, *settings_list = 0;
+
+#define _(v, label, member, min, max, default_value, err_code)                \
+  if (h2m->settings.member != default_value)                                  \
+    {                                                                         \
+      vec_add2 (settings_list, setting, 1);                                   \
+      setting->identifier = HTTP2_SETTINGS_##label;                           \
+      setting->value = h2m->settings.member;                                  \
+    }
+  foreach_http2_settings
+#undef _
+
+    response = http_get_tx_buf (hc);
+  http2_frame_write_settings (settings_list, &response);
+  http_io_ts_write (hc, response, vec_len (response), 0);
+  http_io_ts_after_write (hc, 0, 0, 1);
+}
+
 /*************************************/
 /* request state machine handlers RX */
 /*************************************/
@@ -844,6 +885,23 @@ http2_handle_goaway_frame (http_conn_t *hc, http2_frame_header_t *fh)
   return HTTP2_ERROR_NO_ERROR;
 }
 
+static_always_inline int
+http2_expect_preface (http_conn_t *hc, http2_conn_ctx_t *h2c)
+{
+  u8 *rx_buf;
+
+  ASSERT (hc->flags & HTTP_CONN_F_IS_SERVER);
+  h2c->flags &= ~HTTP2_CONN_F_EXPECT_PREFACE;
+
+  /* already done in http core */
+  if (h2c->flags & HTTP2_CONN_F_PREFACE_VERIFIED)
+    return 0;
+
+  rx_buf = http_get_rx_buf (hc);
+  http_io_ts_read (hc, rx_buf, http2_conn_preface.len, 1);
+  return memcmp (rx_buf, http2_conn_preface.base, http2_conn_preface.len);
+}
+
 /*****************/
 /* http core VFT */
 /*****************/
@@ -1014,6 +1072,7 @@ http2_transport_rx_callback (http_conn_t *hc)
   u32 to_deq;
   u8 *rx_buf;
   http2_error_t rv;
+  http2_conn_ctx_t *h2c;
 
   HTTP_DBG (1, "hc [%u]%x", hc->c_thread_index, hc->hc_hc_index);
 
@@ -1025,6 +1084,28 @@ http2_transport_rx_callback (http_conn_t *hc)
       return;
     }
 
+  h2c = http2_conn_ctx_get_w_thread (hc);
+  if (h2c->flags & HTTP2_CONN_F_EXPECT_PREFACE)
+    {
+      if (to_deq < http2_conn_preface.len)
+       {
+         HTTP_DBG (1, "to_deq %u is less than conn preface size", to_deq);
+         http_disconnect_transport (hc);
+         return;
+       }
+      if (http2_expect_preface (hc, h2c))
+       {
+         HTTP_DBG (1, "conn preface verification failed");
+         http_disconnect_transport (hc);
+         return;
+       }
+      http2_send_server_preface (hc);
+      http_io_ts_drain (hc, http2_conn_preface.len);
+      to_deq -= http2_conn_preface.len;
+      if (to_deq == 0)
+       return;
+    }
+
   if (PREDICT_FALSE (to_deq < HTTP2_FRAME_HEADER_SIZE))
     {
       HTTP_DBG (1, "to_deq %u is less than frame header size", to_deq);
@@ -1159,6 +1240,19 @@ http2_transport_conn_reschedule_callback (http_conn_t *hc)
   /* TODO */
 }
 
+static void
+http2_conn_accept_callback (http_conn_t *hc)
+{
+  http2_conn_ctx_t *h2c;
+
+  HTTP_DBG (1, "hc [%u]%x", hc->c_thread_index, hc->hc_hc_index);
+  h2c = http2_conn_ctx_alloc_w_thread (hc);
+  h2c->flags |= HTTP2_CONN_F_EXPECT_PREFACE;
+  /* already done in http core */
+  if (http_get_transport_proto (hc) == TRANSPORT_PROTO_TCP)
+    h2c->flags |= HTTP2_CONN_F_PREFACE_VERIFIED;
+}
+
 static void
 http2_conn_cleanup_callback (http_conn_t *hc)
 {
@@ -1269,6 +1363,7 @@ const static http_engine_vft_t http2_engine = {
   .transport_reset_callback = http2_transport_reset_callback,
   .transport_conn_reschedule_callback =
     http2_transport_conn_reschedule_callback,
+  .conn_accept_callback = http2_conn_accept_callback,
   .conn_cleanup_callback = http2_conn_cleanup_callback,
   .enable_callback = http2_enable_callback,
   .unformat_cfg_callback = http2_unformat_config_callback,
@@ -1281,6 +1376,7 @@ http2_init (vlib_main_t *vm)
 
   clib_warning ("http/2 enabled");
   h2m->settings = http2_default_conn_settings;
+  h2m->settings.max_concurrent_streams = 100; /* by default unlimited */
   http_register_engine (&http2_engine, HTTP_VERSION_2);
 
   return 0;
index 10e5bfd..f74f92b 100644 (file)
@@ -14,6 +14,9 @@
 
 #define HTTP_FIFO_THRESH (16 << 10)
 
+static const http_token_t http2_conn_preface = { http_token_lit (
+  "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") };
+
 typedef union
 {
   struct
@@ -278,6 +281,7 @@ typedef struct http_engine_vft_
   void (*transport_close_callback) (http_conn_t *hc);
   void (*transport_reset_callback) (http_conn_t *hc);
   void (*transport_conn_reschedule_callback) (http_conn_t *hc);
+  void (*conn_accept_callback) (http_conn_t *hc); /* optional */
   void (*conn_cleanup_callback) (http_conn_t *hc);
   void (*enable_callback) (void);                          /* optional */
   uword (*unformat_cfg_callback) (unformat_input_t *input); /* optional */