http2tcp: 一个通过 HTTP 转发 TCP 流量的小工具

陪她去流浪 桃子 2022年06月03日 编辑 微信上查看 阅读次数:4841

我所在的小组做了一个平台级产品,部署在集团下各公司内,一共几十个站点,站点之间完全独立。从之前的物理机部署迁移到了 K8s 容器环境。 为了解决每次排查问题需要好几个烦人的地方:

  • 找对应环境的机器 IP
  • 申请机器的权限
  • K8s 的 kubeconfig 文件
  • 安装常用工具:kubectl、kafka、网络工具等

这一套流程走下来,很是费时费力。 后来我想到了一个我认为非常优雅的解决方案。

解决方案原型

在每个站点部署这个产品的时候,我在对应的 K8s 集群内创建了一个专门用于 debug 的容器。 这个容器里面,有各种各样的常用工具,有此站点的 kubeconfig 等。 那我怎么快速进这个容器呢?我在这个 debug 容器内跑了一个 sshd 服务,所以,如果能够通过 ssh 登录到这个容器环境内,所有的工具都一应俱全了,并且还在同样的网络内。 我以前写了一篇文章《在容器内运行 sshd 服务》介绍了如何在容器内启动 sshd 服务,可以参考。

所以,现在的问题变成了:如何快速登录到各个站点的 debug 容器内的 sshd 服务? 很显然,各个站点最容易区分的标识应该就是站点的域名了。所以,通过域名登录 sshd 是我需要解决的问题。 因为前端的 API 全部跑在容器环境内,所以我只需要将我需要的流量转发到我的 debug 容器内即可。 我在 nginx 内添加了一条简单的规则来做到:

1
2
3
4
5
6
7
8
location /~debug/ {
	proxy_http_version 1.1;
	proxy_set_header Host $host;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "Upgrade";
	proxy_read_timeout 600s;
	proxy_pass http://debug;
}

现在如果我在我本地访问某个站点,比如:http://example.com/~debug/,就能把流量转发到我的 debug 容器中。 通过主域名转发流量有一个特别明显的好处:不用再额外开端口了,体验非常丝滑。

但是,这还不够。因为这是一条 HTTP 的连接,如何把它“降级”为 TCP 连接?即:从 OSI 协议的应用层协议转换为传输层协议。

以前写过的相关文章或项目参考:

因为 NGINX 不支持 HTTP 的 CONNECT 动词,所以这条路走不通。那就按第二个参考的方案:用 HTTP 的 Upgrade 头部升级协议。 协议非常简单:如果你往 HTTP 的请求头部添加以下两个头部:

1
2
Connection: upgrade
Upgrade: example/1, foo/2

如果服务端支持协议升级,那它就会返回类似:

1
2
3
HTTP/1.1 101 Switching Protocols
Upgrade: foo/2
Connection: Upgrade

HTTP 的 101 状态码可能是非常多人都不知道的状态码,但是它真的实用。

在服务器返回上述响应后,不同于以往的 HTTP 流程——一个往返就结束,此时这条 HTTP 连接会变成 TCP 连接,也就是这条连接会变成双向的全双工连接。 此时就可以在条连接上进行任何传输层协议的进行。如果此时 HTTP 客户端把连接交给 ssh,HTTP 服务器端把连接交给 sshd。 那 ssh 和 sshd 就可以进行通信了。你可能听说过 WebSocket,它也是走的这条路。

上述的所有工作,就是我今天要介绍的小工具的原型。

http2tcp

这是一个不到 300 行 Go 语言写的小工具。具体的使用文档可以见代码仓库:https://github.com/movsb/http2tcp

1
2
3
4
5
6
7
8
9
$ ./http2tcp -h
Usage of http2tcp:
  -s, --server               Run as server.
  -c, --client               Run as client.
  -l, --listen string        Listen address (client & server)
  -e, --endpoint string      Server endpoint.
  -d, --destination string   The destination address to connect to
  -t, --token string         The token used between client and server
  -h, --help                 Show this help

这里以我前面要解决的问题——通过站点域名代理 ssh 协议的使用方式来讲解如何使用此工具。

几个角色:

  • ssh 用来登录的 ssh 客户端
  • http2tcp 将 HTTP 转换成 TCP 协议的小工具
  • nginx 站点域名反向代理网关
  • sshd sshd 服务端

以下是使用步骤:

  1. 首先确保 sshd 在容器内(或你自己的服务器上)运行:比如监听 22 号端口。

  2. 在容器或服务器上运行 http2tcp(服务端角色)

    1
    
    $ ./http2tcp -s -t $TOKEN -l $SERVER_IP_OR_DOMAIN:12345
    

    这条命令运行在服务器上,或者容器内。监听地址和端口任意,只要 nginx 可达即可。

  3. 在 nginx 上配置将指定的路径路由到 http2tcp:

    1
    2
    3
    4
    5
    6
    7
    8
    
    location /~http2tcp/ {
    	proxy_http_version 1.1;
    	proxy_set_header Host $host;
    	proxy_set_header Upgrade $http_upgrade;
    	proxy_set_header Connection "Upgrade";
    	proxy_read_timeout 600s;
    	proxy_pass http://http2tcp:12345;
    }
    
  4. 配置 ~/.ssh/config,以使用 ssh 代理:

    Host example.com
    	ProxyCommand http2tcp -c -d localhost:22 -e https://example.com/~http2tcp/ -t $TOKEN
    

    上述 localhost:22 是 http2tcp 客户端告诉 http2tcp 服务端我要连接 localhost:22,即服务端的 sshd 服务端口。 https://example.com/~http2tcp/ 即是站点域名和转发路径。$TOKEN 是 http2tcp 服务端和客户端事先商量好的密码,不是必须的。

  5. 本地登录:

    1
    
    $ ssh example.com
    

完成!

附录

几点说明:

  • 为了安全,只建议在 HTTPS 站点下使用此能力;

一些可能的不完善:

  • http2tcp 服务器没有限制可以访问的地址,所以你要好好保存与服务器共享的 $TOKEN。

如果你有你自己的虚拟主机,可以把 ssh 端口走 HTTPS 的 443 端口代理出来(但是默认的 22 不要关,以防 web 服务器挂掉后你就彻底上不去服务器了), 这样有一个好处:做流量监控的人或设备,只能监听到标准端口(443)上的流量,然后一般来说这是一个正常的网站,所以很难察觉异常,于是放行?

如果 SSH 走 HTTPS 了,你也就可以在本地放心地 ssh -D 建立 SOCKS5 代理了,只会有一条 HTTPS 连接。是不是藏匿得比较深?

小组的专用版本

http2tcp 是通用的 http 转 tcp 工具。我在小组内定制的版本只需要指定一个域名即可,鉴权使用 ssh 自带的公钥鉴权,非常安全。所以少好几个参数,简直小而美。

标签:ssh · 代理 · HTTP · K8s