内网穿透工具-ngrok
2017年05月18日

概述

ngrok是一款用go语言开发的开源软件,它是一个反向代理,通过在公共的端点和本地运行的Web服务器之间建立一个安全的通道,下图简述了 ngrok 的原理

原理

[用户1,用户2,用户3] -> [公网主机] <-> [内网主机]

用户的需求是访问内网主机,ngrok的原理就是通过公网主机做跳转,以达到访问内网主机需求.

公网主机需要运行ngrok的服务端程序,通常为一个有固定公网IP的VPS,同时需要一个域名,及开放对应端口
内网主机需要运行ngrok的客户端程序,ngrok支持http/https/tcp代理,通常这台主机为nas/web服务器/或者其他相关服务

用户可以通过ngrok定义的子域名访问内网主机的服务.(隧道的定义服务是一对一的)
ngrok可以做TCP端口转发,诸如:ssh:22 / windows远程桌面:3389 / mysql:3306 这些服务,都可以通过ngrok进行转发

配置与部署

  • 服务端选择debian
  • 客户端支持多平台:苹果,mac windows linux 32/64/arm,android(qpython-ngrok);
    配置过程分为以下几步:
  1. 服务端环境配置
  2. git下载项目源码
  3. 生成证书
  4. 编译服务端程序
  5. 编译客户端程序
  6. 部署
  7. 调试

注意事项:

  1. 防火墙设置:服务器和客户端都要打开对应端口,否则不能链接会一直显示 connecting
  2. 证书一定要设置正确:证书会被编译到可执行文件中去,所以设置的时候需要正确设置域名地址
  3. 交叉编译的时候要注意平台是32位系统(386),64位系统(amd64)或者arm ,设置错了不能运行

服务端环境配置

首先要安装必须要的前置工具, 因为现在的阿里云自己的镜像带了RHEL的源,所以可以直接偷懒用yum命令安装所有的东西

apt-get install git gcc golang mercurial
# mercurial 不知道做什么用的

这里要注意,golang的版本要1.8的测试可以正常编译,1.3.3的编译通不过;参考链接

因为以后还要保持运行,所以我加上了supervisor的设置。这样可以保持服务运行,重启电脑之后之类的都可以保持正常运行;supervisor需要依赖python,所以在安装的时候会要求安装python等相关的依赖.

apt-get install supervisor

git 下载项目源码

git clone https://github.com/inconshreveable/ngrok.git

生成证书

在使用官方服务的时候,我们使用的是官方的SSL证书,所以如果直接编译的话,默认的链接地址会到官方的ngrok.com去,所以我们需要自己生成证书。 证书非常关键,所有编译的文件都会携带证书文件在程序内部 所以证书生成的时候要保证所有的地方都是对应的。

首先我们要确定我们要使用的地址,这是受到域名解析服务的影响的。最简单的办法就是ping一下这个地址看是不是能够获得自己的服务器IP.这个服务一定是架设在当前的服务器(vps)上的,所以dns一定是ping了之后指向当前的服务器.
假设我使用的域名是ngrok.example.com,IP地址就是我自己的服务器.当域名为ngrok.example.com的时候,最终效果是远程机器的设置子域名为abc时,访问abc.ngrok.example.com:端口就可以。端口是由运行的时候配置的,并不需要确定某一个端口,都是可以设置的。

#这里修改为自己的域名
NGROK_DOMAIN="ngrok.example.com"

openssl genrsa -out rootCA.key 2048
openssl req -x509 -new -nodes -key rootCA.key -subj "/CN=$NGROK_DOMAIN" -days 5000 -out rootCA.pem
openssl genrsa -out device.key 2048
openssl req -new -key device.key -subj "/CN=$NGROK_DOMAIN" -out device.csr
openssl x509 -req -in device.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out device.crt -days 5000

首先进入到ngrok的目录,然后设定域名地址,这里需要修改为自己的.openssl就是生成SSL证书文件的过程,之后会在ngrok目录下生成root,device等六个文件。然后需要拷贝到配置的目录中,在编译的时候会使用这些文件。

cp -r rootCA.pem assets/client/tls/ngrokroot.crt 
cp -r device.crt assets/server/tls/snakeoil.crt 
cp -r device.key assets/server/tls/snakeoil.key

到这个地方,证书生成已经复制的准备工作就已经完成了。

编译服务端程序

make release-server

等待下载和构建,如果下载失败什么的,估计是因为链接国外的服务器会断线的问题,重新运行一遍make release-server就好
构建完成以后可以在bin目录下看到ngrokd这个文件,这个就是我们后面要开启的服务端.

编译客户端程序

这里我们需要交叉编译,使用不同的编译选项来选择编译以后生成的平台
这里我主要是生成了windows/Linux 64/arm的版本。继续在原先的目录下:

GOOS=linux GOARCH=amd64 make release-client
GOOS=windows GOARCH=amd64 make release-client
GOOS=linux GOARCH=arm make release-client

不同平台使用不同的 GOOS 和 GOARCH,前面的编译选项就是指 go os , go 编译出来的操作系统 (windows,linux,darwin);go arch, 对应的构架 (386,amd64,arm)

平台 cpu 编译参数
Linux 32位 GOOS=linux GOARCH=386
Linux 64位 GOOS=linux GOARCH=amd64
Windows 32位 GOOS=windows GOARCH=386
Windows 64位 GOOS=windows GOARCH=amd64
Mac 32位 GOOS=darwin GOARCH=386
Mac 64位 GOOS=darwin GOARCH=amd64
ARM GOOS=linux GOARCH=arm

通过前面的步骤,就会在bin目录里面生成所有的客户端文件,客户端平台是文件夹的名字,客户端放在对应的目录下,当前Linux平台客户端在bin目录下
通过scp或者其他办法把客户端程序复制到需要部署的内网主机上

这里要注意:

  • 服务端文件叫ngrokd
  • 客户端文件叫ngrok
    所以以后要放到对应的平台,就只需要对应平台里面的ngrok文件就可以了

配置服务端

在服务端直接执行就可以了,其中NGROK_DOMAIN对应的就是一开始设置过的域名地址
但是有一点要重点注意,就是httpAddr等端口的设置

  • httpAddr 是访问普通的http使用的端口号,后面用子域名.ngrok.example.com:6060来访问服务
  • httpsAddr 是访问的https使用的端口号,同上,只不过是需要https的服务访问才用这个端口
  • tunnelAddr 是隧道的端口号,这个端口是ngrok服务之间通信用的,所以这个端口在服务器上和客户端上设置必须一直才能连接成功
NGROK_DOMAIN="ngrok.example.com"
ngrokd -domain="$NGROK_DOMAIN" -httpAddr=":6060" -httpsAddr=":6061" -tunnelAddr=":6062" 

#https设置了tls
#ngrokd -domain="www.aiesst.com" -httpAddr=":6060" -httpsAddr=":6061" -tunnelAddr=":6062" -tlsKey=./device.key -tlsCrt=./device.crt

验证端口是否打开的问题

当打开了服务端程序之后可以测试一下,使用如下命令:

nc -v -w 10 -z 127.0.0.1 6060-6062

localhost [127.0.0.1] 6060 (?) open
localhost [127.0.0.1] 6061 (?) open
localhost [127.0.0.1] 6062 (?) open

如果三个端口都显示open,那么就可以继续后边的步骤了

打开防火墙

如果是centOS的系统,防火墙应该是 firewall-cmd 来控制:

firewall-cmd --permanent --zone=public --add-port=6060-6062/tcp  //永久
#firewall-cmd  --zone=public --add-port=6060-6062/tcp   //临时

如果是ubuntu之类的系统,防火墙一般是iptables来控制:

iptables -A INPUT -p tcp --dport 6060-6062 -j ACCEPT
iptables -A OUTPUT -p tcp --sport 6060-6062 -j ACCEPT

配置客户端

先把之前编译好的客户端程序复制到目标主机上,例如:~/ngrok/:

scp -P 9999 /path/of/ngrok user@domain:~/ngork/

然后新建配置文件~/.ngrok

vim ~/.ngrok

server_addr: "ngrok.example.com:6062"
trust_host_root_certs: false
  • 地址和服务器设置的 NGROK_DOMAIN="ngrok.example.com"中的地址保持一致
  • 端口和服务器设置的通道端口 tunnelAddr=":6062"保持一致

ngrok的默认配置文件为$HOME/.ngrok文件,如果想指定其他路径的配置文件,可以使用-config参数,例如ngrok -config=/path/of/file1.cfg start-all

启动客户端

~/ngrok -log=ngrok_log.txt -subdomain=abc -config=ngrok.cfg 80
  • 日志: -log=ngrok_log.txt 是记录ngrok的日志,如果前期调试的时候加上这个参数,如果不能访问就可以查看到底是什么问题
  • 子域名: -subdomain=abc 是定义访问的时候的子域名,现在访问abc.ngrok.example.com:6060就可以访问到这一台机器上80端口的服务

稍微等待几秒钟,如果看到显示有tunnel status: online,那么后面的 forwarding 对应的内容就是访问本机的地址。
当然在linux上面,你需要再跑一个其他的服务在80端口可以看到访问的效果
通过浏览器访问abc.ngrok.example.com:6060就可以链接到现在的内网主机上的服务

TCP转发

这里以转发ssh为例,重新改写客户端配置文件~/.ngrok

vim ~/.ngrok
server_addr: "ngrok.example.com:6060"
trust_host_root_certs: false
tunnels:
  http:
    subdomain: "abc"
    proto:
      http: "80"
 
  ssh:
    remote_port: 10086
    proto:
      tcp: 9999

这个配置文件定义了两个隧道httpssh,名字可以随便写.其中proto参数规定了隧道的类型(http/https/tcp)
http隧道中,subdomain定义了访问web隧道时候,需要使用的前缀,如上面的配置,需要使用abc.ngrok.example.com:6060才能正常访问

ssh隧道中,remote_port参数规定了外网访问时所使用的端口,而tcp参数指定了,该协议转发的服务实际使用的端口.
例如:系统的ssh服务没有使用默认端口22,使用的是9999端口,我需要在外网使用10086端口连接这台主机的ssh服务,就可以采用如上的配置.

ssh -l username -p 10086 ngrok.example.com

连接时指明远程端口

openrc ngrok客户端服务脚本

#!/sbin/openrc-run

pidfile=/var/run/ngrok.pid
logfile=/var/log/ngrok
name="ngrok client daemon"

start(){
    ebegin "start ngrok daemon"
    start-stop-daemon --start   \
    --make-pidfile              \
    --background                \
    --pidfile ${pidfile}        \
    --stdout ${logfile}         \
    --stderr ${logfile}         \
    --exec /root/ngrok/ngrok    \
    -- start-all
    eend $?
}

stop(){
    ebegin 'stop ngrok daemon'
        start-stop-daemon --stop --pidfile ${pidfile}
    eend 0

注意ngrok路径以及配置文件,都要正确配置后,才可以使用.

总结

yaml配置文件

配置文件采用的yaml,面对网上的各种ngrok教程,我照着抄完,再运行ngrok客户端程序,都提示文件格式错误.

  1. 冒号后要跟空格
  2. remote_port选项的值(端口号),绝对不能使用双引号
  3. tcp选项的值(端口号),可以使用双引号

这种摸着石头过河的方法,真的有够郁闷了

ngrok版本

ngrok分为1.0和2.0两个版本,本文使用的1.0分支,作者在readme文件上也明确指出了1.0分支的状态,保持最低限度的编译运行,不再就功能和安全性作出更新.
而2.0版本并没有开源,只能使用ngrok官方的服务器,并不能指定第三方服务端.相比来说,自己定制化的服务端更符合我的需求.

路由转发问题

此前并没有细研究路由转发的原理,http://url:port实际上访问的就是url的port端口.这在现在的路由程序中,没法解决,毕竟程序不能无休止的监听N个端口,而作为路由程序,只要转发对应的url请求到实际的隧道端口,后边的ngrok服务端程序同样会根据url和port进行隧道连接.

其实这次使用ngrok主要目的还是给群晖dsm做外网访问,在群晖的控制面板里可以打开ssh.群晖6.5也是linux64,所以可以直接照搬前边客户端就可以.端口转发可以改成5000(群晖默认端口).
此次测试过程中,用手机app直接打开abc.ngrok.example.com:6060访问正常,但是在播放视频尤其是高清视频的时候,卡顿特别严重.
ECS控制台显示上下行带宽完全占满,1M/s的小水管,真是不够用的.
整体来说,作为一个反向代理访问内网服务器的web服务还是没有问题的,优点也很明显,任何想要访问内网电脑的终端,只要知道url就可以访问了.

相关链接

内网穿透 ngrok 服务器和客户端配置