Remove legacy vpn proxy code and simplify the client
This commit is contained in:
@@ -1,101 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { settings } from "./config.js";
|
||||
|
||||
const DEFAULT_CLIENT_SETTINGS = {
|
||||
homeBypassEnabled: false,
|
||||
sharedProxyEnabled: false,
|
||||
sharedProxyControlUrl: "",
|
||||
sharedProxy: null,
|
||||
};
|
||||
|
||||
function normalizeProxyPort(value, fallback = settings.proxyPort) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
const min = Number.isInteger(settings.clientProxyPortStart)
|
||||
? settings.clientProxyPortStart
|
||||
: settings.proxyPort;
|
||||
const max = Number.isInteger(settings.clientProxyPortEnd)
|
||||
? settings.clientProxyPortEnd
|
||||
: min;
|
||||
const fallbackPort =
|
||||
Number.isInteger(fallback) && fallback >= min && fallback <= max
|
||||
? fallback
|
||||
: min;
|
||||
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
||||
return fallbackPort;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function readJson(filePath, fallback) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return fallback;
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(filePath, value) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf8");
|
||||
}
|
||||
|
||||
function normalizeUrl(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) return "";
|
||||
try {
|
||||
const url = new URL(raw);
|
||||
if (!["http:", "https:"].includes(url.protocol)) return "";
|
||||
url.hash = "";
|
||||
url.search = "";
|
||||
return url.toString().replace(/\/$/, "");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSharedProxy(value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const host = String(value.host || "").trim();
|
||||
const port = Number.parseInt(value.port, 10);
|
||||
const protocol = value.protocol === "http" ? "http" : "socks5";
|
||||
if (!host || !Number.isInteger(port) || port <= 0 || port > 65535) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
host,
|
||||
port,
|
||||
protocol,
|
||||
checkedAt: value.checkedAt || null,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeClientSettings(input = {}) {
|
||||
const sharedProxy = normalizeSharedProxy(input.sharedProxy);
|
||||
const sharedProxyEnabled = Boolean(input.sharedProxyEnabled && sharedProxy);
|
||||
return {
|
||||
homeBypassEnabled: Boolean(input.homeBypassEnabled),
|
||||
proxyPort: normalizeProxyPort(input.proxyPort),
|
||||
sharedProxyEnabled,
|
||||
sharedProxyControlUrl: normalizeUrl(input.sharedProxyControlUrl),
|
||||
sharedProxy,
|
||||
};
|
||||
}
|
||||
|
||||
export function readClientSettings() {
|
||||
return normalizeClientSettings({
|
||||
...DEFAULT_CLIENT_SETTINGS,
|
||||
proxyPort: settings.proxyPort,
|
||||
...readJson(settings.clientSettingsPath, {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function writeClientSettings(input) {
|
||||
const normalized = normalizeClientSettings({
|
||||
...readClientSettings(),
|
||||
...(input && typeof input === "object" ? input : {}),
|
||||
});
|
||||
writeJson(settings.clientSettingsPath, normalized);
|
||||
return normalized;
|
||||
}
|
||||
@@ -9,29 +9,12 @@ const proxyPort = parsePort(
|
||||
process.env.PROXY_PORT,
|
||||
process.env.APP_MODE === "client" ? 8082 : 8080,
|
||||
);
|
||||
const clientProxyPortStart = parsePort(
|
||||
process.env.CLIENT_PROXY_PORT_START,
|
||||
proxyPort,
|
||||
);
|
||||
const clientProxyPortEnd = parsePort(
|
||||
process.env.CLIENT_PROXY_PORT_END,
|
||||
clientProxyPortStart,
|
||||
);
|
||||
|
||||
export const settings = {
|
||||
appMode: process.env.APP_MODE === "client" ? "client" : "gateway",
|
||||
port: parsePort(process.env.PORT, 3456),
|
||||
proxyPort,
|
||||
clientProxyPortStart,
|
||||
clientProxyPortEnd,
|
||||
tproxyPort: parsePort(process.env.TPROXY_PORT, 7895),
|
||||
tproxyChain: process.env.TPROXY_CHAIN || "VPN_PROXY_TPROXY",
|
||||
tproxySourceBypassChain:
|
||||
process.env.TPROXY_SOURCE_BYPASS_CHAIN || "VPN_PROXY_SRC_BYPASS",
|
||||
tproxySourceForwardChain:
|
||||
process.env.TPROXY_SOURCE_FORWARD_CHAIN || "VPN_PROXY_FWD_BYPASS",
|
||||
tproxySourceNatChain:
|
||||
process.env.TPROXY_SOURCE_NAT_CHAIN || "VPN_PROXY_NAT_BYPASS",
|
||||
bindIp: process.env.PROXY_BIND_IP || "0.0.0.0",
|
||||
dataDir,
|
||||
distDir: process.env.DIST_DIR || "/app/dist",
|
||||
@@ -39,16 +22,9 @@ export const settings = {
|
||||
process.env.SING_BOX_CONFIG || path.join(dataDir, "sing-box-config.json"),
|
||||
cachePath: process.env.SING_BOX_CACHE || "/var/lib/sing-box/cache.db",
|
||||
statePath: path.join(dataDir, "state.json"),
|
||||
customRulesPath: path.join(dataDir, "custom-rules.json"),
|
||||
customRuleSetsPath: path.join(dataDir, "custom-rule-sets.json"),
|
||||
clientSettingsPath: path.join(dataDir, "client-settings.json"),
|
||||
devicesPath: path.join(dataDir, "devices.json"),
|
||||
deviceRulesPath: path.join(dataDir, "device-rules.json"),
|
||||
subscriptionCachePath: path.join(dataDir, "subscription-cache.json"),
|
||||
sharedProxyHost: process.env.SHARED_PROXY_HOST || "",
|
||||
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",
|
||||
};
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { settings } from "./config.js";
|
||||
|
||||
export const DEVICE_MODES = new Set(["direct", "vpn", "rules", "block", "bypass"]);
|
||||
export const DEFAULT_DEVICE_MODES = new Set(["direct", "vpn", "block"]);
|
||||
export const DEFAULT_DEVICE_MODE = "vpn";
|
||||
export const DEFAULT_PROXY_MODE = "vpn";
|
||||
export const TPROXY_INBOUND = "tproxy-in";
|
||||
export const MIXED_INBOUND = "mixed-in";
|
||||
|
||||
const IPISH_RE = /^[\.\d:/]+$/;
|
||||
|
||||
function readJson(filePath, fallback) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return fallback;
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(filePath, value) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf8");
|
||||
}
|
||||
|
||||
function normalizeDeviceMode(mode, fallback = "rules") {
|
||||
const value = String(mode || "").trim().toLowerCase();
|
||||
return DEVICE_MODES.has(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function normalizeDefaultMode(mode) {
|
||||
const value = String(mode || "").trim().toLowerCase();
|
||||
return DEFAULT_DEVICE_MODES.has(value) ? value : DEFAULT_DEVICE_MODE;
|
||||
}
|
||||
|
||||
function normalizeProxyMode(mode) {
|
||||
const value = String(mode || "").trim().toLowerCase();
|
||||
return DEFAULT_DEVICE_MODES.has(value) ? value : DEFAULT_PROXY_MODE;
|
||||
}
|
||||
|
||||
function normalizeIp(ip) {
|
||||
const value = String(ip || "").trim();
|
||||
return value && IPISH_RE.test(value) ? value : "";
|
||||
}
|
||||
|
||||
function normalizeMac(mac) {
|
||||
return String(mac || "").trim();
|
||||
}
|
||||
|
||||
function fromLegacyDeviceRules(input) {
|
||||
const rules = Array.isArray(input) ? input : [];
|
||||
const devices = [];
|
||||
|
||||
for (const rule of rules) {
|
||||
const sourceIps = Array.isArray(rule?.sourceIps) ? rule.sourceIps : [];
|
||||
const mode = normalizeDeviceMode(rule?.outbound, "direct");
|
||||
sourceIps.forEach((sourceIp, ipIndex) => {
|
||||
const ip = normalizeIp(sourceIp);
|
||||
if (!ip) return;
|
||||
devices.push({
|
||||
id: String(rule.id || `dev-${devices.length}`) + `-${ipIndex}`,
|
||||
name: String(rule.name || `Устройство ${devices.length + 1}`).trim(),
|
||||
enabled: rule.enabled !== false,
|
||||
ip,
|
||||
mac: "",
|
||||
mode,
|
||||
lastSeen: null,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
defaultTransparentMode: DEFAULT_DEVICE_MODE,
|
||||
proxyDefaultMode: DEFAULT_PROXY_MODE,
|
||||
devices,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeDeviceProfiles(input) {
|
||||
const raw =
|
||||
input && typeof input === "object" && !Array.isArray(input)
|
||||
? input
|
||||
: { devices: input };
|
||||
const rawDevices = Array.isArray(raw.devices) ? raw.devices : [];
|
||||
|
||||
return {
|
||||
defaultTransparentMode: normalizeDefaultMode(
|
||||
raw.defaultTransparentMode || raw.defaultMode,
|
||||
),
|
||||
proxyDefaultMode: normalizeProxyMode(raw.proxyDefaultMode),
|
||||
devices: rawDevices.map((device, index) => ({
|
||||
id: String(device.id || `dev-${Date.now()}-${index}`),
|
||||
name: String(device.name || `Устройство ${index + 1}`).trim(),
|
||||
enabled: device.enabled !== false,
|
||||
ip: normalizeIp(device.ip || device.sourceIp),
|
||||
mac: normalizeMac(device.mac),
|
||||
mode: normalizeDeviceMode(device.mode || device.outbound, "rules"),
|
||||
lastSeen: device.lastSeen || null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function readDeviceProfiles() {
|
||||
if (fs.existsSync(settings.devicesPath)) {
|
||||
return normalizeDeviceProfiles(readJson(settings.devicesPath, null));
|
||||
}
|
||||
|
||||
if (fs.existsSync(settings.deviceRulesPath)) {
|
||||
return normalizeDeviceProfiles(
|
||||
fromLegacyDeviceRules(readJson(settings.deviceRulesPath, [])),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
defaultTransparentMode: DEFAULT_DEVICE_MODE,
|
||||
proxyDefaultMode: DEFAULT_PROXY_MODE,
|
||||
devices: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function writeDeviceProfiles(value) {
|
||||
const normalized = normalizeDeviceProfiles(value);
|
||||
writeJson(settings.devicesPath, normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function normalizeCidr(ip) {
|
||||
const value = normalizeIp(ip);
|
||||
if (!value) return "";
|
||||
return value.includes("/") ? value : `${value}/32`;
|
||||
}
|
||||
|
||||
export function deviceCidrs(devices, modes) {
|
||||
const allowedModes = new Set(Array.isArray(modes) ? modes : [modes]);
|
||||
return (Array.isArray(devices) ? devices : [])
|
||||
.filter((device) => device.enabled !== false && allowedModes.has(device.mode))
|
||||
.map((device) => normalizeCidr(device.ip))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function legacyDeviceRulesFromProfiles(profiles) {
|
||||
const { devices } = normalizeDeviceProfiles(profiles);
|
||||
return devices.map((device) => ({
|
||||
id: device.id,
|
||||
name: device.name,
|
||||
enabled: device.enabled,
|
||||
sourceIps: device.ip ? [device.ip] : [],
|
||||
outbound: device.mode === "rules" ? "direct" : device.mode,
|
||||
}));
|
||||
}
|
||||
1618
src/server/index.js
1618
src/server/index.js
File diff suppressed because it is too large
Load Diff
@@ -1,340 +0,0 @@
|
||||
// Простой симулятор роутинга sing-box.
|
||||
// Берём список customRules + safety/RU-direct и определяем, какое правило сработает.
|
||||
// Для geoip-ru / geosite-category-ru возвращаем "может сработать" — без скачанного ruleset
|
||||
// мы не можем точно сказать, попадает ли IP/домен в RU.
|
||||
|
||||
import net from "node:net";
|
||||
import { TPROXY_INBOUND, MIXED_INBOUND } from "./devices.js";
|
||||
|
||||
function ipv4ToInt(ip) {
|
||||
const parts = ip.split(".").map((x) => Number.parseInt(x, 10));
|
||||
if (
|
||||
parts.length !== 4 ||
|
||||
parts.some((n) => Number.isNaN(n) || n < 0 || n > 255)
|
||||
)
|
||||
return null;
|
||||
return (
|
||||
((parts[0] << 24) >>> 0) + (parts[1] << 16) + (parts[2] << 8) + parts[3]
|
||||
);
|
||||
}
|
||||
|
||||
function ipInCidr(ip, cidr) {
|
||||
if (!net.isIP(ip)) return false;
|
||||
const [addr, maskStr] = String(cidr).split("/");
|
||||
if (!addr) return false;
|
||||
|
||||
if (net.isIPv4(ip) && net.isIPv4(addr)) {
|
||||
const mask = maskStr === undefined ? 32 : Number.parseInt(maskStr, 10);
|
||||
if (!Number.isInteger(mask) || mask < 0 || mask > 32) return false;
|
||||
const ipInt = ipv4ToInt(ip);
|
||||
const cidrInt = ipv4ToInt(addr);
|
||||
if (ipInt === null || cidrInt === null) return false;
|
||||
if (mask === 0) return true;
|
||||
const m = (~0 << (32 - mask)) >>> 0;
|
||||
return (ipInt & m) === (cidrInt & m);
|
||||
}
|
||||
// IPv6 — упрощённо: точное сравнение строк (без полноценной обработки)
|
||||
return false;
|
||||
}
|
||||
|
||||
const PRIVATE_CIDRS = [
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"127.0.0.0/8",
|
||||
"169.254.0.0/16",
|
||||
];
|
||||
|
||||
function isPrivateIp(ip) {
|
||||
if (!ip) return false;
|
||||
return PRIVATE_CIDRS.some((cidr) => ipInCidr(ip, cidr));
|
||||
}
|
||||
|
||||
function normalizeCidr(ip) {
|
||||
const value = String(ip || "").trim();
|
||||
if (!value) return "";
|
||||
return value.includes("/") ? value : `${value}/32`;
|
||||
}
|
||||
|
||||
function deviceMatchesSourceIp(device, sourceIp) {
|
||||
if (!device?.ip || !sourceIp) return false;
|
||||
return ipInCidr(sourceIp, normalizeCidr(device.ip));
|
||||
}
|
||||
|
||||
function modeOutbound(mode, vpnTag) {
|
||||
if (mode === "vpn") return `${vpnTag} (VPN)`;
|
||||
if (mode === "direct" || mode === "block") return mode;
|
||||
return null;
|
||||
}
|
||||
|
||||
function likelyRuHost(host) {
|
||||
const value = String(host || "").toLowerCase();
|
||||
return value === "ru" || value.endsWith(".ru");
|
||||
}
|
||||
|
||||
function hostMatchesDomain(host, domain) {
|
||||
if (!host || !domain) return false;
|
||||
return host.toLowerCase() === domain.toLowerCase();
|
||||
}
|
||||
|
||||
function hostMatchesSuffix(host, suffix) {
|
||||
if (!host || !suffix) return false;
|
||||
const h = host.toLowerCase();
|
||||
const s = suffix.toLowerCase();
|
||||
return h === s || h.endsWith("." + s) || h.endsWith(s);
|
||||
}
|
||||
|
||||
function hostMatchesKeyword(host, keyword) {
|
||||
if (!host || !keyword) return false;
|
||||
return host.toLowerCase().includes(keyword.toLowerCase());
|
||||
}
|
||||
|
||||
function ruleMatches(rule, target) {
|
||||
const { host = "", ip = "", port, network } = target;
|
||||
|
||||
if (!rule?.enabled) return false;
|
||||
|
||||
const checks = [];
|
||||
|
||||
if (rule.domains?.length) {
|
||||
checks.push(rule.domains.some((d) => hostMatchesDomain(host, d)));
|
||||
}
|
||||
if (rule.domainSuffixes?.length) {
|
||||
checks.push(rule.domainSuffixes.some((d) => hostMatchesSuffix(host, d)));
|
||||
}
|
||||
if (rule.domainKeywords?.length) {
|
||||
checks.push(rule.domainKeywords.some((d) => hostMatchesKeyword(host, d)));
|
||||
}
|
||||
if (rule.ipCidrs?.length) {
|
||||
if (!ip) return false;
|
||||
checks.push(rule.ipCidrs.some((cidr) => ipInCidr(ip, cidr)));
|
||||
}
|
||||
if (rule.ports?.length) {
|
||||
if (port === undefined || port === null || port === "") return false;
|
||||
const p = Number(port);
|
||||
checks.push(
|
||||
rule.ports.some((portStr) => {
|
||||
const s = String(portStr).trim();
|
||||
if (s.includes("-")) {
|
||||
const [from, to] = s.split("-").map((x) => Number(x));
|
||||
return p >= from && p <= to;
|
||||
}
|
||||
return p === Number(s);
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (rule.networks?.length) {
|
||||
if (!network) return false;
|
||||
checks.push(rule.networks.includes(network));
|
||||
}
|
||||
|
||||
if (!checks.length) return false;
|
||||
return checks.every(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Симулирует роутинг и возвращает результат.
|
||||
* @param {object} target { host, ip, port, network }
|
||||
* @param {Array} customRules
|
||||
* @param {object} options { routingRuDirect, vpnTag }
|
||||
*/
|
||||
export function matchRoute(target, customRules, options = {}) {
|
||||
const {
|
||||
routingRuDirect = true,
|
||||
vpnTag = "vpn-out",
|
||||
deviceProfiles = {
|
||||
defaultTransparentMode: "vpn",
|
||||
proxyDefaultMode: "vpn",
|
||||
devices: [],
|
||||
},
|
||||
} = options;
|
||||
const rules = Array.isArray(customRules) ? customRules : [];
|
||||
const inbound = target.inbound || TPROXY_INBOUND;
|
||||
const sourceIp = target.sourceIp || "";
|
||||
const devices = Array.isArray(deviceProfiles.devices)
|
||||
? deviceProfiles.devices
|
||||
: [];
|
||||
const matchedDevice = devices.find(
|
||||
(device) =>
|
||||
device.enabled !== false && deviceMatchesSourceIp(device, sourceIp),
|
||||
);
|
||||
|
||||
if (
|
||||
inbound === TPROXY_INBOUND &&
|
||||
matchedDevice &&
|
||||
matchedDevice.mode === "bypass"
|
||||
) {
|
||||
return {
|
||||
matched: "kernel-bypass",
|
||||
ruleIndex: -1,
|
||||
ruleId: matchedDevice.id,
|
||||
ruleName: `${matchedDevice.name} -> bypass TProxy`,
|
||||
outbound: "direct",
|
||||
reason: "Source IP исключён на уровне iptables до попадания в sing-box",
|
||||
};
|
||||
}
|
||||
|
||||
// 1. private IP → direct
|
||||
if (target.ip && isPrivateIp(target.ip)) {
|
||||
return {
|
||||
matched: "system",
|
||||
ruleIndex: -1,
|
||||
ruleName: "private IP → direct",
|
||||
outbound: "direct",
|
||||
reason: `IP ${target.ip} приватный`,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. global custom rules apply to every inbound before fallbacks.
|
||||
for (let i = 0; i < rules.length; i += 1) {
|
||||
const rule = rules[i];
|
||||
if (ruleMatches(rule, target)) {
|
||||
const outbound =
|
||||
rule.outbound === "vpn" ? `${vpnTag} (VPN)` : rule.outbound;
|
||||
return {
|
||||
matched: "custom",
|
||||
ruleIndex: i,
|
||||
ruleId: rule.id,
|
||||
ruleName: rule.name,
|
||||
outbound,
|
||||
reason: "Совпадение по global custom rule",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 3. RU direct is global. Without a local rule-set DB we only detect obvious .ru hosts.
|
||||
if (routingRuDirect && likelyRuHost(target.host)) {
|
||||
return {
|
||||
matched: "geo",
|
||||
ruleIndex: -2,
|
||||
ruleName: "geosite-category-ru → direct",
|
||||
outbound: "direct",
|
||||
reason: "Домен выглядит как RU; точное попадание в rule-set проверит sing-box",
|
||||
};
|
||||
}
|
||||
|
||||
// 4. transparent device defaults.
|
||||
if (inbound === TPROXY_INBOUND && matchedDevice) {
|
||||
const outbound = modeOutbound(matchedDevice.mode, vpnTag);
|
||||
if (outbound) {
|
||||
return {
|
||||
matched: "device-default",
|
||||
ruleIndex: -1,
|
||||
ruleId: matchedDevice.id,
|
||||
ruleName: `${matchedDevice.name} → ${matchedDevice.mode}`,
|
||||
outbound,
|
||||
reason: "Fallback устройства после global rules",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 5. explicit proxy default.
|
||||
if (inbound === MIXED_INBOUND) {
|
||||
const mode = deviceProfiles.proxyDefaultMode || "vpn";
|
||||
return {
|
||||
matched: "proxy-default",
|
||||
ruleIndex: -1,
|
||||
ruleName: `mixed-in default → ${mode}`,
|
||||
outbound: modeOutbound(mode, vpnTag) || `${vpnTag} (VPN)`,
|
||||
reason: "Fallback explicit HTTP/SOCKS proxy после global rules",
|
||||
};
|
||||
}
|
||||
|
||||
// 6. unknown transparent device default.
|
||||
if (inbound === TPROXY_INBOUND) {
|
||||
const mode = deviceProfiles.defaultTransparentMode || "vpn";
|
||||
return {
|
||||
matched: "transparent-default",
|
||||
ruleIndex: -1,
|
||||
ruleName: `transparent default → ${mode}`,
|
||||
outbound: modeOutbound(mode, vpnTag) || "direct",
|
||||
reason: "Fallback unknown transparent device после global rules",
|
||||
};
|
||||
}
|
||||
|
||||
// 7. final → direct
|
||||
return {
|
||||
matched: "final",
|
||||
ruleIndex: -3,
|
||||
ruleName: "final",
|
||||
outbound: "direct",
|
||||
reason: "Не сработало ни одно правило — итоговый final отправляет напрямую",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Детектор конфликтов: ищет правила, перекрытые предыдущими.
|
||||
* Простая эвристика: если правило-кандидат полностью перекрывается ранее идущим
|
||||
* по доменам/суффиксам/CIDR — отмечаем конфликт.
|
||||
*/
|
||||
export function detectRuleConflicts(rules) {
|
||||
const list = Array.isArray(rules) ? rules : [];
|
||||
const conflicts = [];
|
||||
|
||||
for (let i = 1; i < list.length; i += 1) {
|
||||
const cur = list[i];
|
||||
if (!cur?.enabled) continue;
|
||||
|
||||
for (let j = 0; j < i; j += 1) {
|
||||
const prev = list[j];
|
||||
if (!prev?.enabled) continue;
|
||||
|
||||
// Если outbound одинаковый — это не "конфликт", это дубликат
|
||||
const sameOutbound = prev.outbound === cur.outbound;
|
||||
|
||||
// Проверка перекрытия доменов
|
||||
const overlaps = [];
|
||||
|
||||
// Точные домены покрываются prev.suffix
|
||||
for (const d of cur.domains || []) {
|
||||
if ((prev.domainSuffixes || []).some((s) => hostMatchesSuffix(d, s))) {
|
||||
overlaps.push({
|
||||
kind: "domain",
|
||||
value: d,
|
||||
by: `суффикс ${(prev.domainSuffixes || []).find((s) => hostMatchesSuffix(d, s))}`,
|
||||
});
|
||||
}
|
||||
if ((prev.domains || []).includes(d)) {
|
||||
overlaps.push({ kind: "domain", value: d, by: "точный домен" });
|
||||
}
|
||||
}
|
||||
|
||||
// Суффиксы покрываются более общим суффиксом prev
|
||||
for (const s of cur.domainSuffixes || []) {
|
||||
if (
|
||||
(prev.domainSuffixes || []).some(
|
||||
(ps) => hostMatchesSuffix(s, ps) && ps !== s,
|
||||
)
|
||||
) {
|
||||
overlaps.push({
|
||||
kind: "suffix",
|
||||
value: s,
|
||||
by: "более общий суффикс",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// CIDR
|
||||
for (const c of cur.ipCidrs || []) {
|
||||
if ((prev.ipCidrs || []).includes(c)) {
|
||||
overlaps.push({ kind: "cidr", value: c, by: "тот же CIDR" });
|
||||
}
|
||||
}
|
||||
|
||||
if (overlaps.length) {
|
||||
conflicts.push({
|
||||
ruleId: cur.id,
|
||||
ruleIndex: i,
|
||||
ruleName: cur.name,
|
||||
conflictWithId: prev.id,
|
||||
conflictWithIndex: j,
|
||||
conflictWithName: prev.name,
|
||||
severity: sameOutbound ? "info" : "warning",
|
||||
overlaps,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
@@ -1,352 +1,71 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { settings } from "./config.js";
|
||||
import {
|
||||
MIXED_INBOUND,
|
||||
TPROXY_INBOUND,
|
||||
normalizeCidr,
|
||||
readDeviceProfiles,
|
||||
} from "./devices.js";
|
||||
import { readClientSettings } from "./clientSettings.js";
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { settings } from './config.js';
|
||||
|
||||
const PROXY_TYPES = new Set([
|
||||
"vless",
|
||||
"vmess",
|
||||
"trojan",
|
||||
"shadowsocks",
|
||||
"hysteria2",
|
||||
]);
|
||||
const CUSTOM_OUTBOUNDS = new Set(["direct", "vpn", "block"]);
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
const PROXY_TYPES = new Set(['vless', 'vmess', 'trojan', 'shadowsocks', 'hysteria2']);
|
||||
const MIXED_INBOUND = 'mixed-in';
|
||||
const TPROXY_INBOUND = 'tproxy-in';
|
||||
|
||||
function findOutbound(subscriptionConfig, selectedTag) {
|
||||
const outbounds = Array.isArray(subscriptionConfig?.outbounds)
|
||||
? subscriptionConfig.outbounds
|
||||
: [];
|
||||
const exact = outbounds.find(
|
||||
(outbound) =>
|
||||
outbound.tag === selectedTag && PROXY_TYPES.has(outbound.type),
|
||||
);
|
||||
if (exact) return exact;
|
||||
|
||||
const trimmedTag = String(selectedTag || "").trim();
|
||||
return outbounds.find(
|
||||
(outbound) =>
|
||||
String(outbound.tag || "").trim() === trimmedTag &&
|
||||
PROXY_TYPES.has(outbound.type),
|
||||
);
|
||||
const tag = String(selectedTag || '').trim();
|
||||
return outbounds.find((outbound) => (
|
||||
String(outbound.tag || '').trim() === tag && PROXY_TYPES.has(outbound.type)
|
||||
));
|
||||
}
|
||||
|
||||
function readCustomRuleSets() {
|
||||
try {
|
||||
if (!fs.existsSync(settings.customRuleSetsPath)) return [];
|
||||
const data = JSON.parse(
|
||||
fs.readFileSync(settings.customRuleSetsPath, "utf8"),
|
||||
);
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
? [
|
||||
{
|
||||
type: "remote",
|
||||
tag: "geoip-ru",
|
||||
format: "binary",
|
||||
url: "https://cdn.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-ru.srs",
|
||||
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: downloadDetour,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const custom = (Array.isArray(customRuleSets) ? customRuleSets : [])
|
||||
.filter((rs) => rs.tag && rs.url)
|
||||
.map((rs) => ({
|
||||
type: "remote",
|
||||
tag: String(rs.tag).trim(),
|
||||
format: rs.format || "binary",
|
||||
url: String(rs.url).trim(),
|
||||
download_detour: downloadDetour,
|
||||
}));
|
||||
|
||||
// Пользовательские rule-sets не должны дублировать встроенные
|
||||
const builtInTags = new Set(builtIn.map((rs) => rs.tag));
|
||||
const merged = [
|
||||
...builtIn,
|
||||
...custom.filter((rs) => !builtInTags.has(rs.tag)),
|
||||
];
|
||||
return merged;
|
||||
}
|
||||
|
||||
function uniqueClean(values) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
(Array.isArray(values) ? values : [])
|
||||
.map((value) => String(value || "").trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function parsePorts(values) {
|
||||
return uniqueClean(values)
|
||||
.map((value) => Number.parseInt(value, 10))
|
||||
.filter((value) => Number.isInteger(value) && value > 0 && value <= 65535);
|
||||
}
|
||||
|
||||
function toSingboxRule(customRule, vpnTag, baseRule = {}) {
|
||||
if (!customRule?.enabled) return null;
|
||||
if (!CUSTOM_OUTBOUNDS.has(customRule.outbound)) return null;
|
||||
|
||||
const rule = { ...baseRule };
|
||||
const domains = uniqueClean(customRule.domains);
|
||||
const domainSuffixes = uniqueClean(customRule.domainSuffixes);
|
||||
const domainKeywords = uniqueClean(customRule.domainKeywords);
|
||||
const ipCidrs = uniqueClean(customRule.ipCidrs);
|
||||
const ports = parsePorts(customRule.ports);
|
||||
const networks = uniqueClean(customRule.networks).filter((network) =>
|
||||
["tcp", "udp"].includes(network),
|
||||
);
|
||||
|
||||
if (domains.length) rule.domain = domains;
|
||||
if (domainSuffixes.length) rule.domain_suffix = domainSuffixes;
|
||||
if (domainKeywords.length) rule.domain_keyword = domainKeywords;
|
||||
if (ipCidrs.length) rule.ip_cidr = ipCidrs;
|
||||
if (ports.length) rule.port = ports;
|
||||
if (networks.length) rule.network = networks;
|
||||
|
||||
const ruleSetsRef = uniqueClean(customRule.ruleSets);
|
||||
if (ruleSetsRef.length) rule.rule_set = ruleSetsRef;
|
||||
|
||||
if (
|
||||
!rule.domain &&
|
||||
!rule.domain_suffix &&
|
||||
!rule.domain_keyword &&
|
||||
!rule.ip_cidr &&
|
||||
!rule.port &&
|
||||
!rule.network &&
|
||||
!rule.rule_set
|
||||
) {
|
||||
return null;
|
||||
export function buildGatewayConfig(subscriptionConfig, selectedTag) {
|
||||
const clientMode = settings.appMode === 'client';
|
||||
const vpnOutbound = structuredClone(findOutbound(subscriptionConfig, selectedTag));
|
||||
if (!vpnOutbound) throw new Error(`Outbound не найден: ${selectedTag}`);
|
||||
if (!vpnOutbound.tag) vpnOutbound.tag = 'vpn-out';
|
||||
if (vpnOutbound.type === 'vless' && !vpnOutbound.packet_encoding) {
|
||||
vpnOutbound.packet_encoding = 'xudp';
|
||||
}
|
||||
|
||||
rule.outbound = customRule.outbound === "vpn" ? vpnTag : customRule.outbound;
|
||||
return rule;
|
||||
}
|
||||
|
||||
function customRouteRules(customRules, vpnTag, baseRule = {}) {
|
||||
return (Array.isArray(customRules) ? customRules : [])
|
||||
.map((rule) => toSingboxRule(rule, vpnTag, baseRule))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
// ─── Device rules (маршрутизация по source IP) ──────────────────────────────
|
||||
|
||||
function modeOutbound(mode, vpnTag) {
|
||||
if (mode === "vpn") return vpnTag;
|
||||
if (mode === "direct" || mode === "block") return mode;
|
||||
return null;
|
||||
}
|
||||
|
||||
function deviceDefaultRouteRule(device, vpnTag) {
|
||||
if (!device?.enabled) return null;
|
||||
const outbound = modeOutbound(device.mode, vpnTag);
|
||||
if (!outbound) return null;
|
||||
|
||||
const cidr = normalizeCidr(device.ip);
|
||||
if (!cidr) return null;
|
||||
|
||||
return {
|
||||
inbound: [TPROXY_INBOUND],
|
||||
source_ip_cidr: [cidr],
|
||||
outbound,
|
||||
};
|
||||
}
|
||||
|
||||
function deviceDefaultRouteRules(devices, vpnTag) {
|
||||
return (Array.isArray(devices) ? devices : [])
|
||||
.map((device) => deviceDefaultRouteRule(device, vpnTag))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function inboundDefaultRule(inbound, mode, vpnTag) {
|
||||
const outbound = modeOutbound(mode, vpnTag);
|
||||
if (!outbound) return null;
|
||||
return { inbound: [inbound], outbound };
|
||||
}
|
||||
|
||||
function ruDirectRule() {
|
||||
if (!settings.routingRuDirect) return null;
|
||||
return {
|
||||
rule_set: ["geoip-ru", "geosite-category-ru"],
|
||||
outbound: "direct",
|
||||
};
|
||||
}
|
||||
|
||||
function routeRules(customRules, vpnTag, { includeTransparent = true } = {}) {
|
||||
const deviceProfiles = readDeviceProfiles();
|
||||
const rules = [
|
||||
{
|
||||
ip_is_private: true,
|
||||
outbound: "direct",
|
||||
},
|
||||
];
|
||||
|
||||
// Global rules apply to every inbound before contextual fallbacks.
|
||||
rules.push(...customRouteRules(customRules, vpnTag));
|
||||
|
||||
const ruRule = ruDirectRule();
|
||||
if (ruRule) rules.push(ruRule);
|
||||
|
||||
if (includeTransparent) {
|
||||
// Device defaults are only transparent-gateway fallbacks after global rules.
|
||||
rules.push(...deviceDefaultRouteRules(deviceProfiles.devices, vpnTag));
|
||||
}
|
||||
|
||||
const proxyFallback = inboundDefaultRule(
|
||||
MIXED_INBOUND,
|
||||
deviceProfiles.proxyDefaultMode,
|
||||
vpnTag,
|
||||
);
|
||||
if (proxyFallback) rules.push(proxyFallback);
|
||||
|
||||
if (includeTransparent) {
|
||||
const transparentFallback = inboundDefaultRule(
|
||||
TPROXY_INBOUND,
|
||||
deviceProfiles.defaultTransparentMode,
|
||||
vpnTag,
|
||||
);
|
||||
if (transparentFallback) rules.push(transparentFallback);
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
function sharedProxyOutbound(sharedProxy) {
|
||||
if (!sharedProxy?.host || !sharedProxy?.port) return null;
|
||||
if (sharedProxy.protocol === "http") {
|
||||
return {
|
||||
type: "http",
|
||||
tag: "shared-proxy",
|
||||
server: sharedProxy.host,
|
||||
server_port: sharedProxy.port,
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: "socks",
|
||||
tag: "shared-proxy",
|
||||
server: sharedProxy.host,
|
||||
server_port: sharedProxy.port,
|
||||
version: "5",
|
||||
};
|
||||
}
|
||||
|
||||
export function buildGatewayConfig(
|
||||
subscriptionConfig,
|
||||
selectedTag,
|
||||
{ bypassAll = false } = {},
|
||||
) {
|
||||
const customRuleSets = readCustomRuleSets();
|
||||
const clientMode = settings.appMode === "client";
|
||||
const clientSettings = clientMode ? readClientSettings() : null;
|
||||
const sharedOutbound =
|
||||
clientMode && clientSettings?.sharedProxyEnabled
|
||||
? sharedProxyOutbound(clientSettings.sharedProxy)
|
||||
: null;
|
||||
const directOnlyClient = clientMode && clientSettings?.homeBypassEnabled;
|
||||
const selectedOutbound = sharedOutbound
|
||||
? null
|
||||
: findOutbound(subscriptionConfig, selectedTag);
|
||||
if (!sharedOutbound && !directOnlyClient && !selectedOutbound) {
|
||||
throw new Error(`Outbound не найден: ${selectedTag}`);
|
||||
}
|
||||
|
||||
const vpnOutbound = selectedOutbound ? clone(selectedOutbound) : null;
|
||||
if (vpnOutbound && !vpnOutbound.tag) vpnOutbound.tag = "vpn-out";
|
||||
if (vpnOutbound?.type === "vless" && !vpnOutbound.packet_encoding) {
|
||||
vpnOutbound.packet_encoding = "xudp";
|
||||
}
|
||||
|
||||
const clientOutbound = sharedOutbound
|
||||
? sharedOutbound.tag
|
||||
: clientSettings?.homeBypassEnabled
|
||||
? "direct"
|
||||
: vpnOutbound.tag;
|
||||
const mixedProxyPort = clientSettings?.proxyPort || settings.proxyPort;
|
||||
const proxyOnlyRules = [{ inbound: [MIXED_INBOUND], outbound: clientOutbound }];
|
||||
const inbounds = [
|
||||
...(clientMode
|
||||
? []
|
||||
: [
|
||||
{
|
||||
type: "tproxy",
|
||||
tag: "tproxy-in",
|
||||
listen: "::",
|
||||
listen_port: settings.tproxyPort,
|
||||
sniff: true,
|
||||
sniff_override_destination: true,
|
||||
},
|
||||
]),
|
||||
...(!clientMode ? [{
|
||||
type: 'tproxy',
|
||||
tag: TPROXY_INBOUND,
|
||||
listen: '::',
|
||||
listen_port: settings.tproxyPort,
|
||||
sniff: true,
|
||||
sniff_override_destination: true,
|
||||
}] : []),
|
||||
{
|
||||
type: "mixed",
|
||||
tag: "mixed-in",
|
||||
type: 'mixed',
|
||||
tag: MIXED_INBOUND,
|
||||
listen: settings.bindIp,
|
||||
listen_port: mixedProxyPort,
|
||||
listen_port: settings.proxyPort,
|
||||
sniff: true,
|
||||
set_system_proxy: false,
|
||||
},
|
||||
];
|
||||
const rules = clientMode
|
||||
? [{ inbound: [MIXED_INBOUND], outbound: vpnOutbound.tag }]
|
||||
: [
|
||||
{ inbound: [TPROXY_INBOUND], outbound: vpnOutbound.tag },
|
||||
{ inbound: [MIXED_INBOUND], outbound: vpnOutbound.tag },
|
||||
];
|
||||
|
||||
return {
|
||||
log: {
|
||||
level: settings.logLevel,
|
||||
timestamp: true,
|
||||
},
|
||||
log: { level: settings.logLevel, timestamp: true },
|
||||
experimental: {
|
||||
cache_file: {
|
||||
enabled: true,
|
||||
path: settings.cachePath,
|
||||
},
|
||||
},
|
||||
dns: {
|
||||
independent_cache: true,
|
||||
cache_file: { enabled: true, path: settings.cachePath },
|
||||
},
|
||||
dns: { independent_cache: true },
|
||||
inbounds,
|
||||
outbounds: [
|
||||
...(sharedOutbound ? [sharedOutbound] : vpnOutbound ? [vpnOutbound] : []),
|
||||
{ type: "direct", tag: "direct" },
|
||||
{ type: "block", tag: "block" },
|
||||
vpnOutbound,
|
||||
{ type: 'direct', tag: 'direct' },
|
||||
{ type: 'block', tag: 'block' },
|
||||
],
|
||||
route: {
|
||||
rule_set: bypassAll || clientMode ? [] : ruleSets(customRuleSets, vpnOutbound.tag),
|
||||
rules: bypassAll
|
||||
? [{ ip_is_private: true, outbound: "direct" }]
|
||||
: clientMode
|
||||
? proxyOnlyRules
|
||||
: routeRules(subscriptionConfig.customRules, vpnOutbound.tag, {
|
||||
includeTransparent: !clientMode,
|
||||
}),
|
||||
final: "direct",
|
||||
rule_set: [],
|
||||
rules,
|
||||
final: vpnOutbound.tag,
|
||||
...(clientMode ? {} : { auto_detect_interface: true }),
|
||||
},
|
||||
};
|
||||
@@ -354,24 +73,9 @@ export function buildGatewayConfig(
|
||||
|
||||
export function writeSingboxConfig(config) {
|
||||
fs.mkdirSync(path.dirname(settings.configPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
settings.configPath,
|
||||
JSON.stringify(config, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
export function readSingboxConfig() {
|
||||
if (!fs.existsSync(settings.configPath)) return null;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(settings.configPath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
fs.writeFileSync(settings.configPath, JSON.stringify(config, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
export function removeSingboxConfig() {
|
||||
if (fs.existsSync(settings.configPath)) {
|
||||
fs.rmSync(settings.configPath);
|
||||
}
|
||||
fs.rmSync(settings.configPath, { force: true });
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { settings } from "./config.js";
|
||||
import { deviceCidrs, normalizeCidr } from "./devices.js";
|
||||
|
||||
const DEFAULT_NAT_BYPASS_CIDRS =
|
||||
"0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 240.0.0.0/4";
|
||||
|
||||
function splitCidrs(value) {
|
||||
return String(value || "")
|
||||
.split(/[\s,]+/)
|
||||
.map((item) => normalizeCidr(item))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function unique(list) {
|
||||
return [...new Set(list)];
|
||||
}
|
||||
|
||||
export function sourceBypassCidrs(
|
||||
profiles,
|
||||
envCidrs = process.env.TPROXY_BYPASS_SOURCE_CIDRS || "",
|
||||
) {
|
||||
return unique([
|
||||
...splitCidrs(envCidrs),
|
||||
...deviceCidrs(profiles?.devices || [], "bypass"),
|
||||
]);
|
||||
}
|
||||
|
||||
export function buildSourceBypassIptablesCommands(
|
||||
cidrs,
|
||||
{
|
||||
chain = settings.tproxySourceBypassChain,
|
||||
forwardChain = settings.tproxySourceForwardChain,
|
||||
natChain = settings.tproxySourceNatChain,
|
||||
natBypassCidrs = splitCidrs(
|
||||
process.env.BYPASS_CIDRS || DEFAULT_NAT_BYPASS_CIDRS,
|
||||
),
|
||||
} = {},
|
||||
) {
|
||||
return [
|
||||
["-w", "-t", "mangle", "-F", chain],
|
||||
["-w", "-F", forwardChain],
|
||||
["-w", "-t", "nat", "-F", natChain],
|
||||
...cidrs.map((cidr) => [
|
||||
"-w",
|
||||
"-t",
|
||||
"mangle",
|
||||
"-A",
|
||||
chain,
|
||||
"-s",
|
||||
cidr,
|
||||
"-j",
|
||||
"ACCEPT",
|
||||
]),
|
||||
...cidrs.flatMap((cidr) => [
|
||||
["-w", "-A", forwardChain, "-s", cidr, "-j", "ACCEPT"],
|
||||
[
|
||||
"-w",
|
||||
"-A",
|
||||
forwardChain,
|
||||
"-d",
|
||||
cidr,
|
||||
"-m",
|
||||
"conntrack",
|
||||
"--ctstate",
|
||||
"RELATED,ESTABLISHED",
|
||||
"-j",
|
||||
"ACCEPT",
|
||||
],
|
||||
]),
|
||||
...natBypassCidrs.map((cidr) => [
|
||||
"-w",
|
||||
"-t",
|
||||
"nat",
|
||||
"-A",
|
||||
natChain,
|
||||
"-d",
|
||||
cidr,
|
||||
"-j",
|
||||
"RETURN",
|
||||
]),
|
||||
...cidrs.map((cidr) => [
|
||||
"-w",
|
||||
"-t",
|
||||
"nat",
|
||||
"-A",
|
||||
natChain,
|
||||
"-s",
|
||||
cidr,
|
||||
"-j",
|
||||
"MASQUERADE",
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
export function syncTproxySourceBypass(profiles, options = {}) {
|
||||
if (settings.appMode !== "gateway") {
|
||||
return { success: true, skipped: true, cidrs: [] };
|
||||
}
|
||||
|
||||
const cidrs = sourceBypassCidrs(
|
||||
profiles,
|
||||
options.envCidrs ?? process.env.TPROXY_BYPASS_SOURCE_CIDRS,
|
||||
);
|
||||
const commands = buildSourceBypassIptablesCommands(cidrs, options);
|
||||
|
||||
for (const args of commands) {
|
||||
const result = spawnSync("iptables", args, {
|
||||
encoding: "utf8",
|
||||
timeout: 1000,
|
||||
});
|
||||
if (result.error || result.status !== 0) {
|
||||
return {
|
||||
success: false,
|
||||
cidrs,
|
||||
error:
|
||||
result.error?.message ||
|
||||
(result.stderr || result.stdout || "iptables command failed").trim(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, cidrs };
|
||||
}
|
||||
Reference in New Issue
Block a user