Skip to content

Commit b41ef83

Browse files
committed
Adding origin-side ALPN configuration.
Adding the ability for ATS to specify the ALPN string it sends in the TLS ClientHello handshake.
1 parent 1b982db commit b41ef83

24 files changed

+560
-6
lines changed

doc/admin-guide/files/records.config.en.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3956,6 +3956,12 @@ Client-Related Configuration
39563956
39573957
Enables (``1``) or disables (``0``) TLSv1_3 in the ATS client context. If not specified, enabled by default
39583958

3959+
.. ts:cv:: CONFIG proxy.config.ssl.client.alpn_protocols STRING ""
3960+
:overridable:
3961+
3962+
Set the alpn string that ATS will send to the origin in the ClientHello of
3963+
TLS handshakes. By default no ALPN string will be sent.
3964+
39593965
.. ts:cv:: CONFIG proxy.config.ssl.async.handshake.enabled INT 0
39603966
39613967
Enables the use of OpenSSL async job during the TLS handshake. Traffic

include/ts/apidefs.h.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,7 @@ typedef enum {
884884
TS_CONFIG_SSL_CLIENT_SNI_POLICY,
885885
TS_CONFIG_SSL_CLIENT_PRIVATE_KEY_FILENAME,
886886
TS_CONFIG_SSL_CLIENT_CA_CERT_FILENAME,
887+
TS_CONFIG_SSL_CLIENT_ALPN_PROTOCOLS,
887888
TS_CONFIG_HTTP_HOST_RESOLUTION_PREFERENCE,
888889
TS_CONFIG_HTTP_CONNECT_DEAD_POLICY,
889890
TS_CONFIG_HTTP_MAX_PROXY_CYCLES,

include/tscore/ink_defs.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ countof(const T (&)[N])
9191
#define unlikely(x) __builtin_expect(!!(x), 0)
9292
#endif
9393

94+
#define MAX_ALPN_STRING 30
95+
9496
/* Variables
9597
*/
9698
extern int off;

iocore/net/I_NetVConnection.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@ struct NetVCOptions {
228228

229229
bool tls_upstream = false;
230230

231+
unsigned char alpn_protocols_array[MAX_ALPN_STRING];
232+
int alpn_protocols_array_size = 0;
233+
231234
/**
232235
* Set to DISABLED, PERFMISSIVE, or ENFORCED
233236
* Controls how the server certificate verification is handled

iocore/net/P_SSLConfig.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ struct SSLConfigParams : public ConfigInfo {
101101
long ssl_ctx_options;
102102
long ssl_client_ctx_options;
103103

104+
unsigned char alpn_protocols_array[MAX_ALPN_STRING];
105+
int alpn_protocols_array_size = 0;
106+
104107
char *server_tls13_cipher_suites;
105108
char *client_tls13_cipher_suites;
106109
char *server_groups_list;

iocore/net/SSLConfig.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,15 @@ SSLConfigParams::initialize()
263263
}
264264
#endif
265265

266+
// Read in the protocol string for ALPN to origin
267+
char *clientALPNProtocols = nullptr;
268+
REC_ReadConfigStringAlloc(clientALPNProtocols, "proxy.config.ssl.client.alpn_protocols");
269+
270+
if (clientALPNProtocols) {
271+
this->alpn_protocols_array_size = MAX_ALPN_STRING;
272+
convert_alpn_to_wire_format(clientALPNProtocols, this->alpn_protocols_array, this->alpn_protocols_array_size);
273+
}
274+
266275
#ifdef SSL_OP_CIPHER_SERVER_PREFERENCE
267276
REC_ReadConfigInteger(option, "proxy.config.ssl.server.honor_cipher_order");
268277
if (option) {

iocore/net/SSLNetVConnection.cc

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,16 @@ SSLNetVConnection::sslStartHandShake(int event, int &err)
11631163
return EVENT_ERROR;
11641164
}
11651165

1166+
// If it is negative, we are conscious not setting alpn (e.g. for private server sessions)
1167+
if (options.alpn_protocols_array_size >= 0) {
1168+
if (options.alpn_protocols_array_size > 0) {
1169+
SSL_set_alpn_protos(this->ssl, options.alpn_protocols_array, options.alpn_protocols_array_size);
1170+
} else if (params->alpn_protocols_array_size > 0) {
1171+
// Set the ALPN protocols we are requesting.
1172+
SSL_set_alpn_protos(this->ssl, params->alpn_protocols_array, params->alpn_protocols_array_size);
1173+
}
1174+
}
1175+
11661176
SSL_set_verify(this->ssl, SSL_VERIFY_PEER, verify_callback);
11671177

11681178
// SNI
@@ -1374,9 +1384,9 @@ SSLNetVConnection::sslServerHandShakeEvent(int &err)
13741384
}
13751385
this->set_negotiated_protocol_id({reinterpret_cast<const char *>(proto), static_cast<size_t>(len)});
13761386

1377-
Debug("ssl", "client selected next protocol '%.*s'", len, proto);
1387+
Debug("ssl", "Origin selected next protocol '%.*s'", len, proto);
13781388
} else {
1379-
Debug("ssl", "client did not select a next protocol");
1389+
Debug("ssl", "Origin did not select a next protocol");
13801390
}
13811391
}
13821392

@@ -1523,6 +1533,17 @@ SSLNetVConnection::sslClientHandShakeEvent(int &err)
15231533
X509_free(cert);
15241534
}
15251535
}
1536+
{
1537+
unsigned char const *proto = nullptr;
1538+
unsigned int len = 0;
1539+
// Make note of the negotiated protocol
1540+
SSL_get0_alpn_selected(ssl, &proto, &len);
1541+
if (len == 0) {
1542+
SSL_get0_next_proto_negotiated(ssl, &proto, &len);
1543+
}
1544+
Debug("ssl_alpn", "Negotiated ALPN: %.*s", len, proto);
1545+
this->set_negotiated_protocol_id({reinterpret_cast<const char *>(proto), static_cast<size_t>(len)});
1546+
}
15261547

15271548
// if the handshake is complete and write is enabled reschedule the write
15281549
if (closed == 0 && write.enabled) {

lib/records/I_RecHttp.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,3 +520,26 @@ HttpProxyPort::findHttp(uint16_t family)
520520
This must be called before any proxy port parsing is done.
521521
*/
522522
extern void ts_session_protocol_well_known_name_indices_init();
523+
524+
/** Convert the comma separated ALPN protocol list to wire format.
525+
*
526+
* For the definition of wire format, see the NOTES section in the OpenSSL
527+
* description of SSL_CTX_set_alpn_select_cb:
528+
*
529+
* https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_alpn_select_cb.html
530+
*
531+
* @param[in] protocols The comma separated list of protocols to convert to
532+
* wire format.
533+
*
534+
* @param[out] client_alpn_protocols The output ALPN wire format string
535+
* converted from @a protocols.
536+
*
537+
* @param[in,out] alpn_array_len As an input, this is the size allocated for @a
538+
* client_alpn_protocols. As an output, this is set to the final size of @a
539+
* client_alpn_protocols after conversion.
540+
*
541+
* @return True if the conversion was successful, false otherwise. Note that
542+
* the wire format does not support an empty protocol list, therefore this
543+
* function returns false if @a protocols is an empty string.
544+
*/
545+
bool convert_alpn_to_wire_format(std::string_view protocols, unsigned char *client_alpn_protocols, int &alpn_array_len);

lib/records/RecHttp.cc

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "tscore/ink_defs.h"
2727
#include "tscore/TextBuffer.h"
2828
#include "tscore/Tokenizer.h"
29+
#include <cstring>
2930
#include <strings.h>
3031
#include "tscore/ink_inet.h"
3132
#include <string_view>
@@ -841,3 +842,75 @@ SessionProtocolNameRegistry::nameFor(int idx) const
841842
{
842843
return 0 <= idx && idx < m_n ? m_names[idx] : TextView{};
843844
}
845+
846+
bool
847+
convert_alpn_to_wire_format(std::string_view protocols, unsigned char *client_alpn_protocols, int &alpn_array_len)
848+
{
849+
if (protocols.empty()) {
850+
return false;
851+
}
852+
853+
// Parse the comma separated protocol string into a list of protocol names.
854+
std::vector<std::string_view> alpn_protocols;
855+
std::string_view protocol;
856+
size_t pos = 0;
857+
int computed_alpn_array_len = 0;
858+
while (pos < protocols.size()) {
859+
size_t next_pos = protocols.find(',', pos);
860+
if (next_pos == std::string_view::npos) {
861+
protocol = protocols.substr(pos);
862+
pos = protocols.size();
863+
} else {
864+
protocol = protocols.substr(pos, next_pos - pos);
865+
pos = next_pos + 1;
866+
}
867+
if (protocol.empty()) {
868+
Warning("Empty protocol name in configured ALPN list: %.*s", static_cast<int>(protocols.size()), protocols.data());
869+
return false;
870+
}
871+
if (protocol.size() > 255) {
872+
// The length has to fit in one byte.
873+
Warning("A protocol name larger than 255 bytes in configured ALPN list: %.*s", static_cast<int>(protocols.size()),
874+
protocols.data());
875+
return false;
876+
}
877+
// Check whether we recognize the protocol.
878+
auto const protocol_index = globalSessionProtocolNameRegistry.indexFor(protocol);
879+
if (protocol_index == SessionProtocolNameRegistry::INVALID) {
880+
Warning("Unknown protocol name in configured ALPN list: %.*s", static_cast<int>(protocol.size()), protocol.data());
881+
return false;
882+
}
883+
// We currently only support HTTP/1.x protocols toward the origin.
884+
if (!HTTP_PROTOCOL_SET.contains(protocol_index)) {
885+
Warning("Non-HTTP/1.x protocol name in configured ALPN list: %.*s", static_cast<int>(protocol.size()), protocol.data());
886+
return false;
887+
}
888+
889+
auto const protocol_wire_format = globalSessionProtocolNameRegistry.convert_openssl_alpn_wire_format(protocol_index);
890+
computed_alpn_array_len += protocol_wire_format.size();
891+
if (computed_alpn_array_len > alpn_array_len) {
892+
// We have exceeded the size of the output buffer.
893+
Warning("The output ALPN length is larger than the input buffer size of %d bytes", alpn_array_len);
894+
return false;
895+
}
896+
897+
alpn_protocols.push_back(protocol_wire_format);
898+
}
899+
if (alpn_protocols.empty()) {
900+
Warning("No protocols specified in ALPN list: %.*s", static_cast<int>(protocols.size()), protocols.data());
901+
return false;
902+
}
903+
904+
// All checks pass and the protocols are parsed. Write the result to the
905+
// output buffer.
906+
auto *start = client_alpn_protocols;
907+
auto *end = start;
908+
for (auto &protocol : alpn_protocols) {
909+
auto const len = protocol.size();
910+
memcpy(end, protocol.data(), len);
911+
end += len;
912+
}
913+
alpn_array_len = computed_alpn_array_len;
914+
Debug("ssl_alpn", "Successfully converted ALPN list to wire format: %.*s", static_cast<int>(protocols.size()), protocols.data());
915+
return true;
916+
}

lib/records/unit_tests/test_RecHttp.cc

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@
1818
the License.
1919
*/
2020

21+
#include <array>
2122
#include <string>
2223
#include <string_view>
23-
#include <array>
24+
#include <vector>
2425

2526
#include "catch.hpp"
2627

2728
#include "tscore/BufferWriter.h"
2829
#include "records/I_RecHttp.h"
2930
#include "test_Diags.h"
31+
#include "tscore/ink_defs.h"
3032

3133
using ts::TextView;
3234

@@ -97,3 +99,119 @@ TEST_CASE("RecHttp", "[librecords][RecHttp]")
9799
REQUIRE(view.find(":proto") == TextView::npos); // it's default, should not have this.
98100
}
99101
}
102+
103+
struct ConvertAlpnToWireFormatTestCase {
104+
std::string description;
105+
std::string alpn_input;
106+
unsigned char expected_alpn_wire_format[MAX_ALPN_STRING] = {0};
107+
int expected_alpn_wire_format_len = MAX_ALPN_STRING;
108+
bool expected_return = true;
109+
};
110+
111+
// The following test cases assume that the input array is initialized with {
112+
// 0xab }, and thus expects that as the output on failure.
113+
// clang-format off
114+
std::vector<ConvertAlpnToWireFormatTestCase> convertAlpnToWireFormatTestCases = {
115+
// --------------------------------------------------------------------------
116+
// Malformed input.
117+
// --------------------------------------------------------------------------
118+
{
119+
"Empty input protocol list",
120+
"",
121+
{ 0xab },
122+
MAX_ALPN_STRING,
123+
false
124+
},
125+
{
126+
"Include an empty protocol in the list",
127+
"http/1.1,,http/1.0",
128+
{ 0xab },
129+
MAX_ALPN_STRING,
130+
false
131+
},
132+
{
133+
"A protocol that exceeds the output buffer length (MAX_ALPN_STRING)",
134+
"some_really_long_protocol_name_that_exceeds_the_output_buffer_length_that_is_MAX_ALPN_STRING",
135+
{ 0xab },
136+
MAX_ALPN_STRING,
137+
false
138+
},
139+
{
140+
"The sum of protocols exceeds the output buffer length (MAX_ALPN_STRING)",
141+
"protocol_one,protocol_two,protocol_three",
142+
{ 0xab },
143+
MAX_ALPN_STRING,
144+
false
145+
},
146+
{
147+
"A protocol that exceeds the length described by a single byte (255)",
148+
"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",
149+
{ 0xab },
150+
MAX_ALPN_STRING,
151+
false
152+
},
153+
// --------------------------------------------------------------------------
154+
// Unsupported protocols.
155+
// --------------------------------------------------------------------------
156+
{
157+
"Unrecognized protocol: HTTP/6",
158+
"h6",
159+
{ 0xab },
160+
MAX_ALPN_STRING,
161+
false
162+
},
163+
{
164+
"Single protocol: HTTP/2 (currently unsupported)",
165+
"h2",
166+
{ 0xab },
167+
MAX_ALPN_STRING,
168+
false
169+
},
170+
{
171+
"Single protocol: HTTP/3 (currently unsupported)",
172+
"h3",
173+
{ 0xab },
174+
MAX_ALPN_STRING,
175+
false
176+
},
177+
{
178+
"Both HTTP/1.1 and HTTP/2 (HTTP/2 is currently unsupported)",
179+
"h2,http/1.1",
180+
{ 0xab },
181+
MAX_ALPN_STRING,
182+
false
183+
},
184+
// --------------------------------------------------------------------------
185+
// Happy cases.
186+
// --------------------------------------------------------------------------
187+
{
188+
"Single protocol: HTTP/1.1",
189+
"http/1.1",
190+
{0x08, 'h', 't', 't', 'p', '/', '1', '.', '1'},
191+
9,
192+
true
193+
},
194+
{
195+
"Multiple protocols: HTTP/0.9, HTTP/1.0, HTTP/1.1",
196+
"http/1.1,http/1.0,http/0.9",
197+
{0x08, 'h', 't', 't', 'p', '/', '1', '.', '1', 0x08, 'h', 't', 't', 'p', '/', '1', '.', '0', 0x08, 'h', 't', 't', 'p', '/', '0', '.', '9'},
198+
27,
199+
true
200+
},
201+
};
202+
// clang-format on
203+
204+
TEST_CASE("convert_alpn_to_wire_format", "[librecords][RecHttp]")
205+
{
206+
for (auto const &test_case : convertAlpnToWireFormatTestCases) {
207+
SECTION(test_case.description)
208+
{
209+
unsigned char alpn_wire_format[MAX_ALPN_STRING] = {0xab};
210+
int alpn_wire_format_len = MAX_ALPN_STRING;
211+
auto const result = convert_alpn_to_wire_format(test_case.alpn_input, alpn_wire_format, alpn_wire_format_len);
212+
REQUIRE(result == test_case.expected_return);
213+
REQUIRE(alpn_wire_format_len == test_case.expected_alpn_wire_format_len);
214+
REQUIRE(memcmp(alpn_wire_format, test_case.expected_alpn_wire_format, test_case.expected_alpn_wire_format_len) == 0);
215+
}
216+
}
217+
}

0 commit comments

Comments
 (0)