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()