问题背景
因为网络环境受限,应用服务器无法直接访问外网,需要前置机上中转一下,这种情况可在应用服务器修改/etc/hosts
文件指向前置机,在前置机上的nginx设置四层代理,从而出站。
方案
根据How to Use NGINX as an HTTPS Forward Proxy Server,nginx支持正向代理https,有L7(应用层,使用http connect)和L4(传输层)两种方式,L7和L4对客户端是有感知的,L7代理需要显式设置客户端的代理,L4要修改DNS解析(linux上/etc/hosts文件),
7层代理
7层代理即应用层代理,客户端使用http的connect方法与代理创建tunnel,代理与目标地址建立tcp连接,如此以来,代理与客户端、目标端分别建立了tcp链接,代理无脑转发TCP Stream,在此基础上客户端与目标端完成SSL握手。
4层代理
ngx_stream_core_module模块是实现L4代理的关键,使用nginx -V
查看加载的模块,有以下几个参数,即表示支持4层代理。4层代理即运输层代理,https是运输层之上,所以对于运输层是透明的。
- –with-stream
- –with-stream_ssl_module
- –with-stream_ssl_preread_module
经过考虑,选择了4层代理实现方式,这样只需要修改调用端所在机器的/etc/hosts
,而代码不需要改动,对调用端而言相对透明。以下是实现4层代理的两种方式,本质而言是一样的,语法上有点差异。
解决方案一
博客中的写法
stream {
# 注意,如果访问的是域名,则必须配置resolver;如果访问的是ip,则可以不用配resolver;
resolver 114.114.114.114;
server {
listen 443;
# 从SSL握手中的clientHello包中读出访问的域名
ssl_preread on;
proxy_connect_timeout 5s;
proxy_pass $ssl_preread_server_name:$server_port;
}
}
linux上可通过nslookup some.domain.com
查看DNS;不配置resolver,访问域名时SSL握手失败,在error.log(nginx -V
能看到--error-log-path
选项)会有体现,
nginx在启动时会对域名解析一次,启动后不再解析,所以proxy_pass 中有变量时,必须配置resolver,以达到每次请求都可解析(nginx缓存了DNS解析结果,并不是每次请求都会实际DNS解析),可参考nginx中resolver参数配置解释、Why does a variable not work in NGINX proxy_pass
?;resolver指令还有valid选项用来控制DNS缓存时间;比如
resolver 127.0.0.1 [::1]:5353 valid=30s;
注意:stream块与http块是平级的,切勿嵌套;
解决方案二
如果只访问一个域名,可以写死;
http{
//something else
}
stream {
server {
listen 443;
ssl_preread on;
proxy_pass www.baiducom:443;
}
}
测试SSL时,可使用openssl s_client
,详情可参考openssl s_client工具详解
openssl s_client -connect www.baidu.com:443 -debug
或者
openssl s_client -connect www.baidu.com:443 -msg
或者curl https://some.domain.com/some/path -v
上述解决方案只访问一个域名可以写死,如果访问多个域名该怎么办?老海给出了答案,也可参考官方文档Module ngx_stream_ssl_preread_module样例
stream {
map $ssl_preread_server_name $target {
a.com a-network;
b.com b-network;
}
upstream a-network {
server a.com:443;
}
upstream b-network {
server b.com:443;
}
server {
listen 443;
ssl_preread on;
proxy_pass $target;
}
客户端访问a.com,经过map块映射,$target等于a.com:443,从而被server块中的proxy_pass代理到a.com:443。
总结
方案一和方案二都能满足需求,方案一相对简练,但要注意配置resolver;每次新增访问的域名,方案二都需要相应添加配置;
思考
经同事提醒,同一个nginx不能同时正向和反向代理443端口;想想为什么?