我们在 web 开发中,经常会有暴露本地端口的需求:
- 网站开发到一半,希望分享一下当前成果;
- 某app小程序的开发,只能通过生产环境默认端口进行接口调试;
- 通过公网进行联调;
- ……
ngrok 的出现让我们可以通过服务器的中转,快速暴露本地端口。那么 ngrok 方案有什么不足呢?
- ngrok 在客户端和服务端都要安装;
- ngrok 已经闭源,开始走商业化发展路线,不再适合我等屁民使用;
- 使用自定义域名有诸多不便。
定制方案
我尝试寻找一个简单的方案,来实现部分 ngrok 的能力,满足我们开发调试过程中的大部分需求。
内网穿透到公网
SSH 提供了加密的端口流量转发能力,可以用来快速实现这个步骤。通过 SSH 可以把一个本地端口和远程端口映射起来,打通流量隧道。先看一下 SSH 的相关文档:
-N Do not execute a remote command. This is useful for just forwarding ports.
-R [bind_address:]port:host:hostport
-R [bind_address:]port:local_socket
-R remote_socket:host:hostport
-R remote_socket:local_socket
-R [bind_address:]port
Specifies that connections to the given TCP port or Unix socket on the remote (server) host are to be forwarded to the local side.
通过指定 -N
,我们可以让 SSH 不执行远程命令,而只用于转发流量。
通过指定 -R 远程地址:本地地址
,即可实现端口流量转发。
举个栗子:
$ ssh -NR 2333:localhost:8080 server
即把本地的 8080 端口映射到服务器的 2333 端口,由于没指定 bind_address
,这个端口默认会绑定到本地环回地址(localhost)上,必须通过反向代理才能从外部访问。
我们也可以绑定 bind_address
到 0.0.0.0
,让端口直接暴露到外部:
$ ssh -NR 0.0.0.0:2333:localhost:8080 server
这要求我们开启 OpenSSH 的 GatewayPorts
配置。这种情况下,我们就可以直接从外部访问 server_address:2333
了。
但是,为了提供缓存、资源压缩、权限校验、域名控制等能力,一般来说我们更推荐使用 NGINX 反向代理来暴露端口。通过 NGINX 统一管理,可以在一个端口上服务多个站点,通过域名区分站点,这样我们就可以从默认端口提供服务,地址看上去更自然,同时还能满足某些 app 的特殊要求。
绑定自定义域名
如果没有固定的域名,我们每次开启服务,都需要提供一个新的 address:port
或者 ip:port
地址,这显然是很不友好的。ngrok 的免费版提供的就是一个随机域名。而通过 NGINX,我们可以很轻松地定制一个域名映射规则,然后只需要按照规则开启服务,就可以通过想要的域名访问到。
这里有一个大前提,首先我们要有个域名,而且域名要泛解析到我们的服务器,如 *.gerald.win
添加 CNAME
记录到 gerald.win
。只有解析到了我们的服务器,NGINX 才可以有发挥的空间。
举个栗子,约定域名 tunnel2333.gerald.win
反代到服务器的 localhost:2333
,以此类推,我们只要在打通隧道时映射到同样的端口号,就可以用同样的域名访问。NGINX 配置如下:
server {
listen 80;
server_name "~^tunnel(?\d+)\.gerald\.win$";
location / {
proxy_pass http://127.0.0.1:$port;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_intercept_errors on;
}
}
同样的,我们也可以通过 unix socket 文件名的约定来实现更加定制化的子域名,这里就不详细展开了。
支持HTTPS
随着大家对网络安全的重视,HTTPS 几乎已经成了网站的标配,很多接口都要求服务端必须为 HTTPS 协议,这也在无形中提高了我们的开发调试门槛。通过服务器的转发,这个问题也迎刃而解。
作为普通开发者,我们就不考虑购买昂贵的 HTTPS 证书了。好在良心产品 Let's Encrypt 从 ACME v2 开始已经支持泛解析了,于是 Let's Encrypt 一把梭搞定。这里强烈推荐国人开发的脚本 acme.sh,证书生成过程不再赘述。
有了证书,我们再配置一下提供 HTTPS 服务的 NGINX 配置,还可以给 HTTP 协议访问的页面做个 301 跳转:
server {
listen 80;
server_name "~^tunnel(?\d+)\.gerald\.win$";
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name "~^tunnel(?\d+)\.gerald\.win$";
ssl_certificate /home/gerald/ssl/fullchain.cer;
ssl_certificate_key /home/gerald/ssl/gerald.win.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:$port;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_intercept_errors on;
}
}
这样我们就实现了 HTTPS 协议的支持,而且无需为每个新域名单独配置。
不足
与 ngrok 相比,我们的简化方案其实还是有很多不足的,比如缺失了强大的 inspector,无法动态展示当前连接的用户、访问的页面和流量。这些功能属于锦上添花了,理论上都可以在客户端进行实现,以后可以考虑增强一下。
总结
原理总结如下:
- SSH 打通隧道
- NGINX 反向代理
- Let's Encrypt 实现泛解析 HTTPS 证书
通过 SSH + NGINX,我们基本实现了本地端口的暴露,完美地支持了通过 HTTPS 协议来调试本地的应用。