cmc/cleberg.net

My personal web garden & blog.

clone: git clone https://gitbay.org/cmc/cleberg.net.git

main: content/blog/2023-12-03-unifi-nextdns.org · raw

   1#+date:        [2023-12-03 Sun 00:00:00]
   2#+title:       NextDNS on Unifi Dream Machine
   3#+description: Installing and configuring NextDNS on a Unifi Dream Machine.
   4#+slug:        unifi-nextdns
   5#+filetags:    :linux:self-hosting:
   6
   7* Overview
   8
   9I recently installed NextDNS on my Unifi Dream Machine router using the
  10[[https://github.com/nextdns/nextdns/wiki/UnifiOS][UnifiOS]] wiki page
  11on NextDNS's GitHub repository.
  12
  13As a result of this, I wanted to write down the process in case the wiki
  14or installer ever gets lost.
  15
  16* Wiki
  17
  18The following is copied from the wiki page linked above, with one
  19difference in the =ssh= command.
  20
  21Install instructions for Unifi Dream Machine (UDM) standard and pro
  22routers.
  23
  24** Install
  25
  26Enable SSH:
  27
  28- Go to your unifi admin interface and select your device (not the
  29  controller settings, but the Dream Machine settings)
  30- Click on "Settings" at the bottom of the page
  31- Go to the "Advanced" section on the left pan
  32- Enable SSH
  33- Set a SSH password
  34
  35Connect to your router using =ssh root@xxx.xxx.xxx.xxx= with the
  36password you configured.
  37
  38Run the following command and follow the instructions:
  39
  40#+begin_src sh
  41sh -c 'sh -c "$(curl -sL https://nextdns.io/install)"'
  42#+end_src
  43
  44Note: Queries from the UDM itself won't be routed to NextDNS nor
  45encrypted due to current system limitation. All traffic from other
  46devices on then network will.
  47
  48** Upgrade
  49
  50To upgrade to the last version, simply re-run the installer above. If a
  51new version is available, the upgrade action will added to the list of
  52possible actions.
  53
  54** Uninstall
  55
  56To uninstall, re-run the installer above and select "Remove" in the
  57menu.
  58
  59** Troubleshooting
  60
  61If the installation fail, please the installer in debug mode and contact
  62us at team@nextdns.io with the transcript of the installation:
  63
  64#+begin_src sh
  65sh -c 'DEBUG=1 sh -c "$(curl -sL https://nextdns.io/install)"'
  66#+end_src
  67
  68*** Content Filtering Conflict
  69
  70NextDNS CLI and the UDM Content Filtering or the Ad Blocking features
  71are incompatible. If you want to use NextDNS CLI, please make sure they
  72are disabled.
  73
  74To disable Content Filtering, go to Settings > Network, then for each
  75network, set the Content Filtering feature to None
  76
  77To disable Ad Blocking, go to Settings > Application Firewall. In the
  78General tab, uncheck the Ad Blocking checkbox.
  79
  80*** APT Error
  81
  82If you get an apt error as follow:
  83
  84#+begin_src sh
  85E: Failed to fetch http://security.debian.org/dists/stretch/updates/main/binary-arm64/Packages  404  Not Found [IP: 151.101.70.132 80]
  86#+end_src
  87
  88You may try to following:
  89
  90#+begin_src sh
  91sed -i -e 's/deb.debian.org/archive.debian.org/g' \
  92       -e 's|security.debian.org|archive.debian.org/|g' \
  93       -e '/stretch-updates/d' /etc/apt/sources.list
  94#+end_src
  95
  96** install.sh
  97
  98Here are the contents of the =install.sh= file used above, as of
  992023-12-03:
 100
 101#+begin_src sh
 102#!/bin/sh
 103
 104main() {
 105    OS=$(detect_os)
 106    GOARCH=$(detect_goarch)
 107    GOOS=$(detect_goos)
 108    NEXTDNS_BIN=$(bin_location)
 109    INSTALL_RELEASE=$(get_release)
 110
 111    export NEXTDNS_INSTALLER=1
 112
 113    log_info "OS: $OS"
 114    log_info "GOARCH: $GOARCH"
 115    log_info "GOOS: $GOOS"
 116    log_info "NEXTDNS_BIN: $NEXTDNS_BIN"
 117    log_info "INSTALL_RELEASE: $INSTALL_RELEASE"
 118
 119    if [ -z "$OS" ] || [ -z "$GOARCH" ] || [ -z "$GOOS" ] || [ -z "$NEXTDNS_BIN" ] || [ -z "$INSTALL_RELEASE" ]; then
 120        log_error "Cannot detect running environment."
 121        exit 1
 122    fi
 123
 124    case "$RUN_COMMAND" in
 125    install|upgrade|uninstall|configure) "$RUN_COMMAND"; exit ;;
 126    esac
 127
 128    while true; do
 129        CURRENT_RELEASE=$(get_current_release)
 130        log_debug "Start install loop with CURRENT_RELEASE=$CURRENT_RELEASE"
 131
 132        if [ "$CURRENT_RELEASE" ]; then
 133            if ! is_version_current; then
 134                log_debug "NextDNS is out of date ($CURRENT_RELEASE != $INSTALL_RELEASE)"
 135                menu \
 136                    u "Upgrade NextDNS from $CURRENT_RELEASE to $INSTALL_RELEASE" upgrade \
 137                    c "Configure NextDNS" configure \
 138                    r "Remove NextDNS" uninstall \
 139                    e "Exit" exit
 140            else
 141                log_debug "NextDNS is up to date ($CURRENT_RELEASE)"
 142                menu \
 143                    c "Configure NextDNS" configure \
 144                    r "Remove NextDNS" uninstall \
 145                    e "Exit" exit
 146            fi
 147        else
 148            log_debug "NextDNS is not installed"
 149            menu \
 150                i "Install NextDNS" install \
 151                e "Exit" exit
 152        fi
 153    done
 154}
 155
 156install() {
 157    if [ "$(get_current_release)" ]; then
 158        log_info "Already installed"
 159        return
 160    fi
 161    if type=$(install_type); then
 162        log_info "Installing NextDNS..."
 163        log_debug "Using $type install type"
 164        if "install_$type"; then
 165            if [ ! -x "$NEXTDNS_BIN" ]; then
 166                log_error "Installation failed: binary not installed in $NEXTDNS_BIN"
 167                return 1
 168            fi
 169            configure
 170            post_install
 171            exit 0
 172        fi
 173    else
 174        return $?
 175    fi
 176}
 177
 178upgrade() {
 179    if [ "$(get_current_release)" = "$INSTALL_RELEASE" ]; then
 180        log_info "Already on the latest version"
 181        return
 182    fi
 183    if type=$(install_type); then
 184        log_info "Upgrading NextDNS..."
 185        log_debug "Using $type install type"
 186        "upgrade_$type"
 187    else
 188        return $?
 189    fi
 190}
 191
 192uninstall() {
 193    if type=$(install_type); then
 194        log_info "Uninstalling NextDNS..."
 195        log_debug "Using $type uninstall type"
 196        "uninstall_$type"
 197    else
 198        return $?
 199    fi
 200}
 201
 202precheck() {
 203    if [ -e "/data/unifi" ] && [ -f "/run/dnsfilter/dnsfilter" ]; then
 204        log_warn "UDM Content Filtering and/or Ad Blocking feature is enabled."
 205    log_warn "Please disable it to use NextDNS."
 206        log_warn ""
 207        log_warn "  To disable Content Filtering, go to Settings > Network."
 208        log_warn "  For each network, set the Content Filtering feature to None."
 209    log_warn ""
 210    log_warn "  To disable Ad Blocking, go to Settings > Application Firewall"
 211    log_warn "  In the General tab, uncheck the Ad Blocking checkbox."
 212        log_warn ""
 213        while [ -f "/run/dnsfilter/dnsfilter" ]; do
 214            sleep 1
 215        done
 216        log_info "Content Filtering feature successfuly disabled."
 217    fi
 218}
 219
 220configure() {
 221    log_debug "Start configure"
 222    precheck
 223    args=""
 224    add_arg() {
 225        for value in $2; do
 226            log_debug "Add arg -$1=$value"
 227            args="$args -$1=$value"
 228        done
 229    }
 230    add_arg_bool_ask() {
 231        arg=$1
 232        msg=$2
 233        default=$3
 234        if [ -z "$default" ]; then
 235            default=$(get_config_bool "$arg")
 236        fi
 237        # shellcheck disable=SC2046
 238        add_arg "$arg" $(ask_bool "$msg" "$default")
 239    }
 240    # Use profile from now on
 241    add_arg profile "$(get_profile_id)"
 242
 243    doc "Sending your devices name lets you filter analytics and logs by device."
 244    add_arg_bool_ask report-client-info 'Report device name?' true
 245
 246    case $(guess_host_type) in
 247    router)
 248        add_arg setup-router true
 249        ;;
 250    unsure)
 251        doc "Accept DNS request from other network hosts."
 252        if [ "$(get_config_bool setup-router)" = "true" ]; then
 253            router_default=true
 254        fi
 255        if [ "$(ask_bool 'Setup as a router?' $router_default)" = "true" ]; then
 256            add_arg setup-router true
 257        fi
 258        ;;
 259    esac
 260
 261    doc "Make NextDNS CLI cache responses. This improves latency and reduces the amount"
 262    doc "of queries sent to NextDNS."
 263    if [ "$(guess_host_type)" = "router" ]; then
 264        doc "Note that enabling this feature will disable dnsmasq for DNS to avoid double"
 265        doc "caching."
 266    fi
 267    if [ "$(get_config cache-size)" != "0" ]; then
 268        cache_default=true
 269    fi
 270    if [ "$(ask_bool 'Enable caching?' $cache_default)" = "true" ]; then
 271        add_arg cache-size "10MB"
 272
 273        doc "Instant refresh will force low TTL on responses sent to clients so they rely"
 274        doc "on CLI DNS cache. This will allow changes on your NextDNS config to be applied"
 275        doc "on your LAN hosts without having to wait for their cache to expire."
 276        if [ "$(get_config max-ttl)" = "5s" ]; then
 277            instant_refresh_default=true
 278        fi
 279        if [ "$(ask_bool 'Enable instant refresh?' $instant_refresh_default)" = "true" ]; then
 280            add_arg max-ttl "5s"
 281        fi
 282    fi
 283
 284    if [ "$(guess_host_type)" != "router" ]; then
 285        doc "Changes DNS settings of the host automatically when NextDNS is started."
 286        doc "If you say no here, you will have to manually configure DNS to 127.0.0.1."
 287        add_arg_bool_ask auto-activate 'Automatically setup local host DNS?' true
 288    fi
 289    # shellcheck disable=SC2086
 290    asroot "$NEXTDNS_BIN" install $args
 291}
 292
 293post_install() {
 294    println
 295    println "Congratulations! NextDNS is now installed."
 296    println
 297    println "To upgrade/uninstall, run this command again and select the appropriate option."
 298    println
 299    println "You can use the NextDNS command to control the daemon."
 300    println "Here are a few important commands to know:"
 301    println
 302    println "# Start, stop, restart the daemon:"
 303    println "nextdns start"
 304    println "nextdns stop"
 305    println "nextdns restart"
 306    println
 307    println "# Configure the local host to point to NextDNS or not:"
 308    println "nextdns activate"
 309    println "nextdns deactivate"
 310    println
 311    println "# Explore daemon logs:"
 312    println "nextdns log"
 313    println
 314    println "# For more commands, use:"
 315    println "nextdns help"
 316    println
 317}
 318
 319install_bin() {
 320    bin_path=$NEXTDNS_BIN
 321    if [ "$1" ]; then
 322        bin_path=$1
 323    fi
 324    log_debug "Installing $INSTALL_RELEASE binary for $GOOS/$GOARCH to $bin_path"
 325    case "$INSTALL_RELEASE" in
 326    ,*/*)
 327        # Snapshot
 328        branch=${INSTALL_RELEASE%/*}
 329        hash=${INSTALL_RELEASE#*/}
 330        url="https://snapshot.nextdns.io/${branch}/nextdns-${hash}_${GOOS}_${GOARCH}.tar.gz"
 331        ;;
 332    ,*)
 333        url="https://github.com/nextdns/nextdns/releases/download/v${INSTALL_RELEASE}/nextdns_${INSTALL_RELEASE}_${GOOS}_${GOARCH}.tar.gz"
 334        ;;
 335    esac
 336    log_debug "Downloading $url"
 337    asroot mkdir -p "$(dirname "$bin_path")" &&
 338        curl -sL "$url" | asroot sh -c "tar Ozxf - nextdns > "$bin_path"" &&
 339        asroot chmod 755 "$bin_path"
 340}
 341
 342upgrade_bin() {
 343    tmp=$NEXTDNS_BIN.tmp
 344    if install_bin "$tmp"; then
 345        asroot "$NEXTDNS_BIN" uninstall
 346        asroot mv "$tmp" "$NEXTDNS_BIN"
 347        asroot "$NEXTDNS_BIN" install
 348    fi
 349    log_debug "Removing spurious temporary install file"
 350    asroot rm -rf "$tmp"
 351}
 352
 353uninstall_bin() {
 354    asroot "$NEXTDNS_BIN" uninstall
 355    asroot rm -f "$NEXTDNS_BIN"
 356}
 357
 358install_rpm() {
 359    asroot curl -Ls https://repo.nextdns.io/nextdns.repo -o /etc/yum.repos.d/nextdns.repo &&
 360        asroot yum install -y nextdns
 361}
 362
 363upgrade_rpm() {
 364    asroot yum update -y nextdns
 365}
 366
 367uninstall_rpm() {
 368    asroot yum remove -y nextdns
 369}
 370
 371install_zypper() {
 372    if asroot zypper repos | grep -q nextdns >/dev/null; then
 373        echo "Repository nextdns already exists. Skipping adding repository..."
 374    else
 375        asroot zypper ar -f -r https://repo.nextdns.io/nextdns.repo nextdns
 376    fi
 377    asroot zypper refresh && asroot zypper in -y nextdns
 378}
 379
 380upgrade_zypper() {
 381    asroot zypper up nextdns
 382}
 383
 384uninstall_zypper() {
 385    asroot zypper remove -y nextdns
 386    case $(ask_bool 'Do you want to remove the repository from the repositories list?' true) in
 387    true)
 388        asroot zypper removerepo nextdns
 389        ;;
 390    esac
 391}
 392
 393install_deb() {
 394    if [ -f /etc/default/ubnt-dpkg-cache ]; then
 395        # On UnifiOS 2, make sure the package is persisted over upgrades
 396        sed -e '/^DPKG_CACHE_UBNT_PKGS+=" nextdns"/{:a;n;ba;q}' \
 397            -e '$aDPKG_CACHE_UBNT_PKGS+=" nextdns"' \
 398            -i /etc/default/ubnt-dpkg-cache
 399    fi
 400
 401    install_deb_keyring &&
 402        asroot sh -c 'echo "deb [signed-by=/etc/apt/keyrings/nextdns.gpg] https://repo.nextdns.io/deb stable main" > /etc/apt/sources.list.d/nextdns.list' &&
 403        (dpkg --compare-versions $(dpkg-query --showformat='${Version}' --show apt) ge 1.1 ||
 404         asroot ln -s /etc/apt/keyrings/nextdns.gpg /etc/apt/trusted.gpg.d/.) &&
 405        (test "$OS" = "debian" && asroot apt-get -y install apt-transport-https || true) &&
 406        asroot apt-get update &&
 407        asroot apt-get install -y nextdns
 408}
 409
 410install_deb_keyring() {
 411    # Fallback on curl, some debian based distrib don't have wget while debian
 412    # doesn't have curl by default.
 413    asroot mkdir -p /etc/apt/keyrings
 414    ( asroot wget -qO /etc/apt/keyrings/nextdns.gpg https://repo.nextdns.io/nextdns.gpg ||
 415      asroot curl -sfL https://repo.nextdns.io/nextdns.gpg -o /etc/apt/keyrings/nextdns.gpg ) &&
 416        asroot chmod 0644 /etc/apt/keyrings/nextdns.gpg
 417}
 418
 419upgrade_deb() {
 420    install_deb_keyring &&
 421        asroot apt-get update &&
 422        asroot apt-get install -y nextdns
 423}
 424
 425uninstall_deb() {
 426    asroot apt-get remove -y nextdns
 427}
 428
 429install_apk() {
 430    repo=https://repo.nextdns.io/apk
 431    asroot wget -O /etc/apk/keys/nextdns.pub https://repo.nextdns.io/nextdns.pub &&
 432        (grep -v $repo /etc/apk/repositories; echo $repo) | asroot tee /etc/apk/repositories >/dev/null &&
 433        asroot apk update &&
 434        asroot apk add nextdns
 435}
 436
 437upgrade_apk() {
 438    asroot apk update && asroot apk upgrade nextdns
 439}
 440
 441uninstall_apk() {
 442    asroot apk del nextdns
 443}
 444
 445install_arch() {
 446    asroot pacman -Sy yay &&
 447        yay -Sy nextdns
 448}
 449
 450upgrade_arch() {
 451    yay -Suy nextdns
 452}
 453
 454uninstall_arch() {
 455    asroot pacman -R nextdns
 456}
 457
 458install_merlin_path() {
 459    # Add next to Merlin's path
 460    mkdir -p /tmp/opt/sbin
 461    ln -sf "$NEXTDNS_BIN" /tmp/opt/sbin/nextdns
 462}
 463
 464install_merlin() {
 465    if install_bin; then
 466        install_merlin_path
 467    fi
 468}
 469
 470uninstall_merlin() {
 471    uninstall_bin
 472    rm -f /tmp/opt/sbin/nextdns
 473}
 474
 475upgrade_merlin() {
 476    if upgrade_bin; then
 477        install_merlin_path
 478    fi
 479}
 480
 481install_openwrt() {
 482    opkg update &&
 483        opkg install nextdns
 484    rt=$?
 485    if [ $rt -eq 0 ]; then
 486        case $(ask_bool 'Install the GUI?' true) in
 487        true)
 488            opkg install luci-app-nextdns
 489            rt=$?
 490            ;;
 491        esac
 492    fi
 493    return $rt
 494}
 495
 496upgrade_openwrt() {
 497    opkg update &&
 498        opkg upgrade nextdns
 499}
 500
 501uninstall_openwrt() {
 502    opkg remove nextdns
 503}
 504
 505install_ddwrt() {
 506    if [ "$(nvram get enable_jffs2)" = "0" ]; then
 507        log_error "JFFS support not enabled"
 508        log_info "To enabled JFFS:"
 509        log_info " 1. On the router web page click on Administration."
 510        log_info " 2. Scroll down until you see JFFS2 Support section."
 511        log_info " 3. Click Enable JFFS."
 512        log_info " 4. Click Save."
 513        log_info " 5. Wait couple seconds, then click Apply."
 514        log_info " 6. Wait again. Go back to the Enable JFFS section, and enable Clean JFFS."
 515        log_info " 7. Do not click Save. Click Apply instead."
 516        log_info " 8. Wait till you get the web-GUI back, then disable Clean JFFS again."
 517        log_info " 9. Click Save."
 518        log_info "10. Relaunch this installer."
 519        exit 1
 520    fi
 521    mkdir -p /jffs/nextdns &&
 522        openssl_get https://curl.haxx.se/ca/cacert.pem | http_body > /jffs/nextdns/ca.pem &&
 523        install_bin
 524}
 525
 526upgrade_ddwrt() {
 527    upgrade_bin
 528}
 529
 530uninstall_ddwrt() {
 531    uninstall_bin
 532    rm -rf /jffs/nextdns
 533}
 534
 535install_brew() {
 536    silent_exec brew install nextdns/tap/nextdns
 537}
 538
 539upgrade_brew() {
 540    silent_exec brew upgrade nextdns/tap/nextdns
 541    asroot "$NEXTDNS_BIN" install
 542}
 543
 544uninstall_brew() {
 545    silent_exec brew uninstall nextdns/tap/nextdns
 546}
 547
 548install_freebsd() {
 549    # TODO: port install
 550    install_bin
 551}
 552
 553upgrade_freebsd() {
 554    # TODO: port upgrade
 555    upgrade_bin
 556}
 557
 558uninstall_freebsd() {
 559    # TODO: port uninstall
 560    uninstall_bin
 561}
 562
 563install_pfsense() {
 564    # TODO: port install + UI
 565    install_bin
 566}
 567
 568upgrade_pfsense() {
 569    # TODO: port upgrade
 570    upgrade_bin
 571}
 572
 573uninstall_pfsense() {
 574    # TODO: port uninstall
 575    uninstall_bin
 576}
 577
 578install_opnsense() {
 579    # TODO: port install + UI
 580    install_bin
 581}
 582
 583upgrade_opnsense() {
 584    # TODO: port upgrade
 585    upgrade_bin
 586}
 587
 588uninstall_opnsense() {
 589    # TODO: port uninstall
 590    uninstall_bin
 591}
 592
 593ubios_install_source() {
 594    echo "deb [signed-by=/etc/apt/keyrings/nextdns.gpg] https://repo.nextdns.io/deb stable main" > /data/nextdns.list
 595    podman exec unifi-os mv /data/nextdns.list /etc/apt/sources.list.d/nextdns.list
 596    rm -f /tmp/nextdns.list
 597    podman exec unifi-os apt-get install -y gnupg1 curl
 598    podman exec unifi-os mkdir -p /etc/apt/keyrings/
 599    podman exec unifi-os curl -sfL https://repo.nextdns.io/nextdns.gpg -o /etc/apt/keyrings/nextdns.gpg
 600    podman exec unifi-os apt-get update -o Dir::Etc::sourcelist="sources.list.d/nextdns.list" -o Dir::Etc::sourceparts="-" -o APT::Get::List-Cleanup="0"
 601}
 602
 603install_ubios() {
 604    ubios_install_source
 605    podman exec unifi-os apt-get install -y nextdns
 606}
 607
 608upgrade_ubios() {
 609    ubios_install_source
 610    podman exec unifi-os apt-get install --only-upgrade -y nextdns
 611}
 612
 613uninstall_ubios() {
 614    podman exec unifi-os apt-get remove -y nextdns
 615}
 616
 617install_ubios_snapshot() {
 618    branch=${INSTALL_RELEASE%/*}
 619    hash=${INSTALL_RELEASE#*/}
 620    url="https://snapshot.nextdns.io/${branch}/nextdns-${hash}_${GOOS}_${GOARCH}.tar.gz"
 621    podman exec unifi-os sh -c "curl -o- $url | tar Ozxf - nextdns > /usr/bin/nextdns; /usr/bin/nextdns install"
 622}
 623
 624upgrade_ubios_snapshot() {
 625    /data/nextdns uninstall
 626    install_ubios_snapshot
 627}
 628
 629install_type() {
 630    if [ "$FORCE_INSTALL_TYPE" ]; then
 631        echo "$FORCE_INSTALL_TYPE"; return 0
 632    fi
 633    case "$INSTALL_RELEASE" in
 634    ,*/*)
 635        case $OS in
 636        ubios)
 637            echo "ubios_snapshot"; return 0
 638            ;;
 639        ,*)
 640            # Snapshot mode always use binary install
 641            echo "bin"; return 0
 642            ;;
 643        esac
 644    esac
 645    case $OS in
 646    centos|fedora|rhel)
 647        echo "rpm"
 648        ;;
 649    opensuse-tumbleweed|opensuse-leap|opensuse)
 650        echo "zypper"
 651        ;;
 652    debian|ubuntu|elementary|raspbian|linuxmint|pop|neon|sparky|vyos|Deepin)
 653        echo "deb"
 654        ;;
 655    alpine)
 656        echo "apk"
 657        ;;
 658    arch|manjaro|steamos)
 659        #echo "arch" # TODO: fix AUR install
 660        echo "bin"
 661        ;;
 662    openwrt)
 663        # shellcheck disable=SC1091
 664        . /etc/os-release
 665        major=$(echo "$VERSION_ID" | cut -d. -f1)
 666        case $major in
 667            ,*[!0-9]*)
 668                if [ "$VERSION_ID" = "19.07.0-rc1" ]; then
 669                    # No opkg support before 19.07.0-rc2
 670                    echo "bin"
 671                else
 672                    # Likely 'snapshot' build in this case, but still > major version 19
 673                    echo "openwrt"
 674                fi
 675                ;;
 676            ,*)
 677                if [ "$major" -lt 19 ]; then
 678                    # No opkg support before 19.07.0-rc2
 679                    echo "bin"
 680                else
 681                    echo "openwrt"
 682                fi
 683                ;;
 684        esac
 685        ;;
 686    asuswrt-merlin)
 687        echo "merlin"
 688        ;;
 689    edgeos|synology|clear-linux-os|solus|openbsd|netbsd|overthebox)
 690        echo "bin"
 691        ;;
 692    ddwrt)
 693        echo "ddwrt"
 694        ;;
 695    darwin)
 696        if [ -x /usr/local/bin/brew ] || [ -x /opt/homebrew/bin/brew ]; then
 697            echo "brew"
 698        else
 699            log_debug "Homebrew not installed, fallback on binary install"
 700            echo "bin"
 701        fi
 702        ;;
 703    freebsd)
 704        echo "freebsd"
 705        ;;
 706    pfsense)
 707        echo "pfsense"
 708        ;;
 709    opnsense)
 710        echo "opnsense"
 711        ;;
 712    ubios)
 713        echo "ubios"
 714        ;;
 715    gentoo)
 716        echo "bin"
 717        ;;
 718    void)
 719        # TODO: pkg for xbps
 720        echo "bin"
 721        ;;
 722    ,*)
 723        log_error "Unsupported installation for $(detect_os)"
 724        return 1
 725        ;;
 726    esac
 727}
 728
 729get_config() {
 730    "$NEXTDNS_BIN" config | grep -E "^$1 " | cut -d' ' -f 2
 731}
 732
 733get_config_bool() {
 734    val=$(get_config "$1")
 735    case $val in
 736        true|false)
 737            echo "$val"
 738            ;;
 739    esac
 740    echo "$2"
 741}
 742
 743get_profile_id() {
 744    log_debug "Get profile ID"
 745    if [ "$CONFIG_ID" ]; then
 746        # backward compat
 747        PROFILE_ID="$CONFIG_ID"
 748    fi
 749    while [ -z "$PROFILE_ID" ]; do
 750        default=
 751        prev_id=$(get_config profile)
 752        if [ -z "$prev_id" ]; then
 753            # backward compat
 754            prev_id=$(get_config config)
 755        fi
 756        if [ "$prev_id" ]; then
 757            log_debug "Previous profile ID: $prev_id"
 758            default=" (default=$prev_id)"
 759        fi
 760        print "NextDNS Profile ID%s: " "$default"
 761        read -r id
 762        if [ -z "$id" ]; then
 763            id=$prev_id
 764        fi
 765        if echo "$id" | grep -qE '^[0-9a-f]{6}$'; then
 766            PROFILE_ID=$id
 767            break
 768        else
 769            log_error "Invalid profile ID."
 770            println
 771            println "ID format is 6 alphanumerical lowercase characters (example: 123abc)."
 772            println "Your ID can be found on the Setup tab of https://my.nextdns.io."
 773            println
 774        fi
 775    done
 776    echo "$PROFILE_ID"
 777}
 778
 779log_debug() {
 780    if [ "$DEBUG" = "1" ]; then
 781        printf "\033[30;1mDEBUG: %s\033[0m\n" "$*" >&2
 782    fi
 783}
 784
 785log_info() {
 786    printf "INFO: %s\n" "$*" >&2
 787}
 788
 789log_warn() {
 790    printf "\033[33mWARN: %s\033[0m\n" "$*" >&2
 791}
 792
 793log_error() {
 794    printf "\033[31mERROR: %s\033[0m\n" "$*" >&2
 795}
 796
 797print() {
 798    format=$1
 799    if [ $# -gt 0 ]; then
 800        shift
 801    fi
 802    # shellcheck disable=SC2059
 803    printf "$format" "$@" >&2
 804}
 805
 806println() {
 807    format=$1
 808    if [ $# -gt 0 ]; then
 809        shift
 810    fi
 811    # shellcheck disable=SC2059
 812    printf "$format\n" "$@" >&2
 813}
 814
 815doc() {
 816    # shellcheck disable=SC2059
 817    printf "\033[30;1m%s\033[0m\n" "$*" >&2
 818}
 819
 820menu() {
 821    while true; do
 822        n=0
 823        default=
 824        for item in "$@"; do
 825            case $((n%3)) in
 826            0)
 827                key=$item
 828                if [ -z "$default" ]; then
 829                    default=$key
 830                fi
 831                ;;
 832            1)
 833                echo "$key) $item"
 834                ;;
 835            esac
 836            n=$((n+1))
 837        done
 838        print "Choice (default=%s): " "$default"
 839        read -r choice
 840        if [ -z "$choice" ]; then
 841            choice=$default
 842        fi
 843        n=0
 844        for item in "$@"; do
 845            case $((n%3)) in
 846            0)
 847                key=$item
 848                ;;
 849            2)
 850                if [ "$key" = "$choice" ]; then
 851                    if ! "$item"; then
 852                        log_error "$item: exit $?"
 853                    fi
 854                    break 2
 855                fi
 856                ;;
 857            esac
 858            n=$((n+1))
 859        done
 860        echo "Invalid choice"
 861    done
 862}
 863
 864ask_bool() {
 865    msg=$1
 866    default=$2
 867    case $default in
 868    true)
 869        msg="$msg [Y|n]: "
 870        ;;
 871    false)
 872        msg="$msg [y|N]: "
 873        ;;
 874    ,*)
 875        msg="$msg (y/n): "
 876    esac
 877    while true; do
 878        print "%s" "$msg"
 879        read -r answer
 880        if [ -z "$answer" ]; then
 881            answer=$default
 882        fi
 883        case $answer in
 884        y|Y|yes|YES|true)
 885            echo "true"
 886            return 0
 887            ;;
 888        n|N|no|NO|false)
 889            echo "false"
 890            return 0
 891            ;;
 892        ,*)
 893            echo "Invalid input, use yes or no"
 894            ;;
 895        esac
 896    done
 897}
 898
 899detect_endiannes() {
 900    if ! hexdump /dev/null 2>/dev/null; then
 901        # Some firmwares do not contain hexdump, for those, try to detect endianness
 902        # differently.
 903        case $(cat /proc/cpuinfo) in
 904        ,*BCM5300*)
 905            # RT-AC66U does not support Merlin version over 380.70 which
 906            # lacks hexdump command.
 907            echo "le"
 908            ;;
 909        ,*)
 910            log_error "Cannot determine endianness"
 911            return 1
 912            ;;
 913        esac
 914        return 0
 915    fi
 916    case $(hexdump -s 5 -n 1 -e '"%x"' /bin/sh | head -c1) in
 917    1)
 918        echo "le"
 919        ;;
 920    2)
 921        echo ""
 922        ;;
 923    esac
 924}
 925
 926detect_goarch() {
 927    if [ "$FORCE_GOARCH" ]; then
 928        echo "$FORCE_GOARCH"; return 0
 929    fi
 930    case $(uname -m) in
 931    x86_64|amd64)
 932        echo "amd64"
 933        ;;
 934    i386|i686)
 935        echo "386"
 936        ;;
 937    arm)
 938        # FreeBSD does not include arm version
 939        case "$(sysctl -b hw.model 2>/dev/null)" in
 940        ,*A9*)
 941            echo "armv7"
 942            ;;
 943        ,*)
 944            # Unknown version, fallback to the lowest
 945            echo "armv5"
 946            ;;
 947        esac
 948        ;;
 949    armv5*)
 950        echo "armv5"
 951        ;;
 952    armv6*|armv7*)
 953        if grep -q vfp /proc/cpuinfo 2>/dev/null; then
 954            echo "armv$(uname -m | sed -e 's/[[:alpha:]]//g')"
 955        else
 956            # Soft floating point
 957            echo "armv5"
 958        fi
 959        ;;
 960    aarch64)
 961        case "$(uname -o 2>/dev/null)" in
 962        ASUSWRT-Merlin*)
 963            # XXX when using arm64 build on ASUS AC66U and ACG86U, we get Go error:
 964            # "out of memory allocating heap arena metadata".
 965            echo "armv7"
 966            ;;
 967        ,*)
 968            echo "arm64"
 969            ;;
 970        esac
 971        ;;
 972    armv8*|arm64)
 973        echo "arm64"
 974        ;;
 975    mips*)
 976        # TODO: detect hardfloat
 977        echo "$(uname -m)$(detect_endiannes)_softfloat"
 978        ;;
 979    ,*)
 980        log_error "Unsupported GOARCH: $(uname -m)"
 981        return 1
 982        ;;
 983    esac
 984}
 985
 986detect_goos() {
 987    if [ "$FORCE_GOOS" ]; then
 988        echo "$FORCE_GOOS"; return 0
 989    fi
 990    case $(uname -s) in
 991    Linux)
 992        echo "linux"
 993        ;;
 994    Darwin)
 995        echo "darwin"
 996        ;;
 997    FreeBSD)
 998        echo "freebsd"
 999        ;;
1000    NetBSD)
1001        echo "netbsd"
1002        ;;
1003    OpenBSD)
1004        echo "openbsd"
1005        ;;
1006    ,*)
1007        log_error "Unsupported GOOS: $(uname -s)"
1008        return 1
1009    esac
1010}
1011
1012detect_os() {
1013    if [ "$FORCE_OS" ]; then
1014        echo "$FORCE_OS"; return 0
1015    fi
1016    case $(uname -s) in
1017    Linux)
1018        case $(uname -o) in
1019        GNU/Linux|Linux)
1020            if grep -q -e '^EdgeRouter' -e '^UniFiSecurityGateway' /etc/version 2> /dev/null; then
1021                echo "edgeos"; return 0
1022            fi
1023            if uname -u 2>/dev/null | grep -q '^synology'; then
1024                echo "synology"; return 0
1025            fi
1026            # shellcheck disable=SC1091
1027            dist=$(. /etc/os-release; echo "$ID")
1028            case $dist in
1029            ubios)
1030                if [ -z "$(command -v podman)" ]; then
1031                    log_error "This version of UnifiOS is not supported. Make sure you run version 1.7.0 or above."
1032                    return 1
1033                fi
1034                echo "$dist"; return 0
1035                ;;
1036            debian|ubuntu|elementary|raspbian|centos|fedora|rhel|arch|manjaro|openwrt|clear-linux-os|linuxmint|opensuse-tumbleweed|opensuse-leap|opensuse|solus|pop|neon|overthebox|sparky|vyos|void|alpine|Deepin|gentoo|steamos)
1037                echo "$dist"; return 0
1038                ;;
1039            esac
1040            # shellcheck disable=SC1091
1041            for dist in $(. /etc/os-release; echo "$ID_LIKE"); do
1042                case $dist in
1043                debian|ubuntu|rhel|fedora|openwrt)
1044                    log_debug "Using ID_LIKE"
1045                    echo "$dist"; return 0
1046                    ;;
1047                esac
1048            done
1049            ;;
1050        ASUSWRT-Merlin*)
1051            echo "asuswrt-merlin"; return 0
1052            ;;
1053        DD-WRT)
1054            echo "ddwrt"; return 0
1055        esac
1056        ;;
1057    Darwin)
1058        echo "darwin"; return 0
1059        ;;
1060    FreeBSD)
1061        if [ -f /etc/platform ]; then
1062            case $(cat /etc/platform) in
1063            pfSense)
1064                echo "pfsense"; return 0
1065                ;;
1066            esac
1067        fi
1068        if [ -x /usr/local/sbin/opnsense-version ]; then
1069            case $(/usr/local/sbin/opnsense-version -N) in
1070            OPNsense)
1071                echo "opnsense"; return 0
1072                ;;
1073            esac
1074        fi
1075        echo "freebsd"; return 0
1076        ;;
1077    NetBSD)
1078        echo "netbsd"; return 0
1079        ;;
1080    OpenBSD)
1081        echo "openbsd"; return 0
1082        ;;
1083    ,*)
1084    esac
1085    log_error "Unsupported OS: $(uname -o) $(grep ID "/etc/os-release" 2>/dev/null | xargs)"
1086    return 1
1087}
1088
1089guess_host_type() {
1090    if [ -d /data/unifi ]; then
1091        # Special case when installer is run from inside the ubios podman
1092        echo "router"; return 0
1093    fi
1094
1095    case $OS in
1096    pfsense|opnsense|openwrt|asuswrt-merlin|edgeos|ddwrt|synology|overthebox|ubios)
1097        echo "router"
1098        ;;
1099    darwin|steamos)
1100        echo "workstation"
1101        ;;
1102    ,*)
1103        echo "unsure"
1104        ;;
1105    esac
1106}
1107
1108asroot() {
1109    # Some platform (Merlin) do not have the "id" command and $USER report a non root username with uid 0.
1110    if [ "$(grep '^Uid:' /proc/$$/status 2>/dev/null|cut -f2)" = "0" ] || [ "$USER" = "root" ] || [ "$(id -u 2>/dev/null)" = "0" ]; then
1111        "$@"
1112    elif [ "$(command -v sudo 2>/dev/null)" ]; then
1113        sudo "$@"
1114    else
1115        echo "Root required"
1116        su -m root -c "$*"
1117    fi
1118}
1119
1120silent_exec() {
1121    if [ "$DEBUG" = 1 ]; then
1122        "$@"
1123    else
1124        if ! out=$("$@" 2>&1); then
1125            rt=$?
1126            println "\033[30;1m%s\033[0m" "$out"
1127            return $rt
1128        fi
1129    fi
1130}
1131
1132bin_location() {
1133    case $OS in
1134    centos|fedora|rhel|debian|ubuntu|elementary|raspbian|arch|manjaro|clear-linux-os|linuxmint|opensuse-tumbleweed|opensuse-leap|opensuse|solus|pop|neon|sparky|vyos|void|alpine|Deepin|gentoo)
1135        echo "/usr/bin/nextdns"
1136        ;;
1137    openwrt|overthebox)
1138        echo "/usr/sbin/nextdns"
1139        ;;
1140    synology)
1141        echo "/usr/local/bin/nextdns"
1142    ;;
1143    darwin)
1144    echo "$(brew --prefix 2>/dev/null || echo /usr/local)/bin/nextdns"
1145        ;;
1146    asuswrt-merlin|ddwrt)
1147        echo "/jffs/nextdns/nextdns"
1148        ;;
1149    freebsd|pfsense|opnsense|netbsd|openbsd)
1150        echo "/usr/local/sbin/nextdns"
1151        ;;
1152    edgeos)
1153        echo "/config/nextdns/nextdns"
1154        ;;
1155    ubios)
1156        echo "/data/nextdns"
1157        ;;
1158    steamos)
1159        echo "$HOME/.local/bin/nextdns"
1160        ;;
1161    ,*)
1162        log_error "Unknown bin location for $OS"
1163        ;;
1164    esac
1165}
1166
1167is_version_current() {
1168    case "$INSTALL_RELEASE" in
1169    ,*/*)
1170        # Snapshot
1171        hash=${INSTALL_RELEASE#*/}
1172        test "0.0.0-$hash" = "$CURRENT_RELEASE"
1173        ;;
1174    ,*)
1175        test "$INSTALL_RELEASE" = "$CURRENT_RELEASE"
1176        ;;
1177    esac
1178}
1179
1180get_current_release() {
1181    if [ -x "$NEXTDNS_BIN" ]; then
1182        $NEXTDNS_BIN version|cut -d' ' -f 3
1183    fi
1184}
1185
1186get_release() {
1187    if [ "$NEXTDNS_VERSION" ]; then
1188        echo "$NEXTDNS_VERSION"
1189    else
1190        for cmd in curl wget openssl true; do
1191            # command is the "right" way but may be compiled out of busybox shell
1192            ! command -v $cmd > /dev/null 2>&1 || break
1193            ! which $cmd > /dev/null 2>&1 || break
1194        done
1195        case "$cmd" in
1196        curl) cmd="curl -A curl -s" ;;
1197        wget) cmd="wget -qO- -U curl" ;;
1198        openssl) cmd="openssl_get" ;;
1199        ,*)
1200            log_error "Cannot retrieve latest version"
1201            return
1202            ;;
1203        esac
1204        v=$($cmd "https://api.github.com/repos/nextdns/nextdns/releases/latest" | \
1205            grep '"tag_name":' | esed 's/.*"([^"]+)".*/\1/' | sed -e 's/^v//')
1206        if [ -z "$v" ]; then
1207            log_error "Cannot get latest version: $out"
1208        fi
1209        echo "$v"
1210    fi
1211}
1212
1213esed() {
1214    if (echo | sed -E '' >/dev/null 2>&1); then
1215        sed -E "$@"
1216    else
1217        sed -r "$@"
1218    fi
1219}
1220
1221http_redirect() {
1222    while read -r header; do
1223        case $header in
1224            Location:*)
1225                echo "${header#Location: }"
1226                return
1227            ;;
1228        esac
1229        if [ "$header" = "" ]; then
1230            break
1231        fi
1232    done
1233    cat > /dev/null
1234    return 1
1235}
1236
1237http_body() {
1238    sed -n '/^\r/,$p' | sed 1d
1239}
1240
1241openssl_get() {
1242    host=${1#https://*} # https://dom.com/path -> dom.com/path
1243    path=/${host#*/}    # dom.com/path -> /path
1244    host=${host%$path}  # dom.com/path -> dom.com
1245    printf "GET %s HTTP/1.0\nHost: %s\nUser-Agent: curl\n\n" "$path" "$host" |
1246        openssl s_client -quiet -connect "$host:443" 2>/dev/null
1247}
1248
1249umask 0022
1250main
1251#+end_src