diff --git a/luci-app-adguardhome/.github/workflows/build-apk.yaml b/luci-app-adguardhome/.github/workflows/build-apk.yaml
new file mode 100644
index 00000000..64b83777
--- /dev/null
+++ b/luci-app-adguardhome/.github/workflows/build-apk.yaml
@@ -0,0 +1,70 @@
+name: build-apk
+
+on:
+ push:
+ tags:
+ - 'v*'
+ - 'dev-*'
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+env:
+ SDK_DIR: /builder
+ PKG_DIR_NAME: luci-app-adguardhome-tk
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 90
+ strategy:
+ matrix:
+ sdk: [x86-64-25.12.6]
+ container:
+ image: ghcr.io/openwrt/sdk:${{ matrix.sdk }}
+ options: --user root
+ defaults:
+ run:
+ shell: bash
+
+ steps:
+ - name: 检出代码
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+
+ - name: 缓存依赖源码下载
+ uses: actions/cache@v5
+ with:
+ path: ${{ env.SDK_DIR }}/dl
+ key: adguardhome-dl-v2-${{ runner.os }}-${{ matrix.sdk }}-${{ hashFiles('Makefile', '.github/workflows/build-apk.yaml', 'scripts/*.sh') }}
+ restore-keys: |
+ adguardhome-dl-v2-${{ runner.os }}-${{ matrix.sdk }}-
+
+ - name: 准备 SDK
+ run: bash scripts/prepare-sdk.sh
+
+ - name: 构建包
+ run: runuser -u buildbot -- bash scripts/build-apk.sh
+
+ - name: 整理 APK 产物
+ run: bash scripts/collect-apks.sh
+
+ - name: 上传 APK 压缩包
+ uses: actions/upload-artifact@v7
+ with:
+ name: adguardhome-apks
+ path: dist/*.apk
+ archive: true
+ if-no-files-found: error
+
+ - name: 发布到 Releases
+ if: startsWith(github.ref, 'refs/tags/')
+ uses: softprops/action-gh-release@v3
+ with:
+ tag_name: ${{ github.ref_name }}
+ prerelease: ${{ startsWith(github.ref_name, 'dev-') }}
+ make_latest: 'false'
+ fail_on_unmatched_files: true
+ files: dist/*.apk
diff --git a/luci-app-adguardhome/.gitignore b/luci-app-adguardhome/.gitignore
index 10d5c0e3..05dfba47 100644
--- a/luci-app-adguardhome/.gitignore
+++ b/luci-app-adguardhome/.gitignore
@@ -1,9 +1,2 @@
.DS_Store
-
-# Local debug artifacts from playwright-cli UI verification
-adg-*.png
-m-*.png
-bs-*.png
-*.png
-!screenshots/*.png
-.playwright-cli/
+.local
\ No newline at end of file
diff --git a/luci-app-adguardhome/Makefile b/luci-app-adguardhome/Makefile
index a3d2ffc4..4eb97f6f 100644
--- a/luci-app-adguardhome/Makefile
+++ b/luci-app-adguardhome/Makefile
@@ -1,79 +1,109 @@
-# Copyright (C) 2018-2024 OpenWrt luci-app-adguardhome contributors
#
-# This is free software, licensed under the Apache License, Version 2.0 .
+# Copyright (C) 2025 w9315273
#
include $(TOPDIR)/rules.mk
-PKG_NAME:=luci-app-adguardhome
-PKG_MAINTAINER:=kenzok8
-PKG_VERSION:=1.0
-PKG_RELEASE:=9
+PKG_NAME:=luci-app-adguardhome-tk
+PKG_VERSION:=2.6.0
+PKG_RELEASE:=3
-LUCI_TITLE:=LuCI app for AdGuardHome
-LUCI_PKGARCH:=all
-LUCI_DEPENDS:=+ca-certs +curl +wget-ssl +PACKAGE_$(PKG_NAME)_INCLUDE_binary:adguardhome
-# luci-compat only exists in ucode-based LuCI (24.10+)
-ifneq ($(wildcard $(TOPDIR)/feeds/luci/luci-compat/Makefile),)
- LUCI_DEPENDS+= +luci-compat
-endif
-LUCI_DESCRIPTION:=LuCI support for AdGuardHome
+PKG_MAINTAINER:=w9315273
+PKG_BUILD_DEPENDS:=luci-base/host
-define Package/$(PKG_NAME)/config
-config PACKAGE_$(PKG_NAME)_INCLUDE_binary
- bool "Include Binary File"
- default n
- help
- When enabled, also pulls in the adguardhome binary package.
- Disabled by default to avoid init.d/config name conflict with
- the adguardhome package (uses lowercase init.d/adguardhome,
- while this LuCI app uses /etc/init.d/AdGuardHome). The LuCI
- app downloads the binary at runtime, so the package dependency
- is only needed for offline / sysupgrade scenarios.
+include $(INCLUDE_DIR)/package.mk
+
+define Package/luci-app-adguardhome-tk
+ SECTION:=luci
+ CATEGORY:=LuCI
+ SUBMENU:=3. Applications
+ TITLE:=LuCI app for AdGuardHome
+ PKG_MAINTAINER:=https://github.com/w9315273/luci-app-adguardhome
+ PKGARCH:=all
+ PROVIDES:=luci-app-adguardhome
+ CONFLICTS:=luci-app-adguardhome
+ DEPENDS:=+luci-base +rpcd-mod-ucode +uclient-fetch +jsonfilter +flock +ca-bundle +!(PACKAGE_libustream-openssl||PACKAGE_libustream-wolfssl):libustream-mbedtls +tar
endef
-PKG_CONFIG_DEPENDS:= CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_binary
+define Package/luci-app-adguardhome-tk/description
+ LuCI support for AdGuardHome (nftables-based DNS redirect).
+ This package does NOT include the AdGuardHome core binary.
+endef
-define Package/luci-app-adguardhome/conffiles
-/usr/share/AdGuardHome/links.txt
+define Package/luci-i18n-adguardhome-tk-zh-cn
+ SECTION:=luci
+ CATEGORY:=LuCI
+ HIDDEN:=1
+ TITLE:=AdGuardHome - Simplified Chinese translation
+ PKGARCH:=all
+ PROVIDES:=luci-i18n-adguardhome-zh-cn
+ CONFLICTS:=luci-i18n-adguardhome-zh-cn
+ DEPENDS:=luci-app-adguardhome-tk
+ DEFAULT:=LUCI_LANG_zh_Hans
+endef
+
+define Package/luci-i18n-adguardhome-tk-zh-cn/description
+ Simplified Chinese translation for luci-app-adguardhome-tk.
+endef
+
+define Build/Prepare
+endef
+
+define Build/Compile
+endef
+
+define Package/luci-app-adguardhome-tk/conffiles
/etc/config/AdGuardHome
endef
-define Package/luci-app-adguardhome/postinst
+define Package/luci-app-adguardhome-tk/install
+ cp -pR ./root/* $(1)/
+endef
+
+define Package/luci-i18n-adguardhome-tk-zh-cn/install
+ $(INSTALL_DIR) $(1)/etc/uci-defaults
+ echo "uci set luci.languages.zh_cn='简体中文 (Simplified Chinese)'; uci commit luci" \
+ > $(1)/etc/uci-defaults/luci-i18n-adguardhome-tk-zh-cn
+ $(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
+ po2lmo ./po/zh-cn/AdGuardHome.po \
+ $(1)/usr/lib/lua/luci/i18n/AdGuardHome.zh-cn.lmo
+endef
+
+define Package/luci-app-adguardhome-tk/postinst
#!/bin/sh
- # Stop and disable the lowercase /etc/init.d/adguardhome that ships
- # with the adguardhome binary package, so it can not race the LuCI
- # app's /etc/init.d/AdGuardHome (issue #254).
- if [ -x /etc/init.d/adguardhome ] && [ -x /etc/init.d/AdGuardHome ]; then
- /etc/init.d/adguardhome stop >/dev/null 2>&1
- /etc/init.d/adguardhome disable >/dev/null 2>&1
- fi
- /etc/init.d/AdGuardHome enable >/dev/null 2>&1
- enable="$(uci get AdGuardHome.AdGuardHome.enabled 2>/dev/null)"
- [ "$enable" = "1" ] && /etc/init.d/AdGuardHome reload >/dev/null 2>&1
- rm -f /tmp/luci-indexcache
- rm -f /tmp/luci-modulecache/* 2>/dev/null
+[ -n "$${IPKG_INSTROOT}" ] && exit 0
+
+/etc/init.d/AdGuardHome enable >/dev/null 2>&1
+
+enable=$$(uci get AdGuardHome.AdGuardHome.enabled 2>/dev/null)
+if [ "$$enable" = "1" ]; then
+ /etc/init.d/AdGuardHome reload
+fi
+
+# Remove the former menu definition when upgrading.
+rm -f /usr/share/luci/menu.d/luci-app-adguardhome-js.json
+rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
+rm -f /tmp/luci-modulecache/*
+/etc/init.d/rpcd reload
+
exit 0
endef
-define Package/luci-app-adguardhome/prerm
+define Package/luci-app-adguardhome-tk/prerm
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
- /etc/init.d/AdGuardHome disable >/dev/null 2>&1
- /etc/init.d/AdGuardHome stop >/dev/null 2>&1
- ucitrack_del() {
- local name="$1"
- local track="$(uci show ucitrack 2>/dev/null | grep -F "$name\." | head -1)"
- [ -n "$track" ] && uci -q del "$track" 2>/dev/null
- }
- ucitrack_del "AdGuardHome"
- [ -n "$(uci show ucitrack 2>/dev/null | grep -c 'AdGuardHome')" ] || true
- uci commit ucitrack 2>/dev/null
+ /etc/init.d/AdGuardHome disable
+ /etc/init.d/AdGuardHome stop 2>/dev/null || true
+
+ uci -q batch <<-EOF >/dev/null 2>&1
+ delete ucitrack.@AdGuardHome[-1]
+ commit ucitrack
+EOF
fi
+
exit 0
endef
-include $(TOPDIR)/feeds/luci/luci.mk
+$(eval $(call BuildPackage,luci-app-adguardhome-tk))
-# call BuildPackage - OpenWrt buildroot signature
-$(eval $(call BuildPackage,luci-app-adguardhome))
+$(eval $(call BuildPackage,luci-i18n-adguardhome-tk-zh-cn))
diff --git a/luci-app-adguardhome/README.md b/luci-app-adguardhome/README.md
deleted file mode 100644
index ecdba15b..00000000
--- a/luci-app-adguardhome/README.md
+++ /dev/null
@@ -1,81 +0,0 @@
-# luci-app-adguardhome
-
-适用于 OpenWrt / ImmortalWrt 的 AdGuard Home LuCI 控制面板。
-
-> 基于 [rufengsuixing/luci-app-adguardhome](https://github.com/rufengsuixing/luci-app-adguardhome) 维护,
-> 针对 **ImmortalWrt 24.10+** 进行了大量 UI 重构和 Bug 修复。
-
-## 兼容性
-
-| 系统 | 状态 |
-|------|------|
-| ImmortalWrt 24.10 / 25.12 | 主要测试目标,完全兼容 |
-| OpenWrt 23.05 | 兼容 |
-| OpenWrt 21.02 / 22.03 | 兼容 |
-| OpenWrt 19.07 | 兼容(jail 字段回退为 TextValue) |
-| OpenWrt 18.06 | 基本兼容(DynamicList 自动回退) |
-
-## 截图
-
-### 概览
-
-
-
-### 基础设置
-
-
-
-### 运维
-
-
-
-### 日志
-
-
-
-### 手动配置
-
-
-
-## 功能
-
-- **概览面板**:服务状态、重定向模式、核心版本、一键启停 + Web 控制台快捷入口
-- **基础设置**:启用开关、管理端口、核心更新、5553 重定向(4 种模式)、路径配置、进程参数
-- **工具与任务**:改密、sysupgrade 保留文件、关机备份、Crontab 计划任务
-- **日志查看**:实时轮询、反向/本地时区切换、下载、清空
-- **手动配置**:YAML 编辑器(CodeMirror 可选),保存前二次确认
-- **核心自动更新**:GitHub/AdGuard 多源下载 → UPX 压缩 → 重启服务
-- **UPX 压缩**:>8MB 二进制自动压缩(支持 x86_64 / arm64 / mips 等架构)
-- **计划任务**:自动更新核心、切割日志、IPv6 hosts 同步
-- **升级保护**:sysupgrade 保留文件 + 关机备份
-
-## 安装
-
-### Feed 方式
-
-```bash
-echo 'src-git adguardhome https://github.com/kenzok78/luci-app-adguardhome' >> feeds.conf.default
-./scripts/feeds update -a && ./scripts/feeds install -a -p adguardhome
-make package/luci-app-adguardhome/compile V=s
-```
-
-### 直接编译
-
-```bash
-git clone https://github.com/kenzok78/luci-app-adguardhome.git package/luci-app-adguardhome
-make menuconfig # LuCI → Applications → luci-app-adguardhome
-make -j$(nproc) V=s
-```
-
-## 重定向模式
-
-| 模式 | 说明 |
-|------|------|
-| 不启用 | 仅运行,不拦截 DNS |
-| 作为 dnsmasq 上游 | dnsmasq 转发到 AGH |
-| 53 端口劫持 | iptables 重定向到 AGH |
-| 替代 dnsmasq | AGH 直接监听 53 |
-
-## 许可证
-
-Apache-2.0
diff --git a/luci-app-adguardhome/docs/README.en.md b/luci-app-adguardhome/docs/README.en.md
new file mode 100644
index 00000000..1dc3727a
--- /dev/null
+++ b/luci-app-adguardhome/docs/README.en.md
@@ -0,0 +1,35 @@
+# AdGuard Home for LuCI
+
+Manage AdGuard Home in OpenWrt's LuCI interface. LuCI pages are rewritten in JavaScript, with an rpcd + ucode backend and nftables-based DNS forwarding.
+
+[简体中文](README.md) · [English](README.en.md)
+
+## Features
+
+- Service status and core updates
+- DNS handling through firewall forwarding, dnsmasq upstream, or port 53 replacement
+- YAML configuration editing, validation, and web login password changes
+- Runtime logs with reverse order, local timestamps, download, and a verbose logging toggle
+
+> [!TIP]
+> **Quick installation**
+>
+> Run in the terminal of an OpenWrt router using the APK package manager:
+>
+> ```sh
+> uclient-fetch -O /tmp/install-adguardhome.sh https://raw.githubusercontent.com/w9315273/luci-app-adguardhome/main/scripts/install.sh && sh /tmp/install-adguardhome.sh
+> ```
+
+## Screenshots
+
+### Basic settings
+
+
+
+### Manual configuration
+
+
+
+### Logs
+
+
diff --git a/luci-app-adguardhome/docs/README.md b/luci-app-adguardhome/docs/README.md
new file mode 100644
index 00000000..ae6ad614
--- /dev/null
+++ b/luci-app-adguardhome/docs/README.md
@@ -0,0 +1,35 @@
+# AdGuard Home for LuCI
+
+在 OpenWrt 的 LuCI 界面中管理 AdGuard Home。使用 JavaScript 重写 LuCI 页面,后端采用 rpcd + ucode,并适配 nftables DNS 转发。
+
+[简体中文](README.md) · [English](README.en.md)
+
+## 功能
+
+- 服务状态查看与核心更新
+- 防火墙转发、dnsmasq 上游、替代 dnsmasq 使用 53 端口等 DNS 接入方式
+- YAML 配置编辑与校验、网页登录密码修改
+- 运行日志查看,支持逆序、本地时间、下载和详细日志开关
+
+> [!TIP]
+> **一键安装**
+>
+> 在使用 APK 包管理器的 OpenWrt 路由器终端执行:
+>
+> ```sh
+> uclient-fetch -O /tmp/install-adguardhome.sh https://raw.githubusercontent.com/w9315273/luci-app-adguardhome/main/scripts/install.sh && sh /tmp/install-adguardhome.sh zh-cn
+> ```
+
+## 截图
+
+### 基础设置
+
+
+
+### 手动设置
+
+
+
+### 日志
+
+
diff --git a/luci-app-adguardhome/docs/screenshots/base-en.png b/luci-app-adguardhome/docs/screenshots/base-en.png
new file mode 100644
index 00000000..6e67b273
Binary files /dev/null and b/luci-app-adguardhome/docs/screenshots/base-en.png differ
diff --git a/luci-app-adguardhome/docs/screenshots/base-zh-cn.png b/luci-app-adguardhome/docs/screenshots/base-zh-cn.png
new file mode 100644
index 00000000..221291f6
Binary files /dev/null and b/luci-app-adguardhome/docs/screenshots/base-zh-cn.png differ
diff --git a/luci-app-adguardhome/docs/screenshots/log-en.png b/luci-app-adguardhome/docs/screenshots/log-en.png
new file mode 100644
index 00000000..554ae409
Binary files /dev/null and b/luci-app-adguardhome/docs/screenshots/log-en.png differ
diff --git a/luci-app-adguardhome/docs/screenshots/log-zh-cn.png b/luci-app-adguardhome/docs/screenshots/log-zh-cn.png
new file mode 100644
index 00000000..c16c2aa7
Binary files /dev/null and b/luci-app-adguardhome/docs/screenshots/log-zh-cn.png differ
diff --git a/luci-app-adguardhome/docs/screenshots/manual-en.png b/luci-app-adguardhome/docs/screenshots/manual-en.png
new file mode 100644
index 00000000..c05878cc
Binary files /dev/null and b/luci-app-adguardhome/docs/screenshots/manual-en.png differ
diff --git a/luci-app-adguardhome/docs/screenshots/manual-zh-cn.png b/luci-app-adguardhome/docs/screenshots/manual-zh-cn.png
new file mode 100644
index 00000000..870cafef
Binary files /dev/null and b/luci-app-adguardhome/docs/screenshots/manual-zh-cn.png differ
diff --git a/luci-app-adguardhome/htdocs/luci-static/resources/adguardhome/adg.css b/luci-app-adguardhome/htdocs/luci-static/resources/adguardhome/adg.css
deleted file mode 100644
index a6ee5020..00000000
--- a/luci-app-adguardhome/htdocs/luci-static/resources/adguardhome/adg.css
+++ /dev/null
@@ -1,360 +0,0 @@
-/* luci-app-adguardhome card-style theme (inspired by luci-app-clashoo) */
-.adg-wrap {
- --adg-primary-rgb: 0, 122, 255;
- --adg-primary: rgba(var(--adg-primary-rgb), 0.9);
- --adg-card-bg: rgba(128, 128, 128, 0.04);
- --adg-card-border: rgba(128, 128, 128, 0.16);
- --adg-card-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
- --adg-card-shadow-hover: 0 6px 24px rgba(0, 0, 0, 0.07);
- --adg-radius: 12px;
- --adg-label-muted: rgba(60, 66, 76, 0.85);
- --adg-input-bg: rgba(128, 128, 128, 0.05);
- --adg-input-border: rgba(128, 128, 128, 0.2);
- --adg-ok: #2db84a;
- --adg-warn: #e0a02f;
- --adg-bad: #d05050;
-}
-
-/* Dark mode: triggered either by OS preference, or by an explicit class
- set from head.htm when the active LuCI theme renders a dark background
- regardless of OS preference (Argon mode=dark, bootstrap-dark, etc.) */
-@media (prefers-color-scheme: dark) {
- .adg-wrap {
- --adg-card-bg: rgba(255, 255, 255, 0.05);
- --adg-card-border: rgba(255, 255, 255, 0.16);
- --adg-input-bg: rgba(255, 255, 255, 0.06);
- --adg-input-border: rgba(255, 255, 255, 0.22);
- --adg-label-muted: rgba(220, 226, 232, 0.88);
- }
-}
-.adg-wrap.adg-dark {
- --adg-card-bg: rgba(255, 255, 255, 0.05);
- --adg-card-border: rgba(255, 255, 255, 0.16);
- --adg-input-bg: rgba(255, 255, 255, 0.06);
- --adg-input-border: rgba(255, 255, 255, 0.22);
- --adg-label-muted: rgba(220, 226, 232, 0.88);
-}
-/* LuCI's selected theme is authoritative. If the OS/browser prefers dark but
- Argon/bootstrap is in light mode, keep contrast tuned for a light surface. */
-.adg-wrap:not(.adg-dark) {
- --adg-card-bg: rgba(255, 255, 255, 0.45);
- --adg-card-border: rgba(50, 50, 93, 0.12);
- --adg-input-bg: rgba(255, 255, 255, 0.55);
- --adg-input-border: rgba(50, 50, 93, 0.18);
- --adg-label-muted: rgba(50, 50, 93, 0.78);
- color: #32325d;
-}
-.adg-wrap:not(.adg-dark) .cbi-value-description,
-.adg-wrap:not(.adg-dark) .cbi-section-descr,
-.adg-wrap:not(.adg-dark) .cbi-map-descr,
-.adg-wrap:not(.adg-dark) .adg-ov-card .title,
-.adg-wrap:not(.adg-dark) .adg-ov-card .sub {
- color: rgba(50, 50, 93, 0.70) !important;
- opacity: 1 !important;
-}
-.adg-wrap:not(.adg-dark) .cbi-value > .cbi-value-title,
-.adg-wrap:not(.adg-dark) .cbi-checkbox {
- color: rgba(50, 50, 93, 0.82) !important;
- opacity: 1 !important;
-}
-.adg-wrap:not(.adg-dark) .cbi-value-field input[type="text"],
-.adg-wrap:not(.adg-dark) .cbi-value-field input[type="password"],
-.adg-wrap:not(.adg-dark) .cbi-value-field input[type="number"],
-.adg-wrap:not(.adg-dark) .cbi-value-field select,
-.adg-wrap:not(.adg-dark) .cbi-value-field textarea {
- color: #32325d !important;
- -webkit-text-fill-color: #32325d;
- opacity: 1 !important;
-}
-.adg-wrap:not(.adg-dark) .cbi-value-field input:disabled,
-.adg-wrap:not(.adg-dark) .cbi-value-field select:disabled,
-.adg-wrap:not(.adg-dark) .cbi-value-field textarea:disabled {
- color: #5f6f86 !important;
- -webkit-text-fill-color: #5f6f86;
- opacity: 1 !important;
-}
-/* Some Argon dark rules set transparent input bg with !important — override
- so our inputs stay visible. */
-.adg-wrap.adg-dark .cbi-value-field input[type="text"],
-.adg-wrap.adg-dark .cbi-value-field input[type="password"],
-.adg-wrap.adg-dark .cbi-value-field input[type="number"],
-.adg-wrap.adg-dark .cbi-value-field select,
-.adg-wrap.adg-dark .cbi-value-field textarea {
- background: var(--adg-input-bg) !important;
- border-color: var(--adg-input-border) !important;
- color: #e6eaf0 !important;
-}
-/* Help text + section descriptions — Argon paints them #ccc, bootstrap-dark
- leaves them as default gray-on-dark which is also low-contrast. */
-.adg-wrap.adg-dark .cbi-value-description,
-.adg-wrap.adg-dark .cbi-section-descr,
-.adg-wrap.adg-dark .cbi-map-descr {
- color: rgba(220, 226, 232, 0.78) !important;
-}
-.adg-wrap.adg-dark .cbi-value > .cbi-value-title {
- color: var(--adg-label-muted) !important;
-}
-.adg-wrap.adg-dark .cbi-section > h3,
-.adg-wrap.adg-dark .cbi-section > legend {
- color: #e6eaf0 !important;
-}
-
-/* Cards are constrained for comfortable reading; form fills its natural width.
- Bootstrap theme has sidebar; Argon is full-width. Let the theme decide the
- outer container width — we only cap the card content. */
-.adg-wrap .cbi-section {
- max-width: 1000px;
-}
-
-/* every CBI section becomes a card */
-.adg-wrap .cbi-section {
- background: var(--adg-card-bg);
- border: 1px solid var(--adg-card-border);
- border-radius: var(--adg-radius);
- box-shadow: var(--adg-card-shadow);
- padding: 18px 22px;
- margin: 16px 0;
- transition: box-shadow .2s ease;
-}
-.adg-wrap .cbi-section:hover {
- box-shadow: var(--adg-card-shadow-hover);
-}
-
-/* section heading inside card */
-.adg-wrap .cbi-section > h3,
-.adg-wrap .cbi-section > legend {
- background: transparent !important;
- border: 0 !important;
- box-shadow: none !important;
- padding: 0 0 12px 0 !important;
- margin: 0 0 12px 0 !important;
- font-size: 15px;
- font-weight: 600;
- letter-spacing: .2px;
- border-bottom: 1px solid var(--adg-card-border) !important;
-}
-
-/* label column slimmer (default LuCI is ~280px) */
-.adg-wrap .cbi-value > .cbi-value-title {
- width: 170px;
- min-width: 170px;
- padding-right: 12px;
- color: var(--adg-label-muted);
- font-weight: 500;
-}
-
-/* inputs */
-.adg-wrap .cbi-value-field input[type="text"],
-.adg-wrap .cbi-value-field input[type="password"],
-.adg-wrap .cbi-value-field input[type="number"],
-.adg-wrap .cbi-value-field select,
-.adg-wrap .cbi-value-field textarea {
- background: var(--adg-input-bg);
- border: 1px solid var(--adg-input-border);
- border-radius: 8px;
- padding: 6px 10px;
- max-width: 420px;
- width: 100%;
- box-shadow: none;
-}
-.adg-wrap .cbi-value-field textarea {
- max-width: 420px;
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
-}
-/* long-form textareas need full width (URL lists / YAML editor) */
-.adg-wrap .cbi-value-field textarea[id$="downloadlinks"],
-.adg-wrap .cbi-value-field textarea[id$="escconf"],
-.adg-wrap .cbi-value-field textarea#cbid\.logview\.1\.conf {
- max-width: 100%;
-}
-.adg-wrap .cbi-value-field input:focus,
-.adg-wrap .cbi-value-field select:focus,
-.adg-wrap .cbi-value-field textarea:focus {
- border-color: var(--adg-primary);
- outline: none;
- box-shadow: 0 0 0 3px rgba(var(--adg-primary-rgb), 0.15);
-}
-
-/* help text */
-.adg-wrap .cbi-value-description {
- color: var(--adg-label-muted);
- font-size: 12.5px;
- margin-top: 4px;
-}
-
-/* between-row separator gone (the card itself is the boundary) */
-.adg-wrap .cbi-value {
- border-bottom: 0 !important;
- padding: 8px 0;
-}
-
-/* MultiValue checkbox groups — let items flow inline and wrap naturally */
-.adg-wrap .cbi-value-field .cbi-checkbox {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- margin-right: 16px;
- margin-bottom: 4px;
- font-size: 13px;
- white-space: nowrap;
-}
-.adg-wrap .cbi-value-field .cbi-checkbox input[type="checkbox"] {
- width: auto;
- height: auto;
- margin: 0;
- flex-shrink: 0;
-}
-
-/* DynamicList — compact row layout */
-.adg-wrap .cbi-dynlist .cbi-value-title { display: none; }
-.adg-wrap .cbi-dynlist .cbi-value { padding: 4px 0; }
-.adg-wrap .cbi-dynlist .cbi-value-field { flex: 1; max-width: 420px; }
-.adg-wrap .cbi-dynlist .cbi-value-field input[type="text"] { max-width: 100%; }
-
-/* AdGuardHome custom status row */
-.adg-status-row {
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
- margin: 6px 0 18px 0;
-}
-.adg-status-pill {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 6px 12px;
- border-radius: 999px;
- background: var(--adg-card-bg);
- border: 1px solid var(--adg-card-border);
- font-size: 13px;
- color: var(--adg-label-muted);
-}
-.adg-status-pill .dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: var(--adg-label-muted);
-}
-.adg-status-pill.ok .dot { background: var(--adg-ok); }
-.adg-status-pill.warn .dot { background: var(--adg-warn); }
-.adg-status-pill.bad .dot { background: var(--adg-bad); }
-.adg-status-pill.ok { color: var(--adg-ok); border-color: rgba(45,184,74,.3); }
-.adg-status-pill.bad { color: var(--adg-bad); border-color: rgba(208,80,80,.3); }
-
-/* CBI button color fallback — ensure visibility on themes that use
- transparent backgrounds (e.g. Argon light) */
-.adg-wrap input.cbi-button-apply,
-.adg-wrap button.cbi-button-apply {
- background: #007aff !important;
- border-color: #007aff !important;
- color: #fff !important;
-}
-.adg-wrap input.cbi-button-apply:hover,
-.adg-wrap button.cbi-button-apply:hover {
- background: #0064d2 !important;
- border-color: #0064d2 !important;
-}
-.adg-wrap input.cbi-button-remove,
-.adg-wrap button.cbi-button-remove {
- background: #d05050 !important;
- border-color: #d05050 !important;
- color: #fff !important;
-}
-.adg-wrap input.cbi-button-remove:hover,
-.adg-wrap button.cbi-button-remove:hover {
- background: #b03d3d !important;
- border-color: #b03d3d !important;
-}
-.adg-wrap input.cbi-button-save,
-.adg-wrap button.cbi-button-save {
- background: #2d7d46 !important;
- border-color: #2d7d46 !important;
- color: #fff !important;
-}
-.adg-wrap input.cbi-button-save:hover,
-.adg-wrap button.cbi-button-save:hover {
- background: #226434 !important;
- border-color: #226434 !important;
-}
-
-/* collapsible sections */
-.adg-wrap .cbi-section.adg-collapsible > h3,
-.adg-wrap .cbi-section.adg-collapsible > legend {
- cursor: pointer;
- position: relative;
- padding-right: 28px !important;
- user-select: none;
-}
-.adg-wrap .cbi-section.adg-collapsible > h3::after,
-.adg-wrap .cbi-section.adg-collapsible > legend::after {
- content: "";
- position: absolute;
- right: 4px;
- top: 50%;
- width: 8px;
- height: 8px;
- border-right: 2px solid var(--adg-label-muted);
- border-bottom: 2px solid var(--adg-label-muted);
- transform: translateY(-65%) rotate(45deg);
- transition: transform .2s ease;
-}
-.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > h3::after,
-.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > legend::after {
- transform: translateY(-30%) rotate(-45deg);
-}
-.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > .cbi-value,
-.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > div.cbi-section-node,
-.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > fieldset {
- display: none;
-}
-
-/* mobile tweaks — reset table/flex layout entirely */
-@media (max-width: 720px) {
- .adg-wrap .cbi-section {
- padding: 12px 10px;
- margin: 8px 0;
- }
- /* force flex column, kill any theme table layout */
- .adg-wrap .cbi-value {
- display: flex !important;
- flex-direction: column;
- align-items: stretch;
- border-bottom: 0 !important;
- padding: 6px 0;
- }
- .adg-wrap .cbi-value > .cbi-value-title {
- width: 100% !important;
- min-width: 0 !important;
- padding: 0 0 3px 0 !important;
- flex: none;
- text-align: left;
- }
- .adg-wrap .cbi-value > .cbi-value-field {
- width: 100% !important;
- flex: none;
- }
- .adg-wrap .cbi-value-field input[type="text"],
- .adg-wrap .cbi-value-field input[type="password"],
- .adg-wrap .cbi-value-field input[type="number"],
- .adg-wrap .cbi-value-field select {
- max-width: 100%;
- }
- .adg-wrap .cbi-value-field > div {
- width: 100%;
- }
- /* checkboxes — single column */
- .adg-wrap .cbi-value-field .cbi-checkbox {
- display: flex;
- white-space: normal;
- margin-right: 0;
- margin-bottom: 6px;
- width: 100%;
- }
- /* DynamicList — expand */
- .adg-wrap .cbi-dynlist .cbi-value-field { max-width: 100%; }
- /* hide tooltips on touch */
- .adg-wrap .cbi-tooltip-container,
- .adg-wrap .cbi-tooltip {
- display: none !important;
- }
-}
diff --git a/luci-app-adguardhome/htdocs/luci-static/resources/view/luci-app-adguardhome/index.js b/luci-app-adguardhome/htdocs/luci-static/resources/view/luci-app-adguardhome/index.js
deleted file mode 100644
index e483ee42..00000000
--- a/luci-app-adguardhome/htdocs/luci-static/resources/view/luci-app-adguardhome/index.js
+++ /dev/null
@@ -1,14 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// NOTE: luci-app-adguardhome 使用多页面 CBI 模板架构,luci 23.05+ 通过 menu.d alias 跳转至 base 子页。
-// 此 JS 文件不直接调用,保留供参考。
-'use strict';
-'require view';
-'require rpc';
-'require form';
-
-return view.extend({
- render: function() {
- // 实际界面由 luasrc/model/cbi/AdGuardHome/ 下多个 CBI 模型提供
- return E('p', {}, _('Loading…'));
- }
-});
diff --git a/luci-app-adguardhome/luasrc/controller/AdGuardHome.lua b/luci-app-adguardhome/luasrc/controller/AdGuardHome.lua
deleted file mode 100644
index 658f34c0..00000000
--- a/luci-app-adguardhome/luasrc/controller/AdGuardHome.lua
+++ /dev/null
@@ -1,274 +0,0 @@
-module("luci.controller.AdGuardHome", package.seeall)
-
-local fs = require "nixio.fs"
-local http = require "luci.http"
-local uci = require "luci.model.uci".cursor()
-
-local function service_running()
- local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
-
- if not fs.access(binpath) then
- return false
- end
-
- return luci.sys.call("/etc/init.d/AdGuardHome status >/dev/null 2>&1") == 0
-end
-
-function index()
- -- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复注册
- if not nixio.fs.access("/usr/share/luci/menu.d/luci-app-adguardhome.json") then
- local page = entry({"admin", "services", "AdGuardHome"},
- alias("admin", "services", "AdGuardHome", "overview"),
- _("AdGuard Home"), 10)
- page.dependent = true
- page.acl_depends = { "luci-app-adguardhome" }
-
- entry({"admin", "services", "AdGuardHome", "overview"},
- cbi("AdGuardHome/overview"), _("概览"), 1).leaf = true
- entry({"admin", "services", "AdGuardHome", "base"},
- cbi("AdGuardHome/base"), _("基础设置"), 2).leaf = true
- entry({"admin", "services", "AdGuardHome", "tools"},
- cbi("AdGuardHome/tools"), _("运维"), 3).leaf = true
- entry({"admin", "services", "AdGuardHome", "log"},
- form("AdGuardHome/log"), _("日志"), 4).leaf = true
- entry({"admin", "services", "AdGuardHome", "manual"},
- cbi("AdGuardHome/manual"), _("手动配置"), 5).leaf = true
- end
-
- -- API 路由在新旧版本均需注册
- entry({"admin", "services", "AdGuardHome", "status"},
- call("act_status"), nil).leaf = true
- entry({"admin", "services", "AdGuardHome", "toggle"},
- call("toggle_service"), nil).leaf = true
- entry({"admin", "services", "AdGuardHome", "check"},
- call("check_update"), nil)
- entry({"admin", "services", "AdGuardHome", "doupdate"},
- call("do_update"), nil)
- entry({"admin", "services", "AdGuardHome", "getlog"},
- call("get_log"), nil)
- entry({"admin", "services", "AdGuardHome", "dodellog"},
- call("do_dellog"), nil)
- entry({"admin", "services", "AdGuardHome", "reloadconfig"},
- call("reload_config"), nil)
- entry({"admin", "services", "AdGuardHome", "gettemplateconfig"},
- call("get_template_config"), nil)
-end
-
-local function gen_template_config()
- local dns_servers = ""
- local resolv_auto = "/tmp/resolv.conf.d/resolv.conf.auto"
- if fs.access(resolv_auto) then
- for line in io.lines(resolv_auto) do
- local ns = line:match("^[^#]*nameserver%s+([^%s]+)")
- if ns then
- dns_servers = dns_servers .. " - " .. ns .. "\n"
- end
- end
- end
-
- local tmpl = "/usr/share/AdGuardHome/AdGuardHome_template.yaml"
- local f = io.open(tmpl, "r")
- if not f then return "" end
-
- local lines = {}
- for line in f:lines() do
- if line == "#bootstrap_dns" then
- table.insert(lines, dns_servers)
- elseif line == "#upstream_dns" then
- table.insert(lines, dns_servers)
- else
- table.insert(lines, line)
- end
- end
- f:close()
- return table.concat(lines, "\n")
-end
-
-function get_template_config()
- http.prepare_content("text/plain; charset=utf-8")
- http.write(gen_template_config())
-end
-
-function reload_config()
- fs.remove("/tmp/AdGuardHometmpconfig.yaml")
- http.prepare_content("application/json")
- http.write('{}')
-end
-
-function act_status()
- local result = {}
- result.running = service_running()
-
- local redir = fs.readfile("/var/run/AdGredir")
- result.redirect = (redir == "1")
-
- http.prepare_content("application/json")
- http.write_json(result)
-end
-
-function toggle_service()
- local enabled = http.formvalue("enabled") == "1" and "1" or "0"
- local old_enabled = uci:get("AdGuardHome", "AdGuardHome", "enabled") == "1" and "1" or "0"
- local result = {
- enabled = (enabled == "1")
- }
-
- -- Preflight: when enabling, make sure the binary is actually present.
- -- Without this, init.d reload silently fails and the UI only sees an
- -- opaque "Operation failed" toast (issue #254).
- if enabled == "1" then
- local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
- if not fs.access(binpath) then
- result.success = false
- result.enabled = (old_enabled == "1")
- result.running = false
- result.message = "找不到 AdGuardHome 二进制文件(" .. binpath ..
- "),请先在「运维」页面下载。"
- http.prepare_content("application/json")
- http.write_json(result)
- return
- end
- end
-
- uci:set("AdGuardHome", "AdGuardHome", "enabled", enabled)
- uci:commit("AdGuardHome")
-
- local rc
- if enabled == "1" then
- -- Detach the reload from the CGI request so the HTTP call returns
- -- immediately while the daemon comes up. Fully redirecting fds + `&`
- -- is enough — avoid start-stop-daemon, whose busybox applet is not
- -- enabled on some images (e.g. immortalwrt 25.10), where it would
- -- exit 127 and make the toggle wrongly report a start failure (#254).
- rc = luci.sys.call("/etc/init.d/AdGuardHome reload >/dev/null 2>&1 &")
- else
- rc = luci.sys.call("/etc/init.d/AdGuardHome stop >/dev/null 2>&1")
- end
-
- if enabled == "1" then
- result.running = false
- else
- result.running = service_running()
- for _ = 1, 2 do
- if not result.running then
- break
- end
- luci.sys.call("sleep 1")
- result.running = service_running()
- end
- end
-
- result.pending = (enabled == "1" and not result.running)
- result.success = (rc == 0) and (enabled == "1" or not result.running)
-
- if not result.success then
- uci:set("AdGuardHome", "AdGuardHome", "enabled", old_enabled)
- uci:commit("AdGuardHome")
- result.enabled = (old_enabled == "1")
- result.message = enabled == "1" and "AdGuardHome start failed" or "AdGuardHome stop failed"
- end
-
- http.prepare_content("application/json")
- http.write_json(result)
-end
-
-function do_update()
- fs.writefile("/var/run/lucilogpos", "0")
- http.prepare_content("application/json")
- http.write('{}')
-
- local arg = ""
- if luci.http.formvalue("force") == "1" then
- arg = "force"
- end
-
- local script = "/usr/share/AdGuardHome/update_core.sh"
- if fs.access("/var/run/update_core") then
- if arg == "force" then
- -- Security fix: script 路径是固定常量,arg 只能是 "force" 或 "",无注入风险
- luci.sys.exec("pkill -f '" .. script .. "' 2>/dev/null; " .. script .. " " .. arg .. " >/tmp/AdGuardHome_update.log 2>&1 &")
- end
- else
- luci.sys.exec(script .. " " .. arg .. " >/tmp/AdGuardHome_update.log 2>&1 &")
- end
-end
-
-function get_log()
- local logfile = uci:get("AdGuardHome", "AdGuardHome", "logfile")
-
- if not logfile or logfile == "" then
- http.write("no log available\n")
- return
- end
-
- if logfile == "syslog" then
- if not fs.access("/var/run/AdGuardHomesyslog") then
- luci.sys.exec("(/usr/share/AdGuardHome/getsyslog.sh &); sleep 1;")
- end
- logfile = "/tmp/AdGuardHometmp.log"
- fs.writefile("/var/run/AdGuardHomesyslog", "1")
- elseif not fs.access(logfile) then
- http.write("")
- return
- end
-
- http.prepare_content("text/plain; charset=utf-8")
-
- local fdp = 0
- if fs.access("/var/run/lucilogreload") then
- fdp = 0
- fs.remove("/var/run/lucilogreload")
- else
- local pos = fs.readfile("/var/run/lucilogpos")
- fdp = tonumber(pos) or 0
- end
-
- local f = io.open(logfile, "r")
- if not f then
- http.write("")
- return
- end
-
- f:seek("set", fdp)
- local content = f:read(2048000) or ""
- fdp = f:seek()
- f:close()
-
- fs.writefile("/var/run/lucilogpos", tostring(fdp))
- http.write(content)
-end
-
-function do_dellog()
- local logfile = uci:get("AdGuardHome", "AdGuardHome", "logfile")
- if logfile and logfile ~= "" and logfile ~= "syslog" then
- fs.writefile(logfile, "")
- end
- http.prepare_content("application/json")
- http.write('{}')
-end
-
-function check_update()
- http.prepare_content("text/plain; charset=utf-8")
-
- local pos = fs.readfile("/var/run/lucilogpos")
- local fdp = tonumber(pos) or 0
-
- local f = io.open("/tmp/AdGuardHome_update.log", "r")
- if not f then
- http.write("")
- return
- end
-
- f:seek("set", fdp)
- local content = f:read(2048000) or ""
- fdp = f:seek()
- f:close()
-
- fs.writefile("/var/run/lucilogpos", tostring(fdp))
-
- if fs.access("/var/run/update_core") then
- http.write(content)
- else
- http.write(content .. "\0")
- end
-end
diff --git a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua
deleted file mode 100644
index 961201a2..00000000
--- a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua
+++ /dev/null
@@ -1,244 +0,0 @@
-require("luci.sys")
-require("luci.util")
-require("io")
-
-local m, s, o, o1
-local fs = require "nixio.fs"
-local uci = require "luci.model.uci".cursor()
-
-local configpath = uci:get("AdGuardHome", "AdGuardHome", "configpath") or "/etc/AdGuardHome.yaml"
-local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
-local httpport = uci:get("AdGuardHome", "AdGuardHome", "httpport") or "3000"
-
-local function service_running()
- if not fs.access(binpath) then
- return false
- end
- return luci.sys.call("/etc/init.d/AdGuardHome status >/dev/null 2>&1") == 0
-end
-
-m = Map("AdGuardHome", "AdGuard Home")
-m.description = translate("Free and open source, powerful network-wide ads & trackers blocking DNS server.")
-
--- Inject card-style CSS for AGH pages
-
-
--- ============================================================
--- Section 1: Basic Settings
--- ============================================================
-s = m:section(NamedSection, "AdGuardHome", "AdGuardHome", "基本设置")
-
-o = s:option(Flag, "enabled", translate("Enable"))
-o.default = "0"
-o.optional = false
-o.template = "AdGuardHome/enable_switch"
-o.cfgvalue = function()
- return service_running() and "1" or "0"
-end
-
-o = s:option(Value, "httpport", translate("Browser management port"))
-o.placeholder = "3000"
-o.default = "3000"
-o.datatype = "port"
-o.optional = false
-
-o = s:option(Button, "restart", translate("Update"))
-o.inputtitle = translate("Update core version")
-o.template = "AdGuardHome/AdGuardHome_check"
-o.showfastconfig = not fs.access(configpath)
-
-o = s:option(ListValue, "release_channel", translate("更新通道"),
- translate("正式版稳定;预发布版抢先体验 AdGuard 团队 beta 构建。"))
-o:value("stable", translate("正式版 (stable)"))
-o:value("beta", translate("预发布版 (beta)"))
-o.default = "stable"
-o.rmempty = false
-
-local port = luci.sys.exec("awk '/^ port:/{print $2;exit}' " .. configpath .. " 2>/dev/null")
-if not port or port == "" then
- port = "?"
-end
-
-o = s:option(ListValue, "redirect", port .. translate("Redirect"), translate("AdGuardHome redirect mode"))
-o:value("none", translate("不启用"))
-o:value("dnsmasq-upstream", translate("作为 dnsmasq 上游服务器"))
-o:value("redirect", translate("将 53 端口劫持到 AdGuardHome"))
-o:value("exchange", translate("使用 53 端口替代 dnsmasq"))
-o.default = "none"
-o.optional = true
-
--- ============================================================
--- Section 2: Paths & Logs
--- ============================================================
-s = m:section(NamedSection, "AdGuardHome", "AdGuardHome", "路径与日志")
-
-o = s:option(Value, "binpath", translate("Bin Path"), translate("AdGuardHome Bin path if no bin will auto download"))
-o.default = "/usr/bin/AdGuardHome"
-o.datatype = "string"
-o.optional = false
-o.rmempty = false
-o.validate = function(self, value)
- if value == "" then return nil end
- if fs.stat(value, "type") == "dir" then
- fs.rmdir(value)
- end
- if fs.stat(value, "type") == "dir" then
- m.message = (m.message or "") .. "\n" .. translate("Error: bin path is a directory")
- return nil
- end
- return value
-end
-
-o = s:option(Value, "configpath", translate("Config Path"), translate("AdGuardHome config path"))
-o.default = "/etc/AdGuardHome.yaml"
-o.datatype = "string"
-o.optional = false
-o.rmempty = false
-o.validate = function(self, value)
- if not value then return nil end
- if fs.stat(value, "type") == "dir" then
- fs.rmdir(value)
- end
- if fs.stat(value, "type") == "dir" then
- m.message = (m.message or "") .. "\n" .. translate("Error: config path is a directory")
- return nil
- end
- return value
-end
-
-o = s:option(Value, "workdir", translate("Work dir"), translate("AdGuardHome work dir include rules,audit log and database"))
-o.default = "/etc/AdGuardHome"
-o.datatype = "string"
-o.optional = false
-o.rmempty = false
-o.validate = function(self, value)
- if value == "" then return nil end
- if fs.stat(value, "type") == "reg" then
- m.message = (m.message or "") .. "\n" .. translate("Error: work dir is a file")
- return nil
- end
- if value:sub(-1) == "/" then
- return value:sub(1, -2)
- end
- return value
-end
-
-o = s:option(Value, "logfile", translate("Runtime log file"), translate("AdGuardHome runtime Log file if 'syslog': write to system log;if empty no log"))
-o.datatype = "string"
-o.rmempty = true
-o.validate = function(self, value)
- if value and value ~= "" and fs.stat(value, "type") == "dir" then
- fs.rmdir(value)
- end
- if value and value ~= "" and fs.stat(value, "type") == "dir" then
- m.message = (m.message or "") .. "\n" .. translate("Error: log file is a directory")
- return nil
- end
- return value
-end
-
-o = s:option(Flag, "verbose", translate("Verbose log"))
-o.default = "0"
-o.optional = true
-
--- ============================================================
--- Section 3: Advanced Settings
--- ============================================================
-s = m:section(NamedSection, "AdGuardHome", "AdGuardHome", "高级设置")
-
-o = s:option(ListValue, "upxflag", translate("use upx to compress bin after download"))
-o:value("", translate("none"))
-o:value("-1", translate("compress faster"))
-o:value("-9", translate("compress better"))
-o:value("--best", translate("compress best(can be slow for big files)"))
-o:value("--brute", translate("try all available compression methods & filters [slow]"))
-o:value("--ultra-brute", translate("try even more compression variants [very slow]"))
-o.default = ""
-o.description = translate("bin use less space,but may have compatibility issues")
-o.rmempty = true
-
-o = s:option(Value, "user", translate("Service user"), translate("User the service runs under. If empty, defaults to 'adguardhome'."))
-o.placeholder = "adguardhome"
-o.datatype = "string"
-o.rmempty = true
-
-o = s:option(Value, "group", translate("Service group"), translate("Group the service runs under. If empty, defaults to 'adguardhome'."))
-o.placeholder = "adguardhome"
-o.datatype = "string"
-o.rmempty = true
-
--- Helper:兼容把旧的 newline-separated 字符串读成 table(仅一次性迁移期需要)
-local function split_legacy_list(value)
- if type(value) ~= "string" or value == "" then return value end
- if not value:find("[\r\n]") then return value end
- local t = {}
- for line in value:gmatch("[^\r\n]+") do
- line = line:match("^%s*(.-)%s*$")
- if line ~= "" then t[#t+1] = line end
- end
- return t
-end
-
-o = s:option(DynamicList, "jail_mount", translate("只读文件访问"),
- translate("AdGuardHome 可以读取的目录或文件路径,每行一项。"))
-o.placeholder = "/etc/hosts"
-o.rmempty = true
-o.cfgvalue = function(self, section)
- return split_legacy_list(uci:get("AdGuardHome", section, "jail_mount"))
-end
-
-o = s:option(DynamicList, "jail_mount_rw", translate("读写文件访问"),
- translate("AdGuardHome 可以读写的目录或文件路径,每行一项。"))
-o.placeholder = "/var/log/AdGuardHome"
-o.rmempty = true
-o.cfgvalue = function(self, section)
- return split_legacy_list(uci:get("AdGuardHome", section, "jail_mount_rw"))
-end
-
--- Probe system memory to suggest a sane MiB cap (≈50% of total RAM)
-local total_mem_kb = tonumber(luci.sys.exec("awk '/MemTotal/{print $2}' /proc/meminfo 2>/dev/null")) or 0
-local mem_suggest_mib = (total_mem_kb > 0) and math.floor(total_mem_kb / 1024 / 2) or 128
-
-o = s:option(Value, "memlimit", translate("内存上限 (MiB)"),
- translate("达到阈值时主动触发 GC,避免被系统 OOM 杀进程。留空 = 不限制。建议设为系统内存的 50%。"))
-o.datatype = "uinteger"
-o.placeholder = translate("推荐 ") .. tostring(mem_suggest_mib)
-o.rmempty = true
-
-o = s:option(Flag, "waitonboot", translate("On boot when network ok restart"))
-o.default = "1"
-o.rmempty = true
-
--- ============================================================
--- Commit handler
--- ============================================================
-fs.writefile("/var/run/lucilogpos", "0")
-
-function m.on_commit(map)
- if fs.access("/var/run/AdGserverdis") then
- io.popen("/etc/init.d/AdGuardHome reload &")
- return
- end
-
- local ucitracktest = uci:get("AdGuardHome", "AdGuardHome", "ucitracktest")
- if ucitracktest == "1" then
- return
- elseif ucitracktest == "0" then
- io.popen("/etc/init.d/AdGuardHome reload &")
- else
- if fs.access("/var/run/AdGlucitest") then
- uci:set("AdGuardHome", "AdGuardHome", "ucitracktest", "0")
- io.popen("/etc/init.d/AdGuardHome reload &")
- else
- fs.writefile("/var/run/AdGlucitest", "")
- if ucitracktest == "2" then
- uci:set("AdGuardHome", "AdGuardHome", "ucitracktest", "1")
- else
- uci:set("AdGuardHome", "AdGuardHome", "ucitracktest", "2")
- end
- end
- uci:save("AdGuardHome")
- end
-end
-
-return m
diff --git a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/log.lua b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/log.lua
deleted file mode 100644
index 5d18a88d..00000000
--- a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/log.lua
+++ /dev/null
@@ -1,16 +0,0 @@
-local fs=require"nixio.fs"
-local uci=require"luci.model.uci".cursor()
-local f,t
-f=SimpleForm("logview")
-f.reset = false
-f.submit = false
-t=f:field(TextValue,"conf")
-t.rmempty=true
-t.rows=20
-t.template="AdGuardHome/log"
-t.readonly="readonly"
-local logfile=uci:get("AdGuardHome","AdGuardHome","logfile") or ""
-t.timereplace=(logfile~="syslog" and logfile~="" )
-t.pollcheck=logfile~=""
-fs.writefile("/var/run/lucilogreload","")
-return f
diff --git a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/manual.lua b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/manual.lua
deleted file mode 100644
index d7bf82e9..00000000
--- a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/manual.lua
+++ /dev/null
@@ -1,121 +0,0 @@
-local m, s, o
-local fs = require "nixio.fs"
-local uci = require "luci.model.uci".cursor()
-local sys = require "luci.sys"
-require("string")
-require("io")
-require("table")
-
-m = Map("AdGuardHome")
-
-
-
-local configpath = uci:get("AdGuardHome", "AdGuardHome", "configpath") or "/etc/AdGuardHome.yaml"
-local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
-
-s = m:section(TypedSection, "AdGuardHome")
-s.anonymous = true
-s.addremove = false
-
-o = s:option(TextValue, "escconf")
-o.rows = 66
-o.wrap = "off"
-o.rmempty = true
-
-o.cfgvalue = function(self, section)
- local tmp = fs.readfile("/tmp/AdGuardHometmpconfig.yaml")
- if tmp then return tmp end
-
- local cfg = fs.readfile(configpath)
- if cfg then return cfg end
-
- local dns_servers = ""
- local resolv_auto = "/tmp/resolv.conf.d/resolv.conf.auto"
- if fs.access(resolv_auto) then
- for line in io.lines(resolv_auto) do
- local ns = line:match("^[^#]*nameserver%s+([^%s]+)")
- if ns then
- dns_servers = dns_servers .. " - " .. ns .. "\n"
- end
- end
- end
-
- local tmpl = "/usr/share/AdGuardHome/AdGuardHome_template.yaml"
- local f = io.open(tmpl, "r")
- if not f then return "" end
-
- local lines = {}
- for line in f:lines() do
- if line == "#bootstrap_dns" then
- table.insert(lines, dns_servers)
- elseif line == "#upstream_dns" then
- table.insert(lines, dns_servers)
- else
- table.insert(lines, line)
- end
- end
- f:close()
- return table.concat(lines, "\n")
-end
-
-o.validate = function(self, value)
- if not value then return nil end
- fs.writefile("/tmp/AdGuardHometmpconfig.yaml", value:gsub("\r\n", "\n"))
-
- if fs.access(binpath) then
- local ret = sys.call(binpath .. " -c /tmp/AdGuardHometmpconfig.yaml --check-config 2>/tmp/AdGuardHometest.log")
- if ret == 0 then
- return value
- end
- else
- return value
- end
-
- luci.http.redirect(luci.dispatcher.build_url("admin", "services", "AdGuardHome", "manual"))
- return nil
-end
-
-o.write = function(self, section, value)
- if fs.access("/tmp/AdGuardHometmpconfig.yaml") then
- fs.move("/tmp/AdGuardHometmpconfig.yaml", configpath)
- end
-end
-
-o.remove = function(self, section, value)
- fs.writefile(configpath, "")
-end
-
-o = s:option(DummyValue, "")
-o.anonymous = true
-o.template = "AdGuardHome/yamleditor"
-
-if not fs.access(binpath) then
- o.description = translate("WARNING: no binary found, config validation will be skipped")
-end
-
-if fs.access("/tmp/AdGuardHometest.log") then
- local errlog = fs.readfile("/tmp/AdGuardHometest.log")
- if errlog and errlog ~= "" then
- o = s:option(TextValue, "")
- o.readonly = true
- o.rows = 5
- o.rmempty = true
- o.name = ""
- o.cfgvalue = function(self, section)
- return fs.readfile("/tmp/AdGuardHometest.log")
- end
- end
-end
-
-function m.on_commit(map)
- local ucitracktest = uci:get("AdGuardHome", "AdGuardHome", "ucitracktest")
- if ucitracktest == "1" then
- return
- elseif ucitracktest == "0" then
- io.popen("/etc/init.d/AdGuardHome reload &")
- else
- fs.writefile("/var/run/AdGlucitest", "")
- end
-end
-
-return m
diff --git a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/overview.lua b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/overview.lua
deleted file mode 100644
index 4ece44fd..00000000
--- a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/overview.lua
+++ /dev/null
@@ -1,16 +0,0 @@
-require("luci.sys")
-
-local m, s
-local fs = require "nixio.fs"
-local uci = require "luci.model.uci".cursor()
-
-m = Map("AdGuardHome")
-
-m:section(SimpleSection).template = "AdGuardHome/overview"
-
--- Read-only dashboard — hide form submit / reset buttons
-m.submit = false
-m.reset = false
-m.apply_on_parse = false
-
-return m
diff --git a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/tools.lua b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/tools.lua
deleted file mode 100644
index 55a520b0..00000000
--- a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/tools.lua
+++ /dev/null
@@ -1,113 +0,0 @@
-require("luci.sys")
-require("luci.util")
-
-local m, s, o, o1
-local fs = require "nixio.fs"
-local uci = require "luci.model.uci".cursor()
-
-local workdir = uci:get("AdGuardHome", "AdGuardHome", "workdir") or "/etc/AdGuardHome"
-
-m = Map("AdGuardHome", translate("运维"))
-m.description = translate("改密、备份、计划任务、下载源")
-
-
-
-s = m:section(NamedSection, "AdGuardHome", "AdGuardHome", "")
-s.anonymous = true
-s.addremove = false
-
--- Change password ------------------------------------------------
-o = s:option(Value, "hashpass", translate("Change browser management password"),
- translate("Press load culculate model and culculate finally save/apply"))
-o.default = ""
-o.datatype = "string"
-o.template = "AdGuardHome/AdGuardHome_chpass"
-o.rmempty = true
-
--- Upgrade keep files ---------------------------------------------
-o = s:option(MultiValue, "upprotect", translate("Keep files when system upgrade"),
- translate("刷固件 (sysupgrade) 时保留这些文件。普通用户按推荐勾选即可:核心 + 配置 + filters + sessions.db + stats.db。日志和 querylog 通常不需要保留(querylog 可能占数十 MB)。"))
-o:value("$binpath", translate("核心执行文件 (推荐)"))
-o:value("$configpath", translate("配置文件 (必选)"))
-o:value("$logfile", translate("日志文件"))
-o:value("$workdir/data/sessions.db", translate("登录会话 sessions.db (推荐)"))
-o:value("$workdir/data/stats.db", translate("统计 stats.db (推荐)"))
-o:value("$workdir/data/querylog.json", translate("查询日志 querylog.json (占空间)"))
-o:value("$workdir/data/filters", translate("过滤规则 filters (推荐)"))
-o.widget = "checkbox"
-o.default = "$binpath $configpath $workdir/data/sessions.db $workdir/data/stats.db $workdir/data/filters"
-o.optional = false
-
--- Backup workdir -------------------------------------------------
-o = s:option(MultiValue, "backupfile", translate("Backup workdir files when shutdown"),
- translate("关机时把这些文件复制到下方「备份工作目录路径」,下次开机如果 workdir/data 为空会自动恢复。仅当工作目录位于 tmpfs(如 /tmp)时才需要勾选,持久化路径无需备份。"))
-o1 = s:option(Value, "backupwdpath", translate("Backup workdir path"))
-
-o:value("filters", "filters")
-o:value("stats.db", "stats.db")
-o:value("querylog.json", "querylog.json")
-o:value("sessions.db", "sessions.db")
-o1:depends("backupfile", "filters")
-o1:depends("backupfile", "stats.db")
-o1:depends("backupfile", "querylog.json")
-o1:depends("backupfile", "sessions.db")
-
-if fs.access(workdir .. "/data") then
- for name in fs.glob(workdir .. "/data/*") do
- local basename = fs.basename(name)
- if basename ~= "filters" and basename ~= "stats.db"
- and basename ~= "querylog.json" and basename ~= "sessions.db" then
- o:value(basename, basename)
- o1:depends("backupfile", basename)
- end
- end
-end
-
-o.widget = "checkbox"
-o.default = nil
-o.optional = false
-o.description = translate("Will be restore when workdir/data is empty")
-
-o1.default = "/etc/AdGuardHome"
-o1.datatype = "string"
-o1.optional = false
-o1.validate = function(self, value)
- if value and value ~= "" and fs.stat(value, "type") == "reg" then
- m.message = (m.message or "") .. "\n" .. translate("Error: backup dir is a file")
- return nil
- end
- if value and value:sub(-1) == "/" then
- return value:sub(1, -2)
- end
- return value
-end
-
--- Crontab tasks --------------------------------------------------
-o = s:option(MultiValue, "crontab", translate("Crontab task"),
- translate("Please change time and args in crontab"))
-o:value("autoupdate", translate("Auto update core"))
-o:value("cutquerylog", translate("Auto tail querylog"))
-o:value("cutruntimelog", translate("Auto tail runtime log"))
-o:value("autohost", translate("Auto update ipv6 hosts and restart adh"))
-o.widget = "checkbox"
-o.default = nil
-o.optional = false
-
--- Download links -------------------------------------------------
-o = s:option(TextValue, "downloadlinks", translate("Download links for update"))
-o.optional = false
-o.rows = 4
-o.wrap = "soft"
-o.cfgvalue = function(self, section)
- if fs.access("/usr/share/AdGuardHome/links.txt") then
- return fs.readfile("/usr/share/AdGuardHome/links.txt")
- end
- return ""
-end
-o.write = function(self, section, value)
- if value then
- fs.writefile("/usr/share/AdGuardHome/links.txt", value:gsub("\r\n", "\n"))
- end
-end
-
-return m
diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_check.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_check.htm
deleted file mode 100644
index 251374b0..00000000
Binary files a/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_check.htm and /dev/null differ
diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm
deleted file mode 100644
index 1c274fbf..00000000
--- a/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm
+++ /dev/null
@@ -1,51 +0,0 @@
-<%+AdGuardHome/head%>
-<%+cbi/valueheader%>
-
-
-
-<%+cbi/valuefooter%>
diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_status.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_status.htm
deleted file mode 100644
index b0e8bb36..00000000
--- a/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_status.htm
+++ /dev/null
@@ -1,145 +0,0 @@
-
-
-
diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/enable_switch.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/enable_switch.htm
deleted file mode 100644
index 96ff2b4d..00000000
--- a/luci-app-adguardhome/luasrc/view/AdGuardHome/enable_switch.htm
+++ /dev/null
@@ -1,169 +0,0 @@
-<%+AdGuardHome/head%>
-<%+cbi/valueheader%>
-<%
- local value = self:cfgvalue(section)
- local checked = value == "1"
- local field_id = cbid
-%>
-
-
- />
-
- >
-
-
-<%+cbi/valuefooter%>
diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/head.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/head.htm
deleted file mode 100644
index 583a5ae5..00000000
--- a/luci-app-adguardhome/luasrc/view/AdGuardHome/head.htm
+++ /dev/null
@@ -1,83 +0,0 @@
-
diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm
deleted file mode 100644
index e38d8015..00000000
--- a/luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm
+++ /dev/null
@@ -1,186 +0,0 @@
-<%+cbi/valueheader%>
-<%+AdGuardHome/head%>
-<%+AdGuardHome/log_empty%>
-
-
-
- <%if self.timereplace then%>
-
- <%end%>
-
-
-
-
-
-
-
-<%+cbi/valuefooter%>
diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/log_empty.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/log_empty.htm
deleted file mode 100644
index a818b741..00000000
--- a/luci-app-adguardhome/luasrc/view/AdGuardHome/log_empty.htm
+++ /dev/null
@@ -1,27 +0,0 @@
-<%
-local fs = require "nixio.fs"
-local uci = require "luci.model.uci".cursor()
-
-local logfile = uci:get("AdGuardHome", "AdGuardHome", "logfile") or ""
-local no_logfile = (logfile == "")
-%>
-
-<% if no_logfile then %>
-
-
!
-
-
<%:运行日志未配置%>
-
<%:请在「基础设置 → 路径与日志 → 运行日志」填入日志路径(如 /tmp/AdGuardHome.log),或填 syslog 写入系统日志。%>
-
<%:去设置%>
-
-
-
-<% end %>
diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/overview.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/overview.htm
deleted file mode 100644
index a70c4333..00000000
--- a/luci-app-adguardhome/luasrc/view/AdGuardHome/overview.htm
+++ /dev/null
@@ -1,286 +0,0 @@
-<%+AdGuardHome/head%>
-<%
-local fs = require "nixio.fs"
-local uci = require "luci.model.uci".cursor()
-local sys = require "luci.sys"
-local dsp = require "luci.dispatcher"
-
-local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
-local configpath = uci:get("AdGuardHome", "AdGuardHome", "configpath") or "/etc/AdGuardHome.yaml"
-local httpport = uci:get("AdGuardHome", "AdGuardHome", "httpport") or "3000"
-local redirect = uci:get("AdGuardHome", "AdGuardHome", "redirect") or "none"
-local version = uci:get("AdGuardHome", "AdGuardHome", "version") or "-"
-
-local has_bin = fs.access(binpath)
-local has_config = fs.access(configpath)
-
-local toggle_url = dsp.build_url("admin", "services", "AdGuardHome", "toggle")
-local base_url = dsp.build_url("admin", "services", "AdGuardHome", "base")
-local tools_url = dsp.build_url("admin", "services", "AdGuardHome", "tools")
-local log_url = dsp.build_url("admin", "services", "AdGuardHome", "log")
-
-local redirect_label = {
- none = translate("未启用"),
- ["dnsmasq-upstream"] = translate("作为 dnsmasq 上游"),
- redirect = translate("53 端口劫持"),
- exchange = translate("替代 dnsmasq")
-}
-local redirect_text = redirect_label[redirect] or redirect
-%>
-
-
-
-
-
-
-
-
-
-
-
<%:服务状态%>
-
- <%:正在检测…%>
-
-
-
-
-
-
-
-
-
-
<%:重定向模式%>
-
- <%= redirect_text %>
-
-
- <%:配置「基础设置 → 5553 重定向」%>
-
-
-
-
-
-
<%:核心版本%>
-
- <% if has_bin then %>
- <%= version %>
- <% else %>
- <%:未安装%>
- <% end %>
-
-
- <% if not has_config then %>
- <%:配置文件缺失%>
- <% elseif not has_bin then %>
- <%:点击「基础设置 → 更新核心版本」下载%>
- <% else %>
- <%= binpath %>
- <% end %>
-
-
-
-
-
-
-
-
-
diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/yamleditor.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/yamleditor.htm
deleted file mode 100644
index 2ecdbd0a..00000000
--- a/luci-app-adguardhome/luasrc/view/AdGuardHome/yamleditor.htm
+++ /dev/null
@@ -1,84 +0,0 @@
-<%+AdGuardHome/head%>
-<%+cbi/valueheader%>
-
-
-
-
-
-
-
-
-
-
-<%fs=require"nixio.fs"%>
-<%if fs.access("/tmp/AdGuardHometmpconfig.yaml") then%>
-
-<%end%>
-
-<%+cbi/valuefooter%>
\ No newline at end of file
diff --git a/luci-app-adguardhome/po/zh-cn/AdGuardHome.po b/luci-app-adguardhome/po/zh-cn/AdGuardHome.po
new file mode 100644
index 00000000..c465821f
--- /dev/null
+++ b/luci-app-adguardhome/po/zh-cn/AdGuardHome.po
@@ -0,0 +1,231 @@
+#/cgi-bin/luci/admin/services/AdGuardHome
+msgid "Base Setting"
+msgstr "基础设置"
+
+msgid "Log"
+msgstr "日志"
+
+msgid "Basic Settings"
+msgstr "基础设置"
+
+msgid "Manual Config"
+msgstr "手动设置"
+
+msgid "Enable"
+msgstr "启用"
+
+msgid "Browser management port"
+msgstr "网页管理端口"
+
+msgid "DNS handling mode"
+msgstr "DNS 接管方式"
+
+msgid "Do not handle DNS"
+msgstr "不接管 DNS"
+
+msgid "Run as dnsmasq upstream server"
+msgstr "作为 dnsmasq 的上游服务器"
+
+msgid "Forward port 53 requests through the firewall"
+msgstr "通过防火墙转发 53 端口请求"
+
+msgid "Use port 53 instead of dnsmasq"
+msgstr "使用 53 端口替代 dnsmasq"
+
+msgid "Network devices excluded from forwarding"
+msgstr "不参与转发的网络设备"
+
+msgid "DNS requests arriving on these network devices are not forwarded to AdGuardHome"
+msgstr "从这些网络设备进入的 DNS 请求不转发到 AdGuardHome"
+
+msgid "Bin Path"
+msgstr "执行文件路径"
+
+msgid "Config Path"
+msgstr "配置文件路径"
+
+msgid "Work dir"
+msgstr "工作目录"
+
+msgid "Runtime log file"
+msgstr "运行日志文件"
+
+msgid "If set to \"syslog\", write to system log; if empty, disable file logging"
+msgstr "设为 \"syslog\" 时写入系统日志;留空则禁用文件日志"
+
+msgid "Verbose log"
+msgstr "详细日志"
+
+msgid "Upgrade Core"
+msgstr "更新核心"
+
+msgid "Update core version"
+msgstr "更新核心版本"
+
+msgid "Force update"
+msgstr "强制更新"
+
+msgid "Check..."
+msgstr "检查中..."
+
+msgid "Updated"
+msgstr "已更新"
+
+msgid "Update failed"
+msgstr "更新失败"
+
+msgid "Update in progress"
+msgstr "更新正在进行中"
+
+msgid "Unable to start update"
+msgstr "无法启动更新"
+
+msgid "Update status unavailable"
+msgstr "更新状态不可用"
+
+msgid "AdGuardHome Web"
+msgstr "AdGuardHome 仪表盘"
+
+msgid "Change browser management password"
+msgstr "修改网页登录密码"
+
+msgid "New password"
+msgstr "新密码"
+
+msgid "Change password"
+msgstr "修改密码"
+
+msgid "is empty"
+msgstr "为空"
+
+msgid "Changing password..."
+msgstr "正在修改密码..."
+
+msgid "Password changed"
+msgstr "密码已修改"
+
+msgid "Password change failed"
+msgstr "密码修改失败"
+
+msgid "Core"
+msgstr "核心"
+
+msgid "core error"
+msgstr "核心错误"
+
+msgid "no core"
+msgstr "没有核心"
+
+msgid "no config"
+msgstr "没有配置文件"
+
+msgid "RUNNING"
+msgstr "运行中"
+
+msgid "NOT RUNNING"
+msgstr "未运行"
+
+msgid "Redirect"
+msgstr "重定向"
+
+msgid "Redirected"
+msgstr "已重定向"
+
+msgid "Not redirect"
+msgstr "未重定向"
+
+msgid "Collecting data..."
+msgstr "获取数据中..."
+
+msgid "Status unavailable"
+msgstr "状态不可用"
+
+msgid "reverse"
+msgstr "逆序"
+
+msgid "localtime"
+msgstr "本地时间"
+
+msgid "Please add log path in config to enable log"
+msgstr "请在设置中填写日志路径以启用日志"
+
+msgid "dellog"
+msgstr "删除日志"
+
+msgid "download log"
+msgstr "下载日志"
+
+msgid "Use template"
+msgstr "使用模板"
+
+msgid "Reload Config"
+msgstr "重新载入配置"
+
+msgid "Save & Apply"
+msgstr "保存并应用"
+
+msgid "Unable to read config"
+msgstr "无法读取配置"
+
+msgid "Unable to load editor"
+msgstr "无法加载编辑器"
+
+msgid "Saving..."
+msgstr "正在保存..."
+
+msgid "Saved"
+msgstr "已保存"
+
+msgid "Save failed"
+msgstr "保存失败"
+
+msgid "Core is missing; configuration cannot be validated before saving."
+msgstr "核心文件不存在,保存前无法校验配置。"
+
+msgid "Saved without validation (core missing)."
+msgstr "已保存,但因核心文件不存在而未校验。"
+
+msgid "Configuration validation failed."
+msgstr "配置校验失败。"
+
+msgid "Service reload failed; previous config restored."
+msgstr "服务重载失败,已恢复原配置。"
+
+msgid "Restore failed; backup at"
+msgstr "恢复失败,备份文件位于"
+
+msgid "Config changed on disk; reload before saving."
+msgstr "磁盘上的配置已变化,请重新读取后再保存。"
+
+msgid "Unable to read runtime log"
+msgstr "无法读取运行日志"
+
+msgid "Switching"
+msgstr "正在切换"
+
+msgid "Checking service…"
+msgstr "正在确认服务状态…"
+
+msgid "Switch not completed; check the log"
+msgstr "切换尚未完成,请检查日志"
+
+msgid "Disabled"
+msgstr "已停用"
+
+msgid "Unable to read switch status"
+msgstr "无法读取切换状态"
+
+msgid "Forward port 53 requests through the firewall (recommended)"
+msgstr "通过防火墙转发 53 端口请求(推荐)"
+
+msgid "Log settings"
+msgstr "日志设置"
+
+msgid "Applying verbose logging…"
+msgstr "正在应用详细日志设置…"
+
+msgid "Saved, but service reload failed; check the log"
+msgstr "已保存,但服务重载失败,请检查日志"
+
+msgid "Unable to apply verbose logging"
+msgstr "无法应用详细日志设置"
diff --git a/luci-app-adguardhome/root/etc/AdGuardHome.yaml b/luci-app-adguardhome/root/etc/AdGuardHome.yaml
deleted file mode 100644
index 4bef352f..00000000
--- a/luci-app-adguardhome/root/etc/AdGuardHome.yaml
+++ /dev/null
@@ -1,226 +0,0 @@
-bind_host: 0.0.0.0
-bind_port: 3000
-beta_bind_port: 0
-users:
- - name: admin
- password: $2y$10$vHRcARdPCieYG3RXWomV5evDYN.Nj/edtwEkQgQJZcK6z7qTLaIc6
-auth_attempts: 5
-block_auth_min: 15
-http_proxy: ""
-language: zh-cn
-rlimit_nofile: 0
-debug_pprof: false
-web_session_ttl: 720
-dns:
- bind_hosts:
- - 0.0.0.0
- port: 5553
- statistics_interval: 1
- querylog_enabled: true
- querylog_file_enabled: true
- querylog_interval: 1
- querylog_size_memory: 1000
- anonymize_client_ip: false
- protection_enabled: true
- blocking_mode: default
- blocking_ipv4: ""
- blocking_ipv6: ""
- blocked_response_ttl: 10
- parental_block_host: family-block.dns.adguard.com
- safebrowsing_block_host: standard-block.dns.adguard.com
- ratelimit: 0
- ratelimit_whitelist: []
- refuse_any: false
- upstream_dns:
- - 223.5.5.5
- upstream_dns_file: ""
- bootstrap_dns:
- - 119.29.29.29
- - 223.5.5.5
- all_servers: false
- fastest_addr: false
- fastest_timeout: 1s
- allowed_clients: []
- disallowed_clients: []
- blocked_hosts:
- - version.bind
- - id.server
- - hostname.bind
- trusted_proxies:
- - 127.0.0.0/8
- - ::1/128
- cache_size: 4194304
- cache_ttl_min: 0
- cache_ttl_max: 0
- cache_optimistic: true
- bogus_nxdomain: []
- aaaa_disabled: false
- enable_dnssec: false
- edns_client_subnet: false
- max_goroutines: 300
- ipset: []
- filtering_enabled: true
- filters_update_interval: 24
- parental_enabled: false
- safesearch_enabled: false
- safebrowsing_enabled: false
- safebrowsing_cache_size: 1048576
- safesearch_cache_size: 1048576
- parental_cache_size: 1048576
- cache_time: 30
- rewrites: []
- blocked_services: []
- upstream_timeout: 10s
- local_domain_name: lan
- resolve_clients: true
- use_private_ptr_resolvers: true
- local_ptr_upstreams: []
-tls:
- enabled: false
- server_name: ""
- force_https: false
- port_https: 443
- port_dns_over_tls: 853
- port_dns_over_quic: 784
- port_dnscrypt: 0
- dnscrypt_config_file: ""
- allow_unencrypted_doh: false
- strict_sni_check: false
- certificate_chain: ""
- private_key: ""
- certificate_path: ""
- private_key_path: ""
-filters:
- - enabled: true
- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt
- name: AdGuard DNS filter
- id: 1228750870
- - enabled: true
- url: http://sub.adtchrome.com/adt-chinalist-easylist.txt
- name: 广告终结者使用的拦截规则,基于ChinaList+EasyList修正维护
- id: 139789181
- - enabled: true
- url: https://easylist-downloads.adblockplus.org/easylist.txt
- name: EasyList-去除国际网页中大多数广告,包括不需要的框架、图像和对象
- id: 139789112
- - enabled: true
- url: https://easylist-downloads.adblockplus.org/easylistchina.txt
- name: EasyList China-EasyList针对国内的补充规则
- id: 139789121
- - enabled: true
- url: https://anti-ad.net/adguard.txt
- name: anti-AD命中率最高列表
- id: 139789122
- - enabled: true
- url: https://raw.iqiq.io/banbendalao/ADgk/master/ADgk.txt
- name: ADgk去广告easylistchina补充规则
- id: 139789131
- - enabled: true
- url: https://raw.iqiq.io/banbendalao/ADgk/master/kill-baidu-ad.txt
- name: ADgk百度广告过滤
- id: 139789133
- - enabled: false
- url: https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&mimetype=plaintext
- name: Peter Lowe’s Ad and tracking server list
- id: 139789152
- - enabled: false
- url: https://easylist.to/easylist/fanboy-social.txt
- name: Fanboy's Social Blocking List
- id: 139789153
- - enabled: false
- url: https://easylist.to/easylist/fanboy-annoyance.txt
- name: Fanboy's Annoyance List
- id: 139789154
- - enabled: false
- url: https://secure.fanboy.co.nz/fanboy-cookiemonster.txt
- name: EasyList Cookie List
- id: 139789155
- - enabled: false
- url: https://fanboy.co.nz/fanboy-antifacebook.txt
- name: Anti-Facebook List
- id: 139789161
- - enabled: false
- url: https://filters.adtidy.org/extension/ublock/filters/14.txt
- name: AdGuard Annoyances filter
- id: 139789162
- - enabled: false
- url: https://www.fanboy.co.nz/enhancedstats.txt
- name: Fanboy's Enhanced Tracking List
- id: 139789164
- - enabled: false
- url: https://filters.adtidy.org/extension/ublock/filters/3.txt
- name: AdGuard Tracking Protection filter
- id: 139789165
- - enabled: false
- url: https://easylist.to/easylist/easylist.txt
- name: EasyList
- id: 139789171
- - enabled: false
- url: https://filters.adtidy.org/extension/ublock/filters/11.txt
- name: AdGuard Mobile Ads filter
- id: 139789172
- - enabled: false
- url: https://filters.adtidy.org/extension/ublock/filters/2_without_easylist.txt
- name: AdGuard Base filter
- id: 139789173
- - enabled: false
- url: https://easylist-downloads.adblockplus.org/antiadblockfilters.txt
- name: Adblock Warning Removal List
- id: 139789174
- - enabled: false
- url: https://fanboy.co.nz/fanboy-problematic-sites.txt
- name: Fanboy's problematic-sites
- id: 139789175
- - enabled: false
- url: https://adaway.org/hosts.txt
- name: Adaway HOST
- id: 139789187
-whitelist_filters: []
-user_rules:
- - '@@||taobao.com^$important'
- - '@@||tmall.com^$important'
- - '@@||jd.com^important'
- - '@@||flyme.cn^$important'
- - '@@||meizu.com^$important'
- - '@@||wl.jd.com^$important'
- - '@@||flydigi.com^'
- - '@@||pv.sohu.com^$important'
- - /googleads.$~script,domain=~googleads.github.io
- - /pagead/lvz?
- - '||google.com/pagead/'
- - '||static.doubleclick.net^$domain=youtube.com'
- - '||youtube.com/get_midroll_'
- - '||5hz.org^'
- - '@@||000714.xyz^'
- - '@@||blueskyxn.com^'
- - '||topnewsfeeds.net^'
- - '||nbryb.com^'
- - '||superbdolly.com^'
- - '||salutationcheerlessdemote.com^'
- - '||buildingdoodlesquare.com^'
- - '||colonistnobilityheroic.com^'
-dhcp:
- enabled: false
- interface_name: ""
- dhcpv4:
- gateway_ip: ""
- subnet_mask: ""
- range_start: ""
- range_end: ""
- lease_duration: 86400
- icmp_timeout_msec: 1000
- options: []
- dhcpv6:
- range_start: ""
- lease_duration: 86400
- ra_slaac_only: false
- ra_allow_slaac: false
-clients: []
-log_compress: false
-log_localtime: false
-log_max_backups: 0
-log_max_size: 100
-log_max_age: 3
-log_file: ""
-verbose: false
-schema_version: 10
diff --git a/luci-app-adguardhome/root/etc/config/AdGuardHome b/luci-app-adguardhome/root/etc/config/AdGuardHome
index 827b291d..fb562bb1 100644
--- a/luci-app-adguardhome/root/etc/config/AdGuardHome
+++ b/luci-app-adguardhome/root/etc/config/AdGuardHome
@@ -1,17 +1,11 @@
+
config AdGuardHome 'AdGuardHome'
option enabled '0'
option httpport '3000'
option redirect 'none'
option configpath '/etc/AdGuardHome.yaml'
- option workdir '/etc/AdGuardHome'
+ option workdir '/tmp/AdGuardHome'
option logfile '/tmp/AdGuardHome.log'
option verbose '0'
- option binpath '/usr/bin/AdGuardHome'
- option upxflag ''
- option user ''
- option group ''
- option advanced_settings '0'
- option gc '0'
- option maxprocs '0'
- option memlimit '0'
-
+ option binpath '/usr/bin/AdGuardHome/AdGuardHome'
+ list wan_ifname 'eth0'
diff --git a/luci-app-adguardhome/root/etc/init.d/AdGuardHome b/luci-app-adguardhome/root/etc/init.d/AdGuardHome
index 651ecd90..dfa233b6 100755
--- a/luci-app-adguardhome/root/etc/init.d/AdGuardHome
+++ b/luci-app-adguardhome/root/etc/init.d/AdGuardHome
@@ -6,685 +6,648 @@ START=95
STOP=01
CONFIGURATION=AdGuardHome
-CRON_FILE=/etc/crontabs/root
+NFT_RULES_FILE="/var/etc/adguardhome.nft"
+NFT_RULES_TPL="/usr/share/AdGuardHome/adguardhome.nft.tpl"
+NFT_TABLE="adguardhome"
-extra_command "do_redirect" "0 or 1"
-extra_command "testbackup" "backup or restore"
-extra_command "test_crontab"
-extra_command "force_reload"
-extra_command "isrunning"
+EXTRA_COMMANDS="do_redirect force_reload isrunning"
+EXTRA_HELP=" do_redirect 0 or 1\
+ force_reload
+ isrunning"
-set_forward_dnsmasq()
-{
- local PORT="$1"
- local addr="127.0.0.1#$PORT"
- local OLD_SERVER="$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)"
- echo "$OLD_SERVER" | grep -q "^$addr"
- [ $? -eq 0 ] && return
+get_wan_return_line() {
+ local wan
+ wan="$(uci -q get AdGuardHome.AdGuardHome.wan_ifname 2>/dev/null)"
- uci delete dhcp.@dnsmasq[0].server 2>/dev/null
- uci add_list dhcp.@dnsmasq[0].server="$addr"
- for server in $OLD_SERVER; do
- [ "$server" = "$addr" ] && continue
- done
- uci delete dhcp.@dnsmasq[0].resolvfile 2>/dev/null
- uci set dhcp.@dnsmasq[0].noresolv=1
- uci commit dhcp
- /etc/init.d/dnsmasq restart
+ [ -z "$wan" ] && { echo ""; return 0; }
+
+ local wan_set
+ wan_set="\"$(printf '%s' "$wan" | sed 's/ /\", \"/g')\""
+ echo "iifname { $wan_set } return"
}
-stop_forward_dnsmasq()
-{
- local OLD_PORT="$1"
- local addr="127.0.0.1#$OLD_PORT"
- local OLD_SERVER="$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)"
- echo "$OLD_SERVER" | grep -q "^$addr"
- [ $? -ne 0 ] && return
+set_nft_redirect() {
+ local port="$1"
+ case "$port" in
+ *[!0-9]*|"") return 1 ;;
+ esac
+ [ ! -f "$NFT_RULES_TPL" ] && return 1
- uci del_list dhcp.@dnsmasq[0].server="$addr" 2>/dev/null
- local addrlist="$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)"
- if [ -z "$addrlist" ]; then
- uci set dhcp.@dnsmasq[0].resolvfile=/tmp/resolv.conf.d/resolv.conf.auto 2>/dev/null
- uci delete dhcp.@dnsmasq[0].noresolv 2>/dev/null
- fi
- uci commit dhcp
- /etc/init.d/dnsmasq restart
+ local return_line
+ return_line="$(get_wan_return_line)"
+
+ awk -v port="$port" -v wan_line="$return_line" '{
+ gsub("__AGH_PORT__", port)
+ gsub("__AGH_WAN_RETURN_LINE__", wan_line)
+ print
+ }' "$NFT_RULES_TPL" > "$NFT_RULES_FILE" || return 1
+
+ [ ! -s "$NFT_RULES_FILE" ] && return 1
+
+ nft delete table inet "$NFT_TABLE" 2>/dev/null
+ nft -f "$NFT_RULES_FILE" 2>/dev/null
}
-set_iptable()
-{
- local ipv6_server="$1"
- local tcp_server="$2"
+clear_nft_redirect() {
+ uci -q delete firewall.adguardhome_nft_include
+ uci -q commit firewall
- uci -q batch <<-EOF >/dev/null 2>&1
- delete firewall.AdGuardHome
- set firewall.AdGuardHome=include
- set firewall.AdGuardHome.type=script
- set firewall.AdGuardHome.path=/usr/share/AdGuardHome/firewall.start
- set firewall.AdGuardHome.reload=1
- commit firewall
- EOF
-
- local IPS="$(ip -4 addr show | grep inet | grep -v ':' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)"
- for IP in $IPS; do
- [ -z "$IP" ] && continue
- iptables -t nat -I PREROUTING -p udp -d "$IP" --dport 53 -j REDIRECT --to-ports "$AdGuardHome_PORT" >/dev/null 2>&1
- [ "$tcp_server" = "1" ] && iptables -t nat -I PREROUTING -p tcp -d "$IP" --dport 53 -j REDIRECT --to-ports "$AdGuardHome_PORT" >/dev/null 2>&1
- done
-
- [ "$ipv6_server" = "0" ] && return
-
- IPS="$(ip -6 addr show | grep inet6 | grep -v 'fe80::' | grep -v '::1' | grep 'Global' | awk '{print $2}' | cut -d'/' -f1)"
- for IP in $IPS; do
- [ -z "$IP" ] && continue
- ip6tables -t nat -I PREROUTING -p udp -d "$IP" --dport 53 -j REDIRECT --to-ports "$AdGuardHome_PORT" >/dev/null 2>&1
- [ "$tcp_server" = "1" ] && ip6tables -t nat -I PREROUTING -p tcp -d "$IP" --dport 53 -j REDIRECT --to-ports "$AdGuardHome_PORT" >/dev/null 2>&1
- done
+ [ -f "$NFT_RULES_FILE" ] && rm -f "$NFT_RULES_FILE"
+ nft delete table inet "$NFT_TABLE" 2>/dev/null
}
-clear_iptable()
-{
- uci -q batch <<-EOF >/dev/null 2>&1
- delete firewall.AdGuardHome
- commit firewall
- EOF
+wait_for_pid_exit() {
+ local pid="$1" attempts=0
+ case "$pid" in ''|*[!0-9]*) return 0 ;; esac
+ while kill -0 "$pid" 2>/dev/null; do
+ [ "$attempts" -lt 15 ] || return 1
+ sleep 1
+ attempts=$((attempts + 1))
+ done
+}
- local OLD_PORT="$1"
- local ipv6_server="$2"
- local IPS="$(ip -4 addr show | grep inet | grep -v ':' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)"
- for IP in $IPS; do
- [ -z "$IP" ] && continue
- iptables -t nat -D PREROUTING -p udp -d "$IP" --dport 53 -j REDIRECT --to-ports "$OLD_PORT" >/dev/null 2>&1
- iptables -t nat -D PREROUTING -p tcp -d "$IP" --dport 53 -j REDIRECT --to-ports "$OLD_PORT" >/dev/null 2>&1
- done
+wait_for_dns_port_release() {
+ local port="$1" attempts=0 tables="" table busy
+ for table in /proc/net/tcp /proc/net/tcp6 /proc/net/udp /proc/net/udp6; do
+ [ -r "$table" ] && tables="$tables $table"
+ done
+ [ -n "$tables" ] || return 1
+ port="$(printf '%04X' "$port")" || return 1
+ while :; do
+ busy="$(awk -v port="$port" '
+ FNR > 1 {
+ split($2, address, ":")
+ if (toupper(address[2]) == port &&
+ ((FILENAME ~ /tcp/ && $4 == "0A") ||
+ (FILENAME ~ /udp/ && $4 == "07"))) found = 1
+ }
+ END { print found ? 1 : 0 }
+ ' $tables)" || return 1
+ [ "$busy" = "0" ] && return 0
+ [ "$attempts" -lt 15 ] || return 1
+ sleep 1
+ attempts=$((attempts + 1))
+ done
+}
- [ "$ipv6_server" = "0" ] && return
- echo "warn ip6tables nat mod is needed"
- IPS="$(ip -6 addr show | grep inet6 | grep -v 'fe80::' | grep -v '::1' | grep 'Global' | awk '{print $2}' | cut -d'/' -f1)"
- for IP in $IPS; do
- [ -z "$IP" ] && continue
- ip6tables -t nat -D PREROUTING -p udp -d "$IP" --dport 53 -j REDIRECT --to-ports "$OLD_PORT" >/dev/null 2>&1
- ip6tables -t nat -D PREROUTING -p tcp -d "$IP" --dport 53 -j REDIRECT --to-ports "$OLD_PORT" >/dev/null 2>&1
- done
+restart_dnsmasq() {
+ local released_port="$1" service pids pid
+ service="$(ubus call service list '{"name":"dnsmasq"}' 2>/dev/null)" || return 1
+ pids="$(printf '%s' "$service" | jsonfilter -e '@.dnsmasq.instances.*.pid')"
+ /etc/init.d/dnsmasq stop 8>&- 1000>&- || return 1
+ for pid in $pids; do
+ if ! wait_for_pid_exit "$pid"; then
+ logger -t AdGuardHome 'dnsmasq did not stop; refusing to reuse its old jail.'
+ return 1
+ fi
+ done
+ if [ -n "$released_port" ] && ! wait_for_dns_port_release "$released_port"; then
+ logger -t AdGuardHome "DNS port $released_port is still occupied after stopping dnsmasq."
+ /etc/init.d/dnsmasq start 8>&- 1000>&-
+ return 1
+ fi
+ /etc/init.d/dnsmasq start 8>&- 1000>&- || return 1
+ if [ -n "$released_port" ] && ! wait_for_dns_port_release "$released_port"; then
+ logger -t AdGuardHome "DNS port $released_port was occupied again after starting dnsmasq."
+ return 1
+ fi
+ return 0
+}
+
+save_dnsmasq_upstream_config() {
+ local backup="AdGuardHome.dnsmasq_backup" option value server
+ [ "$(uci -q get "$backup.ready")" = "1" ] && return 0
+
+ uci -q delete "$backup"
+ uci set "$backup=dnsmasq_backup" || return 1
+ value="$(uci -q get dhcp.@dnsmasq[0].server)"
+ for server in $value; do
+ uci add_list "$backup.server=$server" || return 1
+ done
+ for option in resolvfile noresolv; do
+ if value="$(uci -q get "dhcp.@dnsmasq[0].$option")"; then
+ uci set "$backup.$option=$value" || return 1
+ fi
+ done
+ uci set "$backup.ready=1" || return 1
+ if ! uci commit AdGuardHome; then
+ uci -q revert "$backup"
+ return 1
+ fi
+}
+
+apply_dnsmasq_config() {
+ [ "${AGH_DEFER_DNSMASQ_RESTART:-0}" = "1" ] && return 0
+ restart_dnsmasq "$@"
+}
+
+is_scoped_dnsmasq_server() {
+ local domains="${1%/*}"
+ case "$1" in
+ /*) case "$domains/" in */\#/*) return 1 ;; esac; return 0 ;;
+ esac
+ return 1
+}
+
+set_forward_dnsmasq() (
+ set -f
+ local addr="127.0.0.1#$1" servers desired server
+
+ if [ "$(uci -q get AdGuardHome.dnsmasq_backup.ready)" != "1" ] &&
+ [ "$2" = "dnsmasq-upstream" ] && [ "$3" = "1" ]; then
+ stop_forward_dnsmasq "$4" || return 1
+ fi
+ save_dnsmasq_upstream_config || return 1
+
+ servers="$(uci -q get dhcp.@dnsmasq[0].server)"
+ desired="$addr"
+ for server in $servers; do
+ is_scoped_dnsmasq_server "$server" && desired="$desired $server"
+ done
+ if [ "$servers" = "$desired" ] && [ "$(uci -q get dhcp.@dnsmasq[0].noresolv)" = "1" ]; then
+ return 0
+ fi
+
+ uci -q delete dhcp.@dnsmasq[0].server
+ for server in $desired; do
+ uci add_list "dhcp.@dnsmasq[0].server=$server" || return 1
+ done
+ uci set dhcp.@dnsmasq[0].noresolv=1 || return 1
+ uci commit dhcp || return 1
+ apply_dnsmasq_config >/dev/null 2>&1
+)
+
+stop_forward_dnsmasq() (
+ set -f
+ local backup="AdGuardHome.dnsmasq_backup" option value server
+ local addr="127.0.0.1#$1" found=0 has_general=0
+
+ if [ "$(uci -q get "$backup.ready")" = "1" ]; then
+ uci -q delete dhcp.@dnsmasq[0].server
+ value="$(uci -q get "$backup.server")"
+ for server in $value; do
+ uci add_list "dhcp.@dnsmasq[0].server=$server" || return 1
+ done
+ for option in resolvfile noresolv; do
+ if value="$(uci -q get "$backup.$option")"; then
+ uci set "dhcp.@dnsmasq[0].$option=$value" || return 1
+ else
+ uci -q delete "dhcp.@dnsmasq[0].$option"
+ fi
+ done
+ uci commit dhcp || return 1
+ apply_dnsmasq_config >/dev/null 2>&1 || return 1
+ [ "${AGH_DEFER_DNSMASQ_RESTART:-0}" = "1" ] && return 0
+ uci -q delete "$backup"
+ uci commit AdGuardHome
+ return $?
+ fi
+
+ value="$(uci -q get dhcp.@dnsmasq[0].server)"
+ for server in $value; do
+ if [ "$server" = "$addr" ]; then
+ found=1
+ elif ! is_scoped_dnsmasq_server "$server"; then
+ has_general=1
+ fi
+ done
+ [ "$found" = "1" ] || return 0
+ uci -q del_list "dhcp.@dnsmasq[0].server=$addr" || return 1
+ if [ "$has_general" = "0" ]; then
+ if ! uci -q get dhcp.@dnsmasq[0].resolvfile >/dev/null; then
+ value="/tmp/resolv.conf.d/resolv.conf.auto"
+ [ -f "$value" ] || value="/tmp/resolv.conf.auto"
+ uci set "dhcp.@dnsmasq[0].resolvfile=$value" || return 1
+ fi
+ uci -q delete dhcp.@dnsmasq[0].noresolv
+ fi
+ logger -t AdGuardHome 'No DNS snapshot from previous version; removed old upstream, original resolver settings are unavailable.'
+ uci commit dhcp || return 1
+ apply_dnsmasq_config >/dev/null 2>&1
+)
+
+stop_core() {
+ local service instance pid
+ service="$(ubus call service list '{"name":"AdGuardHome"}' 2>/dev/null)" || return 1
+ instance="$(printf '%s' "$service" | jsonfilter -e '@.AdGuardHome.instances.instance1')"
+ [ -n "$instance" ] || return 0
+ pid="$(printf '%s' "$instance" | jsonfilter -e '@.pid')"
+ ubus call service delete '{"name":"AdGuardHome","instance":"instance1"}' >/dev/null || return 1
+ if ! wait_for_pid_exit "$pid"; then
+ logger -t AdGuardHome 'Core did not stop; DNS port settings were not changed.'
+ return 1
+ fi
+}
+
+agh_reload() {
+ [ "${AGH_REDIRECT_RELOAD:-1}" = "1" ] || return 0
+ if [ "${AGH_DEFER_DNSMASQ_RESTART:-0}" = "1" ]; then
+ AGH_RELOAD_PENDING=1
+ return 0
+ fi
+ ubus call service event '{"type":"config.change","data":{"package":"AdGuardHome"}}' >/dev/null 2>&1
+}
+
+rm_port53() {
+ local configpath
+ config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
+
+ local AdGuardHome_PORT dnsmasq_port
+ AdGuardHome_PORT=$(config_editor "dns.port" "" "$configpath" "1")
+ dnsmasq_port=$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)
+ [ -z "$dnsmasq_port" ] && dnsmasq_port="53"
+
+ if [ "$dnsmasq_port" = "$AdGuardHome_PORT" ]; then
+ [ "$dnsmasq_port" = "53" ] && dnsmasq_port="1745"
+ elif [ "$dnsmasq_port" = "53" ]; then
+ return
+ fi
+
+ stop_core || return 1
+ config_editor "dns.port" "$dnsmasq_port" "$configpath"
+ [ "$(config_editor "dns.port" "" "$configpath" "1")" = "$dnsmasq_port" ] || return 1
+ uci set dhcp.@dnsmasq[0].port="53" || return 1
+ uci commit dhcp || return 1
+ apply_dnsmasq_config "$dnsmasq_port" || return 1
+ agh_reload
+}
+
+use_port53() {
+ local configpath
+ config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
+
+ local AdGuardHome_PORT dnsmasq_port
+ AdGuardHome_PORT=$(config_editor "dns.port" "" "$configpath" "1")
+ dnsmasq_port=$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)
+ [ -z "$dnsmasq_port" ] && dnsmasq_port="53"
+
+ if [ "$dnsmasq_port" = "$AdGuardHome_PORT" ]; then
+ [ "$dnsmasq_port" = "53" ] && AdGuardHome_PORT="1745"
+ elif [ "$AdGuardHome_PORT" = "53" ]; then
+ return
+ fi
+
+ stop_core || return 1
+ config_editor "dns.port" "53" "$configpath"
+ [ "$(config_editor "dns.port" "" "$configpath" "1")" = "53" ] || return 1
+ uci set dhcp.@dnsmasq[0].port="$AdGuardHome_PORT" || return 1
+ uci commit dhcp || return 1
+ apply_dnsmasq_config "$dnsmasq_port" || return 1
+ agh_reload
+}
+
+mark_redirect_flag() {
+ local enabled="$1" redirect="$2" agh_port="$3"
+
+ local configpath
+ config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
+
+ local flag=0
+
+ if [ "$enabled" = "1" ] && [ "$redirect" != "none" ]; then
+ flag=1
+
+ if [ "$redirect" = "redirect" ]; then
+ nft list table inet "$NFT_TABLE" >/dev/null 2>&1 || flag=0
+
+ elif [ "$redirect" = "dnsmasq-upstream" ]; then
+ local s
+ s="$(uci -q get dhcp.@dnsmasq[0].server | tr '\n' ' ')"
+ printf '%s\n' "$s" | grep -q -E "(^|[[:space:]])127\.0\.0\.1#${agh_port}([[:space:]]|$)" || flag=0
+
+ elif [ "$redirect" = "exchange" ]; then
+ local cfgp dport
+ cfgp="$(config_editor "dns.port" "" "$configpath" "1")"
+ dport="$(uci -q get dhcp.@dnsmasq[0].port)"
+ if [ "$cfgp" != "53" ] || [ "$dport" = "53" ] ; then
+ flag=0
+ fi
+ fi
+ fi
+
+ echo -n "$flag" > /var/run/AdGredir
+}
+
+finish_dnsmasq_config() {
+ case "$old_redirect:$redirect" in
+ *dnsmasq-upstream*|*exchange*)
+ if [ "$dnsmasq_before" != "$(uci -q export dhcp)" ] || [ "$previous_flag" != "1" ] || [ "$enabled" = "0" ]; then
+ local released_port=""
+ if [ "$enabled" = "1" ] && [ "$redirect" = "exchange" ]; then
+ released_port=53
+ elif [ "$dnsmasq_previous_port" != "$(uci -q get dhcp.@dnsmasq[0].port)" ]; then
+ released_port="$dnsmasq_previous_port"
+ fi
+ restart_dnsmasq "$released_port" || return 1
+ fi
+ ;;
+ esac
+ if { [ "$enabled" = "0" ] || [ "$redirect" != "dnsmasq-upstream" ]; } &&
+ [ "$(uci -q get AdGuardHome.dnsmasq_backup.ready)" = "1" ]; then
+ uci -q delete AdGuardHome.dnsmasq_backup
+ uci commit AdGuardHome || return 1
+ fi
+ return 0
+}
+
+_do_redirect() (
+ exec 8>/var/lock/AdGuardHome-redirect.lock || return 1
+ flock -w 10 8 || return 1
+ local AGH_DEFER_DNSMASQ_RESTART=1 AGH_RELOAD_PENDING=0
+ local dnsmasq_before dnsmasq_previous_port previous_flag
+ dnsmasq_before="$(uci -q export dhcp)" || return 1
+ dnsmasq_previous_port="$(uci -q get dhcp.@dnsmasq[0].port)"
+ dnsmasq_previous_port="${dnsmasq_previous_port:-53}"
+ previous_flag="$(cat /var/run/AdGredir 2>/dev/null)"
+ echo -n "0" > /var/run/AdGredir
+ local section="$CONFIGURATION"
+ args=""
+ ipv6_server=1
+ tcp_server=0
+ enabled="$1"
+ AGH_REDIRECT_RELOAD="${2:-$enabled}"
+
+ config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
+ AdGuardHome_PORT="$(config_editor "dns.port" "" "$configpath" "1")"
+ [ -z "$AdGuardHome_PORT" ] && AdGuardHome_PORT="0"
+
+ config_get redirect "$section" redirect "none"
+ config_get old_redirect "$section" old_redirect "none"
+ config_get old_port "$section" old_port "0"
+ config_get old_enabled "$section" old_enabled "0"
+
+ uci get dhcp.@dnsmasq[0].port >/dev/null 2>&1 || uci set dhcp.@dnsmasq[0].port="53" >/dev/null 2>&1
+ uci commit dhcp
+
+ if [ "$old_enabled" = "1" ] && [ "$old_redirect" = "exchange" ]; then
+ AdGuardHome_PORT="$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)"
+ fi
+
+ if [ "$old_redirect" != "$redirect" ] || [ "$old_port" != "$AdGuardHome_PORT" ] || { [ "$old_enabled" = "1" ] && [ "$enabled" = "0" ]; }; then
+ if [ "$old_redirect" != "none" ]; then
+ if [ "$old_redirect" = "redirect" ] && [ "$old_port" != "0" ]; then
+ clear_nft_redirect
+ elif [ "$old_redirect" = "dnsmasq-upstream" ]; then
+ if [ "$old_enabled" = "1" ] || [ "$(uci -q get AdGuardHome.dnsmasq_backup.ready)" = "1" ]; then
+ if ! stop_forward_dnsmasq "$old_port"; then
+ echo -n "0" > /var/run/AdGredir
+ return 1
+ fi
+ fi
+ old_enabled=0
+ elif [ "$old_redirect" = "exchange" ]; then
+ if ! rm_port53; then
+ echo -n "0" > /var/run/AdGredir
+ return 1
+ fi
+ fi
+ fi
+ elif [ "$old_enabled" = "1" ] && [ "$enabled" = "1" ]; then
+ if [ "$old_redirect" = "redirect" ] && [ "$old_port" != "0" ]; then
+ clear_nft_redirect
+ fi
+ fi
+
+ uci delete AdGuardHome.@AdGuardHome[0].old_redirect 2>/dev/null
+ uci delete AdGuardHome.@AdGuardHome[0].old_port 2>/dev/null
+ uci delete AdGuardHome.@AdGuardHome[0].old_enabled 2>/dev/null
+ uci add_list AdGuardHome.@AdGuardHome[0].old_redirect="$redirect" 2>/dev/null
+ uci add_list AdGuardHome.@AdGuardHome[0].old_port="$AdGuardHome_PORT" 2>/dev/null
+ uci add_list AdGuardHome.@AdGuardHome[0].old_enabled="$enabled" 2>/dev/null
+ uci commit AdGuardHome
+
+ if [ "$enabled" = "0" ] || [ "$AdGuardHome_PORT" = "0" ]; then
+ finish_dnsmasq_config || return 1
+ echo -n "0" > /var/run/AdGredir
+ return 1
+ fi
+
+ if [ "$redirect" = "redirect" ]; then
+ set_nft_redirect "$AdGuardHome_PORT"
+ elif [ "$redirect" = "dnsmasq-upstream" ]; then
+ if ! set_forward_dnsmasq "$AdGuardHome_PORT" "$old_redirect" "$old_enabled" "$old_port"; then
+ echo -n "0" > /var/run/AdGredir
+ return 1
+ fi
+ elif [ "$redirect" = "exchange" ] && [ "$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)" = "53" ]; then
+ if ! use_port53; then
+ echo -n "0" > /var/run/AdGredir
+ return 1
+ fi
+ fi
+
+ finish_dnsmasq_config || return 1
+ mark_redirect_flag "$enabled" "$redirect" "$AdGuardHome_PORT" || return 1
+ AGH_DEFER_DNSMASQ_RESTART=0
+ [ "$AGH_RELOAD_PENDING" = "0" ] || agh_reload
+ return 0
+)
+
+do_redirect() {
+ config_load "${CONFIGURATION}"
+ _do_redirect "$1"
}
service_triggers() {
- procd_add_reload_trigger "$CONFIGURATION"
- [ "$(uci get AdGuardHome.AdGuardHome.redirect 2>/dev/null)" = "redirect" ] && procd_add_reload_trigger firewall
+ procd_add_reload_trigger "$CONFIGURATION"
}
-isrunning(){
- config_load "${CONFIGURATION}"
- _isrunning
- local r=$?
- if [ "$r" = "0" ]; then
- echo "running"
- elif [ "$r" = "1" ]; then
- echo "not run"
- else
- echo "no bin"
- fi
- return $r
+isrunning() {
+ config_load "${CONFIGURATION}"
+ _isrunning
+ local r=$?
+ [ "$r" = "0" ] && echo "running" || ([ "$r" = "1" ] && echo "not run" || echo "no bin")
+ return $r
}
-_isrunning(){
- config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome"
- [ ! -f "$binpath" ] && return 2
- # 用进程名匹配,避免 pgrep -f 自匹配
- pgrep "${binpath##*/}" >/dev/null 2>&1 && return 0
- return 1
+_isrunning() {
+ config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome/AdGuardHome"
+ [ ! -f "$binpath" ] && return 2
+ pgrep -f "$(basename "$binpath")" >/dev/null 2>&1 && return 0
+ return 1
}
-force_reload(){
- config_load "${CONFIGURATION}"
- if _isrunning; then
- procd_send_signal "$CONFIGURATION"
- else
- start
- fi
+force_reload() {
+ config_load "${CONFIGURATION}"
+ _isrunning && procd_send_signal "$CONFIGURATION" || start
}
-get_tz()
-{
- SET_TZ=""
-
- [ -e "/etc/localtime" ] && return
-
- for tzfile in /etc/TZ /var/etc/TZ; do
- [ ! -e "$tzfile" ] && continue
- tz="$(cat "$tzfile" 2>/dev/null)"
- done
-
- [ -z "$tz" ] && return
- SET_TZ="$tz"
+get_tz() {
+ SET_TZ=""
+ [ -e "/etc/localtime" ] && return
+ for tzfile in /etc/TZ /var/etc/TZ; do
+ [ -e "$tzfile" ] && tz="$(cat "$tzfile" 2>/dev/null)" && break
+ done
+ [ -n "$tz" ] && SET_TZ=$tz
}
-rm_port53()
-{
- local AdGuardHome_PORT
- AdGuardHome_PORT="$(config_editor "dns.port" "" "$configpath" "1")"
- local dnsmasq_port="$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)"
- [ -z "$dnsmasq_port" ] && dnsmasq_port="53"
-
- if [ "$dnsmasq_port" = "$AdGuardHome_PORT" ]; then
- [ "$dnsmasq_port" = "53" ] && dnsmasq_port="1745"
- elif [ "$dnsmasq_port" = "53" ]; then
- return
- fi
-
- config_editor "dns.port" "$dnsmasq_port" "$configpath"
- uci set dhcp.@dnsmasq[0].port="53"
- uci commit dhcp
-
- config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome"
- local binname="${binpath##*/}"
- pkill -TERM "$binname" 2>/dev/null
- sleep 2
- pkill -KILL "$binname" 2>/dev/null
- /etc/init.d/dnsmasq restart
+get_filesystem() {
+ echo "$1" | awk '
+ BEGIN{
+ while (("mount"| getline ret) > 0) {
+ split(ret,d); fs[d[3]]=d[5];
+ m=index(d[1],":");
+ if (m==0) { pt[d[3]]=d[1] } else { pt[d[3]]=substr(d[1],m+1) }
+ }
+ }
+ {
+ split($0,d,"/");
+ if ("/" in fs) { result1=fs["/"]; }
+ if ("/" in pt) { result2=pt["/"]; }
+ for (i=2;i<=length(d);i++) {
+ p[i]=p[i-1]"/"d[i];
+ if (p[i] in fs) { result1=fs[p[i]]; result2=pt[p[i]]; }
+ }
+ if (result2 in fs) { result=fs[result2] } else { result=result1 }
+ print(result);
+ }'
}
-use_port53()
-{
- local AdGuardHome_PORT
- AdGuardHome_PORT="$(config_editor "dns.port" "" "$configpath" "1")"
- local dnsmasq_port="$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)"
- [ -z "$dnsmasq_port" ] && dnsmasq_port="53"
- # AGH 端口未配置或无法读取时,使用默认 1745 作为 dnsmasq 的新端口
- [ -z "$AdGuardHome_PORT" ] && AdGuardHome_PORT="1745"
-
- if [ "$dnsmasq_port" = "$AdGuardHome_PORT" ]; then
- [ "$dnsmasq_port" = "53" ] && AdGuardHome_PORT="1745"
- elif [ "$AdGuardHome_PORT" = "53" ]; then
- return
- fi
-
- config_editor "dns.port" "53" "$configpath"
- uci set dhcp.@dnsmasq[0].port="$AdGuardHome_PORT"
- uci commit dhcp
- /etc/init.d/dnsmasq reload
-}
-
-do_redirect()
-{
- config_load "${CONFIGURATION}"
- _do_redirect "$1"
-}
-
-_do_redirect()
-{
- local section="$CONFIGURATION"
- ipv6_server=1
- tcp_server=0
- local enabled="$1"
-
- if [ "$enabled" = "1" ]; then
- echo -n "1" > /var/run/AdGredir
- else
- echo -n "0" > /var/run/AdGredir
- fi
-
- config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
- AdGuardHome_PORT="$(config_editor "dns.port" "" "$configpath" "1")"
-
- [ ! -s "$configpath" ] && cp -f /usr/share/AdGuardHome/AdGuardHome_template.yaml "$configpath"
- [ -z "$AdGuardHome_PORT" ] && AdGuardHome_PORT="0"
-
- config_get redirect "$section" redirect "none"
- config_get old_redirect "$section" old_redirect "none"
- config_get old_port "$section" old_port "0"
- config_get old_enabled "$section" old_enabled "0"
-
- uci get dhcp.@dnsmasq[0].port >/dev/null 2>&1 || uci set dhcp.@dnsmasq[0].port="53" >/dev/null 2>&1
-
- if [ "$old_enabled" = "1" ] && [ "$old_redirect" = "exchange" ]; then
- AdGuardHome_PORT="$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)"
- fi
-
- if [ "$old_redirect" != "$redirect" ] || [ "$old_port" != "$AdGuardHome_PORT" ] || { [ "$old_enabled" = "1" ] && [ "$enabled" = "0" ]; }; then
- if [ "$old_redirect" != "none" ]; then
- if [ "$old_redirect" = "redirect" ] && [ "$old_port" != "0" ]; then
- clear_iptable "$old_port" "$ipv6_server"
- elif [ "$old_redirect" = "dnsmasq-upstream" ]; then
- stop_forward_dnsmasq "$old_port"
- elif [ "$old_redirect" = "exchange" ]; then
- rm_port53
- fi
- fi
- elif [ "$old_enabled" = "1" ] && [ "$enabled" = "1" ]; then
- if [ "$old_redirect" = "redirect" ] && [ "$old_port" != "0" ]; then
- clear_iptable "$old_port" "$ipv6_server"
- fi
- fi
-
- uci delete AdGuardHome.@AdGuardHome[0].old_redirect 2>/dev/null
- uci delete AdGuardHome.@AdGuardHome[0].old_port 2>/dev/null
- uci delete AdGuardHome.@AdGuardHome[0].old_enabled 2>/dev/null
- uci add_list AdGuardHome.@AdGuardHome[0].old_redirect="$redirect" 2>/dev/null
- uci add_list AdGuardHome.@AdGuardHome[0].old_port="$AdGuardHome_PORT" 2>/dev/null
- uci add_list AdGuardHome.@AdGuardHome[0].old_enabled="$enabled" 2>/dev/null
- uci commit AdGuardHome
-
- [ "$enabled" = "0" ] && return 1
- [ "$AdGuardHome_PORT" = "0" ] && return 1
-
- if [ "$redirect" = "redirect" ]; then
- set_iptable "$ipv6_server" "$tcp_server"
- elif [ "$redirect" = "dnsmasq-upstream" ]; then
- set_forward_dnsmasq "$AdGuardHome_PORT"
- elif [ "$redirect" = "exchange" ] && [ "$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)" = "53" ]; then
- use_port53
- fi
-}
-
-get_filesystem()
-{
- local path="$1"
- local mntpt=""
- local fstype=""
-
- while IFS= read -r line; do
- case "$line" in
- /dev/root|/dev/ubi0*|/dev/mtdblock*)
- continue
- ;;
- esac
- local mp="$(echo "$line" | awk '{print $2}')"
- local fs="$(echo "$line" | awk '{print $3}')"
- case "$path" in
- "$mp"*)
- fstype="$fs"
- mntpt="$mp"
- break
- ;;
- esac
- done < /proc/mounts
-
- if [ -z "$mntpt" ]; then
- mntpt="/"
- fstype="$(awk '$2=="/" {print $3}' /proc/mounts)"
- fi
-
- echo "$fstype"
-}
-
-config_editor()
-{
- local yaml="$1"
- local value="$2"
- local file="$3"
- local ro="$4"
-
- if [ "$ro" = "1" ]; then
- awk -v yaml="$yaml" '
- BEGIN {
- split(yaml, part, "\\.")
- i = 1
- l = length(part)
- s = ""
- }
- {
- if (match($0, s "" part[i] ":")) {
- if (i == l) {
- sub(/^[^:]+: */, "")
- print
- exit
- }
- s = s "[- ]{2}"
- i++
- }
- }' "$file"
- else
- # 安全写入:用 sed 只改匹配行,不改其他内容
- # yaml 路径如 "dns.port" → 匹配带缩进的 "port:" 行
- local last_key
- last_key="${yaml##*.}"
- if grep -qE "^[[:space:]]*${last_key}:" "$file" 2>/dev/null; then
- sed -i "s|^\\([[:space:]]*${last_key}:\\).*|\\1 ${value}|" "$file"
- else
- echo "${last_key}: ${value}" >> "$file"
- fi
- fi
+config_editor() {
+ awk -v yaml="$1" -v value="$2" -v file="$3" -v ro="$4" '
+ BEGIN{split(yaml,part,"\.");s="";i=1;l=length(part);}
+ {
+ if (match($0,s""part[i]":")) {
+ if (i==l) {
+ split($0,t,": ");
+ if (ro==""){
+ system("sed -i '\''"FNR"c\\" t[1] ": " value "'\'' " file);
+ } else {
+ print(t[2]);
+ }
+ exit;
+ }
+ s=s"[- ]{2}";
+ i++;
+ }
+ }' "$3"
}
boot_service() {
- rm -f /var/run/AdGserverdis >/dev/null 2>&1
- config_load "${CONFIGURATION}"
- config_get waitonboot $CONFIGURATION waitonboot "0"
- config_get_bool enabled $CONFIGURATION enabled 0
- config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome"
-
- [ -f "$binpath" ] && start_service
-
- if [ "$enabled" = "1" ] && [ "$waitonboot" = "1" ]; then
- procd_open_instance
- procd_set_param command "/usr/share/AdGuardHome/waitnet.sh"
- procd_close_instance
- echo "AdGuardHome: waiting for network..."
- fi
-}
-
-testbackup(){
- config_load "${CONFIGURATION}"
- if [ "$1" = "backup" ]; then
- backup
- elif [ "$1" = "restore" ]; then
- restore
- fi
-}
-
-restore()
-{
- config_get workdir $CONFIGURATION workdir "/etc/AdGuardHome"
- config_get backupwdpath $CONFIGURATION backupwdpath "/etc/AdGuardHome"
- [ -d "$backupwdpath/data" ] && cp -u -r -f "$backupwdpath/data" "$workdir/"
-}
-
-backup() {
- config_get backupwdpath $CONFIGURATION backupwdpath "/etc/AdGuardHome"
- mkdir -p "$backupwdpath/data"
- config_get workdir $CONFIGURATION workdir "/etc/AdGuardHome"
- config_get backupfile $CONFIGURATION backupfile ""
-
- for one in $backupfile; do
- while : ; do
- local cpret
- if [ -d "$backupwdpath/data/$one" ]; then
- cpret="$(cp -u -r -f "$workdir/data/$one" "$backupwdpath/data" 2>&1)"
- else
- cpret="$(cp -u -r -f "$workdir/data/$one" "$backupwdpath/data/$one" 2>&1)"
- fi
- echo "$cpret"
- echo "$cpret" | grep -q "no space left on device"
- if [ $? -eq 0 ]; then
- echo "disk full, deleting querylog and retrying..."
- del_querylog && continue
- rm -rf "$backupwdpath/data/filters"
- rm -rf "$workdir/data/filters" && continue
- echo "backup failed"
- fi
- break
- done
- done
+ rm -f /var/run/AdGserverdis 2>/dev/null
+ config_load "${CONFIGURATION}"
+ config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome/AdGuardHome"
+ [ -f "$binpath" ] && start_service
}
start_service() {
- rm -f /var/run/AdGserverdis >/dev/null 2>&1
- config_load "${CONFIGURATION}"
+ stop_core || return 1
+ rm -f /var/run/AdGserverdis 2>/dev/null
+ config_load "${CONFIGURATION}"
+ config_get hashpass $CONFIGURATION hashpass ""
+ config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
+ if [ -n "$hashpass" ]; then
+ config_editor "users.password" "$hashpass" "$configpath"
+ uci set $CONFIGURATION.$CONFIGURATION.hashpass=""
+ uci commit $CONFIGURATION
+ fi
+ local enabled
+ config_get_bool enabled $CONFIGURATION enabled 0
+ if [ "$enabled" = "0" ]; then
+ _do_redirect 0
+ return
+ fi
+ config_get workdir $CONFIGURATION workdir "/usr/bin/AdGuardHome"
- config_get hashpass $CONFIGURATION hashpass ""
- config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
+ mkdir -p "$workdir/data"
+ local cwdfs
+ cwdfs=$(get_filesystem "$workdir")
+ echo "workdir is a $cwdfs filesystem"
+ if [ "$cwdfs" = "jffs2" ]; then
+ echo "fs error ln db to tmp $workdir $cwdfs"
+ logger "AdGuardHome" "warning db redirect to tmp"
+ touch "$workdir/data/stats.db"
+ if [ ! -L "$workdir/data/stats.db" ]; then
+ mv -f "$workdir/data/stats.db" /tmp/stats.db 2>/dev/null
+ ln -s /tmp/stats.db "$workdir/data/stats.db" 2>/dev/null
+ fi
+ touch "$workdir/data/sessions.db"
+ if [ ! -L "$workdir/data/sessions.db" ]; then
+ mv -f "$workdir/data/sessions.db" /tmp/sessions.db 2>/dev/null
+ ln -s /tmp/sessions.db "$workdir/data/sessions.db" 2>/dev/null
+ fi
+ fi
+ local ADDITIONAL_ARGS=""
+ config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome/AdGuardHome"
- if [ -n "$hashpass" ]; then
- config_editor "users.password" "$hashpass" "$configpath"
- uci set "$CONFIGURATION"."$CONFIGURATION".hashpass=""
- uci commit AdGuardHome
- fi
+ mkdir -p "${binpath%/*}"
+ ADDITIONAL_ARGS="$ADDITIONAL_ARGS -c $configpath"
+ ADDITIONAL_ARGS="$ADDITIONAL_ARGS -w $workdir"
+ config_get httpport $CONFIGURATION httpport 3000
+ ADDITIONAL_ARGS="$ADDITIONAL_ARGS -p $httpport"
- local enabled
- config_get_bool enabled $CONFIGURATION enabled 0
+ printf '%s\n' "$binpath" "$configpath" > /lib/upgrade/keep.d/luci-app-adguardhome
- do_crontab
+ config_get logfile $CONFIGURATION logfile ""
+ if [ -n "$logfile" ]; then
+ ADDITIONAL_ARGS="$ADDITIONAL_ARGS -l $logfile"
+ fi
- if [ "$enabled" = "0" ]; then
- _do_redirect 0
- local binpid
- config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome"
- # 用进程名匹配,避免 pgrep -f 自匹配
- binpid=$(pgrep "${binpath##*/}" 2>/dev/null | head -1)
- if [ -n "$binpid" ]; then
- kill -TERM "$binpid" 2>/dev/null
- sleep 1
- kill -KILL "$binpid" 2>/dev/null
- fi
- return
- fi
+ if [ ! -f "$binpath" ]; then
+ _do_redirect 0
+ /usr/share/AdGuardHome/update_core.sh >/dev/null 2>&1 &
+ exit 0
+ fi
- config_get workdir $CONFIGURATION workdir "/etc/AdGuardHome"
- config_get backupfile $CONFIGURATION backupfile ""
- mkdir -p "$workdir/data"
+ config_get_bool verbose $CONFIGURATION verbose 0
+ if [ "$verbose" -eq 1 ]; then
+ ADDITIONAL_ARGS="$ADDITIONAL_ARGS -v"
+ fi
- if [ -n "$backupfile" ] && [ ! -d "$workdir/data" ]; then
- restore
- fi
+ if [ "$(uci -q get bypass.@global[0].global_server)" ] && [ "$(uci -q get bypass.@global[0].adguardhome)" = "1" ] && [ "$(uci -q get dhcp.@dnsmasq[0].port)" = "53" ]; then
+ uci -q set AdGuardHome.AdGuardHome.redirect='exchange'
+ uci -q commit AdGuardHome
+ config_load "${CONFIGURATION}"
+ fi
- local cwdfs
- cwdfs="$(get_filesystem "$workdir")"
- echo "AdGuardHome: workdir is on $cwdfs filesystem"
- if [ "$cwdfs" = "jffs2" ]; then
- logger -t AdGuardHome -p daemon.warn "jffs2 detected, redirecting db to tmp"
- mkdir -p /tmp/adg_tmp
- for db in stats.db sessions.db; do
- touch "$workdir/data/$db"
- [ ! -L "$workdir/data/$db" ] && mv -f "$workdir/data/$db" "/tmp/adg_tmp/$db" 2>/dev/null
- [ ! -e "/tmp/adg_tmp/$db" ] && touch "/tmp/adg_tmp/$db"
- [ -f "/tmp/adg_tmp/$db" ] && ln -sf "/tmp/adg_tmp/$db" "$workdir/data/$db"
- done
- fi
+ if [ -f "$configpath" ]; then
+ if ! _do_redirect 1 0; then
+ [ "$(config_editor "dns.port" "" "$configpath" "1")" = "0" ] || return 1
+ fi
+ else
+ _do_redirect 0 0
+ fi
- local ADDITIONAL_ARGS=""
- config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome"
- mkdir -p "${binpath%/*}"
+ procd_open_instance
+ get_tz
+ if [ -n "$SET_TZ" ]; then
+ procd_set_param env TZ="$SET_TZ"
+ fi
+ procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
+ procd_set_param limits core="unlimited" nofile="65535 65535"
+ procd_set_param stderr 1
+ procd_set_param command "$binpath" $ADDITIONAL_ARGS
+ procd_set_param file "$configpath" "/etc/hosts" "/etc/config/AdGuardHome"
+ procd_close_instance
+ if [ ! -f "$configpath" ]; then
+ config_get "redirect" "AdGuardHome" "redirect" "none"
+ if [ "$redirect" != "none" ]; then
+ procd_open_instance "waitconfig"
+ procd_set_param command "/usr/share/AdGuardHome/watchconfig.sh"
+ procd_close_instance
+ echo "no config start watching"
+ fi
+ fi
+ echo "AdGuardHome service enabled"
+ echo "luci enable switch=$enabled"
- ADDITIONAL_ARGS="-c $configpath -w $workdir"
- config_get httpport $CONFIGURATION httpport 3000
- ADDITIONAL_ARGS="$ADDITIONAL_ARGS -p $httpport"
-
- config_get upprotect $CONFIGURATION upprotect ""
- if [ -n "$upprotect" ]; then
- {
- for item in $upprotect; do
- echo "$item" | sed 's/\\n/\n/g'
- done
- } > /lib/upgrade/keep.d/luci-app-adguardhome
- fi
-
- config_get logfile $CONFIGURATION logfile ""
- [ -n "$logfile" ] && ADDITIONAL_ARGS="$ADDITIONAL_ARGS -l $logfile"
-
- if [ ! -f "$binpath" ]; then
- _do_redirect 0
- /usr/share/AdGuardHome/update_core.sh >/tmp/AdGuardHome_update.log 2>&1 &
- exit 0
- fi
-
- config_get_bool verbose $CONFIGURATION verbose 0
- [ "$verbose" -eq 1 ] && ADDITIONAL_ARGS="$ADDITIONAL_ARGS -v"
-
- config_get user $CONFIGURATION user ""
- config_get group $CONFIGURATION group ""
- config_get gc $CONFIGURATION gc "0"
- config_get maxprocs $CONFIGURATION maxprocs "0"
- config_get memlimit $CONFIGURATION memlimit "0"
-
- procd_open_instance
- get_tz
- [ -n "$SET_TZ" ] && procd_set_param env TZ="$SET_TZ"
- procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
- procd_set_param limits core="unlimited" nofile="65535 65535"
- procd_set_param stderr 1
- [ -n "$user" ] && procd_set_param user "$user"
- [ -n "$group" ] && procd_set_param group "$group"
- [ "$gc" != "0" ] && procd_set_param env GOGC="$gc"
- [ "$maxprocs" != "0" ] && procd_set_param env GOMAXPROCS="$maxprocs"
- [ -n "$memlimit" ] && [ "$memlimit" != "0" ] && procd_set_param env GOMEMLIMIT="${memlimit}MiB"
-
- # DynamicList 写入 uci 是 `list jail_mount '...'` 格式
- config_list_foreach $CONFIGURATION jail_mount procd_add_jail_mount
- config_list_foreach $CONFIGURATION jail_mount_rw procd_add_jail_mount_rw
-
- # 兼容旧的 newline-separated `option jail_mount '...'` 格式(迁移期)
- local jm jmrw
- jm="$(uci -q get AdGuardHome.AdGuardHome.jail_mount 2>/dev/null)"
- case "$jm" in
- *$'\n'*)
- printf '%s\n' "$jm" | while IFS= read -r mp; do
- [ -n "$mp" ] && procd_add_jail_mount "$mp"
- done
- ;;
- esac
- jmrw="$(uci -q get AdGuardHome.AdGuardHome.jail_mount_rw 2>/dev/null)"
- case "$jmrw" in
- *$'\n'*)
- printf '%s\n' "$jmrw" | while IFS= read -r mp; do
- [ -n "$mp" ] && procd_add_jail_mount_rw "$mp"
- done
- ;;
- esac
-
- procd_set_param command $binpath $ADDITIONAL_ARGS
- procd_set_param file "$configpath" "/etc/hosts" "/etc/config/AdGuardHome"
- procd_close_instance
-
- if [ -f "$configpath" ]; then
- _do_redirect 1
- else
- _do_redirect 0
- config_get redirect "$CONFIGURATION" redirect "none"
- if [ "$redirect" != "none" ]; then
- procd_open_instance
- procd_set_param command "/usr/share/AdGuardHome/watchconfig.sh"
- procd_close_instance
- echo "AdGuardHome: no config found, watching for config file..."
- fi
- fi
-
- echo "AdGuardHome service enabled (switch=$enabled)"
-
- (sleep 10 && \
- pgrep "${binpath##*/}" >/dev/null 2>&1 || { \
- logger -t AdGuardHome "no process found after 10s, canceling redirect"; \
- _do_redirect 0; \
- }) &
-
- local bypass_global="$(uci get bypass.@global[0].global_server 2>/dev/null)"
- local bypass_adg="$(uci get bypass.@global[0].adguardhome 2>/dev/null)"
- local dnsmasq_port="$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)"
- if [ -n "$bypass_global" ] && [ "$bypass_adg" = "1" ] && [ "$dnsmasq_port" = "53" ]; then
- uci -q set AdGuardHome.AdGuardHome.redirect='exchange'
- uci commit AdGuardHome
- do_redirect 1
- fi
}
-reload_service()
-{
- rm -f /var/run/AdGlucitest >/dev/null 2>&1
- echo "AdGuardHome reloading"
- start
+reload_service() {
+ rm -f /var/run/AdGlucitest 2>/dev/null
+ echo "AdGuardHome reloading"
+ start
}
-del_querylog(){
- local backupwdpath
- config_get backupwdpath $CONFIGURATION backupwdpath "/etc/AdGuardHome"
- local workdir
- config_get workdir $CONFIGURATION workdir "/etc/AdGuardHome"
-
- local btarget
- btarget="$(ls "$backupwdpath/data" 2>/dev/null | grep "querylog.json" | sort -r | head -n1)"
- local wtarget
- wtarget="$(ls "$workdir/data" 2>/dev/null | grep "querylog.json" | sort -r | head -n1)"
-
- if [ "$btarget" = "$wtarget" ]; then
- [ -z "$btarget" ] && return 1
- rm -f "$workdir/data/$wtarget"
- rm -f "$backupwdpath/data/$btarget"
- return 0
- fi
-
- if [ "$btarget" \> "$wtarget" ]; then
- rm -f "$backupwdpath/data/$btarget"
- else
- rm -f "$workdir/data/$wtarget"
- fi
- return 0
-}
-
-stop_service()
-{
- config_load "${CONFIGURATION}"
- _do_redirect 0
- do_crontab
-
- if [ "$1" != "nobackup" ]; then
- config_get backupfile $CONFIGURATION backupfile "0"
- [ -n "$backupfile" ] && backup
- fi
-
- echo "AdGuardHome service disabled"
- touch /var/run/AdGserverdis
+stop_service() {
+ stop_core || return 1
+ config_load "${CONFIGURATION}"
+ _do_redirect 0
+ echo "AdGuardHome service disabled"
+ touch /var/run/AdGserverdis
}
boot() {
- rc_procd boot_service "$@"
- if eval "type service_started" 2>/dev/null >/dev/null; then
- service_started
- fi
-}
-
-test_crontab(){
- config_load "${CONFIGURATION}"
- do_crontab
-}
-
-do_crontab(){
- config_get_bool enabled $CONFIGURATION enabled 0
- config_get crontab $CONFIGURATION crontab ""
- local cronreload=0
-
- local findstr default cronenable
- findstr="/usr/share/AdGuardHome/update_core.sh"
- default="30 3 * * * /usr/share/AdGuardHome/update_core.sh"
- [ "$enabled" = "0" ] || [ "${crontab//autoupdate/}" = "$crontab" ] && cronenable=0 || cronenable=1
- crontab_editor
-
- findstr="/usr/share/AdGuardHome/tailto.sh"
- default="0 * * * * /usr/share/AdGuardHome/tailto.sh 2000 \$(uci get AdGuardHome.AdGuardHome.workdir)/data/querylog.json"
- [ "$enabled" = "0" ] || [ "${crontab//cutquerylog/}" = "$crontab" ] && cronenable=0 || cronenable=1
- crontab_editor
-
- findstr="/usr/share/AdGuardHome/tailto.sh"
- default="30 3 * * * /usr/share/AdGuardHome/tailto.sh 2000 \$(uci get AdGuardHome.AdGuardHome.logfile)"
- [ "$enabled" = "0" ] || [ "${crontab//cutruntimelog/}" = "$crontab" ] && cronenable=0 || cronenable=1
- crontab_editor
-
- findstr="/usr/share/AdGuardHome/addhost.sh"
- default="0 * * * * /usr/share/AdGuardHome/addhost.sh"
- [ "$enabled" = "0" ] || [ "${crontab//autohost/}" = "$crontab" ] && cronenable=0 || cronenable=1
- crontab_editor
-
- [ "$cronreload" -gt 0 ] && /etc/init.d/cron restart
-}
-
-crontab_editor(){
- local line
- local reload
- line="$(grep "$findstr" "$CRON_FILE" 2>/dev/null)"
-
- if [ "${line#\#}" != "$line" ]; then
- if [ "$cronenable" = "1" ]; then
- sed -i "\|$findstr|d" "$CRON_FILE"
- echo "${line#\#}" >> "$CRON_FILE"
- cronreload=$((cronreload + 1))
- elif [ -z "$reload" ]; then
- sed -i "\|$findstr|d" "$CRON_FILE"
- echo "$line" >> "$CRON_FILE"
- fi
- else
- if [ "$cronenable" = "1" ]; then
- if [ -z "$line" ]; then
- line="$default"
- reload=1
- fi
- if [ -n "$reload" ]; then
- sed -i "\|$findstr|d" "$CRON_FILE"
- echo "$line" >> "$CRON_FILE"
- cronreload=$((cronreload + 1))
- fi
- elif [ -n "$line" ]; then
- sed -i "\|$findstr|d" "$CRON_FILE"
- echo "#$line" >> "$CRON_FILE"
- cronreload=$((cronreload + 1))
- fi
- fi
-}
+ rc_procd boot_service "$@"
+ eval "type service_started" 2>/dev/null >/dev/null && service_started
+}
\ No newline at end of file
diff --git a/luci-app-adguardhome/root/etc/uci-defaults/40_luci-AdGuardHome b/luci-app-adguardhome/root/etc/uci-defaults/40_luci-AdGuardHome
old mode 100644
new mode 100755
index 37e192cd..e736ba60
--- a/luci-app-adguardhome/root/etc/uci-defaults/40_luci-AdGuardHome
+++ b/luci-app-adguardhome/root/etc/uci-defaults/40_luci-AdGuardHome
@@ -1,15 +1,14 @@
#!/bin/sh
-
+chmod +x /etc/init.d/AdGuardHome
uci -q batch <<-EOF >/dev/null 2>&1
delete ucitrack.@AdGuardHome[-1]
add ucitrack AdGuardHome
set ucitrack.@AdGuardHome[-1].init=AdGuardHome
commit ucitrack
- delete AdGuardHome.AdGuardHome.ucitracktest
- /etc/init.d/AdGuardHome restart
EOF
-rm -f /tmp/luci-indexcache
-
-chmod +x /etc/init.d/AdGuardHome /usr/share/AdGuardHome/*
+# Remove the former menu and both Lua and ucode menu caches.
+rm -f /usr/share/luci/menu.d/luci-app-adguardhome-js.json
+rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
+rm -f /tmp/luci-modulecache/*
exit 0
diff --git a/luci-app-adguardhome/root/lib/upgrade/keep.d/luci-app-adguardhome b/luci-app-adguardhome/root/lib/upgrade/keep.d/luci-app-adguardhome
new file mode 100644
index 00000000..1a5bd6dc
--- /dev/null
+++ b/luci-app-adguardhome/root/lib/upgrade/keep.d/luci-app-adguardhome
@@ -0,0 +1 @@
+/etc/AdGuardHome.yaml
\ No newline at end of file
diff --git a/luci-app-adguardhome/root/usr/lib/lua/luci/i18n/adguardhome.zh-cn.lmo b/luci-app-adguardhome/root/usr/lib/lua/luci/i18n/adguardhome.zh-cn.lmo
deleted file mode 100644
index 7c23d77a..00000000
Binary files a/luci-app-adguardhome/root/usr/lib/lua/luci/i18n/adguardhome.zh-cn.lmo and /dev/null differ
diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/AdGuardHome_template.yaml b/luci-app-adguardhome/root/usr/share/AdGuardHome/AdGuardHome_template.yaml
old mode 100644
new mode 100755
index 612a5770..b6f63852
--- a/luci-app-adguardhome/root/usr/share/AdGuardHome/AdGuardHome_template.yaml
+++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/AdGuardHome_template.yaml
@@ -1,108 +1,123 @@
-bind_host: 0.0.0.0
-bind_port: 3000
-beta_bind_port: 0
+# 模板用户名为 admin 密码为 admin
+# The username is admin and the password is admin.
+# 保存并应用就可以直接用的配置模板文件
+# a template config can be use with a apply
+http:
+ pprof:
+ port: 6060
+ enabled: false
+ address: 0.0.0.0:3000
+ session_ttl: 720h
users:
-- name: root
- password: $2y$10$dwn0hTYoECQMZETBErGlzOId2VANOVsPHsuH13TM/8KnysM5Dh/ve
+ - name: admin
+ password: $2y$10$iGkTW98QtGEZn4I/GC0J4eQhfuLQ53v7L4DDVauEJyWylJrXWwWeO
auth_attempts: 5
block_auth_min: 15
http_proxy: ""
-language: zh-cn
-debug_pprof: false
-web_session_ttl: 720
+language: ""
+theme: auto
dns:
bind_hosts:
- - 0.0.0.0
- port: 1745
- statistics_interval: 30
- querylog_enabled: true
- querylog_file_enabled: true
- querylog_interval: 6h
- querylog_size_memory: 1000
+ - 0.0.0.0
+ port: 3053
anonymize_client_ip: false
- protection_enabled: true
- blocking_mode: default
- blocking_ipv4: ""
- blocking_ipv6: ""
- blocked_response_ttl: 60
- parental_block_host: family-block.dns.adguard.com
- safebrowsing_block_host: standard-block.dns.adguard.com
- ratelimit: 0
+ ratelimit: 20
+ ratelimit_subnet_len_ipv4: 24
+ ratelimit_subnet_len_ipv6: 56
ratelimit_whitelist: []
- refuse_any: false
+ refuse_any: true
upstream_dns:
- - 223.5.5.5
+ - https://dns.cloudflare.com/dns-query
+ - '#'
+ - '# 对 ts.net 相关域名启用 DNS 分流, 交由 Tailscale 解析, 如不使用 Tailscale 可以删除.'
+ - '# Enable DNS conditional forwarding for ts.net via Tailscale; remove if Tailscale is not used.'
+ - '[/ts.net/]100.100.100.100'
upstream_dns_file: ""
bootstrap_dns:
- - 119.29.29.29
- - 223.5.5.5
- all_servers: false
- fastest_addr: false
+ - 1.1.1.1
+ - 1.0.0.1
+ fallback_dns:
+ - quic://dns.adguard-dns.com
+ upstream_mode: parallel
fastest_timeout: 1s
allowed_clients: []
disallowed_clients: []
blocked_hosts:
- - version.bind
- - id.server
- - hostname.bind
+ - version.bind
+ - id.server
+ - hostname.bind
trusted_proxies:
- - 127.0.0.0/8
- - ::1/128
+ - 127.0.0.0/8
+ - ::1/128
+ cache_enabled: true
cache_size: 4194304
cache_ttl_min: 0
cache_ttl_max: 0
- cache_optimistic: true
+ cache_optimistic: false
+ cache_optimistic_answer_ttl: 30s
+ cache_optimistic_max_age: 12h
bogus_nxdomain: []
aaaa_disabled: false
enable_dnssec: false
- edns_client_subnet: false
+ edns_client_subnet:
+ custom_ip: ""
+ enabled: false
+ use_custom: false
max_goroutines: 300
+ handle_ddr: true
ipset: []
- filtering_enabled: true
- filters_update_interval: 24
- parental_enabled: false
- safesearch_enabled: false
- safebrowsing_enabled: false
- safebrowsing_cache_size: 1048576
- safesearch_cache_size: 1048576
- parental_cache_size: 1048576
- cache_time: 30
- rewrites: []
- blocked_services: []
+ ipset_file: ""
+ bootstrap_prefer_ipv6: false
upstream_timeout: 10s
- local_domain_name: lan
- resolve_clients: true
+ private_networks: []
use_private_ptr_resolvers: true
local_ptr_upstreams: []
+ use_dns64: false
+ dns64_prefixes: []
+ serve_http3: false
+ use_http3_upstreams: false
+ serve_plain_dns: true
+ hostsfile_enabled: true
+ pending_requests:
+ enabled: true
tls:
enabled: false
server_name: ""
force_https: false
port_https: 443
port_dns_over_tls: 853
- port_dns_over_quic: 784
+ port_dns_over_quic: 853
port_dnscrypt: 0
dnscrypt_config_file: ""
allow_unencrypted_doh: false
- strict_sni_check: false
certificate_chain: ""
private_key: ""
certificate_path: ""
private_key_path: ""
+ strict_sni_check: false
+querylog:
+ dir_path: ""
+ ignored: []
+ interval: 2160h
+ size_memory: 1000
+ enabled: true
+ file_enabled: true
+statistics:
+ dir_path: ""
+ ignored: []
+ interval: 24h
+ enabled: true
filters:
-- enabled: true
- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt
- name: AdGuard DNS filter
- id: 1628750870
-- enabled: true
- url: https://anti-ad.net/easylist.txt
- name: 'CHN: anti-AD'
- id: 1628750871
+ - enabled: true
+ url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
+ name: AdGuard DNS filter
+ id: 1
whitelist_filters: []
user_rules: []
dhcp:
enabled: false
interface_name: ""
+ local_domain_name: lan
dhcpv4:
gateway_ip: ""
subnet_mask: ""
@@ -116,16 +131,59 @@ dhcp:
lease_duration: 86400
ra_slaac_only: false
ra_allow_slaac: false
-clients: []
-log_compress: false
-log_localtime: false
-log_max_backups: 0
-log_max_size: 100
-log_max_age: 3
-log_file: ""
-verbose: false
+filtering:
+ blocking_ipv4: ""
+ blocking_ipv6: ""
+ blocked_services:
+ schedule:
+ time_zone: UTC
+ ids: []
+ protection_disabled_until: null
+ safe_search:
+ enabled: false
+ bing: true
+ duckduckgo: true
+ ecosia: true
+ google: true
+ pixabay: true
+ yandex: true
+ youtube: true
+ blocking_mode: default
+ parental_block_host: family-block.dns.adguard.com
+ safebrowsing_block_host: standard-block.dns.adguard.com
+ rewrites: []
+ safe_fs_patterns:
+ - /tmp/AdGuardHome/userfilters/*
+ safebrowsing_cache_size: 1048576
+ safesearch_cache_size: 1048576
+ parental_cache_size: 1048576
+ cache_time: 30
+ filters_update_interval: 24
+ blocked_response_ttl: 10
+ filtering_enabled: true
+ rewrites_enabled: true
+ parental_enabled: false
+ safebrowsing_enabled: false
+ protection_enabled: true
+clients:
+ runtime_sources:
+ whois: true
+ arp: true
+ rdns: true
+ dhcp: true
+ hosts: true
+ persistent: []
+log:
+ enabled: true
+ file: ""
+ max_backups: 0
+ max_size: 100
+ max_age: 3
+ compress: false
+ local_time: false
+ verbose: false
os:
group: ""
user: ""
rlimit_nofile: 0
-schema_version: 12
+schema_version: 32
\ No newline at end of file
diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/addhost.sh b/luci-app-adguardhome/root/usr/share/AdGuardHome/addhost.sh
deleted file mode 100755
index a23a2efc..00000000
--- a/luci-app-adguardhome/root/usr/share/AdGuardHome/addhost.sh
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/bin/sh
-
-checkmd5(){
- local nowmd5
- nowmd5="$(md5sum /etc/hosts 2>/dev/null)"
- nowmd5="${nowmd5%% *}"
- local lastmd5
- lastmd5="$(uci get AdGuardHome.AdGuardHome.hostsmd5 2>/dev/null)"
- if [ "$nowmd5" != "$lastmd5" ]; then
- uci set AdGuardHome.AdGuardHome.hostsmd5="$nowmd5"
- uci commit AdGuardHome
- [ "$1" != "noreload" ] && /etc/init.d/AdGuardHome reload >/dev/null 2>&1
- fi
-}
-
-if [ "$1" = "del" ]; then
- sed -i '/programaddstart/,/programaddend/d' /etc/hosts
- checkmd5 "$2"
- exit 0
-fi
-
-awk 'BEGIN {
- dhcp[""] = ""
-}
-{
- if (FILENAME == "/tmp/dhcp.leases" && (NF >= 4)) {
- dhcp[$2] = $4
- }
-}
-END {
- cmd = "ip -6 neighbor show 2>/dev/null | grep -v fe80"
- while ((cmd | getline line) > 0) {
- split(line, f)
- if (f[5] in dhcp && f[5] != "") {
- print f[1] " " dhcp[f[5]]
- }
- }
- close(cmd)
- print "#programaddend"
-}' /tmp/dhcp.leases > /tmp/tmphost 2>/dev/null
-
-grep -q "programaddstart" /etc/hosts 2>/dev/null
-if [ $? -eq 0 ]; then
- sed -i '/programaddstart/,/programaddend/c\#programaddstart' /etc/hosts
- sed -i '/programaddstart/r /tmp/tmphost' /etc/hosts
-else
- {
- echo "#programaddstart"
- cat /tmp/tmphost
- } >> /etc/hosts
-fi
-
-rm -f /tmp/tmphost
-checkmd5 "$2"
diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/adguardhome.nft.tpl b/luci-app-adguardhome/root/usr/share/AdGuardHome/adguardhome.nft.tpl
new file mode 100644
index 00000000..67eef35e
--- /dev/null
+++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/adguardhome.nft.tpl
@@ -0,0 +1,10 @@
+table inet adguardhome {
+ chain prerouting {
+ type nat hook prerouting priority dstnat; policy accept;
+
+ __AGH_WAN_RETURN_LINE__
+
+ fib daddr type local udp dport 53 redirect to :__AGH_PORT__
+ fib daddr type local tcp dport 53 redirect to :__AGH_PORT__
+ }
+}
diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/firewall.start b/luci-app-adguardhome/root/usr/share/AdGuardHome/firewall.start
deleted file mode 100644
index 562117e5..00000000
--- a/luci-app-adguardhome/root/usr/share/AdGuardHome/firewall.start
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/sh
-
-AdGuardHome_enable=$(uci get AdGuardHome.AdGuardHome.enabled)
-redirect=$(uci get AdGuardHome.AdGuardHome.redirect)
-
-if [ $AdGuardHome_enable -eq 1 -a "$redirect" == "redirect" ]; then
- /etc/init.d/AdGuardHome do_redirect 1
-fi
diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/getsyslog.sh b/luci-app-adguardhome/root/usr/share/AdGuardHome/getsyslog.sh
index 908bdf63..e6387b26 100755
--- a/luci-app-adguardhome/root/usr/share/AdGuardHome/getsyslog.sh
+++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/getsyslog.sh
@@ -4,17 +4,17 @@ PATH="/usr/sbin:/usr/bin:/sbin:/bin"
logread -e AdGuardHome > /tmp/AdGuardHometmp.log
logread -e AdGuardHome -f >> /tmp/AdGuardHometmp.log &
pid=$!
-echo "1">/var/run/AdGuardHomesyslog
+echo "1" > /var/run/AdGuardHomesyslog
while true
do
sleep 12
- watchdog=$(cat /var/run/AdGuardHomesyslog)
- if [ "$watchdog"x == "0"x ]; then
- kill $pid
+ watchdog=$(cat /var/run/AdGuardHomesyslog 2>/dev/null)
+ if [ "${watchdog}x" = "0x" ]; then
+ kill "$pid"
rm /tmp/AdGuardHometmp.log
rm /var/run/AdGuardHomesyslog
exit 0
else
- echo "0">/var/run/AdGuardHomesyslog
+ echo "0" > /var/run/AdGuardHomesyslog
fi
-done
+done
\ No newline at end of file
diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/links.txt b/luci-app-adguardhome/root/usr/share/AdGuardHome/links.txt
deleted file mode 100644
index e4f1c8fa..00000000
--- a/luci-app-adguardhome/root/usr/share/AdGuardHome/links.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_${Arch}.tar.gz
-https://github.com/AdguardTeam/AdGuardHome/releases/download/${latest_ver}/AdGuardHome_linux_${Arch}.tar.gz
-https://static.adguard.com/adguardhome/release/AdGuardHome_linux_${Arch}.tar.gz
diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/tailto.sh b/luci-app-adguardhome/root/usr/share/AdGuardHome/tailto.sh
deleted file mode 100755
index 9ccc2190..00000000
--- a/luci-app-adguardhome/root/usr/share/AdGuardHome/tailto.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/sh
-
-tail -n $1 "$2" > /var/run/tailtmp
-cat /var/run/tailtmp > "$2"
-rm /var/run/tailtmp
diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/update_core.sh b/luci-app-adguardhome/root/usr/share/AdGuardHome/update_core.sh
index 1464be6a..2be7a26c 100755
--- a/luci-app-adguardhome/root/usr/share/AdGuardHome/update_core.sh
+++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/update_core.sh
@@ -1,321 +1,164 @@
#!/bin/sh
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
+update_mode=$1
-binpath="$(uci get AdGuardHome.AdGuardHome.binpath 2>/dev/null)"
+# Keep the lock file: unlinking it would allow a second lock on a new inode.
+exec 9>/var/lock/AdGuardHome-update.lock || exit 1
+# Allow a brief status probe to finish before treating the lock as busy.
+if ! flock -w 1 9; then
+ printf '%s\n' '已经有更新任务正在运行,请等待其结束。'
+ exit 2
+fi
+
+# Only the lock owner may reset the log and update status.
+exec >/tmp/AdGuardHome_update.log 2>&1
+cleanup() {
+ local result=$?
+ trap - EXIT
+ [ "$result" -ne 0 ] && touch /var/run/update_core_error
+ rm -f /var/run/update_core
+ exit "$result"
+}
+trap cleanup EXIT
+trap 'exit 1' TERM INT HUP
+rm -f /var/run/update_core_error
+touch /var/run/update_core
+binpath=$(uci get AdGuardHome.AdGuardHome.binpath 2>/dev/null)
if [ -z "$binpath" ]; then
uci set AdGuardHome.AdGuardHome.binpath="/tmp/AdGuardHome/AdGuardHome"
binpath="/tmp/AdGuardHome/AdGuardHome"
fi
+[ ! -d "${binpath%/*}" ] && mkdir -p "${binpath%/*}"
+enabled=$(uci get AdGuardHome.AdGuardHome.enabled 2>/dev/null)
+core_api_url="https://api.github.com/repos/AdguardTeam/AdGuardHome/releases/latest"
-mkdir -p "${binpath%/*}"
+Check_Updates(){
+ downloader_cmd="uclient-fetch"
+ downloader_opts="-T 5 -q -O"
+ downloader_quiet_opts="-q -O -"
+ printf "%s\n" "开始检查更新, 请耐心等待 ..."
+ Cloud_Version="$(${downloader_cmd} ${downloader_quiet_opts} "$core_api_url" 2>/dev/null | sed -n 's/.*"tag_name":[ ]*"\(v[^"]*\)".*/\1/p' | head -n 1)"
+ [ -z "$Cloud_Version" ] && printf "\n检查更新失败, 请检查网络或稍后重试!\n" && EXIT 1
+ if [ -f "$binpath" ]; then
+ Current_Version="$($binpath --version 2>/dev/null | sed -n 's/.*\(v[0-9][0-9.]*\).*/\1/p' | head -n 1)"
+ else
+ Current_Version="未知"
+ fi
+ [ -z "$Current_Version" ] && Current_Version="未知"
+ printf "\n执行文件路径: %s\n\n正在检查更新, 请耐心等待 ...\n" "${binpath%/*}"
+ printf "\n当前 AdGuardHome 版本: %s\n云端 AdGuardHome 版本: %s\n" "$Current_Version" "$Cloud_Version"
+ if [ "$Cloud_Version" != "$Current_Version" ] || [ "$1" = force ]; then
+ Update_Core
+ else
+ printf "\n已是最新版本, 无需更新!\n"
+ EXIT 0
+ fi
+ EXIT 0
+}
-upxflag="$(uci get AdGuardHome.AdGuardHome.upxflag 2>/dev/null)"
+Update_Core(){
+ rm -rf /tmp/AdGuardHome_Update > /dev/null 2>&1
+ mkdir -p "/tmp/AdGuardHome_Update"
+ GET_Arch
+ link="https://github.com/AdguardTeam/AdGuardHome/releases/download/${Cloud_Version}/AdGuardHome_linux_${Arch}.tar.gz"
+ printf "%s\n" "下载链接:${link}"
+ printf "%s\n" "文件名称:${link##*/}"
+ printf "\n开始下载 AdGuardHome 核心文件 ...\n\n"
+ ${downloader_cmd} ${downloader_opts} "/tmp/AdGuardHome_Update/${link##*/}" "$link" > /dev/null 2>&1
+ if [ $? -ne 0 ]; then
+ printf "\nAdGuardHome 核心下载失败,请检查网络、系统时间及 CA 证书 ...\n"
+ rm -rf /tmp/AdGuardHome_Update
+ EXIT 1
+ fi
+ if [ "${link##*.}" = "gz" ]; then
+ printf "\n正在解压 AdGuardHome ...\n"
+ tar -zxf "/tmp/AdGuardHome_Update/${link##*/}" -C "/tmp/AdGuardHome_Update/"
+ if [ ! -e /tmp/AdGuardHome_Update/AdGuardHome ]
+ then
+ printf "%s\n" "AdGuardHome 核心解压失败!"
+ rm -rf "/tmp/AdGuardHome_Update" > /dev/null 2>&1
+ EXIT 1
+ fi
+ downloadbin="/tmp/AdGuardHome_Update/AdGuardHome/AdGuardHome"
+ else
+ downloadbin="/tmp/AdGuardHome_Update/${link##*/}"
+ fi
+ chmod +x "$downloadbin"
+ size_bytes=$(wc -c < "$downloadbin" 2>/dev/null || echo 0)
+ printf "\nAdGuardHome 核心体积: %s\n" "$(awk -v bytes="$size_bytes" 'BEGIN{printf "%.2fMB", bytes/1000000}')"
+ /etc/init.d/AdGuardHome stop 9>&- > /dev/null 2>&1
+ printf "\n移动 AdGuardHome 核心文件到 %s ...\n" "${binpath%/*}"
+ if ! mv -f "$downloadbin" "$binpath" > /dev/null 2>&1 || [ ! -s "$binpath" ]; then
+ printf "AdGuardHome 核心移动失败!\n可能是设备空间不足导致, 请尝试更改 [执行文件路径] 为 /tmp/AdGuardHome\n"
+ EXIT 1
+ fi
+ rm -rf /tmp/AdGuardHome_Update
+ chmod +x "$binpath"
+ if [ "$enabled" = 1 ]; then
+ printf "\n正在重启 AdGuardHome 服务...\n"
+ /etc/init.d/AdGuardHome restart 9>&- > /dev/null 2>&1
+ fi
+ printf "\nAdGuardHome 核心更新成功!\n"
+}
-cleanup() {
- rm -f /var/run/update_core 2>/dev/null
- [ "$1" != "0" ] && touch /var/run/update_core_error
+GET_Arch() {
+ if command -v apk > /dev/null 2>&1; then
+ PM="apk"
+ elif command -v opkg > /dev/null 2>&1; then
+ PM="opkg"
+ else
+ printf "%s\n" "未找到包管理器 (apk/opkg)!" && EXIT 1
+ fi
+ case "${PM}" in
+ apk)
+ Archt="$(apk info -a kernel 2>/dev/null | grep Architecture | awk -F ":[ ]*" '{print $2}')"
+ [ -z "$Archt" ] && Archt="$(uname -m)"
+ ;;
+ opkg)
+ Archt="$(opkg info kernel | grep Architecture | awk -F "[ _]" '{print($2)}')"
+ ;;
+ esac
+ case "${Archt}" in
+ i386)
+ Arch=i386
+ ;;
+ i686)
+ Arch=i386
+ ;;
+ x86|x86_64)
+ Arch=amd64
+ ;;
+ mipsel)
+ Arch=mipsle_softfloat
+ ;;
+ mips)
+ Arch=mips_softfloat
+ ;;
+ mips64el)
+ Arch=mips64le_softfloat
+ ;;
+ mips64)
+ Arch=mips64_softfloat
+ ;;
+ arm)
+ Arch=arm
+ ;;
+ armeb)
+ Arch=armeb
+ ;;
+ aarch64)
+ Arch=arm64
+ ;;
+ *)
+ printf "\nAdGuardHome 暂不支持当前的设备架构: [%s]!\n" "$Archt"
+ EXIT 1
+ esac
+ printf "\n当前设备架构: %s\n" "$Arch"
+}
+
+EXIT(){
exit "$1"
}
-check_already_running(){
- # 先检查锁,再 touch(顺序关键:touch 在函数调用之后执行)
- if [ -f /var/run/update_core ]; then
- echo "已有任务在运行,退出"
- exit 2
- fi
-}
-
-check_wgetcurl(){
- if which curl >/dev/null 2>&1; then
- downloader="curl -L -k --retry 2 --connect-timeout 20 -o"
- return 0
- fi
- if which wget-ssl >/dev/null 2>&1; then
- downloader="wget-ssl --no-check-certificate -t 2 -T 20 -O"
- return 0
- fi
-
- if [ -z "$1" ]; then
- opkg update >/dev/null 2>&1 || { echo "错误:opkg 更新失败"; cleanup 1; }
- fi
-
- if [ -z "$1" ]; then
- opkg remove wget wget-nossl --force-depends >/dev/null 2>&1
- opkg install wget >/dev/null 2>&1 && check_wgetcurl 1 && return 0
- fi
-
- if [ "$1" = "1" ]; then
- opkg install curl >/dev/null 2>&1 && check_wgetcurl 2 && return 0
- fi
-
- if [ "$1" = "2" ]; then
- check_wgetcurl && return 0
- fi
-
- echo "错误:缺少 curl 和 wget"
- cleanup 1
-}
-
-get_arch(){
- # 尝试多种方式获取架构:opkg → apk → uname -m
- local a
- a="$(opkg info kernel 2>/dev/null | grep Architecture | awk '{print $2}')"
- [ -z "$a" ] && a="$(apk info --architecture 2>/dev/null)"
- [ -z "$a" ] && a="$(uname -m)"
- echo "$a"
-}
-
-detect_arch(){
- local Archt
- Archt="$(get_arch)"
-
- case "$Archt" in
- i386|i686|i386|x86)
- Arch="386"
- ;;
- x86_64|x86-64|amd64)
- Arch="amd64"
- ;;
- mipsel|mipsle)
- Arch="mipsle"
- ;;
- mips64el|mips64le)
- Arch="mips64le"
- ;;
- mips)
- Arch="mips"
- ;;
- mips64)
- Arch="mips64"
- ;;
- arm|armv7*|armv8*)
- Arch="arm"
- ;;
- armeb)
- Arch="armeb"
- ;;
- aarch64|arm64)
- Arch="arm64"
- ;;
- *)
- echo "错误:不支持的架构:$Archt"
- cleanup 1
- ;;
- esac
-}
-
-check_latest_version(){
- check_wgetcurl
-
- release_channel="$(uci get AdGuardHome.AdGuardHome.release_channel 2>/dev/null)"
- [ -z "$release_channel" ] && release_channel="stable"
-
- if [ "$release_channel" = "beta" ]; then
- echo "正在检查最新版本(预发布通道)..."
- # /releases 返回时间倒序的所有 release,含 prerelease。取第一条 = 最新 beta。
- local api_url="https://api.github.com/repos/AdguardTeam/AdGuardHome/releases"
- else
- echo "正在检查最新版本(正式版通道)..."
- local api_url="https://api.github.com/repos/AdguardTeam/AdGuardHome/releases/latest"
- fi
-
- local api_result
- api_result="$($downloader - "$api_url" 2>/dev/null)"
- latest_ver="$(echo "$api_result" | grep -oE '"tag_name": *"v[^"]+"' | head -1 | sed 's/.*"v\(.*\)".*/v\1/')"
-
- if [ -z "${latest_ver}" ]; then
- echo "检查最新版本失败,请稍后重试"
- cleanup 1
- fi
-
- if [ -x "$binpath" ]; then
- now_ver="$($binpath --version 2>/dev/null | grep -oE 'v[0-9.]+(-[A-Za-z0-9.]+)?' | head -1)"
- [ -z "$now_ver" ] && now_ver="$($binpath -c /dev/null --check-config 2>&1 | grep -oE 'v[0-9.]+(-[A-Za-z0-9.]+)?' | head -1)"
- else
- now_ver=""
- fi
-
- echo "本地版本:${now_ver:-无},云端版本:${latest_ver}"
-
- if [ "$1" = "force" ]; then
- update_core
- elif [ -z "$now_ver" ]; then
- update_core
- elif [ "$(echo "$latest_ver" | sed 's/[^0-9.]//g')" != "$(echo "$now_ver" | sed 's/[^0-9.]//g')" ]; then
- # 只有云端版本号严格大于本地时才更新
- local newer
- newer="$(printf '%s\n%s' "$latest_ver" "$now_ver" | sed 's/^v//' | sort -Vr | head -1 | sed 's/^/v/')"
- if [ "$newer" = "$latest_ver" ] && [ "$latest_ver" != "$now_ver" ]; then
- update_core
- else
- echo "已是最新版本(本地版本更高或相同)"
- cleanup 0
- fi
- else
- echo "已是最新版本"
- cleanup 0
- fi
-}
-
-apply_upx(){
- [ -z "$upxflag" ] && return
-
- local target="${1:-$binpath}"
- [ ! -f "$target" ] && return
-
- local filesize
- filesize="$(ls -l "$target" 2>/dev/null | awk '{print $5}')"
- [ -z "$filesize" ] && return
- [ "$filesize" -le 8000000 ] && return
-
- echo "二进制文件大于 8MB,正在使用 upx 压缩..."
- fetch_upx
-
- local UPX_DIR="/tmp/upx-${upx_ver_nov}-${Arch}_linux"
- local UPX_BIN="${UPX_DIR}/upx"
- [ ! -x "$UPX_BIN" ] && { echo "upx 可执行文件未找到"; return; }
-
- local upx_out="/tmp/AdGuardHomeupdate/upx-packed-$$"
- $UPX_BIN $upxflag "$target" -o "$upx_out"
- local upxret=$?
- rm -rf "$UPX_DIR"
-
- if [ $upxret -eq 0 ]; then
- rm -f "$target"
- mv -f "$upx_out" "$target"
- chmod 755 "$target"
- echo "upx 压缩完成"
- else
- rm -f "$upx_out"
- fi
-}
-
-fetch_upx(){
- local Archt_upx
- Archt_upx="$(get_arch)"
-
- case "$Archt_upx" in
- i386|i686|x86|i386) Arch="i386";;
- x86_64|x86-64|amd64) Arch="amd64";;
- mipsel|mipsle) Arch="mipsel";;
- mips64el|mips64le) Arch="mipsel";;
- mips) Arch="mips";;
- mips64) Arch="mips";;
- arm|armv7*|armv8*) Arch="arm";;
- aarch64|arm64) Arch="arm64";;
- *)
- echo "upx:不支持的架构 $Archt_upx"
- return 1
- ;;
- esac
-
- upx_latest_ver="$($downloader - "https://api.github.com/repos/upx/upx/releases/latest" 2>/dev/null | grep -oE '"tag_name": *"[^"]+"' | head -1 | sed 's/.*"\(.*\)"/\1/')"
-
- if [ -z "$upx_latest_ver" ]; then
- echo "获取 upx 版本失败"
- return 1
- fi
-
- upx_ver_nov="$(echo "$upx_latest_ver" | sed 's/^v//')"
- local UPX_TGZ="/tmp/upx-${upx_ver_nov}-${Arch}_linux.tar.xz"
- local UPX_URL="https://github.com/upx/upx/releases/download/${upx_latest_ver}/upx-${upx_ver_nov}-${Arch}_linux.tar.xz"
- $downloader "$UPX_TGZ" "$UPX_URL" 2>&1
- [ $? -ne 0 ] && { echo "upx 下载失败"; return 1; }
-
- which xz >/dev/null 2>&1 || { opkg install xz >/dev/null 2>&1 || apk add xz >/dev/null 2>&1; } || { echo "xz 不可用"; return 1; }
-
- xz -d -c "$UPX_TGZ" | tar -x -C "/tmp" >/dev/null 2>&1
-
- [ ! -x "/tmp/upx-${upx_ver_nov}-${Arch}_linux/upx" ] && { echo "upx 解压失败"; return 1; }
- rm -f "$UPX_TGZ"
-}
-
-update_core(){
- echo "正在更新 AdGuardHome 核心..."
- mkdir -p "/tmp/AdGuardHomeupdate"
- rm -rf "/tmp/AdGuardHomeupdate/*" 2>/dev/null
-
- detect_arch
- echo "架构:$Arch"
-
- echo "正在获取下载链接..."
- mkdir -p /tmp/run
- # 按通道生成下载源:beta 优先 static beta;stable 优先 static release
- if [ "$release_channel" = "beta" ]; then
- {
- echo "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_${Arch}.tar.gz"
- echo "https://github.com/AdguardTeam/AdGuardHome/releases/download/${latest_ver}/AdGuardHome_linux_${Arch}.tar.gz"
- } > /tmp/run/AdHlinks.txt
- else
- {
- echo "https://static.adguard.com/adguardhome/release/AdGuardHome_linux_${Arch}.tar.gz"
- echo "https://github.com/AdguardTeam/AdGuardHome/releases/download/${latest_ver}/AdGuardHome_linux_${Arch}.tar.gz"
- } > /tmp/run/AdHlinks.txt
- fi
-
- local downloadbin=""
- local success=""
-
- while read link; do
- [ -z "$link" ] && continue
- eval link="$link"
- echo "尝试下载:$link"
- $downloader "/tmp/AdGuardHomeupdate/${link##*/}" "$link" 2>&1
- if [ $? -eq 0 ] && [ -s "/tmp/AdGuardHomeupdate/${link##*/}" ]; then
- downloadbin="/tmp/AdGuardHomeupdate/${link##*/}"
- success="1"
- echo "下载成功"
- break
- else
- echo "下载失败,尝试下一个..."
- rm -f "/tmp/AdGuardHomeupdate/${link##*/}"
- fi
- done < /tmp/run/AdHlinks.txt
-
- rm -f /tmp/run/AdHlinks.txt
-
- if [ -z "$success" ]; then
- echo "错误:所有下载源均失败"
- cleanup 1
- fi
-
- if [ "${downloadbin##*.}" = "gz" ]; then
- tar -zxf "$downloadbin" -C "/tmp/AdGuardHomeupdate/" 2>/dev/null
- if [ -d "/tmp/AdGuardHomeupdate/AdGuardHome" ]; then
- downloadbin="/tmp/AdGuardHomeupdate/AdGuardHome/AdGuardHome"
- else
- echo "错误:解压失败"
- cleanup 1
- fi
- fi
-
- chmod 755 "$downloadbin"
- echo "下载完成,如已配置 upx 则进行压缩..."
- apply_upx "$downloadbin"
-
- echo "正在停止服务..."
- /etc/init.d/AdGuardHome stop nobackup 2>/dev/null
-
- echo "正在安装新二进制文件..."
- rm -f "$binpath" 2>/dev/null
- mv -f "$downloadbin" "$binpath" 2>/dev/null
- [ $? -ne 0 ] && { echo "mv 失败 - 磁盘空间不足?"; cleanup 1; }
- chmod 755 "$binpath"
-
- rm -rf "/tmp/AdGuardHomeupdate" 2>/dev/null
-
- echo "正在启动服务..."
- /etc/init.d/AdGuardHome start 2>/dev/null
-
- echo "AdGuardHome 已成功更新至 ${latest_ver}"
- cleanup 0
-}
-
-trap "cleanup 1" SIGTERM SIGINT
-
-check_already_running
-touch /var/run/update_core
-rm -f /var/run/update_core_error 2>/dev/null
-
-check_latest_version "$1"
+Check_Updates "$update_mode"
diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/waitnet.sh b/luci-app-adguardhome/root/usr/share/AdGuardHome/waitnet.sh
deleted file mode 100755
index d45518af..00000000
--- a/luci-app-adguardhome/root/usr/share/AdGuardHome/waitnet.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/sh
-
-PATH="/usr/sbin:/usr/bin:/sbin:/bin"
-
-count=0
-while :; do
- for host in www.baidu.com 223.5.5.5 www.google.com 8.8.8.8; do
- ping -c 1 -W 2 -q "$host" >/dev/null 2>&1
- if [ $? -eq 0 ]; then
- /etc/init.d/AdGuardHome force_reload
- exit 0
- fi
- done
-
- count=$((count + 1))
- if [ $count -gt 18 ]; then
- /etc/init.d/AdGuardHome force_reload
- exit 0
- fi
- sleep 5
-done
diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/watchconfig.sh b/luci-app-adguardhome/root/usr/share/AdGuardHome/watchconfig.sh
index 61ba09de..f6850663 100755
--- a/luci-app-adguardhome/root/usr/share/AdGuardHome/watchconfig.sh
+++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/watchconfig.sh
@@ -10,4 +10,4 @@ do
break
fi
done
-return 0
+exit 0
\ No newline at end of file
diff --git a/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json b/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json
index 189428c2..6cdcf8bb 100644
--- a/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json
+++ b/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json
@@ -1,53 +1,45 @@
{
- "admin/services/AdGuardHome": {
- "title": "AdGuard Home",
- "order": 10,
- "action": {
- "type": "alias",
- "path": "admin/services/AdGuardHome/overview"
- },
- "depends": {
- "acl": [ "luci-app-adguardhome" ]
- }
- },
- "admin/services/AdGuardHome/overview": {
- "title": "概览",
- "order": 1,
- "action": {
- "type": "cbi",
- "path": "AdGuardHome/overview"
- }
- },
- "admin/services/AdGuardHome/base": {
- "title": "基础设置",
- "order": 2,
- "action": {
- "type": "cbi",
- "path": "AdGuardHome/base"
- }
- },
- "admin/services/AdGuardHome/tools": {
- "title": "运维",
- "order": 3,
- "action": {
- "type": "cbi",
- "path": "AdGuardHome/tools"
- }
- },
- "admin/services/AdGuardHome/log": {
- "title": "日志",
- "order": 4,
- "action": {
- "type": "form",
- "path": "AdGuardHome/log"
- }
- },
- "admin/services/AdGuardHome/manual": {
- "title": "手动配置",
- "order": 5,
- "action": {
- "type": "cbi",
- "path": "AdGuardHome/manual"
- }
- }
+ "admin/services/AdGuardHome": {
+ "title": "AdGuard Home",
+ "order": 12,
+ "action": {
+ "type": "firstchild"
+ },
+ "depends": {
+ "acl": [ "luci-app-adguardhome" ]
+ }
+ },
+ "admin/services/AdGuardHome/base": {
+ "title": "Base Setting",
+ "order": 1,
+ "action": {
+ "type": "view",
+ "path": "adguardhome/base"
+ },
+ "depends": {
+ "acl": [ "luci-app-adguardhome" ]
+ }
+ },
+ "admin/services/AdGuardHome/log": {
+ "title": "Log",
+ "order": 3,
+ "action": {
+ "type": "view",
+ "path": "adguardhome/log"
+ },
+ "depends": {
+ "acl": [ "luci-app-adguardhome" ]
+ }
+ },
+ "admin/services/AdGuardHome/manual": {
+ "title": "Manual Config",
+ "order": 2,
+ "action": {
+ "type": "view",
+ "path": "adguardhome/manual"
+ },
+ "depends": {
+ "acl": [ "luci-app-adguardhome" ]
+ }
+ }
}
diff --git a/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json b/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json
index 485aa620..41727fce 100644
--- a/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json
+++ b/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json
@@ -2,10 +2,20 @@
"luci-app-adguardhome": {
"description": "Grant UCI access for luci-app-adguardhome",
"read": {
+ "file": {
+ "/var/run/AdGredir": [ "read" ]
+ },
+ "ubus": {
+ "service": [ "list" ],
+ "luci.adguardhome": [ "getCoreInfo", "getRedirectStatus", "getCoreUpdate", "getInterfaces", "getRuntimeLog", "getManualConfig" ]
+ },
"uci": [ "AdGuardHome" ]
},
"write": {
+ "ubus": {
+ "luci.adguardhome": [ "setVerboseLog", "startCoreUpdate", "changeWebPassword", "clearRuntimeLog", "setManualConfig" ]
+ },
"uci": [ "AdGuardHome" ]
}
}
-}
+}
\ No newline at end of file
diff --git a/luci-app-adguardhome/root/usr/share/rpcd/ucode/luci.adguardhome b/luci-app-adguardhome/root/usr/share/rpcd/ucode/luci.adguardhome
new file mode 100644
index 00000000..6e46f612
--- /dev/null
+++ b/luci-app-adguardhome/root/usr/share/rpcd/ucode/luci.adguardhome
@@ -0,0 +1,265 @@
+#!/usr/bin/ucode
+'use strict';
+
+import { stat, open, popen, readfile, writefile, unlink, lsdir } from 'fs';
+import { cursor } from 'uci';
+
+function shellQuote(value) {
+ return "'" + replace(value, /'/g, "'\\''") + "'";
+}
+
+function coreUpdateRunning() {
+ // The kernel releases the lock even if the updater is killed.
+ const result = system(['/usr/bin/flock', '-n', '-E', '2',
+ '/var/lock/AdGuardHome-update.lock', '/bin/true'], 3000);
+ if (result != 0 && result != 2)
+ die('Unable to check core update lock');
+ return result == 2;
+}
+
+return {
+ 'luci.adguardhome': {
+ getRedirectStatus: {
+ call: function() {
+ const conf = cursor();
+ function option(name, fallback) {
+ const value = conf.get('AdGuardHome', 'AdGuardHome', name);
+ return (type(value) == 'array' ? value[0] : value) || fallback;
+ }
+ const result = {
+ mode: option('redirect', 'none'),
+ enabled: option('enabled', '0') == '1',
+ applied_mode: option('old_redirect', 'none'),
+ applied_enabled: option('old_enabled', '0') == '1',
+ binpath: option('binpath', '/usr/bin/AdGuardHome/AdGuardHome'),
+ redirected: trim(readfile('/var/run/AdGredir') || '') == '1'
+ };
+ conf.unload('AdGuardHome');
+ const lock = system(['/usr/bin/flock', '-n', '-E', '2',
+ '/var/lock/AdGuardHome-redirect.lock', '/bin/true'], 3000);
+ if (lock != 0 && lock != 2)
+ die('Unable to check redirect status');
+ result.busy = lock == 2;
+ return result;
+ }
+ },
+ getManualConfig: {
+ call: function() {
+ const conf = cursor();
+ const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
+ '/etc/AdGuardHome.yaml';
+ conf.unload('AdGuardHome');
+
+ const template = readfile('/usr/share/AdGuardHome/AdGuardHome_template.yaml') || '';
+ const current = readfile(configpath);
+ if (current != null)
+ return { content: current, template: template, source: 'current' };
+ if (stat(configpath) != null)
+ return { content: '', template: template, source: 'error' };
+ return {
+ content: template,
+ template: template,
+ source: template ? 'template' : 'missing'
+ };
+ }
+ },
+ setManualConfig: {
+ args: { content: '' },
+ call: function(request) {
+ const content = request.args.content;
+ if (content == null)
+ return { saved: false, reason: 'invalid_content' };
+
+ const conf = cursor();
+ const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
+ '/etc/AdGuardHome.yaml';
+ const binpath = conf.get('AdGuardHome', 'AdGuardHome', 'binpath') ||
+ '/usr/bin/AdGuardHome/AdGuardHome';
+ conf.unload('AdGuardHome');
+
+ const temporaryConfig = '/tmp/AdGuardHometmpconfig.yaml';
+ const validationLog = '/tmp/AdGuardHometest.log';
+ if (writefile(temporaryConfig, replace(content, /\r\n/g, '\n')) == null)
+ return { saved: false, reason: 'write_failed' };
+
+ if (stat(binpath)?.type == 'file') {
+ const valid = system(['/bin/sh', '-c', '"$1" -c "$2" --check-config > "$3" 2>&1',
+ 'sh', binpath, temporaryConfig, validationLog], 30000) == 0;
+ if (!valid)
+ return { saved: false, reason: 'validation_failed', log: readfile(validationLog) || '' };
+ }
+
+ if (writefile(configpath, readfile(temporaryConfig) || '') == null)
+ return { saved: false, reason: 'save_failed' };
+ unlink(temporaryConfig);
+ if (system(['/etc/init.d/AdGuardHome', 'reload'], 30000) != 0)
+ return { saved: false, reason: 'reload_failed' };
+ return { saved: true };
+ }
+ },
+ getInterfaces: {
+ call: function() {
+ return { interfaces: lsdir('/sys/class/net') || [] };
+ }
+ },
+ getCoreInfo: {
+ call: function() {
+ const conf = cursor();
+ const binpath = conf.get('AdGuardHome', 'AdGuardHome', 'binpath') ||
+ '/usr/bin/AdGuardHome/AdGuardHome';
+ const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
+ '/etc/AdGuardHome.yaml';
+ const result = {
+ core_exists: stat(binpath)?.type == 'file',
+ config_exists: stat(configpath)?.type == 'file',
+ version: null
+ };
+
+ if (result.core_exists) {
+ // Pass the configured path as a positional argument; capture both output streams.
+ const proc = popen('/bin/sh -c \'"$1" --version 2>&1\' sh ' + shellQuote(binpath), 'r');
+ if (proc) {
+ const output = proc.read('all') || '';
+ proc.close();
+ result.version = match(output, /version ([^, \t\r\n]+)/)?.[1] || null;
+ }
+ }
+
+ conf.unload('AdGuardHome');
+ return result;
+ }
+ },
+ getCoreUpdate: {
+ call: function() {
+ const running = coreUpdateRunning();
+ const failed = stat('/var/run/update_core_error') != null ||
+ (!running && stat('/var/run/update_core') != null);
+ const log = readfile('/tmp/AdGuardHome_update.log', 131072) || '';
+
+ return {
+ state: running ? 'running' : failed ? 'failure' : log ? 'finished' : 'idle',
+ log: log
+ };
+ }
+ },
+ startCoreUpdate: {
+ args: { force: false },
+ call: function(request) {
+ if (coreUpdateRunning())
+ return { started: false, reason: 'busy' };
+
+ const command = request.args.force
+ ? '/bin/sh /usr/share/AdGuardHome/update_core.sh force >/dev/null 2>&1 /dev/null 2>&1 /dev/null 2>&1 &'], 3000);
+ }
+
+ // Bound memory and start with the most recent 256 KiB on a new view.
+ const limit = 262144;
+ const info = stat(source);
+ const identity = info ? source + ':' + info.dev.major + ':' + info.dev.minor + ':' + info.inode : '';
+ let offset = int(request.args.offset) || 0;
+ let reset = request.args.identity != identity || offset < 0 || offset > (info?.size || 0);
+ if (reset)
+ offset = 0;
+ if (info && info.size - offset > limit) {
+ offset = info.size - limit;
+ reset = true;
+ }
+
+ let content = '';
+ if (info?.type == 'file') {
+ const file = open(source, 'r');
+ if (!file)
+ die('Unable to open runtime log');
+ const positioned = file.seek(offset);
+ const chunk = positioned ? file.read(limit) : null;
+ file.close();
+ if (chunk == null)
+ die('Unable to read runtime log');
+ content = chunk;
+ }
+
+ return {
+ enabled: true,
+ localtime: logfile != 'syslog',
+ // ubus strings stop at NUL; cleared files can contain sparse zero-filled gaps.
+ content_b64: b64enc(content),
+ offset: offset + length(content),
+ identity: identity,
+ reset: reset
+ };
+ }
+ },
+ clearRuntimeLog: {
+ call: function() {
+ const conf = cursor();
+ const logfile = conf.get('AdGuardHome', 'AdGuardHome', 'logfile') || '';
+ conf.unload('AdGuardHome');
+ if (!logfile)
+ return { cleared: false, reason: 'no_logfile' };
+
+ const source = logfile == 'syslog' ? '/tmp/AdGuardHometmp.log' : logfile;
+ return { cleared: writefile(source, '') != null };
+ }
+ }
+ }
+};
\ No newline at end of file
diff --git a/luci-app-adguardhome/root/www/luci-static/resources/codemirror/addon/fold/foldcode.js b/luci-app-adguardhome/root/www/luci-static/resources/codemirror/addon/fold/foldcode.js
deleted file mode 100644
index f93d42b7..00000000
--- a/luci-app-adguardhome/root/www/luci-static/resources/codemirror/addon/fold/foldcode.js
+++ /dev/null
@@ -1 +0,0 @@
-!function(n){"object"==typeof exports&&"object"==typeof module?n(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],n):n(CodeMirror)}(function(n){"use strict";function e(e,o,i,t){if(i&&i.call){var l=i;i=null}else l=r(e,i,"rangeFinder");"number"==typeof o&&(o=n.Pos(o,0));var f=r(e,i,"minFoldSize");function d(n){var r=l(e,o);if(!r||r.to.line-r.from.linee.firstLine();)o=n.Pos(o.line-1,0),u=d(!1);if(u&&!u.cleared&&"unfold"!==t){var a=function(n,e){var o=r(n,e,"widget");if("string"==typeof o){var i=document.createTextNode(o);(o=document.createElement("span")).appendChild(i),o.className="CodeMirror-foldmarker"}else o&&(o=o.cloneNode(!0));return o}(e,i);n.on(a,"mousedown",function(e){c.clear(),n.e_preventDefault(e)});var c=e.markText(u.from,u.to,{replacedWith:a,clearOnEnter:r(e,i,"clearOnEnter"),__isFold:!0});c.on("clear",function(o,r){n.signal(e,"unfold",e,o,r)}),n.signal(e,"fold",e,u.from,u.to)}}n.newFoldFunction=function(n,o){return function(r,i){e(r,i,{rangeFinder:n,widget:o})}},n.defineExtension("foldCode",function(n,o,r){e(this,n,o,r)}),n.defineExtension("isFolded",function(n){for(var e=this.findMarksAt(n),o=0;o=u){if(s&&f&&s.test(f.className))return;i=r(a.indicatorOpen)}}(i||f)&&t.setGutterMarker(n,a.gutter,i)})}function i(t){return new RegExp("(^|\\s)"+t+"(?:$|\\s)\\s*")}function f(t){var o=t.getViewport(),e=t.state.foldGutter;e&&(t.operation(function(){n(t,o.from,o.to)}),e.from=o.from,e.to=o.to)}function a(t,r,n){var i=t.state.foldGutter;if(i){var f=i.options;if(n==f.gutter){var a=e(t,r);a?a.clear():t.foldCode(o(r,0),f)}}}function d(t){var o=t.state.foldGutter;if(o){var e=o.options;o.from=o.to=0,clearTimeout(o.changeUpdate),o.changeUpdate=setTimeout(function(){f(t)},e.foldOnChangeTimeSpan||600)}}function u(t){var o=t.state.foldGutter;if(o){var e=o.options;clearTimeout(o.changeUpdate),o.changeUpdate=setTimeout(function(){var e=t.getViewport();o.from==o.to||e.from-o.to>20||o.from-e.to>20?f(t):t.operation(function(){e.fromo.to&&(n(t,o.to,e.to),o.to=e.to)})},e.updateViewportTimeSpan||400)}}function l(t,o){var e=t.state.foldGutter;if(e){var r=o.line;r>=e.from&&ro))break;r=l}}return r?{from:e.Pos(i.line,t.getLine(i.line).length),to:e.Pos(r,t.getLine(r).length)}:void 0}})});
\ No newline at end of file
diff --git a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/base.js b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/base.js
new file mode 100644
index 00000000..ae1163c4
--- /dev/null
+++ b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/base.js
@@ -0,0 +1,276 @@
+'use strict';
+'require view';
+'require form';
+'require poll';
+'require rpc';
+'require ui';
+'require view/adguardhome/status as status';
+
+var callInterfaces = rpc.declare({
+ object: 'luci.adguardhome',
+ method: 'getInterfaces',
+ expect: { '': {} },
+ reject: true
+});
+
+var callCoreUpdate = rpc.declare({
+ object: 'luci.adguardhome',
+ method: 'getCoreUpdate',
+ expect: { '': {} },
+ reject: true
+});
+
+var callStartCoreUpdate = rpc.declare({
+ object: 'luci.adguardhome',
+ method: 'startCoreUpdate',
+ params: [ 'force' ],
+ expect: { '': {} },
+ reject: true
+});
+
+return view.extend({
+ handleSaveApply: function(ev, mode) {
+ var target = {
+ mode: this.redirectOption.formvalue('AdGuardHome') || 'none',
+ enabled: this.enabledOption.formvalue('AdGuardHome') === '1'
+ };
+ this.sharedStatus.beginSwitch(target);
+ return this.handleSave(ev).then(function() {
+ ui.changes.apply(mode == '0');
+ }).catch(function(error) {
+ this.sharedStatus.cancelSwitch();
+ throw error;
+ }.bind(this));
+ },
+
+ load: function() {
+ return callInterfaces().catch(function() { return { interfaces: [] }; });
+ },
+
+ render: function(data) {
+ let m, s, o;
+ var self = this;
+ var interfaces = data.interfaces || [];
+
+ m = new form.Map('AdGuardHome');
+
+ s = m.section(
+ form.TypedSection,
+ 'AdGuardHome',
+ _('Basic Settings')
+ );
+
+ s.anonymous = true;
+ s.addremove = false;
+
+ o = s.option(
+ form.Flag,
+ 'enabled',
+ _('Enable')
+ );
+ this.enabledOption = o;
+ o.default = '0';
+ o.rmempty = false;
+
+ o = s.option(
+ form.Value,
+ 'httpport',
+ _('Browser management port')
+ );
+ o.datatype = 'port';
+ o.placeholder = '3000';
+ o.default = '3000';
+ o.rmempty = false;
+
+ o = s.option(
+ form.ListValue,
+ 'redirect',
+ _('DNS handling mode')
+ );
+
+ this.redirectOption = o;
+ o.value('none', _('Do not handle DNS'));
+ o.value('redirect', _('Forward port 53 requests through the firewall (recommended)'));
+ o.value('dnsmasq-upstream', _('Run as dnsmasq upstream server'));
+ o.value('exchange', _('Use port 53 instead of dnsmasq'));
+
+ o.default = 'none';
+ o.rmempty = false;
+
+ o = s.option(
+ form.DynamicList,
+ 'wan_ifname',
+ _('Network devices excluded from forwarding'),
+ _('DNS requests arriving on these network devices are not forwarded to AdGuardHome')
+ );
+ o.depends('redirect', 'redirect');
+ o.retain = true;
+ o.datatype = 'string';
+ o.rmempty = true;
+ interfaces.filter(function(name) { return name !== 'lo'; }).forEach(function(name) {
+ o.value(name, name);
+ });
+
+ o = s.option(
+ form.Value,
+ 'binpath',
+ _('Bin Path')
+ );
+ o.default = '/usr/bin/AdGuardHome/AdGuardHome';
+ o.rmempty = false;
+
+ o = s.option(
+ form.Value,
+ 'configpath',
+ _('Config Path')
+ );
+ o.default = '/etc/AdGuardHome.yaml';
+ o.rmempty = false;
+
+ o = s.option(
+ form.Value,
+ 'workdir',
+ _('Work dir')
+ );
+ o.default = '/usr/bin/AdGuardHome';
+ o.rmempty = false;
+
+
+ return m.render().then(function(node) {
+ var sharedStatus = status.render();
+ self.sharedStatus = sharedStatus;
+
+ var updateButton = E('button', {
+ 'class': 'btn cbi-button cbi-button-apply', 'type': 'button'
+ }, _('Update core version'));
+ var forceButton = E('button', {
+ 'class': 'btn cbi-button', 'type': 'button'
+ }, _('Force update'));
+ var updateMessage = E('span', {}, '');
+ var reverseLog = E('input', { 'type': 'checkbox' });
+ var updateLog = E('textarea', {
+ 'class': 'cbi-input-textarea agh-update-log', 'rows': 8, 'readonly': 'readonly',
+ 'style': 'display:none;'
+ });
+ var logOptions = E('label', { 'class': 'agh-update-options', 'style': 'display:none;' }, [
+ reverseLog, E('span', {}, _('reverse'))
+ ]);
+ var updatePanel = E('div', { 'class': 'cbi-value' }, [
+ E('div', { 'class': 'cbi-value-title' }, _('Upgrade Core')),
+ E('div', { 'class': 'cbi-value-field' }, E('div', { 'class': 'agh-update-content' }, [
+ E('div', { 'class': 'agh-toolbar' }, [updateButton, forceButton, updateMessage]),
+ logOptions, updateLog
+ ]))
+ ]);
+ var lastLog = '';
+ var startingAt = 0;
+ var activeUpdate = false;
+ var updateFinishedHere = false;
+
+ function showLog() {
+ updateLog.value = reverseLog.checked ? lastLog.split('\n').reverse().join('\n') : lastLog;
+ }
+ reverseLog.addEventListener('change', showLog);
+
+ function showUpdateStatus(result) {
+ var state = result.state || 'idle';
+ if (startingAt && state === 'idle' && Date.now() - startingAt < 10000)
+ return;
+ startingAt = 0;
+ if (state === 'running')
+ activeUpdate = true;
+ else if (activeUpdate && (state === 'finished' || state === 'failure')) {
+ activeUpdate = false;
+ updateFinishedHere = true;
+ sharedStatus.refresh();
+ }
+
+ updateButton.disabled = forceButton.disabled = state === 'running';
+ if (state === 'running' || updateFinishedHere) {
+ updateMessage.textContent = state === 'running' ? _('Check...') :
+ state === 'finished' ? _('Updated') : _('Update failed');
+ lastLog = result.log || '';
+ logOptions.style.display = lastLog ? 'inline-flex' : 'none';
+ updateLog.style.display = lastLog ? 'block' : 'none';
+ showLog();
+ } else {
+ updateMessage.textContent = '';
+ lastLog = '';
+ logOptions.style.display = updateLog.style.display = 'none';
+ }
+ }
+
+ function startUpdate(force) {
+ startingAt = Date.now();
+ activeUpdate = true;
+ updateFinishedHere = false;
+ updateButton.disabled = forceButton.disabled = true;
+ updateMessage.textContent = _('Check...');
+ lastLog = '';
+ showLog();
+ updateLog.style.display = logOptions.style.display = 'none';
+ callStartCoreUpdate(force).then(function(result) {
+ if (!result.started) {
+ startingAt = 0;
+ activeUpdate = false;
+ updateMessage.textContent = _('Update in progress');
+ return;
+ }
+ return callCoreUpdate().then(showUpdateStatus);
+ }).catch(function() {
+ startingAt = 0;
+ activeUpdate = false;
+ updateButton.disabled = forceButton.disabled = false;
+ updateMessage.textContent = _('Unable to start update');
+ });
+ }
+
+ updateButton.addEventListener('click', function() { startUpdate(false); });
+ forceButton.addEventListener('click', function() { startUpdate(true); });
+ poll.add(function() {
+ return callCoreUpdate().then(showUpdateStatus).catch(function() {
+ updateMessage.textContent = _('Update status unavailable');
+ });
+ }, 3);
+ var settingsCard = node.querySelector('.cbi-section');
+ settingsCard.classList.add('agh-compact-card', 'agh-base-card');
+ var settingsFields = settingsCard.querySelector('.cbi-section-node') || settingsCard;
+ settingsFields.appendChild(updatePanel);
+ node.insertBefore(sharedStatus.node, node.firstChild);
+ node.insertBefore(E('link', {
+ 'rel': 'stylesheet', 'href': L.resource('view/adguardhome/compact.css')
+ }), node.firstChild);
+
+ var portInput = node.querySelector('[data-name="httpport"] input');
+ var portField = node.querySelector('[data-name="httpport"] .cbi-value-field');
+ if (portInput && portField) {
+ var webLink = E('input', {
+ 'type': 'button',
+ 'style': 'width:210px;border-color:Teal;text-align:center;font-weight:bold;color:#337ab7;background:#ffc800;',
+ 'target': '_blank',
+ 'value': _('AdGuardHome Web')
+ });
+ var linkWrap = E('div', { 'style': 'margin-top:0.35rem;' }, webLink);
+ var host = window.location.hostname;
+ if (host.indexOf(':') >= 0 && host.charAt(0) !== '[')
+ host = '[' + host + ']';
+
+ function updateWebLink() {
+ var port = portInput.value.trim();
+ var valid = /^[0-9]+$/.test(port) && +port >= 1 && +port <= 65535;
+ webLink.value = _('AdGuardHome Web') + ': ' + port;
+ webLink.onclick = valid ? function() {
+ window.open('http://' + host + ':' + port + '/');
+ } : null;
+ webLink.style.display = valid ? 'inline-block' : 'none';
+ }
+
+ portField.appendChild(linkWrap);
+ portInput.addEventListener('input', updateWebLink);
+ updateWebLink();
+ }
+
+ return node;
+ });
+ }
+});
diff --git a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/compact.css b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/compact.css
new file mode 100644
index 00000000..ec755092
--- /dev/null
+++ b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/compact.css
@@ -0,0 +1,153 @@
+/* Scoped to the settings and log cards; shared status badges keep their layout. */
+.agh-compact-card {
+ margin-bottom: 0.75rem;
+}
+.agh-compact-card .cbi-value {
+ margin-bottom: 0;
+ padding-top: 0.35rem;
+ padding-bottom: 0.35rem;
+}
+.agh-compact-card .cbi-value-description {
+ margin-top: 0.25rem;
+}
+.agh-compact-card .agh-toolbar {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 0.5rem 0.75rem;
+}
+.agh-compact-card .agh-toolbar > * {
+ margin: 0;
+}
+.agh-compact-card .agh-update-content {
+ width: 100%;
+ max-width: 40rem;
+ min-width: 0;
+}
+.agh-compact-card .agh-update-options {
+ align-items: center;
+ gap: 0.35rem;
+ width: auto;
+ margin: 0.5rem 0 0;
+ padding: 0;
+ float: none;
+}
+.agh-compact-card .agh-update-options input[type="checkbox"] {
+ position: static;
+ flex: 0 0 auto;
+ margin: 0;
+}
+.agh-compact-card textarea.agh-update-log {
+ width: 100%;
+ height: 10rem;
+ min-height: 6rem;
+ box-sizing: border-box;
+ resize: vertical;
+ margin: 0.35rem 0 0;
+}
+.agh-log-view {
+ padding: 0.6em;
+}
+.agh-log-view textarea {
+ display: block;
+ width: 100%;
+ height: clamp(12rem, 32vh, 20rem);
+ min-height: 10rem;
+ box-sizing: border-box;
+ resize: vertical;
+ margin: 0 0 0.6em;
+}
+
+.agh-manual-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 0.5rem 1rem;
+ padding: 0;
+}
+.agh-compact-card .agh-password-controls label {
+ float: none;
+ width: auto;
+ padding: 0;
+}
+.agh-compact-card .agh-password-controls input {
+ width: 14rem;
+ max-width: 100%;
+ min-width: 0;
+}
+
+.agh-manual-card {
+ padding: 0.6em;
+}
+.agh-editor-wrap {
+ margin: 0 0 0.6em;
+}
+.agh-config-actions {
+ margin-left: auto;
+}
+
+/* Keep the base card heading in the theme's original position. */
+.agh-base-card .cbi-section-node {
+ margin: 0;
+ padding: 0.6em;
+}
+.agh-log-view .agh-log-controls {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 0.6em 1.2em;
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ text-align: left;
+}
+.agh-log-view .agh-log-toggles {
+ justify-content: flex-start;
+ flex: 0 1 auto;
+}
+.agh-log-view .agh-log-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ gap: 0.6em 1.2em;
+ flex: 0 1 auto;
+ min-width: 0;
+ margin-left: auto;
+}
+.agh-log-view .agh-log-actions > .cbi-value {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ flex: 0 1 auto;
+ gap: 0.5em;
+ width: auto;
+ min-width: 0;
+ margin: 0;
+ padding: 0;
+ border: 0;
+}
+.agh-log-view .agh-log-actions .cbi-value-title {
+ float: none;
+ flex: 0 0 auto;
+ width: auto;
+ margin: 0;
+ padding: 0;
+}
+.agh-log-view .agh-log-actions .cbi-value-field {
+ flex: 0 1 auto;
+ width: auto;
+ min-width: 0;
+ margin: 0;
+ padding: 0;
+}
+.agh-log-view .agh-log-actions .cbi-value-field input {
+ width: 22em;
+ max-width: 100%;
+ box-sizing: border-box;
+}
+.agh-log-controls .agh-toolbar > span:empty {
+ display: none;
+}
diff --git a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/log.js b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/log.js
new file mode 100644
index 00000000..8fc0bf9e
--- /dev/null
+++ b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/log.js
@@ -0,0 +1,242 @@
+'use strict';
+'require view';
+'require form';
+'require poll';
+'require rpc';
+'require view/adguardhome/status as status';
+
+var callRuntimeLog = rpc.declare({
+ object: 'luci.adguardhome',
+ method: 'getRuntimeLog',
+ params: [ 'offset', 'identity' ],
+ expect: { '': {} },
+ reject: true
+});
+
+var callClearRuntimeLog = rpc.declare({
+ object: 'luci.adguardhome',
+ method: 'clearRuntimeLog',
+ expect: { '': {} },
+ reject: true
+});
+
+var callSetVerboseLog = rpc.declare({
+ object: 'luci.adguardhome',
+ method: 'setVerboseLog',
+ params: [ 'enabled' ],
+ expect: { '': {} },
+ reject: true,
+ timeout: 45000
+});
+
+function decodeLog(result) {
+ var content = result.content || '';
+ if (typeof result.content_b64 === 'string') {
+ var binary = atob(result.content_b64);
+ var bytes = new Uint8Array(binary.length);
+ for (var i = 0; i < binary.length; i++)
+ bytes[i] = binary.charCodeAt(i);
+ content = new TextDecoder('utf-8').decode(bytes);
+ }
+ // Truncating an open log does not reset a non-append writer's file position.
+ return content.replace(/\0/g, '');
+}
+
+function pad(value) {
+ return value < 10 ? '0' + value : value;
+}
+
+function localizeTimestamps(content) {
+ return content.split('\n').map(function(line) {
+ var date = new Date(line.substring(0, 19).replace(/\//g, '-') + ' UTC');
+ if (isNaN(date.getTime()))
+ return line;
+
+ return date.getFullYear() + '/' + pad(date.getMonth() + 1) + '/' + pad(date.getDate()) + ' ' +
+ pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds()) + line.substring(19);
+ }).join('\n');
+}
+
+return view.extend({
+ load: function() {
+ return callRuntimeLog(0, '').catch(function() {
+ return { error: true, localtime: false, content: '', offset: 0 };
+ });
+ },
+
+ render: function(initialLog) {
+ var m = new form.Map('AdGuardHome');
+ var s = m.section(form.TypedSection, 'AdGuardHome');
+ s.anonymous = true;
+ s.addremove = false;
+ var o;
+
+ o = s.option(
+ form.Value,
+ 'logfile',
+ _('Runtime log file')
+ );
+ o.rmempty = true;
+
+ return m.render().then(function(settingsNode) {
+ var sharedStatus = status.render();
+ var logLimit = 262144;
+ var content = decodeLog(initialLog).slice(-logLimit);
+ var identity = initialLog.identity || '';
+ var generation = 0;
+ var offset = initialLog.offset || 0;
+ var enabled = initialLog.enabled;
+ var localtime = initialLog.localtime;
+ var reverse = true;
+ var convertTime = localtime;
+ var logArea = E('textarea', {
+ 'class': 'cbi-input-textarea',
+ 'rows': 16,
+ 'readonly': 'readonly',
+ 'style': 'width:100%;box-sizing:border-box;'
+ });
+ var toggleStyle = 'margin:0 0.35em 0 0;height:auto;flex:none;';
+ var reverseToggle = E('input', { 'type': 'checkbox', 'checked': true, 'style': toggleStyle });
+ var timeToggle = E('input', { 'type': 'checkbox', 'checked': true, 'style': toggleStyle });
+ var reverseLabel = E('label', { 'style': 'display:inline-flex;align-items:center;' }, [
+ reverseToggle, _('reverse')
+ ]);
+ var timeLabel = E('label', { 'style': localtime ? 'display:inline-flex;align-items:center;' : 'display:none;' }, [
+ timeToggle, _('localtime')
+ ]);
+ var verboseToggle = E('input', {
+ 'type': 'checkbox', 'style': toggleStyle
+ });
+ // Boolean HTML attributes are true whenever present, even with a false value.
+ verboseToggle.checked = m.data.get('AdGuardHome', 'AdGuardHome', 'verbose') === '1';
+ verboseToggle.disabled = !!m.readonly;
+ var verboseMessage = E('span', { 'role': 'status' }, '');
+ var verboseLabel = E('label', { 'style': 'display:inline-flex;align-items:center;' }, [
+ verboseToggle, _('Verbose log')
+ ]);
+ verboseToggle.addEventListener('change', function() {
+ var requested = verboseToggle.checked;
+ verboseToggle.disabled = true;
+ verboseMessage.textContent = _('Applying verbose logging…');
+ callSetVerboseLog(requested).then(function(result) {
+ if (!result.saved)
+ throw new Error('save failed');
+ verboseMessage.textContent = result.applied ? '' : _('Saved, but service reload failed; check the log');
+ }).catch(function() {
+ verboseToggle.checked = !requested;
+ verboseMessage.textContent = _('Unable to apply verbose logging');
+ }).then(function() {
+ verboseToggle.disabled = false;
+ });
+ });
+ var message = E('span', {}, '');
+
+ function renderLog() {
+ var displayed = convertTime ? localizeTimestamps(content) : content;
+ logArea.value = reverse ? displayed.split('\n').reverse().join('\n') : displayed;
+ }
+
+ function downloadLog() {
+ var now = new Date();
+ var link = document.createElement('a');
+ link.download = 'AdGuardHome' + (now.getMonth() + 1) + '-' + now.getDate() + '-' +
+ pad(now.getHours()) + '_' + pad(now.getMinutes()) + '.log';
+ link.href = URL.createObjectURL(new Blob([logArea.value], { type: 'text/plain' }));
+ link.click();
+ URL.revokeObjectURL(link.href);
+ }
+
+ var clearButton = E('button', {
+ 'class': 'btn cbi-button cbi-button-apply', 'type': 'button'
+ }, _('dellog'));
+ var downloadButton = E('button', {
+ 'class': 'btn cbi-button', 'type': 'button'
+ }, _('download log'));
+ reverseToggle.addEventListener('change', function() {
+ reverse = reverseToggle.checked;
+ renderLog();
+ });
+ timeToggle.addEventListener('change', function() {
+ convertTime = timeToggle.checked;
+ renderLog();
+ });
+ clearButton.addEventListener('click', function() {
+ clearButton.disabled = true;
+ generation++;
+ callClearRuntimeLog().then(function(result) {
+ if (!result.cleared)
+ throw new Error(result.reason || 'clear failed');
+ content = '';
+ offset = 0;
+ identity = '';
+ generation++;
+ message.textContent = '';
+ renderLog();
+ }).catch(function() {
+ message.textContent = _('Status unavailable');
+ }).then(function() {
+ clearButton.disabled = false;
+ });
+ });
+ downloadButton.addEventListener('click', downloadLog);
+
+ var settingsCard = settingsNode.querySelector('.cbi-section');
+ settingsCard.classList.add('agh-compact-card', 'agh-log-view');
+ var settingsFields = settingsCard.querySelector('.cbi-section-node');
+ var logFileField = settingsFields.querySelector('.cbi-value');
+ var logPath = logFileField.querySelector('input[type="text"]');
+ if (logPath)
+ logPath.title = _('If set to "syslog", write to system log; if empty, disable file logging');
+ var logControls = E('div', { 'class': 'agh-log-controls' }, [
+ E('div', { 'class': 'agh-toolbar agh-log-toggles' }, [
+ reverseLabel, timeLabel, verboseLabel, verboseMessage, message
+ ]),
+ E('div', { 'class': 'agh-log-actions' }, [
+ E('div', { 'class': 'agh-toolbar' }, [clearButton, downloadButton]),
+ logFileField
+ ])
+ ]);
+ // Keep the form field inside its map, outside the theme's section layout.
+ settingsFields.hidden = true;
+ settingsCard.insertBefore(logControls, settingsFields);
+ settingsCard.insertBefore(logArea, logControls);
+ var page = E('div', {}, [
+ E('link', { 'rel': 'stylesheet', 'href': L.resource('view/adguardhome/compact.css') }),
+ sharedStatus.node,
+ settingsNode
+ ]);
+
+ if (initialLog.error)
+ message.textContent = _('Unable to read runtime log');
+ else if (!enabled)
+ message.textContent = _('Please add log path in config to enable log');
+ renderLog();
+
+ poll.add(function() {
+ if (clearButton.disabled)
+ return Promise.resolve();
+ var requestGeneration = generation;
+ return callRuntimeLog(offset, identity).then(function(result) {
+ if (requestGeneration !== generation)
+ return;
+ enabled = result.enabled;
+ if (!enabled) {
+ message.textContent = _('Please add log path in config to enable log');
+ return;
+ }
+ message.textContent = '';
+ if (result.reset || result.offset < offset)
+ content = '';
+ offset = result.offset;
+ identity = result.identity || '';
+ content = (content + decodeLog(result)).slice(-logLimit);
+ renderLog();
+ }).catch(function() {
+ message.textContent = _('Status unavailable');
+ });
+ }, 3);
+
+ return page;
+ });
+ }
+});
\ No newline at end of file
diff --git a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/manual.js b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/manual.js
new file mode 100644
index 00000000..638604de
--- /dev/null
+++ b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/manual.js
@@ -0,0 +1,232 @@
+'use strict';
+'require view';
+'require rpc';
+'require view/adguardhome/status as status';
+
+var callManualConfig = rpc.declare({
+ object: 'luci.adguardhome',
+ method: 'getManualConfig',
+ expect: { '': {} },
+ reject: true
+});
+
+var callSetManualConfig = rpc.declare({
+ object: 'luci.adguardhome',
+ method: 'setManualConfig',
+ params: [ 'content' ],
+ expect: { '': {} },
+ reject: true
+});
+
+var callChangeWebPassword = rpc.declare({
+ object: 'luci.adguardhome',
+ method: 'changeWebPassword',
+ params: [ 'hash' ],
+ expect: { '': {} },
+ reject: true
+});
+
+var codeMirrorLoader;
+var bcryptLoader;
+
+function loadBcrypt() {
+ if (window.TwinBcrypt)
+ return Promise.resolve(window.TwinBcrypt);
+
+ if (!bcryptLoader) {
+ bcryptLoader = new Promise(function(resolve, reject) {
+ var script = document.createElement('script');
+ script.src = L.resource('twin-bcrypt.min.js');
+ script.onload = function() {
+ if (window.TwinBcrypt)
+ resolve(window.TwinBcrypt);
+ else
+ reject(new Error('bcrypt unavailable'));
+ };
+ script.onerror = reject;
+ document.head.appendChild(script);
+ }).catch(function(error) {
+ bcryptLoader = null;
+ throw error;
+ });
+ }
+
+ return bcryptLoader;
+}
+
+function loadCodeMirror() {
+ if (window.CodeMirror)
+ return Promise.resolve(window.CodeMirror);
+ if (!codeMirrorLoader) {
+ codeMirrorLoader = new Promise(function(resolve, reject) {
+ var stylesheet = document.createElement('link');
+ stylesheet.rel = 'stylesheet';
+ stylesheet.href = L.resource('codemirror/lib/codemirror.css');
+ document.head.appendChild(stylesheet);
+ var theme = document.createElement('link');
+ theme.rel = 'stylesheet';
+ theme.href = L.resource('codemirror/theme/dracula.css');
+ document.head.appendChild(theme);
+ var script = document.createElement('script');
+ script.src = L.resource('codemirror/lib/codemirror.js');
+ script.onload = function() {
+ var yaml = document.createElement('script');
+ yaml.src = L.resource('codemirror/mode/yaml/yaml.js');
+ yaml.onload = function() { resolve(window.CodeMirror); };
+ yaml.onerror = reject;
+ document.head.appendChild(yaml);
+ };
+ script.onerror = reject;
+ document.head.appendChild(script);
+ });
+ }
+ return codeMirrorLoader;
+}
+
+return view.extend({
+ handleSave: null,
+ handleSaveApply: null,
+ handleReset: null,
+
+ load: function() {
+ return callManualConfig().catch(function() {
+ return { content: '', template: '', source: 'error' };
+ });
+ },
+
+ render: function(initial) {
+ var sharedStatus = status.render();
+ var config = initial;
+ var showingTemplate = false;
+ var editor;
+ var message = E('span', { 'style': 'margin-left:0.8em;' }, '');
+ var configArea = E('textarea', {
+ 'class': 'cbi-input-textarea',
+ 'rows': 20,
+ 'wrap': 'off',
+ 'style': 'width:100%;box-sizing:border-box;font-family:monospace;'
+ });
+ var editorWrap = E('div', { 'class': 'agh-editor-wrap' }, configArea);
+ var templateButton = E('button', {
+ 'class': 'btn cbi-button', 'type': 'button'
+ }, _('Use template'));
+ var reloadButton = E('button', {
+ 'class': 'btn cbi-button', 'type': 'button', 'style': 'margin-left:0.6em;'
+ }, _('Reload Config'));
+ var saveButton = E('button', {
+ 'class': 'btn cbi-button cbi-button-apply', 'type': 'button', 'style': 'margin-left:0.6em;'
+ }, _('Save & Apply'));
+ var passwordInput = E('input', {
+ 'class': 'cbi-input-password', 'type': 'password',
+ 'id': 'agh-web-password', 'autocomplete': 'new-password', 'spellcheck': 'false'
+ });
+ var passwordButton = E('button', {
+ 'class': 'btn cbi-button cbi-button-apply', 'type': 'button'
+ }, _('Change password'));
+ var passwordMessage = E('span', { 'style': 'margin-left:0.8em;' }, '');
+ var passwordPanel = E('div', { 'class': 'agh-toolbar agh-password-controls' }, [
+ E('label', { 'for': 'agh-web-password' }, _('Change browser management password')),
+ passwordInput, passwordButton, passwordMessage
+ ]);
+
+ function showConfig() {
+ var content = showingTemplate ? config.template : config.content;
+ if (editor)
+ editor.setValue(content);
+ else
+ configArea.value = content;
+ templateButton.disabled = !config.template;
+ }
+
+ templateButton.addEventListener('click', function() {
+ showingTemplate = true;
+ message.textContent = '';
+ showConfig();
+ });
+ reloadButton.addEventListener('click', function() {
+ reloadButton.disabled = true;
+ message.textContent = '';
+ callManualConfig().then(function(result) {
+ config = result;
+ showingTemplate = false;
+ showConfig();
+ }).catch(function() {
+ message.textContent = _('Unable to read config');
+ }).then(function() {
+ reloadButton.disabled = false;
+ });
+ });
+ saveButton.addEventListener('click', function() {
+ var content = editor ? editor.getValue() : configArea.value;
+ saveButton.disabled = true;
+ message.textContent = _('Saving...');
+ callSetManualConfig(content).then(function(result) {
+ if (!result.saved)
+ throw new Error(result.log || result.reason || 'save failed');
+ config.content = content;
+ config.source = 'current';
+ showingTemplate = false;
+ message.textContent = _('Saved');
+ }).catch(function(error) {
+ message.textContent = error.message || _('Save failed');
+ }).then(function() {
+ saveButton.disabled = false;
+ });
+ });
+ passwordButton.addEventListener('click', function() {
+ var password = passwordInput.value;
+ if (!password) {
+ passwordMessage.textContent = _('New password') + ' ' + _('is empty');
+ return;
+ }
+
+ passwordButton.disabled = true;
+ passwordMessage.textContent = _('Changing password...');
+ loadBcrypt().then(function(bcrypt) {
+ var hash = bcrypt.hashSync(password);
+ password = null;
+ passwordInput.value = '';
+ return callChangeWebPassword(hash);
+ }).then(function(result) {
+ if (!result.changed)
+ throw new Error(result.reason || 'change failed');
+ passwordMessage.textContent = _('Password changed');
+ }).catch(function() {
+ passwordMessage.textContent = _('Password change failed');
+ }).then(function() {
+ passwordButton.disabled = false;
+ });
+ });
+ showConfig();
+
+ var page = E('div', {}, [
+ E('link', { 'rel': 'stylesheet', 'href': L.resource('view/adguardhome/compact.css') }),
+ sharedStatus.node,
+ E('div', { 'class': 'cbi-section agh-compact-card agh-manual-card' }, [
+ editorWrap,
+ E('div', { 'class': 'agh-manual-toolbar' }, [
+ passwordPanel,
+ E('div', { 'class': 'agh-toolbar agh-config-actions' }, [message, templateButton, reloadButton, saveButton])
+ ])
+ ])
+ ]);
+ loadCodeMirror().then(function(CodeMirror) {
+ editor = CodeMirror.fromTextArea(configArea, {
+ mode: 'text/yaml',
+ lineNumbers: true,
+ theme: 'dracula',
+ lineWrapping: true,
+ matchBrackets: true
+ });
+ editor.getWrapperElement().style.fontSize = '15px';
+ editor.getWrapperElement().style.lineHeight = '1.5';
+ // Twenty lines at 1.5em, plus CodeMirror's vertical content padding.
+ editor.setSize(null, 'calc(30em + 8px)');
+ editor.refresh();
+ showConfig();
+ }).catch(function() {
+ message.textContent = _('Unable to load editor');
+ });
+ return page;
+ }
+});
diff --git a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/status.js b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/status.js
new file mode 100644
index 00000000..f976e2a9
--- /dev/null
+++ b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/status.js
@@ -0,0 +1,204 @@
+'use strict';
+'require baseclass';
+'require poll';
+'require rpc';
+
+var callCoreInfo = rpc.declare({
+ object: 'luci.adguardhome',
+ method: 'getCoreInfo',
+ expect: { '': {} },
+ reject: true
+});
+
+var callServiceList = rpc.declare({
+ object: 'service',
+ method: 'list',
+ params: [ 'name', 'verbose' ],
+ expect: { '': {} },
+ reject: true
+});
+
+var callRedirectStatus = rpc.declare({
+ object: 'luci.adguardhome',
+ method: 'getRedirectStatus',
+ expect: { '': {} },
+ reject: true
+});
+
+function modeLabel(mode) {
+ return {
+ none: _('Do not handle DNS'),
+ 'dnsmasq-upstream': _('Run as dnsmasq upstream server'),
+ redirect: _('Forward port 53 requests through the firewall'),
+ exchange: _('Use port 53 instead of dnsmasq')
+ }[mode] || _('Status unavailable');
+}
+
+function coreIsRunning(services, binpath) {
+ var instances = services.AdGuardHome && services.AdGuardHome.instances;
+ if (!instances)
+ return false;
+
+ return Object.keys(instances).some(function(name) {
+ var instance = instances[name];
+ return instance.running === true && Array.isArray(instance.command) && instance.command[0] === binpath;
+ });
+}
+
+return baseclass.extend({
+ render: function() {
+ var badgeStyle = 'display:inline-flex;align-items:center;padding:0.4em 0.8em;' +
+ 'border:1px solid;border-radius:999px;font-weight:600;line-height:1.4;';
+ var coreBadge = E('span', { 'style': badgeStyle }, _('Collecting data...'));
+ var serviceBadge = E('span', { 'style': badgeStyle }, _('Collecting data...'));
+ var redirectBadge = E('span', { 'style': badgeStyle }, _('Collecting data...'));
+ var node = E('div', { 'class': 'cbi-section' }, [
+ E('div', { 'style': 'display:flex;flex-wrap:wrap;gap:0.6em;padding:0.6em;' }, [
+ coreBadge, serviceBadge, redirectBadge
+ ])
+ ]);
+
+ function setBadge(badge, label, state) {
+ badge.textContent = label;
+ badge.style.backgroundColor = state === 'pending' ? '#fef3c7' : state === true ? '#dcfce7' : state === false ? '#fee2e2' : '#e5e7eb';
+ badge.style.color = state === 'pending' ? '#92400e' : state === true ? '#14532d' : state === false ? '#7f1d1d' : '#374151';
+ badge.style.borderColor = state === 'pending' ? '#fcd34d' : state === true ? '#86efac' : state === false ? '#fca5a5' : '#d1d5db';
+ }
+
+ function refreshCore() {
+ return callCoreInfo().then(function(info) {
+ if (!info.core_exists)
+ setBadge(coreBadge, _('Core') + ': ' + _('no core'), false);
+ else if (!info.version)
+ setBadge(coreBadge, _('Core') + ': ' + _('core error'), false);
+ else if (!info.config_exists)
+ setBadge(coreBadge, _('Core') + ': ' + info.version + ' (' + _('no config') + ')', false);
+ else
+ setBadge(coreBadge, _('Core') + ': ' + info.version, true);
+ }).catch(function() {
+ setBadge(coreBadge, _('Core') + ': ' + _('Status unavailable'), null);
+ });
+ }
+
+ var storageKey = 'adguardhome.pendingSwitch';
+ var pending = null;
+ var generation = 0;
+ var stable = 0;
+ var nextRefresh = 0;
+ var inFlight = null;
+ try {
+ var saved = JSON.parse(sessionStorage.getItem(storageKey));
+ if (saved && typeof saved.mode === 'string' && typeof saved.enabled === 'boolean' &&
+ typeof saved.started === 'number')
+ pending = saved;
+ } catch (e) {}
+
+ function remember() {
+ try {
+ if (pending)
+ sessionStorage.setItem(storageKey, JSON.stringify(pending));
+ else
+ sessionStorage.removeItem(storageKey);
+ } catch (e) {}
+ }
+
+ function showPending(running) {
+ var label = Date.now() - pending.started >= 30000
+ ? _('Switch not completed; check the log') : _('Switching');
+ if (typeof running === 'boolean')
+ setBadge(serviceBadge, running ? _('RUNNING') : _('NOT RUNNING'), running);
+ else
+ setBadge(serviceBadge, _('Checking service…'), 'pending');
+ setBadge(redirectBadge, label, 'pending');
+ }
+
+ function beginSwitch(target) {
+ generation++;
+ stable = 0;
+ nextRefresh = 0;
+ pending = { mode: target.mode, enabled: target.enabled, started: Date.now() };
+ remember();
+ showPending();
+ }
+
+ function cancelSwitch() {
+ generation++;
+ pending = null;
+ stable = 0;
+ nextRefresh = 0;
+ remember();
+ return refreshStatus();
+ }
+
+ function matches(info, running, target) {
+ return !info.busy && info.mode === target.mode && info.enabled === target.enabled &&
+ info.applied_enabled === target.enabled && running === target.enabled &&
+ (!target.enabled || info.applied_mode === target.mode) &&
+ info.redirected === (target.enabled && target.mode !== 'none');
+ }
+
+ function refreshStatus() {
+ if (inFlight)
+ return inFlight;
+ var currentGeneration = generation;
+ var requestedAt = Date.now();
+ inFlight = Promise.all([callRedirectStatus(), callServiceList('AdGuardHome', true)]).then(function(results) {
+ if (currentGeneration !== generation)
+ return;
+ var info = results[0];
+ var running = coreIsRunning(results[1], info.binpath);
+ // Recover switching feedback after navigation or an apply from another page.
+ if (!pending && (info.busy || info.mode !== info.applied_mode || info.enabled !== info.applied_enabled))
+ beginSwitch(info);
+ if (pending) {
+ stable = matches(info, running, pending) ? stable + 1 : 0;
+ // Two consecutive samples avoid declaring a short-lived process healthy.
+ if (stable < 2) {
+ showPending(running);
+ return;
+ }
+ pending = null;
+ remember();
+ }
+ setBadge(serviceBadge, running ? _('RUNNING') : _('NOT RUNNING'), running);
+ var active = info.redirected && running && info.mode === info.applied_mode;
+ var label = modeLabel(info.mode);
+ if (!info.enabled)
+ label += ' (' + _('Disabled') + ')';
+ else if (info.mode !== 'none' && !active)
+ label += ' (' + _('Not redirect') + ')';
+ setBadge(redirectBadge, label,
+ !info.enabled || info.mode === 'none' ? null : active);
+ }).catch(function() {
+ if (currentGeneration !== generation)
+ return;
+ stable = 0;
+ if (pending) {
+ showPending();
+ setBadge(serviceBadge, _('Status unavailable'), null);
+ setBadge(redirectBadge, _('Unable to read switch status') + ' (' + modeLabel(pending.mode) + ')', null);
+ } else {
+ setBadge(serviceBadge, _('Status unavailable'), null);
+ setBadge(redirectBadge, _('Status unavailable'), null);
+ }
+ }).then(function() {
+ nextRefresh = requestedAt + (pending ? 1000 : 3000);
+ inFlight = null;
+ });
+ return inFlight;
+ }
+
+ if (pending)
+ showPending();
+ refreshCore();
+ refreshStatus();
+ poll.add(function() {
+ if (Date.now() >= nextRefresh)
+ return refreshStatus();
+ }, 1);
+
+ return { node: node, refresh: function() {
+ return Promise.all([refreshCore(), refreshStatus()]);
+ }, beginSwitch: beginSwitch, cancelSwitch: cancelSwitch };
+ }
+});
diff --git a/luci-app-adguardhome/scripts/build-apk.sh b/luci-app-adguardhome/scripts/build-apk.sh
new file mode 100755
index 00000000..a1aacdc9
--- /dev/null
+++ b/luci-app-adguardhome/scripts/build-apk.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$SDK_DIR"
+
+mkdir -p package/${PKG_DIR_NAME}
+rsync -a --delete "$GITHUB_WORKSPACE/" "package/${PKG_DIR_NAME}/" \
+ --exclude '.git*' --exclude '.local' --exclude '_sdk' --exclude 'dist'
+
+./scripts/feeds update -a
+./scripts/feeds install luci-base tar
+
+touch .config
+sed -i -E '/^(# )?CONFIG_(ALL|ALL_KMODS|ALL_NONSHARED|LUCI_LANG_zh_Hans|PACKAGE_TAR_XZ|PACKAGE_luci-app-adguardhome-tk|PACKAGE_luci-i18n-adguardhome-tk-zh-cn)(=| is not set)/d' .config
+printf '%s\n' \
+ 'CONFIG_ALL=n' \
+ 'CONFIG_ALL_KMODS=n' \
+ 'CONFIG_ALL_NONSHARED=n' \
+ 'CONFIG_LUCI_LANG_zh_Hans=y' \
+ '# CONFIG_PACKAGE_TAR_XZ is not set' \
+ 'CONFIG_PACKAGE_luci-app-adguardhome-tk=y' >> .config
+make defconfig
+if ! grep -Eq '^CONFIG_PACKAGE_luci-app-adguardhome-tk=(y|m)$' .config; then
+ echo '::error::luci-app-adguardhome-tk was not enabled after make defconfig'
+ grep -n 'luci-app-adguardhome-tk' .config || true
+ sed -n '/^[[:space:]]*config PACKAGE_luci-app-adguardhome-tk$/,/^[[:space:]]*help/p' tmp/.config-package.in
+ exit 1
+fi
+
+grep -Eq '^CONFIG_PACKAGE_luci-i18n-adguardhome-tk-zh-cn=(y|m)$' .config || {
+ echo '::error::Simplified Chinese translation package was not enabled'
+ exit 1
+}
+
+make package/compile -j"$(nproc)" V=sc
diff --git a/luci-app-adguardhome/scripts/collect-apks.sh b/luci-app-adguardhome/scripts/collect-apks.sh
new file mode 100755
index 00000000..b9067ad2
--- /dev/null
+++ b/luci-app-adguardhome/scripts/collect-apks.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -euo pipefail
+mkdir -p "$GITHUB_WORKSPACE/dist"
+for package in "$PKG_DIR_NAME" luci-i18n-adguardhome-tk-zh-cn; do
+ mapfile -t packages < <(find "$SDK_DIR/bin/packages" -type f -name "${package}-*.apk")
+ if (( ${#packages[@]} == 0 )); then
+ echo "::error::Missing APK: ${package}"
+ exit 1
+ fi
+ cp "${packages[@]}" "$GITHUB_WORKSPACE/dist/"
+done
diff --git a/luci-app-adguardhome/scripts/install.sh b/luci-app-adguardhome/scripts/install.sh
new file mode 100644
index 00000000..54121919
--- /dev/null
+++ b/luci-app-adguardhome/scripts/install.sh
@@ -0,0 +1,86 @@
+#!/bin/sh
+set -e
+
+REPO="w9315273/luci-app-adguardhome"
+LANGUAGE=${1:-en}
+case "$LANGUAGE" in
+ en|zh-cn) ;;
+ *) echo "Usage: sh install.sh [en|zh-cn]"; exit 1 ;;
+esac
+[ "$#" -le 1 ] || { echo "Usage: sh install.sh [en|zh-cn]"; exit 1; }
+
+echo "Getting latest version..."
+RELEASE=$(uclient-fetch -qO- "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null)
+TAG=$(printf '%s\n' "$RELEASE" | sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
+
+[ -n "$TAG" ] || { echo "ERROR: Failed to get latest version"; exit 1; }
+
+case "$TAG" in
+ *-*) ;;
+ *) echo "ERROR: Unexpected tag format: $TAG"; exit 1 ;;
+esac
+
+VERSION=${TAG#v}
+VERSION=${VERSION%-*}-r${VERSION##*-}
+
+PACKAGE=luci-app-adguardhome
+I18N_PACKAGE=luci-i18n-adguardhome-zh-cn
+if printf '%s\n' "$RELEASE" | grep -Eq '"name"[[:space:]]*:[[:space:]]*"luci-app-adguardhome-tk-[^"]*\.apk"'; then
+ PACKAGE=luci-app-adguardhome-tk
+ I18N_PACKAGE=luci-i18n-adguardhome-tk-zh-cn
+fi
+URL="https://github.com/${REPO}/releases/download/${TAG}/${PACKAGE}-${VERSION}.apk"
+
+echo "Version: $VERSION"
+echo "Downloading..."
+work=$(mktemp -d /tmp/agh-install.XXXXXX)
+trap 'rm -rf "$work"' EXIT
+uclient-fetch -qO "$work/main.apk" "$URL"
+set -- "$work/main.apk"
+
+I18N="${I18N_PACKAGE}-${VERSION}.apk"
+has_i18n=0
+if [ "$LANGUAGE" = zh-cn ] && printf '%s\n' "$RELEASE" | grep -Eq "\"name\"[[:space:]]*:[[:space:]]*\"${I18N_PACKAGE}-[^\"]*\\.apk\""; then
+ uclient-fetch -qO "$work/zh-cn.apk" "https://github.com/${REPO}/releases/download/${TAG}/${I18N}"
+ set -- "$@" "$work/zh-cn.apk"
+ has_i18n=1
+fi
+
+if [ "$LANGUAGE" = zh-cn ] && [ "$PACKAGE" = luci-app-adguardhome-tk ] && [ "$has_i18n" != 1 ]; then
+ echo "ERROR: The release is missing the Chinese package"
+ exit 1
+fi
+
+legacy_packages=
+if [ "$PACKAGE" = luci-app-adguardhome-tk ]; then
+ installed=$(apk info)
+ for old in luci-app-adguardhome luci-i18n-adguardhome-zh-cn; do
+ if printf '%s\n' "$installed" | grep -qx "$old"; then
+ if [ "$old" != luci-i18n-adguardhome-zh-cn ] || [ "$LANGUAGE" = zh-cn ]; then
+ set -- "$@" "$old"
+ fi
+ legacy_packages="$legacy_packages $old"
+ fi
+ done
+fi
+
+echo "Installing..."
+apk add --allow-untrusted "$@"
+if [ -n "$legacy_packages" ]; then
+ apk del $legacy_packages
+fi
+
+if [ "$LANGUAGE" = en ]; then
+ installed=$(apk info)
+ for translation in luci-i18n-adguardhome-zh-cn luci-i18n-adguardhome-tk-zh-cn; do
+ if printf '%s\n' "$installed" | grep -qx "$translation"; then
+ apk del "$translation"
+ fi
+ done
+fi
+
+echo "Refreshing LuCI..."
+rm -f /tmp/luci-indexcache /tmp/luci-indexcache.* /tmp/luci-modulecache/* 2>/dev/null
+/etc/init.d/rpcd reload >/dev/null 2>&1
+
+echo "Done."
diff --git a/luci-app-adguardhome/scripts/prepare-sdk.sh b/luci-app-adguardhome/scripts/prepare-sdk.sh
new file mode 100755
index 00000000..f1c2f0e6
--- /dev/null
+++ b/luci-app-adguardhome/scripts/prepare-sdk.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$SDK_DIR"
+
+if [[ ! -x scripts/feeds ]]; then
+ runuser -u buildbot -- bash ./setup.sh
+fi
+
+mkdir -p dl
+chown -R buildbot:buildbot dl