From 6ab5f50f95ecded594a113756d35126569fe9b0b Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Sat, 9 May 2026 09:24:34 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D1=83=D1=81=D1=82=D1=80=D0=BE=D0=B9=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B0=20=D0=B2=20=D0=B6=D1=83=D1=80=D0=BD=D0=B0?= =?UTF-8?q?=D0=BB=D0=B5=20=D1=82=D1=80=D0=B0=D1=84=D0=B8=D0=BA=D0=B0=20Ref?= =?UTF-8?q?s:=20None?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/index.js | 47 ++++++++++++++++++++++++++++++--- src/web/App.jsx | 2 +- src/web/components/LogsPage.jsx | 33 +++++++++++++++++++---- 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/src/server/index.js b/src/server/index.js index b87d294..77ec491 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -100,9 +100,9 @@ const trafficSubscribers = new Set(); // [router] match[N][rule-name] => outbound/direct[tag] // outbound/direct[tag]: dial tcp connection to host:port -// Назначение после --> (основной формат sing-box) +// Назначение после --> (старый формат sing-box) const DEST_ARROW_RE = /-->\s*([\w.\-]+):(\d{1,5})/; -// Назначение в старом словесном стиле +// Назначение в словесном стиле const DEST_WORD_RE = /(?:connection\s+to|dial(?:ing)?|connect(?:ing)?\s+to)\s+([\w.\-]+):(\d{1,5})/i; // Тип аутбаунда: outbound/TYPE[tag] или outbound/TYPE @@ -110,6 +110,21 @@ const OUTBOUND_RE = /outbound\/([a-z0-9_\-]+)/i; // Строка роутера: [router] match[N][rule-name] => outbound/TYPE[tag] const ROUTER_MATCH_LINE_RE = /\[router\].*\bmatch\[\d+\]\[([^\]]+)\].*outbound\/([a-z0-9_\-]+)/i; +// ID соединения: [CONN_ID Nms] +const CONN_ID_RE = /\[(\d{5,12})\s+\d+ms\]/; +// Входящее соединение от устройства: inbound [packet] connection from IP:PORT +const INBOUND_FROM_RE = /inbound(?:\s+packet)?\s+connection\s+from\s+([\d.]+):\d+/i; +// Source IP из --> формата: IP:PORT --> +const SOURCE_ARROW_RE = /\b([\d.]+):\d+\s+-->/; +// Карта source IP по ID соединения +const CONN_TTL_MS = 10_000; +const connSourceMap = new Map(); +setInterval(() => { + const now = Date.now(); + for (const [id, v] of connSourceMap) { + if (now - v.at > CONN_TTL_MS) connSourceMap.delete(id); + } +}, 30_000); // Хранит имя последнего правила из [router] строки (для следующей строки с dest) let _pendingRuleName = null; @@ -117,7 +132,8 @@ let _pendingRuleAt = 0; const RULE_CONTEXT_TTL_MS = 500; function parseTrafficLine(line) { - const clean = line.replace(/\x1b\[\d+m/g, "").trim(); + // Расширенная очистка ANSI (включая многопараметрические: \x1b[38;5;Nm) + const clean = line.replace(/\x1b\[[0-9;]*m/g, "").trim(); // Детектируем строку роутера — содержит правило, но не dest const routerM = clean.match(ROUTER_MATCH_LINE_RE); @@ -127,6 +143,17 @@ function parseTrafficLine(line) { return null; } + // Извлекаем ID соединения для корреляции + const connM = clean.match(CONN_ID_RE); + const connId = connM ? connM[1] : null; + + // Строка "inbound connection from IP:PORT" — сохраняем source IP и выходим + const inboundFromM = clean.match(INBOUND_FROM_RE); + if (inboundFromM) { + if (connId) connSourceMap.set(connId, { sourceIp: inboundFromM[1], at: Date.now() }); + return null; + } + // Берём накопленное имя правила, если свежее let inheritedRule = null; if (_pendingRuleName && Date.now() - _pendingRuleAt < RULE_CONTEXT_TTL_MS) { @@ -150,19 +177,31 @@ function parseTrafficLine(line) { category = "block"; else category = "vpn"; - // Ищем назначение: --> (основной формат), потом словесный + // Ищем назначение: --> (старый формат), потом словесный (inbound/outbound connection to) const destM = clean.match(DEST_ARROW_RE) || clean.match(DEST_WORD_RE); if (!destM) return null; const host = destM[1]; const port = parseInt(destM[2], 10); + // Source IP: из корреляционной карты (новый формат) или из --> (старый формат) + let sourceIp = null; + if (connId) { + const stored = connSourceMap.get(connId); + if (stored && Date.now() - stored.at < CONN_TTL_MS) sourceIp = stored.sourceIp; + } + if (!sourceIp) { + const srcM = clean.match(SOURCE_ARROW_RE); + if (srcM) sourceIp = srcM[1]; + } + return { ts: new Date().toISOString(), outbound: outboundRaw, category, host, port, + sourceIp, matchedRule: inheritedRule, }; } diff --git a/src/web/App.jsx b/src/web/App.jsx index 190b677..aa4de3f 100644 --- a/src/web/App.jsx +++ b/src/web/App.jsx @@ -397,7 +397,7 @@ function App() { onRemoveDevice={removeDevice} /> )} - {page === 'logs' && } + {page === 'logs' && } {page === 'settings' && ( b._lastTs - a._lastTs); } -function TrafficTab() { +function TrafficTab({ deviceRules = [] }) { const [traffic, setTraffic] = useState([]); const [paused, setPaused] = useState(false); const [filter, setFilter] = useState('all'); // all | direct | vpn | block @@ -90,7 +102,9 @@ function TrafficTab() { e.host?.toLowerCase().includes(s) || String(e.port || '').includes(s) || e.outbound?.toLowerCase().includes(s) || - e.matchedRule?.toLowerCase().includes(s), + e.matchedRule?.toLowerCase().includes(s) || + e.sourceIp?.toLowerCase().includes(s) || + getDeviceName(e.sourceIp, deviceRules)?.toLowerCase().includes(s), ); } return grouped ? groupTraffic(list, sortBy) : list; @@ -159,6 +173,7 @@ function TrafficTab() { Время Туннель + Устройство Хост / IP Порт Правило @@ -168,12 +183,20 @@ function TrafficTab() { {filtered.map((e, i) => { const badge = CATEGORY_BADGE[e.category] || CATEGORY_BADGE.other; + const deviceName = getDeviceName(e.sourceIp, deviceRules); return ( {formatTime(e.ts)} {badge.label} + + {deviceName + ? {deviceName} + : e.sourceIp + ? {e.sourceIp} + : } + {e.host || '—'} {e.port || '—'} @@ -195,7 +218,7 @@ function TrafficTab() { ); } -export function LogsPage() { +export function LogsPage({ deviceRules = [] }) { const [tab, setTab] = useState('traffic'); // traffic | logs const [entries, setEntries] = useState([]); const [paused, setPaused] = useState(false); @@ -253,7 +276,7 @@ export function LogsPage() { - {tab === 'traffic' && } + {tab === 'traffic' && } {tab === 'logs' && ( <>