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

陪她去流浪 桃子 微信上查看 阅读次数:36653

我所在的小组做了一个平台级产品,部署在集团下各公司内,一共几十个站点,站点之间完全独立。从之前的物理机部署迁移到了 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

文章评论 41 发表评论 登出
  1. jiaoxiake

    学习了,弄到最后发现托管服务器sshd端口转发是关闭的.

    1. 桃子

      哈哈哈,测试之前应该保证原始的端口转发是可用的呀!对照组实验。

    2. 二狗 https://www.smileat.me

      非常好,完美解决问题

      1. 桃子

        竟然敢用 WebSocket 分支😄,Issues 里面有人说不稳定,但是我实测大半年高强度使用下来很稳定,也就没有修复。

        对于在国内使用,main 分支应该是足够用的。WebSocket 的目的主要是:

        • 去 TLS 的特征化(是否是正规浏览器到服务器的流量);
        • 弱网不稳定的情况下提供稳定的 TCP 连接。

        (你网站字体看着很舒服啊。)

        1. 二狗 https://www.smileat.me

          谢谢大佬夸赞🥰,字体用了 霞鹜文楷,也是从别的 blog 看到偷过来用的。

          用 WebSocket 分支的原因是我的场景是需要走大量数据的 SQL 查询,所以寻求更稳定的 TCP 长连接。因为是调试环境而非生产环境,所以果断尝试,暂时没发现什么问题~~(不过 dalao 出品能有什么问题呢)~~。

          1. 桃子

            谬赞谬赞,能用就好!

            1. 桃子

              字体,用上了用上了!先尝试一下,但是初看有点奇怪,还是觉得你的更好看,也不知道是哪里不对🥹

              1. 桃子

                另外:

                https://www.smileat.me 的证书已于 2024/3/8 过期。

                怎能在妇女节这天说不行?🥵

                1. 二狗

                  因为改用 qwq.me 了,所以就没再弄,你这么说我赶紧回去看了一下,发现是 ACME 的 FileSystem 验证路径出了问题,现在应该好了。

                  我刚想上来看看你的跟随系统的夜间模式主题是怎么实现的,就发现访问速度慢了不少。没载出来的时候 F12 看网络面板,正好是霞鹜文楷的 css 加载过慢。还在想你是不是换了字体哈哈哈。我没挂梯子。也许你应该换个 CDN?

                  我用的是 elemecdn。自测挂不挂梯子速度都还可以。

                  个人感觉霞鹜文楷,以及其他 Serif 字体,蛮挑字体大小的,你的网页,我浏览器上缩放到 90% 以后,感官好了不少。

                  一个不成熟的想法,sans-serif 字体相比于 serif,更适合拿来做技术类博客,成堆的文字,前者看起来更不易疲劳。

                  西文建议还是 sans-serif,我自己用的是 Varela Round。

                  代码还是用 mono,我看你现在代码也被霞鹜文楷覆盖了。

                  1. 桃子

                    发现是 ACME 的 FileSystem 验证路径出了问题

                    看了下,确实已经好了。

                    ...就发现访问速度慢了不少...

                    是的呀,我也注意到了,尝试切换之前我就有这个顾虑,我看到仓库大小有 60+ MB 时就有点儿慌了。 虽然作者说已经按 Unicode 范围拆分字体文件,但是实际体验下来还是差点儿意思。

                    也许你应该换个 CDN?

                    确实不应该用 GitHub,毕竟 GitHub 在国内早就不能顺利访问了。那我待会儿有空换个源试试。

                    我浏览器上缩放到 90% 以后,感官好了不少

                    换之前是 15px,换之后发现由于是衬线字体,相比于非衬线字体来说,有些笔画还是略显苗条。所以我改成了 18px。确实稍微大了点,现在准备改成 16px。

                    一个不成熟的想法,sans-serif 字体相比于 serif,更适合拿来做技术类博客,成堆的文字,前者看起来更不易疲劳。

                    霞鹜文楷有非衬线的版本吗?没有吧?那不然岂不是不能用这个字体了。😅

                    代码还是用 mono,我看你现在代码也被霞鹜文楷覆盖了。

                    再看了一眼,觉得这个字体用作代码的时候有点手写体的感觉,竟然还蛮好看的😋,先暂时用用看。

                  2. 二狗
                    1. 二狗

                      我刚想上来看看你的跟随系统的夜间模式主题是怎么实现的

                      找到了,是 @media(prefers-color-scheme: dark)🤤

                      1. 二狗

                        霞鹜文楷没有非衬线,我的意思是,干脆就不用霞鹜文楷了,原来的字体其实蛮好看的,蛮适合你博客。 并且我还因此考虑要不要也把我的博客也改成非衬线字体,结果尝试了一下,效果并不好,就算了。

                        1. 桃子

                          啊!那岂不是白搞了,哈哈哈哈…… 先保留几天吧!体验下新鲜感!确实蛮好看的。你的好看,别换,哈哈哈哈……

                        2. 二狗

                          电脑上看了看,jsdeliver 快多啦,字体大小也刚刚好。😍

                          1. 桃子

                            受不了,你这么一说,我再仔细审视了一番,简直赏心悦目 ❤️❤️❤️ (又熬夜了)

                          2. 二狗

                            这两天我也总是上来看看,想夸两句,又怕在这里回复太多影响观感。好了,现在我忍不住了,确实好看🤩🤩

                            1. 桃子

                              问题不大,我不介意。哈哈哈哈……

                              不过,我的博客评论是可以任意跨文章转移到其它文章的评论下面去的,如果哪天有空专门写篇记录博客日志(关于字体的),我就转移过去。

                              1. 桃子

                                另外,有鉴于最近一年的旅游超多,最近竟然产生了记录生活的想法……🥹 尽管觉得有点儿不妥,建站十年了快,还几乎没有写过生活。

                                1. 🐶🐶

                                  为啥会不妥呢。记录生活是好事,过程中也许会对人生有更深刻的感悟。我赞同你这么做,实在觉得不妥的,写成私密日志就好啦。

                                  1. 桃子

                                    我有写日记📔的习惯,并且也写了很久了,在一个独立的仓库。但是写日记比较随意,也并不认真,马马虎虎记录一下。哈哈哈🤦‍♂️,心想不是给大家看的,所以就没啥注意的。没事,本身也就是个作为记录日常的载体。如果写成文章公开给大家看,那就要比较用心地写了。

                            2. 🐶🐶

                              那我就不客气了😋

                          3. Doghole

                            头像改了,字体颜色也改了~蓝色清爽很多

                            1. 桃子

                              哈哈哈,睹物思人罢了……🤡 人都没了,才想起换个情侣头像…… 这是病,得治。

                              1. 🐶🐶

                                有点伤了

                        3. Aber https://aber.sh

                          很酷的思路!

                          1. 桃子

                            谢谢~网站做得真漂亮🤩

                          2. test

                            服务端想把http2tcp做成服务,不知道为什么总是启动超时,还有这个去掉token(不含-t)后好像无法连接,提示未认证

                            1. 桃子

                              “启动超时”是什么意思?目前 master 分支的代码中 token 是必须的,你可以随意设置一个就行。

                              可以像下面这样测试:

                              服务端:

                              $ ./http2tcp -s -l localhost:8888 -t 123
                              

                              客户端:

                              $ ./http2tcp -c -e localhost:8888 -t 123 -d baidu.com:80
                              GET / HTTP/1.1
                              Host: baidu.com
                              <回车>
                              HTTP/1.1 200 OK
                              Date: Wed, 07 Dec 2022 13:25:57 GMT
                              Server: Apache
                              Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT
                              ETag: "51-47cf7e6ee8400"
                              Accept-Ranges: bytes
                              Content-Length: 81
                              Cache-Control: max-age=86400
                              Expires: Thu, 08 Dec 2022 13:25:57 GMT
                              Connection: Keep-Alive
                              Content-Type: text/html
                              
                              <html>
                              <meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
                              </html>
                              
                              1. test

                                我手动执行服务端是可以的,关键是我想做成开机启动的服务,在服务中一直staring。

                                Starting http to tcp proxy server
                                http2tcp.service: start operation timed out. Terminating.
                                http2tcp.service: Failed with result 'timeout'.
                                Failed to start http to tcp proxy server.
                                

                                这个应该是创建服务哪里有问题了,可能我对service不熟,找不到原因,一般来说我的其他服务都没有遇到过这种情况

                                1. 桃子

                                  可以发一下你的 service 文件,我这边帮你调试一下。

                                  1. test

                                    好的,谢谢

                                     1
                                     2
                                     3
                                     4
                                     5
                                     6
                                     7
                                     8
                                     9
                                    10
                                    11
                                    12
                                    13
                                    14
                                    
                                    [Unit]
                                    Description=http to tcp proxy server
                                    After=network.target nss-lookup.target
                                    
                                    [Service]
                                    Type=simple
                                    PIDFile=/run/http2tcp.pid
                                    ExecStart=/usr/bin/http2tcp -s -l 127.0.0.1:2222 -t test
                                    ExecStop=-/sbin/start-stop-daemon --quiet --stop --retry QUIT/5 --pidfile /run/http2tcp.pid
                                    TimeoutStopSec=5
                                    KillMode=mixed
                                    
                                    [Install]
                                    WantedBy=multi-user.target
                                    

                                    /usr/bin/http2tcp -s -l 127.0.0.1:2222 -t test >/dev/null 2>&1 & 也同样试过了

                                    1. 桃子

                                      有点奇怪,我找了几台电脑试了一下都没有问题。

                                      要不试试设置一下超时时间:TimeoutSec=0

                                      1. test

                                        可以了,我发现了问题的可能原因,/etc/systemd/system/http2tcp.service和/lib/systemd/system/http2tcp.service文件内容不一致造成的,谢谢了,我明天测试下还有没有断联的问题,我这两天试了下,不定时会自动断开,我不知道你用的时候遇到过这种问题没。

                                        1. 桃子

                                          断开问题我倒是没怎么遇到,可以看看是不是因为服务端的连接数太多,有没有报错之类。

                                          1. test

                                            断联的问题知道了,配置增加发送空包保持连接就可以解决了,应该是http长时间没有活动所以断开的。

                                        2. test

                                          还有关于github我有个小建议,增加个action,弄个自动发布的,我fork你的源码后弄了个自动发布,不过仓储我设为私有了,需要的话我可以公开,很简单

                                          1. 桃子

                                            这个我是一直想弄的,太懒了,一直拖。 周末我搞一搞,有问题我再请教你。

                                            1. test

                                              好,我也是瞎弄的

                                              1. 桃子

                                                弄好了,现在是每次 push 到 main 的时候自动覆盖发布到 latest Release。

                                                感觉用 Docker 的场景好像不大,暂时没有做 Docker 的推送。

                                                1. test

                                                  挺好的,我个人的还增加了windows版本

                              还没有用户发表过评论,我要发表评论
                              编辑评论