feat: добавлены функции для работы с пользовательскими rule-sets
Добавлены новые API-методы для получения и сохранения пользовательских rule-sets. Обновлены компоненты для работы с этими данными, включая интерфейс для добавления и удаления rule-sets. Refs: None
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user