基于主机名的Nginx TCP转发

82

随着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?


我的回答是否有任何遗漏或不清楚之处? - cnst
尽管提供了全面且有参考价值的答案,且未寻求额外澄清或指出问题,仍然未能授予承诺的赏金,评分为-1。 - cnst
3
感谢您最终接受并点赞我的回答,但我很困惑为什么在同一分钟内,我的其他不相关且毫无争议的答案却被莫名其妙地投了反对票?是发布悬赏成为新趋势吗?静悄悄地不给任何理由就不授予奖励,然后在被指出之后,默默地给那个帮助你的人的其他答案投反对票? - cnst
1
Stack Overflow 是一个编程和开发问题的网站。这个问题似乎与编程或开发无关。请参考帮助中心中的 我可以在这里提什么样的问题 。也许 Unix & Linux Stack Exchange 或者 Information Security Stack Exchange 更适合询问此类问题。 - jww
@jww 请将该主题标记为待审查并帮助关闭。 - James Wong
@JamesWong,我们需要在vpn1.app.com上使用SSL吗?如果需要,可以使用通配符证书吗? - rMili
3个回答

113

有了Nginx 1.11.5中新增的ngx_stream_ssl_preread模块和1.11.2中新增的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;
    }
}

51
未来到达这里并认为此方法适用于SSH连接的任何人 - 不必费心了,SSH客户端不会传递DNS名称。 - Dae
14
@Dae,SSH甚至与SSL无关。 - cnst
2
我不得不设置 proxy_protocol on; 才能让它工作,也许其他人也需要这样做... - Carel
1
@Carel 如果您在上游服务器上使用它,则只需要在此处使用proxy_protocol。 - Lochnair
1
@rMili,你做到了,而且是可以的。在这个阶段,证书甚至还没有发送,所以这并不重要。 - Lochnair
显示剩余11条评论

35

假设

如果我理解你的意思正确,你希望nginx在一个IP地址和TCP端口组合上进行监听(例如,listen 10.0.0.1:443),然后根据传入的TCP流量特征将其路由到三个不同的IP地址之一。

你没有明确提到如何区分三个不同的域名,但我的假设是你认为这都只是TLS,并且必须使用某种TLS SNI(服务器名称指示)机制来实现基于域的区分。

我认为http://nginx.org/docs/提供的与流相关的文档对于涉及的模块非常权威和详尽(我在此列出了所有内容,因为显然还没有一个集中的地方来进行交叉引用,例如,“stream core”模块还没有到子模块的引用(而docs/stream/只是重定向回docs/),这确实令人困惑,因为像http://nginx.org/r/upstream这样的东西只记录适用于http,没有提到适用于stream,即使最终指令是相同的)。

答案

请注意,每个 nginx 指令(来自每个模块)都有一个有限的适用上下文数量。

因此,不幸的是在这里没有简单的指令可以窥探到 SNI!

相反的,实际上在 stream_core 中有文件记录,引用一句话,“不同的服务器必须监听不同的地址和端口对。”,正如您所知,这与更常见的 http_core 内的 listen 指令的工作方式相反,而且是一个非常明确的参考,表明目前在 stream 中并未实现任何类型的 SNI 支持。


讨论

作为讨论点和解决方案建议,OpenVPN流量仅使用可窃听SNI的TLS并不一定正确(但我对OpenSSL或SNI不太熟悉):

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

  • 为了讨论,如果OpenVPN只是使用TLS连接,并且如果它没有使用TLS来使用用户证书对用户进行身份验证(这将使对流进行中间人攻击更加困难,但仍然可以始终携带认证数据),那么理论上,如果nginx在stream中的listen周围具有SNI支持,则您可能已经能够使用nginx主动进行中间人攻击(因为proxy_ssl已在stream_proxy中得到支持)。

最重要的是,我认为OpenVPN最好在其自己的基于UDP的协议上运行,这样,您可以在一个基于TCP的https实例和另一个基于UDP的OpenVPN实例中使用相同的IP地址和端口号而不冲突。
最终,你可能会问,那么流模块有什么用处呢?我认为它的目标受众是(0)基于客户端IP地址的哈希,使用多个上游服务器来负载均衡HTTP/2, 以及(1)更为直接和协议无关的stunnel替代品。

1
@TorstenBronger,我认为人们没有阅读问题的正文,这个问题特别涉及OpenVPN。OpenVPN本身是否支持SNI? - cnst
1
顺便说一句,@TorstenBronger,事实证明OpenSSL不支持SNI,所以这个问题是无意义的,因此另一个答案回答了一个不同的问题。来源:http://stackoverflow.com/questions/42078600/if-applicable-does-openvpn-over-tcp-support-sni/42096540#42096540 - cnst
2
此答案已经过时,请查看其他答案。 - Richard Kiefer
1
@KhakharShyam 抱歉,我在那个领域不是很熟练。建议您发布一个关于您具体问题的问题。 - Richard Kiefer
nginx的开发人员在一篇博客文章中对流/ TCP进行了很好的介绍。好的阅读材料。 - Alex
显示剩余3条评论

10

正如@Lochnair所提到的,您可以使用ngx_stream_map模块$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

7
这个例子是通过IP地址进行选择的。这不是问题的核心,因为需要通过SNI名称进行选择。如果每个HTTPS域名都有一个单独的IPv4地址,当然一切都会更容易。 - vog

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接