闲得慌自研「手机局域网遥控电脑」开源项目,零密钥安全架构设计

电脑搁家里,人经常不在跟前。有时候就想看看它是不是还活着,进程有没有抽风,实在不行远程给它关个机。

市面上的远程控制软件我装过,功能是猛,但每次打开都要登录,还要忍受各种广告弹窗,慢,还重。我寻思就这点需求,犯得着为它掏钱包、被广告糊脸吗?

算了,自己写一个吧。

本项目是一套纯局域网、轻量化、高安全的手机遥控电脑方案,全程仅依赖内网 WiFi 通信,不依赖任何公网隧道、不提供中转服务、不暴露公网端口,所有交互封闭在本地局域网内,极大降低入侵风险。

整体采用C/S架构,Android作为客户端,Windows平台运行Node.js作为服务端,底层基于WebSocket长连接双向通信。客户端APK不硬编码任何密钥、Token。项目主动阉割高危能力,不实现鼠标操控、剪贴板劫持、锁屏解锁,只保留监控查看以及可控开关机能力。

业务处理流程

白名单运维流程

系统分层

层级 模块 主要职责
客户端 Android APP 界面渲染、组装请求指令,无密钥硬编码
接入层 WebSocket连接管理 连接管理、心跳保活、消息接收分发
安全层 设备鉴权模块 读取本地白名单、设备序列号校验、时间戳防重放
调度层 消息协议处理 统一JSON编解码、指令路由、统一返回格式
执行层 PowerShell脚本集合 采集系统信息、查询进程、截图、文件遍历、开关机
辅助模块 QQ通知Worker 本地文件队列,异步推送告警消息,不阻塞主循环
本地运维工具 auth.ps1、deploy.ps1 设备授权拉黑、一键部署、注册Windows服务

核心模块实现

设备鉴权模块(安全核心)

鉴权的核心思路:whitelist.json白名单文件只能本机脚本读写,WebSocket接口不能新增修改设备。手机连接上报设备序列号,服务端读取本地文件做校验。修改白名单之后不需要重启服务,设备下次重连即可生效。

这里有一个PowerShell很容易踩的编码坑:如果输出JSON带UTF‑8 BOM,Node的JSON.parse会直接解析报错。ps1脚本保存为UTF‑8‑BOM,但是输出存储的JSON文件,必须使用无BOM的UTF‑8编码。

php 复制代码
function Load-WL { Get-Content $wlFile -Raw -Encoding UTF8 | ConvertFrom-Json }
function Save-WL($wl) {
    $json = $wl | ConvertTo-Json -Depth 6
    $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::WriteAllText($wlFile, $json, $utf8NoBom)
}

Load‑WL读取磁盘白名单,Save‑WL把对象写回磁盘,强制无BOM编码。授权设备时组装设备信息对象,包含序列号、别名、型号、允许标记、添加时间,操作对象属性完成后调用保存函数落地文件。

bash 复制代码
$val = @{ serial = $serial; name = $name; model = $model; allow = $true; addedAt = (Get-Date -Format 'yyyy‑MM‑dd HH:mm:ss') }
if ($wl.devices.PSObject.Properties[$serial]) {
    $wl.devices.PSObject.Properties[$serial].Value = $val
}else{
    $wl.devices | Add‑Member -NotePropertyName $serial -NotePropertyValue $val
}
Save-WL $wl

白名单存储样例:

json 复制代码
{
  "devices": {
    "device_serial_xxxx": {
      "serial":"device_serial_xxxx",
      "name":"我的小米手机",
      "model":"Xiaomi 14",
      "allow":true,
      "addedAt":"2026‑08‑20 10:20:00"
    }
  }
}

Node服务收到每一条WebSocket消息都要执行鉴权,除校验白名单allow字段,增加客户端时间戳校验用来防御重放攻击,抓包得到的旧指令时间偏差过大直接拒绝。

javascript 复制代码
function authDevice(serial, ts) {
    const now = Date.now();
    if(Math.abs(ts - now) > 30*1000){
        return {ok:false, msg:"timestamp invalid, replay reject"};
    }
    const wl = JSON.parse(fs.readFileSync("./whitelist.json","utf‑8"));
    const dev = wl.devices[serial];
    if(!dev) return {ok:false, msg:"device not found"};
    if(!dev.allow) return {ok:false, msg:"device rejected"};
    return {ok:true};
}

鉴权逻辑放在消息处理最前面,所有业务指令先过鉴权,鉴权失败直接返回403,不会向下执行业务分发。

ini 复制代码
ws.on("message",(raw)=>{
    const payload = JSON.parse(raw);
    const authRes = authDevice(payload.serial, payload.ts);
    if(!authRes.ok){
        ws.send(JSON.stringify({code:403,msg:authRes.msg,data:null}));
        return;
    }
    dispatchCmd(payload.cmd, payload.data, ws);
})

未授权设备可以建立WebSocket连接,但是全部业务请求被拦截,仅返回待授权提示,APP据此提示用户回到电脑本地执行授权脚本。

PowerShell子进程调用设计,解决注入与乱码问题

早期踩坑:直接在JS代码内部拼接powershell命令字符串,遇到中文、空格、特殊符号就会乱码,同时存在命令注入风险。

最终方案:业务逻辑全部写独立.ps1脚本,Node只调用脚本文件,不在JS层拼接业务参数。系统信息采集脚本为了适配APP高频轮询,放弃性能较差的Get‑CimInstance,改用WMI一次性读取系统指标。

ini 复制代码
$os = ([wmi]"\.\root\cimv2:Win32_OperatingSystem=@")
$cs = ([wmi]"\.\root\cimv2:Win32_ComputerSystem=@")
$cpuLoad = (Get‑WmiObject -Class Win32_Processor | Measure‑Object -Property LoadPercentage -Average).Average

$obj = @{
    ok      = $true
    cpuLoad = [math]::Round($cpuLoad, 1)
    totalMem = [long]$cs.TotalPhysicalMemory
    freeMem  = [long]$os.FreePhysicalMemory * 1KB
    upDays   = [math]::Floor($uptime.TotalDays)
    disks    = $disks
}
$obj | ConvertTo‑Json -Depth 4 -Compress

脚本内部完成查询,输出压缩JSON到标准输出,Node使用execFile调用脚本,不会拼接任何业务参数。

javascript 复制代码
async function getSysInfo(){
    return new Promise((resolve,reject)=>{
        execFile("powershell.exe",[
            "-NoProfile","‑ExecutionPolicy","Bypass",
            "-File","./sysinfo_fast.ps1"
        ],{encoding:"utf‑8"},(err,stdout)=>{
            if(err) return reject(err);
            const data = JSON.parse(stdout.trim());
            resolve(data);
        })
    })
}

截图常驻进程优化

如果每次截图都新建PowerShell进程,进程创建销毁开销高,连续截图延迟大。采用常驻进程方案:Node只拉起一次powershell,脚本循环等待stdin输入;Node往stdin写入换行就触发截图,截图完成输出base64图片字符串到stdout。

PowerShell核心循环:

php 复制代码
while ($true) {
    $line = [Console]::In.ReadLine()
    if ($null -eq $line) { break }
    $s = Get‑ScreenShotBase64
    [Console]::Out.WriteLine($s)
    [Console]::Out.Flush()
}

Node通过spawn拉起常驻进程,复用进程管道交互,避免反复创建子进程。

php 复制代码
const cp = spawn("powershell.exe",["‑NoProfile","‑ExecutionPolicy","Bypass","‑File","./screenshot.ps1"]);
function triggerScreenshot(){
    cp.stdin.write("\n");
}
cp.stdout.on("data",buf=>{
    const base64Img = buf.toString().trim();
    ws.send(JSON.stringify({code:200,cmd:"screenshot",data:{img:base64Img}}))
})

QQ通知异步队列解耦

WebSocket回调中不能执行同步网络IO,QQ消息发送一旦发生阻塞,会卡住Node整个事件循环,全部客户端受影响。

实现方式:业务不直接发送消息,通知内容写入本地json队列文件;独立qq_worker.js扫描队列目录逐个发送。为规避powershell命令行引号转义bug,消息文本写入临时文件,ps脚本读取文件而不是读取命令行参数。

业务只写入队列,不阻塞WebSocket:

javascript 复制代码
function pushQQNotify(text){
    const filename = `./qq_queue/${Date.now()}.json`;
    fs.writeFileSync(filename,JSON.stringify({text}),"utf‑8");
}

worker处理队列片段:

ini 复制代码
const files = fs.readdirSync(DIR).filter(f => f.endsWith('.json'));
for (const f of files) {
    const fp = path.join(DIR, f);
    const payload = JSON.parse(fs.readFileSync(fp, 'utf8'));
    const msgFile = path.join(os.tmpdir(), 'rc_qq_msg_' + Date.now() + '.txt');
    fs.writeFileSync(msgFile, payload.text, 'utf8');
    execFileSync(PS1, ['‑File', SEND_PS1, '‑MessageFile', msgFile]);
    fs.unlinkSync(fp);
}

deploy.ps1部署关键逻辑

部署脚本完成环境校验、依赖安装、配置初始化,支持后台隐藏运行和NSSM注册Windows服务两种模式。

白名单文件不存在的时候生成初始模板,同样使用无BOM UTF‑8,防止Node解析报错。

ruby 复制代码
$wlFile = Join‑Path $ServerDir 'server\whitelist.json'
if (-not (Test‑Path $wlFile)) {
    $tpl = '{"devices":{}}'
    [System.IO.File]::WriteAllText($wlFile, $tpl, [System.Text.UTF8Encoding]::new($false))
}

启动Node进程设置CreateNoWindow = $true实现后台无黑窗口运行,配置全部通过环境变量注入,不硬编码配置文件。

ini 复制代码
$psi = New‑Object System.Diagnostics.ProcessStartInfo
$psi.FileName = (Get‑Command node).Source
$psi.Arguments = '"' + $ServerJs + '"'
$psi.CreateNoWindow = $true
$psi.EnvironmentVariables['RC_PORT'] = "$Port"
$p = [System.Diagnostics.Process]::Start($psi)

工程踩坑总结

  1. PowerShell编码区分两套:.ps1脚本保存为UTF‑8‑BOM;输出给Node读取的JSON文件必须无BOM UTF‑8,混用直接解析报错。
  2. JS层禁止拼接shell命令字符串,全部调用独立脚本文件,从根源规避Shell注入漏洞。
  3. 高频操作尽量复用常驻子进程,截图这类频繁调用场景,反复创建销毁进程性能损耗巨大。
  4. 安全边界严格把控:白名单修改权限仅限本地脚本,网络链路只做读取校验,不能变更配置。
  5. WebSocket消息强制统一{code,msg,data}结构,前后端协议对齐,避免业务正常但客户端解析失败的隐性bug。

项目开源地址:github.com/YANYIyes/RemoteControl,完整脚本放在GitHub仓库,文章只展示关键代码片段。

相关推荐
FIT2CLOUD飞致云3 小时前
智能运维如何落地?WorkBuddy+ JumpServer Skills给你答案
运维·开源·1panel·运维面板
m4Rk_3 小时前
【论文阅读】Agent 记忆机制(45):ReMemR1——让长期上下文 Agent 可以回溯历史记忆进行非线性推理
论文阅读·人工智能·学习·开源·github
大模型丫丫15 小时前
RAGFlow:开源、可深度定制的企业级 RAG 应用开发引擎
开源·rag
冬奇Lab15 小时前
开源项目第192期:shadcn-admin — Vite + shadcn/ui 构建的后台管理界面参考集
开源·资讯
fthux16 小时前
装闭 RenoPit 源码解析(14):Demo模式、健康检查与Docker部署
人工智能·ai·开源·github·open source·renopit
TunerT_TQ16 小时前
Meta React 源码静态评测:从 4540 个源文件看 React Compiler 的工程化演进
meta·开源·github
越千年17 小时前
A 股行情插件开源了,还带老板模式
开源·工具
梦梦代码精18 小时前
基于UniApp+Vue3+ThinkPHP 8,这套知识付费系统的架构设计有点东西
java·低代码·docker·uni-app·开源·php