3x-ui 面板去除赞助广告与登录页 CPU 占用异常处理记录

环境:Ubuntu 22.04 · 3x-ui v3.11.0 · nginx
内容:赞助广告的检查结果 · nginx 反向代理屏蔽方案 · 暴露面收口 · 登录页 CPU 占用排查
说明:文中域名与面板路径均为化名,配置结构与实测结果保持一致。

摘要

1. 背景与现象

使用官方一键脚本在 VPS 上部署 3x-ui 后,登录页和仪表盘中出现了第三方服务的推广卡片,具体包括:

其中登录页的内容无需登录即可看到,也就是说,任何人知道面板地址都能访问到这些内容。由于面板属于自用的管理后台,决定将这部分内容去除。

处理前先对 3x-ui 源码做了检查:克隆仓库、全仓检索并阅读相关实现,同时拉取线上的实际投放清单作为参照。目的是先确认广告系统的实现方式,再决定在哪一层处理。

2. 代码检查结果

关于前端文件
面板前端是打包嵌入二进制文件的,服务器磁盘上没有可直接修改的网页文件。因此直接改前端文件这条路不可行。

3. 方案选择

按对面板的侵入程度,将可行做法分为五层:

层级做法评估
L1 设置层通过面板设置关闭赞助功能没有该选项,不可行。
L2 网络层屏蔽清单所在的 GitHub 域名面板的自升级脚本使用同一域名,会影响升级。面板如果通过代理出站,也可能绕过 DNS 屏蔽。
L3 反向代理层(采用)在面板前部署 nginx,拦截 /sponsors 的两个接口,返回合法的空清单不修改面板,升级后仍然有效。缺点是 Donate 按钮和赞助商菜单两处界面仍然保留,但内容为空。
L4 源码层修改后端代码、删除前端组件,重新编译并替换二进制可以连界面外壳一起去除,但官方升级后需要重新处理,维护成本最高。
L5 升级策略不使用官方升级,或每次升级后复查拦截是否仍然有效是 L3 的配套措施,不是独立方案。

选择 L3 的原因是处理逻辑位于面板之外:面板如何升级都不会影响 nginx 配置,需要撤销时删除对应配置即可恢复原状。L4 的额外收益只是去掉两个空界面入口,不足以抵消每次升级都要重新编译的成本。

拦截方式

前端期望收到固定格式的响应,因此拦截时不能直接返回 404 或断开连接,而是返回一份格式合法、内容为空的清单,使前端认为请求成功,只是当前没有赞助商:

拦截返回体(HTTP 200)
{"success":true,"msg":"","obj":{"contact":"","sponsors":[]}}

logo 接口直接返回 404。清单为空时前端不会请求 logo,这一条仅作兜底。

4. 部署过程

4.1 服务器现状与约束

4.2 端口选择

最初计划使用 8443,因为它在 Cloudflare 支持的 HTTPS 代理端口之列,方便以后开启橙云。实际操作中先尝试了 443,用 curl 测试时返回内容为空:服务在运行,配置也没有问题,但请求没有到达,推测是该端口在入站路径上不通,未做进一步确认。随后改用 8443,一次验证通过:

验收命令与结果
$ curl -sk -o /tmp/resp.txt -w "code=%{http_code} size=%{size_download}\n" \
    "https://panel.example.com:8443/panel-path/sponsors"
code=200 size=60
$ cat /tmp/resp.txt
{"success":true,"msg":"","obj":{"contact":"","sponsors":[]}}

返回 200、60 字节,内容为预期的空清单,说明拦截生效。随后在浏览器中确认广告消失,登录功能正常。

补充一点:在服务器本机用公网域名访问自己,请求会先出站再入站,如果入站路径不通同样会失败。遇到返回为空时,需要先区分是服务本身的问题还是网络路径的问题。

使用 8443 的注意事项
  • 8443 不在 Cloudflare 的 HTTPS 代理端口名单中(它属于 HTTP 名单)。将来开启橙云时,需要把 nginx 监听端口换成 8443 等名单内的端口,并同时将 SSL 模式改为 Full (Strict)。
  • 访问面板必须写明 https://。使用 http 访问会收到 nginx 的 400 报错,这是明文请求发到 HTTPS 端口的正常提示,不是故障。

4.3 暴露面收口

  1. 面板改为只监听本机。将 3x-ui 的监听地址改为 127.0.0.1,443 不再对公网开放。此后外部只能通过 nginx 访问面板,直连 443 失败属于预期结果。如果 nginx 出现问题,仍可通过 SSH 隧道临时访问面板。
  2. 关闭 nginx 默认站点。nginx 安装后默认在 80 端口提供欢迎页,并持续收到扫描流量。该站点来自 sites-enabled/default 软链接,删除软链接并 reload 即可关闭,源文件保留,可随时恢复。面板自己的配置里没有 80 端口。完成后 nginx 只在 8443 对外监听。

4.4 最终架构

浏览器 https://panel.example.com:443 │ ▼ nginx :443(唯一对外入口) · /sponsors 返回空清单 · /sponsors/... 返回 404 · 其余请求 反向代理 │ 仅回环 127.0.0.1 ▼ 3x-ui 面板 :443(公网不可直达,本体未修改) Xray 节点 :443(未改动)

至此,对外暴露的端口由 443、80、8843 减少为只有 443,节点业务未受影响。

5. 登录页浏览器 CPU 占用偏高

5.1 现象

完成上述处理的第二天,在登录页确认广告效果时发现:浏览器窗口停留在登录页期间,本地电脑 CPU 占用明显升高且不下降,服务器 CPU 正常。

由于问题是在刚改过广告接口之后发现的,最初的怀疑是:赞助请求被拦截后返回异常,前端循环重试,导致 CPU 升高。为避免凭印象下结论,先为这个假设设定了证伪条件,即看到什么结果就说明假设不成立,再逐项验证。

5.2 假设一:赞助请求失败后循环重试(已排除)

结论:与广告请求无关。服务器 CPU 正常、网络请求只有 1 条,说明持续消耗发生在本地浏览器的渲染环节。

5.3 定位:登录页背景动画

检查登录页样式后发现,页面背景有 5 个大尺寸光斑持续运动:

大面积模糊叠加持续动画是浏览器中开销较大的渲染组合,页面开着就会持续占用 CPU/GPU。这与现象一致:只在登录页停留时出现,与广告无关。

5.4 验证过程

前两次验证没有得到有效结论,原因是测试本身没有生效:

  1. 用 DevTools 的 prefers-reduced-motion 模拟来关闭动画,CPU 没有下降。复盘后发现该选项是取值选择而非开关,默认值为「不模拟」,当时的操作实际上没有改变任何设置。
  2. 在控制台注入样式来关闭或隐藏光斑,CPU 仍未下降。但当时无法确认注入的样式是否真正生效。

之后给验证加上了确认步骤:注入样式后,用 getComputedStyle 读取光斑伪元素的计算样式,并目视确认光斑已消失。

带确认步骤的验证片段(浏览器控制台)
let st = document.createElement('style');
st.textContent = '.login-app::before,.login-app::after,.login-content::before,' +
  '.login-content::after,.login-wrapper::before{display:none!important}';
document.head.appendChild(st);
getComputedStyle(document.querySelector('.login-app'), '::before').display;
// 控制台输出 "none",且页面上的彩色光斑消失,测试结果才有效

5.5 对照结果

控制变量后,分别在无痕窗口(无扩展、无历史状态)和普通窗口中,对比光斑开启与关闭两种状态:

光斑开启光斑关闭(计算样式为 none,目视确认消失)
无痕窗口CPU 高CPU 下降
普通窗口CPU 高读数仍高

无痕窗口的结果可以说明问题:在没有扩展和历史状态干扰的环境里,页面代码相同,光斑是唯一变量,开启时 CPU 高,关闭后下降。

普通窗口中关闭光斑后读数仍然偏高,说明存在其他干扰因素。排查时考虑了浏览器扩展(关闭扩展后需要刷新页面,已注入的脚本不会自动失效)以及其他标签页对总 CPU 读数的影响,但没有单独逐项确认。因此最终以修改落地后的实际效果为准,见下一节。

5.6 处理方式

登录页的 HTML 文档由面板在 /<webBasePath>/ 下返回。在 nginx 中对这一个文档使用 sub_filter 做字符串替换,在 </head> 之前注入一段样式,隐藏上述 5 个光斑。这样无论使用哪个浏览器,下发的登录页本身就不包含光斑,不需要再手动操作控制台。

实现时有两点需要注意:

验证方法:reload 后用 curl 请求登录页,能在输出中 grep 到注入的 display:none,说明样式已写入下发的 HTML。

结果:加入该配置并 reload 后,在登录页实测 CPU 占用恢复正常,问题解决。

6. 最终 nginx 配置

去除证书等机器相关内容后,站点配置的核心部分如下(路径为化名):

/etc/nginx/sites-available/xui-panel
server {
    listen 8443 ssl http2;
    server_name panel.example.com;
    ssl_certificate     /path/to/fullchain.pem;
    ssl_certificate_key /path/to/privkey.pem;

    # 1. 拦截赞助清单:返回格式合法的空清单
    location = /panel-path/sponsors {
        access_log off;
        default_type application/json;
        add_header Cache-Control "no-store";
        return 200 '{"success":true,"msg":"","obj":{"contact":"","sponsors":[]}}';
    }

    # 2. 赞助 logo 兜底拦截
    location ^~ /panel-path/sponsors/ {
        access_log off;
        return 404;
    }

    # 3. 登录页文档:注入样式,隐藏背景光斑(仅匹配根文档)
    location = /panel-path/ {
        proxy_pass https://127.0.0.1:443;
        proxy_ssl_verify off;
        proxy_set_header Host $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 Accept-Encoding "";  # sub_filter 需要明文 HTML
        proxy_http_version 1.1;
        sub_filter_once on;
        sub_filter_types text/html;
        sub_filter '</head>' '<style>.login-app::before,.login-app::after,.login-content::before,.login-content::after,.login-wrapper::before{display:none!important}</style></head>';
    }

    # 4. 其余请求:反向代理到本机面板
    location / {
        proxy_pass https://127.0.0.1:443;
        proxy_ssl_verify off;
        proxy_set_header Host $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_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400;
        client_max_body_size 50m;  # 默认 1MB,不调整的话面板导入备份时会被 nginx 拦下
    }
}

面板侧的配套设置:监听地址改为 127.0.0.1;建议将「可信代理」设置为 127.0.0.1/32,否则面板看到的访客 IP 始终是本机地址,登录限流可能误伤自己。

7. 升级后的检查清单

处理逻辑位于面板之外,官方升级不会覆盖它,但升级可能改变接口路径或前端结构。每次升级后建议检查以下几项:

  1. 用 curl 请求一次 /<webBasePath>/sponsors,确认仍返回 60 字节的空清单。
  2. 打开登录页停留约半分钟,确认广告没有重新出现,浏览器 CPU 正常。
  3. 用 curl 获取登录页 HTML,grep 注入的样式关键字,确认 sub_filter 仍在生效。注入所用的类名(.login-app 等)属于当前版本的前端,如果大版本更新后登录页类名发生变化,注入会静默失效,此时需要更换选择器。
  4. 确认面板仍只监听 127.0.0.1:443,nginx 仍只对外监听 8443(可用 ss -tunlp 查看)。
  5. 仅在准备开启 Cloudflare 橙云时:先将 SSL 模式改为 Full (Strict),并将 nginx 监听端口换到 Cloudflare 支持的 HTTPS 端口(如 8443)。

8. 总结

  1. 先检查,再动手。通过代码检查确认了广告系统的实现、没有关闭开关,以及前端是嵌入二进制的,据此选定在 nginx 层处理。
  2. 处理逻辑放在面板之外。拦截和样式注入都在 nginx 中完成,面板升级或重装不受影响,撤销时删除对应配置即可。
  3. 排查先设证伪条件。对「请求循环重试」这一假设,先确定应当观察到什么,再查代码和网络请求,很快就能排除。
  4. 验证要有可观察的确认步骤。排查中有两次测试因为操作没有真正生效而得出了不可靠的结论。之后每一步都增加确认手段,例如打印计算样式、目视确认光斑消失、检查 curl 的返回字节数。
  5. 用对照实验确定原因。在无痕窗口中分别测试光斑开启和关闭,得到明确结论。普通窗口中未能完全排除的干扰因素,则以实际落地后的效果作为最终验证。
  6. 登录页背景动画的开销不小。全屏大面积模糊加无限动画会持续消耗本地资源,在性能较弱的设备上尤其明显。

整个过程中,节点端口未改动,面板退回只监听本机,nginx 默认站点已关闭,对外只保留 8443 一个入口。