Files
NeoTachyon/tachyon/files/usr/lib/singbox/generator.uc
T

1641 lines
57 KiB
Ucode

#!/usr/bin/env ucode
let fs = require("fs");
let common = require("core.common");
let core_ip = require("core.ip");
let uci_core = require("core.uci");
let runtime_constants = require("singbox.constants");
let runtime_country = require("singbox.country");
let runtime_dns = require("singbox.dns");
let runtime_route = require("singbox.route");
let runtime_rulesets = require("singbox.rulesets");
let runtime_servers = require("singbox.servers");
let runtime_subscription = require("singbox.subscription");
let runtime_url = require("core.url");
let runtime_urltest = require("singbox.urltest");
let source_rulesets = require("routing.rulesets");
let rule_config = require("config.rule");
let connections = require("config.connections");
let subscription_share_link = require("subscription.share_link");
let uci = null;
let fixture_uci_data = null;
let runtime_settings_cache = null;
let runtime_ruleset_folder = runtime_constants.TMP_RULESET_FOLDER;
let runtime_supports_xhttp = true;
const TACHYON_RUNTIME_STATE_DIR = getenv("TACHYON_RUNTIME_STATE_DIR") || "/var/run/tachyon";
const TACHYON_SUBSCRIPTION_DEFERRED_SECTIONS_FILE = getenv("TACHYON_SUBSCRIPTION_DEFERRED_SECTIONS_FILE") || TACHYON_RUNTIME_STATE_DIR + "/deferred-sections";
let as_string = common.as_string;
let read_json_file = common.read_json_file;
let read_stdin = common.read_stdin;
let read_stdin_json = common.read_stdin_json;
let write_json = common.write_json;
let csv_to_json_array = common.csv_to_json_array;
let write_json_file = common.write_json_file;
let strip_internal_fields = common.strip_internal_fields;
let array_or_empty = common.array_or_empty;
let object_or_empty = common.object_or_empty;
let option = common.option;
let list_option = common.list_option;
let bool_option = common.bool_option;
let int_option = common.int_option;
let url_decode = runtime_url.decode;
let url_scheme = runtime_url.scheme;
let url_fragment = runtime_url.fragment;
let url_strip_fragment_value = runtime_url.strip_fragment;
let url_host = runtime_url.host;
let url_port = runtime_url.port;
let url_userinfo = runtime_url.userinfo;
let url_path = runtime_url.path;
let url_query_params = runtime_url.query_params;
const CONFIG_NAME = "tachyon";
function parse_deferred_sections(deferred_sections) {
let raw = trim(as_string(deferred_sections || getenv("TACHYON_SUBSCRIPTION_DEFERRED_SECTIONS") || ""));
if (raw == "" && fs.stat(TACHYON_SUBSCRIPTION_DEFERRED_SECTIONS_FILE) != null) {
raw = trim(as_string(fs.readfile(TACHYON_SUBSCRIPTION_DEFERRED_SECTIONS_FILE)) || "");
}
let res = {};
if (raw != "") {
for (let s in split(raw, /[ \t\r\n]+/)) {
s = trim(s);
if (s != "")
res[s] = true;
}
}
return res;
}
// UCI stores values written as '<b 0x...>' strings as binary, which when
// serialized back to JSON appear as '<b 0x...>' — invalid for sing-box.
function uci_bin_to_hex(val) {
if (val == null || val == "") return "";
let s = as_string(val);
s = replace(s, /^<b\s*/i, "");
s = replace(s, /^0x/i, "");
s = replace(s, />$/, "");
s = replace(s, /\s+/g, "");
if (match(s, /^[0-9a-fA-F]+$/)) {
return lc(s);
}
let hex = "";
for (let i = 0; i < length(val); i++) {
let code = ord(val, i);
hex += sprintf("%02x", code);
}
return hex;
}
let generator_outbounds = require("singbox.generator_outbounds");
let generator_routes = require("singbox.generator_routes");
let ctx = {
outbounds: generator_outbounds,
routes: generator_routes,
runtime_supports_xhttp: true
};
let reserved_runtime_tag_set = null;
let assert_unique_outbound_tags = null;
let enabled_sections = null;
let enabled_servers = null;
let reserve_section_outbound_tags = null;
let add_outbound_for_section = null;
let add_service_route_rules = null;
let add_route_for_section = null;
let add_server_routes = null;
let ensure_custom_ruleset = null;
function parent_dir(path) {
path = as_string(path);
let slash = rindex(path, "/");
return slash <= 0 ? "" : substr(path, 0, slash);
}
function ensure_dir(path) {
path = as_string(path);
if (path == "" || path == "/")
return true;
if (fs.stat(path) != null)
return true;
let parent = parent_dir(path);
if (parent != "" && !ensure_dir(parent))
return false;
return fs.mkdir(path, 0755) || fs.stat(path) != null;
}
function ensure_parent_dir(path) {
return ensure_dir(parent_dir(path));
}
function atomic_write_json_file(path, value, indent) {
if (!ensure_parent_dir(path))
return false;
return write_json_file(path, value, indent != null ? indent : 2);
}
function fixture_section_list(type_name) {
let value = object_or_empty(fixture_uci_data)[type_name];
if (type(value) == "array")
return value;
if (type(value) == "object")
return [ value ];
let plural = object_or_empty(fixture_uci_data)[type_name + "s"];
return type(plural) == "array" ? plural : [];
}
function fixture_get_section(section_name) {
let fixture = object_or_empty(fixture_uci_data);
if (section_name == "settings" && type(fixture.settings) == "object")
return fixture.settings;
for (let type_name in [ "settings", "server", "section", "subscription_url", "section_interface", "urltest", "priority_group", "priority_level" ]) {
for (let section in fixture_section_list(type_name)) {
if (as_string(section[".name"]) == section_name)
return section;
}
}
return {};
}
function fixture_cursor(path) {
fixture_uci_data = object_or_empty(read_json_file(path));
connections.set_item_sections_from_data(fixture_uci_data);
return {
load: function(_config_name) {
return true;
},
get_all: function(_config_name, section_name) {
return fixture_get_section(section_name);
},
foreach: function(_config_name, type_name, callback) {
for (let section in fixture_section_list(type_name))
callback(section);
}
};
}
function use_fixture_cursor(path) {
uci = fixture_cursor(path);
runtime_settings_cache = null;
}
function runtime_uci_cursor() {
return {
load: function(package_name) {
return uci_core.load(package_name);
},
get_all: function(package_name, section_name) {
return uci_core.get_all(package_name, section_name);
},
foreach: function(package_name, type_name, callback) {
for (let section in uci_core.section_objects(package_name, type_name))
callback(section);
}
};
}
function uci_cursor() {
if (uci == null)
uci = runtime_uci_cursor();
return uci;
}
function runtime_generate_unsupported(reason) {
warn(reason, "\n");
exit(2);
}
function valid_section_name(name) {
return match(name, /^[A-Za-z0-9_]+$/);
}
function section_enabled(section) {
return bool_option(section, "enabled", true);
}
function runtime_settings() {
if (runtime_settings_cache == null)
runtime_settings_cache = object_or_empty(uci_cursor().get_all(CONFIG_NAME, "settings"));
return runtime_settings_cache;
}
function settings_update_interval() {
let settings = 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 internal_flag(value) {
return value === true || value == 1 || value == "1" || value == "true" || value == "yes";
}
function subscription_group_outbound(outbound) {
if (type(outbound) != "object")
return false;
let t = as_string(outbound.type);
return (t == "selector" || t == "urltest") && internal_flag(outbound.__tachyon_allow_group);
}
function subscription_urltest_group_outbound(outbound) {
if (type(outbound) != "object")
return false;
return as_string(outbound.type || "") == "urltest" && internal_flag(outbound.__tachyon_allow_group);
}
function subscription_outbound_tag(outbound) {
return type(outbound) == "object" ? as_string(outbound.tag || "") : "";
}
function subscription_visibility_refs(outbounds) {
let refs = {
urltest: {},
detour: {}
};
for (let outbound in array_or_empty(outbounds)) {
if (type(outbound) != "object")
continue;
if (subscription_urltest_group_outbound(outbound)) {
for (let tag_name in array_or_empty(outbound.outbounds)) {
tag_name = as_string(tag_name);
if (tag_name != "")
refs.urltest[tag_name] = true;
}
}
let detour = as_string(outbound.detour || "");
if (detour != "")
refs.detour[detour] = true;
}
return refs;
}
function subscription_hidden_outbound(outbound, refs, hide_urltest_group_outbounds, hide_detour_outbounds) {
if (type(outbound) != "object")
return false;
let tag_name = subscription_outbound_tag(outbound);
let urltest_refs = object_or_empty(object_or_empty(refs).urltest);
let detour_refs = object_or_empty(object_or_empty(refs).detour);
let hidden_by_urltest = tag_name != "" && urltest_refs[tag_name];
let hidden_by_detour = tag_name != "" && detour_refs[tag_name];
if (hidden_by_urltest && hide_urltest_group_outbounds !== false)
return true;
if (hidden_by_detour && hide_detour_outbounds !== false)
return true;
return internal_flag(outbound.__tachyon_hidden) && !hidden_by_urltest && !hidden_by_detour;
}
function tag(base, postfix) {
return runtime_constants.tag(base, postfix);
}
function outbound_tag(section_name) {
return runtime_constants.outbound_tag(section_name);
}
function download_via_proxy_section_option_for_purpose(purpose) {
purpose = as_string(purpose || "lists");
if (purpose == "lists")
return "download_lists_via_proxy_section";
if (purpose == "components")
return "download_components_via_proxy_section";
return "";
}
function download_via_proxy_option_for_purpose(purpose) {
purpose = as_string(purpose || "lists");
if (purpose == "lists")
return "download_lists_via_proxy";
if (purpose == "components")
return "download_components_via_proxy";
return "";
}
function download_via_proxy_section(settings, purpose) {
let enabled_option = download_via_proxy_option_for_purpose(purpose);
if (enabled_option == "" || !bool_option(settings, enabled_option, false))
return "";
let all_enabled = type(enabled_sections) == "function" ? enabled_sections() : [];
let valid_proxy_sections = {};
for (let s in all_enabled) {
let act = option(s, "action", "");
if (act != "bypass" && act != "block" && act != "dns" && act != "hosts" && act != "")
valid_proxy_sections[s[".name"]] = true;
}
let section_option = download_via_proxy_section_option_for_purpose(purpose);
let configured = section_option != "" ? option(settings, section_option, "") : "";
if (configured != "" && valid_proxy_sections[configured])
return configured;
let fallback_configured = option(settings, "download_lists_via_proxy_section", "");
if (fallback_configured != "" && valid_proxy_sections[fallback_configured])
return fallback_configured;
for (let s in all_enabled) {
if (valid_proxy_sections[s[".name"]])
return s[".name"];
}
return "";
}
function download_via_proxy_enabled(settings, purpose) {
let enabled_option = download_via_proxy_option_for_purpose(purpose);
return enabled_option != "" && bool_option(settings, enabled_option, false);
}
function download_via_proxy_any_enabled(settings, sections) {
return download_via_proxy_enabled(settings, "lists") ||
download_via_proxy_enabled(settings, "components") ||
length(connections.subscription_download_targets(sections || [])) > 0;
}
function download_detour_tag(settings, purpose) {
let section_name = download_via_proxy_section(settings, purpose);
return section_name == "" ? "" : outbound_tag(section_name);
}
function ruleset_tag(section_name, name, kind) {
kind = as_string(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 clash_api_config(settings, service_address) {
let controller = as_string(service_address || "");
if (bool_option(settings, "enable_yacd", false) && bool_option(settings, "enable_yacd_wan_access", false))
controller = "0.0.0.0";
else if (controller == "")
controller = "0.0.0.0";
let result = {
external_controller: controller + ":9090"
};
if (bool_option(settings, "enable_yacd", false)) {
result.external_ui = "ui";
let secret = option(settings, "yacd_secret_key", "");
if (secret != "")
result.secret = secret;
}
return result;
}
function cli_bool(value) {
return value === true || value == "1" || value == "true" || value == "yes" || value == "on";
}
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 ];
}
let cached_sb_version = null;
function detect_sing_box_version() {
if (cached_sb_version != null)
return cached_sb_version;
let env_file = getenv("SB_VERSION_STATE_FILE");
if (env_file != null && env_file != "") {
let ver = trim(fs.readfile(env_file) || "");
if (ver != "") {
cached_sb_version = ver;
return ver;
}
}
try {
for (let cmd in [ "sing-box version 2>/dev/null", "/usr/bin/sing-box version 2>/dev/null" ]) {
let pipe = fs.popen(cmd, "r");
if (pipe) {
let out = pipe.read("all");
pipe.close();
let m = match(out, /sing-box version ([^\s]+)/);
if (m) {
cached_sb_version = m[1];
try {
let cur_disk = trim(fs.readfile("/etc/tachyon/sing-box-version") || "");
if (cur_disk != cached_sb_version)
fs.writefile("/etc/tachyon/sing-box-version", cached_sb_version + "\n");
} catch (e) {}
return cached_sb_version;
}
}
}
} catch (e) {}
let sb_version_file = "/etc/tachyon/sing-box-version";
let sb_version_val = trim(fs.readfile(sb_version_file) || "");
if (sb_version_val == "") {
let sb_ui_cache = getenv("TACHYON_UI_SING_BOX_VERSION_CACHE_FILE") || "/var/run/tachyon/ui-state/sing-box-version";
sb_version_val = trim(fs.readfile(sb_ui_cache) || "");
}
cached_sb_version = sb_version_val;
return cached_sb_version;
}
function is_sb_1_14_plus_detected(sb_version_val) {
if (sb_version_val == null || sb_version_val == "")
sb_version_val = detect_sing_box_version();
if (sb_version_val != "") {
let m = match(sb_version_val, /^v?([0-9]+)\.([0-9]+)/);
if (m) {
let major = int(m[1]);
let minor = int(m[2]);
return major > 1 || (major == 1 && minor >= 14);
}
}
return false;
}
function is_sb_1_15_plus_detected(sb_version_val) {
if (sb_version_val == null || sb_version_val == "")
sb_version_val = detect_sing_box_version();
if (sb_version_val != "") {
let m = match(sb_version_val, /^v?([0-9]+)\.([0-9]+)/);
if (m) {
let major = int(m[1]);
let minor = int(m[2]);
return major > 1 || (major == 1 && minor >= 15);
}
}
return false;
}
function base_config(settings, service_address, runtime_context) {
runtime_context = object_or_empty(runtime_context);
let log_level = option(settings, "log_level", "warn");
let rewrite_ttl = int_option(settings, "dns_rewrite_ttl", "60");
let turbo_cache = bool_option(settings, "dns_turbo_cache", false);
let cache_path = option(settings, "cache_path", "/tmp/sing-box/cache.db");
if (turbo_cache && (cache_path == "/tmp/sing-box/cache.db" || cache_path == ""))
cache_path = "/etc/sing-box/cache.db";
let cache_dir = replace(cache_path, /\/[^\/]+$/, "");
if (cache_dir != "" && cache_dir != cache_path) {
try { fs.mkdir(cache_dir, 0755); } catch(e) {}
}
let dns_config = runtime_dns.config(settings);
if (dns_config.unsupported)
runtime_generate_unsupported(dns_config.unsupported);
let dns_rules = [];
let dns_hosts_predefined = [];
let raw_dns_hosts = settings["dns_hosts"];
let dns_hosts_entries = [];
if (raw_dns_hosts != null && raw_dns_hosts != "") {
for (let line in split(as_string(raw_dns_hosts), "\n"))
push(dns_hosts_entries, line);
}
for (let host_entry in dns_hosts_entries) {
let entry_str = trim(host_entry);
if (entry_str == "" || substr(entry_str, 0, 1) == "#") continue;
let parts = split(entry_str, /[ \t]+/);
if (length(parts) >= 2) {
let p1 = parts[0];
let p2 = parts[1];
if (core_ip.valid_ip(p1)) {
let ip = p1;
let rr_type = index(ip, ":") != -1 ? "AAAA" : "A";
for (let i = 1; i < length(parts); i++) {
let d = parts[i];
if (d != "" && substr(d, 0, 1) != "#") {
push(dns_hosts_predefined, {
action: "predefined",
domain: [d],
answer: [d + ". 60 IN " + rr_type + " " + ip]
});
}
}
} else if (core_ip.valid_ip(p2)) {
let domain = p1;
let ip = p2;
let rr_type = index(ip, ":") != -1 ? "AAAA" : "A";
push(dns_hosts_predefined, {
action: "predefined",
domain: [domain],
answer: [domain + ". 60 IN " + rr_type + " " + ip]
});
}
}
}
for (let rule in dns_config.rules)
push(dns_rules, rule);
for (let rule in [
{ action: "reject", query_type: [ "HTTPS", "SVCB" ] },
{ action: "reject", domain_suffix: "use-application-dns.net" },
{
action: "route",
server: runtime_constants.FAKEIP_DNS_SERVER_TAG,
rewrite_ttl,
domain: [ runtime_constants.FAKEIP_TEST_DOMAIN, runtime_constants.CHECK_PROXY_IP_DOMAIN ]
}
])
push(dns_rules, rule);
let dns_servers = [];
for (let server in dns_config.servers)
push(dns_servers, server);
let fakeip_server = {
type: "fakeip",
tag: runtime_constants.FAKEIP_DNS_SERVER_TAG,
inet4_range: runtime_constants.FAKEIP_INET4_RANGE
};
let strategy = option(settings, "dns_strategy", "");
if (bool_option(settings, "ipv6_enabled", false) || strategy == "prefer_ipv6" || strategy == "ipv6_only")
fakeip_server.inet6_range = runtime_constants.FAKEIP_INET6_RANGE;
push(dns_servers, fakeip_server);
let inbounds = [
{ type: "tproxy", tag: runtime_constants.TPROXY_INBOUND_TAG, listen: runtime_constants.TPROXY_INBOUND_ADDRESS, listen_port: runtime_constants.TPROXY_INBOUND_PORT, tcp_fast_open: true, udp_fragment: true }
];
if (core_ip.ipv6_supported()) {
push(inbounds, { type: "tproxy", tag: runtime_constants.TPROXY_INBOUND6_TAG, listen: runtime_constants.TPROXY_INBOUND6_ADDRESS, listen_port: runtime_constants.TPROXY_INBOUND_PORT, tcp_fast_open: true, udp_fragment: true });
}
push(inbounds, { type: "direct", tag: runtime_constants.DNS_INBOUND_TAG, listen: runtime_constants.DNS_INBOUND_ADDRESS, listen_port: runtime_constants.DNS_INBOUND_PORT });
if (runtime_context.source_aware_dns) {
push(inbounds, {
type: "direct",
tag: runtime_constants.SOURCE_DNS_INBOUND_TAG,
listen: core_ip.ipv6_supported() ? runtime_constants.SOURCE_DNS_INBOUND_ADDRESS : "0.0.0.0",
listen_port: runtime_constants.SOURCE_DNS_INBOUND_PORT
});
}
for (let inbound in dns_config.inbounds)
push(inbounds, inbound);
let default_outbounds = [
{ type: "direct", tag: runtime_constants.DIRECT_OUTBOUND_TAG },
{ type: "direct", tag: runtime_constants.BYPASS_OUTBOUND_TAG }
];
runtime_context.dns_health_inbounds = dns_config.sniff_inbounds;
runtime_context.default_domain_resolver = runtime_dns.default_domain_resolver(settings);
let dns_section = {
servers: dns_servers,
rules: dns_rules,
final: runtime_constants.DNS_SERVER_TAG,
strategy: option(settings, "dns_strategy", "prefer_ipv4")
};
let sb_version_val = detect_sing_box_version();
let is_sb_1_14_plus = is_sb_1_14_plus_detected(sb_version_val);
let is_sb_1_15_plus = is_sb_1_15_plus_detected(sb_version_val);
ctx.is_sb_1_14_plus = function() { return is_sb_1_14_plus; };
ctx.is_sb_1_15_plus = function() { return is_sb_1_15_plus; };
let use_legacy_rdrc = !is_sb_1_14_plus;
let route_section = runtime_route.config(settings, runtime_context);
if (is_sb_1_14_plus)
route_section.default_http_client = "ruleset-http";
if (is_sb_1_14_plus) {
dns_section.optimistic = {
enabled: true,
timeout: "3d"
};
}
let cache_file_section = {
enabled: true,
path: cache_path,
store_fakeip: true
};
if (use_legacy_rdrc)
cache_file_section.store_rdrc = true;
else
cache_file_section.store_dns = true;
if (is_sb_1_15_plus) {
cache_file_section.buffer_size = "1MB";
cache_file_section.flush_interval = "10m";
}
let base_cfg = {
log: {
disabled: false,
level: log_level,
timestamp: false
},
dns: dns_section,
ntp: {},
certificate: {},
endpoints: [],
inbounds,
outbounds: default_outbounds,
route: route_section,
services: [],
experimental: {
cache_file: cache_file_section,
clash_api: clash_api_config(settings, service_address)
},
__dns_hosts_predefined: dns_hosts_predefined
};
if (is_sb_1_14_plus)
base_cfg.http_clients = [{ tag: "ruleset-http" }];
return base_cfg;
}
function mixed_proxy_enabled_action(action) {
return action == "connection" || action == "proxy" || action == "outbound" || action == "vpn" ||
action == "awg" || action == "byedpi" || action == "zapret" || action == "zapret2" ||
action == "wdtt" || action == "olcrtc";
}
function add_mixed_proxy_for_section(config, section, service_address) {
if (!bool_option(section, "mixed_proxy_enabled", false))
return;
if (ctx.deferred_sections && ctx.deferred_sections[section[".name"]])
return;
let action = option(section, "action", "");
if (!mixed_proxy_enabled_action(action))
runtime_generate_unsupported("mixed proxy inbound is not supported for action " + action);
let listen_port_value = option(section, "mixed_proxy_port", "");
if (match(listen_port_value, /^[0-9]+$/) == null)
runtime_generate_unsupported("mixed proxy port is invalid");
let listen_port = int(listen_port_value, 10);
if (listen_port < 1 || listen_port > 65535)
runtime_generate_unsupported("mixed proxy port is invalid");
let listen = as_string(service_address || "");
if (listen == "")
runtime_generate_unsupported("mixed proxy listen address is not set");
let inbound = {
type: "mixed",
tag: runtime_constants.inbound_tag(section[".name"] + "-mixed"),
listen,
listen_port
};
if (bool_option(section, "mixed_proxy_auth_enabled", false)) {
let username = option(section, "mixed_proxy_username", "");
let password = option(section, "mixed_proxy_password", "");
if (username == "" || password == "")
runtime_generate_unsupported("mixed proxy authentication is enabled but username or password is empty");
inbound.users = [{ username, password }];
}
push(config.inbounds, inbound);
if (type(config.route) == "object" && type(config.route.rules) == "array" &&
length(config.route.rules) > 0 && config.route.rules[0].action == "sniff" &&
type(config.route.rules[0].inbound) == "array")
push(config.route.rules[0].inbound, inbound.tag);
push(config.route.rules, {
action: "route",
inbound: inbound.tag,
ip_is_private: true,
outbound: runtime_constants.DIRECT_OUTBOUND_TAG
});
let section_outbound = runtime_constants.outbound_tag(section[".name"]);
generator_routes.push_section_route_rule(config, {
action: "route",
inbound: inbound.tag,
outbound: section_outbound
}, section_outbound);
}
/*
* Direct Bypass provides an explicit, clean mixed (HTTP+SOCKS5) proxy port on LAN
* mapped directly to the direct outbound marked with OUTBOUND_MARK (0x08000000).
* Traffic received on this port bypasses all section routing rules, VPN tunnels, and
* DPI evasion engines, giving clients a guaranteed straight WAN route.
*/
function add_direct_bypass_proxy(config, settings, service_address) {
if (!bool_option(settings, "direct_bypass_enabled", false))
return;
let listen = as_string(service_address || "");
if (listen == "")
runtime_generate_unsupported("direct bypass listen address is not set");
let port_value = option(settings, "direct_bypass_port", as_string(runtime_constants.DIRECT_BYPASS_DEFAULT_PORT));
if (match(port_value, /^[0-9]+$/) == null)
runtime_generate_unsupported("direct bypass port is invalid");
let listen_port = int(port_value, 10);
if (listen_port < 1 || listen_port > 65535)
runtime_generate_unsupported("direct bypass port is invalid");
let inbound_tag = runtime_constants.DIRECT_BYPASS_INBOUND_TAG;
push(config.inbounds, {
type: "mixed",
tag: inbound_tag,
listen,
listen_port
});
push(config.outbounds, {
type: "direct",
tag: runtime_constants.DIRECT_BYPASS_OUTBOUND_TAG,
routing_mark: runtime_constants.OUTBOUND_MARK
});
if (type(config.route) == "object" && type(config.route.rules) == "array" &&
length(config.route.rules) > 0 && config.route.rules[0].action == "sniff" &&
type(config.route.rules[0].inbound) == "array")
push(config.route.rules[0].inbound, inbound_tag);
push(config.route.rules, {
action: "route",
inbound: inbound_tag,
ip_is_private: true,
outbound: runtime_constants.DIRECT_OUTBOUND_TAG
});
push(config.route.rules, {
action: "route",
inbound: inbound_tag,
outbound: runtime_constants.DIRECT_BYPASS_OUTBOUND_TAG
});
}
function add_service_mixed_proxy_inbound(config, tag_name, listen_port, outbound) {
push(config.inbounds, {
type: "mixed",
tag: tag_name,
listen: runtime_constants.SERVICE_MIXED_INBOUND_ADDRESS,
listen_port
});
if (type(config.route) == "object" && type(config.route.rules) == "array" &&
length(config.route.rules) > 0 && config.route.rules[0].action == "sniff" &&
type(config.route.rules[0].inbound) == "array")
push(config.route.rules[0].inbound, tag_name);
push(config.route.rules, {
action: "route",
inbound: tag_name,
ip_is_private: true,
outbound: runtime_constants.DIRECT_OUTBOUND_TAG
});
generator_routes.push_section_route_rule(config, {
action: "route",
inbound: tag_name,
outbound
}, outbound);
}
function service_mixed_proxy_inbound_tag_for_purpose(purpose) {
return as_string(purpose || "lists") == "components"
? runtime_constants.inbound_tag("service-components")
: runtime_constants.SERVICE_MIXED_INBOUND_TAG;
}
function service_mixed_proxy_port_for_purpose(purpose) {
return runtime_constants.SERVICE_MIXED_INBOUND_PORT +
(as_string(purpose || "lists") == "components" ? 1 : 0);
}
function add_global_download_service_mixed_proxy(config, settings, purpose) {
let outbound = download_detour_tag(settings, purpose);
if (outbound == "")
return;
add_service_mixed_proxy_inbound(
config,
service_mixed_proxy_inbound_tag_for_purpose(purpose),
service_mixed_proxy_port_for_purpose(purpose),
outbound
);
}
function add_subscription_download_service_mixed_proxies(config, sections) {
for (let target in connections.subscription_download_targets(sections)) {
if (ctx.deferred_sections && ctx.deferred_sections[target])
continue;
let port = connections.subscription_download_target_port(sections, target, runtime_constants.SERVICE_MIXED_INBOUND_PORT);
if (port <= 0)
runtime_generate_unsupported("subscription download proxy port could not be resolved");
add_service_mixed_proxy_inbound(
config,
runtime_constants.inbound_tag("service-subscription-" + target),
port,
outbound_tag(target)
);
}
}
function add_service_mixed_proxy(config, settings, sections) {
if (!download_via_proxy_any_enabled(settings, sections))
return;
add_global_download_service_mixed_proxy(config, settings, "lists");
add_global_download_service_mixed_proxy(config, settings, "components");
add_subscription_download_service_mixed_proxies(config, sections);
if (download_via_proxy_enabled(settings, "lists") && download_detour_tag(settings, "lists") == "")
runtime_generate_unsupported("download lists via proxy section is not set");
if (download_via_proxy_enabled(settings, "components") && download_detour_tag(settings, "components") == "")
runtime_generate_unsupported("download components via proxy section is not set");
}
function router_traffic_section(settings) {
if (!bool_option(settings, "route_router_traffic", false))
return "";
return option(settings, "route_router_traffic_section", "");
}
function add_router_traffic_redirect(config, settings) {
let section_name = router_traffic_section(settings);
if (section_name == "")
return;
push(config.inbounds, {
type: "redirect",
tag: runtime_constants.REDIRECT_INBOUND_TAG,
listen: runtime_constants.REDIRECT_INBOUND_ADDRESS,
listen_port: runtime_constants.REDIRECT_INBOUND_PORT
});
let sec_out = outbound_tag(section_name);
if (sec_out != "") {
let target_idx = 0;
if (type(config.route) == "object" && type(config.route.rules) == "array") {
for (let i = 0; i < length(config.route.rules); i++) {
let r = config.route.rules[i];
if (r.action == "sniff" || r.action == "hijack-dns")
target_idx = i + 1;
}
splice(config.route.rules, target_idx, 0, {
action: "route",
inbound: [ runtime_constants.REDIRECT_INBOUND_TAG ],
outbound: sec_out
});
}
}
}
// ─── Content blocking (parental control domains) ─────────────────────────────
// Each enabled `config schedule` with blocked_domains generates:
// 1. DNS rules (on the dedicated dns-block-in inbound) that reject the
// blocked domains for the schedule's devices. IP-addressed devices get
// source_ip_cidr scoping; MAC-only devices are covered by the nftables
// DNS redirect (the DNS packets only reach dns-block-in when the
// nftables redirect rule fires, which already matches by MAC+time).
// 2. Route rules with action reject as a fallback when DNS cannot be
// redirected (always-on schedules only; time-gated schedules rely on
// the nftables redirect so their route rules would leak outside the
// active window).
// mode=allow (whitelist) inverts the DNS match: everything except the
// listed domains is rejected for the device.
function schedule_list_value(schedule, key) {
let values = list_option(schedule, key);
if (length(values) > 0)
return values;
let single = option(schedule, key, "");
return single == "" ? [] : [ single ];
}
function schedule_has_time_window(schedule) {
return trim(option(schedule, "start_time", "")) != "" ||
trim(option(schedule, "end_time", "")) != "";
}
function resolve_mac_to_ips(mac) {
return core_ip.resolve_mac_to_ips(mac);
}
function profile_source_ip_cidrs(profile) {
let result = [];
for (let raw in schedule_list_value(profile, "device_ip")) {
let device = trim(as_string(raw));
if (device == "")
continue;
let is_mac = match(device, /^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/) != null;
if (is_mac) {
for (let ip in resolve_mac_to_ips(device)) {
let cidr = core_ip.valid_ipv6(ip) ? ip + "/128" : ip + "/32";
if (index(result, cidr) < 0)
push(result, cidr);
}
continue;
}
if (core_ip.valid_ip(device)) {
let cidr = core_ip.valid_ipv6(device) ? device + "/128" : device + "/32";
if (index(result, cidr) < 0) push(result, cidr);
} else if (core_ip.valid_ip_cidr(device)) {
if (index(result, device) < 0) push(result, device);
}
}
return result;
}
function schedule_source_ip_cidrs(schedule) {
let result = [];
for (let raw in schedule_list_value(schedule, "device_ip")) {
let device = trim(as_string(raw));
if (device == "")
continue;
let is_mac = match(device, /^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/) != null;
if (is_mac) {
for (let ip in resolve_mac_to_ips(device)) {
let cidr = core_ip.valid_ipv6(ip) ? ip + "/128" : ip + "/32";
if (index(result, cidr) < 0)
push(result, cidr);
}
continue;
}
if (core_ip.valid_ip(device)) {
let cidr = core_ip.valid_ipv6(device) ? device + "/128" : device + "/32";
if (index(result, cidr) < 0) push(result, cidr);
} else if (core_ip.valid_ip_cidr(device)) {
if (index(result, device) < 0) push(result, device);
}
}
for (let p_name in schedule_list_value(schedule, "profile")) {
let prof = ctx.uci_cursor().get_all(CONFIG_NAME, p_name);
if (prof != null && section_enabled(prof)) {
for (let cidr in profile_source_ip_cidrs(prof)) {
if (index(result, cidr) < 0)
push(result, cidr);
}
}
}
return result;
}
function schedule_domain_matcher_rule(rule, schedule) {
let raw = schedule_list_value(schedule, "blocked_domains");
let plain = [];
let keyword = [];
let regex = [];
let full = [];
for (let domain in raw) {
let value = trim(as_string(domain));
if (value == "")
continue;
if (substr(value, 0, 6) == "full:")
push(full, trim(substr(value, 6)));
else if (substr(value, 0, 8) == "keyword:")
push(keyword, trim(substr(value, 8)));
else if (substr(value, 0, 6) == "regex:")
push(regex, trim(substr(value, 6)));
else if (substr(value, 0, 1) == ".")
push(plain, substr(value, 1));
else if (index(value, "*") >= 0)
push(plain, replace(value, "*.", ""));
else
push(plain, value);
}
if (length(full) > 0)
rule.domain = full;
if (length(plain) > 0)
rule.domain_suffix = plain;
if (length(keyword) > 0)
rule.domain_keyword = keyword;
if (length(regex) > 0)
rule.domain_regex = regex;
return rule;
}
function schedule_has_mac_devices(schedule) {
for (let raw in schedule_list_value(schedule, "device_ip")) {
let device = trim(as_string(raw));
if (match(device, /^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/) != null)
return true;
}
for (let p_name in schedule_list_value(schedule, "profile")) {
let prof = ctx.uci_cursor().get_all(CONFIG_NAME, p_name);
if (prof != null && section_enabled(prof)) {
if (schedule_has_mac_devices(prof))
return true;
}
}
return false;
}
function schedule_blocked_domains(schedule) {
let result = [];
for (let domain in schedule_list_value(schedule, "blocked_domains")) {
let value = trim(as_string(domain));
if (value != "")
push(result, value);
}
return result;
}
function enabled_content_block_schedules() {
let result = [];
ctx.uci_cursor().foreach(CONFIG_NAME, "schedule", function(schedule) {
if (section_enabled(schedule) && length(schedule_blocked_domains(schedule)) > 0) {
push(result, schedule);
}
});
return result;
}
function enabled_content_block_profiles() {
let result = [];
ctx.uci_cursor().foreach(CONFIG_NAME, "profile", function(profile) {
if (section_enabled(profile) && length(schedule_blocked_domains(profile)) > 0)
push(result, profile);
});
return result;
}
function enabled_safesearch_profiles() {
let result = [];
ctx.uci_cursor().foreach(CONFIG_NAME, "profile", function(profile) {
if (section_enabled(profile) && bool_option(profile, "safe_search", false))
push(result, profile);
});
return result;
}
function add_safesearch_dns_rules(target, profiles) {
if (length(profiles) == 0)
return;
let rules_arr = (type(target) == "object" && type(target.dns) == "object" && type(target.dns.rules) == "array")
? target.dns.rules
: target;
let all_sources = [];
for (let profile in profiles) {
for (let cidr in profile_source_ip_cidrs(profile)) {
if (index(all_sources, cidr) < 0)
push(all_sources, cidr);
}
}
let search_engines = [
{
domains: [ "google.com", "www.google.com", "google.ru", "www.google.ru" ],
ip: "216.239.38.120"
},
{
domains: [ "yandex.ru", "www.yandex.ru", "ya.ru", "www.ya.ru", "yandex.com", "www.yandex.com" ],
ip: "213.180.193.56"
},
{
domains: [ "bing.com", "www.bing.com" ],
ip: "204.79.197.220"
},
{
domains: [ "duckduckgo.com", "www.duckduckgo.com", "safe.duckduckgo.com" ],
ip: "52.142.124.215"
},
{
domains: [ "youtube.com", "www.youtube.com", "m.youtube.com", "youtubei.googleapis.com" ],
ip: "216.239.38.120"
}
];
for (let engine in search_engines) {
for (let domain in engine.domains) {
let rule = {
action: "predefined",
domain: [ domain ],
answer: [ domain + ". 60 IN A " + engine.ip ]
};
if (length(all_sources) > 0)
rule.source_ip_cidr = all_sources;
push(rules_arr, rule);
}
}
}
function add_content_block_dns_inbound(config) {
push(config.inbounds, {
type: "direct",
tag: runtime_constants.DNS_BLOCK_INBOUND_TAG,
listen: core_ip.ipv6_supported() ? runtime_constants.DNS_BLOCK_INBOUND_ADDRESS : "0.0.0.0",
listen_port: runtime_constants.DNS_BLOCK_INBOUND_PORT
});
}
function add_content_block_dns_rules(target, schedules) {
let rules_arr = (type(target) == "object" && type(target.dns) == "object" && type(target.dns.rules) == "array")
? target.dns.rules
: target;
let added = false;
for (let schedule in schedules) {
let mode = option(schedule, "mode", "block");
let sources = schedule_source_ip_cidrs(schedule);
let has_mac = schedule_has_mac_devices(schedule);
for (let raw_domain in schedule_blocked_domains(schedule)) {
let domain_value = trim(as_string(raw_domain));
if (domain_value == "")
continue;
let rule = {
action: "reject",
inbound: [ runtime_constants.DNS_BLOCK_INBOUND_TAG ]
};
let matchers = {};
schedule_domain_matcher_rule(matchers, { blocked_domains: [ domain_value ] });
for (let key in [ "domain", "domain_suffix", "domain_keyword", "domain_regex" ]) {
if (matchers[key] != null)
rule[key] = matchers[key];
}
if (length(sources) > 0 && !has_mac)
rule.source_ip_cidr = sources;
if (mode == "allow")
rule.invert = true;
push(rules_arr, rule);
added = true;
}
}
return added;
}
function add_content_block_route_rules(target, schedules) {
let rules_arr = (type(target) == "object" && type(target.route) == "object" && type(target.route.rules) == "array")
? target.route.rules
: target;
for (let schedule in schedules) {
if (schedule_has_time_window(schedule))
continue;
let mode = option(schedule, "mode", "block");
let sources = schedule_source_ip_cidrs(schedule);
if (length(sources) == 0)
continue;
let rule = {
action: "reject",
inbound: tproxy_inbound_matcher()
};
schedule_domain_matcher_rule(rule, schedule);
rule.source_ip_cidr = sources;
if (mode == "allow")
rule.invert = true;
push(rules_arr, rule);
}
}
function add_content_blocking(config) {
let schedules = enabled_content_block_schedules();
let profiles = enabled_content_block_profiles();
let safesearch_profiles = enabled_safesearch_profiles();
if (length(schedules) == 0 && length(profiles) == 0 && length(safesearch_profiles) == 0)
return;
add_content_block_dns_inbound(config);
unshift(config.route.rules, {
action: "hijack-dns",
inbound: [ runtime_constants.DNS_BLOCK_INBOUND_TAG ]
});
let new_dns_rules = [];
add_content_block_dns_rules(new_dns_rules, schedules);
add_content_block_dns_rules(new_dns_rules, profiles);
add_safesearch_dns_rules(new_dns_rules, safesearch_profiles);
// Place content blocking DNS rules at the front of config.dns.rules so
// queries on dns-block-in are rejected before any section FakeIP rule matches!
for (let i = length(new_dns_rules) - 1; i >= 0; i--)
unshift(config.dns.rules, new_dns_rules[i]);
let new_route_rules = [];
add_content_block_route_rules(new_route_rules, schedules);
add_content_block_route_rules(new_route_rules, profiles);
// Insert content blocking route rules right after hijack-dns (index 1)
// so sniffed domains from blocked clients are rejected before any section proxy route matches!
for (let i = length(new_route_rules) - 1; i >= 0; i--)
splice(config.route.rules, 1, 0, new_route_rules[i]);
}
function single_or_array(values) {
if (type(values) != "array")
return values;
if (length(values) == 1)
return values[0];
return values;
}
function get_excluded_client_cidrs(settings) {
let result = [];
let seen = {};
let add_cidr = function(ip) {
ip = trim(as_string(ip));
if (ip == "") return;
let is_mac = match(ip, /^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/) != null;
if (is_mac) {
for (let res_ip in resolve_mac_to_ips(ip)) {
let cidr = core_ip.valid_ipv6(res_ip) ? res_ip + "/128" : res_ip + "/32";
if (!seen[cidr]) {
seen[cidr] = true;
push(result, cidr);
}
}
return;
}
if (core_ip.valid_ip(ip)) {
let cidr = core_ip.valid_ipv6(ip) ? ip + "/128" : ip + "/32";
if (!seen[cidr]) {
seen[cidr] = true;
push(result, cidr);
}
} else if (core_ip.valid_ip_cidr(ip)) {
if (!seen[ip]) {
seen[ip] = true;
push(result, ip);
}
}
};
if (settings) {
for (let item in list_option(settings, "excluded_clients"))
add_cidr(item);
for (let item in list_option(settings, "excluded_ips"))
add_cidr(item);
}
return result;
}
function source_aware_dns_sources(sections, settings) {
let sources = [];
let seen = {};
let add_source = function(value) {
value = as_string(value);
if (value == "" || seen[value])
return;
seen[value] = true;
push(sources, value);
};
if (settings) {
for (let cidr in get_excluded_client_cidrs(settings))
add_source(cidr);
}
for (let section in sections) {
let action = option(section, "action", "");
let has_dns_matchers = connections.has_dns_matchers(section);
if (has_dns_matchers) {
for (let ip in core_ip.normalize_to_cidrs(list_option(section, "source_ip_cidr")))
add_source(ip);
}
if (action == "bypass") {
for (let ip in core_ip.normalize_to_cidrs(list_option(section, "fully_routed_ips")))
add_source(ip);
for (let ip in core_ip.normalize_to_cidrs(list_option(section, "source_ip_cidr")))
add_source(ip);
}
if (action == "dns") {
for (let ip in core_ip.normalize_to_cidrs(list_option(section, "fully_routed_ips")))
add_source(ip);
for (let ip in core_ip.normalize_to_cidrs(list_option(section, "source_ip_cidr")))
add_source(ip);
}
}
return sources;
}
function add_source_aware_dns_support(config, source_aware_dns) {
if (length(source_aware_dns) == 0)
return;
push(config.dns.servers, {
type: "udp",
tag: runtime_constants.DNSMASQ_DNS_SERVER_TAG,
server: "127.0.0.1",
server_port: 53
});
}
function add_source_aware_dns_fallback(config, source_aware_dns) {
if (length(source_aware_dns) == 0)
return;
let settings = runtime_settings_cache || {};
let rewrite_ttl = int_option(settings, "dns_rewrite_ttl", "60");
let rule = {
action: "route",
server: runtime_constants.DNSMASQ_DNS_SERVER_TAG,
inbound: [ runtime_constants.SOURCE_DNS_INBOUND_TAG ],
source_ip_cidr: single_or_array(source_aware_dns),
rewrite_ttl
};
push(config.dns.rules, rule);
}
function add_excluded_clients_dns_rule(config, settings) {
let excluded_cidrs = get_excluded_client_cidrs(settings);
if (length(excluded_cidrs) == 0)
return;
let rewrite_ttl = int_option(settings, "dns_rewrite_ttl", "60");
unshift(config.dns.rules, {
action: "route",
server: runtime_constants.DNSMASQ_DNS_SERVER_TAG,
inbound: [ runtime_constants.SOURCE_DNS_INBOUND_TAG ],
source_ip_cidr: single_or_array(excluded_cidrs),
rewrite_ttl
});
}
function add_excluded_clients_route_rule(config, settings) {
let excluded_cidrs = get_excluded_client_cidrs(settings);
if (length(excluded_cidrs) == 0)
return;
splice(config.route.rules, 1, 0, {
action: "route",
outbound: runtime_constants.BYPASS_OUTBOUND_TAG,
source_ip_cidr: single_or_array(excluded_cidrs)
});
}
function generate_config(output_path, service_address, mwan3_active, supports_xhttp, deferred_sections) {
ctx.deferred_sections = parse_deferred_sections(deferred_sections);
ctx.runtime_ruleset_folder = runtime_ruleset_folder;
runtime_supports_xhttp = supports_xhttp == null || as_string(supports_xhttp) == ""
? true
: cli_bool(supports_xhttp);
ctx.runtime_supports_xhttp = runtime_supports_xhttp;
let cursor = uci_cursor();
cursor.load(CONFIG_NAME);
runtime_settings_cache = object_or_empty(cursor.get_all(CONFIG_NAME, "settings"));
let settings = runtime_settings_cache;
let sections = enabled_sections();
let servers = enabled_servers();
if (length(sections) == 0 && length(servers) == 0)
runtime_generate_unsupported("no enabled sections");
let source_aware_dns = source_aware_dns_sources(sections, settings);
let config = base_config(settings, service_address, {
mwan3_active: cli_bool(mwan3_active),
source_aware_dns: length(source_aware_dns) > 0
});
add_source_aware_dns_support(config, source_aware_dns);
let taken = reserved_runtime_tag_set(config.outbounds);
reserve_section_outbound_tags(sections, taken);
for (let server in servers)
runtime_servers.add_server(config, server);
for (let section in sections)
add_outbound_for_section(config, section, taken, sections);
add_service_route_rules(config, sections);
// Add block and bypass sections before proxy sections so exceptions take precedence
for (let section in sections) {
let act = option(section, "action", "");
if (act == "block" || act == "bypass")
add_route_for_section(config, section);
}
for (let section in sections) {
let act = option(section, "action", "");
if (act != "block" && act != "bypass")
add_route_for_section(config, section);
}
add_server_routes(config, servers, sections);
add_source_aware_dns_fallback(config, source_aware_dns);
add_excluded_clients_dns_rule(config, settings);
add_excluded_clients_route_rule(config, settings);
// Append dns_hosts predefined rules AFTER section DNS rules so that
// FakeIP/section-level DNS routing takes precedence over hardcoded IPs
// from dns_hosts or hosts cache. This prevents third-party hosts entries
// (e.g. from "Unlock AI" projects) from short-circuiting FakeIP for
// domains that are already routed through proxy via section rules.
if (type(config.__dns_hosts_predefined) == "array") {
for (let rule in config.__dns_hosts_predefined)
push(config.dns.rules, rule);
}
add_direct_bypass_proxy(config, settings, service_address);
add_service_mixed_proxy(config, settings, sections);
for (let section in sections)
add_mixed_proxy_for_section(config, section, service_address);
add_router_traffic_redirect(config, settings);
add_content_blocking(config);
if (type(config.http_clients) == "array" && length(config.http_clients) > 0) {
let detour = download_detour_tag(settings, "lists");
if (detour != "" && generator_routes.is_valid_detour(config, detour))
config.http_clients[0].detour = detour;
}
assert_unique_outbound_tags(config);
strip_internal_fields(config);
if (!atomic_write_json_file(output_path, config)) {
warn("failed to write ", output_path, "\n");
exit(1);
}
}
function generate_config_fixture(fixture_path, output_path, service_address, mwan3_active, supports_xhttp, deferred_sections) {
use_fixture_cursor(fixture_path);
runtime_subscription.set_section_cache_dir(output_path + ".section-cache");
runtime_ruleset_folder = output_path + ".rulesets";
generate_config(output_path, service_address, mwan3_active, supports_xhttp, deferred_sections);
}
function stdin_length() {
let value = read_stdin_json();
if (type(value) == "array" || type(value) == "object")
print(length(value), "\n");
else
print("0\n");
}
function stdin_contains(needle) {
return index(read_stdin(), as_string(needle)) >= 0;
}
function stdin_regex_matches(pattern) {
pattern = as_string(pattern);
if (pattern == "")
return false;
try {
return match(read_stdin(), regexp(pattern)) != null;
}
catch (e) {
return false;
}
}
function ip_addr_first_inet4() {
for (let line in split(read_stdin(), "\n")) {
let fields = split(trim(as_string(line)), /[ \t]+/);
if (length(fields) < 2 || fields[0] != "inet")
continue;
let slash = index(fields[1], "/");
print(slash >= 0 ? substr(fields[1], 0, slash) : fields[1], "\n");
return;
}
}
function stdin_first_dns_a_address() {
for (let line in split(read_stdin(), "\n")) {
line = as_string(line);
if (match(line, /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) != null) {
print(line, "\n");
return;
}
}
}
function stdin_first_dns_aaaa_address() {
for (let line in split(read_stdin(), "\n")) {
line = as_string(line);
if (match(line, /^[0-9A-Fa-f:]+$/) != null) {
print(line, "\n");
return;
}
}
}
function stdin_first_nslookup_address() {
for (let line in split(read_stdin(), "\n")) {
line = as_string(line);
if (match(line, /^Address[ \t]*[0-9]*:[ \t]*[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) == null &&
match(line, /^Address[ \t]*[0-9]*:[ \t]*[0-9A-Fa-f:]+$/) == null)
continue;
let fields = split(trim(line), /[ \t]+/);
if (length(fields) > 0)
print(fields[length(fields) - 1], "\n");
return;
}
}
function stdin_first_field() {
let data = read_stdin();
let newline = index(data, "\n");
let line = newline >= 0 ? substr(data, 0, newline) : data;
let fields = split(trim(as_string(line)), /[ \t\r\n]+/);
if (length(fields) > 0 && fields[0] != "")
print(fields[0], "\n");
}
function array_append_string(value) {
let result = array_or_empty(read_stdin_json());
push(result, as_string(value));
write_json(result);
}
function normalized_country_list() {
write_json(runtime_urltest.normalized_country_list(read_stdin_json()));
}
function urltest_filter(mode, tags_path, names_path, countries_path, names_filter_path, regex_tags_path, countries_filter_path) {
write_json(runtime_urltest.filter_array(
mode,
read_json_file(tags_path),
read_json_file(names_path),
read_json_file(countries_path),
read_json_file(names_filter_path),
read_json_file(regex_tags_path),
read_json_file(countries_filter_path)
));
}
function object_nonempty_stdin() {
let value = read_stdin_json();
return (type(value) == "array" || type(value) == "object") && length(value) > 0;
}
ctx.uci_cursor = uci_cursor;
ctx.runtime_settings = runtime_settings;
ctx.runtime_generate_unsupported = runtime_generate_unsupported;
ctx.uci_bin_to_hex = uci_bin_to_hex;
ctx.download_detour_tag = download_detour_tag;
ctx.atomic_write_json_file = atomic_write_json_file;
ctx.is_sb_1_14_plus = is_sb_1_14_plus_detected;
ctx.is_sb_1_15_plus = is_sb_1_15_plus_detected;
generator_outbounds.init(ctx);
generator_routes.init(ctx);
reserved_runtime_tag_set = generator_outbounds.reserved_runtime_tag_set;
assert_unique_outbound_tags = generator_outbounds.assert_unique_outbound_tags;
enabled_sections = generator_routes.enabled_sections;
enabled_servers = generator_routes.enabled_servers;
reserve_section_outbound_tags = generator_routes.reserve_section_outbound_tags;
add_outbound_for_section = generator_routes.add_outbound_for_section;
add_service_route_rules = generator_routes.add_service_route_rules;
add_route_for_section = generator_routes.add_route_for_section;
add_server_routes = generator_routes.add_server_routes;
ensure_custom_ruleset = generator_routes.ensure_custom_ruleset;
let mode = ARGV[0] || "";
if (mode == "generate-config")
generate_config(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5]);
else if (mode == "generate-config-fixture")
generate_config_fixture(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6]);
else if (mode == "stdin-length")
stdin_length();
else if (mode == "stdin-contains")
exit(stdin_contains(ARGV[1]) ? 0 : 1);
else if (mode == "stdin-regex-matches")
exit(stdin_regex_matches(ARGV[1]) ? 0 : 1);
else if (mode == "csv-to-json-array")
csv_to_json_array(ARGV[1]);
else if (mode == "ip-addr-first-inet4")
ip_addr_first_inet4();
else if (mode == "stdin-first-dns-a-address")
stdin_first_dns_a_address();
else if (mode == "stdin-first-dns-aaaa-address")
stdin_first_dns_aaaa_address();
else if (mode == "stdin-first-nslookup-address")
stdin_first_nslookup_address();
else if (mode == "stdin-first-field")
stdin_first_field();
else if (mode == "array-append-string")
array_append_string(ARGV[1]);
else if (mode == "normalized-country-list")
normalized_country_list();
else if (mode == "urltest-filter")
urltest_filter(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6], ARGV[7]);
else if (mode == "object-nonempty")
exit(object_nonempty_stdin() ? 0 : 1);
else {
warn("Usage: singbox/generator.uc <operation> ...\n");
exit(1);
}