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

View File

@ -24,14 +24,19 @@ listen:
# #
# Each protocol entry consists of: # Each protocol entry consists of:
# name: name of the probe. These are listed on the command # 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)) # service: (optional) libwrap service name (see hosts_access(5))
# host, port: where to connect when this probe succeeds # host, port: where to connect when this probe succeeds
# #
# Probe-specific options: # Probe-specific options:
# sni: # tls:
# sni_hotnames: list of FQDN for that target # 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:
# regex_patterns: list of patterns to match for # regex_patterns: list of patterns to match for
# that target. # that target.
@ -39,15 +44,26 @@ listen:
# sslh will try each probe in order they are declared, and # sslh will try each probe in order they are declared, and
# connect to the first that matches. # connect to the first that matches.
# #
# You can specify several of 'regex' and 'sni'. # You can specify several of 'regex' and 'tls'.
protocols: protocols:
( (
{ name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; }, { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; },
{ name: "http"; host: "localhost"; port: "80"; }, { name: "http"; host: "localhost"; port: "80"; },
{ name: "sni"; host: "localhost"; port: "993"; sni_hostnames: [ "mail.rutschle.net", "mail.englishintoulouse.com" ]; log_level: 0; }, # match BOTH ALPN/SNI
{ name: "sni"; host: "localhost"; port: "xmpp-client"; sni_hostnames: [ "im.rutschle.net", "im.englishintoulouse.com" ]; log_level: 0;}, { 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 # OpenVPN
{ name: "regex"; host: "localhost"; port: "1194"; regex_patterns: [ "^\x00[\x0D-\xFF]$", "^\x00[\x0D-\xFF]\x38" ]; }, { 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; 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; 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) if(valid_tls < 0)
return -1 == valid_tls ? PROBE_AGAIN : PROBE_NEXT; return -1 == valid_tls ? PROBE_AGAIN : PROBE_NEXT;
if (verbose) fprintf(stderr, "sni hostname: %s\n", hostname); /* There *was* a valid match */
return PROBE_MATCH;
/* 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;
} }
static int is_tls_protocol(const char *p, int len, struct proto *proto) 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")) if (!strcmp(description, "regex"))
return regex_probe; return regex_probe;
/* Special case of "sni" probe for same reason as above*/ /* Special case of "sni/alpn" probe for same reason as above*/
if (!strcmp(description, "sni")) if (!strcmp(description, "sni_alpn"))
return is_sni_protocol; return is_sni_alpn_protocol;
/* Special case of "timeout" is allowed as a probe name in the /* Special case of "timeout" is allowed as a probe name in the
* configuration file even though it's not really a probe */ * 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 /* function to probe that protocol; parameters are buffer and length
* containing the data to probe, and a pointer to the protocol structure */ * containing the data to probe, and a pointer to the protocol structure */
T_PROBE* probe; 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 */ 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 #endif
#ifdef LIBCONFIG #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; int num_probes, i, max_server_name_len, server_name_len;
const char * sni_hostname; const char * config_item;
char** sni_hostname_list; 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) { if (!num_probes) {
fprintf(stderr, "%s: no sni_hostnames specified\n", p->description); fprintf(stderr, "%s: no %s specified\n", p->description, name);
exit(1); return;
} }
max_server_name_len = 0; max_server_name_len = 0;
for (i = 0; i < num_probes; i++) { 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) if(server_name_len > max_server_name_len)
max_server_name_len = server_name_len; max_server_name_len = server_name_len;
} }
sni_hostname_list = calloc(num_probes + 1, ++max_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++) { 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); sni_hostname_list[i] = malloc(max_server_name_len);
strcpy (sni_hostname_list[i], sni_hostname); strcpy (sni_hostname_list[i], config_item);
if(verbose) fprintf(stderr, "sni_hostnames[%d]: %s\n", i, sni_hostname_list[i]); 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 #endif
@ -250,7 +263,7 @@ static void setup_sni_hostnames(struct proto *p, config_setting_t* sni_hostnames
#ifdef LIBCONFIG #ifdef LIBCONFIG
static int config_protocols(config_t *config, struct proto **prots) 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; const char *hostname, *port, *name;
int i, num_prots; int i, num_prots;
struct proto *p, *prev = NULL; 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); resolve_split_name(&(p->saddr), hostname, port);
p->probe = get_probe(name); 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); fprintf(stderr, "line %d: %s: probe unknown\n", config_setting_source_line(prot), name);
exit(1); exit(1);
} }
@ -292,13 +305,16 @@ static int config_protocols(config_t *config, struct proto **prots)
} }
} }
/* Probe-specific options: SNI hostnames */ /* Probe-specific options: SNI/ALPN */
if (!strcmp(name, "sni")) { if (!strcmp(name, "tls")) {
sni_hostnames = config_setting_get_member(prot, "sni_hostnames"); sni_hostnames = config_setting_get_member(prot, "sni_hostnames");
if (sni_hostnames && config_setting_is_array(sni_hostnames)) { alpn_protocols = config_setting_get_member(prot, "alpn_protocols");
setup_sni_hostnames(p, sni_hostnames);
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);
} }
} }
} }
} }
} }

199
tls.c
View File

@ -30,8 +30,6 @@
*/ */
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> /* malloc() */ #include <stdlib.h> /* malloc() */
#include <string.h> /* strncpy() */
#include <sys/socket.h>
#include "tls.h" #include "tls.h"
#define TLS_HEADER_LEN 5 #define TLS_HEADER_LEN 5
@ -42,19 +40,20 @@
#define MIN(X, Y) ((X) < (Y) ? (X) : (Y)) #define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
#endif #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[] = { struct TLSProtocol {
0x15, /* TLS Alert */ int use_alpn;
0x03, 0x01, /* TLS version */ char** sni_hostname_list;
0x00, 0x02, /* Payload length */ char** alpn_protocol_list;
0x02, 0x28, /* Fatal, handshake failure */
}; };
/* Parse a TLS packet for the Server Name Indication extension in the client static int parse_extensions(const struct TLSProtocol *, const char *, size_t);
* hello handshake, returning the first servername found (pointer to static static int parse_server_name_extension(const struct TLSProtocol *, const char *, size_t);
* array) 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: * Returns:
* >=0 - length of the hostname and updates *hostname * >=0 - length of the hostname and updates *hostname
@ -62,35 +61,20 @@ const char tls_alert[] = {
* -1 - Incomplete request * -1 - Incomplete request
* -2 - No Host header included in this request * -2 - No Host header included in this request
* -3 - Invalid hostname pointer * -3 - Invalid hostname pointer
* -4 - malloc failure
* < -4 - Invalid TLS client hello * < -4 - Invalid TLS client hello
*/ */
int 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_content_type;
char tls_version_major; char tls_version_major;
char tls_version_minor; char tls_version_minor;
size_t pos = TLS_HEADER_LEN; size_t pos = TLS_HEADER_LEN;
size_t len; size_t len;
if (hostname == NULL)
return -3;
/* Check that our TCP payload is at least large enough for a TLS header */ /* Check that our TCP payload is at least large enough for a TLS header */
if (data_len < TLS_HEADER_LEN) if (data_len < TLS_HEADER_LEN)
return -1; 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]; tls_content_type = data[0];
if (tls_content_type != TLS_HANDSHAKE_CONTENT_TYPE) { if (tls_content_type != TLS_HANDSHAKE_CONTENT_TYPE) {
if (verbose) fprintf(stderr, "Request did not begin with TLS handshake.\n"); 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_major = data[1];
tls_version_minor = data[2]; tls_version_minor = data[2];
if (tls_version_major < 3) { 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); tls_version_major, tls_version_minor);
return -2; return -2;
@ -108,7 +92,7 @@ parse_tls_header(const char *data, size_t data_len, char **hostname) {
/* TLS record length */ /* TLS record length */
len = ((unsigned char)data[3] << 8) + len = ((unsigned char)data[3] << 8) +
(unsigned char)data[4] + TLS_HEADER_LEN; (unsigned char)data[4] + TLS_HEADER_LEN;
data_len = MIN(data_len, len); data_len = MIN(data_len, len);
/* Check we received entire TLS record length */ /* Check we received entire TLS record length */
@ -167,30 +151,75 @@ parse_tls_header(const char *data, size_t data_len, char **hostname) {
if (pos + len > data_len) if (pos + len > data_len)
return -5; return -5;
return parse_extensions(data + pos, len, hostname); return parse_extensions(tls_data, data + pos, len);
} }
int static int
parse_extensions(const char *data, size_t data_len, char **hostname) { parse_extensions(const struct TLSProtocol *tls_data, const char *data, size_t data_len) {
size_t pos = 0; size_t pos = 0;
size_t len; size_t len;
int last_matched = 0;
if (tls_data == NULL)
return -3;
/* Parse each 4 bytes for the extension header */ /* Parse each 4 bytes for the extension header */
while (pos + 4 <= data_len) { while (pos + 4 <= data_len) {
/* Extension Length */ /* Extension Length */
len = ((unsigned char)data[pos + 2] << 8) + len = ((unsigned char) data[pos + 2] << 8) +
(unsigned char)data[pos + 3]; (unsigned char) data[pos + 3];
if (pos + 4 + len > data_len)
return -5;
size_t extension_type = ((unsigned char) data[pos] << 8) +
(unsigned char) data[pos + 1];
/* Check if it's a server name extension */ /* 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
/* There can be only one extension of each type, so we break our state and move pos to beginning of the extension here */
our state and move p to beinnging of the extension here */ if (tls_data->use_alpn == 2) {
if (pos + 4 + len > data_len) /* we want BOTH alpn and sni to match */
return -5; if (extension_type == 0x00) { /* Server Name */
return parse_server_name_extension(data + pos + 4, len, hostname); 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 */ pos += 4 + len; /* Advance to the next extension header */
} }
/* Check we ended where we expected to */ /* Check we ended where we expected to */
if (pos != data_len) if (pos != data_len)
return -5; return -5;
@ -198,32 +227,25 @@ parse_extensions(const char *data, size_t data_len, char **hostname) {
return -2; return -2;
} }
int static int
parse_server_name_extension(const char *data, size_t data_len, parse_server_name_extension(const struct TLSProtocol *tls_data, const char *data, size_t data_len) {
char **hostname) {
size_t pos = 2; /* skip server name list length */ size_t pos = 2; /* skip server name list length */
size_t len; size_t len;
while (pos + 3 < data_len) { while (pos + 3 < data_len) {
len = ((unsigned char)data[pos + 1] << 8) + len = ((unsigned char)data[pos + 1] << 8) +
(unsigned char)data[pos + 2]; (unsigned char)data[pos + 2];
if (pos + 3 + len > data_len) if (pos + 3 + len > data_len)
return -5; return -5;
switch (data[pos]) { /* name type */ switch (data[pos]) { /* name type */
case 0x00: /* host_name */ case 0x00: /* host_name */
*hostname = malloc(len + 1); if(has_match(tls_data->sni_hostname_list, data + pos + 3, len)) {
if (*hostname == NULL) { return len;
if (verbose) fprintf(stderr, "malloc() failure\n"); } else {
return -4; return -2;
} }
strncpy(*hostname, data + pos + 3, len);
(*hostname)[len] = '\0';
return len;
default: default:
if (verbose) fprintf(stderr, "Unknown server name extension name type: %d\n", if (verbose) fprintf(stderr, "Unknown server name extension name type: %d\n",
data[pos]); data[pos]);
@ -236,3 +258,70 @@ parse_server_name_extension(const char *data, size_t data_len,
return -2; 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" #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 #endif