理解 ngrok 的工作流程

以下面的图为例,在公司、家庭的两台电脑上分别有 80 和 9999 两个端口运行了某个服务,我们想让它们能分别通过测试域名test.ngrok.mydomain.comtest2.ngrok.mydomain.com被访问到。这就需要ngrok服务来做“请求转发”了。

  1. 首先有一台运行ngrok 服务端的服务器(Ubuntu,go环境),这是完成整个转发过程的核心
  2. 在这台运行ngrok服务端的服务器上绑定一个域名 ngrok.mydomain.com
  3. 再将两个测试域名 test.ngrok.mydomain.comtest2.ngrok.mydomain.com以 CNAME 的方式添加到 DNS 记录里面,把请求者两个测试域名的数据全部转发ngrok服务器端运行的服务器ngrok.mydomaincom上面
  4. 在公司、家庭两台电脑上运行ngrok客户端,分别把localhost:80localhost:9999绑定不同的子域名testtest2,等待客户端与服务端通信正常后ngrok服务端就知道可以把来自哪个子域名的数据分别转发给对应的客户端。
  5. 客户端接收到数据后,再次把数据传输给本机上面的 localhost:80localhost:9999

添加域名解析

假设准备提供ngrok服务的域名,以及两个测试域名的根域名为 mydomain.com

运行 ngrok 服务端的服务器 IP 地址为 1.2.3.4

  • 添加一条A记录,主机 ngrok, 记录值 1.2.3.4

添加测试域名

  • 添加 CNAME 记录, 主机test 记录值 test.ngrok.mydomain.com
  • 添加 CNAME记录 主机 test2 记录值 test2.ngrok.mydomain.com
  • 还可以添加更多……

安装 ngrok 服务端

通过 ssh 登录服务器,这里以 Ubuntu 为例。

安装必要的工具和语言环境

1
sudo apt-get install build-essential golang mercurial git

Ubuntu 升级默认的 go 语言版本

默认的 1.2.1 版本的 go 语言环境可能会在后面安装 ngrok 的时候报错:

unknown tls.Config field ‘GetCertificate’ in struct literal

因此需要升级 go 语言环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 看看是不是小于等于 1.2.1
go version
# 卸载
sudo apt-get purge golang*
#下载最新版并解压 https://golang.org/dl/
wget https://storage.googleapis.com/golang/go1.7.1.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.7.1.linux-amd64.tar.gz
#创建目录
mkdir ~/.go
# 设置环境变量
vi ~/.profile
export GOROOT=/usr/local/go
export GOPATH=~/.go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
source .profile
# 升级
sudo update-alternatives --install "/usr/bin/go" "go" "/usr/local/go/bin/go" 0
sudo update-alternatives --set go /usr/local/go/bin/go
go version

下载 ngrok 源码并编译服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
git clone https://github.com/tutumcloud/ngrok.git ngrok
cd ngrok
#生成并替换源码里默认的证书,注意域名要修改为你自己的,这里是一个虚拟的测试域名
NGROK_DOMAIN="ngrok.mydomain.com"
openssl genrsa -out base.key 2048
openssl req -new -x509 -nodes -key base.key -days 10000 -subj "/CN=$NGROK_DOMAIN" -out base.pem
openssl genrsa -out server.key 2048
openssl req -new -key server.key -subj "/CN=$NGROK_DOMAIN" -out server.csr
openssl x509 -req -in server.csr -CA base.pem -CAkey base.key -CAcreateserial -days 10000 -out server.crt
cp base.pem assets/client/tls/ngrokroot.crt
#开始编译,服务端客户端会基于证书来加密通讯,保证了安全性
sudo make release-server release-client
# 如果一切正常,ngrok/bin 目录下应该有 ngrok、ngrokd 两个可执行文件,ngrokd 是服务端文件,ngrok 是 Linux 的客户端
ls -al ./bin

生成不同的客户端(windows、macOS 等)

完成安装后,默认只会生成 Linux 版本的客户端,而我们如果需要 Mac、Windows 的客户端,就要手动生成。

1
2
3
4
5
cd ngrok
GOOS=linux GOARCH=amd64 make release-server release-client
GOOS=windows GOARCH=amd64 make release-server release-client
GOOS=darwin GOARCH=amd64 make release-server release-client
GOOS=linux GOARCH=arm make release-server release-client

生成的客户端文件放在 bin目录下,按操作系统代号分目录存储,例如ngrok/bin/darwin_amd64/ngrok是运行在 macOS 上面的。稍后我们会将它们复制到不同的操作系统下来启动他们。

启动服务端

在服务器上运行下面的命令启动ngrok服务端

1
./bin/ngrokd -tlsKey=server.key -tlsCrt=server.crt -domain="ngrok.mydomain.com" -httpAddr=":8081" -httpsAddr=":8082"

注意,这里httpAddrhttpsAddr是 ngrok服务 转发 http 和 https 请求的端口,为了避免和 Nginx/Apache 等的 80 端口冲突,使用了80818082(后面会有步骤进行 80 端口转发,这里先不表)。

默认还会启动一个4443端口,用于跟活动的客户端进行通讯,如果需要更换端口,使用 -tunnelAddr=":xxx"参数

现在你可以在浏览器里访问 http://ngrok.mydomain.com:8081了,如果有一行提示,表示 ngrok服务端已经运行起来了

Tunnel ngrok.yourdomain.com:8081 not found

然后再访问http://test.ngrok.mydomain.com:8081,如果有下面的提示,表示 CNAME 里面设置的域名别名也已经成功了。

Tunnel test.ngrok.mydomain.com:8081 not found

客户端

配置连接参数

根据你的操作系统不同,将上面生成的不同客户端下载到本地。例如我的本地机器是 macOS,把服务器上 /root/ngrok/bin/darwin_amd64/ngrok下载后另存为 ~/Dowonload/ngrok

给这个ngork客户端文件添加执行权限,然后创建一个配置文件,指定客户端等下要连接ngrok服务端参数

1
2
3
4
5
6
7
8
cd ~/Download
chmod +x ngrok
vi ngrok.cfg
# 填写如下信息,server_addr 指定了服务端的域名和与客户端通信的端口
server_addr: ngrok.mydomain.com:4443
trust_host_root_certs: false

运行客户端

然后运行下面的命令启动客户端。-subdomain参数指定一个子域名(这里为 test),最后一位数字是你本地要暴露的端口-proto是协议,这里是http(ngrok 还支持 tcp协议,原理和方法类似)。

1
2
3
4
5
6
7
8
9
10
11
./ngrok -subdomain test -proto=http -config=ngrok.cfg 80
# 如果连接正常,会有提示
ngrok (Ctrl+C to quit)
Tunnel Status online
Version 1.7/1.7
Forwarding http://test.ngrok.yourdomain.com:8081 -> 127.0.0.1:80
Web Interface 127.0.0.1:4040
# Conn 5
Avg Conn Time 192.70ms

上面的命令执行顺序

  1. 调用 ngrok.cfg 配置文件里的参数来启动客户端,这个配置文件告诉客户端连接哪个服务端(参数中有服务端的域名 ngrok.yourname.com 和连接端口4443
  2. 把本机localhost:80绑定test.ngrok.yourdomain域名,这时,ngrok的服务端就和这个客户端联系起来了(通过这个域名作为标识
  3. 现在可以通过在外网访问 test.ngrok.yourdomain:8081 ,数据被ngrok服务端接到后,发现来源数据含有客户端的域名,于是马上转发给符合匹配的客户端
  4. 客户端收到数据后,再把数据转发给绑定的 localhost:80

现在你可以在浏览器打开 http://test.ngrok.yourdomain.com:8081,显示的效果和你打开 localhost:80 是一模一样的。说明ngrok服务端的转发已经生效,你已经可以在外网访问局域网内部本机上的服务了。

数据监控面板

通过打开 127.0.0.1:4040这个监控面板里看到转发过来的 request/response 等数据。

隐藏 URL 里的 8081 端口,直接使用 80 端口

因为我们运行 ngrok 服务端的时候,为了避免和 nginx/apache 的 80 端口冲突,指定了 ngrok 转发 http 请求的端口为 8081,那么 ngrok 的客户端也必须使用 8081 端口才能接收数据。每次测试的时候,还要在 URL 后面加上:8081,这太繁琐了,那么如何使用 80 端口来访问呢?

通过使用 web 服务的反向代理可以实现,以 nginx 为例。

为服务端使用的域名的 8081 端口添加反向代理

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name ngrok.mydomain.com;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host:8081;
proxy_set_header X-Nginx-Proxy true;
proxy_set_header Connection "";
proxy_pass http://127.0.0.1:8081;
}
}

现在可以直接在浏览器访问 test.ngrok.mydomain.com,而不需要加 :8081 了。