使用SSH反向隧道进行内网穿透

前言

最近在摸索持续构建,正如之前的文章《DKIpaBuilder shell实现自动打包ipa并上传到fir.im》所言,“现在我们需要一个 mac 服务器,但是租赁的价格非常昂贵。网上有人用 mac mini 来当 macOS 服务器,但是并不在外网环境,Coding、GitHub 等平台的 webHook 接收不到。”

于是有个想法,利用 SSH 反向隧道穿透 NAT,连接内网的 mac mini 服务器。

目标

现在有位于公网的 VPS 一台,位于 NAT 之后的 Mac Mini 一台,本机 Macbook Pro 一台,分别取代号 A、B、C。

机器代号 机器位置 地址 账户 ssh/sshd 端口 是否需要运行sshd
A 位于公网 bingo.ren root 22
B 位于NAT之后 localhost dankal 22
C 位于NAT之后 localhost bingo 22

A 上的 WebServer 为 Nginx,通过 location 配置规则,把 webhook 的请求反向代理到了 Nodejs 监听的端口 (eg. 7777)

目标是实现自动化部署需求:当代码提交并 Push 到 GitHub 或者 Coding 等平台时,平台往位于公网的 A 发送一个 Http Request 传递 Push Event,A 上的 nodejs 触发 shell 脚本,通过 ssh 通道连接并执行 B 上的 Shell 脚本,B 上的 Shell 脚本执行 fastlane 命令实现持续构建,自动实现单元测试、屏幕截图、打包、发布等操作。

所以,搞这么多,最基本的就是要穿透内网,这就是现在首先要做的事情。

稳定性维持

不幸的是 SSH 连接是会超时关闭的,如果连接关闭,隧道无法维持,那么 A 就无法利用反向隧道穿透 B 所在的 NAT 了,所以我们需要一种方案来提供一条稳定的 SSH 反向隧道。

一个最简单的方法就是 autossh,这个软件会在超时之后自动重新建立 SSH 隧道,这样就解决了隧道的稳定性问题。

安装 autossh

1
$ brew install autossh

配置公网 VPS(A)

修改配置文件

首先在 A 上编辑 sshd 的配置文件 sshd_config,将 GatewayPorts 开关打开

1
2
3
A $ vim /etc/ssh/sshd_config
GatewayPorts yes
  • 在 CentOS 下默认是#GatewayPorts no,把注释打开,然后把 no 改为 yes;

  • 在 Ubuntu 下默认没有 GatewayPorts,直接补一行 GatewayPorts yes 即可

重启 sshd 服务

1
A $ service sshd restart

防火墙开放端口

以 6766 和 6777 为例,可自由替换。

1
2
B $ sudo iptables -I INPUT -p tcp --dport 6766 -j ACCEPT
B $ sudo iptables -I INPUT -p tcp --dport 6777 -j ACCEPT

配置内网 Mac Mini (B)

新建用户

在 B 上新建一个用户 dankal,由于 macOS 是基于 Unix 系统,暂时还没有找到新建用户的命令,Linux 下可以用下面的命令新建用户。

1
2
B $ sudo useradd -m dankal
B $ sudo passwd dankal

而在 mini 上,暂时基于 GUI 操作,系统偏好设置 -> 用户与群组 -> 点按锁按钮以进行更改 -> 添加用户账户。

配置 SSH 密钥

创建密钥

1
2
B $ su dankal
B $ ssh-keygen -t 'rsa' -C 'dankal@mini'

注意该密钥不要设置密码,也就是运行 ssh-keygen 命令时尽管一路回车,不要输入额外的字符。

上传到 VPS(A)

1
B $ ssh-copy-id root@bingo.ren

以往我都是 scp 上传公钥到服务器上,然后再 cat >> 写入 authorized_keys 文件。

这里用 ssh-copy-id 可以不传公钥,一步实现上面的操作。

反向隧道

由 B 向 A 主动建立一个 SSH 隧道,将 A 的 6766 端口转发到 B 的 22 端口上,只要这条隧道不关闭,这个转发就是有效的。

通过上面的步骤,安装完的 autossh 默认在 /usr/local/Celler/autossh 目录下,而 autossh.sh 脚本的地址为 /usr/local/Celler/autossh/1.4e/bin/autossh.sh。

1
2
B $ cd /usr/local/Celler/autossh/1.4e/bin/
B $ ./autossh -p 22 -M 6777 -NR '*:6766:localhost:22' root@bingo.ren

-M 参数指定的端口用来监听隧道的状态,与端口转发无关。

有了这个端口转发,只需要访问 A 的 6766 端口反向连接 B 即可。

连接

现在可以在 A 上使用这条反向隧道穿透 B 所在的 NAT,SSH 连接到 B:

1
A $ ssh -p 6766 dankal@localhost

或者是在 C 上直接穿透两层 NAT,SSH 连接到 B:

1
C $ ssh -p 6766 dankal@bingo.ren

TODO

  • 如果 B 重启隧道就会消失。那么需要有一种手段在 B 每次启动时使用 autossh 来建立 SSH 隧道。在 linux 下可以使用 systemd 做成服务来解决,在 macOS 下暂时还没有找到解决方案。

  • 动态端口转发,在 VPS(A)安装 ShadowsocksX,SOCKS v5 代理实现访问 Mini(B)上的网页。

后话

实现内网穿透并不难,但对于 Linux/Unix 操作系统的使用的熟练度有一定的要求,我在 ssh、sudoers、iptables 等上面花了不少时间,也踩了挺多的坑。

  • 本文作者: Bingo
  • 本文链接: https://blog.bingo.ren/32.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!