NGINX+SSH 暴露本地端口

我们在 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_address0.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(?<port>\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(?<port>\d+)\.gerald\.win$";

  location / {
    return 301 https://$host$request_uri;
  }
}

server {
  listen 443 ssl http2;
  server_name "~^tunnel(?<port>\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 协议来调试本地的应用。


© 2020