Add ALPN protocol based probe

This commit is contained in:
Travis Burtrum 2016-01-05 00:32:10 -05:00
parent 8758a298ba
commit 8af039d3eb
7 changed files with 216 additions and 104 deletions

View File

@ -2,7 +2,7 @@ vNEXT:
Added USELIBPCRE to make use of regex engine
optional.
Added support for RFC4366 SNI
Added support for RFC4366 SNI and RFC7301 ALPN
(Travis Burtrum)
Changed connection log to include the name of the probe that
@ -10,8 +10,8 @@ vNEXT:
Changed configuration file format: 'probe' field is
no longer required, 'name' field can now contain
'sni' or 'regex', with corresponding options (see
example.org)
'tls' or 'regex', with corresponding options (see
example.cfg)
Added 'log_level' option to each protocol, which
allows to turn off generation of log at each
connection.

View File

@ -24,14 +24,19 @@ listen:
#
# Each protocol entry consists of:
# name: name of the probe. These are listed on the command
# line (ssh -?), plus 'regex', 'sni' and 'timeout'.
# line (ssh -?), plus 'regex' and 'timeout'.
# service: (optional) libwrap service name (see hosts_access(5))
# host, port: where to connect when this probe succeeds
#
# Probe-specific options:
# sni:
# sni_hotnames: list of FQDN for that target
# tls:
# sni_hostnames: list of FQDN for that target
# alpn_protocols: list of ALPN protocols for that target, see:
# https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
#
# if both sni_hostnames AND alpn_protocols are specified, both must match
# if neither are set, it is just checked whether this is the TLS protocol or not
# regex:
# regex_patterns: list of patterns to match for
# that target.
@ -39,15 +44,26 @@ listen:
# sslh will try each probe in order they are declared, and
# connect to the first that matches.
#
# You can specify several of 'regex' and 'sni'.
# You can specify several of 'regex' and 'tls'.
protocols:
(
{ name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; },
{ name: "http"; host: "localhost"; port: "80"; },
{ name: "sni"; host: "localhost"; port: "993"; sni_hostnames: [ "mail.rutschle.net", "mail.englishintoulouse.com" ]; log_level: 0; },
{ name: "sni"; host: "localhost"; port: "xmpp-client"; sni_hostnames: [ "im.rutschle.net", "im.englishintoulouse.com" ]; log_level: 0;},
# match BOTH ALPN/SNI
{ name: "tls"; host: "localhost"; port: "5223"; alpn_protocols: [ "xmpp-client" ]; sni_hostnames: [ "im.somethingelse.net" ]; log_level: 0;},
# just match ALPN
{ name: "tls"; host: "localhost"; port: "443"; alpn_protocols: [ "h2", "http/1.1", "spdy/1", "spdy/2", "spdy/3" ]; log_level: 0; },
{ name: "tls"; host: "localhost"; port: "xmpp-client"; alpn_protocols: [ "xmpp-client" ]; log_level: 0;},
# just match SNI
{ name: "tls"; host: "localhost"; port: "993"; sni_hostnames: [ "mail.rutschle.net", "mail.englishintoulouse.com" ]; log_level: 0; },
{ name: "tls"; host: "localhost"; port: "xmpp-client"; sni_hostnames: [ "im.rutschle.net", "im.englishintoulouse.com" ]; log_level: 0;},
# catch anything else TLS
{ name: "tls"; host: "localhost"; port: "443"; },
# OpenVPN
{ name: "regex"; host: "localhost"; port: "1194"; regex_patterns: [ "^\x00[\x0D-\xFF]$", "^\x00[\x0D-\xFF]\x38" ]; },

29
probe.c
View File

@ -217,32 +217,17 @@ static int is_http_protocol(const char *p, int len, struct proto *proto)
return PROBE_NEXT;
}
static int is_sni_protocol(const char *p, int len, struct proto *proto)
static int is_sni_alpn_protocol(const char *p, int len, struct proto *proto)
{
int valid_tls;
char *hostname;
char **sni_hostname;
valid_tls = parse_tls_header(p, len, &hostname);
valid_tls = parse_tls_header(proto->data, p, len);
if(valid_tls < 0)
return -1 == valid_tls ? PROBE_AGAIN : PROBE_NEXT;
if (verbose) fprintf(stderr, "sni hostname: %s\n", hostname);
/* Assume does not match */
valid_tls = PROBE_NEXT;
for (sni_hostname = proto->data; *sni_hostname; sni_hostname++) {
fprintf(stderr, "matching [%s] with [%s]\n", hostname, *sni_hostname);
if(!strcmp(hostname, *sni_hostname)) {
valid_tls = PROBE_MATCH;
break;
}
}
free(hostname);
return valid_tls;
/* There *was* a valid match */
return PROBE_MATCH;
}
static int is_tls_protocol(const char *p, int len, struct proto *proto)
@ -363,9 +348,9 @@ T_PROBE* get_probe(const char* description) {
if (!strcmp(description, "regex"))
return regex_probe;
/* Special case of "sni" probe for same reason as above*/
if (!strcmp(description, "sni"))
return is_sni_protocol;
/* Special case of "sni/alpn" probe for same reason as above*/
if (!strcmp(description, "sni_alpn"))
return is_sni_alpn_protocol;
/* Special case of "timeout" is allowed as a probe name in the
* configuration file even though it's not really a probe */

View File

@ -27,7 +27,8 @@ struct proto {
/* function to probe that protocol; parameters are buffer and length
* containing the data to probe, and a pointer to the protocol structure */
T_PROBE* probe;
void* data; /* opaque pointer ; used to pass list of regex to regex probe, or sni hostnames to sni probe */
/* opaque pointer ; used to pass list of regex to regex probe, or TLSProtocol struct to sni/alpn probe */
void* data;
struct proto *next; /* pointer to next protocol in list, NULL if last */
};

View File

@ -213,34 +213,47 @@ static void setup_regex_probe(struct proto *p, config_setting_t* probes)
#endif
#ifdef LIBCONFIG
static void setup_sni_hostnames(struct proto *p, config_setting_t* sni_hostnames)
static void setup_sni_alpn_list(struct proto *p, config_setting_t* config_items, const char* name, int alpn)
{
int num_probes, i, max_server_name_len, server_name_len;
const char * sni_hostname;
const char * config_item;
char** sni_hostname_list;
num_probes = config_setting_length(sni_hostnames);
if(!config_items || !config_setting_is_array(config_items)) {
fprintf(stderr, "%s: no %s specified\n", p->description, name);
return;
}
num_probes = config_setting_length(config_items);
if (!num_probes) {
fprintf(stderr, "%s: no sni_hostnames specified\n", p->description);
exit(1);
fprintf(stderr, "%s: no %s specified\n", p->description, name);
return;
}
max_server_name_len = 0;
for (i = 0; i < num_probes; i++) {
server_name_len = strlen(config_setting_get_string_elem(sni_hostnames, i));
server_name_len = strlen(config_setting_get_string_elem(config_items, i));
if(server_name_len > max_server_name_len)
max_server_name_len = server_name_len;
}
sni_hostname_list = calloc(num_probes + 1, ++max_server_name_len);
p->data = (void*)sni_hostname_list;
for (i = 0; i < num_probes; i++) {
sni_hostname = config_setting_get_string_elem(sni_hostnames, i);
config_item = config_setting_get_string_elem(config_items, i);
sni_hostname_list[i] = malloc(max_server_name_len);
strcpy (sni_hostname_list[i], sni_hostname);
if(verbose) fprintf(stderr, "sni_hostnames[%d]: %s\n", i, sni_hostname_list[i]);
strcpy (sni_hostname_list[i], config_item);
if(verbose) fprintf(stderr, "%s: %s[%d]: %s\n", p->description, name, i, sni_hostname_list[i]);
}
p->data = (void*)tls_data_set_list(p->data, alpn, sni_hostname_list);
}
static void setup_sni_alpn(struct proto *p, config_setting_t* sni_hostnames, config_setting_t* alpn_protocols)
{
p->data = (void*)new_tls_data();
p->probe = get_probe("sni_alpn");
setup_sni_alpn_list(p, sni_hostnames, "sni_hostnames", 0);
setup_sni_alpn_list(p, alpn_protocols, "alpn_protocols", 1);
}
#endif
@ -250,7 +263,7 @@ static void setup_sni_hostnames(struct proto *p, config_setting_t* sni_hostnames
#ifdef LIBCONFIG
static int config_protocols(config_t *config, struct proto **prots)
{
config_setting_t *setting, *prot, *patterns, *sni_hostnames;
config_setting_t *setting, *prot, *patterns, *sni_hostnames, *alpn_protocols;
const char *hostname, *port, *name;
int i, num_prots;
struct proto *p, *prev = NULL;
@ -279,7 +292,7 @@ static int config_protocols(config_t *config, struct proto **prots)
resolve_split_name(&(p->saddr), hostname, port);
p->probe = get_probe(name);
if (!p->probe) {
if (!p->probe || !strcmp(name, "sni_alpn")) {
fprintf(stderr, "line %d: %s: probe unknown\n", config_setting_source_line(prot), name);
exit(1);
}
@ -292,13 +305,16 @@ static int config_protocols(config_t *config, struct proto **prots)
}
}
/* Probe-specific options: SNI hostnames */
if (!strcmp(name, "sni")) {
/* Probe-specific options: SNI/ALPN */
if (!strcmp(name, "tls")) {
sni_hostnames = config_setting_get_member(prot, "sni_hostnames");
if (sni_hostnames && config_setting_is_array(sni_hostnames)) {
setup_sni_hostnames(p, sni_hostnames);
alpn_protocols = config_setting_get_member(prot, "alpn_protocols");
if((sni_hostnames && config_setting_is_array(sni_hostnames)) || (alpn_protocols && config_setting_is_array(alpn_protocols))) {
setup_sni_alpn(p, sni_hostnames, alpn_protocols);
}
}
}
}
}

189
tls.c
View File

@ -30,8 +30,6 @@
*/
#include <stdio.h>
#include <stdlib.h> /* malloc() */
#include <string.h> /* strncpy() */
#include <sys/socket.h>
#include "tls.h"
#define TLS_HEADER_LEN 5
@ -42,19 +40,20 @@
#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
#endif
static int parse_extensions(const char *, size_t, char **);
static int parse_server_name_extension(const char *, size_t, char **);
const char tls_alert[] = {
0x15, /* TLS Alert */
0x03, 0x01, /* TLS version */
0x00, 0x02, /* Payload length */
0x02, 0x28, /* Fatal, handshake failure */
struct TLSProtocol {
int use_alpn;
char** sni_hostname_list;
char** alpn_protocol_list;
};
/* Parse a TLS packet for the Server Name Indication extension in the client
* hello handshake, returning the first servername found (pointer to static
* array)
static int parse_extensions(const struct TLSProtocol *, const char *, size_t);
static int parse_server_name_extension(const struct TLSProtocol *, const char *, size_t);
static int parse_alpn_extension(const struct TLSProtocol *, const char *, size_t);
static int has_match(char**, const char*, size_t);
/* Parse a TLS packet for the Server Name Indication and ALPN extension in the client
* hello handshake, returning a status code
*
* Returns:
* >=0 - length of the hostname and updates *hostname
@ -62,35 +61,20 @@ const char tls_alert[] = {
* -1 - Incomplete request
* -2 - No Host header included in this request
* -3 - Invalid hostname pointer
* -4 - malloc failure
* < -4 - Invalid TLS client hello
*/
int
parse_tls_header(const char *data, size_t data_len, char **hostname) {
parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t data_len) {
char tls_content_type;
char tls_version_major;
char tls_version_minor;
size_t pos = TLS_HEADER_LEN;
size_t len;
if (hostname == NULL)
return -3;
/* Check that our TCP payload is at least large enough for a TLS header */
if (data_len < TLS_HEADER_LEN)
return -1;
/* SSL 2.0 compatible Client Hello
*
* High bit of first byte (length) and content type is Client Hello
*
* See RFC5246 Appendix E.2
*/
if (data[0] & 0x80 && data[2] == 1) {
if (verbose) fprintf(stderr, "Received SSL 2.0 Client Hello which can not support SNI.\n");
return -2;
}
tls_content_type = data[0];
if (tls_content_type != TLS_HANDSHAKE_CONTENT_TYPE) {
if (verbose) fprintf(stderr, "Request did not begin with TLS handshake.\n");
@ -100,7 +84,7 @@ parse_tls_header(const char *data, size_t data_len, char **hostname) {
tls_version_major = data[1];
tls_version_minor = data[2];
if (tls_version_major < 3) {
if (verbose) fprintf(stderr, "Received SSL %d.%d handshake which which can not support SNI.\n",
if (verbose) fprintf(stderr, "Received SSL %d.%d handshake which cannot be parsed.\n",
tls_version_major, tls_version_minor);
return -2;
@ -167,13 +151,17 @@ parse_tls_header(const char *data, size_t data_len, char **hostname) {
if (pos + len > data_len)
return -5;
return parse_extensions(data + pos, len, hostname);
return parse_extensions(tls_data, data + pos, len);
}
int
parse_extensions(const char *data, size_t data_len, char **hostname) {
static int
parse_extensions(const struct TLSProtocol *tls_data, const char *data, size_t data_len) {
size_t pos = 0;
size_t len;
int last_matched = 0;
if (tls_data == NULL)
return -3;
/* Parse each 4 bytes for the extension header */
while (pos + 4 <= data_len) {
@ -181,16 +169,57 @@ parse_extensions(const char *data, size_t data_len, char **hostname) {
len = ((unsigned char) data[pos + 2] << 8) +
(unsigned char) data[pos + 3];
/* Check if it's a server name extension */
if (data[pos] == 0x00 && data[pos + 1] == 0x00) {
/* There can be only one extension of each type, so we break
our state and move p to beinnging of the extension here */
if (pos + 4 + len > data_len)
return -5;
return parse_server_name_extension(data + pos + 4, len, hostname);
size_t extension_type = ((unsigned char) data[pos] << 8) +
(unsigned char) data[pos + 1];
/* Check if it's a server name extension */
/* There can be only one extension of each type, so we break
our state and move pos to beginning of the extension here */
if (tls_data->use_alpn == 2) {
/* we want BOTH alpn and sni to match */
if (extension_type == 0x00) { /* Server Name */
if (parse_server_name_extension(tls_data, data + pos + 4, len)) {
/* SNI matched */
if(last_matched) {
/* this is only true if ALPN matched, so return true */
return last_matched;
} else {
/* otherwise store that SNI matched */
last_matched = 1;
}
} else {
// both can't match
return -2;
}
} else if (extension_type == 0x10) { /* ALPN */
if (parse_alpn_extension(tls_data, data + pos + 4, len)) {
/* ALPN matched */
if(last_matched) {
/* this is only true if SNI matched, so return true */
return last_matched;
} else {
/* otherwise store that ALPN matched */
last_matched = 1;
}
} else {
// both can't match
return -2;
}
}
} else if (extension_type == 0x00 && tls_data->use_alpn == 0) { /* Server Name */
return parse_server_name_extension(tls_data, data + pos + 4, len);
} else if (extension_type == 0x10 && tls_data->use_alpn == 1) { /* ALPN */
return parse_alpn_extension(tls_data, data + pos + 4, len);
}
pos += 4 + len; /* Advance to the next extension header */
}
/* Check we ended where we expected to */
if (pos != data_len)
return -5;
@ -198,9 +227,8 @@ parse_extensions(const char *data, size_t data_len, char **hostname) {
return -2;
}
int
parse_server_name_extension(const char *data, size_t data_len,
char **hostname) {
static int
parse_server_name_extension(const struct TLSProtocol *tls_data, const char *data, size_t data_len) {
size_t pos = 2; /* skip server name list length */
size_t len;
@ -213,17 +241,11 @@ parse_server_name_extension(const char *data, size_t data_len,
switch (data[pos]) { /* name type */
case 0x00: /* host_name */
*hostname = malloc(len + 1);
if (*hostname == NULL) {
if (verbose) fprintf(stderr, "malloc() failure\n");
return -4;
}
strncpy(*hostname, data + pos + 3, len);
(*hostname)[len] = '\0';
if(has_match(tls_data->sni_hostname_list, data + pos + 3, len)) {
return len;
} else {
return -2;
}
default:
if (verbose) fprintf(stderr, "Unknown server name extension name type: %d\n",
data[pos]);
@ -236,3 +258,70 @@ parse_server_name_extension(const char *data, size_t data_len,
return -2;
}
static int
parse_alpn_extension(const struct TLSProtocol *tls_data, const char *data, size_t data_len) {
size_t pos = 2;
size_t len;
while (pos + 1 < data_len) {
len = (unsigned char)data[pos];
if (pos + 1 + len > data_len)
return -5;
if (len > 0 && has_match(tls_data->alpn_protocol_list, data + pos + 1, len)) {
return len;
} else if (len > 0) {
if (verbose) fprintf(stderr, "Unknown ALPN name: %.*s\n", (int)len, data + pos + 1);
}
pos += 1 + len;
}
/* Check we ended where we expected to */
if (pos != data_len)
return -5;
return -2;
}
static int
has_match(char** list, const char* name, size_t name_len) {
char **item;
for (item = list; *item; item++) {
if (verbose) fprintf(stderr, "matching [%.*s] with [%s]\n", (int)name_len, name, *item);
if(!strncmp(*item, name, name_len)) {
return 1;
}
}
return 0;
}
struct TLSProtocol *
new_tls_data() {
struct TLSProtocol *tls_data = malloc(sizeof(struct TLSProtocol));
if (tls_data != NULL) {
tls_data->use_alpn = -1;
}
return tls_data;
}
struct TLSProtocol *
tls_data_set_list(struct TLSProtocol *tls_data, int alpn, char** list) {
if (alpn) {
tls_data->alpn_protocol_list = list;
if(tls_data->use_alpn == 0)
tls_data->use_alpn = 2;
else
tls_data->use_alpn = 1;
} else {
tls_data->sni_hostname_list = list;
if(tls_data->use_alpn == 1)
tls_data->use_alpn = 2;
else
tls_data->use_alpn = 0;
}
return tls_data;
}

7
tls.h
View File

@ -28,6 +28,11 @@
#include "common.h"
int parse_tls_header(const char *data, size_t data_len, char **hostname);
struct TLSProtocol;
int parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t data_len);
struct TLSProtocol *new_tls_data();
struct TLSProtocol *tls_data_set_list(struct TLSProtocol *, int, char**);
#endif