Refine routing defaults for global and device fallbacks
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 17s

This commit is contained in:
2026-05-09 09:53:12 +03:00
parent 62b39cdf58
commit aab7533438
14 changed files with 695 additions and 160 deletions

View File

@@ -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,
},
};