- 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
915 lines
32 KiB
Ucode
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);
|
|
}
|