feat: добавлена возможность обхода правил для трафика
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 19s
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 19s
- Реализована функция для включения и отключения обхода правил. - Обновлены компоненты интерфейса для управления режимом обхода. - Добавлена обработка состояния обхода в API. Refs: None
This commit is contained in:
@@ -32,20 +32,44 @@ 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
|
||||
// [router] match[N][rule-name] => outbound/direct[direct]
|
||||
// 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;
|
||||
/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;
|
||||
/(?: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+\[([^\]]+)\]/i;
|
||||
/matched?\s+rule\s+#?\d+\s*\[([^\]]+)\]|matched?\s*\[([^\]]+)\]|\busing\s+rule\s*\[([^\]]+)\]/i;
|
||||
// Строка роутера: [router] match[N][rule-name] => outbound/direct[tag]
|
||||
const ROUTER_MATCH_LINE_RE =
|
||||
/\[router\].*\bmatch\[\d+\]\[([^\]]+)\].*outbound[/\\]([a-z0-9_\-]+)/i;
|
||||
|
||||
// Хранит имя последнего правила из [router] строки (для следующей строки с dest)
|
||||
let _pendingRuleName = null;
|
||||
let _pendingRuleAt = 0;
|
||||
const RULE_CONTEXT_TTL_MS = 300;
|
||||
|
||||
function parseTrafficLine(line) {
|
||||
const clean = line.replace(/\x1b\[\d+m/g, "").trim();
|
||||
|
||||
// Детектируем строку роутера — она содержит имя правила и outbound, но не host:port
|
||||
const routerM = clean.match(ROUTER_MATCH_LINE_RE);
|
||||
if (routerM) {
|
||||
_pendingRuleName = routerM[1];
|
||||
_pendingRuleAt = Date.now();
|
||||
return null; // не выводим отдельную запись в трафик
|
||||
}
|
||||
|
||||
// Берём накопленное имя правила, если свежее
|
||||
let inheritedRule = null;
|
||||
if (_pendingRuleName && Date.now() - _pendingRuleAt < RULE_CONTEXT_TTL_MS) {
|
||||
inheritedRule = _pendingRuleName;
|
||||
}
|
||||
_pendingRuleName = null;
|
||||
_pendingRuleAt = 0;
|
||||
|
||||
const obMatch = clean.match(TRAFFIC_OUTBOUND_RE);
|
||||
if (!obMatch) return null;
|
||||
const outboundRaw = (obMatch[1] || obMatch[2] || "").toLowerCase();
|
||||
@@ -67,7 +91,8 @@ function parseTrafficLine(line) {
|
||||
if (!host && !port) return null;
|
||||
|
||||
const ruleMatch = clean.match(TRAFFIC_RULE_RE);
|
||||
const matchedRule = ruleMatch?.[1] || ruleMatch?.[2] || null;
|
||||
const matchedRule =
|
||||
ruleMatch?.[1] || ruleMatch?.[2] || ruleMatch?.[3] || inheritedRule || null;
|
||||
|
||||
return {
|
||||
ts: new Date().toISOString(),
|
||||
@@ -347,6 +372,7 @@ function publicState() {
|
||||
appliedHistory: state.appliedHistory || [],
|
||||
rulesUpdatedAt: state.rulesUpdatedAt || null,
|
||||
rulesAppliedAt: state.rulesAppliedAt || null,
|
||||
bypassMode: Boolean(state.bypassMode),
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
@@ -391,9 +417,11 @@ async function applySelectedServer(selectedTag) {
|
||||
}
|
||||
|
||||
const customRules = readJson(settings.customRulesPath, []);
|
||||
const stateForBypass = readJson(settings.statePath, {});
|
||||
const generated = buildGatewayConfig(
|
||||
{ ...cached.config, customRules },
|
||||
selectedTag,
|
||||
{ bypassAll: Boolean(stateForBypass.bypassMode) },
|
||||
);
|
||||
writeSingboxConfig(generated);
|
||||
await startSingbox();
|
||||
@@ -498,6 +526,35 @@ async function handleApi(req, res) {
|
||||
return sendJson(res, 200, { success: true });
|
||||
}
|
||||
|
||||
if (req.method === "POST" && req.url === "/api/bypass") {
|
||||
const body = await readBody(req);
|
||||
const enabled = Boolean(body.enabled);
|
||||
const prevState = readJson(settings.statePath, {});
|
||||
writeJson(settings.statePath, { ...prevState, bypassMode: enabled });
|
||||
|
||||
// Перегенерируем и применяем конфиг, если sing-box запущен
|
||||
if (singboxProcess && prevState.selectedTag) {
|
||||
const cached = readJson(settings.subscriptionCachePath, null);
|
||||
if (cached?.config) {
|
||||
const customRules = readJson(settings.customRulesPath, []);
|
||||
const generated = buildGatewayConfig(
|
||||
{ ...cached.config, customRules },
|
||||
prevState.selectedTag,
|
||||
{ bypassAll: enabled },
|
||||
);
|
||||
writeSingboxConfig(generated);
|
||||
await startSingbox();
|
||||
pushLog(
|
||||
"info",
|
||||
enabled
|
||||
? "Режим обхода включён — весь трафик идёт напрямую"
|
||||
: "Режим обхода отключён — правила маршрутизации восстановлены",
|
||||
);
|
||||
}
|
||||
}
|
||||
return sendJson(res, 200, { success: true, bypassMode: enabled });
|
||||
}
|
||||
|
||||
if (req.method === "GET" && req.url === "/api/rules") {
|
||||
return sendJson(res, 200, {
|
||||
success: true,
|
||||
|
||||
@@ -166,7 +166,11 @@ function routeRules(customRules, vpnTag) {
|
||||
return rules;
|
||||
}
|
||||
|
||||
export function buildGatewayConfig(subscriptionConfig, selectedTag) {
|
||||
export function buildGatewayConfig(
|
||||
subscriptionConfig,
|
||||
selectedTag,
|
||||
{ bypassAll = false } = {},
|
||||
) {
|
||||
const selectedOutbound = findOutbound(subscriptionConfig, selectedTag);
|
||||
if (!selectedOutbound) {
|
||||
throw new Error(`Outbound не найден: ${selectedTag}`);
|
||||
@@ -218,9 +222,11 @@ export function buildGatewayConfig(subscriptionConfig, selectedTag) {
|
||||
{ type: "block", tag: "block" },
|
||||
],
|
||||
route: {
|
||||
rule_set: ruleSets(customRuleSets),
|
||||
rules: routeRules(subscriptionConfig.customRules, vpnOutbound.tag),
|
||||
final: vpnOutbound.tag,
|
||||
rule_set: bypassAll ? [] : ruleSets(customRuleSets),
|
||||
rules: bypassAll
|
||||
? [{ ip_is_private: true, outbound: "direct" }]
|
||||
: routeRules(subscriptionConfig.customRules, vpnOutbound.tag),
|
||||
final: bypassAll ? "direct" : vpnOutbound.tag,
|
||||
auto_detect_interface: true,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user