1.需求:
使用常用的Web服务器构建一个具有负载均衡功能的七层反向代理服务器,其具有如下功能:
- 在负载均衡器(前端)和后端间使用SSL链接,教研后端提供证书的有效性
- 前端提供
X-Forwarded-For
以便后端获取客户端真实IP - 无需后端做除"Real-IP Header"外的任何更改
具有Virtual Host的HTTPS主机在接受请求时,存在两个hostname:
- TLS层的SNI(Server Name Indication),被服务器用于提供正确的证书;被客户端用于校验证书有效性.
- HTTP层的
Host:
Header,被用于将请求交由正确的Virtual Host处理
这两个hostname在大多数情况下不被要求相同,而此例中,作为后端的主机在SNI中的域名与Host Header中所包含的不同时会拒绝连接。
一般地,如下配置便可实现反向代理:
- 通过
/etc/hosts
将后端的IP指向backend-01.domain.tld
- 确保后端使用的证书覆盖
backend-01.domain.tld
- 前端做如下配置:
<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_proxy
与mod_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>