#!/bin/bash # Category : Networking # Description : AdGuard - DNS based Ad Blocking (c/u/s/r/i): installAdguard() { local config_variables="$1" if [[ "$adguard" == *[cCtTuUsSrRiI]* ]]; then dockerConfigSetupToContainer silent adguard; local app_name=$CFG_ADGUARD_APP_NAME initializeAppVariables $app_name; fi if [[ "$adguard" == *[cC]* ]]; then editAppConfig $app_name; fi if [[ "$adguard" == *[uU]* ]]; then dockerUninstallApp $app_name; fi if [[ "$adguard" == *[sS]* ]]; then dockerComposeDown $app_name; fi if [[ "$adguard" == *[rR]* ]]; then dockerComposeRestart $app_name; fi if [[ "$adguard" == *[iI]* ]]; then isHeader "Install $app_name" ((menu_number++)) echo "" echo "---- $menu_number. Setting up install folder and config file for $app_name." echo "" dockerConfigSetupToContainer "loud" "$app_name" "install" "$config_variables"; isSuccessful "Install folders and Config files have been setup for $app_name." ((menu_number++)) echo "" echo "---- $menu_number. Setting up the $app_name docker-compose.yml file." echo "" dockerComposeSetupFile $app_name; monitoringToggleAppConfig "$app_name" "docker-compose.yml"; ((menu_number++)) echo "" echo "---- $menu_number. Updating file permissions before starting." echo "" fixPermissionsBeforeStart $app_name; ((menu_number++)) echo "" echo "---- $menu_number. Running the docker-compose.yml to install and start $app_name" echo "" dockerComposeUpdateAndStartApp $app_name install; ((menu_number++)) echo "" echo "---- $menu_number. Completing AdGuardHome initial setup automatically" echo "" # The legacy `$usedport1` variable is no longer populated by the # current install pipeline; the resolved host port is stored in the # PORTS_TAG_1 docker-compose tag (format `external:internal`). Pull # it from there so the curl + URL printout actually point somewhere. local adguard_compose_file="$containers_dir$app_name/docker-compose.yml" local adguard_port_pair adguard_port_pair=$(tagsManagerGetTagContent "$adguard_compose_file" "PORTS_TAG_1") local adguard_admin_port="${adguard_port_pair%%:*}" if [[ -n "$public_ip_v4" && -n "$adguard_admin_port" ]]; then echo " External : http://$public_ip_v4:$adguard_admin_port/" fi if [[ -n "$host_setup" ]]; then echo " Hostname : http://$host_setup/" fi echo "" # AdGuardHome ships a setup wizard that normally needs five clicks in a # browser before the daemon writes its config file. Same wizard is # exposed as an HTTP API (POST /control/install/configure), so we # drive it from here and skip the manual interaction. We pre-poll the # admin endpoint until the container is up, then send the form, then # let the existing post-install sed edits run against the freshly # written AdGuardHome.yaml. local adguard_setup_url="http://127.0.0.1:${adguard_admin_port}" local adguard_attempts=0 local adguard_max_attempts=60 while ((adguard_attempts < adguard_max_attempts)); do if curl -fsS -o /dev/null --max-time 2 "${adguard_setup_url}/control/status" 2>/dev/null \ || curl -fsS -o /dev/null --max-time 2 "${adguard_setup_url}/control/install/get_addresses" 2>/dev/null; then break fi sleep 2 ((adguard_attempts++)) done if ((adguard_attempts >= adguard_max_attempts)); then isError "AdGuardHome admin endpoint did not respond on $adguard_setup_url within $((adguard_max_attempts * 2))s — open the URL and complete setup manually, then re-run the installer to apply the post-setup tweaks." else local adguard_user="${CFG_ADGUARD_USER:-admin}" local adguard_pass="${CFG_ADGUARD_PASSWORD:-}" if [[ -z "$adguard_pass" ]]; then adguard_pass=$(generateRandomPassword) updateConfigOption "CFG_ADGUARD_PASSWORD" "$adguard_pass" >/dev/null 2>&1 || true isNotice "Generated a random AdGuardHome admin password and saved it to CFG_ADGUARD_PASSWORD." fi # Internal container ports are fixed (3000 admin, 53 DNS); host # mapping is what `usedport1` etc. handle. local adguard_payload adguard_payload=$(cat </dev/null 2>&1; then isSuccessful "AdGuardHome admin setup completed automatically (user: $adguard_user)." else # 422/403 here typically means setup was already done on a # previous install; the post-setup tweaks below are still # safe to run against the existing yaml. isNotice "AdGuardHome /control/install/configure rejected the request — assuming it's already configured. If this is a fresh install, complete setup manually at $adguard_setup_url." fi # Small breather so AdGuardHome finishes flushing AdGuardHome.yaml # to disk before the sed edits below touch it. #sleep 3 fi #result=$(sudo sed -i "s/address: 0.0.0.0:80/address: 0.0.0.0:${usedport2}/g" "$containers_dir$app_name/conf/AdGuardHome.yaml") #checkSuccess "Changing port 80 to $usedport2 for Admin Panel" #result=$(sudo sed -i "s/port: 53/port: ${usedport3}/g" "$containers_dir$app_name/conf/AdGuardHome.yaml") #checkSuccess "Changing port 53 to $usedport3 for DNS Port" #result=$(sudo sed -i "s/port_https: 443/port_https: ${usedport4}/g" "$containers_dir$app_name/conf/AdGuardHome.yaml") #checkSuccess "Changing port 443 to $usedport4 for DNS Port" #result=$(sudo sed -i "s/port_dns_over_tls: 853/port_dns_over_tls: ${usedport5}/g" "$containers_dir$app_name/conf/AdGuardHome.yaml") #checkSuccess "Changing port 853 to $usedport5 for port_dns_over_tls" #result=$(sudo sed -i "s/port_dns_over_quic: 853/port_dns_over_quic: ${usedport5}/g" "$containers_dir$app_name/conf/AdGuardHome.yaml") #checkSuccess "Changing port 853 to $usedport5 for port_dns_over_quic" # NOTE: We deliberately do *not* force `tls.enabled: true` here. # That section configures encrypted DNS (DoT/DoH/DoQ) and AdGuardHome # crash-loops on startup with `[fatal] creating dns server: parsing # tls key pair: tls: failed to find any PEM data in certificate input` # if `enabled: true` is set without a real certificate pair pointed # at by `certificate_path` / `private_key_path`. The admin user can # opt into encrypted DNS from Settings → Encryption once they've # provided a cert. if [[ $public == "true" ]]; then result=$(sudo sed -i "s|allow_unencrypted_doh: false|allow_unencrypted_doh: true|g" "$containers_dir$app_name/conf/AdGuardHome.yaml") checkSuccess "Setting allow_unencrypted_doh to false for Traefik" fi result=$(sudo sed -i "s|anonymize_client_ip: false: false|anonymize_client_ip: true|g" "$containers_dir$app_name/conf/AdGuardHome.yaml") checkSuccess "Setting anonymize_client_ip to true for privacy reasons" # Force the admin web bind back to 0.0.0.0:3000 inside the container. # The docker-compose mapping is `:3000`, so the container # MUST listen on 3000 internally for the host port to reach it. After # the install API call AdGuardHome sometimes ends up bound to 0.0.0.0:80 # (its build-time default) instead of the port we sent — which is # exactly what causes "unable to connect" on the host port. local adguard_yaml="$containers_dir$app_name/conf/AdGuardHome.yaml" if [[ -f "$adguard_yaml" ]]; then # New schema (v0.107+): single `address: 0.0.0.0:NN` line under `http:`. sudo sed -i 's|^\(\s*address:\s*\)0\.0\.0\.0:[0-9]\+|\10.0.0.0:3000|' "$adguard_yaml" # Old schema fallback: separate `bind_host:` / `bind_port:` keys. sudo sed -i 's|^\(\s*bind_host:\s*\).*|\10.0.0.0|' "$adguard_yaml" sudo sed -i 's|^\(\s*bind_port:\s*\)[0-9]\+|\13000|' "$adguard_yaml" checkSuccess "Pinned AdGuardHome admin bind to 0.0.0.0:3000 (matches the compose port mapping)." fi dockerComposeRestart "$app_name"; # Health-check after the restart so the user finds out *here* if # AdGuardHome didn't come back up cleanly, rather than later when # they try to open the URL and just see "unable to connect". # # Drop `-f` and accept any HTTP status code: now that the admin # account is configured, `/control/status` returns 401 to an # unauthenticated request — which is fine, it means the server is # up and answering. We only care whether the connection succeeded # at all, not what the response body says. `-w '%{http_code}'` # gives us a 3-digit code on success and an empty string on a # connection failure / timeout. local adguard_health_attempts=0 while ((adguard_health_attempts < 20)); do local adguard_health_code adguard_health_code=$(curl -sS -o /dev/null --max-time 2 \ -w '%{http_code}' "${adguard_setup_url}/control/status" 2>/dev/null) if [[ "$adguard_health_code" =~ ^[1-5][0-9][0-9]$ ]]; then isSuccessful "AdGuardHome admin UI is reachable on $adguard_setup_url (HTTP $adguard_health_code)" break fi sleep 1 ((adguard_health_attempts++)) done if ((adguard_health_attempts >= 20)); then isError "AdGuardHome admin UI did not respond after restart on $adguard_setup_url. Check the container logs (\`docker logs adguard-service\`) and the conf/AdGuardHome.yaml bind address." fi ((menu_number++)) echo "" echo "---- $menu_number. Running Application specific updates (if required)" echo "" appUpdateSpecifics $app_name; ((menu_number++)) echo "" echo "---- $menu_number. Running Headscale setup (if required)" echo "" setupHeadscale $app_name; ((menu_number++)) echo "" echo "---- $menu_number. Adding $app_name to the Apps Database table." echo "" databaseInstallApp $app_name; ((menu_number++)) echo "" echo "---- $menu_number. Updating the WebUI config file." echo "" webuiContainerSetup $app_name install; ((menu_number++)) echo "" echo "---- $menu_number. Refreshing monitoring integration." echo "" monitoringRefreshAll; ((menu_number++)) echo "" echo "---- $menu_number. You can find $app_name files at $containers_dir$app_name" echo "" echo " You can now navigate to your $app_name service using any of the options below : " echo "" # Same final-summary call shape as wireguard / vaultwarden. Pass the # admin user/password we just configured so the user sees the # credentials exactly once, at the end of the install. menuShowFinalMessages "$app_name" "${CFG_ADGUARD_USER:-admin}" "$CFG_ADGUARD_PASSWORD"; menu_number=0 #sleep 3s cd fi adguard=n }