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 = "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(); 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, })); }