quic: ALPN support 46/43846/6
authorMatus Fabian <[email protected]>
Mon, 6 Oct 2025 17:06:51 +0000 (13:06 -0400)
committerFlorin Coras <[email protected]>
Sun, 12 Oct 2025 23:02:15 +0000 (23:02 +0000)
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 <[email protected]>
src/plugins/quic/quic.c
src/plugins/quic/quic.h
src/plugins/quic_quicly/quic_quicly.c
src/plugins/quic_quicly/quic_quicly.h
src/plugins/quic_quicly/quic_quicly_crypto.c
src/plugins/quic_quicly/quic_quicly_crypto.h
test-c/hs-test/quic_test.go [new file with mode: 0644]

index f934ba1..af06ccd 100644 (file)
@@ -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",
index b98310d..5297db7 100644 (file)
@@ -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
index f77d863..dfe5679 100644 (file)
@@ -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)))
index d5610bf..39f1a67 100644 (file)
@@ -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;
index dc3a16a..4ff93ec 100644 (file)
@@ -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;
index 75abab2..5aeb854 100644 (file)
@@ -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 (file)
index 0000000..48d2359
--- /dev/null
@@ -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")
+}