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 <brian.ner...@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()