#include "stream_sniff.h" #include #include #include #include "klog.h" static uint16_t read_u16_be(const unsigned char *p) { return ((uint16_t)p[0] << 8) | (uint16_t)p[1]; } static uint32_t read_u24_be(const unsigned char *p) { return ((uint32_t)p[0] << 16) | ((uint32_t)p[1] << 8) | (uint32_t)p[2]; } static bool valid_sni_name(const unsigned char *p, size_t len) { size_t i; if (len == 0 || len > STREAM_SNI_MAX) { return false; } /* * Keep validation conservative but not overly clever. * * SNI host_name is supposed to be a DNS hostname, not an arbitrary * string. Do not accept NUL/control bytes, slashes, spaces, etc. */ for (i = 0; i < len; i++) { unsigned char c = p[i]; if (isalnum(c) || c == '-' || c == '.') { continue; } return false; } return true; } static enum stream_sniff_status parse_sni_from_client_hello( const unsigned char *buf, size_t len, char *out, size_t out_len, bool *need_more) { size_t pos; size_t record_len; size_t record_end; size_t hs_len; size_t hs_end; size_t session_id_len; size_t cipher_suites_len; size_t compression_methods_len; size_t extensions_len; size_t extensions_end; *need_more = false; if (len == 0) { *need_more = true; return STREAM_SNIFF_NEED_MORE; } /* * TLS record header: * * content_type: 1 byte == 22 handshake * legacy_version: 2 bytes * length: 2 bytes */ if (buf[0] != 0x16) { return STREAM_SNIFF_NOT_TLS; } if (len < 5) { *need_more = true; return STREAM_SNIFF_NEED_MORE; } /* * TLS major version should be 3 for SSLv3/TLS 1.x records. * TLS 1.3 ClientHello commonly still uses legacy record version 0x0301 * or 0x0303, so do not require an exact minor version. */ if (buf[1] != 0x03) { return STREAM_SNIFF_PARSE_ERROR; } record_len = read_u16_be(buf + 3); record_end = 5 + record_len; if (record_len == 0) { return STREAM_SNIFF_PARSE_ERROR; } if (record_end > len) { *need_more = true; return STREAM_SNIFF_NEED_MORE; } pos = 5; /* * Handshake message: * * handshake_type: 1 byte == 1 ClientHello * length: 3 bytes */ if (pos + 4 > record_end) { return STREAM_SNIFF_PARSE_ERROR; } if (buf[pos] != 0x01) { return STREAM_SNIFF_NOT_TLS; } hs_len = read_u24_be(buf + pos + 1); pos += 4; hs_end = pos + hs_len; if (hs_len == 0 || hs_end > record_end) { return STREAM_SNIFF_PARSE_ERROR; } /* * ClientHello body: * * legacy_version: 2 bytes * random: 32 bytes * session_id_len: 1 byte * session_id: variable * cipher_suites_len: 2 bytes * cipher_suites: variable * compression_methods_len: 1 byte * compression_methods: variable * extensions_len: 2 bytes * extensions: variable */ if (pos + 2 + 32 + 1 > hs_end) { return STREAM_SNIFF_PARSE_ERROR; } pos += 2; /* legacy_version */ pos += 32; /* random */ session_id_len = buf[pos]; pos += 1; if (pos + session_id_len > hs_end) { return STREAM_SNIFF_PARSE_ERROR; } pos += session_id_len; if (pos + 2 > hs_end) { return STREAM_SNIFF_PARSE_ERROR; } cipher_suites_len = read_u16_be(buf + pos); pos += 2; if (cipher_suites_len == 0 || (cipher_suites_len % 2) != 0) { return STREAM_SNIFF_PARSE_ERROR; } if (pos + cipher_suites_len > hs_end) { return STREAM_SNIFF_PARSE_ERROR; } pos += cipher_suites_len; if (pos + 1 > hs_end) { return STREAM_SNIFF_PARSE_ERROR; } compression_methods_len = buf[pos]; pos += 1; if (compression_methods_len == 0) { return STREAM_SNIFF_PARSE_ERROR; } if (pos + compression_methods_len > hs_end) { return STREAM_SNIFF_PARSE_ERROR; } pos += compression_methods_len; /* * No extensions means no SNI. */ if (pos == hs_end) { return STREAM_SNIFF_MISSING; } if (pos + 2 > hs_end) { return STREAM_SNIFF_PARSE_ERROR; } extensions_len = read_u16_be(buf + pos); pos += 2; extensions_end = pos + extensions_len; if (extensions_end != hs_end) { return STREAM_SNIFF_PARSE_ERROR; } while (pos + 4 <= extensions_end) { uint16_t ext_type; uint16_t ext_len; size_t ext_data; size_t ext_end; ext_type = read_u16_be(buf + pos); ext_len = read_u16_be(buf + pos + 2); pos += 4; ext_data = pos; ext_end = ext_data + ext_len; if (ext_end > extensions_end) { return STREAM_SNIFF_PARSE_ERROR; } /* * Extension type 0 = server_name. */ if (ext_type == 0) { size_t list_pos; size_t list_len; size_t list_end; if (ext_len < 2) { return STREAM_SNIFF_PARSE_ERROR; } list_len = read_u16_be(buf + ext_data); list_pos = ext_data + 2; list_end = list_pos + list_len; if (list_end != ext_end) { return STREAM_SNIFF_PARSE_ERROR; } while (list_pos + 3 <= list_end) { unsigned char name_type; uint16_t name_len; const unsigned char *name; name_type = buf[list_pos]; name_len = read_u16_be(buf + list_pos + 1); list_pos += 3; if (list_pos + name_len > list_end) { return STREAM_SNIFF_PARSE_ERROR; } name = buf + list_pos; /* * name_type 0 = host_name. */ if (name_type == 0) { if (name_len > STREAM_SNI_MAX) { return STREAM_SNIFF_NAME_TOO_LONG; } if (!valid_sni_name(name, name_len)) { return STREAM_SNIFF_PARSE_ERROR; } if (out_len < (size_t)name_len + 1) { return STREAM_SNIFF_NAME_TOO_LONG; } memcpy(out, name, name_len); out[name_len] = '\0'; return STREAM_SNIFF_PARSED; } list_pos += name_len; } return STREAM_SNIFF_MISSING; } pos = ext_end; } if (pos != extensions_end) { return STREAM_SNIFF_PARSE_ERROR; } return STREAM_SNIFF_MISSING; } void stream_sniff_peek_client_input( struct stream_sniff *s, struct evbuffer *input) { unsigned char buf[STREAM_SNIFF_MAX]; size_t input_len; size_t copy_len; ssize_t n; bool need_more = false; enum stream_sniff_status status; if (s == NULL || input == NULL || s->done) { return; } input_len = evbuffer_get_length(input); if (input_len == 0) { return; } copy_len = input_len; if (copy_len > STREAM_SNIFF_MAX) { copy_len = STREAM_SNIFF_MAX; } n = evbuffer_copyout(input, buf, copy_len); if (n <= 0) { return; } s->observed = (size_t)n; status = parse_sni_from_client_hello( buf, (size_t)n, s->sni, sizeof(s->sni), &need_more ); if (status == STREAM_SNIFF_NEED_MORE && need_more) { if (input_len >= STREAM_SNIFF_MAX) { s->status = STREAM_SNIFF_TRUNCATED; s->done = true; return; } s->status = STREAM_SNIFF_NEED_MORE; return; } s->status = status; switch (status) { case STREAM_SNIFF_PARSED: case STREAM_SNIFF_MISSING: case STREAM_SNIFF_NOT_TLS: case STREAM_SNIFF_TRUNCATED: case STREAM_SNIFF_NAME_TOO_LONG: case STREAM_SNIFF_PARSE_ERROR: s->done = true; break; case STREAM_SNIFF_NOT_OBSERVED: case STREAM_SNIFF_NEED_MORE: default: break; } if (s->done) { LOG_DEBUG("sni sniff", "sni", _LOGV(s->sni), "status", _LOGV(stream_sniff_status_str(s->status)), "observed", _LOGV(s->observed) ); } } const char *stream_sniff_log_sni(const struct stream_sniff *s) { if (s == NULL) { return ""; } if (s->status == STREAM_SNIFF_PARSED && s->sni[0] != '\0') { return s->sni; } switch (s->status) { case STREAM_SNIFF_NOT_OBSERVED: return ""; case STREAM_SNIFF_NEED_MORE: return ""; case STREAM_SNIFF_MISSING: return ""; case STREAM_SNIFF_NOT_TLS: return ""; case STREAM_SNIFF_TRUNCATED: return ""; case STREAM_SNIFF_NAME_TOO_LONG: return ""; case STREAM_SNIFF_PARSE_ERROR: return ""; case STREAM_SNIFF_PARSED: return ""; default: return ""; } }