Blob Blame History Raw
diff --git a/parseconf.c b/parseconf.c
index 3729818..ee1b8b4 100644
--- a/parseconf.c
+++ b/parseconf.c
@@ -188,6 +188,7 @@ parseconf_str_array[] =
   { "rsa_private_key_file", &tunable_rsa_private_key_file },
   { "dsa_private_key_file", &tunable_dsa_private_key_file },
   { "ca_certs_file", &tunable_ca_certs_file },
+  { "ssl_sni_hostname", &tunable_ssl_sni_hostname },
   { "cmds_denied", &tunable_cmds_denied },
   { 0, 0 }
 };
diff --git a/ssl.c b/ssl.c
index 09ec96a..b622347 100644
--- a/ssl.c
+++ b/ssl.c
@@ -41,6 +41,13 @@ static long bio_callback(
   BIO* p_bio, int oper, const char* p_arg, int argi, long argl, long retval);
 static int ssl_verify_callback(int verify_ok, X509_STORE_CTX* p_ctx);
 static DH *ssl_tmp_dh_callback(SSL *ssl, int is_export, int keylength);
+static int ssl_alpn_callback(SSL* p_ssl,
+                             const unsigned char** p_out,
+                             unsigned char* outlen,
+                             const unsigned char* p_in,
+                             unsigned int inlen,
+                             void* p_arg);
+static long ssl_sni_callback(SSL* p_ssl, int* p_al, void* p_arg);
 static int ssl_cert_digest(
   SSL* p_ssl, struct vsf_session* p_sess, struct mystr* p_str);
 static void maybe_log_shutdown_state(struct vsf_session* p_sess);
@@ -285,6 +292,11 @@ ssl_init(struct vsf_session* p_sess)
       SSL_CTX_set_timeout(p_ctx, INT_MAX);
     }
     
+    /* Set up ALPN to check for FTP protocol intention of client. */
+    SSL_CTX_set_alpn_select_cb(p_ctx, ssl_alpn_callback, p_sess);
+    /* Set up SNI callback for an optional hostname check. */
+    SSL_CTX_set_tlsext_servername_callback(p_ctx, ssl_sni_callback);
+    SSL_CTX_set_tlsext_servername_arg(p_ctx, p_sess);
     SSL_CTX_set_tmp_dh_callback(p_ctx, ssl_tmp_dh_callback);
 
     if (tunable_ecdh_param_file)
@@ -871,6 +883,133 @@ ssl_tmp_dh_callback(SSL *ssl, int is_export, int keylength)
   return DH_get_dh(keylength);
 }
 
+static int
+ssl_alpn_callback(SSL* p_ssl,
+                  const unsigned char** p_out,
+                  unsigned char* outlen,
+                  const unsigned char* p_in,
+                  unsigned int inlen,
+                  void* p_arg) {
+    unsigned int i;
+    struct vsf_session* p_sess = (struct vsf_session*) p_arg;
+    int is_ok = 0;
+
+    (void) p_ssl;
+
+    /* Initialize just in case. */
+    *p_out = p_in;
+    *outlen = 0;
+
+    for (i = 0; i < inlen; ++i) {
+        unsigned int left = (inlen - i);
+        if (left < 4) {
+            continue;
+        }
+        if (p_in[i] == 3 && p_in[i + 1] == 'f' && p_in[i + 2] == 't' &&
+            p_in[i + 3] == 'p')
+        {
+            is_ok = 1;
+            *p_out = &p_in[i + 1];
+            *outlen = 3;
+            break;
+        }
+    }
+
+    if (!is_ok)
+    {
+        str_alloc_text(&debug_str, "ALPN rejection");
+        vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str);
+    }
+    if (!is_ok || tunable_debug_ssl)
+    {
+        str_alloc_text(&debug_str, "ALPN data: ");
+        for (i = 0; i < inlen; ++i) {
+            str_append_char(&debug_str, p_in[i]);
+        }
+        vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str);
+    }
+
+    if (is_ok)
+    {
+        return SSL_TLSEXT_ERR_OK;
+    }
+    else
+    {
+        return SSL_TLSEXT_ERR_ALERT_FATAL;
+    }
+}
+
+static long
+ssl_sni_callback(SSL* p_ssl, int* p_al, void* p_arg)
+{
+    static struct mystr s_sni_expected_hostname;
+    static struct mystr s_sni_received_hostname;
+
+    int servername_type;
+    const char* p_sni_servername;
+    struct vsf_session* p_sess = (struct vsf_session*) p_arg;
+    int is_ok = 0;
+
+    (void) p_ssl;
+    (void) p_arg;
+
+    if (tunable_ssl_sni_hostname)
+    {
+        str_alloc_text(&s_sni_expected_hostname, tunable_ssl_sni_hostname);
+    }
+
+    /* The OpenSSL documentation says it is pre-initialized like this, but set
+     * it just in case.
+     */
+    *p_al = SSL_AD_UNRECOGNIZED_NAME;
+
+    servername_type = SSL_get_servername_type(p_ssl);
+    p_sni_servername = SSL_get_servername(p_ssl, TLSEXT_NAMETYPE_host_name);
+    if (p_sni_servername != NULL) {
+        str_alloc_text(&s_sni_received_hostname, p_sni_servername);
+    }
+
+    if (str_isempty(&s_sni_expected_hostname))
+    {
+        is_ok = 1;
+    }
+    else if (servername_type != TLSEXT_NAMETYPE_host_name)
+    {
+        /* Fail. */
+        str_alloc_text(&debug_str, "SNI bad type: ");
+        str_append_ulong(&debug_str, servername_type);
+        vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str);
+    }
+    else
+    {
+        if (!str_strcmp(&s_sni_expected_hostname, &s_sni_received_hostname))
+        {
+            is_ok = 1;
+        }
+        else
+        {
+            str_alloc_text(&debug_str, "SNI rejection");
+            vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str);
+        }
+    }
+
+    if (!is_ok || tunable_debug_ssl)
+    {
+        str_alloc_text(&debug_str, "SNI hostname: ");
+        str_append_str(&debug_str, &s_sni_received_hostname);
+        vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str);
+    }
+
+    if (is_ok)
+    {
+        return SSL_TLSEXT_ERR_OK;
+    }
+    else
+    {
+        return SSL_TLSEXT_ERR_ALERT_FATAL;
+    }
+}
+
 void
 ssl_add_entropy(struct vsf_session* p_sess)
 {
diff --git a/tunables.c b/tunables.c
index c96c1ac..d8dfcde 100644
--- a/tunables.c
+++ b/tunables.c
@@ -152,6 +152,7 @@ const char* tunable_ssl_ciphers;
 const char* tunable_rsa_private_key_file;
 const char* tunable_dsa_private_key_file;
 const char* tunable_ca_certs_file;
+const char* tunable_ssl_sni_hostname;
 
 static void install_str_setting(const char* p_value, const char** p_storage);
 
@@ -309,6 +310,7 @@ tunables_load_defaults()
   install_str_setting(0, &tunable_rsa_private_key_file);
   install_str_setting(0, &tunable_dsa_private_key_file);
   install_str_setting(0, &tunable_ca_certs_file);
+  install_str_setting(0, &tunable_ssl_sni_hostname);
 }
 
 void
diff --git a/tunables.h b/tunables.h
index 8d50150..de6cab0 100644
--- a/tunables.h
+++ b/tunables.h
@@ -157,6 +157,7 @@ extern const char* tunable_ssl_ciphers;
 extern const char* tunable_rsa_private_key_file;
 extern const char* tunable_dsa_private_key_file;
 extern const char* tunable_ca_certs_file;
+extern const char* tunable_ssl_sni_hostname;
 extern const char* tunable_cmds_denied;
 
 #endif /* VSF_TUNABLES_H */
diff --git a/vsftpd.conf.5 b/vsftpd.conf.5
index 815773f..7006287 100644
--- a/vsftpd.conf.5
+++ b/vsftpd.conf.5
@@ -1128,6 +1128,12 @@ for further details.
 
 Default: PROFILE=SYSTEM
 .TP
+.B ssl_sni_hostname
+If set, SSL connections will be rejected unless the SNI hostname in the
+incoming handshakes matches this value.
+
+Default: (none)
+.TP
 .B user_config_dir
 This powerful option allows the override of any config option specified in
 the manual page, on a per-user basis. Usage is simple, and is best illustrated