fix: machine mask

This commit is contained in:
Gloridust
2026-06-06 02:15:16 +08:00
parent 8fd147bccc
commit 27fa9c2670
6 changed files with 105 additions and 2 deletions

View File

@@ -77,6 +77,15 @@ WOC_ENABLE_GPU=
# 避免拖垮整台宿主。常规使用单实例约需 1~1.5GiB,设 3~4 较稳妥。 # 避免拖垮整台宿主。常规使用单实例约需 1~1.5GiB,设 3~4 较稳妥。
WOC_INSTANCE_MEM_GB= WOC_INSTANCE_MEM_GB=
# ── 设备伪装(降低被微信判"非真实设备"风控的概率) ───────────
# 每个实例默认已自动做:唯一且持久的 machine-id避免全网共用同一个触发"设备农场"风控)、
# 像个人电脑的 hostname不再是 woc-wx-<hex>)、移除 /.dockerenv 容器标记。这些恒定开启。
#
# WOC_SPOOF_OS 是否把 /etc/os-release 伪装成 deepin微信官方支持的发行版且 Deepin 基于
# Debian、与本镜像用户态一致。默认 1设 0 恢复显示真实 Debian。
# 说明:设备伪装是「尽力而为」,非保证不被封;详见 doc/设备伪装.md。
WOC_SPOOF_OS=1
# ── 自愈 watchdog应对 KasmVNC/Xvnc 长跑内存泄漏) ─────────── # ── 自愈 watchdog应对 KasmVNC/Xvnc 长跑内存泄漏) ───────────
# 实测 Xvnc 长跑 24h 可膨胀到 ~9GiB原因在 KasmVNC/Xvnc 自身的 framebuffer / 软渲染 cache # 实测 Xvnc 长跑 24h 可膨胀到 ~9GiB原因在 KasmVNC/Xvnc 自身的 framebuffer / 软渲染 cache
# 累积,不归本项目控制。面板内置一个 watchdog周期性检查每个 running 实例的 working set # 累积,不归本项目控制。面板内置一个 watchdog周期性检查每个 running 实例的 working set

View File

@@ -58,6 +58,7 @@
|------|------| |------|------|
| [运行原理与 Docker 指南](doc/运行原理.md) | 工作原理 + 架构图;面向 Docker 新手的逐步拆解、常用命令、架构自动适配 | | [运行原理与 Docker 指南](doc/运行原理.md) | 工作原理 + 架构图;面向 Docker 新手的逐步拆解、常用命令、架构自动适配 |
| [部署与运维](doc/部署与运维.md) | 数据持久化、常见问题排查、忘记超管密码的离线找回、目录结构 | | [部署与运维](doc/部署与运维.md) | 数据持久化、常见问题排查、忘记超管密码的离线找回、目录结构 |
| [设备伪装与风控应对](doc/设备伪装.md) | 唯一 machine-id / 真实 hostname / os-release 伪装;账号被微信强制退出循环时怎么办 |
| [发布到 GHCR](doc/发布到GHCR.md) | 用 GitHub Actions 或本机 buildx 把镜像发布到 GHCR | | [发布到 GHCR](doc/发布到GHCR.md) | 用 GitHub Actions 或本机 buildx 把镜像发布到 GHCR |
| [技术方案](doc/技术方案.md) | 完整设计文档与选型权衡 | | [技术方案](doc/技术方案.md) | 完整设计文档与选型权衡 |

52
doc/设备伪装.md Normal file
View File

@@ -0,0 +1,52 @@
# 设备伪装与风控应对
> 返回 [← README](../README.md)
## 背景
微信 Linux 端会采集**设备指纹**做风控。容器/虚拟化环境若指纹异常(尤其是**大量实例共用同一指纹**),会被判定为"设备农场 / 非真实设备",表现为:**登录后立即被以安全原因强制退出,再次登录仍被踢,循环无法使用**。
> 注意:虚拟化本身不等于风险——高校、企业大量使用云桌面 / 瘦客户端 / 虚拟机,都是虚拟化的真实办公设备。问题出在**指纹不像一台独立的真实设备**(最典型:所有实例共用一个 machine-id。本项目的目标就是让每个实例看起来像一台普通、独立的 Linux 桌面。
## 本项目默认做了什么
以下措施**默认全部开启**,每个实例自动生效(新建实例 / 升级实例后):
| 措施 | 说明 | 开关 |
|------|------|------|
| **唯一且持久的 machine-id** | 每个实例首启生成专属 machine-id存入数据卷重启/升级/重建都不变。解决"全网实例共用镜像里烤死的同一个 machine-id"这一最致命信号。 | 恒开 |
| **真实的 hostname** | 内部主机名伪装成"个人电脑"样式(如 `lenovo-pc-372`),不再是 `woc-wx-<hex>` 这种容器/服务器特征。每实例不同、稳定不变。 | 恒开 |
| **移除 `/.dockerenv`** | 删掉 Docker 注入的容器标记文件。 | 恒开 |
| **os-release 伪装成 deepin** | `/etc/os-release` 显示为 deepin 23微信官方支持的发行版Deepin 基于 Debian与本镜像用户态一致不自相矛盾。 | `WOC_SPOOF_OS`,默认 1设 0 恢复 Debian |
实现位置:`docker/woc-identity.sh`(启动钩子 `/custom-cont-init.d/00-woc-identity`root 身份、在微信启动前执行)+ `panel/server/src/docker.ts`hostname / 开关透传)。
## 手动「重置设备 ID」
若某个微信账号**已经被风控标记**、升级后仍登录即被踢,可以给它换一个全新的设备身份(相当于换一台新电脑):
**管理页 → 该实例卡片 →「安全」→「重置设备 ID 并重启」**
会生成一个新的唯一 machine-id 并重启该实例,之后重新扫码登录。仅对已升级到新镜像的实例可用(旧镜像无设备身份模块,会提示先「升级实例」)。
## 已被标记账号的恢复
- 设备指纹是**面向未来**的:换了干净的唯一指纹后,**新的登录**才会以新设备身份示人。
- 已被风控标记的账号可能有**冷却期**,换设备 ID + 重新登录后不一定立刻恢复,需观察一段时间。
- 建议先拿**非主力账号**验证,确认稳定后再迁主号,避免主号反复触发风控。
## 局限与风险(务必知悉)
- **这是"尽力而为",不是保证。** 风控是持续对抗:腾讯会不断增加新的检测维度(如 X 服务器厂商串、无 GPU 软渲染、SMBIOS 缺失、行为特征等),本项目只能覆盖已知的、可控的指纹。
- **有封号风险。** 在非官方环境运行微信本身违反其使用条款;是否使用、用于何种账号,请自行评估。强烈建议**不要用主力/重要账号**承担试验风险。
- 若伪装后仍被频繁踢,可尝试:`WOC_SPOOF_OS=0` 恢复真实 Debian排除 os 伪装反而被交叉校验的可能),或反馈 issue 一起排查更深层信号。
## 调参
`.env`(复制自 [.env.example](../.env.example)
```bash
WOC_SPOOF_OS=1 # 1=伪装成 deepin默认0=显示真实 Debian
```
machine-id / hostname / dockerenv 三项无开关、恒定开启(它们没有合理的关闭理由)。

View File

@@ -28,6 +28,8 @@ services:
- WOC_INSTANCE_MEM_SOFT_MB=${WOC_INSTANCE_MEM_SOFT_MB:-1500} - WOC_INSTANCE_MEM_SOFT_MB=${WOC_INSTANCE_MEM_SOFT_MB:-1500}
- WOC_INSTANCE_MEM_HARD_MB=${WOC_INSTANCE_MEM_HARD_MB:-2500} - WOC_INSTANCE_MEM_HARD_MB=${WOC_INSTANCE_MEM_HARD_MB:-2500}
- WOC_WATCHDOG_INTERVAL_SEC=${WOC_WATCHDOG_INTERVAL_SEC:-300} - WOC_WATCHDOG_INTERVAL_SEC=${WOC_WATCHDOG_INTERVAL_SEC:-300}
# 设备伪装os-release 伪装成 deepin默认开=0 关恢复 Debian。详见 .env.example / doc/设备伪装.md。
- WOC_SPOOF_OS=${WOC_SPOOF_OS:-1}
# 面板首个管理员账号(仅首次启动、无账号文件时写入;务必改掉默认密码) # 面板首个管理员账号(仅首次启动、无账号文件时写入;务必改掉默认密码)
- PANEL_ADMIN_USER=${WOC_USER:-admin} - PANEL_ADMIN_USER=${WOC_USER:-admin}
- PANEL_ADMIN_PASSWORD=${WOC_PASSWORD:-wechat} - PANEL_ADMIN_PASSWORD=${WOC_PASSWORD:-wechat}

View File

@@ -40,4 +40,21 @@ printf '%s\n' "$MID" > /var/lib/dbus/machine-id 2>/dev/null || true
# 抹掉最明显的容器标记(微信可能据此判定非真实桌面)。/.dockerenv 由 docker 注入,删掉无副作用。 # 抹掉最明显的容器标记(微信可能据此判定非真实桌面)。/.dockerenv 由 docker 注入,删掉无副作用。
rm -f /.dockerenv 2>/dev/null || true rm -f /.dockerenv 2>/dev/null || true
echo "[woc-identity] machine-id 已设为本实例专属(持久化于数据卷)" # 设备伪装:把 /etc/os-release 改成 deepin微信官方支持的发行版Deepin 本就基于 Debian
# 与本镜像的 Debian 用户态一致,不自相矛盾)。面板按 WOC_SPOOF_OS 控制(默认开,=0 关)。
# /etc/os-release 是指向 /usr/lib/os-release 的软链,重定向会写穿到目标,故直接写它即可。
if [ "${WOC_SPOOF_OS:-1}" = "1" ]; then
cat > /etc/os-release <<'OSEOF'
PRETTY_NAME="deepin 23"
NAME="deepin"
VERSION_ID="23"
VERSION="23"
VERSION_CODENAME=beige
ID=deepin
ID_LIKE=debian
HOME_URL="https://www.deepin.org/"
BUG_REPORT_URL="https://bbs.deepin.org/"
OSEOF
fi
echo "[woc-identity] machine-id 已设为本实例专属持久化于数据卷os 伪装=${WOC_SPOOF_OS:-1}"

View File

@@ -20,6 +20,24 @@ const ENABLE_GPU = process.env.WOC_ENABLE_GPU === '1';
const INSTANCE_MEM_GB = Number(process.env.WOC_INSTANCE_MEM_GB) || 0; const INSTANCE_MEM_GB = Number(process.env.WOC_INSTANCE_MEM_GB) || 0;
const INSTANCE_MEM = INSTANCE_MEM_GB > 0 ? Math.floor(INSTANCE_MEM_GB * 1024 * 1024 * 1024) : 0; const INSTANCE_MEM = INSTANCE_MEM_GB > 0 ? Math.floor(INSTANCE_MEM_GB * 1024 * 1024 * 1024) : 0;
// 设备伪装:把 /etc/os-release 伪装成 deepin微信官方支持的发行版且 Deepin 本就基于 Debian
// 与本镜像的 Debian 用户态一致,不会自相矛盾)。默认开启;设 WOC_SPOOF_OS=0 关闭恢复 Debian。
// 配合 00-woc-identity 钩子里的 machine-id 唯一化 + 真实 hostname整体让容器更像一台普通 Linux 桌面,
// 降低被腾讯按"非真实设备/设备农场"判风险的概率。注意:尽力而为,非保证;详见 doc/设备伪装.md。
const SPOOF_OS = process.env.WOC_SPOOF_OS !== '0';
// 给实例容器派生一个"像个人电脑"的内部 hostname替代 woc-wx-<hex> 这种容器/服务器特征)。
// 从 inst.id 稳定派生:同一实例每次重建得到相同名字、不同实例不同。仅作伪装,不参与寻址
// (反代用容器名 containerName不用此 hostname
function realisticHostname(id: string): string {
const words = ['deepin', 'lenovo', 'thinkpad', 'matebook', 'xiaoxin', 'legion', 'dell', 'asus', 'desktop', 'home'];
let h = 0;
for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0;
const w = words[h % words.length];
const n = ((h >>> 8) % 900) + 100; // 100-999避免前导 0
return `${w}-pc-${n}`;
}
const docker = new Docker(); // 默认连 /var/run/docker.sock const docker = new Docker(); // 默认连 /var/run/docker.sock
// 面板自身所在的 docker 网络名;新实例都 attach 到它,便于按容器名互访。 // 面板自身所在的 docker 网络名;新实例都 attach 到它,便于按容器名互访。
@@ -78,6 +96,8 @@ function envList(inst: Instance): string[] {
]; ];
// baseimage 仅检查该变量是否「已设置」(值无关),设上即不再给 Xvnc 加 -hw3d。 // baseimage 仅检查该变量是否「已设置」(值无关),设上即不再给 Xvnc 加 -hw3d。
if (!ENABLE_GPU) env.push('DISABLE_DRI=1'); if (!ENABLE_GPU) env.push('DISABLE_DRI=1');
// 透传 os 伪装开关给容器内的 00-woc-identity 钩子(决定是否把 /etc/os-release 改成 deepin
env.push(`WOC_SPOOF_OS=${SPOOF_OS ? '1' : '0'}`);
return env; return env;
} }
@@ -124,7 +144,9 @@ export async function runInstance(inst: Instance): Promise<void> {
const container = await docker.createContainer({ const container = await docker.createContainer({
name: inst.containerName, name: inst.containerName,
Image: WECHAT_IMAGE, Image: WECHAT_IMAGE,
Hostname: inst.containerName, // 内部 hostname 伪装成"个人电脑"名(不再用 woc-wx-<hex>,那是容器/服务器特征)。
// 反代靠容器名 name 寻址,与此 hostname 无关。
Hostname: realisticHostname(inst.id),
Env: envList(inst), Env: envList(inst),
ExposedPorts: { '3000/tcp': {} }, ExposedPorts: { '3000/tcp': {} },
HostConfig: hostConfig, HostConfig: hostConfig,