penguin/tinyproxy

An L4 proxy designed to act as a tiny transparent shim

src/stream_sniff.c

raw ยท 7952 bytes

#include "stream_sniff.h"

#include <ctype.h>
#include <stdint.h>
#include <string.h>

#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 "<not_observed>";
	}

	if (s->status == STREAM_SNIFF_PARSED && s->sni[0] != '\0') {
		return s->sni;
	}

	switch (s->status) {
	case STREAM_SNIFF_NOT_OBSERVED:
		return "<not_observed>";
	case STREAM_SNIFF_NEED_MORE:
		return "<need_more>";
	case STREAM_SNIFF_MISSING:
		return "<missing>";
	case STREAM_SNIFF_NOT_TLS:
		return "<not_tls>";
	case STREAM_SNIFF_TRUNCATED:
		return "<truncated>";
	case STREAM_SNIFF_NAME_TOO_LONG:
		return "<name_too_long>";
	case STREAM_SNIFF_PARSE_ERROR:
		return "<parse_error>";
	case STREAM_SNIFF_PARSED:
		return "<empty>";
	default:
		return "<unknown>";
	}
}