Refine routing defaults for global and device fallbacks
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 17s
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 17s
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
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";
|
||||
|
||||
const PROXY_TYPES = new Set([
|
||||
"vless",
|
||||
@@ -100,11 +106,11 @@ function parsePorts(values) {
|
||||
.filter((value) => Number.isInteger(value) && value > 0 && value <= 65535);
|
||||
}
|
||||
|
||||
function toSingboxRule(customRule, vpnTag) {
|
||||
function toSingboxRule(customRule, vpnTag, baseRule = {}) {
|
||||
if (!customRule?.enabled) return null;
|
||||
if (!CUSTOM_OUTBOUNDS.has(customRule.outbound)) return null;
|
||||
|
||||
const rule = {};
|
||||
const rule = { ...baseRule };
|
||||
const domains = uniqueClean(customRule.domains);
|
||||
const domainSuffixes = uniqueClean(customRule.domainSuffixes);
|
||||
const domainKeywords = uniqueClean(customRule.domainKeywords);
|
||||
@@ -140,47 +146,57 @@ function toSingboxRule(customRule, vpnTag) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
function customRouteRules(customRules, vpnTag) {
|
||||
function customRouteRules(customRules, vpnTag, baseRule = {}) {
|
||||
return (Array.isArray(customRules) ? customRules : [])
|
||||
.map((rule) => toSingboxRule(rule, vpnTag))
|
||||
.map((rule) => toSingboxRule(rule, vpnTag, baseRule))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
// ─── Device rules (маршрутизация по source IP) ──────────────────────────────
|
||||
|
||||
function readDeviceRules() {
|
||||
try {
|
||||
if (!fs.existsSync(settings.deviceRulesPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(settings.deviceRulesPath, "utf8"));
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
function modeOutbound(mode, vpnTag) {
|
||||
if (mode === "vpn") return vpnTag;
|
||||
if (mode === "direct" || mode === "block") return mode;
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeCidr(ip) {
|
||||
return ip.includes("/") ? ip : `${ip}/32`;
|
||||
}
|
||||
|
||||
function toDeviceRouteRule(device, vpnTag) {
|
||||
function deviceDefaultRouteRule(device, vpnTag) {
|
||||
if (!device?.enabled) return null;
|
||||
const cidrs = (Array.isArray(device.sourceIps) ? device.sourceIps : [])
|
||||
.map((ip) => normalizeCidr(ip.trim()))
|
||||
.filter(Boolean);
|
||||
if (!cidrs.length) return null;
|
||||
const outbound =
|
||||
device.outbound === "vpn" ? vpnTag : device.outbound || "direct";
|
||||
return { source_ip_cidr: cidrs, outbound };
|
||||
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 deviceRouteRules(deviceRules, vpnTag) {
|
||||
return (Array.isArray(deviceRules) ? deviceRules : [])
|
||||
.map((d) => toDeviceRouteRule(d, vpnTag))
|
||||
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) {
|
||||
const deviceRules = readDeviceRules();
|
||||
const deviceProfiles = readDeviceProfiles();
|
||||
const rules = [
|
||||
{
|
||||
ip_is_private: true,
|
||||
@@ -188,17 +204,28 @@ function routeRules(customRules, vpnTag) {
|
||||
},
|
||||
];
|
||||
|
||||
// Правила по устройствам (source IP) — выполняются ДО правил по назначению
|
||||
rules.push(...deviceRouteRules(deviceRules, vpnTag));
|
||||
|
||||
// Global rules apply to every inbound before contextual fallbacks.
|
||||
rules.push(...customRouteRules(customRules, vpnTag));
|
||||
|
||||
if (settings.routingRuDirect) {
|
||||
rules.push({
|
||||
rule_set: ["geoip-ru", "geosite-category-ru"],
|
||||
outbound: "direct",
|
||||
});
|
||||
}
|
||||
const ruRule = ruDirectRule();
|
||||
if (ruRule) rules.push(ruRule);
|
||||
|
||||
// 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);
|
||||
|
||||
const transparentFallback = inboundDefaultRule(
|
||||
TPROXY_INBOUND,
|
||||
deviceProfiles.defaultTransparentMode,
|
||||
vpnTag,
|
||||
);
|
||||
if (transparentFallback) rules.push(transparentFallback);
|
||||
|
||||
return rules;
|
||||
}
|
||||
@@ -263,7 +290,7 @@ export function buildGatewayConfig(
|
||||
rules: bypassAll
|
||||
? [{ ip_is_private: true, outbound: "direct" }]
|
||||
: routeRules(subscriptionConfig.customRules, vpnOutbound.tag),
|
||||
final: bypassAll ? "direct" : vpnOutbound.tag,
|
||||
final: "direct",
|
||||
auto_detect_interface: true,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user