You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by bn...@apache.org on 2022/08/08 23:45:49 UTC

[trafficserver] branch 10-Dev updated: Adding origin-side ALPN configuration. (#8972)

This is an automated email from the ASF dual-hosted git repository.

bneradt pushed a commit to branch 10-Dev
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/10-Dev by this push:
     new 791941a25 Adding origin-side ALPN configuration. (#8972)
791941a25 is described below

commit 791941a2554017acb4117ebc4c3b593763849310
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Mon Aug 8 18:45:43 2022 -0500

    Adding origin-side ALPN configuration. (#8972)
    
    Adding the ability for ATS to specify the ALPN string it sends in the
    TLS ClientHello handshake.
---
 doc/admin-guide/files/records.config.en.rst        |  45 +++++
 include/ts/apidefs.h.in                            |   1 +
 include/tscore/ink_defs.h                          |   2 +
 iocore/net/I_NetVConnection.h                      |   3 +
 iocore/net/P_SSLConfig.h                           |   3 +
 iocore/net/SSLConfig.cc                            |   9 +
 iocore/net/SSLNetVConnection.cc                    |  25 ++-
 lib/records/I_RecHttp.h                            |  24 +++
 lib/records/RecHttp.cc                             |  87 ++++++++++
 lib/records/unit_tests/test_RecHttp.cc             | 125 +++++++++++++-
 mgmt/RecordsConfig.cc                              |   2 +
 plugins/lua/ts_lua_http_config.c                   |   2 +
 proxy/ProxySession.cc                              |  10 ++
 proxy/ProxySession.h                               |  10 ++
 proxy/http/Http1ServerSession.cc                   |   4 +
 proxy/http/HttpConfig.cc                           |   4 +-
 proxy/http/HttpConfig.h                            |   2 +
 proxy/http/HttpProxyServerMain.cc                  |   4 +
 proxy/http/HttpSM.cc                               |  25 ++-
 src/shared/overridable_txn_vars.cc                 |   1 +
 src/traffic_server/InkAPI.cc                       |   6 +
 src/traffic_server/InkAPITest.cc                   |   1 +
 .../tls/tls_client_alpn_configuration.replay.yaml  | 112 +++++++++++++
 .../tls/tls_client_alpn_configuration.test.py      | 183 +++++++++++++++++++++
 24 files changed, 684 insertions(+), 6 deletions(-)

diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst
index 2d8b0cd80..506b579c4 100644
--- a/doc/admin-guide/files/records.config.en.rst
+++ b/doc/admin-guide/files/records.config.en.rst
@@ -3956,6 +3956,51 @@ Client-Related Configuration
 
    Enables (``1``) or disables (``0``) TLSv1_3 in the ATS client context. If not specified, enabled by default
 
+.. ts:cv:: CONFIG proxy.config.ssl.client.alpn_protocols STRING ""
+   :overridable:
+
+   Sets the ALPN string that |TS| will send to the origin in the ClientHello of TLS handshakes.
+   Configuring this to an empty string (the default configuration) means that the ALPN extension
+   will not be sent as a part of the TLS ClientHello.
+
+   Configuring the ALPN string provides a mechanism to control origin-side HTTP protocol
+   negotiation. Configuring this requires an understanding of the ALPN TLS protocol extension. See
+   `RFC 7301 <https://www.rfc-editor.org/rfc/rfc7301.html>`_ for details about the ALPN protocol.
+   See the official `IANA ALPN protocol registration
+   <https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids>`_
+   for the official list of ALPN protocol names. As a summary, the ALPN string is a comma-separated
+   (no spaces) list of protocol names that the TLS client (|TS| in this case) supports. On the TLS
+   server side (origin side in this case), the names are compared in order to the list of protocols
+   supported by the origin. The first match is used, thus the ALPN list should be listed in
+   decreasing order of preference. If no match is found, the TLS server is expected (per the RFC) to
+   fail the TLS handshake with a fatal "no_application_protocol" alert.
+
+   Currently, |TS| supports the following ALPN protocol names:
+
+    - ``http/1.0``
+    - ``http/1.1``
+
+   Here are some example configurations and the consequences of each:
+
+   ================================ ======================================================================
+   Value                            Description
+   ================================ ======================================================================
+   ``""``                           No ALPN extension is sent by |TS| in origin-side TLS handshakes.
+                                    |TS| will assume an HTTP/1.1 connection in this case.
+   ``"http/1.1"``                   Only HTTP/1.1 is advertized by |TS|. Thus, the origin will
+                                    either negotiate HTTP/1.1, or it will fail the handshake if that
+                                    is not supported by the origin.
+   ``"http/1.1,http/1.0"``          Both HTTP/1.1 and HTTP/1.0 are supported by |TS|, but HTTP/1.1
+                                    is preferred.
+   ``"h2,http/1.1,http/1.0"``       HTTP/2 is preferred by |TS| over HTTP/1.1 and HTTP/1.0. Thus, if the
+                                    origin supports HTTP/2, it will be used for the connection. If
+                                    not, it will fall back to HTTP/1.1 or, if that is not supported,
+                                    HTTP/1.0. (HTTP/2 to origin is currently not supported by |TS|.)
+   ``"h2"``                         |TS| only advertizes HTTP/2 support. Thus, the origin will
+                                    either negotiate HTTP/2 or fail the handshake. (HTTP/2 to origin
+                                    is currently not supported by |TS|.)
+   ================================ ======================================================================
+
 .. ts:cv:: CONFIG proxy.config.ssl.async.handshake.enabled INT 0
 
    Enables the use of OpenSSL async job during the TLS handshake.  Traffic
diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in
index f0be4e8d6..66224b975 100644
--- a/include/ts/apidefs.h.in
+++ b/include/ts/apidefs.h.in
@@ -884,6 +884,7 @@ typedef enum {
   TS_CONFIG_SSL_CLIENT_SNI_POLICY,
   TS_CONFIG_SSL_CLIENT_PRIVATE_KEY_FILENAME,
   TS_CONFIG_SSL_CLIENT_CA_CERT_FILENAME,
+  TS_CONFIG_SSL_CLIENT_ALPN_PROTOCOLS,
   TS_CONFIG_HTTP_HOST_RESOLUTION_PREFERENCE,
   TS_CONFIG_HTTP_CONNECT_DEAD_POLICY,
   TS_CONFIG_HTTP_MAX_PROXY_CYCLES,
diff --git a/include/tscore/ink_defs.h b/include/tscore/ink_defs.h
index 0202649f2..2fe735108 100644
--- a/include/tscore/ink_defs.h
+++ b/include/tscore/ink_defs.h
@@ -91,6 +91,8 @@ countof(const T (&)[N])
 #define unlikely(x) __builtin_expect(!!(x), 0)
 #endif
 
+#define MAX_ALPN_STRING 30
+
 /* Variables
  */
 extern int off;
diff --git a/iocore/net/I_NetVConnection.h b/iocore/net/I_NetVConnection.h
index 12a889040..af5cfdacb 100644
--- a/iocore/net/I_NetVConnection.h
+++ b/iocore/net/I_NetVConnection.h
@@ -228,6 +228,9 @@ struct NetVCOptions {
 
   bool tls_upstream = false;
 
+  unsigned char alpn_protocols_array[MAX_ALPN_STRING];
+  int alpn_protocols_array_size = 0;
+
   /**
    * Set to DISABLED, PERFMISSIVE, or ENFORCED
    * Controls how the server certificate verification is handled
diff --git a/iocore/net/P_SSLConfig.h b/iocore/net/P_SSLConfig.h
index 99ebf9db7..a9c854ab7 100644
--- a/iocore/net/P_SSLConfig.h
+++ b/iocore/net/P_SSLConfig.h
@@ -101,6 +101,9 @@ struct SSLConfigParams : public ConfigInfo {
   long ssl_ctx_options;
   long ssl_client_ctx_options;
 
+  unsigned char alpn_protocols_array[MAX_ALPN_STRING];
+  int alpn_protocols_array_size = 0;
+
   char *server_tls13_cipher_suites;
   char *client_tls13_cipher_suites;
   char *server_groups_list;
diff --git a/iocore/net/SSLConfig.cc b/iocore/net/SSLConfig.cc
index 0c75cd9ba..5c8d2ff5e 100644
--- a/iocore/net/SSLConfig.cc
+++ b/iocore/net/SSLConfig.cc
@@ -263,6 +263,15 @@ SSLConfigParams::initialize()
   }
 #endif
 
+  // Read in the protocol string for ALPN to origin
+  char *clientALPNProtocols = nullptr;
+  REC_ReadConfigStringAlloc(clientALPNProtocols, "proxy.config.ssl.client.alpn_protocols");
+
+  if (clientALPNProtocols) {
+    this->alpn_protocols_array_size = MAX_ALPN_STRING;
+    convert_alpn_to_wire_format(clientALPNProtocols, this->alpn_protocols_array, this->alpn_protocols_array_size);
+  }
+
 #ifdef SSL_OP_CIPHER_SERVER_PREFERENCE
   REC_ReadConfigInteger(option, "proxy.config.ssl.server.honor_cipher_order");
   if (option) {
diff --git a/iocore/net/SSLNetVConnection.cc b/iocore/net/SSLNetVConnection.cc
index bee4f5c07..cef530fcc 100644
--- a/iocore/net/SSLNetVConnection.cc
+++ b/iocore/net/SSLNetVConnection.cc
@@ -1163,6 +1163,16 @@ SSLNetVConnection::sslStartHandShake(int event, int &err)
         return EVENT_ERROR;
       }
 
+      // If it is negative, we are consciously not setting ALPN (e.g. for private server sessions)
+      if (options.alpn_protocols_array_size >= 0) {
+        if (options.alpn_protocols_array_size > 0) {
+          SSL_set_alpn_protos(this->ssl, options.alpn_protocols_array, options.alpn_protocols_array_size);
+        } else if (params->alpn_protocols_array_size > 0) {
+          // Set the ALPN protocols we are requesting.
+          SSL_set_alpn_protos(this->ssl, params->alpn_protocols_array, params->alpn_protocols_array_size);
+        }
+      }
+
       SSL_set_verify(this->ssl, SSL_VERIFY_PEER, verify_callback);
 
       // SNI
@@ -1374,9 +1384,9 @@ SSLNetVConnection::sslServerHandShakeEvent(int &err)
         }
         this->set_negotiated_protocol_id({reinterpret_cast<const char *>(proto), static_cast<size_t>(len)});
 
-        Debug("ssl", "client selected next protocol '%.*s'", len, proto);
+        Debug("ssl", "Origin selected next protocol '%.*s'", len, proto);
       } else {
-        Debug("ssl", "client did not select a next protocol");
+        Debug("ssl", "Origin did not select a next protocol");
       }
     }
 
@@ -1523,6 +1533,17 @@ SSLNetVConnection::sslClientHandShakeEvent(int &err)
         X509_free(cert);
       }
     }
+    {
+      unsigned char const *proto = nullptr;
+      unsigned int len           = 0;
+      // Make note of the negotiated protocol
+      SSL_get0_alpn_selected(ssl, &proto, &len);
+      if (len == 0) {
+        SSL_get0_next_proto_negotiated(ssl, &proto, &len);
+      }
+      Debug("ssl_alpn", "Negotiated ALPN: %.*s", len, proto);
+      this->set_negotiated_protocol_id({reinterpret_cast<const char *>(proto), static_cast<size_t>(len)});
+    }
 
     // if the handshake is complete and write is enabled reschedule the write
     if (closed == 0 && write.enabled) {
diff --git a/lib/records/I_RecHttp.h b/lib/records/I_RecHttp.h
index 36f871447..a0ee4b374 100644
--- a/lib/records/I_RecHttp.h
+++ b/lib/records/I_RecHttp.h
@@ -520,3 +520,27 @@ HttpProxyPort::findHttp(uint16_t family)
     This must be called before any proxy port parsing is done.
 */
 extern void ts_session_protocol_well_known_name_indices_init();
+
+/** Convert the comma separated ALPN protocol list to wire format.
+ *
+ * For the definition of wire format, see the NOTES section in the OpenSSL
+ * description of SSL_CTX_set_alpn_select_cb:
+ *
+ * https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_alpn_select_cb.html
+ *
+ * @param[in] protocols The comma separated list of protocols to convert to
+ *   wire format.
+ *
+ * @param[out] wire_format_buffer The output ALPN wire format string converted
+ *   from @a protocols. This is zero'd out if the conversion fails.
+ *
+ * @param[in,out] wire_format_buffer_len As an input, this is the size
+ *   allocated for @a wire_format_buffer. As an output, this is set to the final
+ *   size of @a wire_format_buffer after conversion. This is set to zero if the
+ *   conversion fails.
+ *
+ * @return True if the conversion was successful, false otherwise. Note that
+ * the wire format does not support an empty protocol list, therefore this
+ * function returns false if @a protocols is an empty string.
+ */
+bool convert_alpn_to_wire_format(std::string_view protocols, unsigned char *wire_format_buffer, int &wire_format_buffer_len);
diff --git a/lib/records/RecHttp.cc b/lib/records/RecHttp.cc
index 7e9a62d02..18d4a9c50 100644
--- a/lib/records/RecHttp.cc
+++ b/lib/records/RecHttp.cc
@@ -26,6 +26,7 @@
 #include "tscore/ink_defs.h"
 #include "tscore/TextBuffer.h"
 #include "tscore/Tokenizer.h"
+#include <cstring>
 #include <strings.h>
 #include "tscore/ink_inet.h"
 #include <string_view>
@@ -841,3 +842,89 @@ SessionProtocolNameRegistry::nameFor(int idx) const
 {
   return 0 <= idx && idx < m_n ? m_names[idx] : TextView{};
 }
+
+bool
+convert_alpn_to_wire_format(std::string_view protocols, unsigned char *wire_format_buffer, int &wire_format_buffer_len)
+{
+  // Callers expect wire_format_buffer_len to be zero'd out in the event of an
+  // error. To simplify the error handling from doing this on every return, we
+  // simply zero them out here at the start.
+  auto const orig_wire_format_buffer_len = wire_format_buffer_len;
+  memset(wire_format_buffer, 0, wire_format_buffer_len);
+  wire_format_buffer_len = 0;
+
+  if (protocols.empty()) {
+    return false;
+  }
+
+  // Parse the comma separated protocol string into a list of protocol names.
+  std::vector<std::string_view> alpn_protocols;
+  std::string_view protocol;
+  size_t pos                  = 0;
+  int computed_alpn_array_len = 0;
+  while (pos < protocols.size()) {
+    size_t next_pos = protocols.find(',', pos);
+    if (next_pos == std::string_view::npos) {
+      protocol = protocols.substr(pos);
+      pos      = protocols.size();
+    } else {
+      protocol = protocols.substr(pos, next_pos - pos);
+      pos      = next_pos + 1;
+    }
+    if (protocol.empty()) {
+      Warning("Empty protocol name in configured ALPN list: %.*s", static_cast<int>(protocols.size()), protocols.data());
+      return false;
+    }
+    if (protocol.size() > 255) {
+      // The length has to fit in one byte.
+      Warning("A protocol name larger than 255 bytes in configured ALPN list: %.*s", static_cast<int>(protocols.size()),
+              protocols.data());
+      return false;
+    }
+    // Check whether we recognize the protocol.
+    auto const protocol_index = globalSessionProtocolNameRegistry.indexFor(protocol);
+    if (protocol_index == SessionProtocolNameRegistry::INVALID) {
+      Warning("Unknown protocol name in configured ALPN list: %.*s", static_cast<int>(protocol.size()), protocol.data());
+      return false;
+    }
+    // We currently only support HTTP/1.x protocols toward the origin.
+    if (!HTTP_PROTOCOL_SET.contains(protocol_index)) {
+      Warning("Unsupported non-HTTP/1.x protocol name in configured ALPN list: %.*s", static_cast<int>(protocol.size()),
+              protocol.data());
+      return false;
+    }
+    // But not HTTP/0.9.
+    if (protocol_index == TS_ALPN_PROTOCOL_INDEX_HTTP_0_9) {
+      Warning("Unsupported \"http/0.9\" protocol name in configured ALPN list: %.*s", static_cast<int>(protocol.size()),
+              protocol.data());
+      return false;
+    }
+
+    auto const protocol_wire_format = globalSessionProtocolNameRegistry.convert_openssl_alpn_wire_format(protocol_index);
+    computed_alpn_array_len += protocol_wire_format.size();
+    if (computed_alpn_array_len > orig_wire_format_buffer_len) {
+      // We have exceeded the size of the output buffer.
+      Warning("The output ALPN length (%d bytes) is larger than the output buffer size of %d bytes", computed_alpn_array_len,
+              orig_wire_format_buffer_len);
+      return false;
+    }
+
+    alpn_protocols.push_back(protocol_wire_format);
+  }
+  if (alpn_protocols.empty()) {
+    Warning("No protocols specified in ALPN list: %.*s", static_cast<int>(protocols.size()), protocols.data());
+    return false;
+  }
+
+  // All checks pass and the protocols are parsed. Write the result to the
+  // output buffer.
+  auto *end = wire_format_buffer;
+  for (auto &protocol : alpn_protocols) {
+    auto const len = protocol.size();
+    memcpy(end, protocol.data(), len);
+    end += len;
+  }
+  wire_format_buffer_len = computed_alpn_array_len;
+  Debug("ssl_alpn", "Successfully converted ALPN list to wire format: %.*s", static_cast<int>(protocols.size()), protocols.data());
+  return true;
+}
diff --git a/lib/records/unit_tests/test_RecHttp.cc b/lib/records/unit_tests/test_RecHttp.cc
index a93b1e9b2..8177549ea 100644
--- a/lib/records/unit_tests/test_RecHttp.cc
+++ b/lib/records/unit_tests/test_RecHttp.cc
@@ -18,15 +18,17 @@
    the License.
  */
 
+#include <array>
 #include <string>
 #include <string_view>
-#include <array>
+#include <vector>
 
 #include "catch.hpp"
 
 #include "tscore/BufferWriter.h"
 #include "records/I_RecHttp.h"
 #include "test_Diags.h"
+#include "tscore/ink_defs.h"
 
 using ts::TextView;
 
@@ -97,3 +99,124 @@ TEST_CASE("RecHttp", "[librecords][RecHttp]")
     REQUIRE(view.find(":proto") == TextView::npos); // it's default, should not have this.
   }
 }
+
+struct ConvertAlpnToWireFormatTestCase {
+  std::string description;
+  std::string alpn_input;
+  unsigned char expected_alpn_wire_format[MAX_ALPN_STRING] = {0};
+  int expected_alpn_wire_format_len                        = MAX_ALPN_STRING;
+  bool expected_return                                     = true;
+};
+
+// clang-format off
+std::vector<ConvertAlpnToWireFormatTestCase> convertAlpnToWireFormatTestCases = {
+  // --------------------------------------------------------------------------
+  // Malformed input.
+  // --------------------------------------------------------------------------
+  {
+    "Empty input protocol list",
+    "",
+    { 0 },
+    0,
+    false
+  },
+  {
+    "Include an empty protocol in the list",
+    "http/1.1,,http/1.0",
+    { 0 },
+    0,
+    false
+  },
+  {
+    "A protocol that exceeds the output buffer length (MAX_ALPN_STRING)",
+    "some_really_long_protocol_name_that_exceeds_the_output_buffer_length_that_is_MAX_ALPN_STRING",
+    { 0 },
+    0,
+    false
+  },
+  {
+    "The sum of protocols exceeds the output buffer length (MAX_ALPN_STRING)",
+    "protocol_one,protocol_two,protocol_three",
+    { 0 },
+    0,
+    false
+  },
+  {
+    "A protocol that exceeds the length described by a single byte (255)",
+    "some_really_long_protocol_name_that_exceeds_255_bytes_some_really_long_protocol_name_that_exceeds_255_bytes_some_really_long_protocol_name_that_exceeds_255_bytes_some_really_long_protocol_name_that_exceeds_255_bytes_some_really_long_protocol_name_that_exceeds_255_bytes",
+    { 0 },
+    0,
+    false
+  },
+  // --------------------------------------------------------------------------
+  // Unsupported protocols.
+  // --------------------------------------------------------------------------
+  {
+    "Unrecognized protocol: HTTP/6",
+    "h6",
+    { 0 },
+    0,
+    false
+  },
+  {
+    "Single protocol: HTTP/0.9",
+    "http/0.9",
+    { 0 },
+    0,
+    false
+  },
+  {
+    "Single protocol: HTTP/2 (currently unsupported)",
+    "h2",
+    { 0 },
+    0,
+    false
+  },
+  {
+    "Single protocol: HTTP/3 (currently unsupported)",
+    "h3",
+    { 0 },
+    0,
+    false
+  },
+  {
+    "Both HTTP/1.1 and HTTP/2 (HTTP/2 is currently unsupported)",
+    "h2,http/1.1",
+    { 0 },
+    0,
+    false
+  },
+  // --------------------------------------------------------------------------
+  // Happy cases.
+  // --------------------------------------------------------------------------
+  {
+    "Single protocol: HTTP/1.1",
+    "http/1.1",
+    {0x08, 'h', 't', 't', 'p', '/', '1', '.', '1'},
+    9,
+    true
+  },
+  {
+    "Multiple protocols: HTTP/0.9, HTTP/1.0, HTTP/1.1",
+    "http/1.1,http/1.0",
+    {0x08, 'h', 't', 't', 'p', '/', '1', '.', '1', 0x08, 'h', 't', 't', 'p', '/', '1', '.', '0'},
+    18,
+    true
+  },
+};
+// clang-format on
+
+TEST_CASE("convert_alpn_to_wire_format", "[librecords][RecHttp]")
+{
+  for (auto const &test_case : convertAlpnToWireFormatTestCases) {
+    SECTION(test_case.description)
+    {
+      unsigned char alpn_wire_format[MAX_ALPN_STRING] = {0xab};
+      int alpn_wire_format_len                        = MAX_ALPN_STRING;
+      auto const result = convert_alpn_to_wire_format(test_case.alpn_input, alpn_wire_format, alpn_wire_format_len);
+      REQUIRE(result == test_case.expected_return);
+      REQUIRE(alpn_wire_format_len == test_case.expected_alpn_wire_format_len);
+      REQUIRE(memcmp(alpn_wire_format, test_case.expected_alpn_wire_format, test_case.expected_alpn_wire_format_len) == 0);
+    }
+  }
+}
diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc
index b41f8c17c..b207096a4 100644
--- a/mgmt/RecordsConfig.cc
+++ b/mgmt/RecordsConfig.cc
@@ -1134,6 +1134,8 @@ static const RecordElement RecordsConfig[] =
   ,
   {RECT_CONFIG, "proxy.config.ssl.client.certification_level", RECD_INT, "0", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-2]", RECA_NULL}
   ,
+  {RECT_CONFIG, "proxy.config.ssl.client.alpn_protocols", RECD_STRING, nullptr, RECU_RESTART_TS, RR_NULL, RECC_STR, "^[^[:space:]]*$", RECA_NULL}
+  ,
   {RECT_CONFIG, "proxy.config.ssl.server.cert.path", RECD_STRING, TS_BUILD_SYSCONFDIR, RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
   ,
   {RECT_CONFIG, "proxy.config.ssl.server.cert_chain.filename", RECD_STRING, nullptr, RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
diff --git a/plugins/lua/ts_lua_http_config.c b/plugins/lua/ts_lua_http_config.c
index 092b9af33..807008c61 100644
--- a/plugins/lua/ts_lua_http_config.c
+++ b/plugins/lua/ts_lua_http_config.c
@@ -136,6 +136,7 @@ typedef enum {
   TS_LUA_CONFIG_SSL_CLIENT_SNI_POLICY                         = TS_CONFIG_SSL_CLIENT_SNI_POLICY,
   TS_LUA_CONFIG_SSL_CLIENT_PRIVATE_KEY_FILENAME               = TS_CONFIG_SSL_CLIENT_PRIVATE_KEY_FILENAME,
   TS_LUA_CONFIG_SSL_CLIENT_CA_CERT_FILENAME                   = TS_CONFIG_SSL_CLIENT_CA_CERT_FILENAME,
+  TS_LUA_CONFIG_SSL_CLIENT_ALPN_PROTOCOLS                     = TS_CONFIG_SSL_CLIENT_ALPN_PROTOCOLS,
   TS_LUA_CONFIG_HTTP_HOST_RESOLUTION_PREFERENCE               = TS_CONFIG_HTTP_HOST_RESOLUTION_PREFERENCE,
   TS_LUA_CONFIG_PLUGIN_VC_DEFAULT_BUFFER_INDEX                = TS_CONFIG_PLUGIN_VC_DEFAULT_BUFFER_INDEX,
   TS_LUA_CONFIG_PLUGIN_VC_DEFAULT_BUFFER_WATER_MARK           = TS_CONFIG_PLUGIN_VC_DEFAULT_BUFFER_WATER_MARK,
@@ -268,6 +269,7 @@ ts_lua_var_item ts_lua_http_config_vars[] = {
   TS_LUA_MAKE_VAR_ITEM(TS_CONFIG_SSL_CLIENT_SNI_POLICY),
   TS_LUA_MAKE_VAR_ITEM(TS_CONFIG_SSL_CLIENT_PRIVATE_KEY_FILENAME),
   TS_LUA_MAKE_VAR_ITEM(TS_CONFIG_SSL_CLIENT_CA_CERT_FILENAME),
+  TS_LUA_MAKE_VAR_ITEM(TS_CONFIG_SSL_CLIENT_ALPN_PROTOCOLS),
   TS_LUA_MAKE_VAR_ITEM(TS_CONFIG_HTTP_HOST_RESOLUTION_PREFERENCE),
   TS_LUA_MAKE_VAR_ITEM(TS_CONFIG_HTTP_SERVER_MIN_KEEP_ALIVE_CONNS),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_PER_SERVER_CONNECTION_MAX),
diff --git a/proxy/ProxySession.cc b/proxy/ProxySession.cc
index c0df4f913..a165c2366 100644
--- a/proxy/ProxySession.cc
+++ b/proxy/ProxySession.cc
@@ -26,6 +26,8 @@
 #include "ProxySession.h"
 #include "P_SSLNetVConnection.h"
 
+std::map<int, std::function<PoolableSession *()>> ProtocolSessionCreateMap;
+
 ProxySession::ProxySession() : VConnection(nullptr) {}
 
 ProxySession::ProxySession(NetVConnection *vc) : VConnection(nullptr), _vc(vc) {}
@@ -313,3 +315,11 @@ ProxySession::support_sni() const
 {
   return _vc ? _vc->support_sni() : false;
 }
+
+PoolableSession *
+ProxySession::create_outbound_session(int protocol_index)
+{
+  auto iter = ProtocolSessionCreateMap.find(protocol_index);
+  ink_release_assert(iter != ProtocolSessionCreateMap.end());
+  return iter->second();
+}
diff --git a/proxy/ProxySession.h b/proxy/ProxySession.h
index 4162813db..2d02c4962 100644
--- a/proxy/ProxySession.h
+++ b/proxy/ProxySession.h
@@ -162,6 +162,16 @@ public:
     return nullptr;
   }
 
+  /** Given the ALPN protocol index, create an appropriate outbound session.
+   *
+   * @param[in] protocol_index A TS_ALPN_PROTOCOL value indicating what kind of
+   * protocol was negotiated toward the origin.
+   *
+   * @return A poolable session appropriate for the protocol provided via @a
+   * protocol_index.
+   */
+  static PoolableSession *create_outbound_session(int protocol_index);
+
   ////////////////////
   // Members
 
diff --git a/proxy/http/Http1ServerSession.cc b/proxy/http/Http1ServerSession.cc
index e0a9c8292..8d8ed825e 100644
--- a/proxy/http/Http1ServerSession.cc
+++ b/proxy/http/Http1ServerSession.cc
@@ -256,3 +256,7 @@ Http1ServerSession::new_transaction()
   trans.set_reader(this->get_remote_reader());
   return &trans;
 }
+
+std::function<PoolableSession *()> create_h1_server_session = []() -> PoolableSession * {
+  return httpServerSessionAllocator.alloc();
+};
diff --git a/proxy/http/HttpConfig.cc b/proxy/http/HttpConfig.cc
index 0323be956..0b05605a3 100644
--- a/proxy/http/HttpConfig.cc
+++ b/proxy/http/HttpConfig.cc
@@ -1411,6 +1411,7 @@ HttpConfig::startup()
   HttpEstablishStaticConfigByte(c.http_host_sni_policy, "proxy.config.http.host_sni_policy");
 
   HttpEstablishStaticConfigStringAlloc(c.oride.ssl_client_sni_policy, "proxy.config.ssl.client.sni_policy");
+  HttpEstablishStaticConfigStringAlloc(c.oride.ssl_client_alpn_protocols, "proxy.config.ssl.client.alpn_protocols");
 
   OutboundConnTrack::config_init(&c.global_outbound_conntrack, &c.oride.outbound_conntrack);
 
@@ -1691,7 +1692,8 @@ HttpConfig::reconfigure()
   params->redirect_actions_map = parse_redirect_actions(params->redirect_actions_string, params->redirect_actions_self_action);
   params->http_host_sni_policy = m_master.http_host_sni_policy;
 
-  params->oride.ssl_client_sni_policy = ats_strdup(m_master.oride.ssl_client_sni_policy);
+  params->oride.ssl_client_sni_policy     = ats_strdup(m_master.oride.ssl_client_sni_policy);
+  params->oride.ssl_client_alpn_protocols = ats_strdup(m_master.oride.ssl_client_alpn_protocols);
 
   params->negative_caching_list = m_master.negative_caching_list;
 
diff --git a/proxy/http/HttpConfig.h b/proxy/http/HttpConfig.h
index cf97a483f..148c94bb2 100644
--- a/proxy/http/HttpConfig.h
+++ b/proxy/http/HttpConfig.h
@@ -735,6 +735,7 @@ struct OverridableHttpConfigParams {
   char *ssl_client_cert_filename        = nullptr;
   char *ssl_client_private_key_filename = nullptr;
   char *ssl_client_ca_cert_filename     = nullptr;
+  char *ssl_client_alpn_protocols       = nullptr;
 
   // Host Resolution order
   HostResData host_res_data;
@@ -921,6 +922,7 @@ inline HttpConfigParams::~HttpConfigParams()
   ats_free(reverse_proxy_no_host_redirect);
   ats_free(redirect_actions_string);
   ats_free(oride.ssl_client_sni_policy);
+  ats_free(oride.ssl_client_alpn_protocols);
   ats_free(oride.host_res_data.conf_value);
 
   delete connect_ports;
diff --git a/proxy/http/HttpProxyServerMain.cc b/proxy/http/HttpProxyServerMain.cc
index 68f68102c..6bf058208 100644
--- a/proxy/http/HttpProxyServerMain.cc
+++ b/proxy/http/HttpProxyServerMain.cc
@@ -50,6 +50,8 @@
 
 HttpSessionAccept *plugin_http_accept             = nullptr;
 HttpSessionAccept *plugin_http_transparent_accept = nullptr;
+extern std::function<PoolableSession *()> create_h1_server_session;
+extern std::map<int, std::function<ProxySession *()>> ProtocolSessionCreateMap;
 
 static SLL<SSLNextProtocolAccept> ssl_plugin_acceptors;
 static Ptr<ProxyMutex> ssl_plugin_mutex;
@@ -221,6 +223,8 @@ MakeHttpProxyAcceptor(HttpProxyAcceptor &acceptor, HttpProxyPort &port, unsigned
   if (port.m_session_protocol_preference.intersects(HTTP2_PROTOCOL_SET)) {
     probe->registerEndpoint(ProtocolProbeSessionAccept::PROTO_HTTP2, new Http2SessionAccept(accept_opt));
   }
+  ProtocolSessionCreateMap.insert({TS_ALPN_PROTOCOL_INDEX_HTTP_1_0, create_h1_server_session});
+  ProtocolSessionCreateMap.insert({TS_ALPN_PROTOCOL_INDEX_HTTP_1_1, create_h1_server_session});
 
   if (port.isSSL()) {
     SSLNextProtocolAccept *ssl = new SSLNextProtocolAccept(probe, port.m_transparent_passthrough);
diff --git a/proxy/http/HttpSM.cc b/proxy/http/HttpSM.cc
index 20411f826..9c471e014 100644
--- a/proxy/http/HttpSM.cc
+++ b/proxy/http/HttpSM.cc
@@ -1795,9 +1795,20 @@ HttpSM::handle_api_return()
 PoolableSession *
 HttpSM::create_server_session(NetVConnection *netvc)
 {
-  HttpTransact::State &s  = this->t_state;
-  PoolableSession *retval = httpServerSessionAllocator.alloc();
+  // Figure out what protocol was negotiated
+  int proto_index      = SessionProtocolNameRegistry::INVALID;
+  auto const *sslnetvc = dynamic_cast<ALPNSupport *>(netvc);
+  if (sslnetvc) {
+    proto_index = sslnetvc->get_negotiated_protocol_id();
+  }
+  // No ALPN occurred. Assume it was HTTP/1.x and hope for the best
+  if (proto_index == SessionProtocolNameRegistry::INVALID) {
+    proto_index = TS_ALPN_PROTOCOL_INDEX_HTTP_1_1;
+  }
 
+  PoolableSession *retval = ProxySession::create_outbound_session(proto_index);
+
+  HttpTransact::State &s       = this->t_state;
   retval->sharing_pool         = static_cast<TSServerSessionSharingPoolType>(s.http_config_param->server_session_sharing_pool);
   retval->sharing_match        = static_cast<TSServerSessionSharingMatchMask>(s.txn_conf->server_session_sharing_match);
   MIOBuffer *netvc_read_buffer = new_MIOBuffer(HTTP_SERVER_RESP_HDR_BUFFER_INDEX);
@@ -5299,6 +5310,16 @@ HttpSM::do_http_server_open(bool raw)
   opt.set_ssl_client_cert_name(t_state.txn_conf->ssl_client_cert_filename);
   opt.ssl_client_private_key_name = t_state.txn_conf->ssl_client_private_key_filename;
   opt.ssl_client_ca_cert_name     = t_state.txn_conf->ssl_client_ca_cert_filename;
+  if (is_private()) {
+    // If the connection to origin is private, don't try to negotiate higher overhead protocols.
+    opt.alpn_protocols_array_size = -1;
+    SMDebug("ssl_alpn", "Clear ALPN for private session");
+  } else if (t_state.txn_conf->ssl_client_alpn_protocols != nullptr) {
+    opt.alpn_protocols_array_size = MAX_ALPN_STRING;
+    SMDebug("ssl_alpn", "Setting ALPN to: %s", t_state.txn_conf->ssl_client_alpn_protocols);
+    convert_alpn_to_wire_format(t_state.txn_conf->ssl_client_alpn_protocols, opt.alpn_protocols_array,
+                                opt.alpn_protocols_array_size);
+  }
 
   if (tls_upstream) {
     SMDebug("http", "calling sslNetProcessor.connect_re");
diff --git a/src/shared/overridable_txn_vars.cc b/src/shared/overridable_txn_vars.cc
index 6456f4fac..d54f58440 100644
--- a/src/shared/overridable_txn_vars.cc
+++ b/src/shared/overridable_txn_vars.cc
@@ -160,6 +160,7 @@ const std::unordered_map<std::string_view, std::tuple<const TSOverridableConfigK
      {"proxy.config.ssl.client.cert.path", {TS_CONFIG_SSL_CERT_FILEPATH, TS_RECORDDATATYPE_STRING}},
      {"proxy.config.ssl.client.private_key.filename", {TS_CONFIG_SSL_CLIENT_PRIVATE_KEY_FILENAME, TS_RECORDDATATYPE_STRING}},
      {"proxy.config.ssl.client.CA.cert.filename", {TS_CONFIG_SSL_CLIENT_CA_CERT_FILENAME, TS_RECORDDATATYPE_STRING}},
+     {"proxy.config.ssl.client.alpn_protocols", {TS_CONFIG_SSL_CLIENT_ALPN_PROTOCOLS, TS_RECORDDATATYPE_STRING}},
      {"proxy.config.hostdb.ip_resolve", {TS_CONFIG_HTTP_HOST_RESOLUTION_PREFERENCE, TS_RECORDDATATYPE_STRING}},
      {"proxy.config.plugin.vc.default_buffer_index", {TS_CONFIG_PLUGIN_VC_DEFAULT_BUFFER_INDEX, TS_RECORDDATATYPE_INT}},
      {"proxy.config.plugin.vc.default_buffer_water_mark", {TS_CONFIG_PLUGIN_VC_DEFAULT_BUFFER_WATER_MARK, TS_RECORDDATATYPE_INT}},
diff --git a/src/traffic_server/InkAPI.cc b/src/traffic_server/InkAPI.cc
index 4a1d3beac..8cee37dba 100644
--- a/src/traffic_server/InkAPI.cc
+++ b/src/traffic_server/InkAPI.cc
@@ -8872,6 +8872,7 @@ _conf_to_memberp(TSOverridableConfigKey conf, OverridableHttpConfigParams *overr
   case TS_CONFIG_SSL_CERT_FILEPATH:
   case TS_CONFIG_SSL_CLIENT_PRIVATE_KEY_FILENAME:
   case TS_CONFIG_SSL_CLIENT_CA_CERT_FILENAME:
+  case TS_CONFIG_SSL_CLIENT_ALPN_PROTOCOLS:
     // String, must be handled elsewhere
     break;
   case TS_CONFIG_PARENT_FAILURES_UPDATE_HOSTDB:
@@ -9123,6 +9124,11 @@ TSHttpTxnConfigStringSet(TSHttpTxn txnp, TSOverridableConfigKey conf, const char
       s->t_state.my_txn_conf().ssl_client_ca_cert_filename = const_cast<char *>(value);
     }
     break;
+  case TS_CONFIG_SSL_CLIENT_ALPN_PROTOCOLS:
+    if (value && length > 0) {
+      s->t_state.my_txn_conf().ssl_client_alpn_protocols = const_cast<char *>(value);
+    }
+    break;
   case TS_CONFIG_SSL_CERT_FILEPATH:
     /* noop */
     break;
diff --git a/src/traffic_server/InkAPITest.cc b/src/traffic_server/InkAPITest.cc
index 35dfbf9a0..da277bd45 100644
--- a/src/traffic_server/InkAPITest.cc
+++ b/src/traffic_server/InkAPITest.cc
@@ -8698,6 +8698,7 @@ std::array<std::string_view, TS_CONFIG_LAST_ENTRY> SDK_Overridable_Configs = {
    "proxy.config.ssl.client.sni_policy",
    "proxy.config.ssl.client.private_key.filename",
    "proxy.config.ssl.client.CA.cert.filename",
+   "proxy.config.ssl.client.alpn_protocols",
    "proxy.config.hostdb.ip_resolve",
    "proxy.config.http.connect.dead.policy",
    "proxy.config.http.max_proxy_cycles",
diff --git a/tests/gold_tests/tls/tls_client_alpn_configuration.replay.yaml b/tests/gold_tests/tls/tls_client_alpn_configuration.replay.yaml
new file mode 100644
index 000000000..9ebb7adf2
--- /dev/null
+++ b/tests/gold_tests/tls/tls_client_alpn_configuration.replay.yaml
@@ -0,0 +1,112 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+#
+# Verify negative_revalidating disabled behavior. This replay file assumes:
+#   * ATS is configured with negative_revalidating disabled.
+#   * max_stale_age is set to 6 seconds.
+#
+
+meta:
+  version: "1.0"
+
+sessions:
+
+# HTTP/1.1 over TLS.
+- protocol:
+  - name: tls
+    sni: www.example.com
+  - name: tcp
+  - name: ip
+
+  transactions:
+
+  # This test has more to do with ALPN configuration than the transactions. The
+  # following generates a simple request and response.
+  - client-request:
+      method: GET
+      url: /some/path/2
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Length, 0 ]
+        - [ X-Request, alpn_request ]
+        - [ uuid, first-request ]
+
+    proxy-request:
+      headers:
+        fields:
+        - [ X-Request, {value: 'alpn_request', as: equal } ]
+
+    server-response:
+        status: 200
+        reason: OK
+        headers:
+          fields:
+          - [ Date, "Sat, 16 Mar 2019 03:11:36 GMT" ]
+          - [ Content-Length, 36 ]
+          - [ Connection, keep-alive ]
+          - [ X-Response, alpn_response ]
+
+    proxy-response:
+      headers:
+        fields:
+        - [ X-Response, {value: 'alpn_response', as: equal } ]
+
+# HTTP/2 over TLS.
+- protocol:
+  - name: http
+    version: 2
+  - name: tls
+    sni: www.example.com
+  - name: tcp
+  - name: ip
+
+  transactions:
+
+  # This test has more to do with ALPN configuration than the transactions. The
+  # following generates a simple request and response.
+  - client-request:
+      method: GET
+      url: /some/path/2
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Length, 0 ]
+        - [ X-Request, alpn_request ]
+        - [ uuid, first-request ]
+
+    proxy-request:
+      headers:
+        fields:
+        - [ X-Request, {value: 'alpn_request', as: equal } ]
+
+    server-response:
+        status: 200
+        reason: OK
+        headers:
+          fields:
+          - [ Date, "Sat, 16 Mar 2019 03:11:36 GMT" ]
+          - [ Content-Length, 36 ]
+          - [ Connection, keep-alive ]
+          - [ X-Response, alpn_response ]
+
+    proxy-response:
+      headers:
+        fields:
+        - [ X-Response, {value: 'alpn_response', as equal } ]
diff --git a/tests/gold_tests/tls/tls_client_alpn_configuration.test.py b/tests/gold_tests/tls/tls_client_alpn_configuration.test.py
new file mode 100644
index 000000000..45d1cbb74
--- /dev/null
+++ b/tests/gold_tests/tls/tls_client_alpn_configuration.test.py
@@ -0,0 +1,183 @@
+"""Verify ALPN to origin functionality."""
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from typing import Optional
+
+
+Test.Summary = __doc__
+
+
+class TestAlpnFunctionality:
+    """Define an object to test a set of ALPN functionality."""
+
+    _replay_file: str = 'tls_client_alpn_configuration.replay.yaml'
+    _server_counter: int = 0
+    _ts_counter: int = 0
+    _client_counter: int = 0
+
+    def __init__(
+            self,
+            records_config_alpn: Optional[str] = None,
+            conf_remap_alpn: Optional[str] = None,
+            alpn_is_malformed: bool = False):
+        """Declare the various test Processes.
+
+        :param records_config_alpn: The string with which to configure the ATS
+        ALPN via proxy.config.ssl.client.alpn_protocols in the records.config.
+        If the paramenter is None, then no ALPN configuration will be
+        explicitly set and ATS will use the default value.
+
+        :param conf_remap_alpn: The string with which to configure the Traffic
+        Server ALPN proxy.config.http.alpn_protocols configuration via
+        conf_remap. If the parameter is None, then no conf_remap configuration
+        will be set.
+
+        :param alpn_is_malformed: If True, then the configured ALPN string in
+        the records.config will be malformed. The TestRun will be configured to
+        expect a warning and the server will be configured to receive no ALPN.
+        """
+        self._alpn = records_config_alpn
+        self._alpn_conf_remap_alpn = conf_remap_alpn
+        self._alpn_is_malformed = alpn_is_malformed
+
+        configured_alpn = records_config_alpn if conf_remap_alpn is None else conf_remap_alpn
+        if alpn_is_malformed:
+            configured_alpn = None
+        self._server = self._configure_server(configured_alpn)
+
+        self._ts = self._configure_trafficserver(
+            records_config_alpn,
+            conf_remap_alpn,
+            alpn_is_malformed)
+
+    def _configure_server(self, expected_alpn: Optional[str] = None):
+        """Configure the test server.
+
+        :param expected_alpn: The ALPN expected from the client. If this is
+        None, then the server will not expect an ALPN value.
+        """
+        server = Test.MakeVerifierServerProcess(
+            f'server-{TestAlpnFunctionality._server_counter}',
+            self._replay_file)
+        TestAlpnFunctionality._server_counter += 1
+
+        if expected_alpn is None:
+            server.Streams.stdout = Testers.ContainsExpression(
+                'Negotiated ALPN: none',
+                'Verify that ATS sent no ALPN string.')
+        else:
+            protocols = expected_alpn.split(',')
+            for protocol in protocols:
+                server.Streams.stdout = Testers.ContainsExpression(
+                    f'ALPN.*:.*{protocol}',
+                    'Verify that the server parsed the configured ALPN string from ATS.')
+        return server
+
+    def _configure_trafficserver(
+            self,
+            records_config_alpn: Optional[str] = None,
+            conf_remap_alpn: Optional[str] = None,
+            alpn_is_malformed: bool = False):
+        """Configure a Traffic Server process.
+
+        :param records_config_alpn: See the description of this parameter in
+        TestAlpnFunctionality._init__.
+        """
+        ts = Test.MakeATSProcess(
+            f'ts-{TestAlpnFunctionality._ts_counter}',
+            enable_tls=True,
+            enable_cache=False)
+        TestAlpnFunctionality._ts_counter += 1
+
+        ts.addDefaultSSLFiles()
+        ts.Disk.records_config.update({
+            "proxy.config.ssl.server.cert.path": f'{ts.Variables.SSLDir}',
+            "proxy.config.ssl.server.private_key.path": f'{ts.Variables.SSLDir}',
+            "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE',
+
+            'proxy.config.diags.debug.enabled': 3,
+            'proxy.config.diags.debug.tags': 'ssl',
+        })
+
+        if records_config_alpn is not None:
+            ts.Disk.records_config.update({
+                'proxy.config.ssl.client.alpn_protocols': records_config_alpn,
+            })
+
+        ts.Disk.ssl_multicert_config.AddLine(
+            'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+        )
+
+        conf_remap_specification = ''
+        if conf_remap_alpn is not None:
+            conf_remap_specification = (
+                '@plugin=conf_remap.so '
+                f'@pparam=proxy.config.ssl.client.alpn_protocols={conf_remap_alpn}')
+
+        ts.Disk.remap_config.AddLine(
+            f'map / https://127.0.0.1:{self._server.Variables.https_port} {conf_remap_specification}'
+        )
+
+        if alpn_is_malformed:
+            ts.Disk.diags_log.Content += Testers.ContainsExpression(
+                "WARNING.*ALPN",
+                "There should be no ALPN parse warnings.")
+        else:
+            ts.Disk.diags_log.Content += Testers.ExcludesExpression(
+                "WARNING.*ALPN",
+                "There should be no ALPN parse warnings.")
+
+        return ts
+
+    def run(self):
+        """Configure the TestRun."""
+        description = "default" if self._alpn is None else self._alpn
+        tr = Test.AddTestRun(f'ATS ALPN configuration: {description}')
+        tr.Processes.Default.StartBefore(self._server)
+        tr.Processes.Default.StartBefore(self._ts)
+
+        tr.AddVerifierClientProcess(
+            f'client-{TestAlpnFunctionality._client_counter}',
+            self._replay_file,
+            https_ports=[self._ts.Variables.ssl_port])
+        TestAlpnFunctionality._client_counter += 1
+
+
+TestAlpnFunctionality().run()
+TestAlpnFunctionality(
+    records_config_alpn='http/1.1').run()
+TestAlpnFunctionality(
+    records_config_alpn='http/1.1,http/1.0').run()
+TestAlpnFunctionality(
+    records_config_alpn='http/1.1',
+    conf_remap_alpn='http/1.1,http/1.0').run()
+
+# TODO: HTTP/2 to origin comes later.
+# TestAlpnFunctionality(
+#   records_config_alpn='h2,http1.1').run()
+
+TestAlpnFunctionality(
+    records_config_alpn='not_a_protocol',
+    alpn_is_malformed=True).run()
+
+# Since we do not currently support ALPN with HTTP/2, this will be considered a
+# malformed ALPN protocol.
+# TODO: remove this when we support HTTP/2 to origin.
+TestAlpnFunctionality(
+    records_config_alpn='h2',
+    alpn_is_malformed=True).run()