Files
NeoTachyon/tachyon/files/usr/lib/singbox/generator.uc
T
Dushnilin a21a818ea5 fix: dns_hosts predefined rules now fall after FakeIP/section DNS rules; fix parse_text_domains for // comments
- dns_hosts and hosts cache predefined DNS rules are now appended AFTER
  section-level FakeIP routing rules, preventing third-party hosts entries
  (e.g. from Unlock AI projects) from short-circuiting FakeIP for domains
  already routed through proxy via section rules
- parse_text_domains now properly handles // comments and multi-domain
  comma-separated lines from option domain
2026-08-05 10:37:21 +03:00

915 lines
32 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;
let HOSTS_CACHE_FILE = "/etc/tachyon/hosts-lists/combined.txt";
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";
// Convert a UCI value that may be a binary buffer (<b 0x...>) to a plain hex string.
// 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]+$/)) {
if (length(s) % 2 != 0) s = "0" + s;
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) {
let stamp = clock();
let tmp_path = sprintf("%s.%d.%d.tmp", path, stamp[0], stamp[1]);
if (!ensure_parent_dir(path))
return false;
if (!write_json_file(tmp_path, value))
return false;
if (!fs.rename(tmp_path, path)) {
fs.unlink(tmp_path);
return false;
}
return true;
}
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 section_option = download_via_proxy_section_option_for_purpose(purpose);
let configured = section_option != "" ? option(settings, section_option, "") : "";
if (configured != "")
return configured;
return option(settings, "download_lists_via_proxy_section", "");
}
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 = "127.0.0.1";
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() {
return [ runtime_constants.TPROXY_INBOUND_TAG, runtime_constants.TPROXY_INBOUND6_TAG ];
}
function base_config(settings, service_address, 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", turbo_cache ? "/etc/sing-box/cache.db" : "/tmp/sing-box/cache.db");
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]
});
}
}
}
// Read remote hosts lists cache file
let hosts_cache_st = fs.stat(HOSTS_CACHE_FILE);
if (hosts_cache_st != null && int(hosts_cache_st.size) > 0) {
let fh = null;
try {
fh = fs.open(HOSTS_CACHE_FILE, "r");
if (fh != null) {
let line;
while ((line = fh.read("line")) != null) {
line = trim(line);
if (line == "" || substr(line, 0, 1) == "#")
continue;
let parts = split(line, /[ \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) != "#" && index(d, ":") == -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;
if (index(domain, ":") == -1) {
let rr_type = index(ip, ":") != -1 ? "AAAA" : "A";
push(dns_hosts_predefined, {
action: "predefined",
domain: [domain],
answer: [domain + ". 60 IN " + rr_type + " " + ip]
});
}
}
}
}
}
} catch(e) {
warn("Error reading hosts cache: ", as_string(e), "\n");
}
if (fh) fh.close();
}
for (let rule in dns_config.rules)
push(dns_rules, rule);
for (let rule in [
{ action: "reject", query_type: "HTTPS" },
{ 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);
push(dns_servers, {
type: "fakeip",
tag: runtime_constants.FAKEIP_DNS_SERVER_TAG,
inet4_range: runtime_constants.FAKEIP_INET4_RANGE,
inet6_range: runtime_constants.FAKEIP_INET6_RANGE
});
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 },
{ 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 },
{ type: "direct", tag: runtime_constants.DNS_INBOUND_TAG, listen: runtime_constants.DNS_INBOUND_ADDRESS, listen_port: runtime_constants.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 = object_or_empty(runtime_context);
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"),
independent_cache: true
};
return {
log: {
disabled: false,
level: log_level,
timestamp: false
},
dns: dns_section,
ntp: {},
certificate: {},
endpoints: [],
inbounds,
outbounds: default_outbounds,
route: runtime_route.config(settings, runtime_context),
services: [],
experimental: {
cache_file: {
enabled: true,
path: cache_path,
store_fakeip: true
},
clash_api: clash_api_config(settings, service_address)
},
__dns_hosts_predefined: dns_hosts_predefined
};
}
function mixed_proxy_enabled_action(action) {
return action == "connection" || action == "proxy" || action == "outbound" || action == "vpn" ||
action == "awg" || action == "byedpi" || action == "zapret" || action == "zapret2";
}
function add_mixed_proxy_for_section(config, section, service_address) {
if (!bool_option(section, "mixed_proxy_enabled", false))
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);
push(config.route.rules, {
action: "route",
inbound: inbound.tag,
outbound: runtime_constants.outbound_tag(section[".name"])
});
}
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
});
push(config.route.rules, {
action: "route",
inbound: tag_name,
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)) {
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 generate_config(output_path, service_address, mwan3_active, supports_xhttp) {
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 config = base_config(settings, service_address, { mwan3_active: cli_bool(mwan3_active) });
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);
for (let section in sections)
add_route_for_section(config, section);
add_server_routes(config, servers, sections);
// 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_service_mixed_proxy(config, settings, sections);
for (let section in sections)
add_mixed_proxy_for_section(config, section, service_address);
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) {
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);
}
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;
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]);
else if (mode == "generate-config-fixture")
generate_config_fixture(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5]);
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);
}