2019年上半年google推出的golang1.12为我们带来了很多新特性,具体请见官方 Go 1.12 Release Notes
其中有一项:
net/http/httputil
The ReverseProxy now automatically proxies WebSocket requests.
从该版本开始,httputil库中的 ReverseProxy能够自动转发 websocket请求,参见 reverseproxy.go源码。
从此,无论 http(s) 或者 ws(s) 都能够通过 httputil 官方库实现,而在这之前,我们需要自己实现,或者借用一些第三方库实现,例如:https://github.com/gorilla/websocket 、https://github.com/yhat/wsutil。
本文将从产品实际需求出发,对该功能予以介绍。
公司研发的产品是一个集群产品,在集群的管理节点(头节点)中部署有我们开发的WEB服务,集群中的其它节点中部署有类VNC的远程桌面服务,用户想访问这些节点中的类VNC服务时都要通过管理节点访问。此服务使用的协议是 https 和 websocket+ssl 即 wss。
管理节点中的转发服务需要支持如下功能:
简单的示意图
在正式使用golang开发前(此时还不知道go1.12有这样的功能),我们先用 nginx的转发功能做了概念验证。
nginx 和 类VNC服务部署在同一服务器中,该服务器IP地址为192.168.1.100, 类VNC服务监听在 0.0.0.0:8443.
nginx 转发验证地址为: https://192.168.1.100:7443/demo/
nginx 版本号
root@node01 nginx]# nginx -v
nginx version: nginx/1.12.2
nginx.conf配置文件
http {
...
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
...
server {
listen 7443 ssl;
ssl_certificate /etc/ssl/certs/c.com.crt;
ssl_certificate_key /etc/ssl/certs/c.comss.key;
location /demo/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass https://127.0.0.1:8443/;
}
...
}
}
经访问 https://192.168.1.100:7443/demo/ 测试,该服务在经过nginx转发并且url中带有subpath(/demo/)时,工作一切正常,这证实了我们需求中的前两点在技术上没有问题。
接下来,我们把 https://192.168.1.100:7443/demo/ 嵌入到了 iframe中测试,结果失败。使用chrome调试工具,发现该服务做了简单的防嵌处理,响应头中加入了 X-Frame-Options:Deny。防嵌原理请参见:X-Frame-Options 响应头配置避免点击劫持攻击。
俗话说,“道高一尺,魔高一丈”,很简单,我们在nginx代理中把这个头清除即可!
location /demo/ {
...
proxy_hide_header X-Frame-Options;
...
}
重启nginx后测试,一切正常。
如何实现动态转发?即访问 https://192.168.1.100:7443/vnc/node1/ 时,代理 node1的的远程桌面服务; 访问 https://192.168.1.100:7443/vnc/node2/ 时,代理 node2的的远程桌面服务 … ?
其实不用golang实现,我们也可以使用 nginx / openresty + lua 来实现。网上相关资料很多,可以参见:Dynamic nginx upstreams with Lua and Redis
如果使用nginx/openresty + lua的方案,我们的产品部署时就要绑定nginx/openresty + lua, 这无疑与我们产品设计之初的理念 – 依赖尽量少、部署简单 相违背, 而且放着golang这么强大的功能不用着实可惜。
我们使用了golang echo框架,请参见:echo路由 (吐槽下这个框架,截止目前为止,echo 虽然提供了 reverse proxy功能,但功能很弱,连https都不支持,更别说wss),
路由设置
import "demo.com/httpd/handler/proxy"
...
e := echo.New()
proxy := proxy.New()
e.Any("/vncng/:Host/*", proxy.Handle)
...
func (h *Handler) Handle(c echo.Context) error {
host := c.Param("Host") // 由path中取到要转发的节点地址
var tlsConfig = &tls.Config{
InsecureSkipVerify: true, // 忽略证书验证
}
url := "https://" + host + ":8443" // 服务端口固定为8443端口
target, err := url.Parse(url)
if err != nil {
log.Errorf("url.Parse failed: url=%s error=%v", url, err)
return nil
}
var transport http.RoundTripper = &http.Transport{
Proxy: nil, // 不使用代理,如果想使用系统代理,请使用 http.ProxyFromEnvironment
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: tlsConfig,
DisableCompression: true,
}
proxy := httputil.NewSingleHostReverseProxy(target)
proxy.Transport = transport
proxy.ErrorLog = logger
proxy.ModifyResponse = func(r *http.Response) error {
r.Header.Del("X-Frame-Options") // 重点:代理时移除 X-Frame-Options 头
return nil
}
r := c.Request()
r.URL.Path = "/" + strings.TrimPrefix(r.URL.Path, "/vncng/" + host + "/") // 重点:转发给后端时,要把请求的 Path 转换成 正确的值,例如请求时 path为 /vncng/192.168.1.100/#auth, 发送给后端应该是 /#auth
proxy.ServeHTTP(c.Response(), r)
return nil
}
其实 ReverseProxy 的功能不仅仅如此,我们还可以修改请求/返回内容,也可以做带有权重的负载均衡服务。不过这些是早已经实现的功能,因项目中也未用到,故未有提及。