要解决的问题
本地跑的服务没有公网地址。自己用浏览器访问没问题,但凡是需要外部主动回调的对接就全卡住:
- 微信公众号的消息回调、网页授权回跳
- 支付渠道的异步通知(notify_url)
- 各种第三方 Webhook
- OAuth 回调
这些都是对方的服务器要主动请求你,而且基本都强制 HTTPS。局域网 IP 和 localhost 在这里毫无意义。
常见的做法是用 ngrok 之类的隧道服务,但域名是随机的、每次重启就变,注册回调地址时很难受。自己有一台带公网 IP 的小机器的话,用 frp 自建更省心:域名固定,想开几个开几个。
整体形状
本地服务(:端口A) --frpc--> 公网机 127.0.0.1:端口B --nginx--> https://项目名.example.cn
三层各管一件事:
- frpc/frps 只负责把 TCP 流量从内网穿到公网机的回环地址上。注意是
127.0.0.1,不对外暴露。 - nginx 负责按域名分流、终结 TLS、加转发头。
- DNS 用一条泛解析
*.example.cn指向公网机,之后加子域名不用再动 DNS。
把 TLS 放在 nginx 而不是 frp 上,好处是证书只管一处,隧道那层保持简单。
frps 侧
公网机上跑 frps,配置可以极简:
bindPort = 7000
[auth]
method = "token"
token = "<一串够长的随机字符串>"
frpc 侧
内网机上,每个要暴露的端口一段 proxy:
serverAddr = "<公网机 IP>"
serverPort = 7000
[auth]
method = "token"
token = "<与 frps 相同>"
[[proxies]]
name = "myapp"
type = "tcp"
localIP = "127.0.0.1"
localPort = 3000 # 本地服务端口
remotePort = 3100 # 公网机上的端口
remotePort 要先查再定。撞上已被占用的端口,frpc 会直接连不上,而错误信息不一定明显:
ssh 公网机 'ss -ltn'
nginx 侧
server {
listen 80;
server_name myapp.example.cn;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name myapp.example.cn;
ssl_certificate /path/to/cert/fullchain.pem; # *.example.cn 通配证书
ssl_certificate_key /path/to/cert/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3100;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
几个别省的地方:
proxy_pass写完整的http://127.0.0.1:端口。 只写端口号 nginx 会当主机名去做 DNS 解析,请求会挂住到超时而不是干脆报 502,极难定位。X-Real-IP和X-Forwarded-For都要给。 后端做限流、风控、日志都靠它们。注意$proxy_add_x_forwarded_for是「在客户端送来的值后面追加真实 peer」,所以 XFF 首段是客户端可伪造的,末段才是本机 nginx 写的;X-Real-IP被直接置为$remote_addr,客户端追加不进去,最可信。$connection_upgrade需要在 http 块里有对应的 map,否则 websocket 会断:map $http_upgrade $connection_upgrade { default upgrade; '' close; }
把它变成一条命令
上面这套配置每加一个站点就要重写一遍,纯粹是体力活,而且容易手滑。写个脚本收敛掉:
site add <子域名> <端口> # 生成 vhost、套通配证书、nginx -t、reload
site list # 所有站点及后端死活
site ports # 挑端口前先看占用
site rm <子域名> # 删站
脚本里有三件事值得做:
- 写完先
nginx -t,不过就自动回滚。 新站删掉、旧配置还原,绝不留下让 nginx 起不来的配置。这一条最重要——一个坏配置能让整台机器上所有站点一起挂。 - 不覆盖已存在的站点,除非显式
--force。共享域名的机器上,手滑覆盖掉别人的站是很现实的风险。 - 校验子域名和端口格式,别让奇怪的输入拼进配置文件。
多项目共用一个域名时的规矩
一台穿透机、一个域名被多个项目共用时,冲突几乎是必然的。两条约定能省掉大部分麻烦:
- 每个项目用自己的
<项目名>.example.cn,谁都不许占用顶级域。 顶级域被抢了,整个测试站的入口就废了。 remotePort先查再用,并且在文档里维护一份已占用清单。
一点安全上的提醒
这套东西的本质是把内网服务开到公网上。所以:
- 隧道只穿到公网机的
127.0.0.1,不要直接0.0.0.0暴露端口,让 nginx 做唯一入口。 - frps 的 token 当密码对待,别进仓库、别进聊天记录。
- 测试环境如果接的是真实的支付渠道或真实的公众号,它就不再只是「测试环境」了。这种情况下公网可达意味着真实风险,配置该禁用的禁用。