feat: добавлены функции для работы с пользовательскими rule-sets

Добавлены новые API-методы для получения и сохранения пользовательских rule-sets. Обновлены компоненты для работы с этими данными, включая интерфейс для добавления и удаления rule-sets.

Refs: None
This commit is contained in:
2026-05-08 19:49:44 +03:00
parent 3e18b833c6
commit 27b71077b1
7 changed files with 283 additions and 21 deletions

View File

@@ -14,6 +14,7 @@ export const settings = {
cachePath: process.env.SING_BOX_CACHE || "/var/lib/sing-box/cache.db",
statePath: path.join(dataDir, "state.json"),
customRulesPath: path.join(dataDir, "custom-rules.json"),
customRuleSetsPath: path.join(dataDir, "custom-rule-sets.json"),
subscriptionCachePath: path.join(dataDir, "subscription-cache.json"),
hwidPath: path.join(dataDir, "hwid"),
routingRuDirect: String(process.env.ROUTING_RU_DIRECT || "true") !== "false",

View File

@@ -309,6 +309,9 @@ function normalizeCustomRules(input) {
networks: normalizeList(rule.networks).filter((network) =>
["tcp", "udp"].includes(network),
),
ruleSets: normalizeList(rule.ruleSets).filter((tag) =>
/^[a-z0-9][a-z0-9-]*$/i.test(tag),
),
}));
}
@@ -424,6 +427,28 @@ async function handleApi(req, res) {
});
}
if (req.method === "GET" && req.url === "/api/rule-sets") {
return sendJson(res, 200, {
success: true,
ruleSets: readJson(settings.customRuleSetsPath, []),
});
}
if (req.method === "PUT" && req.url === "/api/rule-sets") {
const body = await readBody(req);
const rawSets = Array.isArray(body.ruleSets) ? body.ruleSets : [];
const normalized = rawSets
.filter((rs) => rs && rs.tag && rs.url)
.map((rs) => ({
tag: String(rs.tag).trim(),
url: String(rs.url).trim(),
format: rs.format === "source" ? "source" : "binary",
}))
.filter((rs) => /^[a-z0-9][a-z0-9-]*$/i.test(rs.tag));
writeJson(settings.customRuleSetsPath, normalized);
return sendJson(res, 200, { success: true, ruleSets: normalized });
}
if (req.method === "POST" && req.url === "/api/route/check") {
const body = await readBody(req);
const host = String(body.host || "").trim();

View File

@@ -33,25 +33,50 @@ function findOutbound(subscriptionConfig, selectedTag) {
);
}
function ruleSets() {
if (!settings.routingRuDirect) return [];
function readCustomRuleSets() {
try {
if (!fs.existsSync(settings.customRuleSetsPath)) return [];
const data = JSON.parse(fs.readFileSync(settings.customRuleSetsPath, "utf8"));
return Array.isArray(data) ? data : [];
} catch {
return [];
}
}
return [
{
function ruleSets(customRuleSets = []) {
const builtIn = settings.routingRuDirect
? [
{
type: "remote",
tag: "geoip-ru",
format: "binary",
url: "https://cdn.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-ru.srs",
download_detour: "direct",
},
{
type: "remote",
tag: "geosite-category-ru",
format: "binary",
url: "https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-category-ru.srs",
download_detour: "direct",
},
]
: [];
const custom = (Array.isArray(customRuleSets) ? customRuleSets : [])
.filter((rs) => rs.tag && rs.url)
.map((rs) => ({
type: "remote",
tag: "geoip-ru",
format: "binary",
url: "https://cdn.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-ru.srs",
tag: String(rs.tag).trim(),
format: rs.format || "binary",
url: String(rs.url).trim(),
download_detour: "direct",
},
{
type: "remote",
tag: "geosite-category-ru",
format: "binary",
url: "https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-category-ru.srs",
download_detour: "direct",
},
];
}));
// Пользовательские rule-sets не должны дублировать встроенные
const builtInTags = new Set(builtIn.map((rs) => rs.tag));
const merged = [...builtIn, ...custom.filter((rs) => !builtInTags.has(rs.tag))];
return merged;
}
function uniqueClean(values) {
@@ -91,13 +116,17 @@ function toSingboxRule(customRule, vpnTag) {
if (ports.length) rule.port = ports;
if (networks.length) rule.network = networks;
const ruleSetsRef = uniqueClean(customRule.ruleSets);
if (ruleSetsRef.length) rule.rule_set = ruleSetsRef;
if (
!rule.domain &&
!rule.domain_suffix &&
!rule.domain_keyword &&
!rule.ip_cidr &&
!rule.port &&
!rule.network
!rule.network &&
!rule.rule_set
) {
return null;
}
@@ -144,6 +173,8 @@ export function buildGatewayConfig(subscriptionConfig, selectedTag) {
vpnOutbound.packet_encoding = "xudp";
}
const customRuleSets = readCustomRuleSets();
return {
log: {
level: settings.logLevel,
@@ -182,7 +213,7 @@ export function buildGatewayConfig(subscriptionConfig, selectedTag) {
{ type: "block", tag: "block" },
],
route: {
rule_set: ruleSets(),
rule_set: ruleSets(customRuleSets),
rules: routeRules(subscriptionConfig.customRules, vpnOutbound.tag),
final: vpnOutbound.tag,
auto_detect_interface: true,