1. 项目概述:为什么需要Arkime的实时通知?
如果你用过Arkime(前身是Moloch)进行全流量包捕获和分析,你肯定遇到过这样的场景:你设置了一个复杂的搜索规则,用来监控某个特定IP的异常行为,或者追踪某个敏感数据包的流向。然后呢?你只能一遍遍地手动刷新页面,或者设置一个定时任务去轮询查询结果。这不仅效率低下,更关键的是,你可能会错过那些需要立即响应的“黄金时间窗口”。一个攻击告警晚几分钟看到,后果可能完全不同。
这就是我决定折腾Arkime实时通知系统的原因。Arkime本身是一个强大的工具,但其Web界面的交互本质上是“拉取”模式,缺乏主动“推送”能力。我们需要一种机制,让Arkime在捕获到符合特定条件的数据包、会话结束时,或者系统状态发生变化时,能主动、实时地通知我们。想象一下,当有可疑的SSH爆破尝试、内部服务器对外发起异常连接,或者某个关键服务的流量突然归零时,你的手机、Slack频道或者内部告警平台能立刻收到一条消息——这才是真正意义上的态势感知。
我选择的方案是WebSocket。相比传统的轮询(Polling)或长轮询(Long Polling),WebSocket提供了全双工、低延迟的通信通道。一旦连接建立,服务器可以随时向客户端推送消息,客户端也可以随时发送请求,非常适合Arkime这种事件驱动、需要实时反馈的场景。整个配置过程,从零开始到收到第一条实时通知,我花了不到5分钟。下面,我就把这套经过实战验证的WebSocket事件推送配置机制拆解给你看。
2. 核心思路与架构设计
在动手之前,我们先理清整个通知系统的数据流和组件角色。Arkime本身并不直接内置一个功能齐全的“通知中心”,它的强大之处在于其开放的API和灵活的架构。我们的目标是在不修改Arkime核心代码的前提下,为其增加一个实时事件推送层。
2.1 整体架构拆解
整个系统可以看作由三个核心部分组成:
- 事件源(Arkime) :这是数据的生产者。Arkime在捕获包、解析会话、更新数据库时,会产生各种事件。我们需要一个“钩子”来捕获这些事件。
- 事件处理与推送中间件(我们的WebSocket服务) :这是系统的中枢。它负责监听Arkime产生的事件,根据预定义的规则进行过滤、格式化,然后通过WebSocket连接推送给所有在线的客户端。
-
事件消费者(客户端)
:这可以是任何能连接WebSocket并解析JSON消息的程序。比如:
- 一个简单的HTML+JavaScript网页,用于在监控大屏上实时显示告警。
- 一个Python脚本,接收到事件后调用企业微信、钉钉或Slack的机器人API发送消息。
- 一个Go语言的后台服务,将事件存入时序数据库(如InfluxDB)用于后续分析。
那么,如何从Arkime“钩”出事件呢?这里有几个备选方案:
-
监听Arkime的日志文件
:Arkime会输出详细的操作日志。我们可以用
tail -f或者类似logstash的工具去解析日志,但这种方式比较“脏”,日志格式可能变化,且解析复杂事件(如一个完整会话的元数据)很困难。 - 利用Arkime的API :Arkime提供了丰富的RESTful API。我们可以定时轮询API来获取新数据,但这又回到了老路上,不是真正的“推送”。
- 监听数据库变化(推荐) :Arkime将所有捕获的会话(Sessions)和包(Packets)的元数据存储在Elasticsearch或OpenSearch中。当新数据写入时,数据库本身会产生“变化”。我们可以监听这个变化。
我选择的是第三种方案,因为它最直接、最可靠。具体来说,我使用了一个轻量级的工具: Elasticsearch的“滚动查询”配合一个简单的Node.js WebSocket服务 。为什么不直接用Elasticsearch的官方“Alerting”功能或Watcher?因为我们需要的是高度定制化、低延迟的推送,并且希望通知逻辑与我们的其他系统(如前端展示、自定义机器人)紧密集成,一个自建的WebSocket服务提供了最大的灵活性。
2.2 技术选型与工具清单
基于以上思路,我选用了以下工具栈,它们都是轻量级、易部署的代表:
-
后端服务(WebSocket Server)
:Node.js +
ws库。Node.js的异步非阻塞特性非常适合处理大量并发的WebSocket连接,ws库是Node.js领域最成熟、性能最好的WebSocket实现。 -
数据库监听
:使用Elasticsearch的“滚动查询”(Scroll API)或更现代的“Point in Time”(PIT)API进行增量数据查询。我们不会使用消耗较大的持续监听(如
_changesAPI,Elasticsearch本身不直接提供类似功能),而是采用一种“智能轮询”的方式。 - Arkime环境 :假设你已经有一个正常运行的Arkime集群(包括Capture节点和Viewer节点),并且数据后端是Elasticsearch/OpenSearch。
这个方案的优势在于 无侵入性 。我们不需要修改Arkime的任何配置或代码,只需要它有写入数据库的权限和我们有读取数据库的权限即可。整个WebSocket服务是独立部署的,即使它宕机,也不会影响Arkime核心的抓包和分析功能。
3. 五分钟极速配置实战
接下来,我们进入实操环节。请确保你有一个可以安装Node.js的环境(Linux/Mac/Windows WSL均可)。
3.1 第一步:初始化项目与安装依赖(1分钟)
打开终端,创建一个新的项目目录并进入。
mkdir arkime-websocket-notifier && cd arkime-websocket-notifier
npm init -y
然后安装我们需要的核心依赖:
ws
用于创建WebSocket服务器,
axios
用于向Arkime/Elasticsearch发送HTTP请求,
dotenv
用于管理环境变量。
npm install ws axios dotenv
3.2 第二步:创建WebSocket服务器核心文件(2分钟)
在项目根目录下,创建两个文件:
.env
和
server.js
。
首先,编辑
.env
文件,配置你的环境变量。
请务必根据你的实际环境修改这些值。
# Arkime Viewer API 地址(用于获取最新会话)
ARKIME_API_BASE=http://your-arkime-viewer-ip:8005
# Elasticsearch 地址(用于直接查询,如果API不满足需求)
ES_HOST=http://your-es-ip:9200
# WebSocket 服务器监听的端口
WS_PORT=8080
# 轮询间隔(毫秒),例如5000表示每5秒检查一次新数据
POLL_INTERVAL=5000
# 要监控的Arkime数据库索引模式,通常为 `sessions3-*`
ES_INDEX_PATTERN=sessions3-*
注意 :这里提供了两种数据源选择。Arkime Viewer的API更友好,但可能无法满足非常复杂的过滤条件。Elasticsearch直接查询更强大,但需要你熟悉其查询语法。我们后续以Arkime API为例,因为它更简单通用。
接下来,创建
server.js
,这是服务端的核心逻辑。
const WebSocket = require('ws');
const axios = require('axios');
require('dotenv').config();
const wss = new WebSocket.Server({ port: process.env.WS_PORT || 8080 });
console.log(`WebSocket 服务器已启动,监听端口: ${process.env.WS_PORT || 8080}`);
// 存储所有连接的客户端
let clients = new Set();
// 存储上一次查询到的最新会话时间戳
let lastTimestamp = Date.now() - 60000; // 默认从1分钟前开始查
wss.on('connection', (ws) => {
console.log('新的客户端连接');
clients.add(ws);
ws.on('close', () => {
console.log('客户端断开连接');
clients.delete(ws);
});
// 可以立即发送一条欢迎消息或当前状态
ws.send(JSON.stringify({
type: 'system',
message: '已连接到Arkime实时通知服务',
timestamp: new Date().toISOString()
}));
});
/**
* 轮询Arkime API,检查新的会话
*/
async function pollNewSessions() {
try {
// 构造Arkime的搜索API URL
// 这里我们搜索从 lastTimestamp 之后开始的新会话,并按时间倒序排列,取最前的几个
const query = {
query: {
bool: {
filter: [
{ range: { firstPacket: { gt: lastTimestamp } } }
]
}
},
sort: [{ firstPacket: 'desc' }],
size: 10 // 每次最多取10条新会话,避免数据量过大
};
const response = await axios.post(
`${process.env.ARKIME_API_BASE}/api/sessions?date=-1`,
query,
{ headers: { 'Content-Type': 'application/json' } }
);
const sessions = response.data?.data || [];
if (sessions.length > 0) {
// 更新最后时间戳为最新会话的时间
lastTimestamp = sessions[0].firstPacket;
// 构建推送消息
const notification = {
type: 'new_sessions',
count: sessions.length,
sessions: sessions.map(s => ({
id: s.id,
srcIp: s.srcIp,
dstIp: s.dstIp,
srcPort: s.srcPort,
dstPort: s.dstPort,
protocol: s.protocol,
firstPacket: new Date(s.firstPacket).toLocaleString(),
lastPacket: new Date(s.lastPacket).toLocaleString(),
bytes: s.bytes,
packets: s.packets,
node: s.node
})),
timestamp: new Date().toISOString()
};
// 广播给所有连接的客户端
broadcast(notification);
console.log(`发现 ${sessions.length} 条新会话,已推送。`);
}
} catch (error) {
console.error('轮询Arkime API时出错:', error.message);
}
}
/**
* 广播消息给所有客户端
*/
function broadcast(data) {
const message = JSON.stringify(data);
clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// 启动定时轮询
setInterval(pollNewSessions, process.env.POLL_INTERVAL || 5000);
console.log(`开始轮询,间隔: ${process.env.POLL_INTERVAL || 5000}ms`);
这段代码做了以下几件事:
- 创建了一个WebSocket服务器。
-
维护了一个
clients集合来管理所有活跃连接。 -
定义了一个
pollNewSessions函数,它定期向Arkime的sessionsAPI发送请求,查询自上次检查以来出现的新会话。 -
如果发现新会话,就将其格式化,然后通过
broadcast函数推送给所有客户端。 -
使用
setInterval启动定时轮询。
3.3 第三步:创建测试客户端页面(1分钟)
为了快速验证服务是否工作,我们创建一个简单的HTML客户端。在项目根目录创建
client.html
。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Arkime 实时通知监控</title>
<style>
body { font-family: sans-serif; margin: 20px; }
#messages { border: 1px solid #ccc; padding: 10px; height: 400px; overflow-y: auto; margin-top: 10px; }
.session { border-bottom: 1px dashed #eee; padding: 5px; margin: 5px 0; }
.ip { font-weight: bold; color: #007bff; }
.protocol { background-color: #e9ecef; padding: 2px 5px; border-radius: 3px; font-size: 0.9em; }
</style>
</head>
<body>
<h1>Arkime 实时会话通知</h1>
<div>连接状态: <span id="status">未连接</span></div>
<button onclick="connectWebSocket()">连接</button>
<button onclick="disconnectWebSocket()">断开</button>
<hr>
<div id="messages"></div>
<script>
let ws;
const messagesDiv = document.getElementById('messages');
const statusSpan = document.getElementById('status');
// 修改这里的地址为你的WebSocket服务器地址
const wsUrl = 'ws://localhost:8080';
function connectWebSocket() {
if (ws && ws.readyState === WebSocket.OPEN) {
logMessage('系统', '已经连接了。');
return;
}
ws = new WebSocket(wsUrl);
statusSpan.textContent = '连接中...';
statusSpan.style.color = 'orange';
ws.onopen = () => {
statusSpan.textContent = '已连接';
statusSpan.style.color = 'green';
logMessage('系统', 'WebSocket连接已建立。');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleNotification(data);
};
ws.onerror = (error) => {
statusSpan.textContent = '连接错误';
statusSpan.style.color = 'red';
logMessage('系统', `连接错误: ${error.message}`);
};
ws.onclose = () => {
statusSpan.textContent = '已断开';
statusSpan.style.color = 'gray';
logMessage('系统', 'WebSocket连接已关闭。');
};
}
function disconnectWebSocket() {
if (ws) {
ws.close();
ws = null;
}
}
function handleNotification(notification) {
switch (notification.type) {
case 'system':
logMessage('系统', notification.message);
break;
case 'new_sessions':
logMessage('新会话', `捕获到 ${notification.count} 条新会话:`);
notification.sessions.forEach(session => {
const sessionEl = document.createElement('div');
sessionEl.className = 'session';
sessionEl.innerHTML = `
<strong>${session.firstPacket}</strong><br>
<span class="ip">${session.srcIp}:${session.srcPort}</span> ->
<span class="ip">${session.dstIp}:${session.dstPort}</span>
<span class="protocol">${session.protocol}</span><br>
数据包: ${session.packets}, 字节: ${session.bytes} (节点: ${session.node})
`;
messagesDiv.appendChild(sessionEl);
});
// 自动滚动到底部
messagesDiv.scrollTop = messagesDiv.scrollHeight;
break;
default:
logMessage('未知', JSON.stringify(notification));
}
}
function logMessage(sender, message) {
const msgEl = document.createElement('div');
msgEl.innerHTML = `<strong>[${new Date().toLocaleTimeString()}] ${sender}:</strong> ${message}`;
messagesDiv.appendChild(msgEl);
}
// 页面加载后自动连接(可选)
window.onload = connectWebSocket;
</script>
</body>
</html>
3.4 第四步:启动与测试(1分钟)
-
启动WebSocket服务器 :
node server.js如果看到
WebSocket 服务器已启动,监听端口: 8080和开始轮询,间隔: 5000ms的输出,说明服务端启动成功。 -
启动Arkime流量捕获 :确保你的Arkime Capture节点正在运行并捕获流量。
-
打开客户端页面 :用浏览器直接打开
client.html文件(或通过一个简单的HTTP服务器,如python3 -m http.server 3000,然后访问http://localhost:3000/client.html)。点击“连接”按钮。 -
触发流量并观察 :在你的网络环境中产生一些新的网络流量(例如,访问一个网页,ping一个地址)。等待几秒(不超过轮询间隔5秒),你应该能在网页的“消息”区域看到新的会话信息被实时推送并显示出来。
至此,一个最基础的Arkime实时通知系统就搭建完成了。从创建项目到看到第一条推送,确实可以在5分钟内完成。
4. 从基础到生产:高级配置与优化
上面的例子只是一个起点,它推送了所有新会话。在实际生产环境中,我们需要更精细的控制、更稳定的服务和更丰富的事件类型。
4.1 事件过滤与规则引擎
推送给所有新会话会产生大量噪音。我们需要一个规则引擎来过滤。修改
server.js
中的
pollNewSessions
函数,在构造查询时加入过滤条件。
例如,只推送源IP或目的IP为特定内网网段的会话:
const query = {
query: {
bool: {
filter: [
{ range: { firstPacket: { gt: lastTimestamp } } },
{
bool: {
should: [
{ prefix: { srcIp: '10.0.0.' } }, // 源IP是10.0.0.0/24网段
{ prefix: { dstIp: '192.168.1.' } } // 目的IP是192.168.1.0/24网段
],
minimum_should_match: 1
}
}
]
}
},
sort: [{ firstPacket: 'desc' }],
size: 20
};
更进一步,我们可以将规则配置化。创建一个
rules.json
文件:
[
{
"name": "监控内部服务器对外连接",
"conditions": {
"srcIp": ["10.0.1.10", "10.0.1.11"],
"dstIp_not_in": ["10.0.0.0/8", "192.168.0.0/16"]
},
"action": "notify",
"channel": "critical_alert"
},
{
"name": "检测高危端口扫描",
"conditions": {
"dstPort": [22, 3389, 445],
"packets": { "lt": 3 }
},
"action": "notify",
"channel": "security_warning"
}
]
然后在服务器代码中加载这个规则文件,对查询到的每个会话进行匹配,只有匹配规则的会话才会被推送,并且可以指定推送到不同的“频道”(对应不同的客户端订阅)。
4.2 支持多种事件类型
除了“新会话”,Arkime还有很多其他有价值的事件可以推送:
- 字段值统计变化 :某个特定标签(tags)的会话数突然激增。
- SPI View变化 :用户保存的视图(SPI View)中有新的匹配项。
- 系统状态 :Capture节点离线、磁盘空间不足。
这些事件的获取方式不同。例如,获取字段统计可能需要调用Arkime的
api/stats
接口;监听SPI View可能需要定期查询
api/spiview
。我们可以为每种事件类型编写独立的轮询函数,并统一通过WebSocket服务管理。
在
server.js
中,可以扩展为多个定时任务:
// 引入规则
const rules = require('./rules.json');
// 轮询新会话
setInterval(() => pollAndFilter('new_sessions', rules), POLL_INTERVAL_SESSIONS);
// 轮询系统状态(间隔可以长一些)
setInterval(pollSystemHealth, POLL_INTERVAL_HEALTH);
// 轮询特定SPI View
setInterval(() => pollSpiview('my_watchlist'), POLL_INTERVAL_SPIVIEW);
4.3 连接管理与心跳机制
在生产环境中,需要更健壮的连接管理。
-
心跳保活
:WebSocket连接可能因为网络问题或代理超时而断开。需要在客户端和服务端实现心跳机制(Ping/Pong)。
-
服务端(
server.js)可以定期向每个客户端发送Ping帧。 -
客户端(
client.html中的JavaScript)需要监听onmessage事件,识别Ping帧并回复Pong。
-
服务端(
- 断线重连 :客户端应该监测连接状态,在断开后尝试指数退避重连。
- 连接认证 :在建立WebSocket连接时,可以要求客户端提供Token或进行HTTP Basic认证(WebSocket握手阶段支持HTTP头)。
一个简单的心跳实现可以在服务端添加:
// 在 connection 事件内
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
// 全局定时器,每隔30秒检查一次所有客户端连接
setInterval(() => {
wss.clients.forEach((client) => {
if (client.isAlive === false) {
console.log('终止无响应客户端连接');
return client.terminate();
}
client.isAlive = false;
client.ping(); // 发送Ping帧
});
}, 30000);
4.4 性能优化与扩展
-
使用Elasticsearch PIT+Search After替代Scroll API
:如果直接查询ES,对于大量数据,Scroll API会消耗大量资源。使用Point-in-Time (PIT) 和
search_after参数进行高效的深度分页查询,更适合持续增量获取的场景。 - 消息队列解耦 :当通知规则非常复杂或客户端数量极多时,轮询逻辑和推送逻辑可以解耦。轮询服务将事件投递到消息队列(如Redis Pub/Sub, RabbitMQ, Kafka),再由多个推送服务实例消费队列并处理WebSocket推送。这提高了系统的可扩展性和可靠性。
-
客户端分组(频道/房间)
:不是所有客户端都需要所有消息。可以实现一个简单的“订阅”机制,客户端在连接时可以发送一个
subscribe消息,指定它关心的频道(如security,performance,all),服务端只向订阅了相应频道的客户端推送消息。
5. 常见问题与排查实录
在实际部署和运行中,我遇到并解决了一些典型问题,这里记录下来供你参考。
5.1 问题一:收不到任何推送消息
-
检查点1:WebSocket服务器是否正常运行?
-
运行
netstat -tlnp | grep :8080(Linux) 或Get-NetTCPConnection -LocalPort 8080(Windows PowerShell) 查看端口是否被监听。 - 查看服务器控制台是否有错误日志。
-
运行
-
检查点2:客户端连接是否正确?
-
在浏览器中按F12打开开发者工具,查看“网络”(Network)选项卡,过滤WS类型,检查WebSocket连接状态是否为101 Switching Protocols。如果连接失败,检查
client.html中的wsUrl地址和端口是否正确,以及服务器防火墙是否放行了该端口。
-
在浏览器中按F12打开开发者工具,查看“网络”(Network)选项卡,过滤WS类型,检查WebSocket连接状态是否为101 Switching Protocols。如果连接失败,检查
-
检查点3:轮询逻辑是否获取到数据?
-
在
server.js的pollNewSessions函数中,添加console.log('查询结果:', sessions.length);,查看控制台输出。如果始终为0,可能是lastTimestamp初始值太新,或者查询条件太严格。尝试将lastTimestamp初始化为一个更早的时间(如Date.now() - 300000,5分钟前)。
-
在
-
检查点4:Arkime API是否可访问?
-
在服务器上使用
curl命令测试Arkime API:curl -X POST "http://your-arkime-viewer:8005/api/sessions?date=-1" -H "Content-Type: application/json" -d '{"query":{"match_all":{}}}'。确保能返回JSON数据。
-
在服务器上使用
5.2 问题二:推送延迟高或不稳定
-
可能原因1:轮询间隔(POLL_INTERVAL)设置不当。
- 解决 :根据你对实时性的要求调整。太短(如1秒)会给Arkime API和自身服务带来不必要的压力;太长(如30秒)则延迟明显。对于一般监控,5-10秒是一个平衡点。
-
可能原因2:Arkime Viewer API响应慢。
-
解决
:优化查询。避免使用过于复杂的聚合查询,确保查询的字段有索引。考虑直接使用Elasticsearch API,并确保查询使用了合理的过滤条件,限制返回大小(
size)。
-
解决
:优化查询。避免使用过于复杂的聚合查询,确保查询的字段有索引。考虑直接使用Elasticsearch API,并确保查询使用了合理的过滤条件,限制返回大小(
-
可能原因3:网络问题。
- 解决 :确保WebSocket服务器、Arkime Viewer和客户端之间的网络延迟低且稳定。如果跨地域,需要考虑网络优化。
5.3 问题三:客户端连接频繁断开
-
可能原因1:缺乏心跳机制,被中间网络设备(如负载均衡器、代理)超时断开。
- 解决 :按照前面“连接管理与心跳机制”一节,实现服务端的Ping/Pong保活。
-
可能原因2:客户端页面被浏览器休眠或切到后台。
-
解决
:在客户端代码中,监听
visibilitychange事件,当页面从后台切回前台时,检查WebSocket连接状态,必要时重新连接。
-
解决
:在客户端代码中,监听
-
可能原因3:服务器端资源耗尽。
-
解决
:监控Node.js进程的内存和CPU使用情况。确保及时清理断开的客户端连接(代码中已有
on('close')事件处理)。对于大量连接,考虑使用ws库的permessage-deflate扩展开启压缩,或升级服务器配置。
-
解决
:监控Node.js进程的内存和CPU使用情况。确保及时清理断开的客户端连接(代码中已有
5.4 问题四:如何将通知发送到其他平台(如钉钉、企业微信)?
我们的WebSocket服务推送的是标准化JSON消息。你可以编写一个“桥接”客户端,它连接到WebSocket服务器,收到消息后,再调用第三方平台的API。
例如,一个简单的Python桥接脚本(使用
websockets
库和
requests
库):
import asyncio
import websockets
import json
import requests
async def listen_and_forward():
uri = "ws://your-websocket-server:8080"
async with websockets.connect(uri) as websocket:
while True:
message = await websocket.recv()
data = json.loads(message)
if data.get('type') == 'new_sessions':
# 格式化消息
text = f"Arkime告警:发现{data['count']}条新会话\n"
for s in data['sessions'][:3]: # 只取前3条
text += f"- {s['srcIp']}:{s['srcPort']} -> {s['dstIp']}:{s['dstPort']} ({s['protocol']})\n"
# 调用钉钉机器人
dingtalk_webhook = "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN"
payload = {
"msgtype": "text",
"text": {"content": text}
}
requests.post(dingtalk_webhook, json=payload)
asyncio.run(listen_and_forward())
这个脚本作为一个常驻进程运行,就能把WebSocket通知转化为钉钉群消息。
整个配置过程的核心,在于理解Arkime的数据产出方式,并选择一个轻量、灵活的中介(WebSocket服务)来桥接数据生产者和消费者。五分钟搭建的只是一个原型,但基于这个原型,你可以根据实际监控需求,扩展出非常强大和个性化的实时网络安全态势感知系统。



1277

被折叠的 条评论
为什么被折叠?



