1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
#include "protocol_snooping.h"
#include <vespa/vespalib/util/size_literals.h>
#include <iostream>
#include <cstdlib>
#include <stdint.h>
namespace vespalib::net::tls::snooping {
namespace {
// Precondition for all helper functions: buffer is at least `min_header_bytes_to_observe()` bytes long
// From RFC 5246:
// 0x16 - Handshake content type byte of TLSCiphertext record
inline bool is_tls_handshake_packet(const char* buf) {
return (buf[0] == 0x16);
}
// First byte of 2-byte ProtocolVersion, always 3 on TLSv1.2 and v1.3
// Next is the TLS minor version, either 1 or 3 depending on version (though the
// RFCs say it _should_ be 1 for backwards compatibility reasons).
// Yes, the TLS spec says that you should technically ignore the protocol version
// field here, but we want all the signals we can get.
inline bool is_expected_tls_protocol_version(const char* buf) {
return ((buf[1] == 0x03) && ((buf[2] == 0x01) || (buf[2] == 0x03)));
}
// Length is big endian u16 in bytes 3, 4
inline uint16_t tls_record_length(const char* buf) {
return (uint16_t(static_cast<unsigned char>(buf[3]) << 8)
+ static_cast<unsigned char>(buf[4]));
}
// First byte of Handshake record in byte 5, which shall be ClientHello (0x01)
inline bool is_client_hello_handshake_record(const char* buf) {
return (buf[5] == 0x01);
}
// Last 2 bytes are the 2 first big-endian bytes of a 3-byte Handshake
// record length field. No support for records that are large enough that
// the MSB should ever be non-zero.
inline bool client_hello_record_size_within_expected_bounds(const char* buf) {
return (buf[6] == 0x00);
}
// The byte after the MSB of the 24-bit handshake record size should be equal
// to the most significant byte of the record length value, minus the Handshake
// record header size.
// Again, we make the assumption that ClientHello messages are not fragmented,
// so their max size must be <= 16KiB. This also just happens to be a lower
// number than the minimum FS4/FRT packet type byte at the same location.
// Oooh yeah, leaky abstractions to the rescue!
inline bool handshake_record_size_matches_length(const char* buf, uint16_t length) {
return (static_cast<unsigned char>(buf[7]) == ((length - 4) >> 8));
}
} // anon ns
TlsSnoopingResult snoop_client_hello_header(const char* buf) noexcept {
if (!is_tls_handshake_packet(buf)) {
return TlsSnoopingResult::HandshakeMismatch;
}
if (!is_expected_tls_protocol_version(buf)) {
return TlsSnoopingResult::ProtocolVersionMismatch;
}
// Length of TLS record follows. Must be <= 16KiB + 2_Ki (16KiB + 256 on v1.3).
// We expect that the first record contains _only_ a ClientHello with no coalescing
// and no fragmentation. This is technically a violation of the TLS spec, but this
// particular detection logic is only intended to be used against other Vespa nodes
// where we control frame sizes and where such fragmentation should not take place.
// We also do not support TLSv1.3 0-RTT which may trigger early data.
uint16_t length = tls_record_length(buf);
if ((length < 4) || (length > (16_Ki + 2_Ki))) {
return TlsSnoopingResult::RecordSizeRfcViolation;
}
if (!is_client_hello_handshake_record(buf)) {
return TlsSnoopingResult::RecordNotClientHello;
}
if (!client_hello_record_size_within_expected_bounds(buf)) {
return TlsSnoopingResult::ClientHelloRecordTooBig;
}
if (!handshake_record_size_matches_length(buf, length)) {
return TlsSnoopingResult::ExpectedRecordSizeMismatch;
}
// Hooray! It very probably most likely is a TLS connection! :D
return TlsSnoopingResult::ProbablyTls;
}
const char* to_string(TlsSnoopingResult result) noexcept {
switch (result) {
case TlsSnoopingResult::ProbablyTls: return "ProbablyTls";
case TlsSnoopingResult::HandshakeMismatch: return "HandshakeMismatch";
case TlsSnoopingResult::ProtocolVersionMismatch: return "ProtocolVersionMismatch";
case TlsSnoopingResult::RecordSizeRfcViolation: return "RecordSizeRfcViolation";
case TlsSnoopingResult::RecordNotClientHello: return "RecordNotClientHello";
case TlsSnoopingResult::ClientHelloRecordTooBig: return "ClientHelloRecordTooBig";
case TlsSnoopingResult::ExpectedRecordSizeMismatch: return "ExpectedRecordSizeMismatch";
}
abort();
}
std::ostream& operator<<(std::ostream& os, TlsSnoopingResult result) {
os << to_string(result);
return os;
}
const char* describe_result(TlsSnoopingResult result) noexcept {
switch (result) {
case TlsSnoopingResult::ProbablyTls:
return "client data matches TLS heuristics, very likely a TLS connection";
case TlsSnoopingResult::HandshakeMismatch:
return "not a TLS handshake packet";
case TlsSnoopingResult::ProtocolVersionMismatch:
return "ProtocolVersion mismatch";
case TlsSnoopingResult::RecordSizeRfcViolation:
return "ClientHello record size is greater than RFC allows";
case TlsSnoopingResult::RecordNotClientHello:
return "record is not ClientHello";
case TlsSnoopingResult::ClientHelloRecordTooBig:
return "ClientHello record is too big (fragmented?)";
case TlsSnoopingResult::ExpectedRecordSizeMismatch:
return "ClientHello vs Handshake header record size mismatch";
}
abort();
}
}
|