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
This commit is contained in:
2026-09-17 14:22:13 +03:00
parent 5bfe7103dc
commit 617107ef15
26 changed files with 1438 additions and 31 deletions
@@ -53,6 +53,13 @@ const componentActionKeyMap: Record<string, UpdatesActionKey> = {
'tailscale:install_version': 'tailscaleInstall',
'tailscale:remove': 'tailscaleRemove',
'tailscale:rollback': 'tailscaleRollback',
'adguard:check_update': 'adguardCheck',
'adguard:install': 'adguardInstall',
'adguard:install_version': 'adguardInstall',
'adguard:remove': 'adguardRemove',
'adguard:rollback': 'adguardRollback',
'adguard:enable': 'adguardEnable',
'adguard:disable': 'adguardDisable',
'direct_bypass:enable': 'directBypassEnable',
'direct_bypass:disable': 'directBypassDisable',
'torrserver_direct:enable': 'torrserverDirectEnable',
@@ -249,6 +249,11 @@ export interface StoreType {
fptn_installed: number;
fptn_backup_version?: string;
fptn_backup_time?: number;
adguard_version?: string;
adguard_installed?: number;
adguard_enabled?: number;
adguard_backup_version?: string;
adguard_backup_time?: number;
server_inbounds_enabled_count: number;
direct_bypass_enabled?: number;
direct_bypass_address?: string;
@@ -301,6 +306,12 @@ export interface StoreType {
tailscaleInstall: { loading: boolean };
tailscaleRemove: { loading: boolean };
tailscaleRollback: { loading: boolean };
adguardCheck: { loading: boolean };
adguardInstall: { loading: boolean };
adguardRemove: { loading: boolean };
adguardRollback: { loading: boolean };
adguardEnable: { loading: boolean };
adguardDisable: { loading: boolean };
directBypassEnable: { loading: boolean };
directBypassDisable: { loading: boolean };
torrserverDirectEnable: { loading: boolean };
@@ -32,6 +32,9 @@ const UNKNOWN_SYSTEM_INFO: StoreType['diagnosticsSystemInfo'] = {
tailscale_installed: 0,
fptn_version: _('unknown'),
fptn_installed: 0,
adguard_version: _('unknown'),
adguard_installed: 0,
adguard_enabled: 0,
server_inbounds_enabled_count: -1,
openwrt_version: _('unknown'),
device_model: _('unknown'),
@@ -53,6 +53,12 @@ function getEmptyUpdatesActions(): StoreType['updatesActions'] {
tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false },
tailscaleRollback: { loading: false },
adguardCheck: { loading: false },
adguardInstall: { loading: false },
adguardRemove: { loading: false },
adguardRollback: { loading: false },
adguardEnable: { loading: false },
adguardDisable: { loading: false },
directBypassEnable: { loading: false },
directBypassDisable: { loading: false },
torrserverDirectEnable: { loading: false },
@@ -207,6 +207,12 @@ export const initialDiagnosticStore: Pick<
tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false },
tailscaleRollback: { loading: false },
adguardCheck: { loading: false },
adguardInstall: { loading: false },
adguardRemove: { loading: false },
adguardRollback: { loading: false },
adguardEnable: { loading: false },
adguardDisable: { loading: false },
directBypassEnable: { loading: false },
directBypassDisable: { loading: false },
torrserverDirectEnable: { loading: false },
@@ -222,6 +228,7 @@ export const initialDiagnosticStore: Pick<
olcrtc: { status: null, latest_version: '', release_url: '' },
fptn: { status: null, latest_version: '', release_url: '' },
tailscale: { status: null, latest_version: '', release_url: '' },
adguard: { status: null, latest_version: '', release_url: '' },
direct_bypass: { status: null, latest_version: '', release_url: '' },
torrserver_direct: { status: null, latest_version: '', release_url: '' },
},
@@ -78,6 +78,8 @@ function getComponentCardTitle(component: Tachyon.ComponentName): string {
return 'FPTN';
case 'tailscale':
return 'Tailscale';
case 'adguard':
return 'AdGuard Home';
default:
return String(component);
}
@@ -106,6 +108,8 @@ function getComponentCurrentVersion(
return sys.fptn_version;
case 'tailscale':
return sys.tailscale_version;
case 'adguard':
return sys.adguard_version;
default:
return undefined;
}
@@ -577,6 +581,23 @@ function patchSystemInfoAfterMutation(result: Tachyon.ComponentActionResult) {
}
}
if (result.component === 'adguard') {
nextSystemInfo.providerInfoLoaded = true;
if (result.action === 'remove') {
nextSystemInfo.adguard_installed = 0;
nextSystemInfo.adguard_version = 'not installed';
nextSystemInfo.adguard_enabled = 0;
} else if (result.action === 'enable') {
nextSystemInfo.adguard_enabled = 1;
} else if (result.action === 'disable') {
nextSystemInfo.adguard_enabled = 0;
} else {
nextSystemInfo.adguard_installed = 1;
nextSystemInfo.adguard_version = version;
}
}
if (result.component === 'direct_bypass') {
nextSystemInfo.direct_bypass_enabled = result.action === 'enable' ? 1 : 0;
}
@@ -1102,6 +1123,8 @@ function getComponentInstallKey(
return 'olcrtcInstall';
case 'tailscale':
return 'tailscaleInstall';
case 'adguard':
return 'adguardInstall';
default:
return 'tachyonInstall';
}
@@ -1151,6 +1174,8 @@ function getComponentBackupVersion(component: Tachyon.ComponentName): string {
return sys.fptn_backup_version || '';
case 'tailscale':
return sys.tailscale_backup_version || '';
case 'adguard':
return sys.adguard_backup_version || '';
default:
return '';
}
@@ -1185,7 +1210,8 @@ function getOptionalComponentActions({
| 'wdtt'
| 'olcrtc'
| 'fptn'
| 'tailscale';
| 'tailscale'
| 'adguard';
installed: boolean;
checkKey: UpdatesActionKey;
installKey: UpdatesActionKey;
@@ -1225,6 +1251,7 @@ const COMPONENT_REPO_URLS: Record<Tachyon.ComponentName, string> = {
olcrtc: 'https://github.com/Dushnilin/openwrt-olcrtc',
fptn: 'https://github.com/Dushnilin/fptn',
tailscale: 'https://openwrt.org/packages/pkgdata/tailscale',
adguard: 'https://github.com/AdguardTeam/AdGuardHome',
direct_bypass: '',
torrserver_direct: '',
};
@@ -1383,6 +1410,28 @@ function getComponentCards(): ComponentCard[] {
removeKey: 'tailscaleRemove',
rollbackKey: 'tailscaleRollback',
});
const adguardInstalled = Boolean(systemInfo.adguard_installed);
const adguardActions = [
...getOptionalComponentActions({
component: 'adguard',
installed: adguardInstalled,
checkKey: 'adguardCheck',
installKey: 'adguardInstall',
removeKey: 'adguardRemove',
rollbackKey: 'adguardRollback',
}),
{
key: systemInfo.adguard_enabled
? ('adguardDisable' as UpdatesActionKey)
: ('adguardEnable' as UpdatesActionKey),
text: systemInfo.adguard_enabled ? _('Disable') : _('Enable'),
icon: renderRotateCcwIcon24,
component: 'adguard' as const,
action: systemInfo.adguard_enabled
? ('disable' as Tachyon.ComponentAction)
: ('enable' as Tachyon.ComponentAction),
},
];
const directBypassEnabled = Boolean(systemInfo.direct_bypass_enabled);
const directBypassEndpoint = systemInfo.direct_bypass_address
@@ -1602,6 +1651,21 @@ function getComponentCards(): ComponentCard[] {
repoUrl: COMPONENT_REPO_URLS.tailscale,
actions: tailscaleActions,
},
{
component: 'adguard',
column: 2,
title: 'AdGuard Home',
version: systemInfoLoading
? _('Loading...')
: adguardInstalled
? `${systemInfo.adguard_enabled ? _('Enabled') : _('Disabled')} · ${systemInfo.adguard_version || _('unknown')}`
: _('Not installed'),
latestVersion: getLatestVersion('adguard'),
releaseUrl: getGitHubReleaseUrl('adguard'),
repoUrl: COMPONENT_REPO_URLS.adguard,
actions: adguardActions,
supportsVersions: true,
},
];
}
+8
View File
@@ -666,6 +666,11 @@ export namespace Tachyon {
fptn_supported?: 0 | 1;
fptn_backup_version?: string;
fptn_backup_time?: number;
adguard_version?: string;
adguard_installed?: 0 | 1;
adguard_enabled?: 0 | 1;
adguard_backup_version?: string;
adguard_backup_time?: number;
direct_bypass_enabled?: 0 | 1;
direct_bypass_address?: string;
direct_bypass_port?: string;
@@ -680,6 +685,7 @@ export namespace Tachyon {
show_component_olcrtc?: 0 | 1;
show_component_fptn?: 0 | 1;
show_component_tailscale?: 0 | 1;
show_component_adguard?: 0 | 1;
show_component_direct_bypass?: 0 | 1;
show_component_torrserver_direct?: 0 | 1;
openwrt_version: string;
@@ -702,6 +708,7 @@ export namespace Tachyon {
zapret_installed: 0 | 1;
zapret2_installed: 0 | 1;
byedpi_installed: 0 | 1;
adguard_installed?: 0 | 1;
wdtt_installed: 0 | 1;
olcrtc_installed: 0 | 1;
server_inbounds_enabled_count: number;
@@ -769,6 +776,7 @@ export namespace Tachyon {
| 'olcrtc'
| 'fptn'
| 'tailscale'
| 'adguard'
| 'direct_bypass'
| 'torrserver_direct';
@@ -6124,6 +6124,12 @@ var initialDiagnosticStore = {
tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false },
tailscaleRollback: { loading: false },
adguardCheck: { loading: false },
adguardInstall: { loading: false },
adguardRemove: { loading: false },
adguardRollback: { loading: false },
adguardEnable: { loading: false },
adguardDisable: { loading: false },
directBypassEnable: { loading: false },
directBypassDisable: { loading: false },
torrserverDirectEnable: { loading: false },
@@ -6139,6 +6145,7 @@ var initialDiagnosticStore = {
olcrtc: { status: null, latest_version: "", release_url: "" },
fptn: { status: null, latest_version: "", release_url: "" },
tailscale: { status: null, latest_version: "", release_url: "" },
adguard: { status: null, latest_version: "", release_url: "" },
direct_bypass: { status: null, latest_version: "", release_url: "" },
torrserver_direct: { status: null, latest_version: "", release_url: "" }
}
@@ -6533,6 +6540,13 @@ var componentActionKeyMap = {
"tailscale:install_version": "tailscaleInstall",
"tailscale:remove": "tailscaleRemove",
"tailscale:rollback": "tailscaleRollback",
"adguard:check_update": "adguardCheck",
"adguard:install": "adguardInstall",
"adguard:install_version": "adguardInstall",
"adguard:remove": "adguardRemove",
"adguard:rollback": "adguardRollback",
"adguard:enable": "adguardEnable",
"adguard:disable": "adguardDisable",
"direct_bypass:enable": "directBypassEnable",
"direct_bypass:disable": "directBypassDisable",
"torrserver_direct:enable": "torrserverDirectEnable",
@@ -6719,6 +6733,12 @@ function getEmptyUpdatesActions() {
tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false },
tailscaleRollback: { loading: false },
adguardCheck: { loading: false },
adguardInstall: { loading: false },
adguardRemove: { loading: false },
adguardRollback: { loading: false },
adguardEnable: { loading: false },
adguardDisable: { loading: false },
directBypassEnable: { loading: false },
directBypassDisable: { loading: false },
torrserverDirectEnable: { loading: false },
@@ -11236,6 +11256,9 @@ var UNKNOWN_SYSTEM_INFO = {
tailscale_installed: 0,
fptn_version: _("unknown"),
fptn_installed: 0,
adguard_version: _("unknown"),
adguard_installed: 0,
adguard_enabled: 0,
server_inbounds_enabled_count: -1,
openwrt_version: _("unknown"),
device_model: _("unknown")
@@ -21159,6 +21182,8 @@ function getComponentCardTitle(component) {
return "FPTN";
case "tailscale":
return "Tailscale";
case "adguard":
return "AdGuard Home";
default:
return String(component);
}
@@ -21184,6 +21209,8 @@ function getComponentCurrentVersion(component) {
return sys.fptn_version;
case "tailscale":
return sys.tailscale_version;
case "adguard":
return sys.adguard_version;
default:
return void 0;
}
@@ -21512,6 +21539,21 @@ function patchSystemInfoAfterMutation(result) {
nextSystemInfo.fptn_version = version;
}
}
if (result.component === "adguard") {
nextSystemInfo.providerInfoLoaded = true;
if (result.action === "remove") {
nextSystemInfo.adguard_installed = 0;
nextSystemInfo.adguard_version = "not installed";
nextSystemInfo.adguard_enabled = 0;
} else if (result.action === "enable") {
nextSystemInfo.adguard_enabled = 1;
} else if (result.action === "disable") {
nextSystemInfo.adguard_enabled = 0;
} else {
nextSystemInfo.adguard_installed = 1;
nextSystemInfo.adguard_version = version;
}
}
if (result.component === "direct_bypass") {
nextSystemInfo.direct_bypass_enabled = result.action === "enable" ? 1 : 0;
}
@@ -21907,6 +21949,8 @@ function getComponentInstallKey(component) {
return "olcrtcInstall";
case "tailscale":
return "tailscaleInstall";
case "adguard":
return "adguardInstall";
default:
return "tachyonInstall";
}
@@ -21945,6 +21989,8 @@ function getComponentBackupVersion(component) {
return sys.fptn_backup_version || "";
case "tailscale":
return sys.tailscale_backup_version || "";
case "adguard":
return sys.adguard_backup_version || "";
default:
return "";
}
@@ -21995,6 +22041,7 @@ var COMPONENT_REPO_URLS = {
olcrtc: "https://github.com/Dushnilin/openwrt-olcrtc",
fptn: "https://github.com/Dushnilin/fptn",
tailscale: "https://openwrt.org/packages/pkgdata/tailscale",
adguard: "https://github.com/AdguardTeam/AdGuardHome",
direct_bypass: "",
torrserver_direct: ""
};
@@ -22139,6 +22186,24 @@ function getComponentCards() {
removeKey: "tailscaleRemove",
rollbackKey: "tailscaleRollback"
});
const adguardInstalled = Boolean(systemInfo.adguard_installed);
const adguardActions = [
...getOptionalComponentActions({
component: "adguard",
installed: adguardInstalled,
checkKey: "adguardCheck",
installKey: "adguardInstall",
removeKey: "adguardRemove",
rollbackKey: "adguardRollback"
}),
{
key: systemInfo.adguard_enabled ? "adguardDisable" : "adguardEnable",
text: systemInfo.adguard_enabled ? _("Disable") : _("Enable"),
icon: renderRotateCcwIcon24,
component: "adguard",
action: systemInfo.adguard_enabled ? "disable" : "enable"
}
];
const directBypassEnabled = Boolean(systemInfo.direct_bypass_enabled);
const directBypassEndpoint = systemInfo.direct_bypass_address ? `${systemInfo.direct_bypass_address}:${systemInfo.direct_bypass_port || "2080"}` : "";
const torrserverRunning = Boolean(systemInfo.torrserver_running);
@@ -22294,6 +22359,17 @@ function getComponentCards() {
releaseUrl: getGitHubReleaseUrl("tailscale"),
repoUrl: COMPONENT_REPO_URLS.tailscale,
actions: tailscaleActions
},
{
component: "adguard",
column: 2,
title: "AdGuard Home",
version: systemInfoLoading ? _("Loading...") : adguardInstalled ? `${systemInfo.adguard_enabled ? _("Enabled") : _("Disabled")} · ${systemInfo.adguard_version || _("unknown")}` : _("Not installed"),
latestVersion: getLatestVersion("adguard"),
releaseUrl: getGitHubReleaseUrl("adguard"),
repoUrl: COMPONENT_REPO_URLS.adguard,
actions: adguardActions,
supportsVersions: true
}
];
}
@@ -2223,6 +2223,7 @@ function dnsTypeChoices() {
{ value: "dot", label: _("DNS over TLS (DoT)") },
{ value: "doq", label: _("DNS over QUIC (DoQ)") },
{ value: "udp", label: "UDP" },
{ value: "adguard", label: _("AdGuard Home") },
];
}
@@ -11287,15 +11288,21 @@ function createSectionContent(section) {
routedDnsTypeOption.value("dot", "DNS over TLS");
routedDnsTypeOption.value("doh", "DNS over HTTPS");
routedDnsTypeOption.value("doq", "DNS over QUIC");
routedDnsTypeOption.value("adguard", _("AdGuard Home"));
const routedDnsServerOption = addDynamicConditionField(section, {
key: "routed_dns_server",
label: _("Routed DNS Servers"),
description: _(
"DNS server addresses for domain resolution in this section (e.g. 1.1.1.1, https://cloudflare-dns.com/dns-query).",
"DNS server addresses for domain resolution in this section (e.g. 1.1.1.1, https://cloudflare-dns.com/dns-query). When 'AdGuard Home' is selected as the protocol, the DNS servers configured in the AdGuard Home provider are used instead.",
),
});
routedDnsServerOption.depends({ routed_dns_enabled: "1" });
routedDnsServerOption.retain = true;
routedDnsServerOption.checkDepends = function (section_id) {
const dnsType = uci.get(UCI_PACKAGE, section_id, "routed_dns_type") || "udp";
return dnsType !== "adguard";
};
const portsOption = addDynamicConditionField(section, {
key: "ports",
@@ -1360,6 +1360,13 @@ function createComponentsVisibilityWidget(option, section_id) {
defaultVisible: true,
fixed: false,
},
{
id: "adguard",
label: _("AdGuard Home"),
desc: _("Caching DNS with ad blocking and failover"),
defaultVisible: true,
fixed: false,
},
{
id: "torrserver_direct",
label: _("TorrServer Direct"),
@@ -1877,6 +1884,34 @@ function createSettingsContent(section, capabilities) {
o.datatype = "uinteger";
o.rmempty = true;
// ─── AdGuard Home main DNS mode ──────────────────────────────────────────
o = section.taboption(
"dns",
form.ListValue,
"dns_main_mode",
_("Main DNS"),
_(
"Choose which resolver serves DNS to the LAN. When AdGuard Home is installed and enabled, 'dnsmasq' keeps the dnsmasq→AdGuard→sing-box chain, and 'AdGuard Home' makes AdGuard the main DNS listener on port 53 (dnsmasq serves DHCP only). Without AdGuard Home this setting has no effect.",
),
);
o.value("dnsmasq", _("dnsmasq (via AdGuard → sing-box)"));
o.value("adguard", _("AdGuard Home (port 53, dnsmasq = DHCP only)"));
o.default = "dnsmasq";
o.rmempty = false;
o = section.taboption(
"dns",
form.Flag,
"dns_adguard_failover_enabled",
_("DNS Failover to AdGuard"),
_(
"When Tachyon (sing-box DNS) is unreachable, AdGuard Home automatically answers from the fallback DNS servers you configure in its own settings (upstream list). DNS keeps resolving through AdGuard until Tachyon recovers.",
),
);
o.default = "0";
o.rmempty = false;
// ─── DNS Strategy ────────────────────────────────────────────────────────
o = section.taboption(
+18
View File
@@ -50,6 +50,9 @@ config settings 'settings'
option cache_path '/tmp/sing-box/cache.db'
option dns_local_cache '0'
option dns_cache_size '10000'
option dns_main_mode 'dnsmasq'
option dns_adguard_failover_enabled '0'
option show_component_adguard '1'
option log_level 'warn'
option exclude_ntp '0'
option shutdown_correctly '0'
@@ -311,5 +314,20 @@ config guest_mode 'guest_mode'
#list days 'monday'
option notify '1'
#config provider 'adguard'
# option enabled '0'
# option webui_enabled '1'
# option webui_port '3000'
# option webui_username ''
# option webui_password ''
# # DNS servers used by AdGuard Home for fallback (when Tachyon DNS is down)
# # and as the resolver for sections that choose 'AdGuard Home':
# #list upstream_dns 'https://dns.google/dns-query'
# #list upstream_dns 'tls://1.1.1.1'
# #list bootstrap_dns '1.1.1.1'
# #list bootstrap_dns '8.8.8.8'
# # Ad-block filter lists (empty = the built-in AdGuard DNS filter):
# #list filter_lists 'https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt'
+37
View File
@@ -0,0 +1,37 @@
#!/bin/sh /etc/rc.common
# Tachyon-managed AdGuard Home service. The configuration file is generated
# from /etc/config/tachyon by providers/adguard/runtime.uc; do not edit the
# YAML by hand, it is overwritten on every reconfigure.
USE_PROCD=1
START=98
STOP=20
PROG=/usr/bin/AdGuardHome
WORK_DIR=/etc/adguard
CONFIG=$WORK_DIR/AdGuardHome.yaml
PID_FILE=/var/run/tachyon/adguard/adguard.pid
start_service() {
[ -x "$PROG" ] || return 0
[ -f "$CONFIG" ] || return 0
mkdir -p "$WORK_DIR" /var/run/tachyon/adguard
procd_open_instance
procd_set_param command "$PROG" -c "$CONFIG" -w "$WORK_DIR" --pidfile "$PID_FILE" --no-check-update
procd_set_param file "$CONFIG"
procd_set_param respawn
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
stop_service() {
[ -f "$PID_FILE" ] || return 0
local pid
pid="$(cat "$PID_FILE" 2>/dev/null)"
[ -n "$pid" ] && kill "$pid" 2>/dev/null
rm -f "$PID_FILE"
}
+205 -1
View File
@@ -1504,6 +1504,170 @@ function resolve_fptn_release(arch, tag) {
return null;
}
function strip_leading_v(value) {
value = as_string(value);
return substr(value, 0, 1) == "v" ? substr(value, 1) : value;
}
function resolve_adguard_release(arch, tag) {
let owner = "AdguardTeam";
let repo = "AdGuardHome";
let releases_json = (tag != null && tag != "") ?
fetch_github_release_by_tag_json(owner, repo, tag) :
fetch_github_releases_json(owner, repo, "30");
if (releases_json == "" || releases_json == "[]")
return null;
let resolved = trim(helper_output_input(releases_json, "adguard-select-asset", [ arch.candidates ]));
let fields = split(resolved, "\t");
if (length(fields) >= 5) {
return {
arch: fields[0],
package_name: fields[1],
package_url: fields[2],
release_url: fields[3],
version: strip_leading_v(fields[4])
};
}
return null;
}
function adguard_runtime_module() {
return LIB_DIR + "/providers/adguard/runtime.uc";
}
function ensure_adguard_provider_section() {
let cursor = uci_core.cursor();
if (cursor == null)
return;
cursor.load("tachyon");
if (cursor.get("tachyon", "adguard", "enabled") != null)
return;
cursor.set("tachyon", "adguard", "provider");
cursor.set("tachyon", "adguard", "enabled", "0");
cursor.set("tachyon", "adguard", "webui_enabled", "1");
cursor.set("tachyon", "adguard", "webui_port", "3000");
cursor.commit("tachyon");
}
function install_adguard(action, target_tag) {
let component = "adguard";
let label = "AdGuard Home";
init_tmp_dir() || action_fail(component, action, "Failed to create temporary directory");
let arch = resolve_arch_candidates();
if (arch == null)
action_fail(component, action, "Failed to detect package architecture");
let release = null;
retry_resolve("Resolving " + label + " package", function() {
release = resolve_adguard_release(arch, target_tag);
return release != null;
});
if (release == null)
action_fail(component, action, "Failed to resolve " + label + " package for this router architecture");
let runtime_module = adguard_runtime_module();
let installed = provider_installed(runtime_module);
let current_version = provider_package_version(runtime_module);
if (action == "check_update") {
if (!installed)
action_success(component, action, label + " is not installed", current_version, release.version, 0, "", release.release_url || "");
check_success(component, current_version, release.version, release.release_url || "");
}
let pkg = download_direct_package(release);
if (pkg == null)
action_fail(component, action, "Failed to download " + label + " package");
let extract_dir = tmp_dir + "/adguard-extract";
command_success_from_args([ "mkdir", "-p", extract_dir ]);
if (!run_logged("Extracting " + label + " archive",
"tar -xzf " + shell_quote(pkg.file) + " -C " + shell_quote(extract_dir)))
action_fail(component, action, "Failed to extract " + label + " archive");
let found_bin = "";
let found = trim(command_output_from_args([ "find", extract_dir, "-maxdepth", "2", "-type", "f", "-name", "AdGuardHome" ]));
for (let line in split(found, "\n")) {
line = trim(as_string(line));
if (line != "") {
found_bin = line;
break;
}
}
if (found_bin == "")
action_fail(component, action, "AdGuardHome binary was not found in the downloaded archive");
module_success([ runtime_module, "stop-runtime" ]);
if (!install_staged_file(found_bin, "/usr/bin/AdGuardHome", "0755"))
action_fail(component, action, "Failed to install " + label + " binary");
command_success_from_args([ "mkdir", "-p", "/etc/adguard" ]);
ensure_adguard_provider_section();
restart_tachyon_after_successful_change();
clear_version_caches();
current_version = provider_package_version(runtime_module);
if (current_version == "")
current_version = pkg.version || "unknown";
action_success(component, action, label + " has been installed", current_version, pkg.version, 1, "latest", release.release_url || "");
}
function remove_adguard() {
let component = "adguard";
let label = "AdGuard Home";
let runtime_module = adguard_runtime_module();
let current_version = provider_package_version(runtime_module);
let cursor = uci_core.cursor();
module_success([ runtime_module, "stop-runtime" ]);
if (cursor != null) {
cursor.load("tachyon");
if (cursor.get("tachyon", "adguard", "enabled") != null)
cursor.set("tachyon", "adguard", "enabled", "0");
cursor.commit("tachyon");
}
remove_file("/usr/bin/AdGuardHome");
remove_file("/etc/init.d/adguard");
remove_file("/var/run/tachyon/adguard/adguard.pid");
clear_version_caches();
restart_tachyon_after_successful_change();
action_success(component, "remove", label + " has been removed", current_version, "", 1);
}
function set_adguard_enabled(action) {
let enable = action == "enable";
let runtime_module = adguard_runtime_module();
let cursor = uci_core.cursor();
if (cursor == null) {
action_fail("adguard", action, "UCI is unavailable");
return;
}
cursor.load("tachyon");
if (cursor.get("tachyon", "adguard", "enabled") == null) {
cursor.set("tachyon", "adguard", "provider");
cursor.set("tachyon", "adguard", "webui_enabled", "1");
cursor.set("tachyon", "adguard", "webui_port", "3000");
}
cursor.set("tachyon", "adguard", "enabled", enable ? "1" : "0");
cursor.commit("tachyon");
remove_file(SYSTEM_INFO_CACHE_FILE);
restart_tachyon_after_successful_change();
action_success(
"adguard",
action,
enable ? "AdGuard Home has been enabled" : "AdGuard Home has been disabled",
enable ? "enabled" : "disabled",
enable ? "enabled" : "disabled",
1
);
}
function download_direct_package(release) {
let package_file = tmp_dir + "/" + release.package_name;
if (!download_with_retry(release.package_url, package_file, release.package_name) || !file_nonempty(package_file))
@@ -3205,6 +3369,8 @@ function normalize_component_name(component) {
return "fptn";
if (component == "tachyon")
return "tachyon";
if (component == "adguardhome" || component == "adguard-home" || component == "ad_guard")
return "adguard";
if (component == "direct-bypass" || component == "directbypass" || component == "direct_proxy")
return "direct_bypass";
if (component == "torrserver_direct" || component == "torrserver-direct")
@@ -3386,6 +3552,22 @@ function create_component_backup(component) {
write_file(meta_file, sprintf("%J\n", meta));
return true;
}
else if (component == "adguard") {
let bin = "/usr/bin/AdGuardHome";
if (!file_exists(bin)) return true;
let st = fs.stat(bin);
let size = (st && st.size) ? st.size : 0;
if (!check_free_disk_space("/etc", size)) return false;
ensure_dir(bdir);
command_success_from_args([ "cp", "-p", bin, bdir + "/AdGuardHome" ]);
let meta = {
component: "adguard",
version: provider_package_version(adguard_runtime_module()),
timestamp: now_seconds()
};
write_file(meta_file, sprintf("%J\n", meta));
return true;
}
return true;
}
@@ -3509,6 +3691,17 @@ function rollback_component(component) {
restart_tachyon_after_successful_change();
action_success("tailscale", "rollback", "Tailscale rolled back to " + backup_version, backup_version, "", 1);
}
else if (component == "adguard") {
let backup_bin = bdir + "/AdGuardHome";
if (!file_exists(backup_bin)) action_fail("adguard", "rollback", "AdGuard Home backup binary is missing");
module_success([ adguard_runtime_module(), "stop-runtime" ]);
remove_file("/usr/bin/AdGuardHome");
command_success_from_args([ "cp", "-p", backup_bin, "/usr/bin/AdGuardHome" ]);
command_success_from_args([ "chmod", "0755", "/usr/bin/AdGuardHome" ]);
clear_version_caches();
restart_tachyon_after_successful_change();
action_success("adguard", "rollback", "AdGuard Home rolled back to " + backup_version, backup_version, "", 1);
}
else {
action_fail(component, "rollback", "Rollback not supported for component " + component);
}
@@ -3555,6 +3748,8 @@ function list_component_releases(component, count) {
owner = "Dushnilin"; repo = "openwrt-olcrtc";
} else if (component == "fptn") {
owner = "Dushnilin"; repo = "fptn";
} else if (component == "adguard") {
owner = "AdguardTeam"; repo = "AdGuardHome";
} else {
print("[]\n"); return;
}
@@ -3619,6 +3814,8 @@ function install_component_version(component, tag) {
install_olcrtc("install", tag);
} else if (component == "fptn") {
install_fptn("install", tag);
} else if (component == "adguard") {
install_adguard("install", tag);
} else {
action_fail(component, "install_version", "Component " + component + " does not support version installation");
}
@@ -3730,7 +3927,8 @@ function component_action(component, action, extra) {
return;
}
if (action != "check_update" && action != "remove" && component != "direct_bypass" && component != "torrserver_direct") {
if (action != "check_update" && action != "remove" && component != "direct_bypass" && component != "torrserver_direct" &&
!(component == "adguard" && (action == "enable" || action == "disable"))) {
create_component_backup(component);
}
@@ -3773,6 +3971,12 @@ function component_action(component, action, extra) {
install_tailscale(action);
else if (component == "tailscale" && action == "remove")
remove_optional_component("tailscale", "tailscale", "Tailscale", LIB_DIR + "/providers/tailscale/runtime.uc");
else if (component == "adguard" && (action == "check_update" || action == "install"))
install_adguard(action);
else if (component == "adguard" && action == "remove")
remove_adguard();
else if (component == "adguard" && (action == "enable" || action == "disable"))
set_adguard_enabled(action);
else if (component == "direct_bypass" && (action == "enable" || action == "disable"))
set_direct_bypass(action);
else if (component == "torrserver_direct" && (action == "enable" || action == "disable"))
@@ -1011,6 +1011,82 @@ function byedpi_select_asset(series, asset_ext, arch_candidates) {
}
}
// AdGuardHome ships static tar.gz archives named
// "AdGuardHome_linux_<suffix>.tar.gz" where <suffix> is AdGuard's own arch
// naming, not OpenWrt's. Map OpenWrt arch candidates onto it.
function adguard_arch_suffix(arch) {
arch = as_string(arch);
if (arch == "x86_64" || arch == "amd64")
return "amd64";
if (arch == "x86" || arch == "i386" || arch == "i486" || arch == "i586" || arch == "i686" || arch == "386")
return "386";
if (arch == "aarch64" || arch == "arm64" || arch == "aarch64_generic" || str_startswith(arch, "aarch64_"))
return "arm64";
if (arch == "riscv64" || arch == "riscv64_generic" || str_startswith(arch, "riscv64_"))
return "riscv64";
if (arch == "s390x")
return "s390x";
if (str_startswith(arch, "arm_")) {
if (str_contains(arch, "cortex-a5") || str_contains(arch, "cortex-a7") || str_contains(arch, "cortex-a8") ||
str_contains(arch, "cortex-a9") || str_contains(arch, "cortex-a15") || str_contains(arch, "mpcore"))
return "armv7";
if (str_contains(arch, "arm1176jzf-s"))
return "armv6";
if (str_contains(arch, "arm926ej-s") || str_contains(arch, "xscale"))
return "armv5";
return "armv7";
}
if (str_startswith(arch, "mips64el") || str_startswith(arch, "mips64le"))
return "mips64le_softfloat";
if (str_startswith(arch, "mipsel") || str_startswith(arch, "mipsle"))
return "mipsle_softfloat";
if (str_startswith(arch, "mips"))
return "mips_softfloat";
if (str_startswith(arch, "powerpc") || str_startswith(arch, "ppc")) {
if (str_contains(arch, "64"))
return "ppc64le";
return "ppc";
}
return "";
}
function adguard_select_asset(arch_candidates) {
let data = read_stdin_json();
let releases = [];
if (type(data) == "array")
releases = data;
else if (type(data) == "object")
releases = [ data ];
for (let release in releases) {
if (type(release) != "object")
continue;
if (release.draft === true)
continue;
for (let arch in split(as_string(arch_candidates), " ")) {
if (arch == "")
continue;
let suffix = adguard_arch_suffix(arch);
if (suffix == "")
continue;
let expected = "AdGuardHome_linux_" + suffix + ".tar.gz";
for (let asset in array_or_empty(release.assets)) {
if (type(asset) != "object")
continue;
let name = as_string(asset.name || "");
let url = as_string(asset.browser_download_url || "");
if (url != "" && name == expected) {
print(arch, "\t", name, "\t", url, "\t",
as_string(release.html_url || ""), "\t", as_string(release.tag_name || ""), "\n");
return;
}
}
}
}
}
function fptn_asset_matches(name, series, arch, ext) {
if (!str_startswith(name, "fptn-client-") && !str_startswith(name, "fptn-"))
return false;
@@ -1540,6 +1616,8 @@ else if (mode == "olcrtc-select-asset")
olcrtc_select_asset(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "fptn-select-asset")
fptn_select_asset(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "adguard-select-asset")
adguard_select_asset(ARGV[1]);
else if (mode == "sing-box-extended-release-tag")
sing_box_extended_release_tag();
else if (mode == "sing-box-lx-release-tag")
+6 -1
View File
@@ -616,13 +616,16 @@ function normalize_component_name(component) {
return "sing_box";
if (component == "tachyon")
return "tachyon";
if (component == "adguardhome" || component == "adguard-home" || component == "ad_guard")
return "adguard";
return component;
}
function valid_component_name(component) {
component = normalize_component_name(component);
return component == "tachyon" || component == "sing_box" || component == "zapret" ||
component == "zapret2" || component == "byedpi" || component == "wdtt" || component == "olcrtc" || component == "fptn" || component == "tailscale";
component == "zapret2" || component == "byedpi" || component == "wdtt" || component == "olcrtc" || component == "fptn" || component == "tailscale" ||
component == "adguard";
}
function settings_component_auto_update_enabled(settings) {
@@ -2045,6 +2048,8 @@ function automatic_component_check_names() {
push(result, "olcrtc");
if (module_success([ LIB_DIR + "/providers/fptn/runtime.uc", "installed" ]))
push(result, "fptn");
if (module_success([ LIB_DIR + "/providers/adguard/runtime.uc", "installed" ]))
push(result, "adguard");
return result;
}
+5 -1
View File
@@ -701,11 +701,15 @@ function routed_dns_enabled(section) {
function routed_dns_type(section) {
let t = option(section, "routed_dns_type", "udp");
if (!contains(["udp", "dot", "doh", "doq"], t))
if (!contains(["udp", "dot", "doh", "doq", "adguard"], t))
t = "udp";
return t;
}
function routed_dns_adguard(section) {
return routed_dns_type(section) == "adguard";
}
function routed_dns_servers(section) {
let servers = list_option(section, "routed_dns_server");
if (length(servers) == 0) {
+20 -16
View File
@@ -1427,14 +1427,16 @@ function dns_action_has_domain_matchers(section) {
function validate_dns_action(section, sections, context) {
let name = section_name(section);
let dns_type = option(section, "dns_type", "udp");
if (!contains([ "udp", "dot", "doh", "doq" ], dns_type))
fail_validation("DNS rule '" + name + "' uses unsupported protocol '" + dns_type + "'. Use udp, dot, doh, or doq. Aborted.");
let dns_servers = list_option(section, "dns_server");
if (length(dns_servers) == 0)
fail_validation("DNS rule '" + name + "' has no DNS server specified. Aborted.");
for (let dns_server in dns_servers) {
if (!dns_server_value_valid(dns_server))
fail_validation("DNS rule '" + name + "' has an invalid DNS server '" + dns_server + "'. Aborted.");
if (!contains([ "udp", "dot", "doh", "doq", "adguard" ], dns_type))
fail_validation("DNS rule '" + name + "' uses unsupported protocol '" + dns_type + "'. Use udp, dot, doh, doq, or adguard. Aborted.");
if (dns_type != "adguard") {
let dns_servers = list_option(section, "dns_server");
if (length(dns_servers) == 0)
fail_validation("DNS rule '" + name + "' has no DNS server specified. Aborted.");
for (let dns_server in dns_servers) {
if (!dns_server_value_valid(dns_server))
fail_validation("DNS rule '" + name + "' has an invalid DNS server '" + dns_server + "'. Aborted.");
}
}
if (length(connections.rule_sets_with_subnets(section)) > 0)
fail_validation("DNS rule '" + name + "' can use domain-only rule sets, but subnet extraction is enabled. Disable 'Include IP addresses and subnets'. Aborted.");
@@ -1624,14 +1626,16 @@ function validate_rule(section, sections, context) {
if (action == "dns" || action == "hosts" || action == "block")
fail_validation("Rule '" + name + "' has routed DNS enabled but action '" + action + "' does not support routed DNS. Aborted.");
let routed_type = connections.routed_dns_type(section);
if (!contains(["udp", "dot", "doh", "doq"], routed_type))
fail_validation("Rule '" + name + "' has invalid routed DNS type '" + routed_type + "'. Use udp, dot, doh, or doq. Aborted.");
let routed_servers = connections.routed_dns_servers(section);
if (length(routed_servers) == 0)
fail_validation("Rule '" + name + "' has routed DNS enabled but no DNS server specified. Aborted.");
for (let srv in routed_servers)
if (!dns_server_value_valid(srv))
fail_validation("Rule '" + name + "' has an invalid routed DNS server '" + srv + "'. Aborted.");
if (!contains(["udp", "dot", "doh", "doq", "adguard"], routed_type))
fail_validation("Rule '" + name + "' has invalid routed DNS type '" + routed_type + "'. Use udp, dot, doh, doq, or adguard. Aborted.");
if (routed_type != "adguard") {
let routed_servers = connections.routed_dns_servers(section);
if (length(routed_servers) == 0)
fail_validation("Rule '" + name + "' has routed DNS enabled but no DNS server specified. Aborted.");
for (let srv in routed_servers)
if (!dns_server_value_valid(srv))
fail_validation("Rule '" + name + "' has an invalid routed DNS server '" + srv + "'. Aborted.");
}
if (!connections.has_dns_matchers(section) && length(list_option(section, "fully_routed_ips")) == 0)
fail_validation("Rule '" + name + "' has routed DNS enabled but no domain conditions or forced devices. Routed DNS requires at least one domain matcher. Aborted.");
}
@@ -61,6 +61,7 @@ const WDTT_RUNTIME_UC = LIB_DIR + "/providers/wdtt/runtime.uc";
const OLCRTC_RUNTIME_UC = LIB_DIR + "/providers/olcrtc/runtime.uc";
const FPTN_RUNTIME_UC = LIB_DIR + "/providers/fptn/runtime.uc";
const TAILSCALE_RUNTIME_UC = LIB_DIR + "/providers/tailscale/runtime.uc";
const ADGUARD_RUNTIME_UC = LIB_DIR + "/providers/adguard/runtime.uc";
const ZAPRET_VALIDATOR_UC = LIB_DIR + "/providers/zapret/validator.uc";
const ZAPRET2_VALIDATOR_UC = LIB_DIR + "/providers/zapret2/validator.uc";
const BYEDPI_VALIDATOR_UC = LIB_DIR + "/providers/byedpi/validator.uc";
@@ -1227,6 +1228,9 @@ function build_system_info() {
let fptn_installed = provider_installed(FPTN_RUNTIME_UC) ? 1 : 0;
let fptn_supported = (fptn_installed == 1 || is_fptn_supported()) ? 1 : 0;
let fptn_version = fptn_installed ? provider_version(FPTN_RUNTIME_UC) : "not installed";
let adguard_installed = provider_installed(ADGUARD_RUNTIME_UC) ? 1 : 0;
let adguard_version = adguard_installed ? provider_version(ADGUARD_RUNTIME_UC) : "not installed";
let adguard_enabled = module_success(ADGUARD_RUNTIME_UC, [ "provider-enabled" ]) ? 1 : 0;
let device_model = first_line_value("/tmp/sysinfo/model", "unknown");
let direct_bypass_enabled = bool_option(settings(), "direct_bypass_enabled", false) ? 1 : 0;
@@ -1245,6 +1249,7 @@ function build_system_info() {
let show_component_olcrtc = bool_option(settings(), "show_component_olcrtc", true) ? 1 : 0;
let show_component_fptn = bool_option(settings(), "show_component_fptn", true) ? 1 : 0;
let show_component_tailscale = bool_option(settings(), "show_component_tailscale", true) ? 1 : 0;
let show_component_adguard = bool_option(settings(), "show_component_adguard", true) ? 1 : 0;
let show_component_direct_bypass = bool_option(settings(), "show_component_direct_bypass", true) ? 1 : 0;
let show_component_torrserver_direct = bool_option(settings(), "show_component_torrserver_direct", true) ? 1 : 0;
@@ -1269,6 +1274,7 @@ function build_system_info() {
let olcrtc_meta = read_backup_meta("olcrtc");
let tailscale_meta = read_backup_meta("tailscale");
let fptn_meta = read_backup_meta("fptn");
let adguard_meta = read_backup_meta("adguard");
return {
tachyon_version: TACHYON_VERSION,
@@ -1313,6 +1319,11 @@ function build_system_info() {
fptn_supported,
fptn_backup_version: fptn_meta ? as_string(fptn_meta.version) : "",
fptn_backup_time: fptn_meta ? int(fptn_meta.timestamp || 0) : 0,
adguard_version,
adguard_installed,
adguard_enabled,
adguard_backup_version: adguard_meta ? as_string(adguard_meta.version) : "",
adguard_backup_time: adguard_meta ? int(adguard_meta.timestamp || 0) : 0,
direct_bypass_enabled,
direct_bypass_address,
direct_bypass_port,
@@ -1327,6 +1338,7 @@ function build_system_info() {
show_component_olcrtc,
show_component_fptn,
show_component_tailscale,
show_component_adguard,
show_component_direct_bypass,
show_component_torrserver_direct,
openwrt_version: openwrt_release(),
@@ -5423,6 +5435,8 @@ else if (mode == "check-zapret2-runtime")
exit(module_passthrough(ZAPRET2_RUNTIME_UC, [ "check" ]));
else if (mode == "check-byedpi-runtime")
exit(module_passthrough(BYEDPI_RUNTIME_UC, [ "check" ]));
else if (mode == "check-adguard-runtime")
exit(module_passthrough(ADGUARD_RUNTIME_UC, [ "check" ]));
else if (mode == "check-tor-runtime") {
print(sprintf("%J", { success: true, data: { tor_installed: 0 } }));
exit(0);
@@ -5461,6 +5475,8 @@ else if (mode == "get-fptn-status")
exit(module_passthrough(FPTN_RUNTIME_UC, [ "status" ]));
else if (mode == "get-tailscale-status")
exit(module_passthrough(TAILSCALE_RUNTIME_UC, [ "status" ]));
else if (mode == "get-adguard-status")
exit(module_passthrough(ADGUARD_RUNTIME_UC, [ "status" ]));
else if (mode == "get-tailscale-peers")
exit(module_passthrough(TAILSCALE_RUNTIME_UC, [ "peers" ]));
else if (mode == "get-system-info")
+5 -2
View File
@@ -1114,7 +1114,7 @@ function write_server_capabilities_json(sing_box_extended, sing_box_tiny, sing_b
});
}
function write_ui_capabilities_json(sing_box_extended, sing_box_tiny, sing_box_compressed, sing_box_lx, sing_box_tailscale, zapret_installed, zapret2_installed, byedpi_installed, server_inbounds_enabled_count) {
function write_ui_capabilities_json(sing_box_extended, sing_box_tiny, sing_box_compressed, sing_box_lx, sing_box_tailscale, zapret_installed, zapret2_installed, byedpi_installed, server_inbounds_enabled_count, adguard_installed) {
write_json({
sing_box_extended: arg_number(sing_box_extended),
sing_box_tiny: arg_number(sing_box_tiny),
@@ -1124,6 +1124,7 @@ function write_ui_capabilities_json(sing_box_extended, sing_box_tiny, sing_box_c
zapret_installed: arg_number(zapret_installed),
zapret2_installed: arg_number(zapret2_installed),
byedpi_installed: arg_number(byedpi_installed),
adguard_installed: arg_number(adguard_installed),
server_inbounds_enabled_count: arg_number(server_inbounds_enabled_count)
});
}
@@ -1377,6 +1378,8 @@ function render_global_system_info() {
print_line("\ud83e\uddf5 Zapret2: " + zapret2_version);
if (flag_is_one(value.byedpi_installed))
print_line("\ud83e\uddf5 ByeDPI: " + byedpi_version);
if (flag_is_one(value.adguard_installed))
print_line("\ud83e\uddf5 AdGuard Home: " + as_string(object_value(value, "adguard_version") || "unknown"));
print_line("\ud83d\udedc OpenWrt: " + openwrt_version);
print_line("\ud83d\udedc Device: " + device_model);
}
@@ -1931,7 +1934,7 @@ else if (mode == "inbounds-check-json")
else if (mode == "server-capabilities-json")
write_server_capabilities_json(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "ui-capabilities-json")
write_ui_capabilities_json(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6], ARGV[7], ARGV[8], ARGV[9]);
write_ui_capabilities_json(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6], ARGV[7], ARGV[8], ARGV[9], ARGV[10]);
else if (mode == "service-status-json")
write_service_status_json(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
else if (mode == "service-status-running")
+54 -6
View File
@@ -5,6 +5,7 @@ let uci = require("core.uci");
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
const SB_DNS_INBOUND_ADDRESS = getenv("SB_DNS_INBOUND_ADDRESS") || "127.0.0.42";
const ADGUARD_LOCAL_SERVER = getenv("ADGUARD_LOCAL_SERVER") || "127.0.0.1#5354";
let common = require("core.common");
let as_string = common.as_string;
let shell_quote = common.shell_quote;
@@ -101,7 +102,31 @@ function dnsmasq_default_servers() {
}
function dnsmasq_default_has_tachyon_dns() {
return list_has(dnsmasq_default_servers(), SB_DNS_INBOUND_ADDRESS);
return list_has(dnsmasq_default_servers(), SB_DNS_INBOUND_ADDRESS) ||
list_has(dnsmasq_default_servers(), ADGUARD_LOCAL_SERVER);
}
function adguard_provider_enabled() {
return truthy(uci_get(CONFIG_NAME + ".adguard.enabled"));
}
function adguard_main_dns_mode() {
if (!adguard_provider_enabled())
return "dnsmasq";
return uci_get(CONFIG_NAME + ".settings.dns_main_mode") == "adguard" ? "adguard" : "dnsmasq";
}
function tachyon_dns_server_target() {
if (adguard_provider_enabled() && adguard_main_dns_mode() == "dnsmasq")
return ADGUARD_LOCAL_SERVER;
return SB_DNS_INBOUND_ADDRESS;
}
function is_tachyon_dns_server(server) {
server = as_string(server);
return server == SB_DNS_INBOUND_ADDRESS ||
starts_with(server, SB_DNS_INBOUND_ADDRESS + "#") ||
server == ADGUARD_LOCAL_SERVER;
}
function dnsmasq_has_tachyon_dns() {
@@ -129,9 +154,14 @@ function dnsmasq_target_cachesize() {
}
function dnsmasq_default_config_is_complete() {
let adguard_mode = adguard_provider_enabled() && adguard_main_dns_mode() == "adguard";
let port_ok = adguard_mode
? uci_get("dhcp.@dnsmasq[0].port") == "0"
: uci_get("dhcp.@dnsmasq[0].port") != "0";
return dnsmasq_default_has_tachyon_dns() &&
uci_get("dhcp.@dnsmasq[0].noresolv") == "1" &&
uci_get("dhcp.@dnsmasq[0].cachesize") == dnsmasq_target_cachesize() &&
port_ok &&
!dnsmasq_legacy_instance_exists();
}
@@ -160,7 +190,7 @@ function backup_dnsmasq_server_list() {
return;
for (let server in words(dnsmasq_default_servers())) {
if (server != SB_DNS_INBOUND_ADDRESS && !starts_with(server, SB_DNS_INBOUND_ADDRESS + "#"))
if (!is_tachyon_dns_server(server))
uci_add_list("dhcp.@dnsmasq[0].tachyon_server", server);
}
}
@@ -210,20 +240,32 @@ function dnsmasq_configure_default_instance() {
backup_dnsmasq_config_option("noresolv", "tachyon_noresolv");
backup_dnsmasq_config_option("cachesize", "tachyon_cachesize");
}
backup_dnsmasq_config_option("port", "tachyon_port");
backup_dnsmasq_config_option("addn_hosts", "tachyon_addn_hosts");
uci_delete("dhcp.@dnsmasq[0].addn_hosts");
if (adguard_provider_enabled() && adguard_main_dns_mode() == "adguard") {
// AdGuard Home serves DNS on :53; dnsmasq keeps DHCP only.
uci_delete("dhcp.@dnsmasq[0].server");
uci_set("dhcp.@dnsmasq[0].port", "0");
uci_set("dhcp.@dnsmasq[0].noresolv", "1");
uci_set("dhcp.@dnsmasq[0].cachesize", "0");
return;
}
uci_delete("dhcp.@dnsmasq[0].server");
uci_add_list("dhcp.@dnsmasq[0].server", SB_DNS_INBOUND_ADDRESS);
uci_add_list("dhcp.@dnsmasq[0].server", tachyon_dns_server_target());
uci_set("dhcp.@dnsmasq[0].noresolv", "1");
uci_set("dhcp.@dnsmasq[0].cachesize", dnsmasq_target_cachesize());
if (uci_get("dhcp.@dnsmasq[0].port") == "0")
uci_delete("dhcp.@dnsmasq[0].port");
}
function dnsmasq_restore_default_instance() {
let server_list = dnsmasq_default_servers();
let backup_servers = uci_get("dhcp.@dnsmasq[0].tachyon_server");
let managed_global_dns = list_has(server_list, SB_DNS_INBOUND_ADDRESS);
let managed_global_dns = list_has(server_list, SB_DNS_INBOUND_ADDRESS) || list_has(server_list, ADGUARD_LOCAL_SERVER);
uci_delete("dhcp.@dnsmasq[0].server");
if (backup_servers != "") {
@@ -233,12 +275,18 @@ function dnsmasq_restore_default_instance() {
}
else {
for (let value in words(server_list)) {
if (value != SB_DNS_INBOUND_ADDRESS && !starts_with(value, SB_DNS_INBOUND_ADDRESS + "#"))
if (!is_tachyon_dns_server(value))
uci_add_list("dhcp.@dnsmasq[0].server", value);
}
}
uci_delete("dhcp.@dnsmasq[0].tachyon_server");
let port = uci_get("dhcp.@dnsmasq[0].tachyon_port");
if (port != "")
restore_dnsmasq_config_option("port", "tachyon_port", "53");
else if (uci_get("dhcp.@dnsmasq[0].port") == "0")
uci_delete("dhcp.@dnsmasq[0].port");
let noresolv = uci_get("dhcp.@dnsmasq[0].tachyon_noresolv");
if (noresolv != "")
restore_dnsmasq_config_option("noresolv", "tachyon_noresolv", "");
@@ -302,7 +350,7 @@ function dnsmasq_configure(force) {
return true;
}
if (uci_get("dhcp.@dnsmasq[0].port") == "0" && (run("pidof AdGuardHome >/dev/null 2>&1") || run("pidof adguardhome >/dev/null 2>&1"))) {
if (!adguard_provider_enabled() && uci_get("dhcp.@dnsmasq[0].port") == "0" && (run("pidof AdGuardHome >/dev/null 2>&1") || run("pidof adguardhome >/dev/null 2>&1"))) {
log("dnsmasq configuration skipped: AdGuardHome is active and dnsmasq DNS is disabled (port 0)", "info");
return true;
}
@@ -0,0 +1,454 @@
#!/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);
}
@@ -0,0 +1,113 @@
#!/usr/bin/env ucode
let common = require("core.common");
let as_string = common.as_string;
function write_json(value) {
print(sprintf("%J", value), "\n");
}
function words(value) {
value = replace(as_string(value), /[\t\r\n]/g, " ");
value = replace(value, / +/g, " ");
value = replace(value, /^ /, "");
value = replace(value, / $/, "");
return value == "" ? [] : split(value, " ");
}
function contains(values, needle) {
for (let value in values)
if (as_string(value) == as_string(needle))
return true;
return false;
}
function valid_port(value) {
value = as_string(value);
return value != "" && match(value, /^[0-9]+$/) != null &&
int(value) >= 1 && int(value) <= 65535;
}
// Upstream DNS accepted forms:
// - plain IP[:port] e.g. 1.1.1.1, 8.8.8.8:53
// - https://host[/dns-query] DoH
// - tls://host[:port] DoT
// - quic://host[:port] DoQ
// - udp://host[:port] explicit plain
function validate_upstream(value) {
value = trim(as_string(value));
if (value == "")
return { valid: false, message: "Empty DNS server" };
if (match(value, /^https:\/\//) != null)
return match(value, /^https:\/\/[^\s\/]+\//) != null || match(value, /^https:\/\/[^\s\/]+$/) != null
? { valid: true, message: "" }
: { valid: false, message: "Invalid DoH URL: " + value };
if (match(value, /^tls:\/\//) != null || match(value, /^quic:\/\//) != null)
return { valid: true, message: "" };
if (match(value, /^udp:\/\//) != null)
value = substr(value, 6);
let hostport = value;
let colon = rindex(hostport, ":");
let host = "";
let port = "";
if (colon >= 0 && index(hostport, "]") < 0) {
host = substr(hostport, 0, colon);
port = substr(hostport, colon + 1);
} else {
host = hostport;
}
if (host == "")
return { valid: false, message: "Empty DNS host" };
if (port != "" && !valid_port(port))
return { valid: false, message: "Invalid DNS port in: " + value };
return { valid: true, message: "" };
}
function validate_upstreams(values) {
for (let value in (type(values) == "array" ? values : [])) {
let result = validate_upstream(value);
if (!result.valid)
return result;
}
return { valid: true, message: "" };
}
function dns_main_mode_valid(mode) {
mode = as_string(mode);
return mode == "dnsmasq" || mode == "adguard";
}
function print_dns_main_mode_valid(mode) {
exit(dns_main_mode_valid(mode) ? 0 : 1);
}
function validate_filter_url(value) {
value = trim(as_string(value));
if (value == "")
return { valid: false, message: "Empty filter list URL" };
return match(value, /^https?:\/\//) != null
? { valid: true, message: "" }
: { valid: false, message: "Filter list must be an http(s) URL: " + value };
}
let mode = ARGV[0] || "";
if (mode == "validate-upstream")
write_json(validate_upstream(ARGV[1]));
else if (mode == "validate-upstreams")
write_json(validate_upstreams(ARGV[1] != null ? [ ARGV[1] ] : []));
else if (mode == "validate-filter-url")
write_json(validate_filter_url(ARGV[1]));
else if (mode == "dns-main-mode-valid")
print_dns_main_mode_valid(ARGV[1]);
else if (mode == "valid-port")
exit(valid_port(ARGV[1]) ? 0 : 1);
else {
warn("Usage: providers/adguard/validator.uc <validate-upstream|validate-filter-url|dns-main-mode-valid|valid-port> ...\n");
exit(1);
}
@@ -123,6 +123,7 @@ const TAILSCALE_UC = LIB_DIR + "/providers/tailscale/runtime.uc";
const WDTT_UC = LIB_DIR + "/providers/wdtt/runtime.uc";
const OLCRTC_UC = LIB_DIR + "/providers/olcrtc/runtime.uc";
const FPTN_UC = LIB_DIR + "/providers/fptn/runtime.uc";
const ADGUARD_UC = LIB_DIR + "/providers/adguard/runtime.uc";
const PARENTAL_QUOTA_UC = LIB_DIR + "/service/parental_quota.uc";
const PACKAGES_UC = LIB_DIR + "/core/packages.uc";
const WATCHDOG_UC = LIB_DIR + "/service/watchdog.uc";
@@ -994,6 +995,10 @@ function start_impl() {
if (status != 0)
return status;
// AdGuard Home must be up before dnsmasq is pointed at it (dnsmasq mode)
// or before dnsmasq DNS is disabled in favour of AdGuard (adguard mode).
module_success(ADGUARD_UC, [ "start-runtime" ]);
if (!setting_bool("dont_touch_dhcp", false)) {
status = dnsmasq_configure(false);
if (status != 0)
@@ -1060,6 +1065,8 @@ function stop_main() {
log_message("FPTN stop failed (non-fatal)", "warn");
if (!module_success(TAILSCALE_UC, [ "stop-runtime" ]))
log_message("Tailscale stop failed (non-fatal)", "warn");
if (!module_success(ADGUARD_UC, [ "stop-runtime" ]))
log_message("AdGuard Home stop failed (non-fatal)", "warn");
module_success(PARENTAL_QUOTA_UC, [ "remove-cron" ]);
module_success(TELEGRAM_UC, [ "stop-runtime" ]);
@@ -1401,14 +1401,27 @@ function add_dns_server_for_section(config, section) {
let detour = dns_action_detour_tag(section);
let server_tags = [];
if (dns_type == "adguard") {
let upstreams = adguard_provider_upstreams();
if (length(upstreams) == 0)
ctx.runtime_generate_unsupported("dns_type 'adguard' requires upstream DNS servers configured in the AdGuard Home provider (config provider 'adguard')");
servers = upstreams;
dns_type = "udp";
}
for (let i = 0; i < length(servers); i++) {
let s_val = servers[i];
let entry_dns_type = dns_type;
if (type(s_val) == "object") {
entry_dns_type = s_val.dns_type;
s_val = s_val.value;
}
let tag_name = length(servers) <= 1
? dns_action_server_tag(section_name)
: dns_action_server_tag(section_name, i + 1);
let server = runtime_dns.server_from_options(
tag_name,
dns_type,
entry_dns_type,
s_val,
detour
);
@@ -1420,6 +1433,29 @@ function add_dns_server_for_section(config, section) {
return server_tags;
}
function adguard_provider_upstreams() {
let result = [];
let settings = object_or_empty(ctx.uci_cursor().get_all(CONFIG_NAME, "adguard"));
for (let value in list_option(settings, "upstream_dns")) {
value = trim(as_string(value));
if (value == "")
continue;
let dns_type = "udp";
if (match(value, /^https:\/\//) != null)
dns_type = "doh";
else if (match(value, /^tls:\/\//) != null)
dns_type = "dot";
else if (match(value, /^quic:\/\//) != null)
dns_type = "doq";
else if (match(value, /^udp:\/\//) != null) {
dns_type = "udp";
value = substr(value, 6);
}
push(result, { value, dns_type });
}
return result;
}
function add_routed_dns_server_for_section(config, section) {
if (!connections.routed_dns_enabled(section))
return;
@@ -1430,14 +1466,30 @@ function add_routed_dns_server_for_section(config, section) {
let tag_name = routed_dns_server_tag(section_name);
let detour = action == "bypass" ? null : outbound_tag(section_name);
if (connections.routed_dns_adguard(section)) {
// "adguard" resolves the section's domains through the DNS servers
// configured in the AdGuard Home provider, keeping caching and the
// user's central DoH/DoT/plain choice without an upstream loop.
let upstreams = adguard_provider_upstreams();
if (length(upstreams) == 0)
ctx.runtime_generate_unsupported("routed DNS type 'adguard' requires upstream DNS servers configured in the AdGuard Home provider (config provider 'adguard')");
servers = upstreams;
dns_type = "udp";
}
for (let i = 0; i < length(servers); i++) {
let s_val = servers[i];
let entry_dns_type = dns_type;
if (type(s_val) == "object") {
entry_dns_type = s_val.dns_type;
s_val = s_val.value;
}
let server_tag = length(servers) <= 1
? tag_name
: runtime_constants.tag(section_name, "routed-dns-server-" + (i + 1));
let server = runtime_dns.server_from_options(
server_tag,
dns_type,
entry_dns_type,
s_val,
detour
);
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
RUNTIME="$ROOT_DIR/tachyon/files/usr/lib/providers/adguard/runtime.uc"
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
# The binary is absent in the test environment, so "installed" must be false.
if ucode -L "$TACHYON_LIB" -- "$RUNTIME" installed >/dev/null 2>&1; then
fail "installed should be false without an AdGuardHome binary"
fi
if ucode -L "$TACHYON_LIB" -- "$RUNTIME" provider-available >/dev/null 2>&1; then
fail "provider-available should be false without an AdGuardHome binary"
fi
# package-version of a missing binary must be empty.
ver="$(ucode -L "$TACHYON_LIB" -- "$RUNTIME" package-version)"
[ -z "$ver" ] || fail "package-version should be empty when not installed, got '$ver'"
# Without UCI the provider section is absent -> disabled.
if ucode -L "$TACHYON_LIB" -- "$RUNTIME" provider-enabled >/dev/null 2>&1; then
fail "provider-enabled should be false when the adguard provider section is absent"
fi
# Default DNS main mode is dnsmasq.
mode="$(ucode -L "$TACHYON_LIB" -- "$RUNTIME" dns-main-mode)"
[ "$mode" = "dnsmasq" ] || fail "default dns_main_mode should be dnsmasq, got '$mode'"
# Default listen port in dnsmasq mode is the local 5354 port.
port="$(ucode -L "$TACHYON_LIB" -- "$RUNTIME" dns-listen-port)"
[ "$port" = "5354" ] || fail "dnsmasq mode listen port should be 5354, got '$port'"
host="$(ucode -L "$TACHYON_LIB" -- "$RUNTIME" dns-listen-host)"
[ "$host" = "127.0.0.1" ] || fail "dnsmasq mode listen host should be 127.0.0.1, got '$host'"
status="$(ucode -L "$TACHYON_LIB" -- "$RUNTIME" status)"
JSON_VALUE="$status" node - <<'NODE'
const value = JSON.parse(process.env.JSON_VALUE);
if (String(value.installed) !== 'false') process.exit(1);
if (String(value.enabled) !== 'false') process.exit(1);
if (String(value.dns_main_mode) !== 'dnsmasq') process.exit(1);
NODE
check="$(ucode -L "$TACHYON_LIB" -- "$RUNTIME" check)"
JSON_VALUE="$check" node - <<'NODE'
const value = JSON.parse(process.env.JSON_VALUE);
if (String(value.adguard_installed) !== 'false') process.exit(1);
if (value.adguard_provider_path !== '/usr/bin/AdGuardHome') process.exit(1);
NODE
printf 'OK: adguard runtime tests passed\n'
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
VALIDATOR="$ROOT_DIR/tachyon/files/usr/lib/providers/adguard/validator.uc"
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_json_field() {
local json="$1"
local field="$2"
local expected="$3"
JSON_VALUE="$json" node - "$field" "$expected" <<'NODE'
const field = process.argv[2];
const expected = process.argv[3];
const value = JSON.parse(process.env.JSON_VALUE);
const actual = String(value[field]);
if (actual !== expected) {
console.error(`expected ${field}=${expected}, got ${actual}`);
process.exit(1);
}
NODE
}
doh_json="$(ucode -L "$TACHYON_LIB" -- "$VALIDATOR" validate-upstream 'https://dns.google/dns-query')"
assert_json_field "$doh_json" valid true
dot_json="$(ucode -L "$TACHYON_LIB" -- "$VALIDATOR" validate-upstream 'tls://1.1.1.1')"
assert_json_field "$dot_json" valid true
plain_json="$(ucode -L "$TACHYON_LIB" -- "$VALIDATOR" validate-upstream '8.8.8.8')"
assert_json_field "$plain_json" valid true
udp_json="$(ucode -L "$TACHYON_LIB" -- "$VALIDATOR" validate-upstream 'udp://1.1.1.1:53')"
assert_json_field "$udp_json" valid true
bad_doh_json="$(ucode -L "$TACHYON_LIB" -- "$VALIDATOR" validate-upstream 'http://not-tls.example')"
assert_json_field "$bad_doh_json" valid false
empty_json="$(ucode -L "$TACHYON_LIB" -- "$VALIDATOR" validate-upstream '')"
assert_json_field "$empty_json" valid false
bad_port_json="$(ucode -L "$TACHYON_LIB" -- "$VALIDATOR" validate-upstream '1.1.1.1:99999')"
assert_json_field "$bad_port_json" valid false
if ucode -L "$TACHYON_LIB" -- "$VALIDATOR" dns-main-mode-valid 'dnsmasq' >/dev/null 2>&1; then
:
else
fail "dnsmasq should be a valid dns_main_mode"
fi
if ucode -L "$TACHYON_LIB" -- "$VALIDATOR" dns-main-mode-valid 'adguard' >/dev/null 2>&1; then
:
else
fail "adguard should be a valid dns_main_mode"
fi
if ucode -L "$TACHYON_LIB" -- "$VALIDATOR" dns-main-mode-valid 'bogus' >/dev/null 2>&1; then
fail "bogus dns_main_mode should be rejected"
fi
if ucode -L "$TACHYON_LIB" -- "$VALIDATOR" valid-port '3000' >/dev/null 2>&1; then
:
else
fail "3000 should be a valid port"
fi
if ucode -L "$TACHYON_LIB" -- "$VALIDATOR" valid-port 'notaport' >/dev/null 2>&1; then
fail "non-numeric port should be rejected"
fi
filter_json="$(ucode -L "$TACHYON_LIB" -- "$VALIDATOR" validate-filter-url 'https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt')"
assert_json_field "$filter_json" valid true
bad_filter_json="$(ucode -L "$TACHYON_LIB" -- "$VALIDATOR" validate-filter-url 'ftp://filter')"
assert_json_field "$bad_filter_json" valid false
printf 'OK: adguard validator tests passed\n'