From b9885401050ad27d9fa13ffa67d5e43441f495c0 Mon Sep 17 00:00:00 2001
From: moparisthebest <admin@moparisthebest.com>
Date: Sun, 12 Jul 2015 23:10:53 -0400
Subject: [PATCH] Add SNI hostname based probe

---
 Makefile    |   4 +-
 example.cfg |   1 +
 probe.c     |  31 +++++++
 probe.h     |   3 +-
 sslh-main.c |  39 ++++++++-
 tls.c       | 238 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 tls.h       |  33 ++++++++
 7 files changed, 345 insertions(+), 4 deletions(-)
 create mode 100644 tls.c
 create mode 100644 tls.h

diff --git a/Makefile b/Makefile
index 001e7f5..146d29d 100644
--- a/Makefile
+++ b/Makefile
@@ -23,7 +23,7 @@ CC ?= gcc
 CFLAGS ?=-Wall -g $(CFLAGS_COV)
 
 LIBS=
-OBJS=common.o sslh-main.o probe.o
+OBJS=common.o sslh-main.o probe.o tls.o
 
 ifneq ($(strip $(USELIBWRAP)),)
 	LIBS:=$(LIBS) -lwrap
@@ -63,7 +63,7 @@ sslh-select: version.h $(OBJS) sslh-select.o Makefile common.h
 	#strip sslh-select
 
 echosrv: $(OBJS) echosrv.o
-	$(CC) $(CFLAGS) $(LDFLAGS) -o echosrv echosrv.o probe.o common.o $(LIBS)
+	$(CC) $(CFLAGS) $(LDFLAGS) -o echosrv echosrv.o probe.o common.o tls.o $(LIBS)
 
 $(MAN): sslh.pod Makefile
 	pod2man --section=8 --release=$(VERSION) --center=" " sslh.pod | gzip -9 - > $(MAN)
diff --git a/example.cfg b/example.cfg
index 20299c9..f9d76fa 100644
--- a/example.cfg
+++ b/example.cfg
@@ -36,6 +36,7 @@ listen:
 protocols:
 (
      { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; probe: "builtin"; },
+     { name: "sni"; host: "localhost"; port: "993"; probe: "builtin"; sni_hostnames: [ "imap.example.org", "imap.example.com" ]; },
      { name: "openvpn"; host: "localhost"; port: "1194"; probe: [ "^\x00[\x0D-\xFF]$", "^\x00[\x0D-\xFF]\x38" ]; },
      { name: "xmpp"; host: "localhost"; port: "5222"; probe: [ "jabber" ]; },
      { name: "http"; host: "localhost"; port: "80"; probe: "builtin"; },
diff --git a/probe.c b/probe.c
index c5500b5..1a5f194 100644
--- a/probe.c
+++ b/probe.c
@@ -216,6 +216,33 @@ 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)
+{
+    int valid_tls;
+    char *hostname;
+
+    valid_tls = parse_tls_header(p, len, &hostname);
+
+    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;
+
+    char **sni_hostname = proto->data;
+
+    for (; *sni_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)
 {
     if (len < 3)
@@ -334,6 +361,10 @@ 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;
+
     return NULL;
 }
 
diff --git a/probe.h b/probe.h
index d79b795..81ac684 100644
--- a/probe.h
+++ b/probe.h
@@ -4,6 +4,7 @@
 #define __PROBE_H_
 
 #include "common.h"
+#include "tls.h"
 
 typedef enum {
     PROBE_NEXT,  /* Enough data, probe failed -- it's some other protocol */
@@ -23,7 +24,7 @@ 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 */
+    void* data;     /* opaque pointer ; used to pass list of regex to regex probe, or sni hostnames to sni probe */
     struct proto *next; /* pointer to next protocol in list, NULL if last */
 };
 
diff --git a/sslh-main.c b/sslh-main.c
index 2029856..60fe6ce 100644
--- a/sslh-main.c
+++ b/sslh-main.c
@@ -211,13 +211,45 @@ 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)
+{
+    int num_probes, i, max_server_name_len, server_name_len;
+    const char * sni_hostname;
+    char** sni_hostname_list;
+
+    num_probes = config_setting_length(sni_hostnames);
+    if (!num_probes) {
+        fprintf(stderr, "%s: no sni_hostnames specified\n", p->description);
+        exit(1);
+    }
+
+    max_server_name_len = 0;
+    for (i = 0; i < num_probes; i++) {
+        server_name_len = strlen(config_setting_get_string_elem(sni_hostnames, 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);
+        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]);
+    }
+}
+#endif
+
 /* Extract configuration for protocols to connect to.
  * out: newly-allocated list of protocols
  */
 #ifdef LIBCONFIG
 static int config_protocols(config_t *config, struct proto **prots)
 {
-    config_setting_t *setting, *prot, *probes;
+    config_setting_t *setting, *prot, *probes, *sni_hostnames;
     const char *hostname, *port, *name;
     int i, num_prots;
     struct proto *p, *prev = NULL;
@@ -265,6 +297,11 @@ static int config_protocols(config_t *config, struct proto **prots)
                         }
                     }
                 }
+
+                sni_hostnames = config_setting_get_member(prot, "sni_hostnames");
+                if (sni_hostnames && config_setting_is_array(sni_hostnames)) {
+                    setup_sni_hostnames(p, sni_hostnames);
+                }
             }
         }
     }
diff --git a/tls.c b/tls.c
new file mode 100644
index 0000000..16c072c
--- /dev/null
+++ b/tls.c
@@ -0,0 +1,238 @@
+/*
+ * Copyright (c) 2011 and 2012, Dustin Lundquist <dustin@null-ptr.net>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+/*
+ * This is a minimal TLS implementation intended only to parse the server name
+ * extension.  This was created based primarily on Wireshark dissection of a
+ * TLS handshake and RFC4366.
+ */
+#include <stdio.h>
+#include <stdlib.h> /* malloc() */
+#include <string.h> /* strncpy() */
+#include <sys/socket.h>
+#include "tls.h"
+
+#define TLS_HEADER_LEN 5
+#define TLS_HANDSHAKE_CONTENT_TYPE 0x16
+#define TLS_HANDSHAKE_TYPE_CLIENT_HELLO 0x01
+
+#ifndef MIN
+#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 */
+};
+
+/* Parse a TLS packet for the Server Name Indication extension in the client
+ * hello handshake, returning the first servername found (pointer to static
+ * array)
+ *
+ * Returns:
+ *  >=0  - length of the hostname and updates *hostname
+ *         caller is responsible for freeing *hostname
+ *  -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) {
+    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");
+        return -5;
+    }
+
+    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",
+              tls_version_major, tls_version_minor);
+
+        return -2;
+    }
+
+    /* TLS record length */
+    len = ((unsigned char)data[3] << 8) +
+        (unsigned char)data[4] + TLS_HEADER_LEN;
+    data_len = MIN(data_len, len);
+
+    /* Check we received entire TLS record length */
+    if (data_len < len)
+        return -1;
+
+    /*
+     * Handshake
+     */
+    if (pos + 1 > data_len) {
+        return -5;
+    }
+    if (data[pos] != TLS_HANDSHAKE_TYPE_CLIENT_HELLO) {
+        if (verbose) fprintf(stderr, "Not a client hello\n");
+
+        return -5;
+    }
+
+    /* Skip past fixed length records:
+       1	Handshake Type
+       3	Length
+       2	Version (again)
+       32	Random
+       to	Session ID Length
+     */
+    pos += 38;
+
+    /* Session ID */
+    if (pos + 1 > data_len)
+        return -5;
+    len = (unsigned char)data[pos];
+    pos += 1 + len;
+
+    /* Cipher Suites */
+    if (pos + 2 > data_len)
+        return -5;
+    len = ((unsigned char)data[pos] << 8) + (unsigned char)data[pos + 1];
+    pos += 2 + len;
+
+    /* Compression Methods */
+    if (pos + 1 > data_len)
+        return -5;
+    len = (unsigned char)data[pos];
+    pos += 1 + len;
+
+    if (pos == data_len && tls_version_major == 3 && tls_version_minor == 0) {
+        if (verbose) fprintf(stderr, "Received SSL 3.0 handshake without extensions\n");
+        return -2;
+    }
+
+    /* Extensions */
+    if (pos + 2 > data_len)
+        return -5;
+    len = ((unsigned char)data[pos] << 8) + (unsigned char)data[pos + 1];
+    pos += 2;
+
+    if (pos + len > data_len)
+        return -5;
+    return parse_extensions(data + pos, len, hostname);
+}
+
+int
+parse_extensions(const char *data, size_t data_len, char **hostname) {
+    size_t pos = 0;
+    size_t len;
+
+    /* 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];
+
+        /* 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);
+        }
+        pos += 4 + len; /* Advance to the next extension header */
+    }
+    /* Check we ended where we expected to */
+    if (pos != data_len)
+        return -5;
+
+    return -2;
+}
+
+int
+parse_server_name_extension(const char *data, size_t data_len,
+        char **hostname) {
+    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];
+
+        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;
+                }
+
+                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]);
+        }
+        pos += 3 + len;
+    }
+    /* Check we ended where we expected to */
+    if (pos != data_len)
+        return -5;
+
+    return -2;
+}
diff --git a/tls.h b/tls.h
new file mode 100644
index 0000000..ce7a8c6
--- /dev/null
+++ b/tls.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2011 and 2012, Dustin Lundquist <dustin@null-ptr.net>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#ifndef TLS_H
+#define TLS_H
+
+#include "common.h"
+
+int parse_tls_header(const char *data, size_t data_len, char **hostname);
+
+#endif