monitor

安装

反向代理

一键脚本装出的 hub 只监听 127.0.0.1,公网访问不到,域名由反向代理接进来。下面任选一种。

caddy

证书、X-Forwarded-Proto 和 WebSocket 都由 caddy 自动处理,请求体大小默认不限:

hub.example.com {
    reverse_proxy 127.0.0.1:28080
}
caddyfile

nginx

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;
    }
}
nginx

80 端口跳转到 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:404
yaml
# 建隧道,并把域名解析到它
cloudflared tunnel create monitor
cloudflared tunnel route dns monitor hub.example.com
# 装成系统服务;只想临时试跑用 cloudflared tunnel run monitor
cloudflared service install
bash

在 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;
nginx

nginx 要带 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/ws
bash

面板节点页的「添加节点」能点,说明是从 HTTPS 域名进的;从 IP 或明文地址进面板时它是灰的。hub 依据的是浏览器发来的 Origin,不看 Host 和 X-Forwarded-Proto,所以反代不要清掉或改写 Origin。

X-Forwarded-Proto 是否生效,看用 GitHub 登录后的会话 cookie 有没有 Secure 标志:浏览器开发者工具的应用 → Cookie 里,monitor_session 那一行。密码登录按 Origin 判断,看不出这一项。

在 GitHub 上修改这一页最后更新 2026-10-19