feat(rulesets): add native plain list (.lst/.txt) download and compilation in rule_set & fix OlcRTC/WDTT status

This commit is contained in:
Dushnilin
2026-09-16 22:41:52 +03:00
parent 5eeaa91864
commit 61c01109ad
16 changed files with 1543 additions and 1269 deletions
+85 -3
View File
@@ -2743,11 +2743,44 @@ function import_rule_sets_with_subnets_from_rule(section, settings) {
if (!import_custom_ruleset_subnets_from_local(reference, "source", section, "Rule set " + reference))
ok = false;
}
else if (match(reference, /^https?:\/\//) != null) {
let format = extension == "json" ? "source" : (extension == "srs" ? "binary" : singbox_rulesets_module().remote_format(reference));
if (!import_custom_ruleset_subnets_from_remote(reference, format, section, "Rule set " + reference, settings))
else if (match(reference, /^\/.*\.(lst|txt)$/) != null) {
if (!add_plain_subnet_file_to_nft_for_section(section, reference))
ok = false;
}
else if (match(reference, /^https?:\/\//) != null) {
if (extension == "lst" || extension == "txt" || singbox_rulesets_module().is_plain_list_reference(reference)) {
let tmpfile = temp_path();
if (tmpfile == "") {
ok = false;
} else {
if (download_to_file(reference, tmpfile, service_proxy_address(settings, "lists")) && file_nonempty(tmpfile)) {
convert_crlf_to_lf(tmpfile);
if (!add_plain_subnet_file_to_nft_for_section(section, tmpfile))
ok = false;
let tag_name = "inline-custom-" + singbox_rulesets_module().hash12(reference) + "-ruleset";
let srs_path = TMP_RULESET_FOLDER + "/" + tag_name + ".srs";
let json_path = TMP_RULESET_FOLDER + "/" + tag_name + ".json";
let etc_srs = "/etc/tachyon/rulesets/" + tag_name + ".srs";
let etc_json = "/etc/tachyon/rulesets/" + tag_name + ".json";
common.ensure_dir("/etc/tachyon/rulesets");
common.ensure_dir(TMP_RULESET_FOLDER);
let res = routing_rulesets_module().compile_plain_list(tmpfile, srs_path, json_path);
if (res.format == "binary")
common.copy_file(srs_path, etc_srs);
else
common.copy_file(json_path, etc_json);
} else {
log_message("Failed to download remote plain list " + reference + "; keeping existing cache", "warn");
}
remove_file(tmpfile);
}
}
else {
let format = extension == "json" ? "source" : (extension == "srs" ? "binary" : singbox_rulesets_module().remote_format(reference));
if (!import_custom_ruleset_subnets_from_remote(reference, format, section, "Rule set " + reference, settings))
ok = false;
}
}
else {
log_message("Unsupported rule set reference for subnet import: " + reference, "error");
ok = false;
@@ -2757,6 +2790,52 @@ function import_rule_sets_with_subnets_from_rule(section, settings) {
return ok;
}
function update_remote_plain_rulesets_from_rule(section, settings) {
if (!bool_option(section, "enabled", true))
return true;
let rulesets = connections.rule_sets(section);
if (length(rulesets) == 0)
return true;
let ok = true;
for (let reference in rulesets) {
reference = as_string(reference);
if (match(reference, /^https?:\/\//) == null)
continue;
if (!singbox_rulesets_module().is_plain_list_reference(reference))
continue;
log_message("Updating remote plain rule set for '" + section_name(section) + "': " + reference, "info");
let tmpfile = temp_path();
if (tmpfile == "") {
ok = false;
continue;
}
if (download_to_file(reference, tmpfile, service_proxy_address(settings, "lists")) && file_nonempty(tmpfile)) {
convert_crlf_to_lf(tmpfile);
let tag_name = "inline-custom-" + singbox_rulesets_module().hash12(reference) + "-ruleset";
let srs_path = TMP_RULESET_FOLDER + "/" + tag_name + ".srs";
let json_path = TMP_RULESET_FOLDER + "/" + tag_name + ".json";
let etc_srs = "/etc/tachyon/rulesets/" + tag_name + ".srs";
let etc_json = "/etc/tachyon/rulesets/" + tag_name + ".json";
common.ensure_dir("/etc/tachyon/rulesets");
common.ensure_dir(TMP_RULESET_FOLDER);
let res = routing_rulesets_module().compile_plain_list(tmpfile, srs_path, json_path);
if (res.format == "binary")
common.copy_file(srs_path, etc_srs);
else
common.copy_file(json_path, etc_json);
}
else {
log_message("Failed to download remote plain rule set " + reference + "; keeping existing cache", "warn");
}
remove_file(tmpfile);
}
return ok;
}
function import_domains_from_remote_plain_file(url, section, settings) {
let tmpfile = temp_path();
if (tmpfile == "")
@@ -3155,6 +3234,9 @@ function list_update() {
for (let section in sections)
if (!import_domains_from_remote_domain_lists(section, settings))
ok = false;
for (let section in sections)
if (!update_remote_plain_rulesets_from_rule(section, settings))
ok = false;
// Фаза 2: применение к nft (захватываем лок reload, чтобы не конфликтовать
// с reload-firewall, который пересоздаёт таблицы и сеты nftables)
+3 -4
View File
@@ -363,8 +363,9 @@ function ruleset_reference_valid(reference, community_services) {
return reference == "" ||
community_service_valid(reference, community_services) ||
(remote_reference(reference) && !remote_plain_list_reference(reference)) ||
absolute_reference_with_extension(reference, ".srs", ".json");
remote_reference(reference) ||
absolute_reference_with_extension(reference, ".srs", ".json") ||
absolute_reference_with_extension(reference, ".lst", ".txt");
}
function plain_domain_ip_list_reference_valid(reference) {
@@ -1223,8 +1224,6 @@ function validate_service_value(service, context) {
}
function validate_ruleset_reference_value(reference, context) {
if (remote_plain_list_reference(reference))
fail_validation("Plain .lst/.txt list '" + reference + "' belongs in domain_ip_lists, not rule_set. Aborted.");
if (ruleset_reference_valid(reference, context.community_services))
return;
+13
View File
@@ -538,6 +538,18 @@ function remove_file(path) {
try { fs.unlink(path); return true; } catch (e) { return true; }
}
function copy_file(source, target) {
source = as_string(source);
target = as_string(target);
let data = fs.readfile(source);
if (data == null)
return false;
let slash = rindex(target, "/");
if (slash > 0)
ensure_dir(substr(target, 0, slash));
return fs.writefile(target, data) != null;
}
function unlink_file(path) {
return remove_file(path);
}
@@ -705,6 +717,7 @@ return {
command_output_from_args,
ensure_dir,
remove_file,
copy_file,
unlink_file,
write_file,
file_exists,
@@ -1218,6 +1218,10 @@ function build_system_info() {
let zapret2_version = zapret2_installed ? provider_version(ZAPRET2_RUNTIME_UC) : "not installed";
let byedpi_installed = provider_installed(BYEDPI_RUNTIME_UC) ? 1 : 0;
let byedpi_version = byedpi_installed ? provider_version(BYEDPI_RUNTIME_UC) : "not installed";
let wdtt_installed = provider_installed(WDTT_RUNTIME_UC) ? 1 : 0;
let wdtt_version = wdtt_installed ? provider_version(WDTT_RUNTIME_UC) : "not installed";
let olcrtc_installed = provider_installed(OLCRTC_RUNTIME_UC) ? 1 : 0;
let olcrtc_version = olcrtc_installed ? provider_version(OLCRTC_RUNTIME_UC) : "not installed";
let tailscale_installed = provider_installed(TAILSCALE_RUNTIME_UC) ? 1 : 0;
let tailscale_version = tailscale_installed ? provider_version(TAILSCALE_RUNTIME_UC) : "not installed";
let fptn_installed = provider_installed(FPTN_RUNTIME_UC) ? 1 : 0;
@@ -1261,6 +1265,8 @@ function build_system_info() {
let zapret_meta = read_backup_meta("zapret");
let zapret2_meta = read_backup_meta("zapret2");
let byedpi_meta = read_backup_meta("byedpi");
let wdtt_meta = read_backup_meta("wdtt");
let olcrtc_meta = read_backup_meta("olcrtc");
let tailscale_meta = read_backup_meta("tailscale");
let fptn_meta = read_backup_meta("fptn");
@@ -1290,6 +1296,14 @@ function build_system_info() {
byedpi_installed,
byedpi_backup_version: byedpi_meta ? as_string(byedpi_meta.version) : "",
byedpi_backup_time: byedpi_meta ? int(byedpi_meta.timestamp || 0) : 0,
wdtt_version,
wdtt_installed,
wdtt_backup_version: wdtt_meta ? as_string(wdtt_meta.version) : "",
wdtt_backup_time: wdtt_meta ? int(wdtt_meta.timestamp || 0) : 0,
olcrtc_version,
olcrtc_installed,
olcrtc_backup_version: olcrtc_meta ? as_string(olcrtc_meta.version) : "",
olcrtc_backup_time: olcrtc_meta ? int(olcrtc_meta.timestamp || 0) : 0,
tailscale_version,
tailscale_installed,
tailscale_backup_version: tailscale_meta ? as_string(tailscale_meta.version) : "",
@@ -31,6 +31,7 @@ let bool_option = common.bool_option;
let list_option = common.list_option;
let section_name = common.section_name;
let read_json_file = common.read_json_file;
let write_json = common.write_json;
function yaml_quote(value) {
value = as_string(value);
@@ -70,6 +71,12 @@ function package_version() {
let ver = trim(fs.readfile("/etc/olcrtc/.version") || "");
if (ver != "")
return ver;
let pkgs = require("core.packages");
if (pkgs && pkgs.version) {
let v = pkgs.version("olcrtc");
if (v != "")
return v;
}
return "";
}
@@ -32,6 +32,7 @@ let list_option = common.list_option;
let section_name = common.section_name;
let read_json_file = common.read_json_file;
let write_json_file = common.write_json_file;
let write_json = common.write_json;
function lan_mac_address() {
for (let iface in ["br-lan", "lan", "eth0", "eth1"]) {
@@ -84,6 +85,12 @@ function package_version() {
if (ver != "")
return ver;
}
let pkgs = require("core.packages");
if (pkgs && pkgs.version) {
let v = pkgs.version("wdtt") || pkgs.version("qwdtt");
if (v != "")
return v;
}
return "";
}
+29
View File
@@ -469,6 +469,34 @@ function has_rules(path) {
return length(array_or_empty(ruleset.rules)) > 0;
}
function compile_plain_list(plain_path, ruleset_srs_path, ruleset_json_path) {
create_source(ruleset_json_path);
import_plain_list(plain_path, ruleset_json_path, "domain_suffix", "domains", "5000");
import_plain_list(plain_path, ruleset_json_path, "ip_cidr", "subnets", "5000");
let has_doms = has_domain_matchers(ruleset_json_path);
let rules_exist = has_rules(ruleset_json_path);
if (rules_exist) {
let compile_ok = common.command_success_from_args([
"sing-box", "rule-set", "compile", ruleset_json_path, "-o", ruleset_srs_path
]);
let srs_st = fs.stat(ruleset_srs_path);
if (compile_ok && srs_st != null && srs_st.size >= 16) {
fs.unlink(ruleset_json_path);
return { format: "binary", path: ruleset_srs_path, has_domains: has_doms };
}
return { format: "source", path: ruleset_json_path, has_domains: has_doms };
}
let rulesets_mod = null;
try { rulesets_mod = require("singbox.rulesets"); } catch (e) {}
if (rulesets_mod && rulesets_mod.ensure_empty_srs_stub)
rulesets_mod.ensure_empty_srs_stub(ruleset_srs_path);
fs.unlink(ruleset_json_path);
return { format: "binary", path: ruleset_srs_path, has_domains: false };
}
function module_exports() {
return {
create_source,
@@ -480,6 +508,7 @@ function module_exports() {
extract_ip_cidr_nft_elements,
has_domain_matchers,
has_rules,
compile_plain_list,
ruleset_tag,
read_json_file,
write_json_file
+17 -5
View File
@@ -751,11 +751,19 @@ function has_community_subnet_list(value) {
return rule_config.has_community_subnet_list(value);
}
function rule_has_list_update_source(enabled, action, community_lists, remote_domain_lists, remote_subnet_lists, rule_set_with_subnets, domain_ip_lists) {
function has_remote_plain_ruleset(value) {
for (let item in split(as_string(value), /[ \t\r\n]+/)) {
if (match(item, /^https?:\/\//) != null && match(item, /\.(lst|txt)$/i) != null)
return true;
}
return false;
}
function rule_has_list_update_source(enabled, action, community_lists, remote_domain_lists, remote_subnet_lists, rule_set_with_subnets, domain_ip_lists, rule_set) {
if (!arg_bool(enabled))
return false;
if (as_string(action) == "dns")
return list_has_remote_references(domain_ip_lists);
return list_has_remote_references(domain_ip_lists) || has_remote_plain_ruleset(rule_set);
if (as_string(action) == "hosts")
return false;
@@ -764,7 +772,8 @@ function rule_has_list_update_source(enabled, action, community_lists, remote_do
as_string(remote_domain_lists) != "" ||
as_string(remote_subnet_lists) != "" ||
as_string(rule_set_with_subnets) != "" ||
list_has_remote_references(domain_ip_lists)
list_has_remote_references(domain_ip_lists) ||
has_remote_plain_ruleset(rule_set)
);
}
@@ -1244,6 +1253,7 @@ function append_list_update_signature_body(body, section) {
body = signature_add_value(body, "lists." + name + ".action", action);
if (action == "dns") {
body = signature_add_value(body, "lists." + name + ".domain_ip_lists", option(section, "domain_ip_lists", ""));
body = signature_add_value(body, "lists." + name + ".rule_set", connections.rule_sets_value(section));
return body;
}
if (action == "hosts") {
@@ -1261,6 +1271,7 @@ function append_list_update_signature_body(body, section) {
body = signature_add_value(body, "lists." + name + ".remote_subnet_lists", option(section, "remote_subnet_lists", ""));
body = signature_add_value(body, "lists." + name + ".rule_set_with_subnets", connections.rule_sets_with_subnets_value(section));
body = signature_add_value(body, "lists." + name + ".domain_ip_lists", option(section, "domain_ip_lists", ""));
body = signature_add_value(body, "lists." + name + ".rule_set", connections.rule_sets_value(section));
return body;
}
@@ -1959,7 +1970,8 @@ function has_list_update_sources_from_sections(sections) {
option(section, "remote_domain_lists", ""),
option(section, "remote_subnet_lists", ""),
connections.rule_sets_with_subnets_value(section),
option(section, "domain_ip_lists", "")
option(section, "domain_ip_lists", ""),
connections.rule_sets_value(section)
))
return true;
@@ -2221,7 +2233,7 @@ else if (mode == "community-service-has-subnet-list")
else if (mode == "filter-community-subnet-lists")
filter_community_subnet_lists(ARGV[1]);
else if (mode == "rule-has-list-update-source")
exit(rule_has_list_update_source(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6], ARGV[7]) ? 0 : 1);
exit(rule_has_list_update_source(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6], ARGV[7], ARGV[8]) ? 0 : 1);
else if (mode == "rule-has-nft-list-update-source")
exit(rule_has_nft_list_update_source(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6]) ? 0 : 1);
else if (mode == "rule-has-subscription-update-source")
+2
View File
@@ -1011,6 +1011,8 @@ function capability_flags() {
zapret_installed: file_executable(ZAPRET_PROVIDER_NFQWS_BIN) ? 1 : 0,
zapret2_installed: file_executable(ZAPRET2_PROVIDER_NFQWS2_BIN) ? 1 : 0,
byedpi_installed: file_executable(BYEDPI_BIN) ? 1 : 0,
wdtt_installed: (file_executable("/usr/bin/qwdtt") || file_executable("/usr/bin/wdtt")) ? 1 : 0,
olcrtc_installed: file_executable("/usr/bin/olcrtc") ? 1 : 0,
server_inbounds_enabled_count: 0
};
@@ -952,9 +952,121 @@ function ensure_custom_ruleset(config, reference) {
return { tag: tag_name, kind };
let extension = runtime_rulesets.file_extension(reference);
if (runtime_rulesets.is_plain_list_reference(reference))
ctx.runtime_generate_unsupported("plain .lst/.txt list '" + reference + "' belongs in domain_ip_lists, not rule_set");
if (substr(reference, 0, 1) == "/") {
let folder = ctx.runtime_ruleset_folder || runtime_ruleset_folder;
common.ensure_dir(folder);
if (runtime_rulesets.is_plain_list_reference(reference)) {
let srs_path = folder + "/" + tag_name + ".srs";
let json_path = folder + "/" + tag_name + ".json";
let etc_srs = "/etc/tachyon/rulesets/" + tag_name + ".srs";
let etc_json = "/etc/tachyon/rulesets/" + tag_name + ".json";
if (substr(reference, 0, 1) == "/") {
if (!helpers.file_is_usable(reference, 1))
return null;
let ref_st = fs.stat(reference);
let srs_st = fs.stat(srs_path);
if (srs_st && runtime_rulesets.is_valid_srs_file(srs_path) && ref_st && srs_st.mtime >= ref_st.mtime) {
push(config.route.rule_set, {
type: "local",
tag: tag_name,
format: "binary",
path: srs_path
});
return { tag: tag_name, kind: "domains" };
}
let res = source_rulesets.compile_plain_list(reference, srs_path, json_path);
push(config.route.rule_set, {
type: "local",
tag: tag_name,
format: res.format,
path: res.path
});
return { tag: tag_name, kind: res.has_domains ? "domains" : "unknown" };
}
else if (substr(reference, 0, 7) == "http://" || substr(reference, 0, 8) == "https://") {
if (runtime_rulesets.is_valid_srs_file(srs_path)) {
push(config.route.rule_set, {
type: "local",
tag: tag_name,
format: "binary",
path: srs_path
});
return { tag: tag_name, kind: "domains" };
}
else if (runtime_rulesets.is_valid_srs_file(etc_srs)) {
common.copy_file(etc_srs, srs_path);
push(config.route.rule_set, {
type: "local",
tag: tag_name,
format: "binary",
path: srs_path
});
return { tag: tag_name, kind: "domains" };
}
else if (fs.stat(json_path) != null) {
push(config.route.rule_set, {
type: "local",
tag: tag_name,
format: "source",
path: json_path
});
return { tag: tag_name, kind: "domains" };
}
else if (fs.stat(etc_json) != null) {
common.copy_file(etc_json, json_path);
push(config.route.rule_set, {
type: "local",
tag: tag_name,
format: "source",
path: json_path
});
return { tag: tag_name, kind: "domains" };
}
else {
let tmp_plain = folder + "/" + tag_name + ".tmp.plain";
let dl_ok = false;
if (common.command_success("command -v curl"))
dl_ok = common.command_success_from_args([ "curl", "-fsSL", "--connect-timeout", "5", "-m", "10", reference, "-o", tmp_plain ]);
else if (common.command_success("command -v wget"))
dl_ok = common.command_success_from_args([ "wget", "-q", "-T", "10", "-O", tmp_plain, reference ]);
if (dl_ok && helpers.file_is_usable(tmp_plain, 1)) {
common.ensure_dir("/etc/tachyon/rulesets");
let res = source_rulesets.compile_plain_list(tmp_plain, srs_path, json_path);
if (res.format == "binary")
common.copy_file(srs_path, etc_srs);
else
common.copy_file(json_path, etc_json);
fs.unlink(tmp_plain);
push(config.route.rule_set, {
type: "local",
tag: tag_name,
format: res.format,
path: res.path
});
return { tag: tag_name, kind: res.has_domains ? "domains" : "unknown" };
}
else {
fs.unlink(tmp_plain);
runtime_rulesets.ensure_empty_srs_stub(srs_path);
push(config.route.rule_set, {
type: "local",
tag: tag_name,
format: "binary",
path: srs_path
});
return { tag: tag_name, kind: "domains" };
}
}
}
else {
ctx.runtime_generate_unsupported("plain list reference is not supported by sing-box config generation");
}
}
else if (substr(reference, 0, 1) == "/") {
if (extension != "srs" && extension != "json")
ctx.runtime_generate_unsupported("local rule_set extension is not supported by sing-box config generation");
// Skip broken local ruleset files — nonexistent or suspiciously small