From 8af039d3eb3e890482fd6755532d7786a104cae8 Mon Sep 17 00:00:00 2001 From: moparisthebest Date: Tue, 5 Jan 2016 00:32:10 -0500 Subject: [PATCH] Add ALPN protocol based probe --- ChangeLog | 6 +- example.cfg | 28 ++++++-- probe.c | 29 ++------ probe.h | 3 +- sslh-main.c | 48 ++++++++----- tls.c | 199 +++++++++++++++++++++++++++++++++++++--------------- tls.h | 7 +- 7 files changed, 216 insertions(+), 104 deletions(-) diff --git a/ChangeLog b/ChangeLog index f107380..c3f8d58 100644 --- a/ChangeLog +++ b/ChangeLog @@ -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. diff --git a/example.cfg b/example.cfg index 91f0f36..06a439c 100644 --- a/example.cfg +++ b/example.cfg @@ -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" ]; }, diff --git a/probe.c b/probe.c index 73be132..a2d0d55 100644 --- a/probe.c +++ b/probe.c @@ -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 */ diff --git a/probe.h b/probe.h index 0a663fb..bc69acf 100644 --- a/probe.h +++ b/probe.h @@ -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 */ }; diff --git a/sslh-main.c b/sslh-main.c index e35bfd9..246814d 100644 --- a/sslh-main.c +++ b/sslh-main.c @@ -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); } } + } } } diff --git a/tls.c b/tls.c index 16c072c..8640dec 100644 --- a/tls.c +++ b/tls.c @@ -30,8 +30,6 @@ */ #include #include /* malloc() */ -#include /* strncpy() */ -#include #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; @@ -108,7 +92,7 @@ parse_tls_header(const char *data, size_t data_len, char **hostname) { /* TLS record length */ 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); /* 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) 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) { /* Extension Length */ - len = ((unsigned char)data[pos + 2] << 8) + - (unsigned char)data[pos + 3]; + len = ((unsigned char) data[pos + 2] << 8) + + (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 */ - 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); + /* 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,32 +227,25 @@ 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; while (pos + 3 < data_len) { len = ((unsigned char)data[pos + 1] << 8) + - (unsigned char)data[pos + 2]; + (unsigned char)data[pos + 2]; if (pos + 3 + len > data_len) return -5; 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; + if(has_match(tls_data->sni_hostname_list, data + pos + 3, len)) { + return len; + } else { + return -2; } - - strncpy(*hostname, data + pos + 3, len); - - (*hostname)[len] = '\0'; - - return len; 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; +} diff --git a/tls.h b/tls.h index ce7a8c6..0e79e0c 100644 --- a/tls.h +++ b/tls.h @@ -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