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

153
src/server/devices.js Normal file
View File

@@ -0,0 +1,153 @@
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"]);
export const DEFAULT_DEVICE_MODES = new Set(["direct", "vpn", "block"]);
export const DEFAULT_DEVICE_MODE = "direct";
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();
if (value === "bypass") return "direct";
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,
}));
}