feat: добавлена обработка трафика и интерфейс для его отображения
Refs: None
This commit is contained in:
@@ -26,6 +26,62 @@ const LOG_BUFFER_SIZE = 500;
|
||||
const logBuffer = [];
|
||||
const logSubscribers = new Set();
|
||||
|
||||
const TRAFFIC_BUFFER_SIZE = 500;
|
||||
const trafficBuffer = [];
|
||||
const trafficSubscribers = new Set();
|
||||
|
||||
// Паттерны для парсинга трафика из логов sing-box.
|
||||
// sing-box пишет строки вида:
|
||||
// outbound/direct[tag]: dial tcp connection to host:port from ip:port
|
||||
// [router] matched rule #0 [rule-name], outbound: vpn, domain: example.com
|
||||
// [TCP] DIRECT host:port --> direct
|
||||
const TRAFFIC_OUTBOUND_RE = /outbound[/\\]([a-z0-9_\-]+)|\boutbound:\s*([a-z0-9_\-]+)/i;
|
||||
const TRAFFIC_DEST_RE = /(?:to|dial|connection to|DIRECT|REJECT)\s+(?:tcp\s+|udp\s+)?(?:[^\s]*\s+to\s+)?([a-zA-Z0-9._\-]+|\d{1,3}(?:\.\d{1,3}){3}):(\d{1,5})/i;
|
||||
const TRAFFIC_DOMAIN_RE = /\bdomain:\s*([a-zA-Z0-9._\-]+)/i;
|
||||
const TRAFFIC_RULE_RE = /matched\s+rule\s+#\d+\s*\[([^\]]+)\]|matched\s+\[([^\]]+)\]/i;
|
||||
|
||||
function parseTrafficLine(line) {
|
||||
const clean = line.replace(/\x1b\[\d+m/g, "").trim();
|
||||
|
||||
const obMatch = clean.match(TRAFFIC_OUTBOUND_RE);
|
||||
if (!obMatch) return null;
|
||||
const outboundRaw = (obMatch[1] || obMatch[2] || "").toLowerCase();
|
||||
if (!outboundRaw) return null;
|
||||
|
||||
let category = "other";
|
||||
if (outboundRaw === "direct" || outboundRaw.startsWith("direct")) category = "direct";
|
||||
else if (outboundRaw === "block" || outboundRaw === "reject") category = "block";
|
||||
else if (outboundRaw !== "dns-out" && outboundRaw !== "dns") category = "vpn";
|
||||
else return null; // пропускаем DNS-аутбаунды
|
||||
|
||||
const domainMatch = clean.match(TRAFFIC_DOMAIN_RE);
|
||||
const destMatch = clean.match(TRAFFIC_DEST_RE);
|
||||
|
||||
const host = domainMatch?.[1] || destMatch?.[1] || null;
|
||||
const port = destMatch?.[2] ? parseInt(destMatch[2], 10) : null;
|
||||
if (!host && !port) return null;
|
||||
|
||||
const ruleMatch = clean.match(TRAFFIC_RULE_RE);
|
||||
const matchedRule = ruleMatch?.[1] || ruleMatch?.[2] || null;
|
||||
|
||||
return {
|
||||
ts: new Date().toISOString(),
|
||||
outbound: outboundRaw,
|
||||
category,
|
||||
host: host || "",
|
||||
port,
|
||||
matchedRule,
|
||||
};
|
||||
}
|
||||
|
||||
function pushTrafficEntry(entry) {
|
||||
trafficBuffer.push(entry);
|
||||
if (trafficBuffer.length > TRAFFIC_BUFFER_SIZE) trafficBuffer.shift();
|
||||
for (const sub of trafficSubscribers) {
|
||||
try { sub(entry); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function pushLog(level, line) {
|
||||
const entry = { ts: new Date().toISOString(), level, line };
|
||||
logBuffer.push(entry);
|
||||
@@ -35,6 +91,11 @@ function pushLog(level, line) {
|
||||
subscriber(entry);
|
||||
} catch {}
|
||||
}
|
||||
// Парсим трафик из info/debug строк
|
||||
if (level === "info" || level === "debug") {
|
||||
const traffic = parseTrafficLine(line);
|
||||
if (traffic) pushTrafficEntry(traffic);
|
||||
}
|
||||
}
|
||||
|
||||
// Sing-box пишет все логи в stderr, поэтому парсим уровень из содержимого строки.
|
||||
@@ -401,6 +462,28 @@ async function handleApi(req, res) {
|
||||
return handleLogsStream(req, res);
|
||||
}
|
||||
|
||||
if (req.method === "GET" && req.url === "/api/traffic/stream") {
|
||||
res.writeHead(200, {
|
||||
"content-type": "text/event-stream; charset=utf-8",
|
||||
"cache-control": "no-cache, no-transform",
|
||||
connection: "keep-alive",
|
||||
"x-accel-buffering": "no",
|
||||
});
|
||||
for (const entry of trafficBuffer.slice(-200)) {
|
||||
res.write(`data: ${JSON.stringify(entry)}\n\n`);
|
||||
}
|
||||
const sub = (entry) => res.write(`data: ${JSON.stringify(entry)}\n\n`);
|
||||
trafficSubscribers.add(sub);
|
||||
const keepalive = setInterval(() => { try { res.write(": ping\n\n"); } catch {} }, 15000);
|
||||
req.on("close", () => { clearInterval(keepalive); trafficSubscribers.delete(sub); });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "DELETE" && req.url === "/api/traffic") {
|
||||
trafficBuffer.splice(0);
|
||||
return sendJson(res, 200, { success: true });
|
||||
}
|
||||
|
||||
if (req.method === "GET" && req.url === "/api/rules") {
|
||||
return sendJson(res, 200, {
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user