Fix LAN proxy binding in routing setup
Some checks failed
Build and Deploy Gateway / build-and-deploy (push) Failing after 13s

This commit is contained in:
2026-05-09 10:11:40 +03:00
parent aab7533438
commit cab4313c70
15 changed files with 171 additions and 42 deletions

View File

@@ -21,6 +21,30 @@ import { matchRoute, detectRuleConflicts } from "./routeMatcher.js";
import { tcpPing, resolveHost } from "./ping.js";
const APPLY_HISTORY_LIMIT = 10;
const FALLBACK_RULE_SET_CATALOG = {
geosite: [
"geosite-category-ru",
"geosite-google",
"geosite-youtube",
"geosite-telegram",
"geosite-openai",
"geosite-apple",
"geosite-github",
"geosite-steam",
"geosite-discord",
"geosite-netflix",
"geosite-cloudflare",
"geosite-category-ads-all",
],
geoip: [
"geoip-ru",
"geoip-cloudflare",
"geoip-telegram",
"geoip-google",
"geoip-netflix",
"geoip-private",
],
};
fs.mkdirSync(settings.dataDir, { recursive: true });
@@ -402,6 +426,88 @@ function sendJson(res, statusCode, payload) {
res.end(body);
}
function normalizeRuleSetUrl(url) {
const value = String(url || "").trim();
if (!value) return [];
const urls = [value];
const jsdelivrMatch = value.match(
/^https:\/\/cdn\.jsdelivr\.net\/gh\/([^/]+)\/([^@/]+)@([^/]+)\/(.+)$/i,
);
if (jsdelivrMatch) {
const [, owner, repo, ref, filePath] = jsdelivrMatch;
urls.push(
`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filePath}`,
);
}
const rawMatch = value.match(
/^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)$/i,
);
if (rawMatch) {
const [, owner, repo, ref, filePath] = rawMatch;
urls.push(`https://cdn.jsdelivr.net/gh/${owner}/${repo}@${ref}/${filePath}`);
}
return Array.from(new Set(urls));
}
function downloadFile(urlOrUrls, outputPath) {
return new Promise((resolve, reject) => {
const candidates = Array.isArray(urlOrUrls)
? Array.from(new Set(urlOrUrls.flatMap((url) => normalizeRuleSetUrl(url))))
: normalizeRuleSetUrl(urlOrUrls);
let index = 0;
let lastError = "";
function tryNext() {
const url = candidates[index];
if (!url) {
reject(new Error(lastError || "Не удалось скачать файл"));
return;
}
let stderr = "";
const dl = spawn("curl", [
"-fsSL",
"--retry",
"2",
"--retry-delay",
"1",
"--connect-timeout",
"10",
"--max-time",
"45",
"-A",
"vpn-proxy-app",
url,
"-o",
outputPath,
]);
dl.stderr.on("data", (d) => {
stderr += d;
});
dl.on("error", (err) => {
lastError = err.message;
index += 1;
tryNext();
});
dl.on("close", (code) => {
if (code === 0) {
resolve(url);
return;
}
lastError = `curl ${url} завершился с кодом ${code}: ${stderr}`;
index += 1;
tryNext();
});
}
tryNext();
});
}
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
@@ -490,6 +596,7 @@ function publicState() {
mode: "gateway",
port: settings.port,
proxyPort: settings.proxyPort,
proxyBindIp: settings.bindIp,
tproxyPort: settings.tproxyPort,
routingRuDirect: settings.routingRuDirect,
configExists: fs.existsSync(settings.configPath),
@@ -772,7 +879,7 @@ async function handleApi(req, res) {
});
}
const profiles = writeDeviceProfiles({
defaultTransparentMode: "direct",
defaultTransparentMode: "vpn",
proxyDefaultMode: "vpn",
devices,
});
@@ -865,26 +972,7 @@ async function handleApi(req, res) {
const tmpSrs = path.join(os.tmpdir(), `singbox-rs-${Date.now()}.srs`);
const tmpJson = tmpSrs.replace(".srs", ".json");
try {
await new Promise((resolve, reject) => {
let stderr = "";
// Скачиваем через curl (доступен в контейнере)
const dl = spawn("curl", [
"-fsSL",
"--max-time",
"30",
url,
"-o",
tmpSrs,
]);
dl.stderr.on("data", (d) => {
stderr += d;
});
dl.on("close", (code) => {
if (code !== 0)
reject(new Error(`curl завершился с кодом ${code}: ${stderr}`));
else resolve();
});
});
const downloadedFrom = await downloadFile(url, tmpSrs);
// Декомпилировать через sing-box rule-set decompile
const dec = spawnSync(
@@ -896,6 +984,13 @@ async function handleApi(req, res) {
},
);
if (dec.error) {
return sendJson(res, 200, {
success: false,
error: `sing-box не найден или не запустился: ${dec.error.message}`,
});
}
if (dec.status !== 0) {
return sendJson(res, 200, {
success: false,
@@ -942,6 +1037,7 @@ async function handleApi(req, res) {
const result = {
tag,
url,
downloadedFrom,
entries,
stats,
cachedAt: new Date().toISOString(),
@@ -987,6 +1083,11 @@ async function handleApi(req, res) {
{ headers },
),
]);
if (!gsRes.ok || !giRes.ok) {
throw new Error(
`GitHub API недоступен: geosite=${gsRes.status}, geoip=${giRes.status}`,
);
}
const gsData = await gsRes.json();
const giData = await giRes.json();
@@ -999,11 +1100,21 @@ async function handleApi(req, res) {
.map((f) => f.path.replace(".srs", ""))
.sort();
if (!geosite.length && !geoip.length) {
throw new Error("GitHub API вернул пустой каталог rule-set");
}
const result = { geosite, geoip, cachedAt: new Date().toISOString() };
writeJson(cacheFile, result);
return sendJson(res, 200, { success: true, ...result });
} catch (err) {
return sendJson(res, 500, { success: false, error: err.message });
const result = {
...FALLBACK_RULE_SET_CATALOG,
cachedAt: new Date().toISOString(),
fallback: true,
warning: `GitHub каталог не загрузился, показан встроенный список: ${err.message}`,
};
return sendJson(res, 200, { success: true, ...result });
}
}