使用官方一键脚本在 VPS 上部署 3x-ui 后,登录页和仪表盘中出现了第三方服务的推广卡片,具体包括:
其中登录页的内容无需登录即可看到,也就是说,任何人知道面板地址都能访问到这些内容。由于面板属于自用的管理后台,决定将这部分内容去除。
处理前先对 3x-ui 源码做了检查:克隆仓库、全仓检索并阅读相关实现,同时拉取线上的实际投放清单作为参照。目的是先确认广告系统的实现方式,再决定在哪一层处理。
internal/web/service/panel/sponsor.go 中写死了远程清单地址。每次有人打开面板,面板会向 GitHub 请求一份 sponsors.json。请求成功后缓存 1 小时,失败后 10 分钟重试;刷新失败时保留旧清单继续展示,源码注释说明这样做是为了避免付费赞助位闪烁消失。GET /sponsors 与 GET /sponsors/logo/:name 是公开路由,目的是让登录页也能展示内容。实际地址位于面板自定义路径之下,形式为 /<webBasePath>/sponsors。adTag 是节点所有者给自己频道使用的字段。检查中未发现第二套推送系统,也未发现遥测。按对面板的侵入程度,将可行做法分为五层:
| 层级 | 做法 | 评估 |
|---|---|---|
| L1 设置层 | 通过面板设置关闭赞助功能 | 没有该选项,不可行。 |
| L2 网络层 | 屏蔽清单所在的 GitHub 域名 | 面板的自升级脚本使用同一域名,会影响升级。面板如果通过代理出站,也可能绕过 DNS 屏蔽。 |
| L3 反向代理层(采用) | 在面板前部署 nginx,拦截 /sponsors 的两个接口,返回合法的空清单 | 不修改面板,升级后仍然有效。缺点是 Donate 按钮和赞助商菜单两处界面仍然保留,但内容为空。 |
| L4 源码层 | 修改后端代码、删除前端组件,重新编译并替换二进制 | 可以连界面外壳一起去除,但官方升级后需要重新处理,维护成本最高。 |
| L5 升级策略 | 不使用官方升级,或每次升级后复查拦截是否仍然有效 | 是 L3 的配套措施,不是独立方案。 |
选择 L3 的原因是处理逻辑位于面板之外:面板如何升级都不会影响 nginx 配置,需要撤销时删除对应配置即可恢复原状。L4 的额外收益只是去掉两个空界面入口,不足以抵消每次升级都要重新编译的成本。
前端期望收到固定格式的响应,因此拦截时不能直接返回 404 或断开连接,而是返回一份格式合法、内容为空的清单,使前端认为请求成功,只是当前没有赞助商:
{"success":true,"msg":"","obj":{"contact":"","sponsors":[]}}logo 接口直接返回 404。清单为空时前端不会请求 logo,这一条仅作兜底。
*:443)。最初计划使用 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 字节,内容为预期的空清单,说明拦截生效。随后在浏览器中确认广告消失,登录功能正常。
补充一点:在服务器本机用公网域名访问自己,请求会先出站再入站,如果入站路径不通同样会失败。遇到返回为空时,需要先区分是服务本身的问题还是网络路径的问题。
https://。使用 http 访问会收到 nginx 的 400 报错,这是明文请求发到 HTTPS 端口的正常提示,不是故障。127.0.0.1,443 不再对公网开放。此后外部只能通过 nginx 访问面板,直连 443 失败属于预期结果。如果 nginx 出现问题,仍可通过 SSH 隧道临时访问面板。sites-enabled/default 软链接,删除软链接并 reload 即可关闭,源文件保留,可随时恢复。面板自己的配置里没有 80 端口。完成后 nginx 只在 8443 对外监听。至此,对外暴露的端口由 443、80、8843 减少为只有 443,节点业务未受影响。
完成上述处理的第二天,在登录页确认广告效果时发现:浏览器窗口停留在登录页期间,本地电脑 CPU 占用明显升高且不下降,服务器 CPU 正常。
由于问题是在刚改过广告接口之后发现的,最初的怀疑是:赞助请求被拦截后返回异常,前端循环重试,导致 CPU 升高。为避免凭印象下结论,先为这个假设设定了证伪条件,即看到什么结果就说明假设不成立,再逐项验证。
useSponsorsQuery.ts,配置为 retry: false(失败不重试)、staleTime 为一小时(一小时内重新聚焦窗口也不会再次请求),且没有轮询间隔。此外 nginx 返回的是「200 成功 + 空清单」,前端走成功分支,列表为空时赞助组件不渲染,不存在触发失败重试的路径。sponsors,在登录页停留数分钟,请求只有 1 条。如果是请求循环造成的,这里应当有大量请求。结论:与广告请求无关。服务器 CPU 正常、网络请求只有 1 条,说明持续消耗发生在本地浏览器的渲染环节。
检查登录页样式后发现,页面背景有 5 个大尺寸光斑持续运动:
filter: blur(90px–100px) 的重度模糊;backdrop-filter: blur(24px),光斑每移动一帧,卡片区域就要对背景重新采样并模糊一次。大面积模糊叠加持续动画是浏览器中开销较大的渲染组合,页面开着就会持续占用 CPU/GPU。这与现象一致:只在登录页停留时出现,与广告无关。
前两次验证没有得到有效结论,原因是测试本身没有生效:
之后给验证加上了确认步骤:注入样式后,用 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",且页面上的彩色光斑消失,测试结果才有效控制变量后,分别在无痕窗口(无扩展、无历史状态)和普通窗口中,对比光斑开启与关闭两种状态:
| 光斑开启 | 光斑关闭(计算样式为 none,目视确认消失) | |
|---|---|---|
| 无痕窗口 | CPU 高 | CPU 下降 |
| 普通窗口 | CPU 高 | 读数仍高 |
无痕窗口的结果可以说明问题:在没有扩展和历史状态干扰的环境里,页面代码相同,光斑是唯一变量,开启时 CPU 高,关闭后下降。
普通窗口中关闭光斑后读数仍然偏高,说明存在其他干扰因素。排查时考虑了浏览器扩展(关闭扩展后需要刷新页面,已注入的脚本不会自动失效)以及其他标签页对总 CPU 读数的影响,但没有单独逐项确认。因此最终以修改落地后的实际效果为准,见下一节。
登录页的 HTML 文档由面板在 /<webBasePath>/ 下返回。在 nginx 中对这一个文档使用 sub_filter 做字符串替换,在 </head> 之前注入一段样式,隐藏上述 5 个光斑。这样无论使用哪个浏览器,下发的登录页本身就不包含光斑,不需要再手动操作控制台。
实现时有两点需要注意:
proxy_set_header Accept-Encoding "";,禁止后端返回 gzip 压缩的 HTML,否则 sub_filter 无法匹配到明文内容。proxy_set_header,就不会继承上层的转发头,需要把所需的转发头完整重写一遍。验证方法:reload 后用 curl 请求登录页,能在输出中 grep 到注入的 display:none,说明样式已写入下发的 HTML。
结果:加入该配置并 reload 后,在登录页实测 CPU 占用恢复正常,问题解决。
去除证书等机器相关内容后,站点配置的核心部分如下(路径为化名):
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 始终是本机地址,登录限流可能误伤自己。
处理逻辑位于面板之外,官方升级不会覆盖它,但升级可能改变接口路径或前端结构。每次升级后建议检查以下几项:
curl 请求一次 /<webBasePath>/sponsors,确认仍返回 60 字节的空清单。curl 获取登录页 HTML,grep 注入的样式关键字,确认 sub_filter 仍在生效。注入所用的类名(.login-app 等)属于当前版本的前端,如果大版本更新后登录页类名发生变化,注入会静默失效,此时需要更换选择器。127.0.0.1:443,nginx 仍只对外监听 8443(可用 ss -tunlp 查看)。curl 的返回字节数。整个过程中,节点端口未改动,面板退回只监听本机,nginx 默认站点已关闭,对外只保留 8443 一个入口。