#!/usr/bin/env ucode let fs = require("fs"); let common = require("core.common"); let helpers = require("core.helpers"); let core_ip = require("core.ip"); let core_url = require("core.url"); let runtime_constants = require("singbox.constants"); let runtime_dns = require("singbox.dns"); let runtime_route = require("singbox.route"); let runtime_rulesets = require("singbox.rulesets"); let runtime_subscription = require("singbox.subscription"); let runtime_servers = require("singbox.servers"); let runtime_urltest = require("singbox.urltest"); let connections = require("config.connections"); let rule_config = require("config.rule"); let source_rulesets = require("routing.rulesets"); let as_string = common.as_string; let option = common.option; let list_option = common.list_option; let bool_option = common.bool_option; let int_option = common.int_option; let array_or_empty = common.array_or_empty; let object_or_empty = common.object_or_empty; let outbound_tag = runtime_constants.outbound_tag; let tag = runtime_constants.tag; const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon"; let runtime_ruleset_folder = getenv("TACHYON_RULESET_FOLDER") || "/usr/share/tachyon/rulesets"; let ctx = {}; function settings_update_interval() { let settings = ctx.runtime_settings(); if (!bool_option(settings, "list_update_enabled", true)) return ""; let update_interval = option(settings, "update_interval", "1d"); return update_interval != "" ? update_interval : "1d"; } function remote_ruleset_update_interval() { let update_interval = settings_update_interval(); return update_interval != "" ? update_interval : runtime_constants.DISABLED_UPDATE_INTERVAL; } function init(c) { ctx = c; } function valid_section_name(name) { name = as_string(name); return match(name, /^[A-Za-z0-9_]+$/); } function section_enabled(section) { return bool_option(section, "enabled", true); } function ruleset_tag(section_name, name, kind) { return kind == "" ? section_name + "-" + name + "-ruleset" : section_name + "-" + name + "-" + kind + "-ruleset"; } function ruleset_registered(config, tag_name) { for (let rule_set in array_or_empty(config.route && config.route.rule_set)) { if (type(rule_set) == "object" && rule_set.tag == tag_name) return true; } return false; } function duration_to_seconds(value) { let rest = as_string(value); if (rest == "") return null; if (match(rest, /^[0-9]+$/) != null) return int(rest, 10); let total = 0.0; let multipliers = { ns: 0.000000001, us: 0.000001, ms: 0.001, s: 1, m: 60, h: 3600, d: 86400 }; while (rest != "") { let matched = match(rest, /^([0-9]+(\.[0-9]+)?)(ns|us|ms|s|m|h|d)/); if (!matched) return null; let token = as_string(matched[0]); total += (matched[1] * 1) * multipliers[matched[3]]; rest = substr(rest, length(token)); } return total <= 0 ? null : int(total + 0.5); } function urltest_check_interval(section, urltest_id) { let interval = connections.urltest_check_interval(section, urltest_id); return interval != "" ? interval : "3m"; } function urltest_idle_timeout(section, urltest_id) { let configured = connections.urltest_idle_timeout(section, urltest_id); let interval = urltest_check_interval(section, urltest_id); let interval_seconds = duration_to_seconds(interval); let idle_seconds = duration_to_seconds(configured != "" ? configured : runtime_constants.URLTEST_DEFAULT_IDLE_TIMEOUT); // sing-box refuses to start a URLTest group whose interval exceeds its // idle_timeout, and substitutes URLTEST_DEFAULT_IDLE_TIMEOUT when the // option is omitted. Raise the timeout to the interval instead of emitting // a config that fails with "interval must be less or equal than idle_timeout". if (interval_seconds != null && idle_seconds != null && interval_seconds > idle_seconds) return interval; return configured; } function supported_urltest_filter_mode(mode) { return mode == "include" || mode == "exclude" || mode == "mixed"; } function filter_mode_uses_include(mode) { return mode == "include" || mode == "mixed"; } function filter_mode_uses_exclude(mode) { return mode == "exclude" || mode == "mixed"; } function configured_country_filter(mode, include_countries, exclude_countries) { return (filter_mode_uses_include(mode) && length(array_or_empty(include_countries)) > 0) || (filter_mode_uses_exclude(mode) && length(array_or_empty(exclude_countries)) > 0); } function section_needs_country_is(section) { let dashboard_mode = connections.dashboard_filter_mode(section); if (connections.dashboard_detect_server_country(section) == "country_is" && configured_country_filter( dashboard_mode, connections.dashboard_include_countries(section), connections.dashboard_exclude_countries(section) )) return true; for (let urltest_id in connections.urltests(section)) { let mode = connections.urltest_filter_mode(section, urltest_id); if (connections.urltest_detect_server_country(section, urltest_id) == "country_is" && configured_country_filter( mode, connections.urltest_include_countries(section, urltest_id), connections.urltest_exclude_countries(section, urltest_id) )) return true; } for (let group_id in connections.priority_groups(section)) { for (let level_id in connections.priority_levels(group_id)) { if (connections.priority_level_direct(group_id, level_id)) continue; let mode = connections.priority_level_filter_mode(group_id, level_id); if (connections.priority_level_detect_server_country(group_id, level_id) == "country_is" && configured_country_filter( mode, connections.priority_level_include_countries(group_id, level_id), connections.priority_level_exclude_countries(group_id, level_id) )) return true; } } return false; } function section_has_direct_priority_level(section) { for (let group_id in connections.priority_groups(section)) for (let level_id in connections.priority_levels(group_id)) if (connections.priority_level_direct(group_id, level_id)) return true; return false; } function urltest_country_metadata(section, urltest_id, state) { let names = object_or_empty(object_or_empty(state.outboundMetadata).names); let from_flags = runtime_urltest.countries_from_flag_names(names); let detect_method = connections.urltest_detect_server_country(section, urltest_id); if (detect_method == "flag_emoji") return from_flags; let metadata = object_or_empty(object_or_empty(state.outboundMetadata).countries); let result = {}; for (let tag, c in from_flags) result[tag] = c; for (let tag, c in metadata) if (c != "") result[tag] = c; return result; } function array_contains(values, needle) { for (let value in array_or_empty(values)) { if (value == needle) return true; } return false; } function unique_string_array(values) { let result = []; let seen = {}; for (let value in array_or_empty(values)) { value = as_string(value); if (value == "" || seen[value]) continue; seen[value] = true; push(result, value); } return result; } function object_keys_set(values) { let result = {}; for (let value in array_or_empty(values)) result[value] = true; return result; } function tag_display_name(tag, names) { let name = as_string(object_or_empty(names)[tag] || ""); return name != "" ? name : tag; } function regex_match_set(tags, names, regexes) { return object_keys_set(runtime_urltest.regex_matching_tag_array(tags, names, regexes)); } function tag_name_filter_matches(tag, names, name_filter, regex_set) { let name = tag_display_name(tag, names); return array_contains(name_filter, name) || array_contains(name_filter, tag) || regex_set[tag]; } function tag_country_filter_matches(tag, countries, country_filter) { let country = uc(as_string(object_or_empty(countries)[tag] || "")); return country != "" && array_contains(country_filter, country); } function tag_attribute_filter_matches(tag, metadata, selected_values) { selected_values = array_or_empty(selected_values); if (length(selected_values) == 0) return true; let value = lc(as_string(object_or_empty(metadata)[tag] || "")); if (value == "") return false; for (let selected in selected_values) if (lc(as_string(selected)) == value) return true; return false; } function proxy_parameter_filter_matches_all(tag, metadata, protocols, transports, securities) { metadata = object_or_empty(metadata); return tag_attribute_filter_matches(tag, metadata.protocols, protocols) && tag_attribute_filter_matches(tag, metadata.transports, transports) && tag_attribute_filter_matches(tag, metadata.securities, securities); } function proxy_parameter_filter_matches_any(tag, metadata, protocols, transports, securities) { metadata = object_or_empty(metadata); return (length(array_or_empty(protocols)) > 0 && tag_attribute_filter_matches(tag, metadata.protocols, protocols)) || (length(array_or_empty(transports)) > 0 && tag_attribute_filter_matches(tag, metadata.transports, transports)) || (length(array_or_empty(securities)) > 0 && tag_attribute_filter_matches(tag, metadata.securities, securities)); } function name_or_country_filter_configured(name_filter, regexes, country_filter) { return length(array_or_empty(name_filter)) > 0 || length(array_or_empty(regexes)) > 0 || length(array_or_empty(country_filter)) > 0; } function urltest_all_candidate_outbounds(urltest_candidate_tags) { return unique_string_array(urltest_candidate_tags); } function urltest_matching_candidate_outbounds(urltest_candidate_tags, names, countries, name_filter, regexes, country_filter, metadata, proxy_parameters_enabled, proxy_parameters_operator, protocols, transports, securities, additional_matches) { names = object_or_empty(names); countries = object_or_empty(countries); country_filter = runtime_urltest.normalized_country_list(country_filter); let regex_set = regex_match_set(urltest_candidate_tags, names, regexes); let base_filter_configured = name_or_country_filter_configured(name_filter, regexes, country_filter); let additional_set = object_keys_set(additional_matches); let result = []; for (let tag in array_or_empty(urltest_candidate_tags)) { let base_matches = tag_name_filter_matches(tag, names, name_filter, regex_set) || tag_country_filter_matches(tag, countries, country_filter); let matches = additional_set[tag] || base_matches; if (proxy_parameters_enabled && proxy_parameters_operator == "or") { matches = additional_set[tag] || base_matches || proxy_parameter_filter_matches_any( tag, metadata, protocols, transports, securities ); } else if (proxy_parameters_enabled) { if (!base_filter_configured) base_matches = true; matches = additional_set[tag] || (base_matches && proxy_parameter_filter_matches_all( tag, metadata, protocols, transports, securities )); } if (matches) push(result, tag); } return unique_string_array(result); } function urltest_exclude_outbounds(all_outbounds, excluded_outbounds) { let excluded = object_keys_set(excluded_outbounds); let result = []; for (let tag in array_or_empty(all_outbounds)) { if (!excluded[tag]) push(result, tag); } return result; } function filter_candidate_outbounds(filter_mode, urltest_candidate_tags, names, countries, metadata, include_names, include_regex, include_countries, include_proxy_parameters, include_protocols, include_transports, include_securities, exclude_names, exclude_regex, exclude_countries, exclude_proxy_parameters, exclude_protocols, exclude_transports, exclude_securities, include_additional_matches, exclude_additional_matches) { let all_outbounds = urltest_all_candidate_outbounds(urltest_candidate_tags); if (filter_mode == "" || filter_mode == "disabled") return all_outbounds; if (!supported_urltest_filter_mode(filter_mode)) return all_outbounds; let include_outbounds = urltest_matching_candidate_outbounds( urltest_candidate_tags, names, countries, include_names, include_regex, include_countries, metadata, include_proxy_parameters, "and", include_protocols, include_transports, include_securities, include_additional_matches ); let exclude_outbounds = urltest_matching_candidate_outbounds( urltest_candidate_tags, names, countries, exclude_names, exclude_regex, exclude_countries, metadata, exclude_proxy_parameters, "or", exclude_protocols, exclude_transports, exclude_securities, exclude_additional_matches ); if (filter_mode == "include") return include_outbounds; if (filter_mode == "exclude") return urltest_exclude_outbounds(all_outbounds, exclude_outbounds); if (filter_mode == "mixed") return urltest_exclude_outbounds(include_outbounds, exclude_outbounds); return all_outbounds; } function urltest_filtered_outbounds(section, urltest_id, urltest_candidate_tags, state) { return filter_candidate_outbounds( connections.urltest_filter_mode(section, urltest_id), urltest_candidate_tags, object_or_empty(object_or_empty(state.outboundMetadata).names), urltest_country_metadata(section, urltest_id, state), object_or_empty(state.outboundMetadata), connections.urltest_include_outbounds(section, urltest_id), connections.urltest_include_regex(section, urltest_id), connections.urltest_include_countries(section, urltest_id), connections.urltest_include_proxy_parameters(section, urltest_id), connections.urltest_include_protocols(section, urltest_id), connections.urltest_include_transports(section, urltest_id), connections.urltest_include_securities(section, urltest_id), connections.urltest_exclude_outbounds(section, urltest_id), connections.urltest_exclude_regex(section, urltest_id), connections.urltest_exclude_countries(section, urltest_id), connections.urltest_exclude_proxy_parameters(section, urltest_id), connections.urltest_exclude_protocols(section, urltest_id), connections.urltest_exclude_transports(section, urltest_id), connections.urltest_exclude_securities(section, urltest_id) ); } function priority_level_country_metadata(group_id, level_id, state) { let names = object_or_empty(object_or_empty(state.outboundMetadata).names); let from_flags = runtime_urltest.countries_from_flag_names(names); let detect_method = connections.priority_level_detect_server_country(group_id, level_id); if (detect_method == "flag_emoji") return from_flags; let metadata = object_or_empty(object_or_empty(state.outboundMetadata).countries); let result = {}; for (let tag, c in from_flags) result[tag] = c; for (let tag, c in metadata) if (c != "") result[tag] = c; return result; } function priority_level_filtered_outbounds(group_id, level_id, urltest_candidate_tags, state) { if (connections.priority_level_direct(group_id, level_id)) return [ runtime_constants.DIRECT_OUTBOUND_TAG ]; return filter_candidate_outbounds( connections.priority_level_filter_mode(group_id, level_id), urltest_candidate_tags, object_or_empty(object_or_empty(state.outboundMetadata).names), priority_level_country_metadata(group_id, level_id, state), object_or_empty(state.outboundMetadata), connections.priority_level_include_outbounds(group_id, level_id), connections.priority_level_include_regex(group_id, level_id), connections.priority_level_include_countries(group_id, level_id), connections.priority_level_include_proxy_parameters(group_id, level_id), connections.priority_level_include_protocols(group_id, level_id), connections.priority_level_include_transports(group_id, level_id), connections.priority_level_include_securities(group_id, level_id), connections.priority_level_exclude_outbounds(group_id, level_id), connections.priority_level_exclude_regex(group_id, level_id), connections.priority_level_exclude_countries(group_id, level_id), connections.priority_level_exclude_proxy_parameters(group_id, level_id), connections.priority_level_exclude_protocols(group_id, level_id), connections.priority_level_exclude_transports(group_id, level_id), connections.priority_level_exclude_securities(group_id, level_id) ); } function dashboard_country_metadata(section, state) { let names = object_or_empty(object_or_empty(state.outboundMetadata).names); let from_flags = runtime_urltest.countries_from_flag_names(names); if (connections.dashboard_detect_server_country(section) == "flag_emoji") return from_flags; let metadata = object_or_empty(object_or_empty(state.outboundMetadata).countries); let result = {}; for (let tag, c in from_flags) result[tag] = c; for (let tag, c in metadata) if (c != "") result[tag] = c; return result; } function selected_group_outbounds(group_names, group_outbounds) { group_outbounds = object_or_empty(group_outbounds); let result = []; for (let group_name in array_or_empty(group_names)) for (let tag_name in array_or_empty(group_outbounds[group_name])) push(result, tag_name); return unique_string_array(result); } function remember_dashboard_group_outbounds(group_outbounds, group_name, outbounds) { group_name = as_string(group_name); if (group_name == "") return; let combined = array_or_empty(group_outbounds[group_name]); for (let tag_name in array_or_empty(outbounds)) push(combined, tag_name); group_outbounds[group_name] = unique_string_array(combined); } function dashboard_filtered_outbounds(section, selector_tags, state, group_outbounds) { return filter_candidate_outbounds( connections.dashboard_filter_mode(section), selector_tags, object_or_empty(object_or_empty(state.outboundMetadata).names), dashboard_country_metadata(section, state), object_or_empty(state.outboundMetadata), connections.dashboard_include_outbounds(section), connections.dashboard_include_regex(section), connections.dashboard_include_countries(section), connections.dashboard_include_proxy_parameters(section), connections.dashboard_include_protocols(section), connections.dashboard_include_transports(section), connections.dashboard_include_securities(section), connections.dashboard_exclude_outbounds(section), connections.dashboard_exclude_regex(section), connections.dashboard_exclude_countries(section), connections.dashboard_exclude_proxy_parameters(section), connections.dashboard_exclude_protocols(section), connections.dashboard_exclude_transports(section), connections.dashboard_exclude_securities(section), selected_group_outbounds(connections.dashboard_include_groups(section), group_outbounds), selected_group_outbounds(connections.dashboard_exclude_groups(section), group_outbounds) ); } function priority_levels_with_outbounds(group_id, urltest_candidate_tags, state) { let result = []; let assigned = {}; for (let level_id in connections.priority_levels(group_id)) { let outbounds = []; for (let tag_name in priority_level_filtered_outbounds(group_id, level_id, urltest_candidate_tags, state)) { if (!assigned[tag_name]) { assigned[tag_name] = true; push(outbounds, tag_name); } } push(result, { id: level_id, displayName: connections.priority_level_display_name(group_id, level_id), order: int(connections.priority_level_order(group_id, level_id), 10), direct: connections.priority_level_direct(group_id, level_id), filter_mode: connections.priority_level_filter_mode(group_id, level_id), detect_server_country: connections.priority_level_detect_server_country(group_id, level_id), outbounds }); } return result; } function priority_group_outbounds(levels) { let result = []; let seen = {}; for (let level in array_or_empty(levels)) { for (let tag_name in array_or_empty(level.outbounds)) { tag_name = as_string(tag_name); if (tag_name != "" && !seen[tag_name]) { seen[tag_name] = true; push(result, tag_name); } } } return result; } function urltest_outbound_tag(section_name, urltest_id) { urltest_id = as_string(urltest_id); return urltest_id == "urltest" ? outbound_tag(section_name + "-urltest") : outbound_tag(section_name + "-urltest-" + urltest_id); } function priority_outbound_tag(section_name, group_id) { return outbound_tag(section_name + "-priority-" + as_string(group_id)); } function add_urltest_outbound(config, section, urltest_id, urltest_candidate_tags, state) { let section_name = section[".name"]; let urltest_outbounds = urltest_filtered_outbounds(section, urltest_id, urltest_candidate_tags, state); let urltest_tag = urltest_outbound_tag(section_name, urltest_id); let display_name = connections.urltest_display_name(section, urltest_id); let urltest_outbound = { type: "urltest", tag: urltest_tag, outbounds: urltest_outbounds, url: connections.urltest_testing_url(section, urltest_id), interval: urltest_check_interval(section, urltest_id), tolerance: int(connections.urltest_tolerance(section, urltest_id), 10), interrupt_exist_connections: connections.urltest_interrupt_exist_connections(section, urltest_id) }; let idle_timeout = urltest_idle_timeout(section, urltest_id); if (idle_timeout != "") urltest_outbound.idle_timeout = idle_timeout; runtime_subscription.remember_outbound_metadata(state, urltest_tag, display_name, urltest_outbound); runtime_subscription.remember_urltest_group_config(state, urltest_tag, { displayName: display_name, outbounds: urltest_outbounds, url: urltest_outbound.url, interval: urltest_outbound.interval, tolerance: urltest_outbound.tolerance, idle_timeout: urltest_outbound.idle_timeout, interrupt_exist_connections: urltest_outbound.interrupt_exist_connections }); if (length(urltest_outbounds) == 0) return { tag: "", outbounds: [] }; push(config.outbounds, urltest_outbound); return { tag: urltest_tag, outbounds: urltest_outbounds }; } function add_priority_group_outbound(config, section, group_id, urltest_candidate_tags, state) { let section_name = section[".name"]; let levels = priority_levels_with_outbounds(group_id, urltest_candidate_tags, state); let outbounds = priority_group_outbounds(levels); let priority_tag = priority_outbound_tag(section_name, group_id); let display_name = connections.priority_group_display_name(section, group_id); let outbound = { type: "selector", tag: priority_tag, outbounds, default: outbounds[0], interrupt_exist_connections: connections.priority_group_interrupt_exist_connections(section, group_id) }; runtime_subscription.remember_outbound_metadata(state, priority_tag, display_name, outbound); runtime_subscription.remember_priority_group(state, priority_tag, { id: group_id, tag: priority_tag, section: section_name, displayName: display_name, health_url: connections.priority_group_health_url(section, group_id), active_check_interval: connections.priority_group_active_check_interval(section, group_id), check_timeout: connections.priority_group_check_timeout(section, group_id), recovery_check_interval: connections.priority_group_recovery_check_interval(section, group_id), pick_fastest: connections.priority_group_pick_fastest(section, group_id), switch_to_faster_same_priority: connections.priority_group_switch_to_faster_same_priority(section, group_id), fastest_check_interval: connections.priority_group_fastest_check_interval(section, group_id), interrupt_exist_connections: connections.priority_group_interrupt_exist_connections(section, group_id), pin_dashboard: connections.priority_group_pin_dashboard(section, group_id), outbounds, levels }); if (length(outbounds) == 0) return { tag: "", outbounds: [] }; push(config.outbounds, outbound); return { tag: priority_tag, outbounds }; } function is_mieru_detour(config, tag, depth) { depth = int(depth || 0); if (depth > 5 || tag == null || tag == "") return false; for (let out in array_or_empty(config.outbounds)) { if (type(out) == "object" && out.tag == tag) { if (out.type == "mieru") return true; if ((out.type == "selector" || out.type == "urltest") && type(out.outbounds) == "array") { for (let sub in out.outbounds) { if (is_mieru_detour(config, sub, depth + 1)) return true; } } } } return false; } function is_valid_detour(config, tag) { if (tag == null || tag == "") return false; if (tag == runtime_constants.DIRECT_OUTBOUND_TAG || tag == runtime_constants.BYPASS_OUTBOUND_TAG) return true; for (let out in array_or_empty(config.outbounds)) { if (type(out) == "object" && out.tag == tag) { if (out.tag == "DPI-out" || (out.type == "socks" && (out.server == "127.0.0.1" || out.server == "::1"))) return false; if (out.routing_mark != null) return false; if (out.type == "mieru" || is_mieru_detour(config, tag, 0)) return false; return true; } } return false; } function section_excluded_candidate_tags(section, candidate_tags, state) { let mode = connections.dashboard_filter_mode(section); if (mode != "exclude" && mode != "mixed") return []; let exclude_names = connections.dashboard_exclude_outbounds(section); let exclude_regex = connections.dashboard_exclude_regex(section); let exclude_countries = connections.dashboard_exclude_countries(section); let exclude_proxy_parameters = connections.dashboard_exclude_proxy_parameters(section); let exclude_protocols = connections.dashboard_exclude_protocols(section); let exclude_transports = connections.dashboard_exclude_transports(section); let exclude_securities = connections.dashboard_exclude_securities(section); let has_exclude_criteria = length(exclude_names) > 0 || length(exclude_regex) > 0 || length(exclude_countries) > 0 || (exclude_proxy_parameters && ( length(exclude_protocols) > 0 || length(exclude_transports) > 0 || length(exclude_securities) > 0 )); if (!has_exclude_criteria) return []; return urltest_matching_candidate_outbounds( candidate_tags, object_or_empty(object_or_empty(state.outboundMetadata).names), dashboard_country_metadata(section, state), exclude_names, exclude_regex, exclude_countries, object_or_empty(state.outboundMetadata), exclude_proxy_parameters, "or", exclude_protocols, exclude_transports, exclude_securities, [] ); } function add_proxy_selector(config, section, selector_tags, urltest_candidate_tags, state, text_urltest_tag) { let section_name = section[".name"]; let selector_tag = outbound_tag(section_name); let selector_outbounds = selector_tags; let selector_default = selector_tags[0]; let urltest_tags = []; let priority_tags = []; let group_outbounds = {}; let section_excluded = section_excluded_candidate_tags(section, urltest_candidate_tags, state); let group_candidate_tags = length(section_excluded) > 0 ? urltest_exclude_outbounds(urltest_candidate_tags, section_excluded) : urltest_candidate_tags; for (let urltest_id in connections.urltests(section)) { let urltest = add_urltest_outbound(config, section, urltest_id, group_candidate_tags, state); remember_dashboard_group_outbounds( group_outbounds, connections.urltest_display_name(section, urltest_id), urltest.outbounds ); if (urltest.tag == "") continue; push(urltest_tags, urltest.tag); } if (as_string(text_urltest_tag) != "" && index(urltest_tags, as_string(text_urltest_tag)) < 0) push(urltest_tags, text_urltest_tag); for (let group_id in connections.priority_groups(section)) { let priority = add_priority_group_outbound(config, section, group_id, group_candidate_tags, state); remember_dashboard_group_outbounds( group_outbounds, connections.priority_group_display_name(section, group_id), priority.outbounds ); if (priority.tag == "") continue; push(priority_tags, priority.tag); } selector_outbounds = dashboard_filtered_outbounds(section, selector_tags, state, group_outbounds); selector_default = selector_outbounds[0]; if (length(urltest_tags) > 0 || length(priority_tags) > 0) { for (let tag in urltest_tags) push(selector_outbounds, tag); for (let tag in priority_tags) push(selector_outbounds, tag); selector_default = length(urltest_tags) > 0 ? urltest_tags[0] : priority_tags[0]; } if (length(selector_outbounds) == 0) { if (length(selector_tags) > 0) { selector_outbounds = selector_tags; selector_default = selector_outbounds[0]; warn("Section " + section_name + ": server filtering produced no matches, falling back to all available subscription servers\n"); } else { selector_outbounds = [ runtime_constants.DIRECT_OUTBOUND_TAG ]; selector_default = runtime_constants.DIRECT_OUTBOUND_TAG; warn("Section " + section_name + ": subscription has no loaded servers yet, using direct fallback until cache is populated\n"); } } push(config.outbounds, { type: "selector", tag: selector_tag, outbounds: selector_outbounds, default: selector_default, interrupt_exist_connections: true }); } function ensure_custom_ruleset(config, reference) { let tag_name; let kind = runtime_rulesets.kind_from_reference_hint(reference); if (runtime_rulesets.is_community(reference)) { tag_name = "builtin-" + reference + "-ruleset"; kind = "domains"; if (!ruleset_registered(config, tag_name)) { let folder = ctx.runtime_ruleset_folder || runtime_ruleset_folder; let tmp_srs = folder + "/community-" + reference + ".srs"; let etc_srs = "/etc/tachyon/rulesets/community-" + reference + ".srs"; let local_path = null; if (runtime_rulesets.is_valid_srs_file(tmp_srs) || helpers.file_is_usable(tmp_srs, 16)) local_path = tmp_srs; else if (runtime_rulesets.is_valid_srs_file(etc_srs) || helpers.file_is_usable(etc_srs, 16)) local_path = etc_srs; if (local_path != null) { push(config.route.rule_set, { type: "local", tag: tag_name, format: "binary", path: local_path }); } else { let rule_set = { type: "remote", tag: tag_name, format: "binary", url: runtime_rulesets.community_url(reference) }; let detour = ctx.download_detour_tag(ctx.runtime_settings()); let is_1_14 = ctx.is_sb_1_14_plus && ctx.is_sb_1_14_plus(); if (!is_1_14 && is_valid_detour(config, detour)) rule_set.download_detour = detour; rule_set.update_interval = remote_ruleset_update_interval(); push(config.route.rule_set, rule_set); } } return { tag: tag_name, kind }; } tag_name = "inline-custom-" + runtime_rulesets.hash12(reference) + "-ruleset"; if (kind == "unknown") kind = "domains"; if (ruleset_registered(config, tag_name)) return { tag: tag_name, kind }; let extension = runtime_rulesets.file_extension(reference); if (substr(reference, 0, 1) == "/") { if (extension != "srs" && extension != "json") ctx.runtime_generate_unsupported("local rule_set extension is not supported by sing-box config generation"); // Skip broken local ruleset files — nonexistent or suspiciously small if (!helpers.file_is_usable(reference, 100)) return null; push(config.route.rule_set, { type: "local", tag: tag_name, format: extension == "json" ? "source" : "binary", path: reference }); } else if (substr(reference, 0, 7) == "http://" || substr(reference, 0, 8) == "https://") { let rule_set = { type: "remote", tag: tag_name, format: runtime_rulesets.remote_format(reference), url: reference }; let detour = ctx.download_detour_tag(ctx.runtime_settings()); let is_1_14 = ctx.is_sb_1_14_plus && ctx.is_sb_1_14_plus(); if (!is_1_14 && is_valid_detour(config, detour)) rule_set.download_detour = detour; rule_set.update_interval = remote_ruleset_update_interval(); push(config.route.rule_set, rule_set); } else { ctx.runtime_generate_unsupported("rule_set reference is not supported by sing-box config generation"); } return { tag: tag_name, kind }; } function ensure_community_ruleset(config, section_name, community) { if (!runtime_rulesets.is_community(community)) ctx.runtime_generate_unsupported("unknown community list " + community); let tag_name = ruleset_tag(section_name, community, "community"); if (!ruleset_registered(config, tag_name)) { let folder = ctx.runtime_ruleset_folder || runtime_ruleset_folder; let tmp_srs = folder + "/community-" + community + ".srs"; let etc_srs = "/etc/tachyon/rulesets/community-" + community + ".srs"; let local_path = null; if (runtime_rulesets.is_valid_srs_file(tmp_srs) || helpers.file_is_usable(tmp_srs, 16)) local_path = tmp_srs; else if (runtime_rulesets.is_valid_srs_file(etc_srs) || helpers.file_is_usable(etc_srs, 16)) local_path = etc_srs; if (local_path != null) { push(config.route.rule_set, { type: "local", tag: tag_name, format: "binary", path: local_path }); } else { let rule_set = { type: "remote", tag: tag_name, format: "binary", url: runtime_rulesets.community_url(community), update_interval: remote_ruleset_update_interval() }; let detour = ctx.download_detour_tag(ctx.runtime_settings(), "lists"); if (detour == "" && section_name != "") { let sec_out = outbound_tag(section_name); if (sec_out != "" && sec_out != runtime_constants.DIRECT_OUTBOUND_TAG && sec_out != runtime_constants.BYPASS_OUTBOUND_TAG) { detour = sec_out; } } let is_1_14 = ctx.is_sb_1_14_plus && ctx.is_sb_1_14_plus(); if (!is_1_14 && is_valid_detour(config, detour)) rule_set.download_detour = detour; push(config.route.rule_set, rule_set); } } return { tag: tag_name, kind: "domains" }; } function domain_ip_list_ruleset_tag(section_name) { return ruleset_tag(section_name, "lists", ""); } function domain_ip_list_ruleset_path(section_name) { let folder = ctx.runtime_ruleset_folder || runtime_ruleset_folder; let tag = domain_ip_list_ruleset_tag(section_name); let tmp_path = folder + "/" + tag + ".json"; let etc_path = "/etc/tachyon/rulesets/" + tag + ".json"; if (source_rulesets.has_rules(tmp_path)) return tmp_path; if (source_rulesets.has_rules(etc_path)) return etc_path; return tmp_path; } function reference_is_local(reference) { return substr(as_string(reference), 0, 1) == "/"; } function source_file_exists(path) { return fs.readfile(path) != null; } function rebuild_local_domain_ip_list_ruleset(section_name, references, domains_only) { let folder = ctx.runtime_ruleset_folder || runtime_ruleset_folder; let ruleset_path = folder + "/" + domain_ip_list_ruleset_tag(section_name) + ".json"; let has_local = false; for (let reference in references) { if (reference_is_local(reference)) { has_local = true; break; } } if (!has_local) return; fs.unlink(ruleset_path); source_rulesets.create_source(ruleset_path); for (let reference in references) { reference = as_string(reference); if (!reference_is_local(reference)) continue; if (!source_file_exists(reference)) { warn("local domain/IP list not found: ", reference, "\n"); continue; } source_rulesets.import_plain_list(reference, ruleset_path, "domain_suffix", "domains", "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, domains_only) { if (length(references) == 0) return; 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)) return; let tag_name = domain_ip_list_ruleset_tag(section_name); if (!ruleset_registered(config, tag_name)) { push(config.route.rule_set, { type: "local", tag: tag_name, format: "source", path: ruleset_path }); } if (!domains_only) push(rule_set_tags, tag_name); if (source_rulesets.has_domain_matchers(ruleset_path)) push(dns_rule_set_tags, tag_name); } function legacy_condition_values(section, key) { let raw_values = object_or_empty(section)[key]; let list_values = type(raw_values) == "array" ? raw_values : []; let option_text_values = type(raw_values) == "array" || key == "domain" ? [] : rule_config.text_list_values(raw_values, "comma-space"); let text_value = option(section, key + "_text", ""); let text_values = rule_config.text_list_values(text_value, "comma-space"); if (bool_option(section, key + "_text_mode", false) || bool_option(section, "conditions_text_mode", false)) return text_values; if (length(list_values) > 0) return list_values; if (length(option_text_values) > 0) return option_text_values; return text_values; } function combined_domain_source_values(section) { let values = []; if (type(object_or_empty(section)["domain"]) != "array") { for (let value in rule_config.text_list_values(option(section, "domain", ""), "comma-space")) if (as_string(value) != "") push(values, as_string(value)); } for (let value in rule_config.text_list_values(option(section, "domain_suffix_text", ""), "comma-space")) if (as_string(value) != "") push(values, as_string(value)); for (let value in list_option(section, "domain_suffix")) if (as_string(value) != "") push(values, as_string(value)); for (let value in list_option(section, "user_domains")) if (as_string(value) != "") push(values, as_string(value)); for (let value in rule_config.text_list_values(option(section, "user_domains_text", ""), "comma-space")) if (as_string(value) != "") push(values, as_string(value)); return values; } function domain_suffix_condition_value_kind(value) { return rule_config.prefixed_domain_kind_value(value); } function domain_conditions(section) { let result = { domain: [], domain_suffix: [], domain_keyword: [], domain_regex: [] }; for (let key in [ "domain", "domain_keyword", "domain_regex" ]) { for (let value in legacy_condition_values(section, key)) { let normalized = rule_config.domain_value_for_key(value, key); if (normalized != null) push(result[key], normalized); } } for (let value in combined_domain_source_values(section)) { let normalized = domain_suffix_condition_value_kind(value); if (normalized != null) push(result[normalized.kind], normalized.value); } return result; } function add_domain_array(rule, key, values) { if (length(values) > 0) rule[key] = values; } function push_dns_matcher_rule(config, rule) { push(config.dns.rules, rule); } function section_dns_server(section) { if (option(section, "action", "") == "bypass") return runtime_constants.DNS_SERVER_TAG; if (connections.routed_dns_enabled(section)) return runtime_constants.tag(section[".name"], "routed-dns-server"); return runtime_constants.FAKEIP_DNS_SERVER_TAG; } function routed_dns_server_tag(section_name) { return runtime_constants.tag(section_name, "routed-dns-server"); } function single_or_array(values) { return length(values) == 1 ? values[0] : values; } function dns_action_server_tag(section_name, idx) { if (idx == null) return runtime_constants.tag(section_name, "dns-server"); return runtime_constants.tag(section_name, "dns-server-" + idx); } 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 section_dns_servers(section) { let servers = list_option(section, "dns_server"); if (length(servers) == 0) { let single = option(section, "dns_server", ""); servers = [single]; } return servers; } function add_dns_server_for_section(config, section) { let section_name = section[".name"]; let servers = section_dns_servers(section); let dns_type = option(section, "dns_type", "udp"); let detour = dns_action_detour_tag(section); let server_tags = []; for (let i = 0; i < length(servers); i++) { let s_val = servers[i]; let tag_name = length(servers) <= 1 ? dns_action_server_tag(section_name) : dns_action_server_tag(section_name, i + 1); let server = runtime_dns.server_from_options( tag_name, dns_type, s_val, detour ); if (server.unsupported) ctx.runtime_generate_unsupported(server.unsupported); push(config.dns.servers, server); push(server_tags, tag_name); } return server_tags; } function add_routed_dns_server_for_section(config, section) { if (!connections.routed_dns_enabled(section)) return; let section_name = section[".name"]; let servers = connections.routed_dns_servers(section); let dns_type = connections.routed_dns_type(section); let tag_name = routed_dns_server_tag(section_name); let detour = outbound_tag(section_name); for (let i = 0; i < length(servers); i++) { let s_val = servers[i]; let server_tag = length(servers) <= 1 ? tag_name : runtime_constants.tag(section_name, "routed-dns-server-" + (i + 1)); let server = runtime_dns.server_from_options( server_tag, dns_type, s_val, detour ); if (server.unsupported) ctx.runtime_generate_unsupported(server.unsupported); push(config.dns.servers, server); } } function source_dns_inbound_matcher() { return [ runtime_constants.SOURCE_DNS_INBOUND_TAG ]; } function add_source_dns_matchers(rule, source_ip_cidr) { if (length(source_ip_cidr) == 0) return; rule.inbound = source_dns_inbound_matcher(); rule.source_ip_cidr = single_or_array(source_ip_cidr); } function add_dns_action_rules_for_section(config, section) { let domains = domain_conditions(section); let domain = domains.domain; let domain_suffix = domains.domain_suffix; let domain_keyword = domains.domain_keyword; let domain_regex = domains.domain_regex; let rule_set_tags = []; let section_name = section[".name"]; let source_ip_cidr = core_ip.normalize_to_cidrs(legacy_condition_values(section, "source_ip_cidr")); let fully_routed_ips = core_ip.normalize_to_cidrs(list_option(section, "fully_routed_ips")); 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 != null && 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(ctx.runtime_settings(), "dns_rewrite_ttl", "60"); let section_servers = section_dns_servers(section); let server_tags = []; if (length(section_servers) <= 1) { push(server_tags, dns_action_server_tag(section_name)); } else { for (let i = 0; i < length(section_servers); i++) push(server_tags, dns_action_server_tag(section_name, i + 1)); } let server_tag = single_or_array(server_tags); let has_inline_domains = length(domain) > 0 || length(domain_suffix) > 0 || length(domain_keyword) > 0 || length(domain_regex) > 0; if (length(fully_routed_ips) > 0) { let dns_rule = { action: "route", server: server_tag, rewrite_ttl }; add_source_dns_matchers(dns_rule, fully_routed_ips); push_dns_matcher_rule(config, dns_rule); } 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); add_source_dns_matchers(dns_rule, source_ip_cidr); push_dns_matcher_rule(config, dns_rule); } if (length(rule_set_tags) > 0) { let dns_rule = { action: "route", server: server_tag, rewrite_ttl, rule_set: single_or_array(rule_set_tags) }; add_source_dns_matchers(dns_rule, source_ip_cidr); push_dns_matcher_rule(config, dns_rule); } if (!has_inline_domains && length(rule_set_tags) == 0 && length(fully_routed_ips) == 0) ctx.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); } function add_dscp_matchers(rule, section) { let dscp_vals = connections.dscp_list(section); if (length(dscp_vals) > 0) rule.dscp = dscp_vals; } function add_port_matchers(rule, section) { let values = []; for (let value in list_option(section, "ports")) push(values, value); for (let value in rule_config.text_list_values(option(section, "ports_text", ""), "comma-space")) push(values, value); let ports = []; let port_ranges = []; let seen = {}; for (let value in values) { value = trim(as_string(value)); if (value == "" || seen[value]) continue; seen[value] = true; let dash = index(value, "-"); if (dash < 0) { let port = normalize_port_number_value(value); if (port != null) push(ports, port); continue; } let start = normalize_port_number_value(substr(value, 0, dash)); let end = normalize_port_number_value(substr(value, dash + 1)); if (start != null && end != null && start <= end) push(port_ranges, start == end ? as_string(start) : sprintf("%d:%d", start, end)); } if (length(ports) > 0) rule.port = ports; if (length(port_ranges) > 0) rule.port_range = port_ranges; } function tproxy_inbound_matcher() { if (!core_ip.ipv6_supported()) return [ runtime_constants.TPROXY_INBOUND_TAG ]; return [ runtime_constants.TPROXY_INBOUND_TAG, runtime_constants.TPROXY_INBOUND6_TAG ]; } function outbound_supports_udp(config, tag_name, visited) { tag_name = as_string(tag_name); if (tag_name == "") return false; if (tag_name == runtime_constants.DIRECT_OUTBOUND_TAG || tag_name == runtime_constants.BYPASS_OUTBOUND_TAG) return true; visited = visited || {}; if (visited[tag_name]) return true; visited[tag_name] = true; for (let outb in (config.outbounds || [])) { if (as_string(outb.tag) == tag_name) { let t = as_string(outb.type); if (t == "http" || (t == "socks" && as_string(outb.version) == "4") || t == "block") return false; if (t == "selector" || t == "urltest") { if (type(outb.outbounds) == "array" && length(outb.outbounds) > 0) { for (let nested_tag in outb.outbounds) { if (!outbound_supports_udp(config, nested_tag, visited)) return false; } return true; } return false; } return true; } } for (let ep in (config.endpoints || [])) { if (as_string(ep.tag) == tag_name) return true; } return false; } function push_section_route_rule(config, rule, target_outbound) { if (target_outbound && !outbound_supports_udp(config, target_outbound)) { if (rule.network == "tcp") { push(config.route.rules, rule); return; } if (rule.network == "udp") { delete rule.outbound; rule.action = "reject"; push(config.route.rules, rule); return; } let udp_rule = {}; for (let k, v in rule) udp_rule[k] = v; delete udp_rule.outbound; udp_rule.action = "reject"; udp_rule.network = "udp"; push(config.route.rules, udp_rule); rule.network = "tcp"; } push(config.route.rules, rule); } function add_fully_routed_ips_rule(config, section) { let source_ip_cidr = core_ip.normalize_to_cidrs(list_option(section, "fully_routed_ips")); if (length(source_ip_cidr) == 0) return; let target = runtime_route.target(section, outbound_tag(section[".name"])); if (target.unsupported) ctx.runtime_generate_unsupported(target.unsupported); let route_rule = { action: target.action, inbound: tproxy_inbound_matcher() }; if (target.outbound) route_rule.outbound = target.outbound; route_rule.source_ip_cidr = single_or_array(source_ip_cidr); push_section_route_rule(config, route_rule, target.outbound); } function add_excluded_ips_rule(config, section) { let excluded = list_option(section, "excluded_ips"); if (length(excluded) == 0) return; let resolved = core_ip.normalize_to_cidrs(excluded); if (length(resolved) == 0) return; let route_rule = { action: "route", inbound: tproxy_inbound_matcher(), outbound: runtime_constants.DIRECT_OUTBOUND_TAG, source_ip_cidr: single_or_array(resolved) }; push(config.route.rules, route_rule); } function add_excluded_protocol_rule(config, section) { let excluded = list_option(section, "excluded_protocol"); if (length(excluded) == 0) return; let route_rule = { action: "route", inbound: tproxy_inbound_matcher(), outbound: runtime_constants.DIRECT_OUTBOUND_TAG, protocol: single_or_array(excluded) }; push(config.route.rules, route_rule); } function add_protocol_matchers(rule, section) { let protocols = list_option(section, "protocol"); if (length(protocols) > 0) rule.protocol = single_or_array(protocols); } function load_community_subnet_cidrs(community) { let service = as_string(community); let paths = [ "/tmp/sing-box/rulesets/community-subnets-" + service + ".lst", "/etc/tachyon/rulesets/community-subnets-" + service + ".lst", (ctx.runtime_ruleset_folder || runtime_ruleset_folder || "/tmp/sing-box/rulesets") + "/community-subnets-" + service + ".lst" ]; for (let path in paths) { let content = fs.readfile(path); if (content != null && content != "") { let cidrs = []; for (let line in split(as_string(content), "\n")) { line = trim(replace(as_string(line), /\r/g, "")); if (line != "" && substr(line, 0, 1) != "#" && match(line, /^[0-9a-fA-F:.]+(\/[0-9]+)?$/) && !match(line, /\.$/)) { if (service == "discord" && core_ip.is_cloudflare_shared_cidr(line)) continue; push(cidrs, line); } } if (length(cidrs) > 0) return cidrs; } } return []; } function add_combined_route_for_section(config, section) { let domains = domain_conditions(section); let domain = domains.domain; let domain_suffix = domains.domain_suffix; let domain_keyword = domains.domain_keyword; let domain_regex = domains.domain_regex; let ip_cidr = legacy_condition_values(section, "ip_cidr"); let source_ip_cidr = core_ip.normalize_to_cidrs(legacy_condition_values(section, "source_ip_cidr")); let rule_set_tags = []; let dns_rule_set_tags = []; let section_name = section[".name"]; add_excluded_ips_rule(config, section); add_excluded_protocol_rule(config, section); add_fully_routed_ips_rule(config, section); let include_community_subnets = bool_option(section, "community_subnets", true); for (let community in connections.community_lists(section)) { let service = as_string(community); let ensured = ensure_community_ruleset(config, section_name, service); push(rule_set_tags, ensured.tag); push(dns_rule_set_tags, ensured.tag); if (include_community_subnets) { for (let cidr in load_community_subnet_cidrs(community)) push(ip_cidr, cidr); } } for (let reference in connections.rule_sets(section)) { let ensured = ensure_custom_ruleset(config, as_string(reference)); if (ensured == null) continue; push(rule_set_tags, ensured.tag); if (ensured.kind == "domains") push(dns_rule_set_tags, ensured.tag); } for (let reference in connections.rule_sets_with_subnets(section)) { let ensured = ensure_custom_ruleset(config, as_string(reference)); if (ensured == null) continue; push(rule_set_tags, ensured.tag); if (ensured.kind == "domains") push(dns_rule_set_tags, ensured.tag); } add_domain_ip_list_ruleset( config, section_name, rule_set_tags, dns_rule_set_tags, list_option(section, "domain_ip_lists"), false ); let target = runtime_route.target(section, outbound_tag(section_name)); if (target.unsupported) ctx.runtime_generate_unsupported(target.unsupported); let has_domain = length(domain) > 0 || length(domain_suffix) > 0 || length(domain_keyword) > 0 || length(domain_regex) > 0; let has_ruleset = length(rule_set_tags) > 0; let has_ip_cidr = length(ip_cidr) > 0; let create_section_route_rule = function() { let r = { action: target.action, inbound: tproxy_inbound_matcher() }; if (target.outbound) r.outbound = target.outbound; if (length(source_ip_cidr) > 0) r.source_ip_cidr = single_or_array(source_ip_cidr); add_port_matchers(r, section); add_dscp_matchers(r, section); add_protocol_matchers(r, section); return r; }; if (has_domain) { let domain_rule = create_section_route_rule(); add_domain_array(domain_rule, "domain", domain); add_domain_array(domain_rule, "domain_suffix", domain_suffix); add_domain_array(domain_rule, "domain_keyword", domain_keyword); add_domain_array(domain_rule, "domain_regex", domain_regex); let resolve = runtime_route.resolve_rule_for_section(section, domain_rule); if (type(resolve) == "object" && resolve.warning) warn(resolve.warning, "\n"); else if (type(resolve) == "object" && resolve.rule) push(config.route.rules, resolve.rule); push_section_route_rule(config, domain_rule, target.outbound); } if (has_ruleset) { let rs_rule = create_section_route_rule(); rs_rule.rule_set = single_or_array(rule_set_tags); let resolve = runtime_route.resolve_rule_for_section(section, rs_rule); if (type(resolve) == "object" && resolve.warning) warn(resolve.warning, "\n"); else if (type(resolve) == "object" && resolve.rule) push(config.route.rules, resolve.rule); push_section_route_rule(config, rs_rule, target.outbound); } if (has_ip_cidr) { let ip_rule = create_section_route_rule(); ip_rule.ip_cidr = ip_cidr; push_section_route_rule(config, ip_rule, target.outbound); } let country_list = connections.geoip_country_list(section); let country_mode = connections.geoip_country_mode(section); if (target.outbound && length(country_list) > 0) { let geo_tags = []; for (let cc in country_list) { let ip_ruleset = ensure_community_ruleset(config, section_name, "geoip_" + cc); if (ip_ruleset && ip_ruleset.tag) push(geo_tags, ip_ruleset.tag); if (cc == "ru") { let site_ruleset = ensure_community_ruleset(config, section_name, "geosite_ru"); if (site_ruleset && site_ruleset.tag) push(geo_tags, site_ruleset.tag); } } if (length(geo_tags) > 0) { let geoip_route_rule = create_section_route_rule(); geoip_route_rule.rule_set = geo_tags; if (country_mode == "exclude") geoip_route_rule.invert = true; push_section_route_rule(config, geoip_route_rule, target.outbound); } } if (!has_domain && !has_ruleset && !has_ip_cidr && length(country_list) == 0) { let fallback_rule = create_section_route_rule(); let has_any_matcher = fallback_rule.source_ip_cidr != null || fallback_rule.port != null || fallback_rule.port_range != null || fallback_rule.protocol != null || fallback_rule.dscp != null; if (has_any_matcher) push_section_route_rule(config, fallback_rule, target.outbound); } let rewrite_ttl = int_option(ctx.runtime_settings(), "dns_rewrite_ttl", "60"); if (length(domain) > 0 || length(domain_suffix) > 0 || length(domain_keyword) > 0 || length(domain_regex) > 0) { let dns_rule = { action: "route", server: section_dns_server(section), 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); add_source_dns_matchers(dns_rule, source_ip_cidr); push_dns_matcher_rule(config, dns_rule); } if (length(dns_rule_set_tags) > 0) { let dns_rule = { action: "route", server: section_dns_server(section), rewrite_ttl, rule_set: single_or_array(dns_rule_set_tags) }; add_source_dns_matchers(dns_rule, source_ip_cidr); push_dns_matcher_rule(config, dns_rule); } } function unsupported_matcher_key(section) { let unsupported_options = [ "subnet", "subnet_text", "local_domain_lists", "local_subnet_lists", "remote_domain_lists", "remote_subnet_lists" ]; for (let key in unsupported_options) { if (length(list_option(section, key)) > 0 || option(section, key, "") != "") return key; } return ""; } function add_outbound_for_section(config, section, taken, sections) { let section_name = section[".name"]; if (ctx.deferred_sections && ctx.deferred_sections[section_name]) return; let action = option(section, "action", ""); if (!valid_section_name(section_name)) ctx.runtime_generate_unsupported("section name is not safe for sing-box config generation"); let unsupported_matcher = unsupported_matcher_key(section); if (unsupported_matcher != "") ctx.runtime_generate_unsupported("section has unsupported matcher " + unsupported_matcher); if (connections.is_connections_action(action)) ctx.outbounds.add_connections_outbound(config, section, taken); else if (action == "awg") ctx.outbounds.add_awg_endpoint(config, section); else if (action == "warp") ctx.outbounds.add_warp_endpoint(config, section); else if (action == "anytls") ctx.outbounds.add_anytls_outbound(config, section); else if (action == "snell") ctx.outbounds.add_snell_outbound(config, section); else if (action == "mieru") ctx.outbounds.add_mieru_outbound(config, section); else if (action == "sudoku") ctx.outbounds.add_sudoku_outbound(config, section); else if (action == "masque") ctx.outbounds.add_masque_endpoint(config, section); else if (action == "openvpn") ctx.outbounds.add_openvpn_endpoint(config, section); else if (action == "zapret") ctx.outbounds.add_zapret_outbound(config, section, sections); else if (action == "zapret2") ctx.outbounds.add_zapret2_outbound(config, section, sections); else if (action == "byedpi") ctx.outbounds.add_byedpi_outbound(config, section, sections); else if (action == "wdtt") ctx.outbounds.add_wdtt_outbound(config, section, sections); else if (action == "olcrtc") ctx.outbounds.add_olcrtc_outbound(config, section, sections); else if (action == "bypass") { /* route-only action */ } else if (action == "block") { /* route-only action */ } else if (action == "dns") { add_dns_server_for_section(config, section); } else if (action == "hosts") { /* hosts-only action — no outbound or route needed */ } else { ctx.runtime_generate_unsupported("unsupported action " + action); } if (action != "dns" && action != "hosts" && action != "bypass" && action != "block") add_routed_dns_server_for_section(config, section); } function reserve_section_outbound_tags(sections, taken) { for (let section in sections) { let action = option(section, "action", ""); if (connections.is_connections_action(action) || action == "awg" || action == "warp" || action == "byedpi" || action == "zapret" || action == "zapret2" || action == "wdtt" || action == "olcrtc" || action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" || action == "masque" || action == "openvpn") taken[outbound_tag(section[".name"])] = true; if (!connections.is_connections_action(action)) continue; for (let urltest_id in connections.urltests(section)) taken[urltest_outbound_tag(section[".name"], urltest_id)] = true; for (let group_id in connections.priority_groups(section)) taken[priority_outbound_tag(section[".name"], group_id)] = true; } } function add_route_for_section(config, section) { if (ctx.deferred_sections && ctx.deferred_sections[section[".name"]]) return; let action = option(section, "action", ""); if (action == "dns") add_dns_action_rules_for_section(config, section); else if (action == "hosts") { /* hosts-only action — entries are consumed globally via combined cache */ } else add_combined_route_for_section(config, section); } function failover_candidate(sections) { let result = []; for (let section in sections) { if (ctx.deferred_sections && ctx.deferred_sections[section[".name"]]) continue; let action = option(section, "action", ""); if (connections.is_connections_action(action) || action == "awg" || action == "warp" || action == "byedpi" || action == "zapret" || action == "zapret2" || action == "wdtt" || action == "olcrtc" || action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" || action == "masque" || action == "openvpn") { push(result, section); } } return result; } function failover_state_file() { return getenv("TACHYON_FAILOVER_STATE_FILE") || "/etc/tachyon/failover_section"; } // Persisted user/agent choice survives restarts; ignored when the section // disappeared from the candidate list. function failover_default_name(candidates) { let raw = ""; try { raw = trim(as_string(fs.readfile(failover_state_file())) || ""); } catch (e) { raw = ""; } if (raw != "") { for (let section in candidates) if (as_string(section[".name"]) == raw) return raw; } return as_string(candidates[0][".name"]); } const FAILOVER_GROUP_TAG = "tachyon-failover"; function add_service_route_rules(config, sections) { let settings = object_or_empty(ctx.uci_cursor().get_all(CONFIG_NAME, "settings")); if (bool_option(settings, "dns_detour_enabled", false)) { let detour_section = option(settings, "dns_detour_section", ""); if (detour_section != "") { let detour_target = outbound_tag(detour_section); let dns_servers = list_option(settings, "dns_server"); if (length(dns_servers) == 0) { let single_dns = option(settings, "dns_server", ""); if (single_dns != "") dns_servers = [single_dns]; } let fallback_servers = list_option(settings, "dns_fallback_server"); if (length(fallback_servers) == 0) { let single_fb = option(settings, "dns_fallback_server", ""); if (single_fb != "") fallback_servers = [single_fb]; } let all_dns = []; for (let s in dns_servers) push(all_dns, s); for (let s in fallback_servers) push(all_dns, s); let detour_ips = []; let detour_domains = []; for (let s in all_dns) { s = trim(as_string(s)); if (s == "") continue; s = split(s, "#")[0]; let host = core_url.host(s); if (host == "") host = s; host = split(host, "/")[0]; if (core_ip.valid_ipv4(host) || core_ip.valid_ipv6(host)) push(detour_ips, host); else if (host != "") push(detour_domains, host); } if (length(detour_ips) > 0) { push_section_route_rule(config, { action: "route", inbound: tproxy_inbound_matcher(), outbound: detour_target, ip_cidr: single_or_array(detour_ips) }, detour_target); } if (length(detour_domains) > 0) { push_section_route_rule(config, { action: "route", inbound: tproxy_inbound_matcher(), outbound: detour_target, domain: single_or_array(detour_domains) }, detour_target); } } } let candidates = failover_candidate(sections); let first = length(candidates) > 0 ? candidates[0] : null; let failover_active = bool_option(settings, "section_failover_enabled", false) && length(candidates) > 1; if (first != null) { push(config.route.rules, { action: "resolve", inbound: tproxy_inbound_matcher(), server: runtime_constants.DNS_SERVER_TAG, domain: runtime_constants.CHECK_PROXY_IP_DOMAIN }); let catchall_target = outbound_tag(first[".name"]); if (failover_active) { let member_tags = []; for (let section in candidates) push(member_tags, outbound_tag(section[".name"])); push(config.outbounds, { type: "selector", tag: FAILOVER_GROUP_TAG, outbounds: member_tags, default: outbound_tag(failover_default_name(candidates)), interrupt_exist_connections: true }); catchall_target = FAILOVER_GROUP_TAG; } push_section_route_rule(config, { action: "route", inbound: tproxy_inbound_matcher(), outbound: catchall_target, domain: runtime_constants.CHECK_PROXY_IP_DOMAIN }, catchall_target); } push(config.route.rules, { action: "route-options", domain: runtime_constants.FAKEIP_TEST_DOMAIN, override_port: 8443 }); } function enabled_sections() { let result = []; ctx.uci_cursor().foreach(CONFIG_NAME, "section", function(section) { if (section_enabled(section)) push(result, section); }); return result; } function enabled_servers() { let result = []; ctx.uci_cursor().foreach(CONFIG_NAME, "server", function(section) { if (section_enabled(section)) push(result, section); }); return result; } function section_by_name(sections, name) { name = as_string(name); for (let section in sections) if (as_string(section[".name"]) == name) return section; return null; } function add_server_routes(config, servers, sections) { for (let server in servers) { // Native-mode Tailscale runs outside sing-box: no inbound, no rules. if (runtime_servers.is_native_tailscale(server)) continue; runtime_servers.add_sniff_rule(config, server); let inbound = runtime_constants.server_inbound_tag(server[".name"]); let isolate_lan = bool_option(server, "isolate_lan_for_users", false) || bool_option(server, "block_private_for_users", false); let isolated_users = list_option(server, "isolated_users"); if (length(isolated_users) == 0) isolated_users = list_option(server, "blocked_users"); if (isolate_lan) { let isolate_rule = { action: "reject", inbound: inbound, ip_is_private: true }; if (length(isolated_users) > 0) isolate_rule.auth_user = isolated_users; let isolated_subnets = list_option(server, "isolated_subnets"); if (length(isolated_subnets) > 0) isolate_rule.ip_cidr = isolated_subnets; push(config.route.rules, isolate_rule); } let custom_rules = list_option(server, "custom_route_rules"); if (length(custom_rules) == 0) custom_rules = list_option(server, "custom_rules_json"); for (let rule_str in custom_rules) { try { let r = json(rule_str); if (type(r) == "object") { if (!r.inbound) r.inbound = inbound; push(config.route.rules, r); } } catch(e) {} } let routing_mode = option(server, "routing_mode", "rules"); if (routing_mode == "rules") { runtime_servers.clone_rules_for_inbound( config, runtime_constants.TPROXY_INBOUND_TAG, inbound, runtime_constants.CHECK_PROXY_IP_DOMAIN ); } else if (routing_mode == "direct") { push(config.route.rules, { action: "route", inbound, outbound: runtime_constants.DIRECT_OUTBOUND_TAG }); } else if (routing_mode == "section") { let routing_section_name = option(server, "routing_section", ""); let routing_section = section_by_name(sections, routing_section_name); if (routing_section == null) ctx.runtime_generate_unsupported("server references missing routing section " + routing_section_name); let action = option(routing_section, "action", ""); if (action == "bypass" || action == "block") ctx.runtime_generate_unsupported("server routing section " + routing_section_name + " cannot use action " + action); let target = runtime_route.target(routing_section, outbound_tag(routing_section[".name"])); if (target.unsupported) ctx.runtime_generate_unsupported(target.unsupported); let rule = { action: target.action, inbound }; if (target.outbound) rule.outbound = target.outbound; push_section_route_rule(config, rule, target.outbound); } else { ctx.runtime_generate_unsupported("unsupported server routing_mode " + routing_mode); } } } return { init, ensure_custom_ruleset, ruleset_registered, section_needs_country_is, section_has_direct_priority_level, urltest_outbound_tag, priority_outbound_tag, add_urltest_outbound, add_priority_group_outbound, add_proxy_selector, ensure_community_ruleset, domain_ip_list_ruleset_tag, domain_ip_list_ruleset_path, add_domain_ip_list_ruleset, legacy_condition_values, domain_conditions, add_domain_array, push_dns_matcher_rule, section_dns_server, add_dns_server_for_section, add_dns_action_rules_for_section, add_port_matchers, add_fully_routed_ips_rule, add_excluded_ips_rule, add_combined_route_for_section, unsupported_matcher_key, add_outbound_for_section, reserve_section_outbound_tags, add_route_for_section, add_service_route_rules, enabled_sections, enabled_servers, section_by_name, add_server_routes, outbound_supports_udp, push_section_route_rule, is_valid_detour, load_community_subnet_cidrs };