Files
NeoTachyon/tachyon/files/usr/lib/providers/adguard/runtime.uc
T
hacker07 617107ef15 feat(adguard): add AdGuard Home component with DNS modes, failover and section upstream
Add AdGuard Home as an installable Tachyon component (component card with
check/update/remove/rollback/enable/disable) backed by providers/adguard
(validator + runtime) and an init.d service.

Backend:
- action.uc: install/remove/enable/backup/rollback/dispatch/releases for
  'adguard' (tar.gz from AdguardTeam/AdGuardHome, OpenWrt->AdGuard arch map)
- updater.uc: adguard asset selection; updates.uc: valid_component_name and
  automatic update checks
- runtime.uc: generates AdGuardHome.yaml from UCI (upstream=single-box DNS,
  fallback_dns for failover, web UI on LAN, filters), manages the daemon
- dns/apply.uc: dns_main_mode (dnsmasq->AdGuard->sing-box chain, or AdGuard on
  :53 with dnsmasq DHCP-only)
- settings: dns_main_mode (Main DNS) + dns_adguard_failover_enabled
- sections: routed_dns_type/dns_type 'adguard' resolve via the AdGuard provider
  upstreams; validator accepts the new type
- system-info: adguard_version/installed/enabled/backup fields

Frontend:
- component card 'AdGuard Home' with enable/disable + version actions
- LuCI DNS settings tab: Main DNS selector and failover flag
- section.js: AdGuard Home routed DNS protocol option

Tests: tests/adguard_validator.sh, tests/adguard_runtime.sh
2026-09-17 14:22:13 +03:00

455 lines
14 KiB
Ucode

#!/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 <start-runtime|stop-runtime|configure|status|check|installed|package-version|dns-main-mode|dns-listen-host|dns-listen-port|provider-enabled> ...\n");
exit(1);
}