基于主机名的 Nginx TCP 转发

Nginx TCP forwarding based on hostname

随着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 guides and the module documentation,但它似乎没有被很好地引用。如果有人能指出正确的方向,我将不胜感激。

ServerFault 相关问题:Can a Reverse Proxy use SNI with SSL pass through?

假设

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

您没有明确提及您希望它如何区分所涉及的 3 个不同域,但我的假设是您假设它只是 TLS,并且必须使用某种 TLS SNI(服务器domain-based 区分的名称指示)机制。

我认为 http://nginx.org/docs/ is quite authoritative and exhaustive for the modules at stake (I'm listing all of it here, since apparently there's no central place for cross-referencing this yet, e.g., no references from the "stream core" module to the submodules yet (and docs/stream/ just redirects back docs/), which is indeed quite confusing, since stuff like http://nginx.org/r/upstream 中提供的 stream-related 文档仅记录为适用于 http,而没有提及对 stream 的适用性,即使最后指令大致相同):


回答

请注意,每个模块中的每个 nginx 指令都有有限数量的适用 Context

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

相反,it's actually documented in stream_core that, to quote, "Different servers must listen on different address:port pairs.", which, as you may note, is also contrary to how the listen directive works within the more-common http_core,并且是一个相当明确的参考,即目前在 stream.[=40 中的 listen 没有实现任何类型的 SNI 支持。 =]


讨论

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

  • 考虑到即使 SNI 今天是被动窥探的,这显然与 TLS 保持连接安全的承诺背道而驰,因此,可能会在 TLS 的未来版本中改变。

  • 为了便于讨论,如果 OpenVPN 只是使用 TLS 连接,并且 不是 使用 TLS 通过用户证书对用户进行身份验证(这会使流中的 MitM 变得更加困难,但仍然始终携带身份验证数据),然后,理论上,如果 nginx 确实支持 SNIstream 内的 listen 附近,那么你可能已经能够使用 nginx 主动中间人攻击它(自 proxy_ssl is already supported in stream_proxy)。

最重要的是,我认为 OpenVPN 最好是 运行 自己的 UDP-based 协议,在这种情况下,您可以对 [=] 的一个实例使用相同的 IP 地址和端口号100=] https 和另一个 UDP-based OpenVPN 没有冲突。

最后,你可能会问,stream 模块到底有什么用呢?我相信它的目标受众是,(0),负载平衡 HTTP/2 与多个 upstream 服务器,基于客户端 IP-address 的 hash,例如, and/or, (1),stunnel.

的更直接和 protocol-agnostic 替代

现在可以通过添加 1.11.2 中添加的 ngx_stream_ssl_preread module added in Nginx 1.11.5 and the ngx_stream_map module 来实现。

这允许 Nginx 读取 TLS Client 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 module and variable $server_addr 来解决此问题。这是我的例子。

我的主机IP是192.168.168.22,我用keepalived绑定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