Go1.12, 轻松实现 HTTPS & WSS 动态反向代理

Go1.12, 轻松实现动态 HTTPS & WSS 反向代理

    • 前言
    • 需求提出
    • 使用nginx做POC
    • 动态反向代理的选型
    • golang来了,舍我其谁?
    • 动态转发的路由设置
    • 转发核心代码(proxy.go)
    • ReverseProxy的其它功能

前言

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。
管理节点中的转发服务需要支持如下功能:

  1. 因为后端是多个节点,所以管理节点要能够根据参数动态转发请求到指定的后端节点
  2. 要同时运行 https 转发和 wss转发
  3. 要支持把该远程桌面服务以iframe的方式嵌入到管理节点的WEB页面中

简单的示意图

用户 192.168.1.100:7443 172.16.1.100:8443 172.16.1.101:8443 https://192.168.1.100:7443/vncng/172.16.1.100/ https://172.16.1.100:8443 opt [ proxy for 172.16.1.100 ] https://192.168.1.100:7443/vncng/172.16.1.101/ https://172.16.1.101:8443 opt [ proxy for 172.16.1.101 ] 用户 192.168.1.100:7443 172.16.1.100:8443 172.16.1.101:8443

使用nginx做POC

在正式使用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

golang来了,舍我其谁?

如果使用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)
...

转发核心代码(proxy.go)

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的其它功能

其实 ReverseProxy 的功能不仅仅如此,我们还可以修改请求/返回内容,也可以做带有权重的负载均衡服务。不过这些是早已经实现的功能,因项目中也未用到,故未有提及。

你可能感兴趣的:(GOLANG)