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
455 lines
14 KiB
Ucode
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);
|
|
}
|