安装
反向代理
一键脚本装出的 hub 只监听 127.0.0.1,公网访问不到,域名由反向代理接进来。下面任选一种。
caddy
证书、X-Forwarded-Proto 和 WebSocket 都由 caddy 自动处理,请求体大小默认不限:
hub.example.com {
reverse_proxy 127.0.0.1:28080
}caddyfilenginx
map $http_upgrade $connection_upgrade { default upgrade; '' close; }
server {
# http2:连接悄悄断开后,浏览器能自己发现并换新连接
# nginx 1.25.1 起 nginx -t 会提示这种写法已过时,不影响使用
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name hub.example.com;
ssl_certificate /etc/letsencrypt/live/hub.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/hub.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:28080;
proxy_http_version 1.1;
# 导入备份、上传主题按 4 MiB 分片上传,8m 足够,与数据库大小无关
client_max_body_size 8m;
# 压缩由 hub 自己决定:后台的回复带着节点地址,不能被压缩
gzip off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# /api/agent/ws 与 /api/ws 是 WebSocket 长连接
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}
}nginx80 端口跳转到 HTTPS:
server {
listen 80;
listen [::]:80;
server_name hub.example.com;
return 301 https://$host$request_uri;
}nginx宝塔 / aapanel
面板里添加的反向代理可以直接用,目标地址填 http://127.0.0.1:28080。它生成的配置里没有
X-Forwarded-Proto,用 GitHub 登录时会话 cookie 因此拿不到 Secure 标志,要在站点配置文件的 location ^~ / 块里补一行:
proxy_set_header X-Forwarded-Proto $scheme;nginx改的是站点配置文件。改完不要再在反向代理的表单里点保存,那会重新生成这段配置,这一行随之丢失。
Cloudflare 隧道
隧道由 cloudflared 向外建立,不用开入站端口,只有 IPv4 的机器也能得到双栈入口。配置保存为
~/.cloudflared/config.yml:
tunnel: <tunnel-id>
credentials-file: /root/.cloudflared/<tunnel-id>.json
ingress:
- hostname: hub.example.com
service: http://127.0.0.1:28080
- service: http_status:404yaml# 建隧道,并把域名解析到它
cloudflared tunnel create monitor
cloudflared tunnel route dns monitor hub.example.com
# 装成系统服务;只想临时试跑用 cloudflared tunnel run monitor
cloudflared service installbash在 WAF 或边缘规则里按路径做了放行列表的,要覆盖 hub 的全部路径和主题的前端路由(默认主题的节点详情页是 /node/{id})。被挡的请求 403 来自边缘,hub 没有日志:升级 hub 后新增的接口不通,或详情页「点进去正常,一刷新就被拦」,都是这个原因。路径清单见架构与协议。
用 Cloudflare Access 保护面板
Access 要求先在浏览器里登录,agent 做不到这一步:安装和上报的请求会在边缘被转去 Access 的登录页,到不了 hub。给 agent 用到的路径单独建一个 Access 应用,策略选 Bypass,包含这三条路径:
hub.example.com/api/agent
hub.example.com/agent
hub.example.com/install.sh其余路径仍由覆盖整个域名的那个应用保护。同一域名下路径更具体的应用优先,每条路径都包含它下面的子路径,/api/agent/ws、/agent/x86_64 不用另写。这三条路径不靠 Access 也有自己的门:上报要节点的 token,注册要注册窗口内有效的 key,安装脚本和 agent 二进制本来就是公开的,不含密钥。
公开状态页在同一个域名下,整个域名放进 Access 后,访客也要先登录才能看到。
四项必需的设置
上面的 caddy 和 nginx 配置已经满足前三项。自己改写配置、或用别的反代时逐项核对。
1. 转发 WebSocket 头,关掉缓冲,放大超时
/api/agent/ws(agent 上报)和 /api/ws(浏览器实时推送)是长连接。没转发 Upgrade / Connection
头,或读写超时还是默认的 60 秒,节点会周期性掉线又自己回来。
2. 请求体上限 8 MiB
导入备份和上传主题按 4 MiB 分片上传,hub 对单个请求的上限是 8 MiB。这个数不随数据库变大:1 GiB 的备份也只是 256 个 4 MiB 的请求,Cloudflare 免费版每个请求 100 MB 的上限同样够用。
nginx 默认的 client_max_body_size 1m 连一片都放不过。这时 413 来自反代,hub 没有日志,面板收到 413
会直接提示调大 client_max_body_size。
3. 透传 X-Forwarded-Proto 和 X-Forwarded-For
- 没有
X-Forwarded-Proto:用 GitHub 登录时会话 cookie 拿不到Secure(密码登录看浏览器的Origin,不受影响)。前面还有 Cloudflare 以外的 CDN 时同样如此,见什么时候需要--site - 没有
X-Forwarded-For:登录限流按反代的地址计数,所有访客共用一个计数;节点的连接来源也记成反代地址,NAT 后面的节点显示不出出口地址,网卡上没有公网地址的节点也查不到国家
hub 只在连接来自本机或内网地址时才采信 X-Forwarded-For,并取最后一个值。上面的写法都是追加而不是覆盖,最后一个值就是反代实际看到的地址。
前面是 Cloudflare(域名开了代理,或走隧道)时,节点的出口地址和国家不用另外配置:hub 在通过 token
校验的 agent 连接上读取 Cloudflare 写入的 CF-Connecting-IP。登录限流默认不读这个头,因为从 Cloudflare
网络连到源站的不只有它的代理(Worker 也可以),这个头能被伪造;限流因此仍按边缘地址计,同一个边缘后面的访客共用一个计数。下面的开关打开后才读它。
前面是别家 CDN,或隧道先到本机的 nginx 再到 hub 时,最后一个值是边缘或隧道的地址:所有访客共用一个限流计数,登录通知里的地址是边缘的。节点的出口地址和国家不受影响,hub 读 X-Forwarded-For 里第一个公网地址,什么都不用配。
要让登录通知和限流也按访客算,有两种办法,选一种即可。
在反代上换。只信任列出的网段,源站不必只对 CDN 开放。nginx 和 caddy 各有现成的写法:
# 写在 server 块里。每个回源网段一行,照 CDN 公布的列表填;隧道在本机时填 127.0.0.1
set_real_ip_from 192.0.2.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;nginxnginx 要带 realip 模块,nginx -V 2>&1 | grep realip 有输出即可。caddy 的写法:
{
servers {
trusted_proxies static 192.0.2.0/24
trusted_proxies_strict
}
}
hub.example.com {
reverse_proxy 127.0.0.1:28080 {
header_up X-Forwarded-For {client_ip}
}
}caddyfile名单之外的地址写进 X-Forwarded-For 的内容不会被采信:绕过 CDN 直连源站的请求,仍然按它自己的地址计数。
或者声明源站只对 Cloudflare 或隧道开放。前面是 Cloudflare 或隧道时不用配反代。设置页的「我确认源站只对 Cloudflare 或隧道开放」默认关,打开后登录通知和限流取访客地址:Cloudflare 写的 CF-Connecting-IP,没有这个头时取 X-Forwarded-For 里最右边的公网地址(隧道经本机反代送来时,右边的内网跳会被跳过)。这是你向 hub 作的保证,hub 不会替你限制谁能连上源站,要你自己做到:
- 用隧道,源站不开公网端口
- 或者让 Cloudflare 回源时带一个密钥头,反代没看到就拒绝
只放行 Cloudflare 的回源地址不够:别人的 Worker 也从那些地址连过来,请求头随便写。
不是却打开,别人直连源站并自己写 CF-Connecting-IP 或 X-Forwarded-For,就能每次换一个地址猜密码,登录限流形同虚设。
别家 CDN 用不上这个开关:它后面再接一层有公网地址的反代时,最右边的公网地址仍是 CDN 边缘,用上一种办法。保存后,开关下面会显示当前认出你的地址:是你自己的,说明生效;是 CDN 边缘的,说明属于这种情况。
不要写 proxy_set_header X-Forwarded-For $http_cf_connecting_ip;,本页早先的版本给过这一行,照抄过的请删掉。有人绕过 Cloudflare 直连源站时,这个头由他自己填:换着值试密码,登录限流就失效;填上你的地址,你会被锁在登录页外 15 分钟。
4. 放行 POST / PUT / DELETE
面板的写操作用到这三个方法。只放行 GET / HEAD 的 WAF 规则会让整个后台变成只读。
反代自己开了压缩的(nginx 的 gzip_types 含 application/json,宝塔等面板常这样配),对 hub 这个站点关掉,像上面的配置那样写 gzip off。hub 自己压缩该压的回复;后台的节点列表带着各节点的地址,又带着 agent 上报的内容,压缩后的长度能被用来猜出这些地址。
验证
# 面板能打开,且走 HTTPS:第一行是 HTTP/2 200
# 是 HTTP/1.1 200 说明 nginx 的 listen 没带 http2
curl -sI https://hub.example.com/admin | head -1
# WebSocket 能升级:输出 101。升级后连接保持,3 秒后由 --max-time 结束
curl -s --http1.1 --max-time 3 -o /dev/null -w '%{http_code}\n' \
-H 'Connection: Upgrade' -H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
https://hub.example.com/api/wsbash面板节点页的「添加节点」能点,说明是从 HTTPS 域名进的;从 IP 或明文地址进面板时它是灰的。hub 依据的是浏览器发来的 Origin,不看 Host 和 X-Forwarded-Proto,所以反代不要清掉或改写 Origin。
X-Forwarded-Proto 是否生效,看用 GitHub 登录后的会话 cookie 有没有 Secure 标志:浏览器开发者工具的应用 → Cookie 里,monitor_session 那一行。密码登录按 Origin 判断,看不出这一项。