From b43c7090f83ec5a0fff17850ac31fcd5f76ea986 Mon Sep 17 00:00:00 2001 From: Matus Fabian Date: Wed, 2 Apr 2025 13:44:44 -0400 Subject: [PATCH] http: http/2 starting tcp connection Type: feature Change-Id: Ic731cf0f758f247cd067ca13ec2901c2c01b5065 Signed-off-by: Matus Fabian --- extras/hs-test/http2_test.go | 50 +++++++++++++++++++++ extras/hs-test/infra/suite_h2.go | 90 +++++++++++++++++++++++++++++++++++++ src/plugins/http/http.c | 53 ++++++++++++++++++++-- src/plugins/http/http2/http2.c | 96 ++++++++++++++++++++++++++++++++++++++++ src/plugins/http/http_private.h | 4 ++ 5 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 extras/hs-test/http2_test.go create mode 100644 extras/hs-test/infra/suite_h2.go diff --git a/extras/hs-test/http2_test.go b/extras/hs-test/http2_test.go new file mode 100644 index 00000000000..5fa32192652 --- /dev/null +++ b/extras/hs-test/http2_test.go @@ -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, "", " not found in the result!") + s.AssertContains(writeOut, "", " 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 index 00000000000..54a373e315b --- /dev/null +++ b/extras/hs-test/infra/suite_h2.go @@ -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)) + } + } +}) diff --git a/src/plugins/http/http.c b/src/plugins/http/http.c index 1099f9eee8d..321f0017f92 100644 --- a/src/plugins/http/http.c +++ b/src/plugins/http/http.c @@ -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) diff --git a/src/plugins/http/http2/http2.c b/src/plugins/http/http2/http2.c index e31b3eef908..b155a69a2b0 100644 --- a/src/plugins/http/http2/http2.c +++ b/src/plugins/http/http2/http2.c @@ -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; diff --git a/src/plugins/http/http_private.h b/src/plugins/http/http_private.h index 10e5bfdbed4..f74f92b0949 100644 --- a/src/plugins/http/http_private.h +++ b/src/plugins/http/http_private.h @@ -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 */ -- 2.16.6