Add DNS action for routing sections

This commit is contained in:
ushan0v
2026-07-13 00:28:22 +03:00
parent 8d82681fcb
commit 8a7e25c6ed
17 changed files with 1865 additions and 1127 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+19 -10
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: FORKOP\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-12 20:23+0300\n"
"PO-Revision-Date: 2026-07-12 20:23+0300\n"
"POT-Creation-Date: 2026-07-12 00:20+0300\n"
"PO-Revision-Date: 2026-07-12 00:20+0300\n"
"Last-Translator: ushan0v\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -71,11 +71,11 @@ msgstr "Добавьте хотя бы один DNS-сервер"
msgid "Add prefix to nodes"
msgstr "Добавлять префикс к узлам"
msgid "Add URLs or local paths to .lst lists"
msgstr "Добавляйте URL или локальные пути к спискам .lst"
msgid "Add URLs or local paths to .lst lists. Only domain rules are supported."
msgstr "Добавьте URL или локальные пути к спискам .lst. Поддерживаются только доменные правила."
msgid "Add URLs or local paths to .srs / .json lists"
msgstr "Добавьте URL или локальные пути к спискам .srs / .json"
msgid "Add URLs or local paths to .srs / .json lists. Only domain rules are supported."
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Поддерживаются только доменные правила."
msgid "Additional marking rules found"
msgstr "Найдены дополнительные правила маркировки"
@@ -467,6 +467,9 @@ msgstr "Стратегия DNS"
msgid "DNS through proxy"
msgstr "DNS через прокси"
msgid "DNS through section"
msgstr "DNS через секцию"
msgid "DNS Unavailability Timeout"
msgstr "Таймаут недоступности DNS"
@@ -485,8 +488,8 @@ msgstr "Не тестировать указанные серверы."
msgid "Does not resolve"
msgstr "Не резолвится"
msgid "Domain and IP Lists"
msgstr "Списки доменов и IPs"
msgid "Domain lists"
msgstr "Списки доменов"
msgid "Domain Resolver"
msgstr "Резолвер доменов"
@@ -587,6 +590,9 @@ msgstr "Введите отображаемое имя"
msgid "Enter a level name"
msgstr "Введите название уровня"
msgid "Enter a valid DNS server address"
msgstr "Укажите корректный адрес DNS-сервера"
msgid "Enter HWID"
msgstr "Укажите HWID"
@@ -1505,6 +1511,9 @@ msgstr "Маршрут"
msgid "Route main DNS requests through the selected section."
msgstr "Маршрутизировать основные DNS-запросы через выбранную секцию."
msgid "Route requests to this DNS server through another section."
msgstr "Направлять запросы к этому DNS-серверу через другую секцию."
msgid "Routing mode"
msgstr "Режим маршрутизации"
@@ -1580,8 +1589,8 @@ msgstr "Выберите распространённый профиль кли
msgid "Select a network interface"
msgstr "Выберите сетевой интерфейс"
msgid "Select a predefined list for routing"
msgstr "Выберите предустановленный набор правил для маршрутизации"
msgid "Select a predefined domain list"
msgstr "Выберите готовый список доменов"
msgid "Select a section"
msgstr "Выберите секцию"
+13 -3
View File
@@ -576,7 +576,12 @@ function rule_has_list_update_source(section) {
if (rule_config == null)
rule_config = require("config.rule");
return bool_option(section, "enabled", true) && (
if (!bool_option(section, "enabled", true))
return false;
if (option(section, "action", "") == "dns")
return list_has_remote_references(option(section, "domain_ip_lists", ""));
return (
rule_config.has_community_subnet_list(connections.community_lists_value(section)) ||
option(section, "remote_domain_lists", "") != "" ||
option(section, "remote_subnet_lists", "") != "" ||
@@ -2051,11 +2056,12 @@ function import_domain_ip_list_file_into_rulesets(filepath, section) {
let ruleset_filepath = domain_ip_list_ruleset_path(section);
let ok = nft_module_success([ "split-domain-subnet-file", filepath, domains_tmpfile, subnets_tmpfile ]);
let domains_only = option(section, "action", "") == "dns";
if (ok)
ok = ruleset_module_success([ "import-plain-list", domains_tmpfile, ruleset_filepath, "domain_suffix", "domains", "5000" ]);
if (ok)
if (ok && !domains_only)
ok = ruleset_module_success([ "import-plain-list", subnets_tmpfile, ruleset_filepath, "ip_cidr", "subnets", "5000" ]);
if (ok)
if (ok && !domains_only)
ok = add_plain_subnet_file_to_nft_for_section(section, subnets_tmpfile);
remove_files([ domains_tmpfile, subnets_tmpfile ]);
@@ -2107,6 +2113,8 @@ function rebuild_domain_ip_lists_from_rule(section, settings) {
function import_builtin_subnets_from_rule(section, settings) {
if (!bool_option(section, "enabled", true))
return true;
if (option(section, "action", "") == "dns")
return true;
let ok = true;
for (let service in connections.community_lists(section)) {
@@ -2218,6 +2226,8 @@ function import_custom_ruleset_subnets_from_remote(url, format, section, label,
function import_rule_sets_with_subnets_from_rule(section, settings) {
if (!bool_option(section, "enabled", true))
return true;
if (option(section, "action", "") == "dns")
return true;
let references = connections.rule_sets_with_subnets(section);
if (length(references) == 0)
+54 -5
View File
@@ -814,7 +814,7 @@ function rule_action(section) {
}
function rule_action_supported(action) {
return contains([ "connection", "proxy", "outbound", "vpn", "bypass", "block", "zapret", "zapret2", "byedpi" ], as_string(action));
return contains([ "connection", "proxy", "outbound", "vpn", "bypass", "block", "dns", "zapret", "zapret2", "byedpi" ], as_string(action));
}
function server_routing_section_action_supported(action) {
@@ -1274,7 +1274,52 @@ function validate_provider_strategy(kind, section, context) {
fail_validation("Invalid ByeDPI strategy for rule '" + name + "': " + result.message);
}
function validate_rule(section, context) {
function dns_action_has_domain_matchers(section) {
for (let key in [ "domain", "domain_suffix", "domain_keyword", "domain_regex" ])
if (option(section, key, "") != "" || option(section, key + "_text", "") != "" || length(list_option(section, key)) > 0)
return true;
return length(connections.community_lists(section)) > 0 ||
length(connections.rule_sets(section)) > 0 ||
length(list_option(section, "domain_ip_lists")) > 0;
}
function validate_dns_action(section, sections, context) {
let name = section_name(section);
let dns_type = option(section, "dns_type", "udp");
if (!contains([ "udp", "dot", "doh" ], dns_type))
fail_validation("DNS rule '" + name + "' uses unsupported protocol '" + dns_type + "'. Use udp, dot, or doh. Aborted.");
let dns_server = option(section, "dns_server", "");
if (!dns_server_value_valid(dns_server))
fail_validation("DNS rule '" + name + "' has an invalid DNS server '" + dns_server + "'. Aborted.");
if (length(connections.rule_sets_with_subnets(section)) > 0)
fail_validation("DNS rule '" + name + "' can use domain-only rule sets, but subnet extraction is enabled. Disable 'Include IP addresses and subnets'. Aborted.");
if (!dns_action_has_domain_matchers(section))
fail_validation("DNS rule '" + name + "' must contain at least one domain condition or domain rule set. Aborted.");
if (!bool_option(section, "dns_detour_enabled", false))
return;
let target_name = option(section, "dns_detour_section", "");
if (target_name == "")
fail_validation("DNS rule '" + name + "' routes DNS through a section, but no section is selected. Aborted.");
if (target_name == name)
fail_validation("DNS rule '" + name + "' cannot use itself as a DNS detour. Aborted.");
let target = null;
for (let candidate in sections)
if (section_name(candidate) == target_name) {
target = candidate;
break;
}
if (target == null)
fail_validation("DNS rule '" + name + "' references missing detour section '" + target_name + "'. Aborted.");
if (!section_enabled(target))
fail_validation("DNS rule '" + name + "' references disabled detour section '" + target_name + "'. Aborted.");
if (!download_section_action_available(rule_action(target), context.byedpi_installed, context.zapret_installed, context.zapret2_installed))
fail_validation("DNS rule '" + name + "' references detour section '" + target_name + "' with unsupported action '" + rule_action(target) + "'. Select an enabled Connection, VPN, Zapret, Zapret2, or ByeDPI section. Aborted.");
}
function validate_rule(section, sections, context) {
if (!section_enabled(section))
return;
@@ -1285,8 +1330,12 @@ function validate_rule(section, context) {
if (!rule_action_supported(action))
fail_validation("Enabled rule '" + name + "' uses unsupported action '" + action + "'. Aborted.");
for (let value in list_option(section, "ports"))
validate_port_condition_value(value, name);
if (action != "dns")
for (let value in list_option(section, "ports"))
validate_port_condition_value(value, name);
if (action == "dns")
validate_dns_action(section, sections, context);
if (action == "zapret") {
if (!context.zapret_installed) {
@@ -1572,7 +1621,7 @@ function validate_runtime_config(context) {
validate_server_routing_sections(sections);
for (let section in sections)
validate_rule(section, context);
validate_rule(section, sections, context);
}
function context_from_runtime() {
+6 -1
View File
@@ -1470,7 +1470,10 @@ function nft_rule_signature_body(body, section) {
if (section_name == "" || !bool_option(section, "enabled", true))
return body;
body = signature_add_value(body, "rule." + section_name + ".action", option(section, "action", ""));
let action = option(section, "action", "");
body = signature_add_value(body, "rule." + section_name + ".action", action);
if (action == "dns")
return body;
body = signature_add_value(body, "rule." + section_name + ".ip_cidr", section_rule_condition_csv(section, "ip_cidr", "subnets"));
body = signature_add_value(body, "rule." + section_name + ".source_ip_cidr", section_rule_condition_csv(section, "source_ip_cidr", "subnets"));
body = signature_add_value(body, "rule." + section_name + ".ports", section_rule_ports_csv(section));
@@ -1612,6 +1615,8 @@ function nft_add_section_source_matchers(section, table, chunk_size_text) {
function nft_populate_runtime_set_for_section(section, deferred_sections, table, common_set, port_set, ip_port_set, interface_set, localv4_set, mark, mangle_chain_context, inserted_fully_routed_ips, common6_set, ip_port6_set, localv6_set) {
if (!bool_option(section, "enabled", true))
return true;
if (section_action(section) == "dns")
return true;
let ports = section_rule_ports_csv(section);
let ip_values = section_rule_condition_csv(section, "ip_cidr", "subnets");
+42 -12
View File
@@ -664,8 +664,13 @@ function has_community_subnet_list(value) {
return rule_config.has_community_subnet_list(value);
}
function rule_has_list_update_source(enabled, community_lists, remote_domain_lists, remote_subnet_lists, rule_set_with_subnets, domain_ip_lists) {
return arg_bool(enabled) && (
function rule_has_list_update_source(enabled, action, community_lists, remote_domain_lists, remote_subnet_lists, rule_set_with_subnets, domain_ip_lists) {
if (!arg_bool(enabled))
return false;
if (as_string(action) == "dns")
return list_has_remote_references(domain_ip_lists);
return (
has_community_subnet_list(community_lists) ||
as_string(remote_domain_lists) != "" ||
as_string(remote_subnet_lists) != "" ||
@@ -674,8 +679,11 @@ function rule_has_list_update_source(enabled, community_lists, remote_domain_lis
);
}
function rule_has_nft_list_update_source(enabled, community_lists, remote_subnet_lists, rule_set_with_subnets, domain_ip_lists) {
return arg_bool(enabled) && (
function rule_has_nft_list_update_source(enabled, action, community_lists, remote_subnet_lists, rule_set_with_subnets, domain_ip_lists) {
if (!arg_bool(enabled) || as_string(action) == "dns")
return false;
return (
has_community_subnet_list(community_lists) ||
as_string(remote_subnet_lists) != "" ||
as_string(rule_set_with_subnets) != "" ||
@@ -873,7 +881,10 @@ function nft_runtime_signature_body(settings, sections) {
if (name == "" || !bool_option(section, "enabled", true))
continue;
body = signature_add_value(body, "rule." + name + ".action", option(section, "action", ""));
let action = option(section, "action", "");
body = signature_add_value(body, "rule." + name + ".action", action);
if (action == "dns")
continue;
body = signature_add_value(body, "rule." + name + ".ip_cidr", section_rule_condition_csv(section, "ip_cidr", "subnets"));
body = signature_add_value(body, "rule." + name + ".source_ip_cidr", section_rule_condition_csv(section, "source_ip_cidr", "subnets"));
body = signature_add_value(body, "rule." + name + ".ports", section_rule_ports_csv(section));
@@ -1046,6 +1057,13 @@ function append_list_update_signature_body(body, section) {
if (name == "" || !bool_option(section, "enabled", true))
return body;
let action = option(section, "action", "");
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", ""));
return body;
}
body = signature_add_value(body, "lists." + name + ".ports", section_rule_ports_csv(section));
body = signature_add_value(body, "lists." + name + ".community_subnet_lists", rule_config.filter_community_subnet_lists_value(connections.community_lists_value(section)));
body = signature_add_value(body, "lists." + name + ".remote_domain_lists", option(section, "remote_domain_lists", ""));
@@ -1212,18 +1230,28 @@ function append_sing_box_rule_signature_body(body, section, sections) {
else if (action == "zapret" || action == "zapret2") {
body = signature_add_mixed_proxy_body(body, section, prefix);
}
else if (action == "dns") {
body = signature_add_value(body, prefix + ".dns_type", option(section, "dns_type", "udp"));
body = signature_add_value(body, prefix + ".dns_server", option(section, "dns_server", ""));
body = signature_add_value(body, prefix + ".dns_detour_enabled", bool_option_value(section, "dns_detour_enabled", false));
if (bool_option(section, "dns_detour_enabled", false))
body = signature_add_value(body, prefix + ".dns_detour_section", option(section, "dns_detour_section", ""));
}
body = signature_add_value(body, prefix + ".domain", section_rule_condition_csv(section, "domain", "domains"));
body = signature_add_value(body, prefix + ".domain_suffix", section_rule_condition_csv(section, "domain_suffix", "domains"));
body = signature_add_value(body, prefix + ".domain_keyword", section_rule_condition_csv(section, "domain_keyword", "generic"));
body = signature_add_value(body, prefix + ".domain_regex", section_rule_condition_csv(section, "domain_regex", "generic"));
body = signature_add_value(body, prefix + ".ip_cidr", section_rule_condition_csv(section, "ip_cidr", "subnets"));
body = signature_add_value(body, prefix + ".source_ip_cidr", section_rule_condition_csv(section, "source_ip_cidr", "subnets"));
body = signature_add_value(body, prefix + ".ports", section_rule_ports_csv(section));
body = signature_add_value(body, prefix + ".fully_routed_ips", option(section, "fully_routed_ips", ""));
if (action != "dns") {
body = signature_add_value(body, prefix + ".ip_cidr", section_rule_condition_csv(section, "ip_cidr", "subnets"));
body = signature_add_value(body, prefix + ".source_ip_cidr", section_rule_condition_csv(section, "source_ip_cidr", "subnets"));
body = signature_add_value(body, prefix + ".ports", section_rule_ports_csv(section));
body = signature_add_value(body, prefix + ".fully_routed_ips", option(section, "fully_routed_ips", ""));
}
body = signature_add_value(body, prefix + ".community_lists", connections.community_lists_value(section));
body = signature_add_value(body, prefix + ".rule_set", connections.rule_sets_value(section));
body = signature_add_value(body, prefix + ".rule_set_with_subnets", connections.rule_sets_with_subnets_value(section));
if (action != "dns")
body = signature_add_value(body, prefix + ".rule_set_with_subnets", connections.rule_sets_with_subnets_value(section));
body = signature_add_value(body, prefix + ".domain_ip_lists", option(section, "domain_ip_lists", ""));
return body;
@@ -1555,6 +1583,7 @@ function has_list_update_sources_from_sections(sections) {
for (let section in sections)
if (rule_has_list_update_source(
bool_option(section, "enabled", true),
option(section, "action", ""),
connections.community_lists_value(section),
option(section, "remote_domain_lists", ""),
option(section, "remote_subnet_lists", ""),
@@ -1570,6 +1599,7 @@ function has_nft_list_update_sources_from_sections(sections) {
for (let section in sections)
if (rule_has_nft_list_update_source(
bool_option(section, "enabled", true),
option(section, "action", ""),
connections.community_lists_value(section),
option(section, "remote_subnet_lists", ""),
connections.rule_sets_with_subnets_value(section),
@@ -1783,9 +1813,9 @@ 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]) ? 0 : 1);
exit(rule_has_list_update_source(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6], ARGV[7]) ? 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]) ? 0 : 1);
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")
exit(rule_has_subscription_update_source(ARGV[1], ARGV[2]) ? 0 : 1);
else if (mode == "service-trigger-signature")
+94 -7
View File
@@ -2255,7 +2255,7 @@ function source_file_exists(path) {
return fs.readfile(path) != null;
}
function rebuild_local_domain_ip_list_ruleset(section_name, references) {
function rebuild_local_domain_ip_list_ruleset(section_name, references, domains_only) {
let ruleset_path = domain_ip_list_ruleset_path(section_name);
let has_local = false;
@@ -2282,15 +2282,16 @@ function rebuild_local_domain_ip_list_ruleset(section_name, references) {
}
source_rulesets.import_plain_list(reference, ruleset_path, "domain_suffix", "domains", "5000");
source_rulesets.import_plain_list(reference, ruleset_path, "ip_cidr", "subnets", "5000");
if (!domains_only)
source_rulesets.import_plain_list(reference, ruleset_path, "ip_cidr", "subnets", "5000");
}
}
function add_domain_ip_list_ruleset(config, section_name, rule_set_tags, dns_rule_set_tags, references) {
function add_domain_ip_list_ruleset(config, section_name, rule_set_tags, dns_rule_set_tags, references, domains_only) {
if (length(references) == 0)
return;
rebuild_local_domain_ip_list_ruleset(section_name, references);
rebuild_local_domain_ip_list_ruleset(section_name, references, domains_only);
let ruleset_path = domain_ip_list_ruleset_path(section_name);
if (!source_rulesets.has_rules(ruleset_path))
@@ -2306,7 +2307,8 @@ function add_domain_ip_list_ruleset(config, section_name, rule_set_tags, dns_rul
});
}
push(rule_set_tags, tag_name);
if (!domains_only)
push(rule_set_tags, tag_name);
if (source_rulesets.has_domain_matchers(ruleset_path))
push(dns_rule_set_tags, tag_name);
}
@@ -2410,6 +2412,84 @@ function single_or_array(values) {
return length(values) == 1 ? values[0] : values;
}
function dns_action_server_tag(section_name) {
return runtime_constants.tag(section_name, "dns-server");
}
function dns_action_detour_tag(section) {
if (!bool_option(section, "dns_detour_enabled", false))
return "";
let target_section = option(section, "dns_detour_section", "");
return target_section == "" ? "" : outbound_tag(target_section);
}
function add_dns_server_for_section(config, section) {
let server = runtime_dns.server_from_options(
dns_action_server_tag(section[".name"]),
option(section, "dns_type", "udp"),
option(section, "dns_server", ""),
dns_action_detour_tag(section)
);
if (server.unsupported)
runtime_generate_unsupported(server.unsupported);
push(config.dns.servers, server);
}
function add_dns_action_rules_for_section(config, section) {
let domain = domain_condition_values(section, "domain");
let domain_suffix = domain_condition_values(section, "domain_suffix");
let domain_keyword = domain_condition_values(section, "domain_keyword");
let domain_regex = domain_condition_values(section, "domain_regex");
let rule_set_tags = [];
let section_name = section[".name"];
for (let community in connections.community_lists(section)) {
let ensured = ensure_community_ruleset(config, section_name, as_string(community));
push(rule_set_tags, ensured.tag);
}
for (let reference in connections.rule_sets(section)) {
let ensured = ensure_custom_ruleset(config, as_string(reference));
if (ensured.kind == "domains")
push(rule_set_tags, ensured.tag);
}
add_domain_ip_list_ruleset(
config,
section_name,
[],
rule_set_tags,
list_option(section, "domain_ip_lists"),
true
);
let rewrite_ttl = int_option(runtime_settings(), "dns_rewrite_ttl", "60");
let server_tag = dns_action_server_tag(section_name);
let has_inline_domains = length(domain) > 0 || length(domain_suffix) > 0 ||
length(domain_keyword) > 0 || length(domain_regex) > 0;
if (has_inline_domains) {
let dns_rule = {
action: "route",
server: server_tag,
rewrite_ttl
};
add_domain_array(dns_rule, "domain", domain);
add_domain_array(dns_rule, "domain_suffix", domain_suffix);
add_domain_array(dns_rule, "domain_keyword", domain_keyword);
add_domain_array(dns_rule, "domain_regex", domain_regex);
push_dns_matcher_rule(config, dns_rule);
}
if (length(rule_set_tags) > 0) {
push_dns_matcher_rule(config, {
action: "route",
server: server_tag,
rewrite_ttl,
rule_set: single_or_array(rule_set_tags)
});
}
if (!has_inline_domains && length(rule_set_tags) == 0)
runtime_generate_unsupported("DNS action '" + section_name + "' has no domain matchers");
}
function normalize_port_number_value(value) {
return rule_config.normalize_port_number_value(value);
}
@@ -2504,7 +2584,8 @@ function add_combined_route_for_section(config, section) {
section_name,
rule_set_tags,
dns_rule_set_tags,
list_option(section, "domain_ip_lists")
list_option(section, "domain_ip_lists"),
false
);
let target = runtime_route.target(section, outbound_tag(section_name));
@@ -2600,6 +2681,9 @@ function add_outbound_for_section(config, section, taken, sections) {
else if (action == "block") {
/* route-only action */
}
else if (action == "dns") {
add_dns_server_for_section(config, section);
}
else {
runtime_generate_unsupported("unsupported action " + action);
}
@@ -2623,7 +2707,10 @@ function reserve_section_outbound_tags(sections, taken) {
}
function add_route_for_section(config, section) {
add_combined_route_for_section(config, section);
if (option(section, "action", "") == "dns")
add_dns_action_rules_for_section(config, section);
else
add_combined_route_for_section(config, section);
}
function add_service_route_rules(config, sections) {
@@ -13,6 +13,17 @@ const ACTION_PROVIDERS_AVAILABILITY_EVENT =
main.FORKOP_ACTION_PROVIDERS_AVAILABILITY_EVENT ||
"forkop:action-providers-availability";
const RULE_SET_ITEM_SETTINGS_KEY = "rule_set_settings";
const ROUTING_ACTIONS = [
"connection",
"proxy",
"outbound",
"vpn",
"bypass",
"block",
"zapret",
"zapret2",
"byedpi",
];
const CONNECTIONS_BLOCKED_INTERFACES = [
"br-lan",
"eth0",
@@ -98,6 +109,49 @@ function refreshOutboundDetourSectionOptionValues(option, sectionId) {
});
}
function isDnsDetourTargetSection(section, currentSectionId) {
const sectionName = getUciSectionName(section);
const action = (section && section.action) || "";
if (
!sectionName ||
sectionName === currentSectionId ||
section.enabled === "0"
) {
return false;
}
if (["connection", "proxy", "outbound", "vpn"].includes(action)) {
return true;
}
if (action === "zapret") {
return isZapretInstalledForUi();
}
if (action === "zapret2") {
return isZapret2InstalledForUi();
}
return action === "byedpi" && isByedpiInstalledForUi();
}
function refreshDnsDetourSectionOptionValues(option, sectionId) {
option.keylist = [];
option.vallist = [];
(uci.sections(UCI_PACKAGE, "section") || [])
.filter((section) => isDnsDetourTargetSection(section, sectionId))
.forEach((targetSection) => {
option.value(
getUciSectionName(targetSection),
getUciSectionLabel(targetSection),
);
});
}
function dependsOnRoutingAction(option) {
ROUTING_ACTIONS.forEach((action) => option.depends("action", action));
return option;
}
const ZAPRET_LEGACY_DEFAULT_NFQWS_OPT =
"--filter-tcp=80 <HOSTLIST> --dpi-desync=fake,fakedsplit --dpi-desync-autottl=2 --dpi-desync-fooling=badsum --new --filter-tcp=443 --hostlist=/opt/zapret/ipset/zapret-hosts-google.txt --dpi-desync=fake,multidisorder --dpi-desync-split-pos=1,midsld --dpi-desync-repeats=11 --dpi-desync-fooling=badsum --dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com --new --filter-udp=443 --hostlist=/opt/zapret/ipset/zapret-hosts-google.txt --dpi-desync=fake --dpi-desync-repeats=11 --dpi-desync-fake-quic=/opt/zapret/files/fake/quic_initial_www_google_com.bin --new --filter-udp=443 <HOSTLIST_NOAUTO> --dpi-desync=fake --dpi-desync-repeats=11 --new --filter-tcp=443 <HOSTLIST> --dpi-desync=multidisorder --dpi-desync-split-pos=1,sniext+1,host+1,midsld-2,midsld,midsld+2,endhost-1";
@@ -3374,6 +3428,8 @@ function getActionOptionLabel(action) {
return "Bypass";
case "connection":
return "Connection";
case "dns":
return "DNS";
case "vpn":
return "VPN";
case "zapret":
@@ -3419,6 +3475,7 @@ function populateActionOptionValues(option) {
option.value("connection", getActionOptionLabel("connection"));
option.value("bypass", "Bypass");
option.value("block", "Block");
option.value("dns", "DNS");
if (isZapretInstalledForUi()) {
option.value("zapret", getActionOptionLabel("zapret"));
}
@@ -6087,6 +6144,8 @@ function addDynamicConditionField(section, config) {
uci.unset(UCI_PACKAGE, section_id, `${config.key}_text`);
uci.unset(UCI_PACKAGE, section_id, `${config.key}_text_mode`);
};
return o;
}
function addLocalDeviceSubnetDynamicField(section, config) {
@@ -6195,6 +6254,8 @@ function addTextConditionField(section, config) {
config.afterWrite(section_id);
}
};
return o;
}
function loadRulesetValues(option) {
@@ -6300,6 +6361,10 @@ function writeBuiltInRulesetReferences(section_id, values) {
function writeCustomRulesetReferences(section_id, values) {
const refs = uniqueDynamicListItems(values);
if (getRuleResolvedAction(section_id) === "dns") {
writeDnsRulesetReferences(section_id, refs);
return;
}
const subnetRefs = getConfigListValues(
section_id,
"rule_set_with_subnets",
@@ -6315,6 +6380,12 @@ function writeCustomRulesetReferences(section_id, values) {
uci.unset(UCI_PACKAGE, section_id, RULE_SET_ITEM_SETTINGS_KEY);
}
function writeDnsRulesetReferences(section_id, values) {
writeListOption(section_id, "rule_set", uniqueDynamicListItems(values));
uci.unset(UCI_PACKAGE, section_id, "rule_set_with_subnets");
uci.unset(UCI_PACKAGE, section_id, RULE_SET_ITEM_SETTINGS_KEY);
}
function createSectionContent(section) {
let o;
@@ -6376,6 +6447,68 @@ function createSectionContent(section) {
return this.cfgvalue(section_id);
});
};
o = section.taboption(
"settings",
form.ListValue,
"dns_type",
_("DNS protocol"),
_("DNS protocol used by the resolver"),
);
o.depends("action", "dns");
dnsTypeChoices().forEach((choice) => o.value(choice.value, choice.label));
o.default = "udp";
o.rmempty = false;
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"dns_server",
_("DNS server"),
_("DNS server used by the resolver"),
);
o.depends("action", "dns");
o.rmempty = false;
o.modalonly = true;
o.validate = function (_section_id, value) {
const normalized = `${value || ""}`.trim();
if (!normalized) {
return _("DNS server address cannot be empty");
}
const validation = main.validateDNS(normalized);
return validation.valid ? true : _("Enter a valid DNS server address");
};
o = section.taboption(
"settings",
form.Flag,
"dns_detour_enabled",
_("DNS through section"),
_("Route requests to this DNS server through another section."),
);
o.depends("action", "dns");
o.default = "0";
o.rmempty = false;
o.modalonly = true;
o = section.taboption(
"settings",
form.ListValue,
"dns_detour_section",
_("DNS requests through section"),
);
o.depends({ action: "dns", dns_detour_enabled: "1" });
o.rmempty = false;
o.modalonly = true;
o.load = function (section_id) {
refreshDnsDetourSectionOptionValues(this, section_id);
return uci.get(UCI_PACKAGE, section_id, "dns_detour_section") || "";
};
o.validate = function (_section_id, value) {
return value ? true : _("Select a section");
};
o = section.taboption(
"settings",
form.TextValue,
@@ -6993,7 +7126,7 @@ function createSectionContent(section) {
},
});
addTextConditionField(section, {
const ipConditionOption = addTextConditionField(section, {
key: "ip_cidr",
optionName: "ip_cidr",
legacyTextOptionName: "ip_cidr_text",
@@ -7001,13 +7134,14 @@ function createSectionContent(section) {
description: _("Match destination IPs or subnets"),
textAnalyze: analyzeIpCidrText,
});
dependsOnRoutingAction(ipConditionOption);
const builtInRulesetOption = section.taboption(
"conditions",
form.DynamicList,
"community_lists",
_("Built-in rule sets"),
_("Select a predefined list for routing"),
_("Select a predefined domain list"),
);
builtInRulesetOption.modalonly = true;
builtInRulesetOption.placeholder = _("Service list");
@@ -7107,9 +7241,12 @@ function createSectionContent(section) {
SettingsDynamicList,
"rule_set",
_("Rule sets"),
_("Add URLs or local paths to .srs / .json lists"),
_(
"Add URLs or local paths to .srs / .json lists. Only domain rules are supported.",
),
);
ruleSetOption.modalonly = true;
dependsOnRoutingAction(ruleSetOption);
ruleSetOption.renderItemSettingsModal = showRuleSetSettingsModal;
ruleSetOption.load = function (section_id) {
return getCustomRulesetReferences(section_id);
@@ -7126,12 +7263,38 @@ function createSectionContent(section) {
return validateCustomRulesetReference(value);
};
const dnsRuleSetOption = section.taboption(
"conditions",
form.DynamicList,
"_dns_rule_set",
_("Rule sets"),
_(
"Add URLs or local paths to .srs / .json lists. Only domain rules are supported.",
),
);
dnsRuleSetOption.depends("action", "dns");
dnsRuleSetOption.modalonly = true;
dnsRuleSetOption.load = function (section_id) {
return getCustomRulesetReferences(section_id);
};
dnsRuleSetOption.write = function (section_id, value) {
writeDnsRulesetReferences(section_id, value);
};
dnsRuleSetOption.remove = function (section_id) {
writeDnsRulesetReferences(section_id, []);
};
dnsRuleSetOption.validate = function (_section_id, value) {
return validateCustomRulesetReference(value);
};
const domainIpListsOption = section.taboption(
"conditions",
form.DynamicList,
"domain_ip_lists",
_("Domain and IP Lists"),
_("Add URLs or local paths to .lst lists"),
_("Domain lists"),
_(
"Add URLs or local paths to .lst lists. Only domain rules are supported.",
),
);
domainIpListsOption.modalonly = true;
domainIpListsOption.load = function (section_id) {
@@ -7141,28 +7304,31 @@ function createSectionContent(section) {
return validatePlainListReference(value);
};
addLocalDeviceSubnetDynamicField(section, {
const sourceIpOption = addLocalDeviceSubnetDynamicField(section, {
key: "source_ip_cidr",
label: _("Device filter"),
description: _(
"Apply section rules only to the specified local IP addresses",
),
});
dependsOnRoutingAction(sourceIpOption);
addLocalDeviceSubnetDynamicField(section, {
const fullyRoutedOption = addLocalDeviceSubnetDynamicField(section, {
key: "fully_routed_ips",
label: _("Forced device routing"),
description: _(
"All traffic from these IP addresses will be routed through the section unconditionally, ignoring all other conditions.",
),
});
dependsOnRoutingAction(fullyRoutedOption);
addDynamicConditionField(section, {
const portsOption = addDynamicConditionField(section, {
key: "ports",
label: _("Ports"),
description: _("Match destination ports. Use a single port or a range"),
dynamicValidate: validatePortCondition,
});
dependsOnRoutingAction(portsOption);
}
function loadSectionTableOptions(sectionRef) {
+19 -10
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: FORKOP\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-12 20:23+0300\n"
"PO-Revision-Date: 2026-07-12 20:23+0300\n"
"POT-Creation-Date: 2026-07-12 00:20+0300\n"
"PO-Revision-Date: 2026-07-12 00:20+0300\n"
"Last-Translator: ushan0v\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -71,11 +71,11 @@ msgstr "Добавьте хотя бы один DNS-сервер"
msgid "Add prefix to nodes"
msgstr "Добавлять префикс к узлам"
msgid "Add URLs or local paths to .lst lists"
msgstr "Добавляйте URL или локальные пути к спискам .lst"
msgid "Add URLs or local paths to .lst lists. Only domain rules are supported."
msgstr "Добавьте URL или локальные пути к спискам .lst. Поддерживаются только доменные правила."
msgid "Add URLs or local paths to .srs / .json lists"
msgstr "Добавьте URL или локальные пути к спискам .srs / .json"
msgid "Add URLs or local paths to .srs / .json lists. Only domain rules are supported."
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Поддерживаются только доменные правила."
msgid "Additional marking rules found"
msgstr "Найдены дополнительные правила маркировки"
@@ -467,6 +467,9 @@ msgstr "Стратегия DNS"
msgid "DNS through proxy"
msgstr "DNS через прокси"
msgid "DNS through section"
msgstr "DNS через секцию"
msgid "DNS Unavailability Timeout"
msgstr "Таймаут недоступности DNS"
@@ -485,8 +488,8 @@ msgstr "Не тестировать указанные серверы."
msgid "Does not resolve"
msgstr "Не резолвится"
msgid "Domain and IP Lists"
msgstr "Списки доменов и IPs"
msgid "Domain lists"
msgstr "Списки доменов"
msgid "Domain Resolver"
msgstr "Резолвер доменов"
@@ -587,6 +590,9 @@ msgstr "Введите отображаемое имя"
msgid "Enter a level name"
msgstr "Введите название уровня"
msgid "Enter a valid DNS server address"
msgstr "Укажите корректный адрес DNS-сервера"
msgid "Enter HWID"
msgstr "Укажите HWID"
@@ -1505,6 +1511,9 @@ msgstr "Маршрут"
msgid "Route main DNS requests through the selected section."
msgstr "Маршрутизировать основные DNS-запросы через выбранную секцию."
msgid "Route requests to this DNS server through another section."
msgstr "Направлять запросы к этому DNS-серверу через другую секцию."
msgid "Routing mode"
msgstr "Режим маршрутизации"
@@ -1580,8 +1589,8 @@ msgstr "Выберите распространённый профиль кли
msgid "Select a network interface"
msgstr "Выберите сетевой интерфейс"
msgid "Select a predefined list for routing"
msgstr "Выберите предустановленный набор правил для маршрутизации"
msgid "Select a predefined domain list"
msgstr "Выберите готовый список доменов"
msgid "Select a section"
msgstr "Выберите секцию"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -107,7 +107,7 @@ function assertCurrentKeepsStableValues(name, required = [], retired = []) {
}
}
assertCurrentKeepsStableValues("action", ["connection", "bypass", "block", "zapret", "zapret2", "byedpi"], ["direct", "proxy", "vpn", "outbound"]);
assertCurrentKeepsStableValues("action", ["connection", "bypass", "block", "zapret", "zapret2", "byedpi", "dns"], ["direct", "proxy", "vpn", "outbound"]);
assertCurrentKeepsStableValues("protocol", ["tailscale", "vless", "vmess", "trojan", "shadowsocks", "hysteria2", "socks", "mtproto", "json_inbound"]);
assertCurrentKeepsStableValues("security", ["reality", "tls", "none"]);
assertCurrentKeepsStableValues("transport", ["tcp", "ws", "grpc", "http", "httpupgrade", "xhttp"]);
+64
View File
@@ -120,6 +120,19 @@ cat >"$WORK_DIR/valid.json" <<'JSON'
"enabled": "1",
"action": "zapret",
"domain_ip_lists": [ "https://example.com/provider.lst" ]
},
{
".name": "custom_dns",
".type": "section",
"enabled": "1",
"action": "dns",
"dns_type": "dot",
"dns_server": "9.9.9.9:5353",
"dns_detour_enabled": "1",
"dns_detour_section": "proxy",
"domain_suffix": [ "dns.example" ],
"rule_set": [ "https://example.com/domain-only.srs" ],
"domain_ip_lists": [ "https://example.com/domains.lst" ]
}
],
"server": [
@@ -226,6 +239,57 @@ cat >"$WORK_DIR/bad-direct-action.json" <<'JSON'
JSON
assert_rejects "bad direct action" "$WORK_DIR/bad-direct-action.json" "unsupported action 'direct'"
cat >"$WORK_DIR/bad-dns-rule-server.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings" },
"section": [
{ ".name": "dns", ".type": "section", "enabled": "1", "action": "dns", "dns_server": "bad value", "domain_suffix": [ "example.org" ] }
]
}
JSON
assert_rejects "bad DNS rule server" "$WORK_DIR/bad-dns-rule-server.json" "invalid DNS server"
cat >"$WORK_DIR/bad-dns-rule-protocol.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings" },
"section": [
{ ".name": "dns", ".type": "section", "enabled": "1", "action": "dns", "dns_type": "tcp", "dns_server": "9.9.9.9", "domain_suffix": [ "example.org" ] }
]
}
JSON
assert_rejects "bad DNS rule protocol" "$WORK_DIR/bad-dns-rule-protocol.json" "unsupported protocol 'tcp'"
cat >"$WORK_DIR/bad-dns-rule-matchers.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings" },
"section": [
{ ".name": "dns", ".type": "section", "enabled": "1", "action": "dns", "dns_server": "9.9.9.9" }
]
}
JSON
assert_rejects "DNS rule without domain matchers" "$WORK_DIR/bad-dns-rule-matchers.json" "must contain at least one domain condition"
cat >"$WORK_DIR/bad-dns-rule-subnets.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings" },
"section": [
{ ".name": "dns", ".type": "section", "enabled": "1", "action": "dns", "dns_server": "9.9.9.9", "rule_set_with_subnets": [ "https://example.com/mixed.srs" ] }
]
}
JSON
assert_rejects "DNS rule subnet extraction" "$WORK_DIR/bad-dns-rule-subnets.json" "domain-only rule sets"
cat >"$WORK_DIR/bad-dns-rule-detour.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings" },
"section": [
{ ".name": "dns", ".type": "section", "enabled": "1", "action": "dns", "dns_server": "9.9.9.9", "dns_detour_enabled": "1", "dns_detour_section": "bypass", "domain_suffix": [ "example.org" ] },
{ ".name": "bypass", ".type": "section", "enabled": "1", "action": "bypass", "domain_suffix": [ "bypass.example" ] }
]
}
JSON
assert_rejects "DNS rule unsupported detour" "$WORK_DIR/bad-dns-rule-detour.json" "unsupported action 'bypass'"
cat >"$WORK_DIR/bad-port.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings" },
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SECTION_JS="$ROOT_DIR/luci-app-forkop/htdocs/luci-static/resources/view/forkop/section.js"
node - "$SECTION_JS" <<'NODE'
const fs = require("fs");
const source = fs.readFileSync(process.argv[2], "utf8");
function fail(message) {
console.error(`FAIL: ${message}`);
process.exit(1);
}
function requiredIndex(needle, from = 0) {
const index = source.indexOf(needle, from);
if (index < 0) fail(`missing source contract: ${needle}`);
return index;
}
const domains = requiredIndex('key: "domain_suffix"');
const ips = requiredIndex('const ipConditionOption = addTextConditionField(section, {', domains);
const builtIns = requiredIndex('const builtInRulesetOption = section.taboption(', ips);
if (source.slice(domains, ips).includes("dependsOnRoutingAction")) {
fail("DNS action must keep the Domains field visible");
}
if (!source.slice(ips, builtIns).includes("dependsOnRoutingAction(ipConditionOption)")) {
fail("DNS action must hide the IPs field");
}
const sectionContent = requiredIndex("function createSectionContent(section)");
const protocol = requiredIndex('"dns_type"', sectionContent);
const server = requiredIndex('"dns_server"', protocol);
if (server <= protocol || !source.slice(protocol, server).includes('_("DNS protocol")')) {
fail("DNS protocol must be rendered before DNS server");
}
if (source.slice(server, requiredIndex('"dns_detour_enabled"', server)).includes('placeholder = "1.1.1.1"')) {
fail("DNS server must not render the 1.1.1.1 placeholder");
}
for (const expected of [
'_("DNS server used by the resolver")',
'_("DNS requests through section")',
'"Add URLs or local paths to .srs / .json lists. Only domain rules are supported."',
'"Add URLs or local paths to .lst lists. Only domain rules are supported."',
]) {
requiredIndex(expected);
}
const ruleSetOption = requiredIndex("const ruleSetOption = section.taboption(");
const dnsRuleSetOption = requiredIndex("const dnsRuleSetOption = section.taboption(", ruleSetOption);
const domainLists = requiredIndex("const domainIpListsOption", dnsRuleSetOption);
if (!source.slice(ruleSetOption, dnsRuleSetOption).includes("SettingsDynamicList") ||
!source.slice(ruleSetOption, dnsRuleSetOption).includes("dependsOnRoutingAction(ruleSetOption)")) {
fail("routing rule sets must keep their settings DynamicList");
}
if (!source.slice(dnsRuleSetOption, domainLists).includes("form.DynamicList") ||
!source.slice(dnsRuleSetOption, domainLists).includes('"_dns_rule_set"') ||
!source.slice(dnsRuleSetOption, domainLists).includes('dnsRuleSetOption.depends("action", "dns")') ||
!source.slice(dnsRuleSetOption, domainLists).includes("writeDnsRulesetReferences")) {
fail("DNS rule sets must use a dedicated classic DynamicList");
}
NODE
printf 'DNS action UI checks passed\n'
+14 -1
View File
@@ -457,6 +457,16 @@ cat >"$WORK_DIR/populate-fixture.json" <<'JSON'
"action": "proxy",
"domain_suffix": [ "example.org" ],
"ports": [ "8443" ]
},
{
".name": "dns_only",
".type": "section",
"enabled": "1",
"action": "dns",
"domain_suffix": [ "dns-only.example" ],
"ip_cidr": [ "192.0.2.53" ],
"ports": [ "5353" ],
"fully_routed_ips": [ "192.168.1.53/32" ]
}
]
}
@@ -554,6 +564,9 @@ fi
if grep -Fq $'nft\tadd\telement\tinet\tForkopTable\tforkop_rule_ports_with_domain_ports\t{ 8443 }' "$NFT_LOG"; then
fail "ports with destination matchers should not populate global port set"
fi
if grep -Fq '192.0.2.53' "$NFT_LOG" || grep -Fq '192.168.1.53' "$NFT_LOG" || grep -Fq 'forkop_rule_dns_only' "$NFT_LOG"; then
fail "DNS action should not populate nft sets or fully routed rules"
fi
if [ "$(grep -F $'ip\tsaddr\t192.168.1.20/32' "$NFT_LOG" | wc -l | tr -d ' ')" != "3" ]; then
fail "duplicate fully routed source should insert exactly one tcp/udp/local rule set"
fi
@@ -717,7 +730,7 @@ nft_ucode nft-populate-runtime-sets-fixture "$WORK_DIR/populate-fixture.json" 0
: > "$NFT_LOG"
NFT_MANGLE_CHAIN_OUTPUT='ip saddr 192.168.1.20 meta l4proto tcp meta mark set 0x00100000 counter' \
nft_ucode nft-populate-runtime-sets-fixture "$WORK_DIR/populate-fixture.json" 1 "disabled deferred inline inline_no_ports ports_only ports_with_domain" ForkopTable forkop_subnets forkop_ports forkop_ip_ports forkop_interfaces localv4 0x00100000
nft_ucode nft-populate-runtime-sets-fixture "$WORK_DIR/populate-fixture.json" 1 "disabled deferred inline inline_no_ports ports_only ports_with_domain dns_only" ForkopTable forkop_subnets forkop_ports forkop_ip_ports forkop_interfaces localv4 0x00100000
if grep -Fq $'nft\tinsert\trule' "$NFT_LOG"; then
fail "existing nft-normalized /32 fully routed source should not insert"
fi
+69 -7
View File
@@ -63,23 +63,31 @@ if state_ucode community-service-has-subnet-list youtube >/dev/null 2>&1; then
fail "youtube should not be a subnet-list community service"
fi
state_ucode rule-has-list-update-source 1 "youtube telegram" "" "" "" "" >/dev/null ||
state_ucode rule-has-list-update-source 1 proxy "youtube telegram" "" "" "" "" >/dev/null ||
fail "community subnet service should require list update"
state_ucode rule-has-list-update-source 1 "" "https://example.com/domains.lst" "" "" "" >/dev/null ||
state_ucode rule-has-list-update-source 1 proxy "" "https://example.com/domains.lst" "" "" "" >/dev/null ||
fail "remote domain list should require list update"
state_ucode rule-has-list-update-source 1 "" "" "" "" "local.lst https://example.com/mixed.lst" >/dev/null ||
state_ucode rule-has-list-update-source 1 proxy "" "" "" "" "local.lst https://example.com/mixed.lst" >/dev/null ||
fail "remote domain_ip list should require list update"
if state_ucode rule-has-list-update-source 0 "telegram" "https://example.com/domains.lst" "" "" "" >/dev/null 2>&1; then
if state_ucode rule-has-list-update-source 0 proxy "telegram" "https://example.com/domains.lst" "" "" "" >/dev/null 2>&1; then
fail "disabled rule should not require list update"
fi
state_ucode rule-has-list-update-source 1 dns "" "" "" "" "https://example.com/domains.lst" >/dev/null ||
fail "remote DNS domain list should require list update"
if state_ucode rule-has-list-update-source 1 dns "telegram" "" "" "" "" >/dev/null 2>&1; then
fail "DNS rules should not import built-in subnet lists"
fi
state_ucode rule-has-nft-list-update-source 1 "telegram" "" "" "" >/dev/null ||
state_ucode rule-has-nft-list-update-source 1 proxy "telegram" "" "" "" >/dev/null ||
fail "community subnet service should require nft list update"
state_ucode rule-has-nft-list-update-source 1 "" "https://example.com/subnets.lst" "" "" >/dev/null ||
state_ucode rule-has-nft-list-update-source 1 proxy "" "https://example.com/subnets.lst" "" "" >/dev/null ||
fail "remote subnet list should require nft list update"
if state_ucode rule-has-nft-list-update-source 1 "" "" "" "local.lst" >/dev/null 2>&1; then
if state_ucode rule-has-nft-list-update-source 1 proxy "" "" "" "local.lst" >/dev/null 2>&1; then
fail "local-only domain_ip list should not require nft list update"
fi
if state_ucode rule-has-nft-list-update-source 1 dns "telegram" "https://example.com/subnets.lst" "" "https://example.com/mixed.lst" >/dev/null 2>&1; then
fail "DNS rules should never require nft list updates"
fi
state_ucode rule-has-subscription-update-source 1 "1h" >/dev/null ||
fail "subscription proxy with update interval should require subscription update"
@@ -1082,12 +1090,25 @@ cat >"$WORK_DIR/reload-state-signatures.json" <<'JSON'
"action": "proxy",
"urltest_enabled": "1",
"urltest_check_interval": "7m"
},
{
".name": "dns_only",
".type": "section",
"enabled": "1",
"action": "dns",
"ports": [ "5353" ],
"community_lists": [ "telegram" ],
"remote_subnet_lists": [ "https://example.com/ignored-subnets.lst" ],
"rule_set_with_subnets": [ "https://example.com/ignored-subnets.srs" ],
"domain_ip_lists": [ "https://example.com/dns-domains.lst" ]
}
]
}
JSON
cat >"$WORK_DIR/list-signature.expected" <<'EOF_LIST_SIG'
[lists.list_proxy.action]
proxy
[lists.list_proxy.ports]
443,80,443-444
[lists.list_proxy.community_subnet_lists]
@@ -1100,6 +1121,8 @@ https://example.com/subnets.lst
https://example.com/subnets.srs
[lists.list_proxy.domain_ip_lists]
local.lst https://example.com/mixed.lst
[lists.sub_paused.action]
proxy
[lists.sub_paused.ports]
[lists.sub_paused.community_subnet_lists]
@@ -1112,6 +1135,8 @@ local.lst https://example.com/mixed.lst
[lists.sub_paused.domain_ip_lists]
[lists.implicit_action.action]
[lists.implicit_action.ports]
[lists.implicit_action.community_subnet_lists]
@@ -1124,6 +1149,8 @@ local.lst https://example.com/mixed.lst
[lists.implicit_action.domain_ip_lists]
[lists.urltest_custom.action]
proxy
[lists.urltest_custom.ports]
[lists.urltest_custom.community_subnet_lists]
@@ -1136,6 +1163,10 @@ local.lst https://example.com/mixed.lst
[lists.urltest_custom.domain_ip_lists]
[lists.dns_only.action]
dns
[lists.dns_only.domain_ip_lists]
https://example.com/dns-domains.lst
EOF_LIST_SIG
cat >"$WORK_DIR/cron-signature.expected" <<'EOF_CRON_SIG'
@@ -1143,6 +1174,8 @@ cat >"$WORK_DIR/cron-signature.expected" <<'EOF_CRON_SIG'
6h
[settings.component_update_check_interval]
2h
[lists.list_proxy.action]
proxy
[lists.list_proxy.ports]
443,80,443-444
[lists.list_proxy.community_subnet_lists]
@@ -1155,6 +1188,8 @@ https://example.com/subnets.lst
https://example.com/subnets.srs
[lists.list_proxy.domain_ip_lists]
local.lst https://example.com/mixed.lst
[lists.sub_paused.action]
proxy
[lists.sub_paused.ports]
[lists.sub_paused.community_subnet_lists]
@@ -1167,6 +1202,8 @@ local.lst https://example.com/mixed.lst
[lists.sub_paused.domain_ip_lists]
[lists.implicit_action.action]
[lists.implicit_action.ports]
[lists.implicit_action.community_subnet_lists]
@@ -1179,6 +1216,8 @@ local.lst https://example.com/mixed.lst
[lists.implicit_action.domain_ip_lists]
[lists.urltest_custom.action]
proxy
[lists.urltest_custom.ports]
[lists.urltest_custom.community_subnet_lists]
@@ -1191,6 +1230,10 @@ local.lst https://example.com/mixed.lst
[lists.urltest_custom.domain_ip_lists]
[lists.dns_only.action]
dns
[lists.dns_only.domain_ip_lists]
https://example.com/dns-domains.lst
[subscription.list_proxy.subscription_urls]
[ { "url": "https://example.com/sub1.txt", "subscription_update_enabled": "1", "subscription_update_interval": "1h", "download_via_proxy_section": "", "auto_user_agent": "1", "user_agent": "", "auto_hwid": "1", "hwid": "", "show_dashboard_metadata": "1", "prefix_nodes": "0", "node_prefix": "", "include_urltest_groups": "1", "hide_urltest_group_outbounds": "1", "hide_detour_outbounds": "1" }, { "url": "https://example.com/sub2.txt", "subscription_update_enabled": "1", "subscription_update_interval": "1h", "download_via_proxy_section": "", "auto_user_agent": "1", "user_agent": "", "auto_hwid": "1", "hwid": "", "show_dashboard_metadata": "1", "prefix_nodes": "0", "node_prefix": "", "include_urltest_groups": "1", "hide_urltest_group_outbounds": "1", "hide_detour_outbounds": "1" } ]
[subscription.list_proxy.subscription_update_interval]
@@ -1232,6 +1275,25 @@ assert_eq "7m" \
state_ucode has-subscription-update-sources-fixture "$WORK_DIR/reload-state-signatures.json" >/dev/null ||
fail "fixture should have subscription update sources"
cat >"$WORK_DIR/dns-action-signature-udp.json" <<'JSON'
{
"section": [
{ ".name": "dns", ".type": "section", "enabled": "1", "action": "dns", "dns_type": "udp", "dns_server": "9.9.9.9", "domain_suffix": [ "example.org" ] }
]
}
JSON
cat >"$WORK_DIR/dns-action-signature-dot.json" <<'JSON'
{
"section": [
{ ".name": "dns", ".type": "section", "enabled": "1", "action": "dns", "dns_type": "dot", "dns_server": "9.9.9.9", "domain_suffix": [ "example.org" ] }
]
}
JSON
if [ "$(state_ucode sing-box-signature-fixture "$WORK_DIR/dns-action-signature-udp.json")" = \
"$(state_ucode sing-box-signature-fixture "$WORK_DIR/dns-action-signature-dot.json")" ]; then
fail "DNS action protocol should change the sing-box signature"
fi
cat >"$WORK_DIR/reload-state.expected" <<EOF_RELOAD_STATE
format=1
service_trigger_signature=$(state_ucode service-trigger-signature-fixture "$WORK_DIR/reload-state-signatures.json")
+90
View File
@@ -323,6 +323,66 @@ cat >"$WORK_DIR/runtime-matchers-fixture.json" <<'JSON'
}
JSON
cat >"$WORK_DIR/dns-action.lst" <<'EOF_DNS_ACTION_LIST'
list-dns.example
198.51.100.0/24
EOF_DNS_ACTION_LIST
cat >"$WORK_DIR/dns-action-fixture.json" <<JSON
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "dns_first",
".type": "section",
"enabled": "1",
"action": "dns",
"dns_type": "dot",
"dns_server": "9.9.9.9:5353",
"dns_detour_enabled": "1",
"dns_detour_section": "detour",
"domain_suffix": [ "dns-first.example" ],
"community_lists": [ "youtube" ],
"rule_set": [ "https://example.com/domain-only.srs" ],
"domain_ip_lists": [ "$WORK_DIR/dns-action.lst" ],
"ip_cidr": [ "203.0.113.0/24" ],
"source_ip_cidr": [ "192.0.2.1/32" ],
"fully_routed_ips": [ "192.0.2.2/32" ],
"ports": [ "443" ]
},
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\"}" ],
"domain_suffix": [ "dns-first.example", "proxy-first.example" ]
},
{
".name": "dns_second",
".type": "section",
"enabled": "1",
"action": "dns",
"dns_server": "8.8.8.8",
"domain_suffix": [ "proxy-first.example" ]
},
{
".name": "detour",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\"}" ],
"domain_suffix": [ "detour.example" ]
}
]
}
JSON
cat >"$WORK_DIR/urltest-filter-fixture.json" <<'JSON'
{
"settings": {
@@ -904,6 +964,7 @@ generate_config "$WORK_DIR/disabled-updates-fixture.json" "$WORK_DIR/disabled.js
generate_config "$WORK_DIR/default-updates-fixture.json" "$WORK_DIR/default.json"
generate_config "$WORK_DIR/server-inbound-fixture.json" "$WORK_DIR/server.json"
generate_config "$WORK_DIR/runtime-matchers-fixture.json" "$WORK_DIR/matchers.json"
generate_config "$WORK_DIR/dns-action-fixture.json" "$WORK_DIR/dns-action.json"
generate_config "$WORK_DIR/urltest-filter-fixture.json" "$WORK_DIR/urltest.json"
generate_config "$WORK_DIR/provider-actions-fixture.json" "$WORK_DIR/providers.json"
generate_config "$WORK_DIR/manual-transport-fixture.json" "$WORK_DIR/manual.json"
@@ -1036,6 +1097,14 @@ function dns_rule(config, predicate) {
return null;
}
function dns_rule_index(config, predicate) {
let rules = config.dns.rules || [];
for (let i = 0; i < length(rules); i++)
if (rules[i] && predicate(rules[i]))
return i;
return -1;
}
function dns_server(config, predicate) {
for (let server in config.dns.servers || [])
if (server && predicate(server))
@@ -1087,6 +1156,27 @@ assert(route_rule(matchers, r => contains(r.inbound, "tproxy-in") && contains(r.
assert(route_rule(matchers, r => r.outbound == "direct-out" && contains(r.source_ip_cidr, "192.168.1.5/32")) == null, "routing excluded source removed");
assert(route_rule(matchers, r => r.outbound == "proxy-out" && contains(r.source_ip_cidr, "10.0.0.2/32") && contains(r.source_ip_cidr, "2001:db8::2/128")) != null, "source_ip_cidr matcher");
let dns_action = cfg("dns-action");
let dns_first_server = dns_server(dns_action, r => r.tag == "dns_first-dns-server");
assert(dns_first_server && dns_first_server.type == "tls" && dns_first_server.server == "9.9.9.9" && dns_first_server.server_port == 5353, "DNS action protocol and server");
assert(dns_first_server.detour == "detour-out", "DNS action detour");
assert(dns_server(dns_action, r => r.tag == "dns_second-dns-server" && r.server == "8.8.8.8") != null, "second DNS action server");
assert(outbound(dns_action, "dns_first-out") == null && outbound(dns_action, "dns_second-out") == null, "DNS actions do not create outbounds");
assert(route_rule(dns_action, r => contains(r.domain_suffix, "dns-first.example") && r.outbound != null) != null, "proxy still owns its route rule");
assert(route_rule(dns_action, r => contains(r.domain_suffix, "list-dns.example")) == null, "DNS action does not create route rules");
let dns_first_index = dns_rule_index(dns_action, r => contains(r.domain_suffix, "dns-first.example") && r.server == "dns_first-dns-server");
let proxy_after_dns_index = dns_rule_index(dns_action, r => contains(r.domain_suffix, "dns-first.example") && r.server == "fakeip-server");
assert(dns_first_index >= 0 && proxy_after_dns_index > dns_first_index, "higher DNS action wins over lower proxy DNS rule");
let proxy_first_index = dns_rule_index(dns_action, r => contains(r.domain_suffix, "proxy-first.example") && r.server == "fakeip-server");
let dns_after_proxy_index = dns_rule_index(dns_action, r => contains(r.domain_suffix, "proxy-first.example") && r.server == "dns_second-dns-server");
assert(proxy_first_index >= 0 && dns_after_proxy_index > proxy_first_index, "higher proxy DNS rule wins over lower DNS action");
assert(dns_rule(dns_action, r => contains(r.rule_set, "dns_first-youtube-community-ruleset") && r.server == "dns_first-dns-server") != null, "DNS action built-in domain rule set");
let dns_custom_ruleset = ruleset_url(dns_action, "https://example.com/domain-only.srs");
assert(dns_custom_ruleset && dns_rule(dns_action, r => contains(r.rule_set, dns_custom_ruleset.tag) && r.server == "dns_first-dns-server") != null, "DNS action custom domain rule set");
let dns_list = json(fs.readfile(dir + "/dns-action.json.rulesets/dns_first-lists-ruleset.json"));
assert(index(sprintf("%J", dns_list), "list-dns.example") >= 0, "DNS action imports domains from plain lists");
assert(index(sprintf("%J", dns_list), "198.51.100.0/24") < 0, "DNS action drops subnets from plain lists");
let urltest = cfg("urltest");
let urltest_out = outbound(urltest, "proxy-urltest-out");
assert(urltest_out && length(urltest_out.outbounds) == 1 && urltest_out.outbounds[0] == "proxy-1-out", "URLTest include filter");