fix: удален ненужный параметр SING_BOX_CONFIG из конфигурации сервиса
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 6s
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 6s
This commit is contained in:
@@ -20,7 +20,6 @@ services:
|
|||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
DATA_DIR: /var/lib/vpn-proxy
|
DATA_DIR: /var/lib/vpn-proxy
|
||||||
SING_BOX_CONFIG: /etc/sing-box/config.json
|
|
||||||
SING_BOX_CACHE: /var/lib/sing-box/cache.db
|
SING_BOX_CACHE: /var/lib/sing-box/cache.db
|
||||||
volumes:
|
volumes:
|
||||||
- vpn-proxy-data:/var/lib/vpn-proxy
|
- vpn-proxy-data:/var/lib/vpn-proxy
|
||||||
|
|||||||
@@ -31,35 +31,37 @@ const trafficBuffer = [];
|
|||||||
const trafficSubscribers = new Set();
|
const trafficSubscribers = new Set();
|
||||||
|
|
||||||
// Паттерны для парсинга трафика из логов sing-box.
|
// Паттерны для парсинга трафика из логов sing-box.
|
||||||
// sing-box пишет строки вида:
|
// Форматы логов sing-box:
|
||||||
// [router] match[N][rule-name] => outbound/direct[direct]
|
// [TCP] 192.168.1.1:PORT --> example.com:443 outbound/direct[direct]
|
||||||
|
// [UDP] 192.168.1.1:PORT --> 8.8.8.8:53 outbound/direct[direct]
|
||||||
|
// [router] match[N][rule-name] => outbound/direct[tag]
|
||||||
// outbound/direct[tag]: dial tcp connection to host:port
|
// outbound/direct[tag]: dial tcp connection to host:port
|
||||||
// [TCP] DIRECT host:port --> direct
|
|
||||||
const TRAFFIC_OUTBOUND_RE =
|
// Назначение после --> (основной формат sing-box)
|
||||||
/outbound[/\\]([a-z0-9_\-]+)|\boutbound[:\s]+([a-z0-9_\-]+)/i;
|
const DEST_ARROW_RE = /-->\s*([\w.\-]+):(\d{1,5})/;
|
||||||
const TRAFFIC_DEST_RE =
|
// Назначение в старом словесном стиле
|
||||||
/(?:to|dial|connect(?:ion)?\s+to|accepted\s+(?:from\s+[^\s]+\s+to\s+)?)([a-zA-Z0-9._\-]+|\d{1,3}(?:\.\d{1,3}){3}):(\d{1,5})(?!\d)/i;
|
const DEST_WORD_RE =
|
||||||
const TRAFFIC_DOMAIN_RE = /\bdomain:\s*([a-zA-Z0-9._\-]+)/i;
|
/(?:connection\s+to|dial(?:ing)?|connect(?:ing)?\s+to)\s+([\w.\-]+):(\d{1,5})/i;
|
||||||
const TRAFFIC_RULE_RE =
|
// Тип аутбаунда: outbound/TYPE[tag] или outbound/TYPE
|
||||||
/matched?\s+rule\s+#?\d+\s*\[([^\]]+)\]|matched?\s*\[([^\]]+)\]|\busing\s+rule\s*\[([^\]]+)\]/i;
|
const OUTBOUND_RE = /outbound\/([a-z0-9_\-]+)/i;
|
||||||
// Строка роутера: [router] match[N][rule-name] => outbound/direct[tag]
|
// Строка роутера: [router] match[N][rule-name] => outbound/TYPE[tag]
|
||||||
const ROUTER_MATCH_LINE_RE =
|
const ROUTER_MATCH_LINE_RE =
|
||||||
/\[router\].*\bmatch\[\d+\]\[([^\]]+)\].*outbound[/\\]([a-z0-9_\-]+)/i;
|
/\[router\].*\bmatch\[\d+\]\[([^\]]+)\].*outbound\/([a-z0-9_\-]+)/i;
|
||||||
|
|
||||||
// Хранит имя последнего правила из [router] строки (для следующей строки с dest)
|
// Хранит имя последнего правила из [router] строки (для следующей строки с dest)
|
||||||
let _pendingRuleName = null;
|
let _pendingRuleName = null;
|
||||||
let _pendingRuleAt = 0;
|
let _pendingRuleAt = 0;
|
||||||
const RULE_CONTEXT_TTL_MS = 300;
|
const RULE_CONTEXT_TTL_MS = 500;
|
||||||
|
|
||||||
function parseTrafficLine(line) {
|
function parseTrafficLine(line) {
|
||||||
const clean = line.replace(/\x1b\[\d+m/g, "").trim();
|
const clean = line.replace(/\x1b\[\d+m/g, "").trim();
|
||||||
|
|
||||||
// Детектируем строку роутера — она содержит имя правила и outbound, но не host:port
|
// Детектируем строку роутера — содержит правило, но не dest
|
||||||
const routerM = clean.match(ROUTER_MATCH_LINE_RE);
|
const routerM = clean.match(ROUTER_MATCH_LINE_RE);
|
||||||
if (routerM) {
|
if (routerM) {
|
||||||
_pendingRuleName = routerM[1];
|
_pendingRuleName = routerM[1];
|
||||||
_pendingRuleAt = Date.now();
|
_pendingRuleAt = Date.now();
|
||||||
return null; // не выводим отдельную запись в трафик
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Берём накопленное имя правила, если свежее
|
// Берём накопленное имя правила, если свежее
|
||||||
@@ -70,37 +72,35 @@ function parseTrafficLine(line) {
|
|||||||
_pendingRuleName = null;
|
_pendingRuleName = null;
|
||||||
_pendingRuleAt = 0;
|
_pendingRuleAt = 0;
|
||||||
|
|
||||||
const obMatch = clean.match(TRAFFIC_OUTBOUND_RE);
|
// Ищем outbound
|
||||||
if (!obMatch) return null;
|
const obM = clean.match(OUTBOUND_RE);
|
||||||
const outboundRaw = (obMatch[1] || obMatch[2] || "").toLowerCase();
|
if (!obM) return null;
|
||||||
if (!outboundRaw) return null;
|
const outboundRaw = obM[1].toLowerCase();
|
||||||
|
|
||||||
let category = "other";
|
// Пропускаем DNS-аутбаунды
|
||||||
if (outboundRaw === "direct" || outboundRaw.startsWith("direct"))
|
if (outboundRaw === "dns-out" || outboundRaw === "dns") return null;
|
||||||
|
|
||||||
|
let category;
|
||||||
|
if (outboundRaw === "direct" || outboundRaw.startsWith("direct-"))
|
||||||
category = "direct";
|
category = "direct";
|
||||||
else if (outboundRaw === "block" || outboundRaw === "reject")
|
else if (outboundRaw === "block" || outboundRaw === "reject")
|
||||||
category = "block";
|
category = "block";
|
||||||
else if (outboundRaw !== "dns-out" && outboundRaw !== "dns") category = "vpn";
|
else category = "vpn";
|
||||||
else return null; // пропускаем DNS-аутбаунды
|
|
||||||
|
|
||||||
const domainMatch = clean.match(TRAFFIC_DOMAIN_RE);
|
// Ищем назначение: --> (основной формат), потом словесный
|
||||||
const destMatch = clean.match(TRAFFIC_DEST_RE);
|
const destM = clean.match(DEST_ARROW_RE) || clean.match(DEST_WORD_RE);
|
||||||
|
if (!destM) return null;
|
||||||
|
|
||||||
const host = domainMatch?.[1] || destMatch?.[1] || null;
|
const host = destM[1];
|
||||||
const port = destMatch?.[2] ? parseInt(destMatch[2], 10) : null;
|
const port = parseInt(destM[2], 10);
|
||||||
if (!host && !port) return null;
|
|
||||||
|
|
||||||
const ruleMatch = clean.match(TRAFFIC_RULE_RE);
|
|
||||||
const matchedRule =
|
|
||||||
ruleMatch?.[1] || ruleMatch?.[2] || ruleMatch?.[3] || inheritedRule || null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ts: new Date().toISOString(),
|
ts: new Date().toISOString(),
|
||||||
outbound: outboundRaw,
|
outbound: outboundRaw,
|
||||||
category,
|
category,
|
||||||
host: host || "",
|
host,
|
||||||
port,
|
port,
|
||||||
matchedRule,
|
matchedRule: inheritedRule,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,9 +126,22 @@ function pushLog(level, line) {
|
|||||||
// Парсим трафик из info/debug строк
|
// Парсим трафик из info/debug строк
|
||||||
if (level === "info" || level === "debug") {
|
if (level === "info" || level === "debug") {
|
||||||
const traffic = parseTrafficLine(line);
|
const traffic = parseTrafficLine(line);
|
||||||
if (traffic) pushTrafficEntry(traffic);
|
if (traffic) {
|
||||||
|
pushTrafficEntry(traffic);
|
||||||
|
} else if (
|
||||||
|
(level === "info" || level === "debug") &&
|
||||||
|
_debugUnparsed < 30 &&
|
||||||
|
/\bTCP\b|\bUDP\b|\boutbound\b/i.test(line.replace(/\x1b\[\d+m/g, ""))
|
||||||
|
) {
|
||||||
|
_debugUnparsed++;
|
||||||
|
process.stdout.write(
|
||||||
|
`[traffic:unmatched] ${line.replace(/\x1b\[\d+m/g, "").trim()}\n`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _debugUnparsed = 0;
|
||||||
|
|
||||||
// Sing-box пишет все логи в stderr, поэтому парсим уровень из содержимого строки.
|
// Sing-box пишет все логи в stderr, поэтому парсим уровень из содержимого строки.
|
||||||
// Формат: ESC[<n>m LEVEL ESC[0m, где ESC = \x1b
|
// Формат: ESC[<n>m LEVEL ESC[0m, где ESC = \x1b
|
||||||
@@ -1068,6 +1081,33 @@ if (existingPid && isPidAlive(existingPid)) {
|
|||||||
attachExistingSingbox(existingPid);
|
attachExistingSingbox(existingPid);
|
||||||
} else {
|
} else {
|
||||||
removeSingboxPid();
|
removeSingboxPid();
|
||||||
|
|
||||||
|
// Если конфиг отсутствует (например после передплоя), пробуем пересобрать из кэша
|
||||||
|
if (!fs.existsSync(settings.configPath)) {
|
||||||
|
const stateData = readJson(settings.statePath, {});
|
||||||
|
const cached = readJson(settings.subscriptionCachePath, null);
|
||||||
|
if (stateData.selectedTag && cached?.config) {
|
||||||
|
try {
|
||||||
|
const customRules = readJson(settings.customRulesPath, []);
|
||||||
|
const generated = buildGatewayConfig(
|
||||||
|
{ ...cached.config, customRules },
|
||||||
|
stateData.selectedTag,
|
||||||
|
{ bypassAll: Boolean(stateData.bypassMode) },
|
||||||
|
);
|
||||||
|
writeSingboxConfig(generated);
|
||||||
|
pushLog(
|
||||||
|
"info",
|
||||||
|
`Конфиг sing-box восстановлен из кэша (сервер: ${stateData.selectedTag})`,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
pushLog(
|
||||||
|
"error",
|
||||||
|
`Не удалось восстановить конфиг sing-box: ${err.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await startSingbox().catch((error) => {
|
await startSingbox().catch((error) => {
|
||||||
console.warn(`[control] sing-box не запущен: ${error.message}`);
|
console.warn(`[control] sing-box не запущен: ${error.message}`);
|
||||||
pushLog("error", `sing-box не запущен при старте: ${error.message}`);
|
pushLog("error", `sing-box не запущен при старте: ${error.message}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user