#!/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 '' strings as binary, which when // serialized back to JSON appear as '' — invalid for sing-box. function uci_bin_to_hex(val) { if (val == null || val == "") return ""; let s = as_string(val); 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 ]; } function is_sb_1_14_plus_detected(sb_version_val) { if (sb_version_val == null || sb_version_val == "") { let sb_version_file = getenv("SB_VERSION_STATE_FILE") || "/etc/tachyon/sing-box-version"; 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) || ""); } if (sb_version_val == "") { try { let pipe = fs.popen("sing-box version 2>/dev/null", "r"); if (pipe) { let out = pipe.read("all"); pipe.close(); let m = match(out, /sing-box version ([^\s]+)/); if (m) sb_version_val = m[1]; } } catch (e) {} } } if (sb_version_val != "") return match(sb_version_val, /^v?1\.(1[4-9]|[2-9][0-9])\./) != null; 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", turbo_cache ? "/etc/sing-box/cache.db" : "/tmp/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" }, { 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_file = getenv("SB_VERSION_STATE_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) || ""); } if (sb_version_val == "") { try { let pipe = fs.popen("sing-box version 2>/dev/null", "r"); if (pipe) { let out = pipe.read("all"); pipe.close(); let m = match(out, /sing-box version ([^\s]+)/); if (m) sb_version_val = m[1]; } } catch (e) {} } let sb_variant_file = getenv("SB_VARIANT_STATE_FILE") || "/etc/tachyon/sing-box-variant"; let sb_variant_val = trim(fs.readfile(sb_variant_file) || ""); let is_extended_variant = sb_variant_val == "extended" || sb_variant_val == "extended-compressed" || index(sb_version_val, "extended") >= 0; let use_legacy_rdrc = is_extended_variant || (sb_version_val != "" ? (match(sb_version_val, /^v?1\.1[0-3]\./) != null) : false); let is_sb_1_14_plus = is_sb_1_14_plus_detected(sb_version_val); let route_section = runtime_route.config(settings, runtime_context); if (is_sb_1_14_plus) route_section.default_http_client = "ruleset-http"; 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; 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: "hijack-dns", inbound: inbound.tag, port: 53 }); push(config.route.rules, { action: "hijack-dns", inbound: inbound.tag, protocol: "dns" }); 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: "hijack-dns", inbound: inbound_tag, port: 53 }); push(config.route.rules, { action: "hijack-dns", inbound: inbound_tag, protocol: "dns" }); 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: "hijack-dns", inbound: tag_name, port: 53 }); push(config.route.rules, { action: "hijack-dns", inbound: tag_name, protocol: "dns" }); 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"); } // ─── 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_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; 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 ...\n"); exit(1); }