修改Apache mod_proxy实现自定义SNI

1.需求:

使用常用的Web服务器构建一个具有负载均衡功能的七层反向代理服务器,其具有如下功能:

  1. 在负载均衡器(前端)和后端间使用SSL链接,教研后端提供证书的有效性
  2. 前端提供X-Forwarded-For以便后端获取客户端真实IP
  3. 无需后端做除"Real-IP Header"外的任何更改

具有Virtual Host的HTTPS主机在接受请求时,存在两个hostname:

  • TLS层的SNI(Server Name Indication),被服务器用于提供正确的证书;被客户端用于校验证书有效性.
  • HTTP层的Host:Header,被用于将请求交由正确的Virtual Host处理

这两个hostname在大多数情况下不被要求相同,而此例中,作为后端的主机在SNI中的域名与Host Header中所包含的不同时会拒绝连接

一般地,如下配置便可实现反向代理:

  1. 通过/etc/hosts将后端的IP指向backend-01.domain.tld
  2. 确保后端使用的证书覆盖backend-01.domain.tld
  3. 前端做如下配置:
<VirtualHost *:443>
    SSLEngine On
    SSLProxyEngine On
    ServerName domain.tld
    ProxyPreserveHost On
    SSLProxyCheckPeerExpire On
	SSLProxyCheckPeerName On
    <Location />
        ProxyPass https://backend-01.domain.tld/
        ProxyPassReverse https://domain.tld/
        RequestHeader set Host domain.tld
        RequestHeader append X-Forwarded-For %{REMOTE_ADDR}s
    </Location>
</VirtualHost>

如此配置后,前端向后端发送请求时,用domain.tld作为Host Header,而用backend-01.domain.tld作为SNI hostname.两个主机名不同将导致后端拒绝连接.

如果前端直接将domain.tld通过/etc/hosts指向后端的IP,可能导致不可预知的错误.

由此一来,计划修改Apache源码,以实现自定义向后端发起请求时的SNI,对mod_proxymod_proxy_http2做如下修改:

diff -Nar -u3 httpd-2.4.38/modules/http2/mod_proxy_http2.c httpd-2.4.38-patched/modules/http2/mod_proxy_http2.c
--- a/modules/http2/mod_proxy_http2.c	2018-02-14 10:43:36.000000000 +1100
+++ b/modules/http2/mod_proxy_http2.c	2019-03-15 20:25:17.822474505 +1100
@@ -594,12 +594,22 @@
             /* New conection: set a note on the connection what CN is
              * requested and what protocol we want */
             if (ctx->p_conn->ssl_hostname) {
-                ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, ctx->owner, 
-                              "set SNI to %s for (%s)", 
-                              ctx->p_conn->ssl_hostname, 
-                              ctx->p_conn->hostname);
-                apr_table_setn(ctx->p_conn->connection->notes,
-                               "proxy-request-hostname", ctx->p_conn->ssl_hostname);
+                if (worker->s->custom_sni_set != 1) {
+                  ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, ctx->owner, 
+                                "set SNI to %s for (%s)", 
+                                ctx->p_conn->ssl_hostname, 
+                                ctx->p_conn->hostname);
+                  apr_table_setn(ctx->p_conn->connection->notes,
+                                 "proxy-request-hostname", ctx->p_conn->ssl_hostname);
+                }
+                if (worker->s->custom_sni_set == 1) {
+                  ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, ctx->owner, 
+                                "set SNI to custom %s for (%s)", 
+                                worker->s->custom_sni, 
+                                ctx->p_conn->hostname);
+                  apr_table_setn(ctx->p_conn->connection->notes,
+                                 "proxy-request-hostname", worker->s->custom_sni);
+                }
             }
             if (ctx->is_ssl) {
                 apr_table_setn(ctx->p_conn->connection->notes,
diff -Nar -u3 httpd-2.4.38/modules/proxy/mod_proxy.c httpd-2.4.38-patched/modules/proxy/mod_proxy.c
--- a/modules/proxy/mod_proxy.c	2018-09-01 05:09:50.000000000 +1000
+++ b/modules/proxy/mod_proxy.c	2019-03-15 20:29:28.650492064 +1100
@@ -113,6 +113,15 @@
             return "LoadFactor must be a number between 1..100";
         worker->s->lbfactor = ival;
     }
+    else if (!strcasecmp(key, "custom_sni")) {
+        /* Set customized SNI (SSL hostname) for the worker
+         */
+        if (strlen(val) >= sizeof(worker->s->custom_sni))
+            return apr_psprintf(p, "Custom SNI must be < %d characters",
+                    (int)sizeof(worker->s->custom_sni));
+        PROXY_STRNCPY(worker->s->custom_sni, val);
+        worker->s->custom_sni_set = 1;
+    }
     else if (!strcasecmp(key, "retry")) {
         /* If set it will give the retry timeout for the worker
          * The default value is 60 seconds, meaning that if
diff -Nar -u3 httpd-2.4.38/modules/proxy/mod_proxy.h httpd-2.4.38-patched/modules/proxy/mod_proxy.h
--- a/modules/proxy/mod_proxy.h	2018-09-11 21:57:19.000000000 +1000
+++ b/modules/proxy/mod_proxy.h	2019-03-15 20:33:49.508689199 +1100
@@ -359,6 +359,7 @@
 #define PROXY_BALANCER_MAX_STICKY_SIZE  64
 
 #define PROXY_RFC1035_HOSTNAME_SIZE	256
+#define PROXY_CUSTOM_SNI_SIZE 256
 
 /* RFC-1035 mentions limits of 255 for host-names and 253 for domain-names,
  * dotted together(?) this would fit the below size (+ trailing NUL).
@@ -431,6 +432,7 @@
     unsigned int     keepalive:1;
     unsigned int     disablereuse:1;
     unsigned int     is_address_reusable:1;
+    unsigned int     custom_sni_set:1;
     unsigned int     retry_set:1;
     unsigned int     timeout_set:1;
     unsigned int     acquire_set:1;
@@ -451,6 +453,7 @@
     apr_interval_time_t interval;
     char      upgrade[PROXY_WORKER_MAX_SCHEME_SIZE];/* upgrade protocol used by mod_proxy_wstunnel */
     char      hostname_ex[PROXY_RFC1035_HOSTNAME_SIZE];  /* RFC1035 compliant version of the remote backend address */
+    char      custom_sni[PROXY_CUSTOM_SNI_SIZE]; /* Custom TLS SNI hostname */
     apr_size_t   response_field_size; /* Size of proxy response buffer in bytes. */
     unsigned int response_field_size_set:1;
 } proxy_worker_shared;
diff -Nar -u3 httpd-2.4.38/modules/proxy/mod_proxy_http.c httpd-2.4.38-patched/modules/proxy/mod_proxy_http.c
--- a/modules/proxy/mod_proxy_http.c	2018-09-12 00:04:53.000000000 +1000
+++ b/modules/proxy/mod_proxy_http.c	2019-03-15 20:35:27.670518121 +1100
@@ -1981,11 +1981,19 @@
              * requested, such that mod_ssl can check if it is requested to do
              * so.
              */
-            if (backend->ssl_hostname) {
+            if (worker->s->custom_sni_set != 1 &&
+                backend->ssl_hostname) {
                 apr_table_setn(backend->connection->notes,
                                "proxy-request-hostname",
                                backend->ssl_hostname);
             }
+
+            if (worker->s->custom_sni_set == 1 &&
+                worker->s->custom_sni) {
+                apr_table_setn(backend->connection->notes,
+                               "proxy-request-hostname",
+                               worker->s->custom_sni);
+            }
         }
 
         /* Step Four: Send the Request

使用此补丁后,可在ProxyPass中用custom_sni指定要使用的SNI hostname.

前端使用如下配置即可:

<VirtualHost *:443>
    SSLEngine On
    SSLProxyEngine On
    ServerName domain.tld
    ProxyPreserveHost On
    SSLProxyCheckPeerExpire On
	SSLProxyCheckPeerName On
    <Location />
        ProxyPass https://backend-01.domain.tld/ custom_sni=domain.tld
        ProxyPassReverse https://domain.tld/
        RequestHeader set Host domain.tld
        RequestHeader append X-Forwarded-For %{REMOTE_ADDR}s
    </Location>
</VirtualHost>