aboutsummaryrefslogtreecommitdiffstats
path: root/vespalib/src/vespa/vespalib/net/tls/protocol_snooping.cpp
blob: c954e35bcb6ada9b12e46e9ad6de78f1a7d61bc0 (plain) (blame)
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();
}

}