feat: добавлена поддержка кэша прямого обхода с использованием ipset
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 19s

Refs: None
This commit is contained in:
2026-05-08 22:27:58 +03:00
parent 781cbbb026
commit 5c9a291920
6 changed files with 130 additions and 4 deletions

View File

@@ -21,6 +21,68 @@ fs.mkdirSync(settings.dataDir, { recursive: true });
const SINGBOX_PID_FILE = path.join(settings.dataDir, "singbox.pid");
// ─── Direct bypass cache (ipset) ────────────────────────────────────────────
const DIRECT_BYPASS_SET = process.env.DIRECT_BYPASS_SET || "vpn_direct_bypass";
const DIRECT_BYPASS_TTL = process.env.DIRECT_BYPASS_TTL || "3600";
const IPSET_AVAILABLE = (() => {
try {
spawnSync("ipset", ["version"], { timeout: 1000 });
return true;
} catch {
return false;
}
})();
const IP_RE = /^\d{1,3}(?:\.\d{1,3}){3}$/;
// Локальный счётчик добавленных IP (RAM-only, сбрасывается при перезапуске)
let directBypassCount = 0;
function addToDirectBypass(ip) {
if (!IPSET_AVAILABLE || !IP_RE.test(ip)) return;
try {
spawnSync(
"ipset",
["add", DIRECT_BYPASS_SET, ip, "timeout", DIRECT_BYPASS_TTL, "-exist"],
{
timeout: 500,
},
);
directBypassCount++;
} catch {}
}
function flushDirectBypass() {
directBypassCount = 0;
if (!IPSET_AVAILABLE) return;
try {
spawnSync("ipset", ["flush", DIRECT_BYPASS_SET], { timeout: 1000 });
} catch {}
}
function listDirectBypass() {
if (!IPSET_AVAILABLE) return [];
try {
const result = spawnSync(
"ipset",
["list", DIRECT_BYPASS_SET, "-output", "plain"],
{
encoding: "utf8",
timeout: 2000,
},
);
const lines = (result.stdout || "").split("\n");
// После строки "Members:" идут IP-адреса
const membersIdx = lines.findIndex((l) => l.trim() === "Members:");
if (membersIdx === -1) return [];
return lines
.slice(membersIdx + 1)
.map((l) => l.trim().split(" ")[0])
.filter((l) => IP_RE.test(l));
} catch {
return [];
}
}
let singboxProcess = null;
let singboxStartedAt = null;
const LOG_BUFFER_SIZE = 500;
@@ -129,6 +191,10 @@ function pushLog(level, line) {
const traffic = parseTrafficLine(line);
if (traffic) {
pushTrafficEntry(traffic);
// Если direct и назначение — IP, добавляем в bypass-кэш
if (traffic.category === "direct" && IP_RE.test(traffic.host)) {
addToDirectBypass(traffic.host);
}
} else if (
(level === "info" || level === "debug") &&
_debugUnparsed < 30 &&
@@ -387,6 +453,8 @@ function publicState() {
rulesUpdatedAt: state.rulesUpdatedAt || null,
rulesAppliedAt: state.rulesAppliedAt || null,
bypassMode: Boolean(state.bypassMode),
directBypassCount,
directBypassAvailable: IPSET_AVAILABLE,
...rest,
};
}
@@ -540,6 +608,21 @@ async function handleApi(req, res) {
return sendJson(res, 200, { success: true });
}
if (req.method === "GET" && req.url === "/api/direct-cache") {
const members = listDirectBypass();
return sendJson(res, 200, {
success: true,
count: members.length,
available: IPSET_AVAILABLE,
members,
});
}
if (req.method === "DELETE" && req.url === "/api/direct-cache") {
flushDirectBypass();
return sendJson(res, 200, { success: true });
}
if (req.method === "POST" && req.url === "/api/bypass") {
const body = await readBody(req);
const enabled = Boolean(body.enabled);