Files
NeoTachyon/tachyon/files/usr/lib/config/connections.uc
T
Dushnilin 2019ddd12a release: 1.3.26 — Watchdog proxy restart protection, WAN monitor debounce, and subscription bootstrap resilience
- Fix #52: Watchdog proxy restart on single URL failure:
  * Validate failure against secondary independent probe URL before declaring proxy down
  * Skip restart when direct WAN is also degraded or down (!direct_ok)
  * Implement 120s cooldown between automated proxy restarts
  * Fix Telegram notification to accurately state 'Перезапуск sing-box' instead of 'Перезапуск роутера'
- Fix #51: Telegram bot spamming WAN up/down notifications:
  * Filter hotplug events strictly by badwan_monitored_interfaces (defaults to wan)
  * Add atomic directory lock with debounce delay and real ubus state query
  * Prevent repeated notifications when interface state has not transitioned
- Feat: 2-VPN deadlock elimination & self-download subscription proxy:
  * Isolate unbootstrapped dependent subscription sections on initial boot (deferred_sections)
  * Allow self-download proxy target for subscriptions with explicit UI hint '(текущее правило при активности)'
  * Enable automatic local proxy fallback for subscription refresh when direct WAN download fails while sing-box is active
  * Add regression test in tests/subscription_bootstrap_deadlock.sh
- Fix: Parental control quota enforcement:
  * Correct route rule filtering when daily quota minutes are exhausted without a fixed time interval
- i18n & Version bump:
  * Bump fe-app-tachyon to 1.3.26 and update Russian PO/POT translations
2026-09-12 16:50:04 +03:00

1613 lines
54 KiB
Ucode

#!/usr/bin/env ucode
let common = require("core.common");
let uci_core = require("core.uci");
let as_string = common.as_string;
let object_or_empty = common.object_or_empty;
let bool_value_common = common.bool_value;
let remove_file = common.remove_file;
function bool_value(value, fallback) {
if (value == null || value == "")
return !!fallback;
return bool_value_common(value);
}
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
const ITEM_TYPES = [
"subscription_url",
"section_interface",
"urltest",
"priority_group",
"priority_level"
];
let item_sections = null;
function raw_option(section, key) {
return object_or_empty(section)[key];
}
function option(section, key, fallback) {
return common.option(section, key, fallback);
}
function bool_option(section, key, fallback) {
let val = raw_option(section, key);
if (val == null)
return !!fallback;
return bool_value(val);
}
function list_value(section, key) {
let value = raw_option(section, key);
if (value == null)
return [];
if (type(value) == "array")
return value;
value = as_string(value);
return value == "" ? [] : [ value ];
}
function whitespace_list_value(section, key) {
let value = raw_option(section, key);
if (type(value) == "array")
return value;
return common.list_option(section, key);
}
function section_name(section) {
return option(section, ".name", "");
}
function empty_item_sections() {
let result = {};
for (let type_name in ITEM_TYPES) {
result[type_name] = {
by_name: {},
list: []
};
}
return result;
}
function add_item_section(index, type_name, section) {
section = object_or_empty(section);
let name = section_name(section);
if (name == "")
return;
if (index[type_name] == null)
index[type_name] = { by_name: {}, list: [] };
index[type_name].by_name[name] = section;
push(index[type_name].list, section);
}
function item_index_from_cursor(cursor, config_name) {
let index = empty_item_sections();
if (cursor == null)
return index;
config_name = as_string(config_name || CONFIG_NAME);
for (let type_name in ITEM_TYPES) {
try {
cursor.foreach(config_name, type_name, function(section) {
add_item_section(index, type_name, section);
});
}
catch (e) {
// An absent config or an unknown section type throws here. The
// index simply ends up without entries of that type, which is the
// same state as a config that declares none; uci reports the parse
// error itself to the system log.
}
}
return index;
}
function fixture_section_list(data, type_name) {
data = object_or_empty(data);
let value = data[type_name];
if (type(value) == "array")
return value;
if (type(value) == "object")
return [ value ];
let plural = data[type_name + "s"];
return type(plural) == "array" ? plural : [];
}
function item_index_from_data(data) {
let index = empty_item_sections();
for (let type_name in ITEM_TYPES)
for (let section in fixture_section_list(data, type_name))
add_item_section(index, type_name, section);
return index;
}
function item_index_from_uci() {
let index = empty_item_sections();
for (let type_name in ITEM_TYPES)
for (let section in uci_core.section_objects(CONFIG_NAME, type_name))
add_item_section(index, type_name, section);
return index;
}
function set_item_sections(index) {
item_sections = type(index) == "object" ? index : empty_item_sections();
}
function set_item_sections_from_cursor(cursor, config_name) {
set_item_sections(item_index_from_cursor(cursor, config_name));
}
function set_item_sections_from_data(data) {
set_item_sections(item_index_from_data(data));
}
function get_item_sections() {
if (item_sections == null)
item_sections = item_index_from_uci();
return item_sections;
}
function child_section(type_name, item_id) {
return object_or_empty(object_or_empty(object_or_empty(get_item_sections())[type_name]).by_name)[as_string(item_id)];
}
function owned_child_section(parent, type_name, item_id) {
let child = child_section(type_name, item_id);
if (type(child) != "object")
return null;
return option(child, "section", "") == section_name(parent) ? child : null;
}
function child_items(parent, type_name) {
let result = [];
for (let child in object_or_empty(object_or_empty(get_item_sections())[type_name]).list || [])
if (option(child, "section", "") == section_name(parent))
push(result, child);
return result;
}
function child_items_by_owner(type_name, owner_key, owner_value) {
owner_value = as_string(owner_value);
let result = [];
for (let child in object_or_empty(object_or_empty(get_item_sections())[type_name]).list || [])
if (option(child, owner_key, "") == owner_value)
push(result, child);
return result;
}
function child_item_by_value(parent, type_name, value_key, value) {
value = as_string(value);
for (let child in child_items(parent, type_name))
if (option(child, value_key, "") == value)
return child;
return null;
}
function child_option(child, key, fallback) {
if (type(child) != "object")
return as_string(fallback);
return option(child, key, fallback);
}
function child_option_alias(child, keys, fallback) {
if (type(child) != "object")
return as_string(fallback);
for (let key in keys) {
let value = raw_option(child, key);
if (value != null)
return as_string(value);
}
return as_string(fallback);
}
function child_bool(child, key, fallback) {
if (type(child) != "object")
return !!fallback;
return bool_option(child, key, fallback);
}
function child_list(child, key, fallback) {
if (type(child) != "object")
return fallback == null ? [] : fallback;
let value = raw_option(child, key);
if (value == null)
value = fallback;
if (value == null)
return [];
if (type(value) == "array")
return value;
value = as_string(value);
return value == "" ? [] : [ value ];
}
function child_list_alias(child, keys, fallback) {
if (type(child) == "object") {
for (let key in keys) {
let value = raw_option(child, key);
if (value != null)
return child_list(child, key, []);
}
}
return fallback == null ? [] : fallback;
}
function child_values(parent, type_name, value_key, legacy_key) {
let items = child_items(parent, type_name);
if (length(items) > 0) {
let result = [];
for (let child in items) {
let value = option(child, value_key, "");
if (value != "")
push(result, value);
}
return result;
}
return legacy_key == "" ? [] : whitespace_list_value(parent, legacy_key);
}
function settings_map(section, key) {
let value = option(section, key, "");
if (value == "")
return {};
try {
value = json(value);
}
catch (e) {
return {};
}
return object_or_empty(value);
}
function item_settings(section, key, item) {
return object_or_empty(settings_map(section, key)[as_string(item)]);
}
function item_option(section, key, item, option_name, fallback) {
let value = item_settings(section, key, item)[option_name];
if (value == null)
return as_string(fallback);
return as_string(value);
}
function item_bool(section, key, item, option_name, fallback) {
return bool_value(item_settings(section, key, item)[option_name], fallback);
}
function item_list(section, key, item, option_name, fallback) {
let value = item_settings(section, key, item)[option_name];
if (value == null)
value = fallback;
if (value == null)
return [];
if (type(value) == "array")
return value;
value = as_string(value);
return value == "" ? [] : [ value ];
}
function is_legacy_connection_action(action) {
action = as_string(action);
return action == "proxy" || action == "outbound" || action == "vpn";
}
function is_connections_action(action) {
action = as_string(action);
return action == "connection" || is_legacy_connection_action(action);
}
function is_wdtt_action(action) {
return as_string(action) == "wdtt";
}
function is_olcrtc_action(action) {
return as_string(action) == "olcrtc";
}
function wdtt_sections() {
let result = [];
let sections = uci_core.section_objects(CONFIG_NAME, "section");
for (let section in sections) {
if (bool_option(section, "enabled", true) && option(section, "action", "") == "wdtt")
push(result, section);
}
return result;
}
function olcrtc_sections() {
let result = [];
let sections = uci_core.section_objects(CONFIG_NAME, "section");
for (let section in sections) {
if (bool_option(section, "enabled", true) && option(section, "action", "") == "olcrtc")
push(result, section);
}
return result;
}
function normalize_action(action) {
action = as_string(action);
return is_connections_action(action) ? "connection" : action;
}
function action(section) {
return normalize_action(option(section, "action", ""));
}
function connection_urls(section) {
let links = whitespace_list_value(section, "selector_proxy_links");
let text = option(section, "selector_proxy_links_text", "");
if (text != "") {
for (let line in split(text, /[\r\n]+/)) {
line = trim(line);
if (line != "" && match(line, /^[^#\s]/))
push(links, line);
}
}
return links;
}
function urltest_text_links(section) {
let text = option(section, "urltest_proxy_links_text", "");
if (text == "")
return [];
let links = [];
for (let line in split(text, /[\r\n]+/)) {
line = trim(line);
if (line != "" && match(line, /^[^#\s]/))
push(links, line);
}
return links;
}
function subscription_urls(section) {
return child_values(section, "subscription_url", "url", "subscription_urls");
}
function interfaces(section) {
let items = child_values(section, "section_interface", "name", "");
if (length(items) > 0)
return items;
let result = list_value(section, "interfaces");
if (length(result) == 0) {
let value = option(section, "interface", "");
if (value != "")
push(result, value);
}
return result;
}
function outbound_jsons(section) {
let result = list_value(section, "outbound_jsons");
if (length(result) == 0) {
let value = option(section, "outbound_json", "");
if (value != "")
push(result, value);
}
return result;
}
function urltests(section) {
let result = [];
for (let child in child_items(section, "urltest")) {
let id = section_name(child);
if (id != "")
push(result, id);
}
if (length(result) > 0)
return result;
return bool_option(section, "urltest_enabled", false) ? [ "urltest" ] : [];
}
function priority_groups(section) {
let result = [];
for (let child in child_items(section, "priority_group")) {
let id = section_name(child);
if (id != "")
push(result, id);
}
return result;
}
function dashboard_filter_mode(section) {
return option(section, "dashboard_filter_mode", "disabled") || "disabled";
}
function dashboard_detect_server_country(section) {
return option(section, "dashboard_detect_server_country", "flag_emoji") || "flag_emoji";
}
function dashboard_include_countries(section) {
return list_value(section, "dashboard_include_countries");
}
function dashboard_include_outbounds(section) {
return list_value(section, "dashboard_include_outbounds");
}
function dashboard_include_regex(section) {
return list_value(section, "dashboard_include_regex");
}
function dashboard_include_proxy_parameters(section) {
return bool_option(section, "dashboard_include_proxy_parameters", false);
}
function dashboard_include_protocols(section) {
return list_value(section, "dashboard_include_protocols");
}
function dashboard_include_transports(section) {
return list_value(section, "dashboard_include_transports");
}
function dashboard_include_securities(section) {
return list_value(section, "dashboard_include_securities");
}
function dashboard_include_groups(section) {
return list_value(section, "dashboard_include_groups");
}
function dashboard_exclude_countries(section) {
return list_value(section, "dashboard_exclude_countries");
}
function dashboard_exclude_outbounds(section) {
return list_value(section, "dashboard_exclude_outbounds");
}
function dashboard_exclude_regex(section) {
return list_value(section, "dashboard_exclude_regex");
}
function dashboard_exclude_proxy_parameters(section) {
return bool_option(section, "dashboard_exclude_proxy_parameters", false);
}
function dashboard_exclude_protocols(section) {
return list_value(section, "dashboard_exclude_protocols");
}
function dashboard_exclude_transports(section) {
return list_value(section, "dashboard_exclude_transports");
}
function dashboard_exclude_securities(section) {
return list_value(section, "dashboard_exclude_securities");
}
function dashboard_exclude_groups(section) {
return list_value(section, "dashboard_exclude_groups");
}
function priority_group_child(section, value) {
return owned_child_section(section, "priority_group", value);
}
function priority_group_settings(section, value) {
let child = priority_group_child(section, value);
return child != null ? child : {};
}
function priority_level_child(group_id, value) {
let child = child_section("priority_level", value);
if (type(child) != "object")
return null;
return option(child, "group", "") == as_string(group_id) ? child : null;
}
function priority_level_sort(left, right) {
let left_order = int(option(left, "order", "0"), 10);
let right_order = int(option(right, "order", "0"), 10);
if (left_order != right_order)
return left_order < right_order ? -1 : 1;
let left_name = section_name(left);
let right_name = section_name(right);
return left_name == right_name ? 0 : (left_name < right_name ? -1 : 1);
}
function priority_level_items(group_id) {
let result = child_items_by_owner("priority_level", "group", group_id);
sort(result, priority_level_sort);
return result;
}
function priority_levels(group_id) {
let result = [];
for (let child in priority_level_items(group_id)) {
let id = section_name(child);
if (id != "")
push(result, id);
}
return result;
}
function community_lists(section) {
return whitespace_list_value(section, "community_lists");
}
function rule_sets(section) {
return whitespace_list_value(section, "rule_set");
}
function rule_sets_with_subnets(section) {
return whitespace_list_value(section, "rule_set_with_subnets");
}
function list_option_value_from_array(values) {
return join(" ", values || []);
}
function community_lists_value(section) {
return list_option_value_from_array(community_lists(section));
}
function rule_sets_value(section) {
return list_option_value_from_array(rule_sets(section));
}
function combined_domain_condition_text(section) {
if (type(raw_option(section, "domain")) != "array") {
let value = option(section, "domain", "");
if (value != "")
return value;
}
return option(section, "domain_suffix_text", "");
}
function rule_config_or_null() {
try {
return require("config.rule");
} catch (e) {
return null;
}
}
function rule_condition_csv(section, key, kind) {
let rule_config = rule_config_or_null();
if (!rule_config)
return "";
return rule_config.rule_condition_csv_value(
key,
kind,
option(section, key + "_text_mode", "0"),
option(section, "conditions_text_mode", "0"),
option(section, key + "_text", ""),
option(section, key, ""),
combined_domain_condition_text(section),
option(section, "domain_suffix", "")
);
}
function has_dns_matchers(section) {
return rule_condition_csv(section, "domain", "domains") != "" ||
rule_condition_csv(section, "domain_suffix", "domains") != "" ||
rule_condition_csv(section, "domain_keyword", "generic") != "" ||
rule_condition_csv(section, "domain_regex", "generic") != "" ||
length(community_lists(section)) > 0 ||
length(rule_sets(section)) > 0 ||
length(rule_sets_with_subnets(section)) > 0 ||
option(section, "domain_ip_lists", "") != "";
}
const DSCP_ALIASES = {
"cs0": 0, "cs1": 8, "cs2": 16, "cs3": 24, "cs4": 32, "cs5": 40, "cs6": 48, "cs7": 56,
"ef": 46,
"af11": 10, "af12": 12, "af13": 14,
"af21": 18, "af22": 20, "af23": 22,
"af31": 26, "af32": 28, "af33": 30,
"af41": 34, "af42": 36, "af43": 38
};
function parse_dscp_value(item) {
item = lc(trim(as_string(item)));
if (item == "") return null;
if (DSCP_ALIASES[item] != null)
return DSCP_ALIASES[item];
let num = null;
if (substr(item, 0, 2) == "0x")
num = int(item, 16);
else
num = int(item, 10);
if (num != null && num >= 0 && num <= 63)
return num;
return null;
}
function dscp_list(section) {
let raw = whitespace_list_value(section, "dscp");
let result = [];
for (let item in raw) {
let parsed = parse_dscp_value(item);
if (parsed != null)
push(result, parsed);
}
return result;
}
function dscp_value(section) {
return list_option_value_from_array(dscp_list(section));
}
function routed_dns_enabled(section) {
return bool_option(section, "routed_dns_enabled", false);
}
function routed_dns_type(section) {
let t = option(section, "routed_dns_type", "udp");
if (!contains(["udp", "dot", "doh", "doq"], t))
t = "udp";
return t;
}
function routed_dns_servers(section) {
let servers = list_option(section, "routed_dns_server");
if (length(servers) == 0) {
let single = option(section, "routed_dns_server", "");
servers = [single];
}
return servers;
}
function contains(arr, val) {
if (type(arr) != "array") return false;
for (let item in arr) {
if (item == val) return true;
}
return false;
}
function geoip_country_list(section) {
let raw = whitespace_list_value(section, "geoip_country");
let result = [];
for (let item in raw) {
item = lc(trim(as_string(item)));
if (item == "" || item == "all") continue;
if (item == "non-ru") {
if (!contains(result, "ru")) push(result, "ru");
continue;
}
if (match(item, /^[a-z]{2}$/) != null && !contains(result, item))
push(result, item);
}
return result;
}
function geoip_country_mode(section) {
let raw = option(section, "geoip_country", "all");
let mode = lc(trim(option(section, "geoip_mode", "")));
if (mode == "exclude" || mode == "include")
return mode;
if (raw == "non-ru")
return "exclude";
return "include";
}
function geoip_country_value(section) {
return list_option_value_from_array(geoip_country_list(section));
}
function rule_sets_with_subnets_value(section) {
return list_option_value_from_array(rule_sets_with_subnets(section));
}
function has_connection_sources(section) {
return length(connection_urls(section)) > 0 ||
length(subscription_urls(section)) > 0 ||
length(interfaces(section)) > 0 ||
length(outbound_jsons(section)) > 0;
}
function subscription_url_settings(section, value) {
let child = child_item_by_value(section, "subscription_url", "url", value);
return child != null ? child : item_settings(section, "subscription_url_settings", value);
}
function interface_settings(section, value) {
let child = child_item_by_value(section, "section_interface", "name", value);
return child != null ? child : item_settings(section, "interface_settings", value);
}
function interface_domain_resolver_enabled(section, value) {
let child = child_item_by_value(section, "section_interface", "name", value);
if (child != null)
return child_bool(child, "domain_resolver_enabled", false);
return item_bool(section, "interface_settings", value, "domain_resolver_enabled",
bool_option(section, "domain_resolver_enabled", false));
}
function interface_domain_resolver_dns_type(section, value) {
let child = child_item_by_value(section, "section_interface", "name", value);
if (child != null)
return child_option(child, "domain_resolver_dns_type", "udp");
return item_option(section, "interface_settings", value, "domain_resolver_dns_type",
option(section, "domain_resolver_dns_type", "udp") || "udp");
}
function interface_domain_resolver_dns_server(section, value) {
let child = child_item_by_value(section, "section_interface", "name", value);
if (child != null)
return child_option(child, "domain_resolver_dns_server", "8.8.8.8");
return item_option(section, "interface_settings", value, "domain_resolver_dns_server",
option(section, "domain_resolver_dns_server", "8.8.8.8") || "8.8.8.8");
}
function urltest_child(section, value) {
let child = owned_child_section(section, "urltest", value);
if (child != null)
return child;
return child_item_by_value(section, "urltest", "id", value);
}
function urltest_settings(section, value) {
let child = urltest_child(section, value);
return child != null ? child : item_settings(section, "urltest_settings", value);
}
function subscription_update_enabled(section, value) {
let child = child_item_by_value(section, "subscription_url", "url", value);
if (child != null)
return child_bool(child, "subscription_update_enabled", true);
return item_bool(section, "subscription_url_settings", value, "subscription_update_enabled",
bool_option(section, "subscription_update_enabled", true));
}
function subscription_update_interval(section, value) {
let child = child_item_by_value(section, "subscription_url", "url", value);
if (child != null)
return child_option(child, "subscription_update_interval", "1h");
return item_option(section, "subscription_url_settings", value, "subscription_update_interval",
option(section, "subscription_update_interval", "1h") || "1h");
}
function subscription_dashboard_metadata_enabled(section, value) {
let child = child_item_by_value(section, "subscription_url", "url", value);
if (child != null)
return child_bool(child, "show_dashboard_metadata", true);
return item_bool(section, "subscription_url_settings", value, "show_dashboard_metadata", true);
}
function subscription_auto_user_agent(section, value) {
let child = child_item_by_value(section, "subscription_url", "url", value);
if (child != null) {
if (raw_option(child, "auto_user_agent") != null)
return child_bool(child, "auto_user_agent", true);
return child_option(child, "user_agent", "") == "";
}
let settings = item_settings(section, "subscription_url_settings", value);
if (raw_option(settings, "auto_user_agent") != null)
return item_bool(section, "subscription_url_settings", value, "auto_user_agent", true);
return item_option(section, "subscription_url_settings", value, "user_agent", "") == "";
}
function subscription_auto_hwid(section, value) {
let child = child_item_by_value(section, "subscription_url", "url", value);
if (child != null) {
if (raw_option(child, "auto_hwid") != null)
return child_bool(child, "auto_hwid", true);
return child_option(child, "hwid", "") == "";
}
let settings = item_settings(section, "subscription_url_settings", value);
if (raw_option(settings, "auto_hwid") != null)
return item_bool(section, "subscription_url_settings", value, "auto_hwid", true);
return item_option(section, "subscription_url_settings", value, "hwid", "") == "";
}
function subscription_include_urltest_groups(section, value) {
let child = child_item_by_value(section, "subscription_url", "url", value);
if (child != null)
return child_bool(child, "include_urltest_groups", true);
return item_bool(section, "subscription_url_settings", value, "include_urltest_groups", true);
}
function subscription_hide_urltest_group_outbounds(section, value) {
let child = child_item_by_value(section, "subscription_url", "url", value);
if (child != null)
return child_bool(child, "hide_urltest_group_outbounds", true);
return item_bool(section, "subscription_url_settings", value, "hide_urltest_group_outbounds", true);
}
function subscription_hide_detour_outbounds(section, value) {
let child = child_item_by_value(section, "subscription_url", "url", value);
if (child != null)
return child_bool(child, "hide_detour_outbounds", true);
return item_bool(section, "subscription_url_settings", value, "hide_detour_outbounds", true);
}
function subscription_prefix_nodes(section, value) {
let child = child_item_by_value(section, "subscription_url", "url", value);
if (child != null)
return child_bool(child, "prefix_nodes", false);
return item_bool(section, "subscription_url_settings", value, "prefix_nodes", false);
}
function subscription_node_prefix(section, value) {
if (!subscription_prefix_nodes(section, value))
return "";
let child = child_item_by_value(section, "subscription_url", "url", value);
let prefix = child != null
? child_option(child, "node_prefix", "")
: item_option(section, "subscription_url_settings", value, "node_prefix", "");
return trim(as_string(prefix));
}
function subscription_user_agent(section, value) {
if (subscription_auto_user_agent(section, value))
return "";
let child = child_item_by_value(section, "subscription_url", "url", value);
let configured = child != null
? child_option(child, "user_agent", "")
: item_option(section, "subscription_url_settings", value, "user_agent", "");
return configured != "" ? configured : "sing-box";
}
function subscription_hwid(section, value) {
if (subscription_auto_hwid(section, value))
return "";
let child = child_item_by_value(section, "subscription_url", "url", value);
if (child != null)
return child_option(child, "hwid", "");
return item_option(section, "subscription_url_settings", value, "hwid", "");
}
function subscription_custom_device_headers(section, value) {
let child = child_item_by_value(section, "subscription_url", "url", value);
if (child != null)
return child_bool(child, "custom_device_headers", false);
return item_bool(section, "subscription_url_settings", value, "custom_device_headers", false);
}
function subscription_device_header(section, value, name) {
let child = child_item_by_value(section, "subscription_url", "url", value);
if (child != null)
return child_option(child, name, "");
return item_option(section, "subscription_url_settings", value, name, "");
}
function subscription_device_os(section, value) {
return subscription_device_header(section, value, "device_os");
}
function subscription_ver_os(section, value) {
return subscription_device_header(section, value, "ver_os");
}
function subscription_device_model(section, value) {
return subscription_device_header(section, value, "device_model");
}
function subscription_device_locale(section, value) {
return subscription_device_header(section, value, "device_locale");
}
function subscription_app_version(section, value) {
return subscription_device_header(section, value, "app_version");
}
function subscription_accept_language(section, value) {
return subscription_device_header(section, value, "accept_language");
}
function subscription_device_headers(section, value) {
return {
enabled: subscription_custom_device_headers(section, value),
device_os: subscription_device_os(section, value),
ver_os: subscription_ver_os(section, value),
device_model: subscription_device_model(section, value),
device_locale: subscription_device_locale(section, value),
app_version: subscription_app_version(section, value),
accept_language: subscription_accept_language(section, value)
};
}
function device_headers_signature(headers) {
headers = object_or_empty(headers);
if (!headers.enabled)
return "";
return sprintf("%J", {
enabled: "1",
device_os: as_string(headers.device_os),
ver_os: as_string(headers.ver_os),
device_model: as_string(headers.device_model),
device_locale: as_string(headers.device_locale),
app_version: as_string(headers.app_version),
accept_language: as_string(headers.accept_language)
});
}
function subscription_device_headers_signature(section, value) {
return device_headers_signature(subscription_device_headers(section, value));
}
function subscription_download_section(section, value) {
let child = child_item_by_value(section, "subscription_url", "url", value);
if (child != null) {
if (!child_bool(child, "download_via_proxy_enabled", false))
return "";
return child_option(child, "download_via_proxy_section", "");
}
if (!item_bool(section, "subscription_url_settings", value, "download_via_proxy_enabled", false))
return "";
return item_option(section, "subscription_url_settings", value, "download_via_proxy_section", "");
}
function subscription_insecure(section, value) {
if (section != null && section.subscription_insecure != null)
return bool_option(section, "subscription_insecure", false) ? "1" : "0";
let child = child_item_by_value(section, "subscription_url", "url", value);
if (child != null)
return bool_option(child, "subscription_insecure", false) ? "1" : "0";
return item_bool(section, "subscription_url_settings", value, "subscription_insecure", false) ? "1" : "0";
}
function urltest_check_interval(section, value) {
let child = urltest_child(section, value);
if (child != null)
return child_option_alias(child, [ "check_interval", "urltest_check_interval" ], "3m");
return item_option(section, "urltest_settings", value, "urltest_check_interval",
option(section, "urltest_check_interval", "3m") || "3m");
}
function urltest_tolerance(section, value) {
let child = urltest_child(section, value);
if (child != null)
return child_option_alias(child, [ "tolerance", "urltest_tolerance" ], "50");
return item_option(section, "urltest_settings", value, "urltest_tolerance",
option(section, "urltest_tolerance", "50") || "50");
}
function urltest_testing_url(section, value) {
let child = urltest_child(section, value);
if (child != null)
return child_option_alias(child, [ "testing_url", "urltest_testing_url" ], "https://www.gstatic.com/generate_204");
return item_option(section, "urltest_settings", value, "urltest_testing_url",
option(section, "urltest_testing_url", "https://www.gstatic.com/generate_204") || "https://www.gstatic.com/generate_204");
}
function urltest_display_name(section, value) {
let fallback = as_string(value);
if (fallback == "urltest" && length(child_items(section, "urltest")) == 0)
fallback = "Fastest";
let child = urltest_child(section, value);
if (child != null)
return child_option_alias(child, [ "name", "display_name" ], fallback);
return item_option(section, "urltest_settings", value, "display_name", fallback);
}
function urltest_idle_timeout(section, value) {
let child = urltest_child(section, value);
if (child != null)
return child_option(child, "idle_timeout", "");
return item_option(section, "urltest_settings", value, "idle_timeout", "");
}
function urltest_interrupt_exist_connections(section, value) {
let child = urltest_child(section, value);
if (child != null)
return child_bool(child, "interrupt_exist_connections", true);
return item_bool(section, "urltest_settings", value, "interrupt_exist_connections", true);
}
function urltest_filter_mode(section, value) {
let child = urltest_child(section, value);
if (child != null)
return child_option_alias(child, [ "filter_mode", "urltest_filter_mode" ], "disabled");
return item_option(section, "urltest_settings", value, "urltest_filter_mode",
option(section, "urltest_filter_mode", "disabled") || "disabled");
}
function urltest_detect_server_country(section, value) {
let child = urltest_child(section, value);
if (child != null)
return child_option(child, "detect_server_country", "flag_emoji");
return item_option(section, "urltest_settings", value, "detect_server_country",
option(section, "detect_server_country", "flag_emoji") || "flag_emoji");
}
function urltest_pin_dashboard(section, value) {
let child = urltest_child(section, value);
if (child != null)
return child_bool(child, "pin_dashboard", true);
return item_bool(section, "urltest_settings", value, "pin_dashboard", true);
}
function urltest_include_countries(section, value) {
let child = urltest_child(section, value);
if (child != null)
return child_list_alias(child, [ "include_countries", "urltest_include_countries" ], []);
return item_list(section, "urltest_settings", value, "urltest_include_countries",
list_value(section, "urltest_include_countries"));
}
function urltest_include_outbounds(section, value) {
let child = urltest_child(section, value);
if (child != null)
return child_list_alias(child, [ "include_outbounds", "urltest_include_outbounds" ], []);
return item_list(section, "urltest_settings", value, "urltest_include_outbounds",
list_value(section, "urltest_include_outbounds"));
}
function urltest_include_regex(section, value) {
let child = urltest_child(section, value);
if (child != null)
return child_list_alias(child, [ "include_regex", "urltest_include_regex" ], []);
return item_list(section, "urltest_settings", value, "urltest_include_regex",
list_value(section, "urltest_include_regex"));
}
function urltest_include_proxy_parameters(section, value) {
let child = urltest_child(section, value);
return child != null ? child_bool(child, "include_proxy_parameters", false) : false;
}
function urltest_include_protocols(section, value) {
let child = urltest_child(section, value);
return child != null ? child_list(child, "include_protocols", []) : [];
}
function urltest_include_transports(section, value) {
let child = urltest_child(section, value);
return child != null ? child_list(child, "include_transports", []) : [];
}
function urltest_include_securities(section, value) {
let child = urltest_child(section, value);
return child != null ? child_list(child, "include_securities", []) : [];
}
function urltest_exclude_countries(section, value) {
let child = urltest_child(section, value);
if (child != null)
return child_list_alias(child, [ "exclude_countries", "urltest_exclude_countries" ], []);
return item_list(section, "urltest_settings", value, "urltest_exclude_countries",
list_value(section, "urltest_exclude_countries"));
}
function urltest_exclude_outbounds(section, value) {
let child = urltest_child(section, value);
if (child != null)
return child_list_alias(child, [ "exclude_outbounds", "urltest_exclude_outbounds" ], []);
return item_list(section, "urltest_settings", value, "urltest_exclude_outbounds",
list_value(section, "urltest_exclude_outbounds"));
}
function urltest_exclude_regex(section, value) {
let child = urltest_child(section, value);
if (child != null)
return child_list_alias(child, [ "exclude_regex", "urltest_exclude_regex" ], []);
return item_list(section, "urltest_settings", value, "urltest_exclude_regex",
list_value(section, "urltest_exclude_regex"));
}
function urltest_exclude_proxy_parameters(section, value) {
let child = urltest_child(section, value);
return child != null ? child_bool(child, "exclude_proxy_parameters", false) : false;
}
function urltest_exclude_protocols(section, value) {
let child = urltest_child(section, value);
return child != null ? child_list(child, "exclude_protocols", []) : [];
}
function urltest_exclude_transports(section, value) {
let child = urltest_child(section, value);
return child != null ? child_list(child, "exclude_transports", []) : [];
}
function urltest_exclude_securities(section, value) {
let child = urltest_child(section, value);
return child != null ? child_list(child, "exclude_securities", []) : [];
}
function priority_group_display_name(section, value) {
let child = priority_group_child(section, value);
return child != null ? child_option(child, "name", as_string(value)) : as_string(value);
}
function priority_group_health_url(section, value) {
let child = priority_group_child(section, value);
return child != null ? child_option(child, "health_url", "https://www.gstatic.com/generate_204") : "https://www.gstatic.com/generate_204";
}
function priority_group_active_check_interval(section, value) {
let child = priority_group_child(section, value);
return child != null ? child_option(child, "active_check_interval", "5s") : "5s";
}
function priority_group_check_timeout(section, value) {
let child = priority_group_child(section, value);
return child != null ? child_option(child, "check_timeout", "2s") : "2s";
}
function priority_group_recovery_check_interval(section, value) {
let child = priority_group_child(section, value);
return child != null ? child_option(child, "recovery_check_interval", "15s") : "15s";
}
function priority_group_pick_fastest(section, value) {
let child = priority_group_child(section, value);
return child != null ? child_bool(child, "pick_fastest", false) : false;
}
function priority_group_switch_to_faster_same_priority(section, value) {
let child = priority_group_child(section, value);
return child != null ? child_bool(child, "switch_to_faster_same_priority", false) : false;
}
function priority_group_fastest_check_interval(section, value) {
let child = priority_group_child(section, value);
return child != null ? child_option(child, "fastest_check_interval", "3m") : "3m";
}
function priority_group_interrupt_exist_connections(section, value) {
let child = priority_group_child(section, value);
return child != null ? child_bool(child, "interrupt_exist_connections", true) : true;
}
function priority_group_pin_dashboard(section, value) {
let child = priority_group_child(section, value);
return child != null ? child_bool(child, "pin_dashboard", true) : true;
}
function priority_level_display_name(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_option(child, "name", as_string(value)) : as_string(value);
}
function priority_level_order(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_option(child, "order", "0") : "0";
}
function priority_level_direct(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_bool(child, "direct", false) : false;
}
function priority_level_filter_mode(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_option(child, "filter_mode", "include") : "include";
}
function priority_level_include_countries(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_list_alias(child, [ "include_countries", "country" ], []) : [];
}
function priority_level_include_outbounds(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_list_alias(child, [ "include_outbounds", "server_name" ], []) : [];
}
function priority_level_include_regex(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_list_alias(child, [ "include_regex", "regex" ], []) : [];
}
function priority_level_include_proxy_parameters(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_bool(child, "include_proxy_parameters", false) : false;
}
function priority_level_include_protocols(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_list(child, "include_protocols", []) : [];
}
function priority_level_include_transports(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_list(child, "include_transports", []) : [];
}
function priority_level_include_securities(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_list(child, "include_securities", []) : [];
}
function priority_level_exclude_countries(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_list(child, "exclude_countries", []) : [];
}
function priority_level_exclude_outbounds(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_list(child, "exclude_outbounds", []) : [];
}
function priority_level_exclude_regex(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_list(child, "exclude_regex", []) : [];
}
function priority_level_exclude_proxy_parameters(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_bool(child, "exclude_proxy_parameters", false) : false;
}
function priority_level_exclude_protocols(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_list(child, "exclude_protocols", []) : [];
}
function priority_level_exclude_transports(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_list(child, "exclude_transports", []) : [];
}
function priority_level_exclude_securities(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_list(child, "exclude_securities", []) : [];
}
function priority_level_countries(group_id, value) {
return priority_level_include_countries(group_id, value);
}
function priority_level_outbounds(group_id, value) {
return priority_level_include_outbounds(group_id, value);
}
function priority_level_regex(group_id, value) {
return priority_level_include_regex(group_id, value);
}
function priority_level_detect_server_country(group_id, value) {
let child = priority_level_child(group_id, value);
return child != null ? child_option(child, "detect_server_country", "flag_emoji") : "flag_emoji";
}
function append_unique(result, seen, value) {
value = as_string(value);
if (value == "" || seen[value])
return;
seen[value] = true;
push(result, value);
}
function subscription_download_targets(sections) {
let result = [];
let seen = {};
for (let section in sections) {
section = object_or_empty(section);
let name = option(section, ".name", "");
for (let source in subscription_urls(section)) {
let target = subscription_download_section(section, source);
if (target != "")
append_unique(result, seen, target);
}
}
return result;
}
function subscription_download_target_port(sections, target, base_port) {
target = as_string(target);
base_port = int(base_port || 0);
let targets = subscription_download_targets(sections);
for (let i = 0; i < length(targets); i++)
if (targets[i] == target)
return base_port + 2 + i;
return 0;
}
// Cascade-delete all child sections (subscription_url, urltest, etc.) that
// belong to the given parent section, along with cross-references and disk caches.
// Returns the number of children removed.
function cascade_delete_section(cursor, config_name, parent_name) {
let removed = 0;
config_name = as_string(config_name || CONFIG_NAME);
parent_name = as_string(parent_name || "");
if (parent_name == "")
return 0;
// 1. Delete priority_level belonging to priority_group in this section
try {
let group_names = [];
cursor.foreach(config_name, "priority_group", function(section) {
if (option(section, "section", "") == parent_name)
push(group_names, section_name(section));
});
for (let gname in group_names) {
let level_names = [];
cursor.foreach(config_name, "priority_level", function(section) {
if (option(section, "group", "") == gname || option(section, "section", "") == parent_name)
push(level_names, section_name(section));
});
for (let lname in level_names) {
cursor.delete(config_name, lname);
removed++;
}
cursor.delete(config_name, gname);
removed++;
}
} catch (e) {}
// 2. Delete all other child sections (subscription_url, section_interface, urltest, priority_level)
for (let type_name in [ "subscription_url", "section_interface", "urltest", "priority_level" ]) {
try {
let to_delete = [];
cursor.foreach(config_name, type_name, function(section) {
if (option(section, "section", "") == parent_name)
push(to_delete, section_name(section));
});
for (let name in to_delete) {
cursor.delete(config_name, name);
removed++;
}
} catch (e) {}
}
// 3. Clean references in settings
try {
let smart_detect = cursor.get(config_name, "settings", "smart_detect_sections");
if (type(smart_detect) == "array") {
let filtered = [];
for (let sec in smart_detect) {
if (as_string(sec) != parent_name)
push(filtered, sec);
}
if (length(filtered) != length(smart_detect))
cursor.set(config_name, "settings", "smart_detect_sections", filtered);
} else if (as_string(smart_detect) == parent_name) {
cursor.delete(config_name, "settings", "smart_detect_sections");
}
for (let opt in [ "dns_detour_section", "download_lists_via_proxy_section", "download_components_via_proxy_section", "warp_proxy_section" ]) {
if (as_string(cursor.get(config_name, "settings", opt)) == parent_name)
cursor.delete(config_name, "settings", opt);
}
} catch (e) {}
// 4. Clean references in other sections
try {
cursor.foreach(config_name, "section", function(sec) {
let sname = section_name(sec);
if (sname == parent_name) return;
if (option(sec, "outbound_detour_section", "") == parent_name) {
cursor.delete(config_name, sname, "outbound_detour_section");
cursor.set(config_name, sname, "outbound_detour_enabled", "0");
}
if (option(sec, "dns_detour_section", "") == parent_name) {
cursor.delete(config_name, sname, "dns_detour_section");
cursor.set(config_name, sname, "dns_detour_enabled", "0");
}
});
} catch (e) {}
// 5. Clean references in servers
try {
cursor.foreach(config_name, "server", function(srv) {
let sname = section_name(srv);
if (option(srv, "routing_section", "") == parent_name) {
cursor.delete(config_name, sname, "routing_section");
if (option(srv, "routing_mode", "") == "section")
cursor.set(config_name, sname, "routing_mode", "rules");
}
});
} catch (e) {}
// 6. Delete section cache files
try {
remove_file("/var/run/tachyon/section-cache/" + parent_name + ".json");
remove_file("/etc/tachyon/subscription-cache/" + parent_name + ".json");
remove_file("/etc/tachyon/subscription-cache/" + parent_name + ".yaml");
remove_file("/etc/tachyon/subscription-cache/" + parent_name + ".txt");
} catch (e) {}
return removed;
}
function cli_delete_section(sec_name) {
sec_name = trim(as_string(sec_name));
if (sec_name == "") {
warn("Usage: tachyon delete_section <section_name>\n");
return 1;
}
let c = uci_core.cursor();
c.load(CONFIG_NAME);
let s = c.get_all(CONFIG_NAME, sec_name);
if (!s) {
warn("Section '" + sec_name + "' not found\n");
return 1;
}
cascade_delete_section(c, CONFIG_NAME, sec_name);
c.delete(CONFIG_NAME, sec_name);
c.commit(CONFIG_NAME);
print(sprintf("{\"success\":true,\"deleted\":%J}\n", sec_name));
return 0;
}
let cli_mode = as_string(ARGV[0]);
if (cli_mode == "delete-section")
exit(cli_delete_section(ARGV[1]));
return {
option,
bool_option,
list_value,
whitespace_list_value,
cascade_delete_section,
set_item_sections,
set_item_sections_from_cursor,
set_item_sections_from_data,
settings_map,
item_settings,
item_option,
item_bool,
is_connections_action,
normalize_action,
action,
connection_urls,
subscription_urls,
interfaces,
outbound_jsons,
urltests,
dashboard_filter_mode,
dashboard_detect_server_country,
dashboard_include_countries,
dashboard_include_outbounds,
dashboard_include_regex,
dashboard_include_proxy_parameters,
dashboard_include_protocols,
dashboard_include_transports,
dashboard_include_securities,
dashboard_include_groups,
dashboard_exclude_countries,
dashboard_exclude_outbounds,
dashboard_exclude_regex,
dashboard_exclude_proxy_parameters,
dashboard_exclude_protocols,
dashboard_exclude_transports,
dashboard_exclude_securities,
dashboard_exclude_groups,
community_lists,
rule_sets,
rule_sets_with_subnets,
dscp_list,
dscp_value,
community_lists_value,
rule_sets_value,
rule_sets_with_subnets_value,
has_connection_sources,
subscription_url_settings,
interface_settings,
urltest_settings,
subscription_update_enabled,
subscription_update_interval,
subscription_dashboard_metadata_enabled,
subscription_auto_user_agent,
subscription_auto_hwid,
subscription_include_urltest_groups,
subscription_hide_urltest_group_outbounds,
subscription_hide_detour_outbounds,
subscription_prefix_nodes,
subscription_node_prefix,
subscription_user_agent,
subscription_hwid,
subscription_custom_device_headers,
subscription_device_os,
subscription_ver_os,
subscription_device_model,
subscription_device_locale,
subscription_app_version,
subscription_accept_language,
subscription_device_headers,
device_headers_signature,
subscription_device_headers_signature,
subscription_download_section,
subscription_insecure,
urltest_text_links,
interface_domain_resolver_enabled,
interface_domain_resolver_dns_type,
interface_domain_resolver_dns_server,
urltest_check_interval,
urltest_tolerance,
urltest_testing_url,
urltest_display_name,
urltest_idle_timeout,
urltest_interrupt_exist_connections,
urltest_filter_mode,
urltest_detect_server_country,
urltest_pin_dashboard,
urltest_include_countries,
urltest_include_outbounds,
urltest_include_regex,
urltest_include_proxy_parameters,
urltest_include_protocols,
urltest_include_transports,
urltest_include_securities,
urltest_exclude_countries,
urltest_exclude_outbounds,
urltest_exclude_regex,
urltest_exclude_proxy_parameters,
urltest_exclude_protocols,
urltest_exclude_transports,
urltest_exclude_securities,
priority_groups,
priority_group_settings,
priority_levels,
priority_group_display_name,
priority_group_health_url,
priority_group_active_check_interval,
priority_group_check_timeout,
priority_group_recovery_check_interval,
priority_group_pick_fastest,
priority_group_switch_to_faster_same_priority,
priority_group_fastest_check_interval,
priority_group_interrupt_exist_connections,
priority_group_pin_dashboard,
priority_level_display_name,
priority_level_order,
priority_level_direct,
priority_level_filter_mode,
priority_level_include_countries,
priority_level_include_outbounds,
priority_level_include_regex,
priority_level_include_proxy_parameters,
priority_level_include_protocols,
priority_level_include_transports,
priority_level_include_securities,
priority_level_exclude_countries,
priority_level_exclude_outbounds,
priority_level_exclude_regex,
priority_level_exclude_proxy_parameters,
priority_level_exclude_protocols,
priority_level_exclude_transports,
priority_level_exclude_securities,
priority_level_countries,
priority_level_outbounds,
priority_level_regex,
priority_level_detect_server_country,
parse_dscp_value,
dscp_list,
dscp_value,
geoip_country_list,
geoip_country_mode,
geoip_country_value,
subscription_download_targets,
subscription_download_target_port,
has_dns_matchers,
routed_dns_enabled,
routed_dns_type,
routed_dns_servers,
item_index_from_cursor,
cli_delete_section,
wdtt_sections,
olcrtc_sections,
is_wdtt_action,
is_olcrtc_action
};