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
|
||||
environment:
|
||||
DATA_DIR: /var/lib/vpn-proxy
|
||||
SING_BOX_CONFIG: /etc/sing-box/config.json
|
||||
SING_BOX_CACHE: /var/lib/sing-box/cache.db
|
||||
volumes:
|
||||
- vpn-proxy-data:/var/lib/vpn-proxy
|
||||
|
||||
@@ -31,35 +31,37 @@ const trafficBuffer = [];
|
||||
const trafficSubscribers = new Set();
|
||||
|
||||
// Паттерны для парсинга трафика из логов sing-box.
|
||||
// sing-box пишет строки вида:
|
||||
// [router] match[N][rule-name] => outbound/direct[direct]
|
||||
// Форматы логов sing-box:
|
||||
// [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
|
||||
// [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|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 TRAFFIC_DOMAIN_RE = /\bdomain:\s*([a-zA-Z0-9._\-]+)/i;
|
||||
const TRAFFIC_RULE_RE =
|
||||
/matched?\s+rule\s+#?\d+\s*\[([^\]]+)\]|matched?\s*\[([^\]]+)\]|\busing\s+rule\s*\[([^\]]+)\]/i;
|
||||
// Строка роутера: [router] match[N][rule-name] => outbound/direct[tag]
|
||||
|
||||
// Назначение после --> (основной формат 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
|
||||
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;
|
||||
/\[router\].*\bmatch\[\d+\]\[([^\]]+)\].*outbound\/([a-z0-9_\-]+)/i;
|
||||
|
||||
// Хранит имя последнего правила из [router] строки (для следующей строки с dest)
|
||||
let _pendingRuleName = null;
|
||||
let _pendingRuleAt = 0;
|
||||
const RULE_CONTEXT_TTL_MS = 300;
|
||||
const RULE_CONTEXT_TTL_MS = 500;
|
||||
|
||||
function parseTrafficLine(line) {
|
||||
const clean = line.replace(/\x1b\[\d+m/g, "").trim();
|
||||
|
||||
// Детектируем строку роутера — она содержит имя правила и outbound, но не host:port
|
||||
// Детектируем строку роутера — содержит правило, но не dest
|
||||
const routerM = clean.match(ROUTER_MATCH_LINE_RE);
|
||||
if (routerM) {
|
||||
_pendingRuleName = routerM[1];
|
||||
_pendingRuleAt = Date.now();
|
||||
return null; // не выводим отдельную запись в трафик
|
||||
return null;
|
||||
}
|
||||
|
||||
// Берём накопленное имя правила, если свежее
|
||||
@@ -70,37 +72,35 @@ function parseTrafficLine(line) {
|
||||
_pendingRuleName = null;
|
||||
_pendingRuleAt = 0;
|
||||
|
||||
const obMatch = clean.match(TRAFFIC_OUTBOUND_RE);
|
||||
if (!obMatch) return null;
|
||||
const outboundRaw = (obMatch[1] || obMatch[2] || "").toLowerCase();
|
||||
if (!outboundRaw) return null;
|
||||
// Ищем outbound
|
||||
const obM = clean.match(OUTBOUND_RE);
|
||||
if (!obM) return null;
|
||||
const outboundRaw = obM[1].toLowerCase();
|
||||
|
||||
let category = "other";
|
||||
if (outboundRaw === "direct" || outboundRaw.startsWith("direct"))
|
||||
// Пропускаем DNS-аутбаунды
|
||||
if (outboundRaw === "dns-out" || outboundRaw === "dns") return null;
|
||||
|
||||
let category;
|
||||
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-аутбаунды
|
||||
else category = "vpn";
|
||||
|
||||
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 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] || ruleMatch?.[3] || inheritedRule || null;
|
||||
const host = destM[1];
|
||||
const port = parseInt(destM[2], 10);
|
||||
|
||||
return {
|
||||
ts: new Date().toISOString(),
|
||||
outbound: outboundRaw,
|
||||
category,
|
||||
host: host || "",
|
||||
host,
|
||||
port,
|
||||
matchedRule,
|
||||
matchedRule: inheritedRule,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,10 +126,23 @@ function pushLog(level, line) {
|
||||
// Парсим трафик из info/debug строк
|
||||
if (level === "info" || level === "debug") {
|
||||
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, поэтому парсим уровень из содержимого строки.
|
||||
// Формат: ESC[<n>m LEVEL ESC[0m, где ESC = \x1b
|
||||
const SINGBOX_LEVEL_RE =
|
||||
@@ -1068,6 +1081,33 @@ if (existingPid && isPidAlive(existingPid)) {
|
||||
attachExistingSingbox(existingPid);
|
||||
} else {
|
||||
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) => {
|
||||
console.warn(`[control] sing-box не запущен: ${error.message}`);
|
||||
pushLog("error", `sing-box не запущен при старте: ${error.message}`);
|
||||
|
||||
Reference in New Issue
Block a user