Fix LAN proxy binding in routing setup
Some checks failed
Build and Deploy Gateway / build-and-deploy (push) Failing after 13s
Some checks failed
Build and Deploy Gateway / build-and-deploy (push) Failing after 13s
This commit is contained in:
@@ -6,7 +6,7 @@ export const settings = {
|
||||
port: Number(process.env.PORT || 3456),
|
||||
proxyPort: Number(process.env.PROXY_PORT || 8080),
|
||||
tproxyPort: Number(process.env.TPROXY_PORT || 7895),
|
||||
bindIp: process.env.PROXY_BIND_IP || "127.0.0.1",
|
||||
bindIp: process.env.PROXY_BIND_IP || "0.0.0.0",
|
||||
dataDir,
|
||||
distDir: process.env.DIST_DIR || "/app/dist",
|
||||
configPath:
|
||||
@@ -20,6 +20,7 @@ export const settings = {
|
||||
subscriptionCachePath: path.join(dataDir, "subscription-cache.json"),
|
||||
hwidPath: path.join(dataDir, "hwid"),
|
||||
routingRuDirect: String(process.env.ROUTING_RU_DIRECT || "true") !== "false",
|
||||
ruleSetDownloadDetour: process.env.RULE_SET_DOWNLOAD_DETOUR || "vpn",
|
||||
logLevel: process.env.LOG_LEVEL || "info",
|
||||
appName: "VPN Proxy Gateway",
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { settings } from "./config.js";
|
||||
|
||||
export const DEVICE_MODES = new Set(["direct", "vpn", "rules", "block"]);
|
||||
export const DEFAULT_DEVICE_MODES = new Set(["direct", "vpn", "block"]);
|
||||
export const DEFAULT_DEVICE_MODE = "direct";
|
||||
export const DEFAULT_DEVICE_MODE = "vpn";
|
||||
export const DEFAULT_PROXY_MODE = "vpn";
|
||||
export const TPROXY_INBOUND = "tproxy-in";
|
||||
export const MIXED_INBOUND = "mixed-in";
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ export function matchRoute(target, customRules, options = {}) {
|
||||
routingRuDirect = true,
|
||||
vpnTag = "vpn-out",
|
||||
deviceProfiles = {
|
||||
defaultTransparentMode: "direct",
|
||||
defaultTransparentMode: "vpn",
|
||||
proxyDefaultMode: "vpn",
|
||||
devices: [],
|
||||
},
|
||||
@@ -227,7 +227,7 @@ export function matchRoute(target, customRules, options = {}) {
|
||||
|
||||
// 6. unknown transparent device default.
|
||||
if (inbound === TPROXY_INBOUND) {
|
||||
const mode = deviceProfiles.defaultTransparentMode || "direct";
|
||||
const mode = deviceProfiles.defaultTransparentMode || "vpn";
|
||||
return {
|
||||
matched: "transparent-default",
|
||||
ruleIndex: -1,
|
||||
|
||||
@@ -51,7 +51,14 @@ function readCustomRuleSets() {
|
||||
}
|
||||
}
|
||||
|
||||
function ruleSets(customRuleSets = []) {
|
||||
function ruleSetDownloadDetour(vpnTag) {
|
||||
const detour = String(settings.ruleSetDownloadDetour || "vpn").trim();
|
||||
if (!detour || detour === "vpn") return vpnTag;
|
||||
return detour;
|
||||
}
|
||||
|
||||
function ruleSets(customRuleSets = [], vpnTag = "direct") {
|
||||
const downloadDetour = ruleSetDownloadDetour(vpnTag);
|
||||
const builtIn = settings.routingRuDirect
|
||||
? [
|
||||
{
|
||||
@@ -59,14 +66,14 @@ function ruleSets(customRuleSets = []) {
|
||||
tag: "geoip-ru",
|
||||
format: "binary",
|
||||
url: "https://cdn.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-ru.srs",
|
||||
download_detour: "direct",
|
||||
download_detour: downloadDetour,
|
||||
},
|
||||
{
|
||||
type: "remote",
|
||||
tag: "geosite-category-ru",
|
||||
format: "binary",
|
||||
url: "https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-category-ru.srs",
|
||||
download_detour: "direct",
|
||||
download_detour: downloadDetour,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
@@ -78,7 +85,7 @@ function ruleSets(customRuleSets = []) {
|
||||
tag: String(rs.tag).trim(),
|
||||
format: rs.format || "binary",
|
||||
url: String(rs.url).trim(),
|
||||
download_detour: "direct",
|
||||
download_detour: downloadDetour,
|
||||
}));
|
||||
|
||||
// Пользовательские rule-sets не должны дублировать встроенные
|
||||
@@ -286,7 +293,7 @@ export function buildGatewayConfig(
|
||||
{ type: "block", tag: "block" },
|
||||
],
|
||||
route: {
|
||||
rule_set: bypassAll ? [] : ruleSets(customRuleSets),
|
||||
rule_set: bypassAll ? [] : ruleSets(customRuleSets, vpnOutbound.tag),
|
||||
rules: bypassAll
|
||||
? [{ ip_is_private: true, outbound: "direct" }]
|
||||
: routeRules(subscriptionConfig.customRules, vpnOutbound.tag),
|
||||
|
||||
Reference in New Issue
Block a user