Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions doc/admin-guide/files/records.config.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if users want to set "ALPN string".

The ALPN protocol IDs are inconsistent; The ID for HTTP/1.1 is "http/1.1" but not "h1.1" where ones for HTTP/2 and 3 are "h2" and "h3". I don't think these IDs are user friendly.

Even if we use those raw IDs, the doc should say what IDs are available. Obviously we can't set "spdy/1", although it's a registered valid ID. We shouldn't send "h3" on a regular TLS connection, and similarly we shouldn't send "h2" on a QUIC connection.

Also, the order of IDs is unclear if a user is unfamiliar with ALPN.

I don't have a good idea right now, but I think we need discussion about this setting at minimum. Changing this setting would be an incompatible change and we wouldn't be able to change it until 11.0 once we make 10.0 release with this setting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are all really good points. I think I've addressed each of your thoughts:

  1. I've updated the ALPN parsing logic to enforce that the user only passes expected protocols, currently http/1.1 and http/1.0. With HTTP/2 to origin we'll add h2. If any other protocol strings are passed, a WARNING is emitted and no ALPN extension is sent (the default behavior). This makes the configuration "fool-proof" for the user in that they can't break anything by configuring it, and a WARNING is emitted if something is wrong with their configured ALPN.
  2. I expanded our documentation of this feature, pointing to the ALPN RFC and the official IANA list of registered ALPN protocol strings. I also explicitly documented the ALPN protocols ATS supports. (Again, we'll add h2 with the HTTP/2 to origin PR.)

Thank you for your thoughtful feedback on this, @maskit. It really improved this feature.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating the documentation. At minimum it works for me, but I can't tell if users want to read the external documents. The example configurations are nice. I guess most users would just see the examples, and copy a setting value from there. In that sense, whether name or ID is not really important, although I think using names (or aliases) requires less knowledge.

I'm not going to judge if the setting format is user friendly, but if I made a Web UI for a CDN service that uses ATS, it'd have a reorderable list for this setting and the items would have protocol names but not IDs. It wouldn't even tell it's a setting for ALPN.

And very strictly speaking, "," is a valid character for an ALPN protocol ID. I hope future protocol IDs (which we want to support) won't use it.

: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
Expand Down
1 change: 1 addition & 0 deletions include/ts/apidefs.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions include/tscore/ink_defs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions iocore/net/I_NetVConnection.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions iocore/net/P_SSLConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions iocore/net/SSLConfig.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
25 changes: 23 additions & 2 deletions iocore/net/SSLNetVConnection.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
}
}

Expand Down Expand Up @@ -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) {
Expand Down
24 changes: 24 additions & 0 deletions lib/records/I_RecHttp.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
87 changes: 87 additions & 0 deletions lib/records/RecHttp.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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;
}
Loading