#!/usr/bin/env ucode let fs = require("fs"); let common = require("core.common"); let uci_core = require("core.uci"); let as_string = common.as_string; let bool_value = common.bool_value; let write_json = common.write_json; let shell_quote = common.shell_quote; let command_from_args = common.command_from_args; let command_output = common.command_output; let command_output_from_args = common.command_output_from_args; let command_status = common.command_status; let command_success = common.command_success; let command_success_from_args = common.command_success_from_args; let object_or_empty = common.object_or_empty; let array_or_empty = common.array_or_empty; let option = common.option; let bool_option = common.bool_option; let list_option = common.list_option; const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon"; const LIB_DIR = getenv("TACHYON_LIB") || "/usr/lib/tachyon"; const ADGUARD_BIN = getenv("ADGUARD_BIN") || "/usr/bin/AdGuardHome"; const ADGUARD_WORK_DIR = getenv("ADGUARD_WORK_DIR") || "/etc/adguard"; const ADGUARD_CONFIG = getenv("ADGUARD_CONFIG") || ADGUARD_WORK_DIR + "/AdGuardHome.yaml"; const ADGUARD_SERVICE_INIT = getenv("ADGUARD_SERVICE_INIT") || "/etc/init.d/adguard"; const ADGUARD_STATE_DIR = getenv("ADGUARD_STATE_DIR") || "/var/run/tachyon/adguard"; const ADGUARD_PID_FILE = getenv("ADGUARD_PID_FILE") || ADGUARD_STATE_DIR + "/adguard.pid"; const ADGUARD_DEFAULT_WEBUI_PORT = getenv("ADGUARD_DEFAULT_WEBUI_PORT") || "3000"; const ADGUARD_LOCAL_PORT = getenv("ADGUARD_LOCAL_PORT") || "5354"; const SB_DNS_INBOUND_ADDRESS = getenv("SB_DNS_INBOUND_ADDRESS") || "127.0.0.42"; const SB_DNS_INBOUND_PORT = getenv("SB_DNS_INBOUND_PORT") || "53"; const ADGUARD_DEFAULT_FILTER = getenv("ADGUARD_DEFAULT_FILTER") || "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt"; function log_message(message, level) { common.log_message(message, level); } function command_exists(name) { return command_success_from_args([ "command", "-v", name ]); } function uci_sections(type_name) { return uci_core.section_objects(CONFIG_NAME, as_string(type_name)); } function uci_settings() { return object_or_empty(uci_core.get_all(CONFIG_NAME, "settings")); } function provider_section() { for (let section in uci_sections("provider")) { if (as_string(section[".name"]) == "adguard") return section; } // Legacy/alternate: a dedicated top-level section named adguard. let direct = uci_core.get_all(CONFIG_NAME, "adguard"); if (type(direct) == "object" && length(keys(direct)) > 0) return direct; return {}; } function provider_enabled() { return bool_option(provider_section(), "enabled", false); } function dns_main_mode() { let mode = option(uci_settings(), "dns_main_mode", "dnsmasq"); return mode == "adguard" ? "adguard" : "dnsmasq"; } function failover_enabled() { return bool_option(uci_settings(), "dns_adguard_failover_enabled", false); } function webui_enabled() { return bool_option(provider_section(), "webui_enabled", true); } function webui_port() { let value = option(provider_section(), "webui_port", ADGUARD_DEFAULT_WEBUI_PORT); return match(value, /^[0-9]+$/) != null && int(value) >= 1 && int(value) <= 65535 ? int(value) : int(ADGUARD_DEFAULT_WEBUI_PORT); } function webui_username() { return trim(as_string(option(provider_section(), "webui_username", ""))); } function webui_password() { return as_string(option(provider_section(), "webui_password", "")); } function upstream_dns_list() { let result = []; for (let value in list_option(provider_section(), "upstream_dns")) { value = trim(as_string(value)); if (value != "") push(result, value); } return result; } function bootstrap_dns_list() { let result = []; for (let value in list_option(provider_section(), "bootstrap_dns")) { value = trim(as_string(value)); if (value != "") push(result, value); } if (length(result) == 0) { push(result, "1.1.1.1"); push(result, "8.8.8.8"); } return result; } function filter_list_urls() { let result = []; for (let value in list_option(provider_section(), "filter_lists")) { value = trim(as_string(value)); if (value != "") push(result, value); } if (length(result) == 0) push(result, ADGUARD_DEFAULT_FILTER); return result; } function lan_ipv4() { let addr = uci_core.get("network.lan.ipaddr"); if (addr != "" && match(addr, /^[0-9.]+$/) != null) return as_string(addr); // Fallback to the auto-detected Tachyon service listen address. if (fs.stat(LIB_DIR + "/singbox/runtime.uc") != null) { let detected = trim(command_output_from_args([ "ucode", "-L", LIB_DIR, LIB_DIR + "/singbox/runtime.uc", "service-listen-address" ])); if (detected != "" && match(detected, /^[0-9.]+$/) != null) return detected; } return "0.0.0.0"; } function dns_listen_host() { return dns_main_mode() == "adguard" ? lan_ipv4() : "127.0.0.1"; } function dns_listen_port() { return dns_main_mode() == "adguard" ? 53 : int(ADGUARD_LOCAL_PORT); } function provider_available() { let stat = fs.stat(ADGUARD_BIN); return stat != null && stat.mode != null && (int(stat.mode) & 73) != 0; } function first_nonempty_field(value) { for (let line in split(as_string(value), "\n")) { line = trim(as_string(line)); if (line == "") continue; let fields = split(line, /[ \t\r\n]+/); if (length(fields) > 0) return as_string(fields[length(fields) - 1]); } return ""; } function package_version() { if (!provider_available()) return ""; let output = command_output_from_args([ ADGUARD_BIN, "--version" ]); let version = first_nonempty_field(output); return replace(as_string(version), /^v/, ""); } function runtime_pid_running(pid) { pid = as_string(pid); return match(pid, /^[0-9]+$/) != null && command_success_from_args([ "kill", "-0", pid ]); } function running_pid() { let pid = trim(as_string(fs.readfile(ADGUARD_PID_FILE) || "")); if (runtime_pid_running(pid)) return pid; let output = trim(command_output_from_args([ "pgrep", "-f", "AdGuardHome" ])); for (let line in split(output, "\n")) { line = trim(as_string(line)); if (line != "") return line; } return ""; } function running() { return running_pid() != ""; } function service_enabled() { return fs.stat(ADGUARD_SERVICE_INIT) != null && command_success_from_args([ ADGUARD_SERVICE_INIT, "enabled" ]); } function service_running() { return fs.stat(ADGUARD_SERVICE_INIT) != null && command_success_from_args([ ADGUARD_SERVICE_INIT, "status" ]); } function yaml_escape(value) { value = replace(as_string(value), /\\/g, "\\\\"); value = replace(value, /"/g, "\\\""); return value; } function yaml_list(lines, values) { for (let value in values) push(lines, " - \"" + yaml_escape(value) + "\""); return lines; } function build_config_yaml() { let lines = []; let upstreams = []; // Primary upstream: sing-box DNS inbound, so FakeIP and per-domain routing keep working. push(upstreams, SB_DNS_INBOUND_ADDRESS + ":" + SB_DNS_INBOUND_PORT); let fallback = []; if (failover_enabled()) fallback = upstream_dns_list(); push(lines, "bind_host: 0.0.0.0"); push(lines, "bind_port: " + as_string(webui_port())); push(lines, "http_proxy: \"\""); push(lines, "language: \"\""); push(lines, "dns:"); push(lines, " bind_hosts:"); push(lines, " - " + dns_listen_host()); push(lines, " port: " + as_string(dns_listen_port())); push(lines, " upstream_dns:"); yaml_list(lines, upstreams); push(lines, " fallback_dns:"); yaml_list(lines, fallback); push(lines, " bootstrap_dns:"); yaml_list(lines, bootstrap_dns_list()); push(lines, " upstream_dns_file: \"\""); push(lines, " cache_size: 4194304"); push(lines, " cache_ttl_min: 0"); push(lines, " cache_ttl_max: 0"); push(lines, " cache_optimistic: false"); push(lines, " aaaa_disabled: false"); push(lines, " enable_dnssec: false"); push(lines, " max_goroutines: 300"); push(lines, " upstream_timeout: 10s"); push(lines, " use_http3_upstreams: false"); push(lines, "filtering:"); push(lines, " protection_enabled: true"); push(lines, " filtering_enabled: true"); push(lines, " filters_update_interval: 24"); push(lines, " filters:"); let filter_id = 1; for (let url in filter_list_urls()) { push(lines, " - enabled: true"); push(lines, " url: \"" + yaml_escape(url) + "\""); push(lines, " name: \"" + yaml_escape(url) + "\""); push(lines, " id: " + as_string(filter_id)); filter_id++; } push(lines, " user_rules: []"); push(lines, " rules: []"); push(lines, "querylog:"); push(lines, " enabled: false"); push(lines, " file_enabled: false"); push(lines, " interval: 0"); push(lines, " size_memory: 1000"); push(lines, "statistics:"); push(lines, " enabled: false"); push(lines, " interval: 0"); push(lines, "clients:"); push(lines, " runtime_sources:"); push(lines, " whois: true"); push(lines, " arp: true"); push(lines, " rdns: true"); push(lines, " dhcp: true"); push(lines, " hosts: true"); push(lines, " persistent: []"); push(lines, "log:"); push(lines, " enabled: true"); push(lines, " file: \"\""); push(lines, " max_backups: 0"); push(lines, " max_size: 100"); push(lines, " max_age: 3"); push(lines, " compress: false"); push(lines, " local_time: false"); push(lines, " verbose: false"); push(lines, "os:"); push(lines, " group: \"\""); push(lines, " user: \"\""); push(lines, " rlimit_nofile: 0"); push(lines, "schema_version: 28"); return join("\n", lines) + "\n"; } function ensure_dirs() { return command_success_from_args([ "mkdir", "-p", ADGUARD_WORK_DIR, ADGUARD_STATE_DIR ]); } function write_config() { if (!ensure_dirs()) return false; let tmp = ADGUARD_CONFIG + ".tmp." + as_string(int(clock()[1])); if (fs.writefile(tmp, build_config_yaml()) == null) { common.remove_file(tmp); return false; } if (!fs.rename(tmp, ADGUARD_CONFIG)) { common.remove_file(tmp); return false; } return true; } function apply_webui_auth() { // AdGuardHome stores a bcrypt hash for the admin user. When no users are // configured yet the control API accepts /control/install to create the // first admin from a plaintext password. Best effort: if it fails the web // UI simply stays open on the LAN. let username = webui_username(); let password = webui_password(); if (username == "" || password == "") return true; if (!command_exists("curl")) return false; let payload = sprintf("%J", { username, password }); let command = command_from_args([ "curl", "-fsS", "-m", "10", "-H", "Content-Type: application/json", "-d", payload, "http://127.0.0.1:" + as_string(webui_port()) + "/control/install" ]) + " >/dev/null 2>&1"; return command_success(command); } function restart_service() { if (fs.stat(ADGUARD_SERVICE_INIT) == null) return false; command_success_from_args([ ADGUARD_SERVICE_INIT, "restart" ]); return true; } function stop_service() { if (fs.stat(ADGUARD_SERVICE_INIT) == null) return true; command_success_from_args([ ADGUARD_SERVICE_INIT, "stop" ]); command_success_from_args([ "killall", "AdGuardHome" ]); common.remove_file(ADGUARD_PID_FILE); return true; } function configure() { if (!provider_available()) return false; if (!write_config()) return false; return true; } function start_runtime() { if (!provider_enabled() || !provider_available()) { stop_service(); return 0; } if (!configure()) return 1; command_success_from_args([ ADGUARD_SERVICE_INIT, "enable" ]); restart_service(); // Give the daemon a moment, then try to set the web UI credentials once. command_success_from_args([ "sleep", "2" ]); apply_webui_auth(); if (!running()) log_message("AdGuard Home is configured but the daemon is not running yet; check the service log", "warn"); return 0; } function stop_runtime() { stop_service(); return 0; } function status_json() { let installed = provider_available(); let version = installed ? package_version() : "not installed"; if (installed && version == "") version = "unknown"; let enabled = provider_enabled(); let config_ready = fs.stat(ADGUARD_CONFIG) != null; write_json({ installed, provider_available: installed, provider_path: ADGUARD_BIN, version, enabled, running: running(), service_enabled: service_enabled(), service_running: service_running(), config_ready, dns_main_mode: dns_main_mode(), dns_listen_host: dns_listen_host(), dns_listen_port: dns_listen_port(), webui_enabled: webui_enabled(), webui_port: webui_port(), upstream_count: length(upstream_dns_list()), failover_enabled: failover_enabled() }); } function check_json() { write_json({ adguard_installed: provider_available(), adguard_provider_path: ADGUARD_BIN }); } let mode = ARGV[0] || ""; if (mode == "start-runtime") exit(start_runtime()); else if (mode == "stop-runtime") exit(stop_runtime()); else if (mode == "configure") exit(configure() ? 0 : 1); else if (mode == "status") status_json(); else if (mode == "check") check_json(); else if (mode == "installed" || mode == "provider-available") exit(provider_available() ? 0 : 1); else if (mode == "package-version") print(package_version(), "\n"); else if (mode == "dns-main-mode") print(dns_main_mode(), "\n"); else if (mode == "dns-listen-host") print(dns_listen_host(), "\n"); else if (mode == "dns-listen-port") print(as_string(dns_listen_port()), "\n"); else if (mode == "provider-enabled") exit(provider_enabled() ? 0 : 1); else { warn("Usage: providers/adguard/runtime.uc ...\n"); exit(1); }