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