基于主机名的 Nginx TCP 转发

随着 Nginx 社区版本 TCP 负载均衡的发布,我想混合使用 OpenVPN 和 SSL 直通数据。Nginx 知道如何路由流量的唯一方法是通过他们的域名。

 vpn1.app.com ─┬─► nginx at 10.0.0.1 ─┬─► vpn1  at 10.0.0.3
vpn2.app.com ─┤                      ├─► vpn2  at 10.0.0.4
https.app.com ─┘                      └─► https at 10.0.0.5

我已经看了 TCP 指南模块文档,但它似乎没有很好的参考。如果有人能给我指明正确的方向,我会很感激的。

关于 ServerFault 的相关问题: 反向代理可以通过 SSL 传递使用 SNI 吗?

81483 次浏览

假设

如果我没有理解错的话,您实际上希望 nginx 监听单个 IP 地址和 TCP 端口组合(例如 listen 10.0.0.1:443) ,然后根据传入 TCP 流通信的特征,将其路由到3个不同的 IP 地址之一。

你没有明确提到你期望它如何区分3个不同的领域,但是我的假设是你假设它只是 TLS,并且必须采用某种 TLS SNI (服务器名称指示)机制来进行基于领域的区分。

我相信在 http://nginx.org/docs/中提供的流相关文档是非常权威和详尽的(我在这里列出了所有这些,因为显然还没有交叉引用的中心位置,例如,还没有从“流核心”模块到子模块的引用(而 docs/stream/只是重定向回 docs/) ,这确实是相当混乱的,因为像 http://nginx.org/r/upstream这样的东西只被记录到适用于 http,而没有提到适用于 stream,即使指令在最后是相同的) :


回答我

注意,来自每个模块的每个 nginx 指令只有有限数量的适用 Context

因此,不幸的是,这里根本没有指令来窥探 SNI!

相反,stream_core中有记载,引用“ Different servers must listen on different address:port pairs.”,你可能会注意到,这也是如何 ABC2指令在更常见的 http_core中起作用相反,是一个相当明确的参考事实,目前没有任何种类的 SNI 支持实施的 listenstream


讨论

作为一个讨论点和解决方案建议,假设 OpenVPN 流量只是 TLS 和可窥探的 SNI 也不一定是正确的(但我不太熟悉 OpenSSL 或 SNI) :

  • 考虑到即使 SNI 现在是被动的可窥探的,这显然违背了 TLS 保持连接安全的承诺,因此,在 TLS 的未来版本中可能会发生变化。

  • 为了便于讨论,如果 OpenVPN 只使用 TLS 连接,如果它 不是使用 TLS 对用户进行身份验证(这将使得 MitM 流更加困难,但仍然始终携带身份验证数据) ,那么,理论上,如果 nginx 有 SNI 支持stream内围绕 listen,那么你可能已经能够使用 nginx 主动地 MitM 它(自 ABC3中已经支持 proxy_ssl)。

最重要的是,我相信 OpenVPN 最好是在它自己的基于 UDP 的协议上运行,在这种情况下,您可以使用相同的 IP 地址和端口号,一个实例的基于 TCP 的 https 和另一个基于 UDP 的 OpenVPN 没有冲突。

最后,您可能会问,那么流模块对于什么是有用的呢?我相信它的目标受众将是,(0) ,基于客户端 IP 地址的 hash的多个 upstream服务器的负载平衡 HTTP/2,和/或,(1) ,一个更直接和协议无关的 stunnel替代品。

现在,随着在 Nginx 1.11.5版本和1.11.2版本中分别添加了 Ngx _ stream _ ssl _ preread 模块Ngx _ stream _ map 模块,这种情况已经成为可能。

这允许 Nginx 读取 TLS 客户端 Hello,并根据 SNI 扩展决定使用哪个后端。

stream {


map $ssl_preread_server_name $name {
vpn1.app.com vpn1_backend;
vpn2.app.com vpn2_backend;
https.app.com https_backend;
default https_default_backend;
}


upstream vpn1_backend {
server 10.0.0.3:443;
}


upstream vpn2_backend {
server 10.0.0.4:443;
}


upstream https_backend {
server 10.0.0.5:443;
}


upstream https_default_backend {
server 127.0.0.1:443;
}


server {
listen 10.0.0.1:443;
proxy_pass $name;
ssl_preread on;
}
}

正如@Lochnair 提到的,您可以使用 Ngx _ stream _ map 模块变量 $server _ addr来解决这个问题。

我的主机 IP 是 192.168.168.22,并且我使用保持有效的将2个虚拟 IP 绑定到 eth0

$sudo ip a
...
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
link/ether 5c:f3:fc:b9:f0:84 brd ff:ff:ff:ff:ff:ff
inet 192.168.168.22/24 brd 192.168.168.255 scope global eth0
valid_lft forever preferred_lft forever
inet 192.168.168.238/32 scope global eth0
valid_lft forever preferred_lft forever
inet 192.168.168.239/32 scope global eth0
valid_lft forever preferred_lft forever


$nginx -v
nginx version: nginx/1.13.2


$cat /etc/nginx/nginx.conf
...
stream {
upstream pod53{
server 10.1.5.3:3306;
}
upstream pod54{
server 10.1.5.4:3306;
}


map $server_addr $x {
192.168.168.238 pod53;
192.168.168.239 pod54;
}
server {
listen 3306;
proxy_pass $x;
}
}

因此,我可以通过不同的 VIP 访问具有相同端口3306的不同 MySQL 服务。就像通过不同的 server_name访问具有相同端口的不同 HTTP 服务一样。

192.168.168.238 -> 10.1.5.3
192.168.168.239 -> 10.1.5.4