From: Matus Fabian Date: Mon, 6 Oct 2025 17:06:51 +0000 (-0400) Subject: quic: ALPN support X-Git-Url: https://gerrit.fd.io/r/gitweb?a=commitdiff_plain;h=refs%2Fchanges%2F46%2F43846%2F6;p=vpp.git quic: ALPN support App can pass ALPN protocols list via alpn_protos member of transport_endpt_crypto_cfg_t. For server it should be ordered by preference. If all set to zeros ALPN negotiation is disabled. In case that server supports no protocols that client advertised, then handshake fail. Type: improvement Change-Id: I1ca11dd7d4e0dbc83a01da9ded37dd62ebf37023 Signed-off-by: Matus Fabian --- diff --git a/src/plugins/quic/quic.c b/src/plugins/quic/quic.c index f934ba13ede..af06ccdb5d7 100644 --- a/src/plugins/quic/quic.c +++ b/src/plugins/quic/quic.c @@ -179,8 +179,7 @@ quic_connect_stream (session_t * quic_session, session_endpoint_cfg_t * sep) /* Find base session to which the user want to attach a stream */ quic_session_handle = session_handle (quic_session); - QUIC_DBG (2, "Connect stream: quic_session_handle 0x%lx", - quic_session_handle); + QUIC_DBG (2, "Connect stream: session 0x%lx", quic_session_handle); if (session_type_transport_proto (quic_session->session_type) != TRANSPORT_PROTO_QUIC) @@ -279,6 +278,15 @@ quic_connect_stream (session_t * quic_session, session_endpoint_cfg_t * sep) return 0; } +static_always_inline void +quic_ctx_set_alpn_protos (quic_ctx_t *ctx, transport_endpt_crypto_cfg_t *ccfg) +{ + ctx->alpn_protos[0] = ccfg->alpn_protos[0]; + ctx->alpn_protos[1] = ccfg->alpn_protos[1]; + ctx->alpn_protos[2] = ccfg->alpn_protos[2]; + ctx->alpn_protos[3] = ccfg->alpn_protos[3]; +} + static int quic_connect_connection (session_endpoint_cfg_t * sep) { @@ -330,6 +338,7 @@ quic_connect_connection (session_endpoint_cfg_t * sep) cargs->sep_ext.ns_index = app->ns_index; cargs->sep_ext.transport_flags = TRANSPORT_CFG_F_CONNECTED; + quic_ctx_set_alpn_protos (ctx, ccfg); ctx->crypto_engine = ccfg->crypto_engine; ctx->ckpair_index = ccfg->ckpair_index; error = quic_eng_crypto_context_acquire (ctx); @@ -420,6 +429,8 @@ quic_start_listen (u32 quic_listen_session_index, lctx->parent_app_id = app_wrk->app_index; lctx->udp_session_handle = udp_handle; lctx->c_s_index = quic_listen_session_index; + lctx->listener_ctx_id = lctx_index; + quic_ctx_set_alpn_protos (lctx, ccfg); lctx->crypto_engine = ccfg->crypto_engine; lctx->ckpair_index = ccfg->ckpair_index; if ((rv = quic_eng_crypto_context_acquire (lctx))) @@ -847,6 +858,14 @@ quic_get_transport_endpoint (u32 ctx_index, clib_thread_index_t thread_index, quic_common_get_transport_endpoint (ctx, tep, is_lcl); } +static tls_alpn_proto_t +quic_get_alpn_selected (u32 ctx_index, clib_thread_index_t thread_index) +{ + quic_ctx_t *ctx; + ctx = quic_ctx_get (ctx_index, thread_index); + return ctx->alpn_selected; +} + static session_cb_vft_t quic_app_cb_vft = { .session_accept_callback = quic_udp_session_accepted_callback, .session_disconnect_callback = quic_udp_session_disconnect_callback, @@ -878,6 +897,7 @@ static transport_proto_vft_t quic_proto = { .format_listener = format_quic_listener, .get_transport_endpoint = quic_get_transport_endpoint, .get_transport_listener_endpoint = quic_get_transport_listener_endpoint, + .get_alpn_selected = quic_get_alpn_selected, .transport_options = { .name = "quic", .short_name = "Q", diff --git a/src/plugins/quic/quic.h b/src/plugins/quic/quic.h index b98310d100b..5297db7e3ef 100644 --- a/src/plugins/quic/quic.h +++ b/src/plugins/quic/quic.h @@ -168,6 +168,8 @@ typedef struct quic_ctx_ u32 ckpair_index; u32 crypto_engine; u32 crypto_context_index; + u8 alpn_protos[4]; + tls_alpn_proto_t alpn_selected; u8 flags; struct diff --git a/src/plugins/quic_quicly/quic_quicly.c b/src/plugins/quic_quicly/quic_quicly.c index f77d86396e8..dfe5679290b 100644 --- a/src/plugins/quic_quicly/quic_quicly.c +++ b/src/plugins/quic_quicly/quic_quicly.c @@ -895,7 +895,7 @@ quic_quicly_accept_connection (quic_quicly_rx_packet_ctx_t *pctx) quic_session = session_alloc (ctx->c_thread_index); QUIC_DBG (2, - "Accept connection (new quic_session): session_handle 0x%lx, " + "Accept connection (new quic_session): session 0x%lx, " "session_index %u, ctx_index %u, thread %u", session_handle (quic_session), quic_session->session_index, ctx->c_c_index, ctx->c_thread_index); @@ -917,6 +917,17 @@ quic_quicly_accept_connection (quic_quicly_rx_packet_ctx_t *pctx) 2, "Accept connection: conn key value 0x%llx, ctx_index %u, thread %u", kv.value, pctx->ctx_index, pctx->thread_index); + if (lctx->alpn_protos[0]) + { + const char *proto = ptls_get_negotiated_protocol (quicly_get_tls (conn)); + if (proto) + { + tls_alpn_proto_id_t id = { .base = (u8 *) proto, + .len = strlen (proto) }; + ctx->alpn_selected = tls_alpn_proto_by_str (&id); + } + } + /* If notify fails, reset connection immediatly */ rv = app_worker_init_accepted (quic_session); if (rv) @@ -1027,14 +1038,27 @@ quic_quicly_connect (quic_ctx_t *ctx, u32 ctx_index, { clib_bihash_kv_16_8_t kv; quicly_context_t *quicly_ctx; + ptls_iovec_t alpn_list[4]; + ptls_handshake_properties_t hs_properties = { + .client.negotiated_protocols.list = alpn_list + }; + const tls_alpn_proto_id_t *alpn_proto; quic_quicly_main_t *qqm = &quic_quicly_main; - int ret; + int ret, i; + /* build alpn list if app provided something */ + for (i = 0; i < sizeof (ctx->alpn_protos) && ctx->alpn_protos[i]; i++) + { + alpn_proto = &tls_alpn_proto_ids[ctx->alpn_protos[i]]; + alpn_list[i].base = alpn_proto->base; + alpn_list[i].len = (size_t) alpn_proto->len; + hs_properties.client.negotiated_protocols.count++; + } quicly_ctx = quic_quicly_get_quicly_ctx_from_ctx (ctx); - ret = quicly_connect ( - (quicly_conn_t **) &ctx->conn, quicly_ctx, (char *) ctx->srv_hostname, sa, - NULL, &qqm->next_cid[thread_index], ptls_iovec_init (NULL, 0), - &qqm->hs_properties, NULL, NULL); + ret = quicly_connect ((quicly_conn_t **) &ctx->conn, quicly_ctx, + (char *) ctx->srv_hostname, sa, NULL, + &qqm->next_cid[thread_index], + ptls_iovec_init (NULL, 0), &hs_properties, NULL, NULL); ++qqm->next_cid[thread_index].master_id; /* save context handle in quicly connection */ quic_quicly_store_conn_ctx (ctx->conn, ctx); @@ -1289,6 +1313,19 @@ quic_quicly_on_quic_session_connected (quic_ctx_t *ctx) quic_session->session_type = session_type_from_proto_and_ip (TRANSPORT_PROTO_QUIC, ctx->udp_is_ip4); + if (ctx->alpn_protos[0]) + { + const char *proto = + ptls_get_negotiated_protocol (quicly_get_tls (ctx->conn)); + if (proto) + { + QUIC_DBG (2, "alpn proto selected %s", proto); + tls_alpn_proto_id_t id = { .base = (u8 *) proto, + .len = strlen (proto) }; + ctx->alpn_selected = tls_alpn_proto_by_str (&id); + } + } + /* If quic session connected fails, immediatly close connection */ app_wrk = app_worker_get (ctx->parent_app_wrk_id); if ((rv = app_worker_init_connected (app_wrk, quic_session))) diff --git a/src/plugins/quic_quicly/quic_quicly.h b/src/plugins/quic_quicly/quic_quicly.h index d5610bf84e2..39f1a670b2c 100644 --- a/src/plugins/quic_quicly/quic_quicly.h +++ b/src/plugins/quic_quicly/quic_quicly.h @@ -56,7 +56,6 @@ typedef struct quic_quicly_main_ quic_main_t *qm; ptls_cipher_suite_t ***quic_ciphers; u32 *per_thread_crypto_key_indices; - ptls_handshake_properties_t hs_properties; clib_bihash_16_8_t connection_hash; /**< quic connection id -> conn handle */ quic_quicly_session_cache_t session_cache; quicly_cid_plaintext_t *next_cid; diff --git a/src/plugins/quic_quicly/quic_quicly_crypto.c b/src/plugins/quic_quicly/quic_quicly_crypto.c index dc3a16a9582..4ff93ecaf7d 100644 --- a/src/plugins/quic_quicly/quic_quicly_crypto.c +++ b/src/plugins/quic_quicly/quic_quicly_crypto.c @@ -206,16 +206,20 @@ quic_quicly_on_closed_by_remote (quicly_closed_by_remote_t *self, #if QUIC_DEBUG >= 2 if (ctx->c_s_index == QUIC_SESSION_INVALID) { - clib_warning ("Unopened Session closed by peer (%U) %.*S ", - quic_quicly_format_err, code, reason_len, reason); + clib_warning ("Unopened Session closed by peer: error %U, reason %U, " + "ctx_index %u, thread %u", + quic_quicly_format_err, code, format_ascii_bytes, reason, + reason_len, ctx->c_c_index, ctx->c_thread_index); } else { session_t *quic_session = session_get (ctx->c_s_index, ctx->c_thread_index); - clib_warning ("Session 0x%lx closed by peer (%U) %.*s ", + clib_warning ("Session closed by peer: session 0x%lx, error %U, reason " + "%U, ctx_index %u, thread %u", session_handle (quic_session), quic_quicly_format_err, - code, reason_len, reason); + code, format_ascii_bytes, reason, reason_len, + ctx->c_c_index, ctx->c_thread_index); } #endif ctx->conn_state = QUIC_CONN_STATE_PASSIVE_CLOSING; @@ -239,6 +243,67 @@ static quicly_closed_by_remote_t on_closed_by_remote = { }; static quicly_now_t quicly_vpp_now_cb = { quic_quicly_get_time }; +static int +quic_quicly_on_client_hello_ptls (ptls_on_client_hello_t *self, ptls_t *tls, + ptls_on_client_hello_parameters_t *params) +{ + quic_quicly_on_client_hello_t *ch_ctx = + (quic_quicly_on_client_hello_t *) self; + quic_ctx_t *lctx; + const tls_alpn_proto_id_t *alpn_proto; + int i, j, ret; + + lctx = quic_quicly_get_quic_ctx (ch_ctx->lctx_index, 0); + + /* handle ALPN, both sides need to offer something */ + if (params->negotiated_protocols.count && lctx->alpn_protos[0]) + { + for (i = 0; i < sizeof (lctx->alpn_protos) && lctx->alpn_protos[i]; i++) + { + alpn_proto = &tls_alpn_proto_ids[lctx->alpn_protos[i]]; + for (j = 0; j < params->negotiated_protocols.count; j++) + { + if (alpn_proto->len != params->negotiated_protocols.list[j].len) + continue; + if (!memcmp (alpn_proto->base, + params->negotiated_protocols.list[j].base, + alpn_proto->len)) + goto alpn_proto_match; + } + } +#if QUIC_DEBUG >= 2 + u8 *client_alpn_list = 0; + for (j = 0; j < params->negotiated_protocols.count; j++) + { + if (j > 0) + vec_add (client_alpn_list, ", ", 2); + vec_add (client_alpn_list, params->negotiated_protocols.list[j].base, + params->negotiated_protocols.list[j].len); + } + clib_warning ( + "unsupported alpn proto(s) requested by client: proto [%U], " + "ctx_index %u, thread %u", + format_ascii_bytes, client_alpn_list, + (uword) vec_len (client_alpn_list), lctx->c_c_index, + lctx->c_thread_index); +#endif + return PTLS_ALERT_NO_APPLICATION_PROTOCOL; + alpn_proto_match: + if ((ret = ptls_set_negotiated_protocol (tls, (char *) alpn_proto->base, + alpn_proto->len)) != 0) + { + QUIC_ERR ("ptls_set_negotiated_protocol failed: error %d, ctx_index " + "%u, thread %u", + ret, lctx->c_c_index, lctx->c_thread_index); + return ret; + } + QUIC_DBG (2, "alpn proto selected %U, ctx_index %u, thread %u", + format_ascii_bytes, alpn_proto->base, (uword) alpn_proto->len, + lctx->c_c_index, lctx->c_thread_index); + } + return 0; +} + static int quic_quicly_init_crypto_context (crypto_context_t *crctx, quic_ctx_t *ctx) { @@ -300,6 +365,9 @@ quic_quicly_init_crypto_context (crypto_context_t *crctx, quic_ctx_t *ctx) quicly_ctx = &data->quicly_ctx; ptls_ctx = &data->ptls_ctx; + data->client_hello_ctx.super.cb = quic_quicly_on_client_hello_ptls; + data->client_hello_ctx.lctx_index = ctx->listener_ctx_id; + ptls_ctx->random_bytes = ptls_openssl_random_bytes; ptls_ctx->get_time = &ptls_get_time; ptls_ctx->key_exchanges = ptls_openssl_key_exchanges; @@ -309,7 +377,7 @@ quic_quicly_init_crypto_context (crypto_context_t *crctx, quic_ctx_t *ctx) ptls_ctx->cipher_suites); ptls_ctx->certificates.list = NULL; ptls_ctx->certificates.count = 0; - ptls_ctx->on_client_hello = NULL; + ptls_ctx->on_client_hello = &data->client_hello_ctx.super; ptls_ctx->emit_certificate = NULL; ptls_ctx->sign_certificate = NULL; ptls_ctx->verify_certificate = NULL; diff --git a/src/plugins/quic_quicly/quic_quicly_crypto.h b/src/plugins/quic_quicly/quic_quicly_crypto.h index 75abab28ce3..5aeb8541ad4 100644 --- a/src/plugins/quic_quicly/quic_quicly_crypto.h +++ b/src/plugins/quic_quicly/quic_quicly_crypto.h @@ -64,11 +64,18 @@ struct cipher_context_t crypto_key_t key; }; +typedef struct quic_quicly_on_client_hello_ +{ + ptls_on_client_hello_t super; + u32 lctx_index; +} quic_quicly_on_client_hello_t; + typedef struct quic_quicly_crypto_context_data_ { quicly_context_t quicly_ctx; char cid_key[QUIC_IV_LEN]; ptls_context_t ptls_ctx; + quic_quicly_on_client_hello_t client_hello_ctx; } quic_quicly_crypto_context_data_t; static_always_inline u8 diff --git a/test-c/hs-test/quic_test.go b/test-c/hs-test/quic_test.go new file mode 100644 index 00000000000..48d235952ec --- /dev/null +++ b/test-c/hs-test/quic_test.go @@ -0,0 +1,88 @@ +package main + +import ( + . "fd.io/hs-test/infra" +) + +func init() { + RegisterVethTests(QuicAlpMatchTest, QuicAlpnOverlapMatchTest, QuicAlpnServerPriorityMatchTest, QuicAlpnMismatchTest, QuicAlpnEmptyServerListTest, QuicAlpnEmptyClientListTest) +} + +func QuicAlpMatchTest(s *VethsSuite) { + serverAddress := s.Interfaces.Server.Ip4AddressString() + ":" + s.Ports.Port1 + s.Log(s.Containers.ServerVpp.VppInstance.Vppctl("test alpn server alpn-proto1 3 uri quic://" + serverAddress)) + + uri := "quic://" + serverAddress + o := s.Containers.ClientVpp.VppInstance.Vppctl("test alpn client alpn-proto1 3 uri " + uri) + s.Log(o) + s.AssertNotContains(o, "connect failed") + s.AssertNotContains(o, "timeout") + // selected based on 1:1 match + s.AssertContains(o, "ALPN selected: h3") +} + +func QuicAlpnOverlapMatchTest(s *VethsSuite) { + serverAddress := s.Interfaces.Server.Ip4AddressString() + ":" + s.Ports.Port1 + s.Log(s.Containers.ServerVpp.VppInstance.Vppctl("test alpn server alpn-proto1 3 alpn-proto2 1 uri quic://" + serverAddress)) + + uri := "quic://" + serverAddress + o := s.Containers.ClientVpp.VppInstance.Vppctl("test alpn client alpn-proto1 2 alpn-proto2 3 uri " + uri) + s.Log(o) + s.AssertNotContains(o, "connect failed") + s.AssertNotContains(o, "timeout") + // selected based on overlap + s.AssertContains(o, "ALPN selected: h3") +} + +func QuicAlpnServerPriorityMatchTest(s *VethsSuite) { + serverAddress := s.Interfaces.Server.Ip4AddressString() + ":" + s.Ports.Port1 + s.Log(s.Containers.ServerVpp.VppInstance.Vppctl("test alpn server alpn-proto1 3 alpn-proto2 1 uri quic://" + serverAddress)) + + uri := "quic://" + serverAddress + o := s.Containers.ClientVpp.VppInstance.Vppctl("test alpn client alpn-proto1 1 alpn-proto2 3 uri " + uri) + s.Log(o) + s.AssertNotContains(o, "connect failed") + s.AssertNotContains(o, "timeout") + // selected based on server priority + s.AssertContains(o, "ALPN selected: h3") +} + +func QuicAlpnMismatchTest(s *VethsSuite) { + s.Skip("QUIC bug: handshake failure not reported to client app as connect error, skipping...") + serverAddress := s.Interfaces.Server.Ip4AddressString() + ":" + s.Ports.Port1 + s.Log(s.Containers.ServerVpp.VppInstance.Vppctl("test alpn server alpn-proto1 2 alpn-proto2 1 uri quic://" + serverAddress)) + + uri := "quic://" + serverAddress + o := s.Containers.ClientVpp.VppInstance.Vppctl("test alpn client alpn-proto1 3 alpn-proto2 4 uri " + uri) + s.Log(o) + s.AssertNotContains(o, "timeout") + s.AssertNotContains(o, "ALPN selected") + // connection refused on mismatch + s.AssertContains(o, "connect error failed quic handshake") +} + +func QuicAlpnEmptyServerListTest(s *VethsSuite) { + serverAddress := s.Interfaces.Server.Ip4AddressString() + ":" + s.Ports.Port1 + s.Log(s.Containers.ServerVpp.VppInstance.Vppctl("test alpn server uri quic://" + serverAddress)) + + uri := "quic://" + serverAddress + o := s.Containers.ClientVpp.VppInstance.Vppctl("test alpn client alpn-proto1 3 alpn-proto2 2 uri " + uri) + s.Log(o) + s.AssertNotContains(o, "connect failed") + s.AssertNotContains(o, "timeout") + // no alpn negotiation + s.AssertContains(o, "ALPN selected: none") +} + +func QuicAlpnEmptyClientListTest(s *VethsSuite) { + serverAddress := s.Interfaces.Server.Ip4AddressString() + ":" + s.Ports.Port1 + s.Log(s.Containers.ServerVpp.VppInstance.Vppctl("test alpn server alpn-proto1 3 alpn-proto2 1 uri quic://" + serverAddress)) + + uri := "quic://" + serverAddress + o := s.Containers.ClientVpp.VppInstance.Vppctl("test alpn client uri " + uri) + s.Log(o) + s.AssertNotContains(o, "connect failed") + s.AssertNotContains(o, "timeout") + // no alpn negotiation + s.AssertContains(o, "ALPN selected: none") +}