系統目標
- 即時接收 Snort IDS 警報 (Unix Domain Socket)
- 解析攻擊源 IP、攻擊類型、嚴重度
- 自動下發 iptables 規則封鎖來源 IP (可配置封鎖時長)
- 提供 PHP Web 管理介面:查看封鎖列表、手動解封、調整策略
- 高併發、高可靠:服務進程獨立於 Snort,採用多線程/多進程模型
整體架構
+----------------+ Unix Socket +------------------+ iptables +----------+
| Snort IDS | ───────────────────▶ | PHP Daemon | ────────────────▶ | Kernel |
| (Alert Output)| Alertpkt 結構體 | (AO_Handler) | ipset/iptables | Netfilter|
+----------------+ | (Block_Handler) | +----------+
| (Web_Handler) | ▲
+------------------+ │
▲ │
│ HTTP/JSON │
▼ │
+------------------+
| PHP Web UI |
| (Admin Panel) |
+------------------+
核心資料結構 (C 風格對應 PHP 陣列)
// Alertpkt 關鍵欄位 (Snort spo_alert_unixsock.h)
$alert = [
'msg' => 'ATTACK-RESPONSES id check returned root', // 警報訊息
'pkt' => [ ... ], // pcap_pkthdr + 原始封包
'event' => [
'sid' => 1234, // 規則 SID
'gid' => 1,
'rev' => 5,
'classification' => 2, // 攻擊分類
'priority' => 1, // 優先級
'timestamp' => 1700000000,
],
'ip_src' => '192.168.1.100',
'ip_dst' => '10.0.0.50',
'port_src' => 54321,
'port_dst' => 80,
'proto' => 6, // TCP
];
PHP Daemon 關鍵代碼骨架 (Swoole 4.8+ 協程版)
<?php
// daemon.php
use Swoole\Process;
use Swoole\Coroutine;
use Swoole\Coroutine\Socket;
$snortSockPath = '/var/log/snort_alert';
$adminSockPath = '/var/run/ids_admin.sock';
$blockTTL = 3600; // 秒
// 共享記憶體表:封鎖列表 IP => 過期時間戳
$blockTable = new Swoole\Table(1024);
$blockTable->column('expire', Swoole\Table::TYPE_INT);
$blockTable->create();
// ---------- 1. Snort 警報接收協程 ----------
Coroutine::create(function () use ($snortSockPath, $blockTable, $blockTTL) {
$sock = new Socket(AF_UNIX, SOCK_DGRAM, 0);
$sock->bind($snortSockPath);
echo "[AO_Handler] Listening on $snortSockPath\n";
while (true) {
$data = $sock->recv(65535);
if ($data === false) continue;
$alert = parseAlertpkt($data); // 解析二進制結構體
if (!$alert) continue;
$ip = $alert['ip_src'];
// 白名單檢查
if (in_array($ip, ['10.0.0.1', '127.0.0.1'])) continue;
// 原子檢查並加入封鎖表
if (!$blockTable->exist($ip)) {
$expire = time() + $blockTTL;
$blockTable->set($ip, ['expire' => $expire]);
// 下發 iptables 規則 (非阻塞)
exec("ipset add blacklist $ip timeout $blockTTL 2>/dev/null || iptables -I INPUT -s $ip -j DROP");
syslog(LOG_INFO, "[IDS] Blocked $ip for {$blockTTL}s: {$alert['msg']}");
}
}
});
// ---------- 2. 封鎖過期清理協程 ----------
Coroutine::create(function () use ($blockTable) {
while (true) {
Coroutine::sleep(60);
foreach ($blockTable as $ip => $row) {
if ($row['expire'] < time()) {
$blockTable->del($ip);
exec("ipset del blacklist $ip 2>/dev/null || iptables -D INPUT -s $ip -j DROP");
syslog(LOG_INFO, "[IDS] Unblocked expired $ip");
}
}
}
});
// ---------- 3. Web 管理 API (HTTP Server) ----------
$http = new Swoole\Http\Server('0.0.0.0', 9501);
$http->on('Request', function ($req, $resp) use ($blockTable) {
$resp->header('Content-Type', 'application/json');
$path = $req->server['request_uri'];
if (preg_match('#^/api/blocks$#', $path)) {
$list = [];
foreach ($blockTable as $ip => $row) {
$list[] = ['ip' => $ip, 'expire' => $row['expire'], 'remaining' => max(0, $row['expire'] - time())];
}
$resp->end(json_encode(['blocks' => $list]));
} elseif (preg_match('#^/api/unblock/(\d+\.\d+\.\d+\.\d+)$#', $path, $m)) {
$ip = $m[1];
if ($blockTable->exist($ip)) {
$blockTable->del($ip);
exec("ipset del blacklist $ip 2>/dev/null || iptables -D INPUT -s $ip -j DROP");
$resp->end(json_encode(['success' => true]));
} else {
$resp->status(404);
$resp->end(json_encode(['error' => 'Not blocked']));
}
} else {
$resp->status(404);
$resp->end(json_encode(['error' => 'Not found']));
}
});
$http->start();
function parseAlertpkt(string $data): ?array {
// 實際需根據 Snort 版本 unpack Alertpkt 結構體
// 此處略過二進制解析,假設返回關鍵陣列
return [
'msg' => '...',
'ip_src' => '192.168.1.' . random_int(1, 254),
// ...
];
}
關鍵技術點解析
| 模組 | 技術選型 | 理由 |
| Snort 輸出 | output alert_unixsock | 低延遲、無磁盤 I/O、易於程式讀取 |
| 封鎖載體 | ipset + iptables | ipset 批量操作 O(1)、支援 timeout 自動過期、效能優於純 iptables |
| Daemon 框架 | Swoole 協程 | 單進程高併發、原生 Unix Socket/HTTP Server、共享記憶體表免鎖 |
| 資料同步 | Swoole\Table | 進程間無鎖讀寫、自動過期掃描 |
| 權限 | Daemon 需 root (iptables) / Web UI 以 www-data 通過 Unix Socket 與 Daemon 通訊 | 最小權限原則 |
Web 管理介面 (Vue 3 + Axios 片段)
<template>
<div class="container">
<h2>當前封鎖列表 (<span class="badge">{{ blocks.length }}</span>)</h2>
<table class="table">
<thead><tr><th>IP</th><th>剩餘秒數</th><th>操作</th></tr></thead>
<tbody>
<tr v-for="b in blocks" :key="b.ip">
<td>{{ b.ip }}</td>
<td>{{ b.remaining }}s</td>
<td><button @click="unblock(b.ip)" class="btn btn-danger btn-sm">解封</button></td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
const blocks = ref([])
async function load() { blocks.value = (await axios.get('/api/blocks')).data.blocks }
async function unblock(ip) { await axios.post('/api/unblock/' + ip); load() }
onMounted(() => { load(); setInterval(load, 5000) })
</script>
部署與運維清單
- Snort 配置:`output alert_unixsock` → 重啟 Snort。
- 建立 ipset:`ipset create blacklist hash:ip timeout 3600`;`iptables -I INPUT -m set --match-set blacklist src -j DROP`。
- Daemon 以 systemd 服務運行 (User=root, Capability=CAP_NET_ADMIN)。
- Web UI 反向代理 Nginx + HTTPS + Basic Auth / OAuth2 Proxy。
- 日誌輪轉:`/var/log/ids_daemon.log` 配置 logrotate。
- 監控:Prometheus 抓取 `/metrics` (Swoole 內建) + Grafana 儀表板 (封鎖數、警報率、CPU/記憶體)。