Goby 是一款网络资产测绘与安全扫描工具,能够对目标网络进行端口探测、服务指纹识别和常见漏洞检测。本文记录在鸿蒙 PC 平台上,基于 Electron 壳方案从零构建该工具的完整流程,帮助开发者理解网络扫描类应用在鸿蒙平台的适配要点。

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

AtomGit 仓库地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_goby

一、技术架构分析

1.1 原始架构(Go 语言版)

  • 技术栈:Go + Web 前端
  • 核心逻辑:SYN 半开放扫描 + 内置指纹库 + 数百条 PoC
  • 部署方式:独立安装包

1.2 目标架构(鸿蒙 Electron)

  • 技术栈:Electron + HTML/CSS/JavaScript + 鸿蒙 web_engine 模块
  • 核心逻辑:纯 Node.js 内置模块(net/http/dns/os)实现 TCP 全连接扫描 + HTTP 指纹 + 8 条漏洞规则
  • 部署方式:HAP 包(鸿蒙应用)

1.3 架构差异

对比项Go 原版鸿蒙适配版
扫描引擎Go net 包Node.js net 模块
端口探测SYN 半开放扫描TCP 全连接(net.Socket)
指纹识别内置指纹库HTTP GET + 标题/Server 头提取
漏洞检测数百条 PoC8 条核心规则
前端框架ReactHTML/CSS/Vanilla JavaScript
外部依赖go mod零依赖(纯 Node.js 内置模块)
部署方式独立安装包HAP 包(鸿蒙应用)

二、环境准备

2.1 开发环境要求

  • 操作系统:Windows 10/11
  • 开发工具:DevEco Studio(鸿蒙官方 IDE)
  • HarmonyOS SDK:API 15+
  • Node.js:v20+(Electron 依赖)

2.2 项目结构

ohos_hap/
├── electron-apps/
│   └── Goby/                    # Goby Electron 应用源码
│       ├── main.js              # 主进程(窗口管理 + IPC + 扫描调度)
│       ├── scanner.js           # 扫描引擎(TCP 探测 + HTTP 指纹 + 漏洞检测)
│       ├── renderer.js          # 渲染进程(UI 交互 + 看板渲染)
│       ├── index.html           # HTML 布局(IDE 风格)
│       ├── package.json         # 项目配置
│       └── styles/
│           └── goby.css         # Catppuccin Mocha 深色主题
├── web_engine/                  # 鸿蒙 web_engine 模块
│   └── src/main/resources/
│       └── resfile/resources/app/  # 部署目录
│           ├── main.js
│           ├── scanner.js
│           ├── renderer.js
│           ├── index.html
│           ├── package.json
│           └── styles/goby.css
└── build-profile.json5          # 鸿蒙构建配置

三、核心适配流程

3.1 第一步:实现扫描引擎

文件:electron-apps/Goby/scanner.js

扫描引擎是整个应用的核心,基于 Node.js 内置 net 模块实现真实 TCP connect 扫描,覆盖 30 个常见端口:

// scanner.js - 端口及服务映射
var PORT_MAP = {
  21: 'FTP', 22: 'SSH', 23: 'Telnet', 25: 'SMTP', 53: 'DNS',
  80: 'HTTP', 110: 'POP3', 111: 'RPCBind', 135: 'MSRPC', 139: 'NetBIOS',
  143: 'IMAP', 443: 'HTTPS', 445: 'SMB', 993: 'IMAPS', 995: 'POP3S',
  1433: 'MSSQL', 1521: 'Oracle', 2049: 'NFS', 3306: 'MySQL',
  3389: 'RDP', 5432: 'PostgreSQL', 5900: 'VNC', 5901: 'VNC',
  6379: 'Redis', 8080: 'HTTP-Proxy', 8443: 'HTTPS-Alt',
  8888: 'HTTP-Alt', 9090: 'WebConsole', 27017: 'MongoDB',
  9200: 'Elasticsearch', 11211: 'Memcached'
};

TCP 端口探测使用独立的 net.Socket 进行真实连接,带 done 防重入标志:

// scanner.js - TCP 端口探测
function probePort(host, port, timeout) {
  return new Promise(function(resolve) {
    var sock = new net.Socket();
    var done = false;
    sock.setTimeout(timeout || 2000);
    sock.connect(port, host, function() {
      if (done) return;
      done = true;
      sock.destroy();
      resolve({ port: port, open: true });
    });
    sock.on('timeout', function() {
      if (done) return;
      done = true;
      sock.destroy();
      resolve({ port: port, open: false });
    });
    sock.on('error', function() {
      if (done) return;
      done = true;
      sock.destroy();
      resolve({ port: port, open: false });
    });
  });
}

关键要点

  • 零外部依赖:仅使用 Node.js 内置的 net/http/dns/os 模块
  • done 防重入:每个 Socket 的 timeout/error/connect 回调都检查 done 标志,防止重复 resolve
  • 2 秒超时:单个端口探测超时 2 秒,避免长时间阻塞

3.2 第二步:实现扫描调度与并发控制

文件:electron-apps/Goby/scanner.js

扫描循环使用独立的 dispatchIdx 调度指针和 tryFinish 统一收尾,避免异步回调中的索引竞态:

// scanner.js - 分批扫描,控制并发
var dispatchIdx = 0;
var activeCount = 0;
var allDone = false;

function tryFinish() {
  if (activeCount === 0 && (dispatchIdx >= targets.length || allDone)) {
    if (!allDone) {
      allDone = true;
      self.runVulnChecks().then(function() {
        self.finishScan();
      });
    }
  }
}

function scanNextHost(host) {
  if (allDone || !self.running) {
    activeCount--;
    tryFinish();
    return;
  }
  // ... 扫描逻辑 ...
  activeCount--;
  var next = dispatchIdx++;
  if (next < targets.length && self.running) {
    setTimeout(function() { scanNextHost(targets[next]); }, 50);
  }
  tryFinish();
}

关键要点

  • dispatchIdx 独立调度指针:与 activeCount 分离,避免双重递增导致跳过主机
  • tryFinish 统一收尾:所有完成判断集中在一处,避免多处 emit done
  • 主机间 50ms 间隔 + 默认并发 10:防止鸿蒙平台大量 TCP 连接触发 XComponent 崩溃
  • running 标志检查:scanNextHost 入口检查 self.running,确保 stop 后不再调度新主机

3.3 第三步:实现漏洞检测引擎

文件:electron-apps/Goby/scanner.js

内置 8 条漏洞检测规则,全部基于真实协议交互:

规则 ID名称端口检测方法等级
GOBY-001Redis 未授权访问6379TCP 发送 INFO 命令,检查 redis_version高危
GOBY-002Memcached 未授权访问11211TCP 发送 stats 命令,检查 STAT version高危
GOBY-003Elasticsearch 未授权访问9200HTTP GET /,解析 JSON version.number高危
GOBY-004MongoDB 未授权访问27017发送 Wire Protocol OP_QUERY 报文高危
GOBY-005HTTP 安全头缺失80检查 4 个安全响应头中危
GOBY-006FTP 匿名登录21FTP 握手发送 USER/PASS anonymous高危
GOBY-007Telnet 暴露23TCP 连接检查 login/username 关键字中危
GOBY-008RDP 暴露3389发送 X.224 Connection Request中危

以 Redis 检测为例:

// scanner.js - GOBY-001 Redis 未授权访问
{ id: 'GOBY-001', name: 'Redis 未授权访问', port: 6379, protocol: 'tcp',
  check: function(host, port, cb) {
    var sock = new net.Socket();
    sock.setTimeout(3000);
    sock.connect(port, host, function() {
      sock.write('INFO\r\n');
    });
    var data = '';
    sock.on('data', function(chunk) { data += chunk.toString(); });
    sock.on('timeout', function() { sock.destroy(); cb(false); });
    sock.on('error', function() { cb(false); });
    sock.on('end', function() {
      cb(data.indexOf('redis_version') >= 0);
    });
  }
}

漏洞检测使用 Promise.race 设置 30 秒整体超时兜底,防止任何规则回调丢失导致扫描卡住:

// scanner.js - 漏洞检测超时兜底
var timeoutPromise = new Promise(function(resolve) {
  setTimeout(function() {
    self.log('WARN', '漏洞检测超时(30s),强制结束');
    resolve();
  }, 30000);
});

return Promise.race([
  Promise.all(checks).then(function() {
    self.emit('progress', { phase: 'vulncheck', total: self.assets.length, scanned: self.assets.length });
  }),
  timeoutPromise
]);

关键要点

  • 8 条规则全部使用真实 TCP/HTTP 交互,零模拟数据
  • GOBY-006/007/008 使用 done 防重入标志,防止 close 事件导致双重回调
  • GOBY-004 MongoDB 需要构造 60 字节 Wire Protocol 报文(MsgHeader + OP_QUERY + BSON)
  • Promise.race 超时兜底确保扫描不会永远卡住

3.4 第四步:创建主进程与 IPC 通道

文件:electron-apps/Goby/main.js

// main.js - 主进程核心结构
var electron = require('electron');
var app = electron.app;
var BrowserWindow = electron.BrowserWindow;
var ipcMain = electron.ipcMain;
var scannerModule = require('./scanner');

app.disableHardwareAcceleration();

var mainWindow = null;
var scanner = null;

function createWindow() {
  var display = screen.getPrimaryDisplay();
  mainWindow = new BrowserWindow({
    width: Math.floor(display.workAreaSize.width * 0.9),
    height: Math.floor(display.workAreaSize.height * 0.85),
    frame: true,
    transparent: false,
    resizable: true,
    webPreferences: { nodeIntegration: true, contextIsolation: false }
  });
  mainWindow.loadFile('index.html');
}

app.whenReady().then(createWindow);

主进程提供 5 个 IPC 通道:

// 启动扫描
ipcMain.handle('scan:start', function(event, targetInput, ports, concurrency) {
  if (scanner && scanner.running) {
    return { success: false, error: '扫描任务正在运行中' };
  }
  // 清理旧扫描引擎,防止事件泄漏
  if (scanner) {
    scanner.removeAllListeners();
    scanner = null;
  }
  scanner = new scannerModule.GobyScanner();
  // ... 挂载事件监听 ...
  setImmediate(function() { scanner.scan(targetInput, portList, conc); });
  return { success: true };
});

// 停止扫描
ipcMain.handle('scan:stop', function() {
  if (!scanner || !scanner.running) {
    return { success: false, error: '当前无运行中的扫描任务' };
  }
  scanner.running = false;
  scanner.endTime = Date.now();
  scanner.scanLog.push({ time: new Date().toISOString().substring(11, 23), level: 'WARN', msg: '用户手动停止扫描' });
  var result = { assets: scanner.assets, vulns: scanner.vulns, log: scanner.scanLog, duration: scanner.endTime - scanner.startTime, summary: scanner.getSummary(), stopped: true };
  scanner.removeAllListeners();
  scanner = null;
  if (mainWindow && !mainWindow.isDestroyed()) {
    mainWindow.webContents.send('scan:done', result);
  }
  return { success: true };
});

其余 IPC 通道包括:scan:getNetworkInfo(获取本机网卡信息)、scan:getLastResult(获取上次扫描结果)、scan:exportReport(导出报告)。

关键要点

  • 所有 IPC 通道统一返回 { success: true, … } 或 { success: false, error: … } 格式
  • scan:start 创建新 scanner 前必须 removeAllListeners 清理旧实例,防止事件泄漏
  • scan:stop 直接构建结果并清理,不再通过 emit 触发事件链,减少 IPC 发送次数
  • frame: true + transparent: false 防止 XComponent 崩溃

3.5 第五步:设计 IDE 风格页面布局

文件:electron-apps/Goby/index.html

页面采用 IDE 风格布局,包含工具栏、预设标签栏、侧边面板和中央标签页:

<body>
  <!-- 顶部工具栏 -->
  <div id="toolbar">
    <div class="toolbar-left">
      <span class="app-logo">G</span>
      <span class="app-title">Goby</span>
      <span class="app-subtitle">Security Scanner</span>
    </div>
    <div class="toolbar-center">
      <div id="targetInput" class="target-input-wrap">
        <span class="input-icon">🎯</span>
        <input type="text" id="txtTarget" placeholder="输入目标:IP / CIDR / 范围" spellcheck="false">
      </div>
      <button id="btnScan" class="tb-btn primary" title="开始扫描">▶ 扫描</button>
      <button id="btnStop" class="tb-btn danger" title="停止扫描" disabled>⏹ 停止</button>
      <button id="btnExport" class="tb-btn" title="导出报告">📄 导出</button>
    </div>
    <div class="toolbar-right">
      <span id="statusText" class="status-text">就绪</span>
    </div>
  </div>
  <!-- 快速测试预设(内联标签,无绝对定位) -->
  <div id="presetBar" class="preset-bar">
    <span class="preset-label">快测:</span>
    <span class="preset-tag" data-value="127.0.0.1">🏠 127.0.0.1 <em>本机回环</em></span>
    <span class="preset-tag" data-value="scanme.nmap.org">🎯 scanme.nmap.org <em>Nmap测试</em></span>
    <span class="preset-tag" data-value="example.com">🌍 example.com <em>安全头检测</em></span>
  </div>
  <!-- 主体区域 -->
  <div id="mainArea">
    <div id="sidebar"><!-- 网络信息 + 端口配置 --></div>
    <div id="content"><!-- 标签页:资产/漏洞/看板/原始输出 --></div>
  </div>
</body>

关键要点

  • 工具栏三组布局:左侧 Logo、中间操作区、右侧状态文字
  • 预设标签栏使用自然文档流的内联标签,不使用 position:absolute 下拉框
  • 并发数下拉框使用自定义 div 实现,不使用原生 select(鸿蒙平台会崩溃)
  • 所有交互元素避免 position:fixed + z-index + transform/animation 组合

3.6 第六步:实现渲染进程交互

文件:electron-apps/Goby/renderer.js

通知系统改为状态栏文字更新,不使用浮动层弹窗:

// renderer.js - 状态栏通知(替代 DOM 弹窗)
function notify(msg, type) {
  var st = el('statusText');
  if (!st) return;
  st.textContent = msg;
  st.className = 'status-text' + (type === 'error' ? ' error' : type === 'success' ? ' success' : '');
}

数据看板在页面加载时初始化零状态,扫描完成后自动更新:

// renderer.js - 看板渲染
function renderDashboard(summary) {
  var overviewHtml = '<div class="overview-grid">';
  overviewHtml += '<div class="overview-item"><div class="overview-num">' + summary.totalAssets + '</div><div class="overview-label">活跃资产</div></div>';
  overviewHtml += '<div class="overview-item"><div class="overview-num danger">' + summary.totalVulns + '</div><div class="overview-label">发现漏洞</div></div>';
  overviewHtml += '<div class="overview-item"><div class="overview-num">' + (summary.duration / 1000).toFixed(1) + 's</div><div class="overview-label">扫描耗时</div></div>';
  overviewHtml += '<div class="overview-item"><div class="overview-num">' + summary.totalPorts + '</div><div class="overview-label">探测总数</div></div>';
  overviewHtml += '</div>';
  el('dashAssetOverview').innerHTML = overviewHtml;
  renderBarChart(el('dashPortChart'), summary.portStats, 'port');
  renderBarChart(el('dashServiceChart'), summary.serviceStats, 'service');
}

扫描进行中点击预设标签时,状态栏提示用户先停止:

// renderer.js - 预设标签点击保护
document.querySelectorAll('.preset-tag').forEach(function(tag) {
  tag.addEventListener('click', function() {
    var val = this.getAttribute('data-value');
    if (val) {
      if (isScanning) {
        notify('扫描进行中,请先点"停止"再切换目标', 'error');
        return;
      }
      el('txtTarget').value = val;
    }
  });
});

关键要点

  • notify 函数直接更新状态栏文字和颜色,零浮动层
  • 看板使用 renderBarChart 渲染纯 CSS 柱状图,不依赖外部图表库
  • 预设标签在扫描进行中禁止切换,防止用户误操作

四、部署与测试

4.1 同步文件到鸿蒙项目

每次修改代码后,需要将 electron-apps/Goby/ 下的文件同步到鸿蒙部署目录:

# 同步脚本
$src = "electron-apps\Goby"
$dst = "web_engine\src\main\resources\resfile\resources\app"
"package.json","main.js","scanner.js","index.html","renderer.js","styles\goby.css" | ForEach-Object {
    Copy-Item "$src\$_" "$dst\$_" -Force
    Write-Host "OK: $_"
}

部署路径:web_engine/src/main/resources/resfile/resources/app/

注意:每次修改代码后都需要同步,否则构建的 HAP 包不会包含最新代码。

4.2 在鸿蒙设备上测试

  1. 连接鸿蒙设备(HUAWEI MateBook Pro 或启动模拟器)
  2. 点击 Run > Run ‘entry’
  3. 安装完成后,应用自动启动
  4. 点击预设标签栏的 127.0.0.1 或 scanme.nmap.org
  5. 点击 ▶ 扫描 开始测试

五、常见问题 FAQ

Q1:扫描停止后再扫描再停止,应用崩溃?

问题现象:扫描 → 停止 → 再扫描 → 再停止,应用报 SIGABRT 崩溃

根本原因:每次 scan:start 创建新 scanner 并挂载 5 个事件监听器,但旧 scanner 未被清理。旧 scanner 的残留 TCP 连接超时后触发事件,通过已失效的监听器发送 IPC 导致崩溃

解决方案:scan:start 开头调用 removeAllListeners 清理旧实例;scan:stop 中直接构建结果并清理

// 正确写法 - 创建前清理
if (scanner) {
  scanner.removeAllListeners();
  scanner = null;
}
scanner = new scannerModule.GobyScanner();

Q2:漏洞检测永远不完成,一直显示扫描中?

问题现象:端口扫描完成后,状态一直显示扫描中,看板不更新

根本原因:GOBY-008 RDP 检测规则缺少 close 事件处理。远端直接关闭连接时,data/timeout/error 均不触发,只有 close 事件但无处理,回调永远不调用,Promise.all 永远不完成

解决方案:为 GOBY-008 添加 close 事件处理 + runVulnChecks 添加 30 秒超时兜底

// 修复后 - GOBY-008 添加 close 处理
sock.on('timeout', function() { if (!done) { done = true; sock.destroy(); cb(resp.length > 0); } });
sock.on('error', function() { if (!done) { done = true; cb(false); } });
sock.on('close', function() { if (!done) { done = true; cb(false); } });

Q3:扫描完成或停止时应用崩溃?

问题现象:扫描完成或点停止后,应用崩溃报 SIGABRT

根本原因:notify 函数创建带 position:fixed + z-index:999 + CSS transform 动画的浮动通知弹窗,触发鸿蒙适配层创建 SubWindow,XComponent 超时崩溃

解决方案:将 notify 改为直接更新状态栏文字和颜色

// 错误写法(鸿蒙平台必崩)
var div = document.createElement('div');
div.className = 'goby-notification';  // position:fixed + z-index:999 + animation
document.body.appendChild(div);

// 正确写法
function notify(msg, type) {
  var st = el('statusText');
  st.textContent = msg;
  st.className = 'status-text' + (type === 'error' ? ' error' : ' success');
}

Q4:看板扫描耗时显示 NaN?

问题现象:扫描完成后,数据看板的扫描耗时显示 “NaNs”

根本原因:scan:stop 处理中未设置 endTime,getSummary 计算 undefined - startTime = NaN

解决方案:停止时补充 endTime 设置

// 修复后
scanner.running = false;
scanner.endTime = Date.now();  // 补充 endTime

Q5:点击预设标签后扫描按钮点不了?

问题现象:点击预设下拉框中的 IP 后,扫描按钮无法点击

根本原因:预设下拉框使用 position:absolute + z-index:200,在鸿蒙平台上创建不可见的覆盖层,遮挡扫描按钮的点击事件

解决方案:移除绝对定位下拉框,改为工具栏下方的内联标签按钮行

<!-- 错误写法(绝对定位遮挡按钮) -->
<div class="target-presets-dropdown">...</div>

<!-- 正确写法(内联标签,自然文档流) -->
<div class="preset-bar">
  <span class="preset-tag" data-value="127.0.0.1">🏠 127.0.0.1</span>
  <span class="preset-tag" data-value="scanme.nmap.org">🎯 scanme.nmap.org</span>
</div>

Q6:如何同步文件到鸿蒙项目?

问题现象:修改了 electron-apps/Goby/ 下的文件,但构建后没有生效

根本原因:文件没有同步到鸿蒙 web_engine 模块的部署目录

解决方案:使用 PowerShell 脚本同步文件(参见 4.1 节)

部署路径:web_engine/src/main/resources/resfile/resources/app/

注意:每次修改代码后都需要同步,否则构建的 HAP 包不会包含最新代码。

六、总结

本文记录了 Goby 网络资产测绘工具在鸿蒙 PC 平台的完整适配过程,核心经验如下:

  • 零依赖设计:扫描引擎仅使用 Node.js 内置的 net/http/dns 模块,无需 npm 安装任何第三方包,天然兼容鸿蒙沙盒环境
  • 真实协议交互:8 条漏洞检测规则全部基于真实 TCP/HTTP 协议交互,不使用模拟数据,确保扫描结果可验证
  • 并发控制策略:默认并发 10 + 主机间 50ms 调度间隔,在扫描效率与鸿蒙平台稳定性之间取得平衡
  • 防重入机制:所有 Socket 回调统一使用 done 标志防重入,避免 close/timeout/error 事件交叉触发导致的双重回调
  • 鸿蒙三防适配:禁用 alert/confirm/prompt、禁用原生 select、禁用 position:fixed + z-index + transform 动画组合,全面规避 XComponent 崩溃

这些经验对于在鸿蒙平台开发网络扫描类、安全检测类应用具有直接参考价值。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐