feat: добавлены правила маршрутизации по устройствам и управление ими через API
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 19s
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 19s
Refs: None
This commit is contained in:
@@ -15,6 +15,7 @@ export const settings = {
|
||||
statePath: path.join(dataDir, "state.json"),
|
||||
customRulesPath: path.join(dataDir, "custom-rules.json"),
|
||||
customRuleSetsPath: path.join(dataDir, "custom-rule-sets.json"),
|
||||
deviceRulesPath: path.join(dataDir, "device-rules.json"),
|
||||
subscriptionCachePath: path.join(dataDir, "subscription-cache.json"),
|
||||
hwidPath: path.join(dataDir, "hwid"),
|
||||
routingRuDirect: String(process.env.ROUTING_RU_DIRECT || "true") !== "false",
|
||||
|
||||
@@ -436,6 +436,7 @@ async function startSingbox() {
|
||||
function publicState() {
|
||||
const state = readJson(settings.statePath, {});
|
||||
const customRules = readJson(settings.customRulesPath, []);
|
||||
const deviceRules = readJson(settings.deviceRulesPath, []);
|
||||
const { subscriptionUrl, ...rest } = state;
|
||||
return {
|
||||
mode: "gateway",
|
||||
@@ -449,6 +450,7 @@ function publicState() {
|
||||
subscriptionHost: maskSubscriptionUrl(subscriptionUrl),
|
||||
hasSubscription: Boolean(subscriptionUrl),
|
||||
customRules,
|
||||
deviceRules,
|
||||
appliedHistory: state.appliedHistory || [],
|
||||
rulesUpdatedAt: state.rulesUpdatedAt || null,
|
||||
rulesAppliedAt: state.rulesAppliedAt || null,
|
||||
@@ -492,6 +494,21 @@ function normalizeCustomRules(input) {
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeDeviceRules(input) {
|
||||
const rules = Array.isArray(input) ? input : [];
|
||||
return rules.map((r, index) => ({
|
||||
id: String(r.id || `dev-${Date.now()}-${index}`),
|
||||
name: String(r.name || `Устройство ${index + 1}`).trim(),
|
||||
enabled: r.enabled !== false,
|
||||
sourceIps: normalizeList(r.sourceIps).filter((ip) =>
|
||||
/^[\.\d:/]+$/.test(ip),
|
||||
),
|
||||
outbound: ["direct", "vpn", "block"].includes(r.outbound)
|
||||
? r.outbound
|
||||
: "direct",
|
||||
}));
|
||||
}
|
||||
|
||||
async function applySelectedServer(selectedTag) {
|
||||
const cached = readJson(settings.subscriptionCachePath, null);
|
||||
if (!cached?.config) {
|
||||
@@ -679,6 +696,20 @@ async function handleApi(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === "GET" && req.url === "/api/device-rules") {
|
||||
return sendJson(res, 200, {
|
||||
success: true,
|
||||
deviceRules: readJson(settings.deviceRulesPath, []),
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === "PUT" && req.url === "/api/device-rules") {
|
||||
const body = await readBody(req);
|
||||
const rules = normalizeDeviceRules(body.deviceRules);
|
||||
writeJson(settings.deviceRulesPath, rules);
|
||||
return sendJson(res, 200, { success: true, deviceRules: rules });
|
||||
}
|
||||
|
||||
if (req.method === "GET" && req.url === "/api/rule-sets") {
|
||||
return sendJson(res, 200, {
|
||||
success: true,
|
||||
|
||||
@@ -146,7 +146,41 @@ function customRouteRules(customRules, vpnTag) {
|
||||
.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 normalizeCidr(ip) {
|
||||
return ip.includes("/") ? ip : `${ip}/32`;
|
||||
}
|
||||
|
||||
function toDeviceRouteRule(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 };
|
||||
}
|
||||
|
||||
function deviceRouteRules(deviceRules, vpnTag) {
|
||||
return (Array.isArray(deviceRules) ? deviceRules : [])
|
||||
.map((d) => toDeviceRouteRule(d, vpnTag))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function routeRules(customRules, vpnTag) {
|
||||
const deviceRules = readDeviceRules();
|
||||
const rules = [
|
||||
{
|
||||
ip_is_private: true,
|
||||
@@ -154,6 +188,9 @@ function routeRules(customRules, vpnTag) {
|
||||
},
|
||||
];
|
||||
|
||||
// Правила по устройствам (source IP) — выполняются ДО правил по назначению
|
||||
rules.push(...deviceRouteRules(deviceRules, vpnTag));
|
||||
|
||||
rules.push(...customRouteRules(customRules, vpnTag));
|
||||
|
||||
if (settings.routingRuDirect) {
|
||||
|
||||
Reference in New Issue
Block a user