Add c-ares package support and improve DoH failover

- Generalize custom release package downloading for AWG and c-ares.
- Download libcares from c-ares-openwrt-package releases and add --skip-cares-download.
- Add libcares to required router packages.
- Simplify DoH failover and rely on procd for https-dns-proxy lifecycle management.
- Repair missing provider-domain dnsmasq forwards without requiring an endpoint switch.
- Remove the custom https-dns-proxy respawn patch.
- Update README examples and package documentation.
This commit is contained in:
Karen Khachatryan 2026-08-16 22:46:00 +03:00
parent fe95095843
commit 39c89af7c6
5 changed files with 114 additions and 39 deletions

View file

@ -561,20 +561,20 @@ vim config.json
./upgrade_routers.py
```
Для локального просмотра структуры, без загрузки AWG пакетов и без синхронизации `packages/`, можно запускать так:
Для локального просмотра структуры, без загрузки AWG/c-ares пакетов и без синхронизации `packages/`, можно запускать так:
```sh
./generate_configs.py --skip-awg-download --skip-package-sync
./generate_configs.py --skip-awg-download --skip-cares-download --skip-package-sync
```
Такой режим удобен, если AWG `.apk` и per-router package repos уже не нужны для текущей проверки.
Такой режим удобен, если custom `.apk` и per-router package repos уже не нужны для текущей проверки.
Hooks при этом все равно запускаются, поэтому `tools/generate.py` все еще может требовать `wg` и `openssl`, если нужно создать недостающие WireGuard/OpenVPN secrets.
Для просмотра только синхронизированной template структуры без generator hooks добавляйте `--skip-hooks`:
```sh
./generate_configs.py --skip-awg-download --skip-package-sync --skip-hooks
./generate_configs.py --skip-awg-download --skip-cares-download --skip-package-sync --skip-hooks
```
Dynamic direct-list sources из `tools/default.py` должны быть доступны, если они включены.
@ -582,7 +582,7 @@ Dynamic direct-list sources из `tools/default.py` должны быть дос
Если нужен полностью локальный smoke run без загрузки country/ASN direct-list IP sets, добавьте `--skip-direct-downloads`:
```sh
./generate_configs.py --skip-awg-download --skip-package-sync --skip-direct-downloads
./generate_configs.py --skip-awg-download --skip-cares-download --skip-package-sync --skip-direct-downloads
```
В этом режиме generated `direct.txt` будет содержать только static direct entries:
@ -1034,7 +1034,16 @@ Dnsmasq по умолчанию смотрит на:
1. DoH endpoints из `https-dns-proxy`.
1. DNS servers из `/tmp/resolv.conf.d/resolv.conf.auto`.
Скрипт проверяет endpoints сверху вниз, выбирает первый отвечающий endpoint и синхронизирует с ним:
Выбор устроен так же, как в `exit-route.sh`: скрипт идет по приоритетному списку сверху вниз и останавливается на первом работающем endpoint. Поэтому в steady state автоматически проверяются только endpoints выше текущего и сам текущий:
- если более приоритетный endpoint снова заработал, dnsmasq переключается на него;
- если текущий endpoint жив, менее приоритетные endpoints не проверяются;
- если текущий endpoint перестал отвечать, сразу выбирается следующий работающий endpoint по приоритету;
- каждый новый цикл снова начинается с самого приоритетного endpoint, поэтому failback происходит автоматически.
`check-doh.sh` занимается только проверкой DNS и выбором endpoint. Жизненным циклом `https-dns-proxy` занимается procd; отдельный recovery `stop`/`start` перед DNS downgrade больше не нужен.
Выбранный endpoint синхронизируется с:
```text
dhcp.@dnsmasq[0].server
@ -1042,8 +1051,6 @@ dhcp.@dnsmasq[0].server
Если ни один endpoint не отвечает, скрипт ставит последний endpoint из списка как fail-open резерв. Обычно это DNS провайдера.
При переключении общего DNS endpoint на DNS провайдера скрипт после перезапуска dnsmasq также пытается перезапустить `https-dns-proxy`. Это best-effort попытка заново выполнить bootstrap DoH через уже работающий провайдерский DNS. Ошибка перезапуска не мешает DNS failover, а следующая проверка автоматически вернет dnsmasq на DoH, если тот снова начнет отвечать.
Дополнительно поддерживается split DNS по доменным зонам.
При каждом применении нового endpoint скрипт заново собирает `dhcp.@dnsmasq[0].server`:
@ -1342,6 +1349,7 @@ babeld
curl
iperf3
jq-full
libcares
luci
luci-app-https-dns-proxy
luci-app-watchcat
@ -1513,7 +1521,7 @@ Profile name является безопасным ASCII identifier.
- OpenVPN defaults
- DoH/DNS failover defaults
- direct-list sources
- OpenWrt/AWG package URLs
- OpenWrt/AWG/c-ares package URLs
- имена managed файлов и директорий
Именно там меняются правила, которые должны быть одинаковыми для всех конфигов.
@ -1527,7 +1535,7 @@ Profile name является безопасным ASCII identifier.
```sh
./generate_configs.py
./generate_configs.py --config prod.json
./generate_configs.py --skip-awg-download --skip-package-sync
./generate_configs.py --skip-awg-download --skip-cares-download --skip-package-sync
./generate_configs.py --skip-hooks
./generate_configs.py --force
./generate_configs.py --details
@ -1538,6 +1546,7 @@ Profile name является безопасным ASCII identifier.
1. читает и валидирует `config.json`
1. создает `routers/` из `routers/example`
1. скачивает AWG2 `.apk`, если не указан `--skip-awg-download`
1. скачивает `libcares` из `c-ares-openwrt-package`, если не указан `--skip-cares-download`
1. синхронизирует per-router `packages/`, если не указан `--skip-package-sync`
1. синхронизирует шаблонные файлы из `routers/example`
1. запускает `tools/generate.py`
@ -1923,10 +1932,10 @@ Python syntax:
python3 -m py_compile *.py tools/*.py
```
Быстрая проверка template/config flow без загрузки AWG packages:
Быстрая проверка template/config flow без загрузки custom packages:
```sh
./generate_configs.py --skip-awg-download --skip-package-sync --skip-direct-downloads
./generate_configs.py --skip-awg-download --skip-cares-download --skip-package-sync --skip-direct-downloads
```
Валидация generated config:

View file

@ -13,6 +13,8 @@ from tools.default import (
CONFIG_PATH,
AWG_PACKAGE_NAMES,
AWG_RELEASE_BASE_URL,
CARES_PACKAGE_NAMES,
CARES_RELEASE_BASE_URL,
MIN_OPENWRT_VERSION_TEXT,
PACKAGE_EXTENSION,
PACKAGE_SOURCE_ROOT,
@ -61,9 +63,12 @@ def package_source_dir_from_profile(version: str, profile: DeviceProfile) -> Pat
return PACKAGE_SOURCE_ROOT / version / profile.board / profile.arch
def ensure_named_awg_packages_for_profile(
def ensure_named_release_packages_for_profile(
version: str,
profile: DeviceProfile,
package_names: list[str],
release_base_url: str,
label: str,
) -> None:
board, arch, target, subtarget = profile_board_arch(profile)
@ -72,9 +77,9 @@ def ensure_named_awg_packages_for_profile(
remove_package_indexes(dst_dir)
postfix = f"_v{version}_{arch}_{target}_{subtarget}"
base_url = f"{AWG_RELEASE_BASE_URL}/v{version}"
base_url = f"{release_base_url}/v{version}"
for package_name in AWG_PACKAGE_NAMES:
for package_name in package_names:
simple_dst = dst_dir / f"{package_name}.{PACKAGE_EXTENSION}"
if simple_dst.is_file() and simple_dst.stat().st_size > 0:
@ -86,7 +91,7 @@ def ensure_named_awg_packages_for_profile(
if not try_download_file(url, tmp_dst):
die(
"failed to download AWG2 apk package "
f"failed to download {label} apk package "
f"{package_name} for {version} {board} {arch}. Tried: {url}"
)
@ -94,10 +99,13 @@ def ensure_named_awg_packages_for_profile(
print(f"Downloaded: {simple_dst}")
def ensure_awg_packages(
def ensure_release_packages(
cfg: ConfigData,
routers: list[RouterDef],
version: str,
package_names: list[str],
release_base_url: str,
label: str,
) -> None:
seen: set[tuple[str, str]] = set()
@ -110,7 +118,13 @@ def ensure_awg_packages(
continue
seen.add(key)
ensure_named_awg_packages_for_profile(version, profile)
ensure_named_release_packages_for_profile(
version,
profile,
package_names,
release_base_url,
label,
)
def copy_apk_repo_with_canonical_names(
@ -192,6 +206,11 @@ def main() -> None:
action="store_true",
help="do not download AWG packages from GitHub releases",
)
ap.add_argument(
"--skip-cares-download",
action="store_true",
help="do not download libcares from the c-ares GitHub release",
)
ap.add_argument(
"--skip-package-sync",
action="store_true",
@ -243,7 +262,24 @@ def main() -> None:
ensure_router_from_example(EXAMPLE_ROUTER_DIR, router)
if not args.skip_awg_download:
ensure_awg_packages(cfg_data, routers, cfg_data.openwrt_version)
ensure_release_packages(
cfg_data,
routers,
cfg_data.openwrt_version,
AWG_PACKAGE_NAMES,
AWG_RELEASE_BASE_URL,
"AWG2",
)
if not args.skip_cares_download:
ensure_release_packages(
cfg_data,
routers,
cfg_data.openwrt_version,
CARES_PACKAGE_NAMES,
CARES_RELEASE_BASE_URL,
"c-ares",
)
if not args.skip_package_sync:
for router in routers:

View file

@ -91,7 +91,7 @@ nslookup_ok() {
nslookup "$DOMAIN" "$_ep" >/dev/null 2>&1
}
first_working_endpoint() {
best_available_endpoint() {
for _ep in "$@"; do
if nslookup_ok "$_ep"; then
echo "$_ep"
@ -158,6 +158,31 @@ dnsmasq_domain_server_value() {
echo "/${_domain_server_domain}/${_domain_server_upstream}"
}
dnsmasq_server_value_configured() {
_expected="$1"
for _configured in $(uci -q get dhcp.@dnsmasq[0].server 2>/dev/null); do
[ "$_configured" = "$_expected" ] && return 0
done
return 1
}
dnsmasq_provider_domains_configured() {
_provider_eps_resolv="$(get_nameservers_from_resolv)"
for _provider_domain in $PROVIDER_DOMAINS; do
for _provider_ep_resolv in $_provider_eps_resolv; do
_provider_val="$(
dnsmasq_domain_server_value "$_provider_domain" "$_provider_ep_resolv"
)"
dnsmasq_server_value_configured "$_provider_val" || return 1
done
done
return 0
}
add_dnsmasq_provider_domain_servers() {
_provider_eps_resolv="$(get_nameservers_from_resolv)"
@ -199,18 +224,26 @@ apply_once() {
cur="$(current_endpoint || true)"
# shellcheck disable=SC2086
target="$(first_working_endpoint $endpoints || true)"
target="$(best_available_endpoint $endpoints || true)"
if [ -z "$target" ]; then
# shellcheck disable=SC2086
target="$(last_endpoint $endpoints || true)"
echo "no working DNS endpoint; use fallback=${target:-none} endpoints=[$endpoints]"
echo "no working DNS endpoint; fallback=${target:-none} endpoints=[$endpoints]"
fi
[ -n "$target" ] || return 0
[ "$cur" = "$target" ] && return 0
echo "switch DNS endpoint: current=${cur:-none} target=$target"
if [ "$cur" = "$target" ]; then
if dnsmasq_provider_domains_configured; then
return 0
fi
echo "repair dnsmasq provider-domain forwards"
else
echo "switch DNS endpoint: current=${cur:-none} target=$target"
fi
set_dnsmasq_server_endpoint "$target" || return 1
}

View file

@ -25,16 +25,6 @@ update_release_from_deploy_version() {
rm -f /etc/deploy_version
}
patch_https_dns_proxy_respawn() {
init='/etc/init.d/https-dns-proxy'
[ -f "$init" ] || return 0
sed -i \
's/^\([[:space:]]*procd_set_param[[:space:]][[:space:]]*respawn\).*$/\1 3600 5 0/' \
"$init"
}
setup_doh() {
echo 'doh:x:4453:' >>/etc/group
echo 'doh:x:4453:4453:doh:/var/run/doh:/bin/false' >>/etc/passwd
@ -60,9 +50,6 @@ append_if_exists /etc/config/firewall_part /etc/config/firewall
# Log size
uci -q set system.@system[0].log_size='10240'
# Make https-dns-proxy retry forever after crashes
patch_https_dns_proxy_respawn
# Set DOH
setup_doh

View file

@ -316,7 +316,7 @@ WIFI_ENCRYPTION = "psk2"
WIFI_CELL_DENSITY = "0"
# ============================================================
# OPENWRT / AWG PACKAGE BUILD DEFAULTS
# OPENWRT / CUSTOM PACKAGE BUILD DEFAULTS
# ============================================================
MIN_OPENWRT_MAJOR = 25
@ -325,7 +325,12 @@ MIN_OPENWRT_VERSION = (MIN_OPENWRT_MAJOR, MIN_OPENWRT_MINOR)
MIN_OPENWRT_VERSION_TEXT = f"{MIN_OPENWRT_MAJOR}.{MIN_OPENWRT_MINOR}"
OPENWRT_RELEASE_BASE_URL = "https://downloads.openwrt.org/releases"
AWG_RELEASE_BASE_URL = "https://github.com/karen07/amneziawg-openwrt/releases/download"
AWG_RELEASE_BASE_URL = (
"https://github.com/karen07/amneziawg-openwrt-package/releases/download"
)
CARES_RELEASE_BASE_URL = (
"https://github.com/karen07/c-ares-openwrt-package/releases/download"
)
AWG_PACKAGE_NAMES = [
"kmod-amneziawg",
@ -333,6 +338,10 @@ AWG_PACKAGE_NAMES = [
"luci-proto-amneziawg",
]
CARES_PACKAGE_NAMES = [
"libcares",
]
# OpenWrt image packages required by generated router configs and
# router example files. User-facing config.packages is for extra packages.
ROUTER_REQUIRED_PACKAGES = [
@ -340,6 +349,7 @@ ROUTER_REQUIRED_PACKAGES = [
"curl",
"iperf3",
"jq-full",
"libcares",
"luci-app-https-dns-proxy",
"luci-app-watchcat",
"luci-proto-amneziawg",