diff --git a/luci-app-adguardhome/.github/workflows/build-apk.yaml b/luci-app-adguardhome/.github/workflows/build-apk.yaml
deleted file mode 100644
index 64b83777..00000000
--- a/luci-app-adguardhome/.github/workflows/build-apk.yaml
+++ /dev/null
@@ -1,70 +0,0 @@
-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 05dfba47..10d5c0e3 100644
--- a/luci-app-adguardhome/.gitignore
+++ b/luci-app-adguardhome/.gitignore
@@ -1,2 +1,9 @@
.DS_Store
-.local
\ No newline at end of file
+
+# Local debug artifacts from playwright-cli UI verification
+adg-*.png
+m-*.png
+bs-*.png
+*.png
+!screenshots/*.png
+.playwright-cli/
diff --git a/luci-app-adguardhome/Makefile b/luci-app-adguardhome/Makefile
index 4eb97f6f..a3d2ffc4 100644
--- a/luci-app-adguardhome/Makefile
+++ b/luci-app-adguardhome/Makefile
@@ -1,109 +1,79 @@
+# Copyright (C) 2018-2024 OpenWrt luci-app-adguardhome contributors
#
-# Copyright (C) 2025 w9315273
+# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
-PKG_NAME:=luci-app-adguardhome-tk
-PKG_VERSION:=2.6.0
-PKG_RELEASE:=3
+PKG_NAME:=luci-app-adguardhome
+PKG_MAINTAINER:=kenzok8
+PKG_VERSION:=1.0
+PKG_RELEASE:=9
-PKG_MAINTAINER:=w9315273
-PKG_BUILD_DEPENDS:=luci-base/host
+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
-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
+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.
endef
-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
+PKG_CONFIG_DEPENDS:= CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_binary
-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
+define Package/luci-app-adguardhome/conffiles
+/usr/share/AdGuardHome/links.txt
/etc/config/AdGuardHome
endef
-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
+define Package/luci-app-adguardhome/postinst
#!/bin/sh
-[ -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
-
+ # 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
exit 0
endef
-define Package/luci-app-adguardhome-tk/prerm
+define Package/luci-app-adguardhome/prerm
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
- /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
+ /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
fi
-
exit 0
endef
-$(eval $(call BuildPackage,luci-app-adguardhome-tk))
+include $(TOPDIR)/feeds/luci/luci.mk
-$(eval $(call BuildPackage,luci-i18n-adguardhome-tk-zh-cn))
+# call BuildPackage - OpenWrt buildroot signature
+$(eval $(call BuildPackage,luci-app-adguardhome))
diff --git a/luci-app-adguardhome/README.md b/luci-app-adguardhome/README.md
new file mode 100644
index 00000000..ecdba15b
--- /dev/null
+++ b/luci-app-adguardhome/README.md
@@ -0,0 +1,81 @@
+# 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
deleted file mode 100644
index 1dc3727a..00000000
--- a/luci-app-adguardhome/docs/README.en.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# 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
deleted file mode 100644
index ae6ad614..00000000
--- a/luci-app-adguardhome/docs/README.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# 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
deleted file mode 100644
index 6e67b273..00000000
Binary files a/luci-app-adguardhome/docs/screenshots/base-en.png and /dev/null differ
diff --git a/luci-app-adguardhome/docs/screenshots/base-zh-cn.png b/luci-app-adguardhome/docs/screenshots/base-zh-cn.png
deleted file mode 100644
index 221291f6..00000000
Binary files a/luci-app-adguardhome/docs/screenshots/base-zh-cn.png and /dev/null differ
diff --git a/luci-app-adguardhome/docs/screenshots/log-en.png b/luci-app-adguardhome/docs/screenshots/log-en.png
deleted file mode 100644
index 554ae409..00000000
Binary files a/luci-app-adguardhome/docs/screenshots/log-en.png and /dev/null differ
diff --git a/luci-app-adguardhome/docs/screenshots/log-zh-cn.png b/luci-app-adguardhome/docs/screenshots/log-zh-cn.png
deleted file mode 100644
index c16c2aa7..00000000
Binary files a/luci-app-adguardhome/docs/screenshots/log-zh-cn.png and /dev/null differ
diff --git a/luci-app-adguardhome/docs/screenshots/manual-en.png b/luci-app-adguardhome/docs/screenshots/manual-en.png
deleted file mode 100644
index c05878cc..00000000
Binary files a/luci-app-adguardhome/docs/screenshots/manual-en.png and /dev/null differ
diff --git a/luci-app-adguardhome/docs/screenshots/manual-zh-cn.png b/luci-app-adguardhome/docs/screenshots/manual-zh-cn.png
deleted file mode 100644
index 870cafef..00000000
Binary files a/luci-app-adguardhome/docs/screenshots/manual-zh-cn.png and /dev/null 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
new file mode 100644
index 00000000..a6ee5020
--- /dev/null
+++ b/luci-app-adguardhome/htdocs/luci-static/resources/adguardhome/adg.css
@@ -0,0 +1,360 @@
+/* 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
new file mode 100644
index 00000000..e483ee42
--- /dev/null
+++ b/luci-app-adguardhome/htdocs/luci-static/resources/view/luci-app-adguardhome/index.js
@@ -0,0 +1,14 @@
+// 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
new file mode 100644
index 00000000..658f34c0
--- /dev/null
+++ b/luci-app-adguardhome/luasrc/controller/AdGuardHome.lua
@@ -0,0 +1,274 @@
+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
new file mode 100644
index 00000000..961201a2
--- /dev/null
+++ b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua
@@ -0,0 +1,244 @@
+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
new file mode 100644
index 00000000..5d18a88d
--- /dev/null
+++ b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/log.lua
@@ -0,0 +1,16 @@
+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
new file mode 100644
index 00000000..d7bf82e9
--- /dev/null
+++ b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/manual.lua
@@ -0,0 +1,121 @@
+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
new file mode 100644
index 00000000..4ece44fd
--- /dev/null
+++ b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/overview.lua
@@ -0,0 +1,16 @@
+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
new file mode 100644
index 00000000..55a520b0
--- /dev/null
+++ b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/tools.lua
@@ -0,0 +1,113 @@
+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
new file mode 100644
index 00000000..251374b0
Binary files /dev/null and b/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_check.htm differ
diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm
new file mode 100644
index 00000000..1c274fbf
--- /dev/null
+++ b/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm
@@ -0,0 +1,51 @@
+<%+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
new file mode 100644
index 00000000..b0e8bb36
--- /dev/null
+++ b/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_status.htm
@@ -0,0 +1,145 @@
+
+
+
diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/enable_switch.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/enable_switch.htm
new file mode 100644
index 00000000..96ff2b4d
--- /dev/null
+++ b/luci-app-adguardhome/luasrc/view/AdGuardHome/enable_switch.htm
@@ -0,0 +1,169 @@
+<%+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
new file mode 100644
index 00000000..583a5ae5
--- /dev/null
+++ b/luci-app-adguardhome/luasrc/view/AdGuardHome/head.htm
@@ -0,0 +1,83 @@
+
diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm
new file mode 100644
index 00000000..e38d8015
--- /dev/null
+++ b/luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm
@@ -0,0 +1,186 @@
+<%+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
new file mode 100644
index 00000000..a818b741
--- /dev/null
+++ b/luci-app-adguardhome/luasrc/view/AdGuardHome/log_empty.htm
@@ -0,0 +1,27 @@
+<%
+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
new file mode 100644
index 00000000..a70c4333
--- /dev/null
+++ b/luci-app-adguardhome/luasrc/view/AdGuardHome/overview.htm
@@ -0,0 +1,286 @@
+<%+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
new file mode 100644
index 00000000..2ecdbd0a
--- /dev/null
+++ b/luci-app-adguardhome/luasrc/view/AdGuardHome/yamleditor.htm
@@ -0,0 +1,84 @@
+<%+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
deleted file mode 100644
index c465821f..00000000
--- a/luci-app-adguardhome/po/zh-cn/AdGuardHome.po
+++ /dev/null
@@ -1,231 +0,0 @@
-#/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
new file mode 100644
index 00000000..4bef352f
--- /dev/null
+++ b/luci-app-adguardhome/root/etc/AdGuardHome.yaml
@@ -0,0 +1,226 @@
+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 fb562bb1..827b291d 100644
--- a/luci-app-adguardhome/root/etc/config/AdGuardHome
+++ b/luci-app-adguardhome/root/etc/config/AdGuardHome
@@ -1,11 +1,17 @@
-
config AdGuardHome 'AdGuardHome'
option enabled '0'
option httpport '3000'
option redirect 'none'
option configpath '/etc/AdGuardHome.yaml'
- option workdir '/tmp/AdGuardHome'
+ option workdir '/etc/AdGuardHome'
option logfile '/tmp/AdGuardHome.log'
option verbose '0'
- option binpath '/usr/bin/AdGuardHome/AdGuardHome'
- list wan_ifname 'eth0'
+ option binpath '/usr/bin/AdGuardHome'
+ option upxflag ''
+ option user ''
+ option group ''
+ option advanced_settings '0'
+ option gc '0'
+ option maxprocs '0'
+ option memlimit '0'
+
diff --git a/luci-app-adguardhome/root/etc/init.d/AdGuardHome b/luci-app-adguardhome/root/etc/init.d/AdGuardHome
index dfa233b6..651ecd90 100755
--- a/luci-app-adguardhome/root/etc/init.d/AdGuardHome
+++ b/luci-app-adguardhome/root/etc/init.d/AdGuardHome
@@ -6,648 +6,685 @@ START=95
STOP=01
CONFIGURATION=AdGuardHome
-NFT_RULES_FILE="/var/etc/adguardhome.nft"
-NFT_RULES_TPL="/usr/share/AdGuardHome/adguardhome.nft.tpl"
-NFT_TABLE="adguardhome"
+CRON_FILE=/etc/crontabs/root
-EXTRA_COMMANDS="do_redirect force_reload isrunning"
-EXTRA_HELP=" do_redirect 0 or 1\
- force_reload
- isrunning"
+extra_command "do_redirect" "0 or 1"
+extra_command "testbackup" "backup or restore"
+extra_command "test_crontab"
+extra_command "force_reload"
+extra_command "isrunning"
-get_wan_return_line() {
- local wan
- wan="$(uci -q get AdGuardHome.AdGuardHome.wan_ifname 2>/dev/null)"
+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
- [ -z "$wan" ] && { echo ""; return 0; }
-
- local wan_set
- wan_set="\"$(printf '%s' "$wan" | sed 's/ /\", \"/g')\""
- echo "iifname { $wan_set } return"
+ 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
}
-set_nft_redirect() {
- local port="$1"
- case "$port" in
- *[!0-9]*|"") return 1 ;;
- esac
- [ ! -f "$NFT_RULES_TPL" ] && return 1
+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
- 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
+ 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
}
-clear_nft_redirect() {
- uci -q delete firewall.adguardhome_nft_include
- uci -q commit firewall
+set_iptable()
+{
+ local ipv6_server="$1"
+ local tcp_server="$2"
- [ -f "$NFT_RULES_FILE" ] && rm -f "$NFT_RULES_FILE"
- nft delete table inet "$NFT_TABLE" 2>/dev/null
+ 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
}
-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
-}
+clear_iptable()
+{
+ uci -q batch <<-EOF >/dev/null 2>&1
+ delete firewall.AdGuardHome
+ commit firewall
+ EOF
-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
-}
+ 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
-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"
+ [ "$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
}
service_triggers() {
- procd_add_reload_trigger "$CONFIGURATION"
+ procd_add_reload_trigger "$CONFIGURATION"
+ [ "$(uci get AdGuardHome.AdGuardHome.redirect 2>/dev/null)" = "redirect" ] && procd_add_reload_trigger firewall
}
-isrunning() {
- config_load "${CONFIGURATION}"
- _isrunning
- local r=$?
- [ "$r" = "0" ] && echo "running" || ([ "$r" = "1" ] && echo "not run" || echo "no bin")
- return $r
+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_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome/AdGuardHome"
- [ ! -f "$binpath" ] && return 2
- pgrep -f "$(basename "$binpath")" >/dev/null 2>&1 && return 0
- return 1
+_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
}
-force_reload() {
- config_load "${CONFIGURATION}"
- _isrunning && procd_send_signal "$CONFIGURATION" || start
+force_reload(){
+ config_load "${CONFIGURATION}"
+ if _isrunning; then
+ procd_send_signal "$CONFIGURATION"
+ else
+ start
+ fi
}
-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
+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_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);
- }'
+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
}
-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"
+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
}
boot_service() {
- rm -f /var/run/AdGserverdis 2>/dev/null
- config_load "${CONFIGURATION}"
- config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome/AdGuardHome"
- [ -f "$binpath" ] && start_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
}
start_service() {
- 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"
+ rm -f /var/run/AdGserverdis >/dev/null 2>&1
+ config_load "${CONFIGURATION}"
- 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"
+ config_get hashpass $CONFIGURATION hashpass ""
+ config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
- 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"
+ if [ -n "$hashpass" ]; then
+ config_editor "users.password" "$hashpass" "$configpath"
+ uci set "$CONFIGURATION"."$CONFIGURATION".hashpass=""
+ uci commit AdGuardHome
+ fi
- printf '%s\n' "$binpath" "$configpath" > /lib/upgrade/keep.d/luci-app-adguardhome
+ local enabled
+ config_get_bool enabled $CONFIGURATION enabled 0
- config_get logfile $CONFIGURATION logfile ""
- if [ -n "$logfile" ]; then
- ADDITIONAL_ARGS="$ADDITIONAL_ARGS -l $logfile"
- fi
+ do_crontab
- if [ ! -f "$binpath" ]; then
- _do_redirect 0
- /usr/share/AdGuardHome/update_core.sh >/dev/null 2>&1 &
- exit 0
- 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
- config_get_bool verbose $CONFIGURATION verbose 0
- if [ "$verbose" -eq 1 ]; then
- ADDITIONAL_ARGS="$ADDITIONAL_ARGS -v"
- fi
+ config_get workdir $CONFIGURATION workdir "/etc/AdGuardHome"
+ config_get backupfile $CONFIGURATION backupfile ""
+ mkdir -p "$workdir/data"
- 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
+ if [ -n "$backupfile" ] && [ ! -d "$workdir/data" ]; then
+ restore
+ 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 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
- 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"
+ local ADDITIONAL_ARGS=""
+ config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome"
+ mkdir -p "${binpath%/*}"
+ 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 2>/dev/null
- echo "AdGuardHome reloading"
- start
+reload_service()
+{
+ rm -f /var/run/AdGlucitest >/dev/null 2>&1
+ echo "AdGuardHome reloading"
+ start
}
-stop_service() {
- stop_core || return 1
- config_load "${CONFIGURATION}"
- _do_redirect 0
- echo "AdGuardHome service disabled"
- touch /var/run/AdGserverdis
+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
}
boot() {
- rc_procd boot_service "$@"
- eval "type service_started" 2>/dev/null >/dev/null && service_started
-}
\ No newline at end of file
+ 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
+}
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 100755
new mode 100644
index e736ba60..37e192cd
--- a/luci-app-adguardhome/root/etc/uci-defaults/40_luci-AdGuardHome
+++ b/luci-app-adguardhome/root/etc/uci-defaults/40_luci-AdGuardHome
@@ -1,14 +1,15 @@
#!/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
-# 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/*
+rm -f /tmp/luci-indexcache
+
+chmod +x /etc/init.d/AdGuardHome /usr/share/AdGuardHome/*
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
deleted file mode 100644
index 1a5bd6dc..00000000
--- a/luci-app-adguardhome/root/lib/upgrade/keep.d/luci-app-adguardhome
+++ /dev/null
@@ -1 +0,0 @@
-/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
new file mode 100644
index 00000000..7c23d77a
Binary files /dev/null and b/luci-app-adguardhome/root/usr/lib/lua/luci/i18n/adguardhome.zh-cn.lmo 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 100755
new mode 100644
index b6f63852..612a5770
--- a/luci-app-adguardhome/root/usr/share/AdGuardHome/AdGuardHome_template.yaml
+++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/AdGuardHome_template.yaml
@@ -1,123 +1,108 @@
-# 模板用户名为 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
+bind_host: 0.0.0.0
+bind_port: 3000
+beta_bind_port: 0
users:
- - name: admin
- password: $2y$10$iGkTW98QtGEZn4I/GC0J4eQhfuLQ53v7L4DDVauEJyWylJrXWwWeO
+- name: root
+ password: $2y$10$dwn0hTYoECQMZETBErGlzOId2VANOVsPHsuH13TM/8KnysM5Dh/ve
auth_attempts: 5
block_auth_min: 15
http_proxy: ""
-language: ""
-theme: auto
+language: zh-cn
+debug_pprof: false
+web_session_ttl: 720
dns:
bind_hosts:
- - 0.0.0.0
- port: 3053
+ - 0.0.0.0
+ port: 1745
+ statistics_interval: 30
+ querylog_enabled: true
+ querylog_file_enabled: true
+ querylog_interval: 6h
+ querylog_size_memory: 1000
anonymize_client_ip: false
- ratelimit: 20
- ratelimit_subnet_len_ipv4: 24
- ratelimit_subnet_len_ipv6: 56
+ 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_whitelist: []
- refuse_any: true
+ refuse_any: false
upstream_dns:
- - 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'
+ - 223.5.5.5
upstream_dns_file: ""
bootstrap_dns:
- - 1.1.1.1
- - 1.0.0.1
- fallback_dns:
- - quic://dns.adguard-dns.com
- upstream_mode: parallel
+ - 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
+ - version.bind
+ - id.server
+ - hostname.bind
trusted_proxies:
- - 127.0.0.0/8
- - ::1/128
- cache_enabled: true
+ - 127.0.0.0/8
+ - ::1/128
cache_size: 4194304
cache_ttl_min: 0
cache_ttl_max: 0
- cache_optimistic: false
- cache_optimistic_answer_ttl: 30s
- cache_optimistic_max_age: 12h
+ cache_optimistic: true
bogus_nxdomain: []
aaaa_disabled: false
enable_dnssec: false
- edns_client_subnet:
- custom_ip: ""
- enabled: false
- use_custom: false
+ edns_client_subnet: false
max_goroutines: 300
- handle_ddr: true
ipset: []
- ipset_file: ""
- bootstrap_prefer_ipv6: false
+ 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
- private_networks: []
+ local_domain_name: lan
+ resolve_clients: true
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: 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: ""
- 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/HostlistsRegistry/assets/filter_1.txt
- name: AdGuard DNS filter
- id: 1
+- 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
whitelist_filters: []
user_rules: []
dhcp:
enabled: false
interface_name: ""
- local_domain_name: lan
dhcpv4:
gateway_ip: ""
subnet_mask: ""
@@ -131,59 +116,16 @@ dhcp:
lease_duration: 86400
ra_slaac_only: false
ra_allow_slaac: 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
+clients: []
+log_compress: false
+log_localtime: false
+log_max_backups: 0
+log_max_size: 100
+log_max_age: 3
+log_file: ""
+verbose: false
os:
group: ""
user: ""
rlimit_nofile: 0
-schema_version: 32
\ No newline at end of file
+schema_version: 12
diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/addhost.sh b/luci-app-adguardhome/root/usr/share/AdGuardHome/addhost.sh
new file mode 100755
index 00000000..a23a2efc
--- /dev/null
+++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/addhost.sh
@@ -0,0 +1,54 @@
+#!/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
deleted file mode 100644
index 67eef35e..00000000
--- a/luci-app-adguardhome/root/usr/share/AdGuardHome/adguardhome.nft.tpl
+++ /dev/null
@@ -1,10 +0,0 @@
-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
new file mode 100644
index 00000000..562117e5
--- /dev/null
+++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/firewall.start
@@ -0,0 +1,8 @@
+#!/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 e6387b26..908bdf63 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 2>/dev/null)
- if [ "${watchdog}x" = "0x" ]; then
- kill "$pid"
+ watchdog=$(cat /var/run/AdGuardHomesyslog)
+ if [ "$watchdog"x == "0"x ]; 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
\ No newline at end of file
+done
diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/links.txt b/luci-app-adguardhome/root/usr/share/AdGuardHome/links.txt
new file mode 100644
index 00000000..e4f1c8fa
--- /dev/null
+++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/links.txt
@@ -0,0 +1,3 @@
+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
new file mode 100755
index 00000000..9ccc2190
--- /dev/null
+++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/tailto.sh
@@ -0,0 +1,5 @@
+#!/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 2be7a26c..1464be6a 100755
--- a/luci-app-adguardhome/root/usr/share/AdGuardHome/update_core.sh
+++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/update_core.sh
@@ -1,164 +1,321 @@
#!/bin/sh
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
-update_mode=$1
-# 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)
+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"
-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
-}
+mkdir -p "${binpath%/*}"
-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"
-}
+upxflag="$(uci get AdGuardHome.AdGuardHome.upxflag 2>/dev/null)"
-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(){
+cleanup() {
+ rm -f /var/run/update_core 2>/dev/null
+ [ "$1" != "0" ] && touch /var/run/update_core_error
exit "$1"
}
-Check_Updates "$update_mode"
+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"
diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/waitnet.sh b/luci-app-adguardhome/root/usr/share/AdGuardHome/waitnet.sh
new file mode 100755
index 00000000..d45518af
--- /dev/null
+++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/waitnet.sh
@@ -0,0 +1,21 @@
+#!/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 f6850663..61ba09de 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
-exit 0
\ No newline at end of file
+return 0
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 6cdcf8bb..189428c2 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,45 +1,53 @@
{
- "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" ]
- }
- }
+ "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"
+ }
+ }
}
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 41727fce..485aa620 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,20 +2,10 @@
"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
deleted file mode 100644
index 6e46f612..00000000
--- a/luci-app-adguardhome/root/usr/share/rpcd/ucode/luci.adguardhome
+++ /dev/null
@@ -1,265 +0,0 @@
-#!/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
new file mode 100644
index 00000000..f93d42b7
--- /dev/null
+++ b/luci-app-adguardhome/root/www/luci-static/resources/codemirror/addon/fold/foldcode.js
@@ -0,0 +1 @@
+!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
deleted file mode 100644
index ae1163c4..00000000
--- a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/base.js
+++ /dev/null
@@ -1,276 +0,0 @@
-'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
deleted file mode 100644
index ec755092..00000000
--- a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/compact.css
+++ /dev/null
@@ -1,153 +0,0 @@
-/* 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
deleted file mode 100644
index 8fc0bf9e..00000000
--- a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/log.js
+++ /dev/null
@@ -1,242 +0,0 @@
-'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
deleted file mode 100644
index 638604de..00000000
--- a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/manual.js
+++ /dev/null
@@ -1,232 +0,0 @@
-'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
deleted file mode 100644
index f976e2a9..00000000
--- a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/status.js
+++ /dev/null
@@ -1,204 +0,0 @@
-'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
deleted file mode 100755
index a1aacdc9..00000000
--- a/luci-app-adguardhome/scripts/build-apk.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/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
deleted file mode 100755
index b9067ad2..00000000
--- a/luci-app-adguardhome/scripts/collect-apks.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/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
deleted file mode 100644
index 54121919..00000000
--- a/luci-app-adguardhome/scripts/install.sh
+++ /dev/null
@@ -1,86 +0,0 @@
-#!/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
deleted file mode 100755
index f1c2f0e6..00000000
--- a/luci-app-adguardhome/scripts/prepare-sdk.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/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