diff --git a/.github/workflows/dorothy-workflow.yml b/.github/workflows/dorothy-workflow.yml index 48a65cd27..c6d933dec 100644 --- a/.github/workflows/dorothy-workflow.yml +++ b/.github/workflows/dorothy-workflow.yml @@ -116,7 +116,7 @@ jobs: strategy: fail-fast: false matrix: - runner: [ubuntu-24.04, ubuntu-22.04, macos-14, macos-12] + runner: [ubuntu-24.04, ubuntu-22.04, macos-15, macos-14, macos-12] # ubuntu-20.04 not supported, echo-wait fails: https://github.com/bevry/dorothy/actions/runs/9705310169/job/26787151094#step:2:1346 runs-on: ${{ matrix.runner }} steps: @@ -131,7 +131,7 @@ jobs: strategy: fail-fast: false matrix: - runner: [macos-14, macos-12] + runner: [macos-15, macos-14, macos-12] runs-on: ${{ matrix.runner }} steps: - name: 'Uninstall Homebrew' diff --git a/commands/choose b/commands/choose index fdd75c694..1246d49ae 100755 --- a/commands/choose +++ b/commands/choose @@ -629,7 +629,7 @@ function choose_() ( load_dorothy_config 'styles.bash' # refresh the styles - refresh_style_cache -- 'question_title_prompt' 'question_title_result' 'question_body' 'input_warning' 'input_error' 'icon_prompt' 'result_value' 'error' 'notice' 'count_spacer' 'result_line' 'active_line' 'selected_line' 'default_line' 'empty_line' 'inactive_line' 'legend' 'key' 'count_more' 'count_selected' 'count_defaults' 'count_empty' 'bar_top' 'bar_middle' 'bar_bottom' 'icon_multi_selected' 'icon_multi_default' 'icon_multi_active' 'icon_multi_standard' 'icon_single_selected' 'icon_single_default' 'icon_single_active_required' 'icon_single_active_optional' 'icon_single_standard' 'icon_nothing_provided' 'icon_no_selection' 'icon_nothing_selected' 'legend_legend_spacer' 'legend_key_spacer' 'key_key_spacer' 'indent_bar' 'indent_active' 'indent_inactive' 'indent_subsequent' 'nocolor__count_spacer' 'color__count_spacer' + refresh_style_cache -- 'question_title_prompt' 'question_title_result' 'question_body' 'input_warning' 'input_error' 'icon_prompt' 'result_value' 'error' 'notice' 'count_spacer' 'result_line' 'active_line' 'selected_line' 'default_line' 'empty_line' 'inactive_line' 'legend' 'key' 'count_more' 'count_selected' 'count_defaults' 'count_empty' 'bar_top' 'bar_middle' 'bar_bottom' 'icon_multi_selected' 'icon_multi_default' 'icon_multi_active' 'icon_multi_standard' 'icon_single_selected' 'icon_single_default' 'icon_single_active_required' 'icon_single_active_optional' 'icon_single_standard' 'icon_nothing_provided' 'icon_no_selection' 'icon_nothing_selected' 'legend_legend_spacer' 'legend_key_spacer' 'key_key_spacer' 'indent_bar' 'indent_active' 'indent_inactive' 'blockquote' 'nocolor__count_spacer' 'color__count_spacer' # select icons if test "$option_multi" = 'yes'; then @@ -1495,7 +1495,7 @@ function choose_() ( else # need to format item, as it is too big item_rendered="$item_original" - item_rendered="${item_rendered//$'\n'/$'\n'"$style__indent_subsequent"}" # re-add the necessary indentation + item_rendered="${item_rendered//$'\n'/$'\n'"${style__indent_bar}${style__blockquote}"}" # re-add the necessary indentation item_rendered="$(echo-wrap --width="$content_columns" -- "$item_rendered")" item_rows="$(echo-count-lines -- "$item_rendered")" items_renders[item_index]="$item_rendered" @@ -1755,7 +1755,7 @@ function choose_() ( fi } function render_result { - local render="$question_title_result" index + local render="$question_title_result" index item item_rendered if test -n "$commentary"; then if test -n "$render"; then render+=" $commentary" @@ -1772,7 +1772,9 @@ function choose_() ( # add results only if lingering, as there may be more than terminal height, so clearing wouldn't support such for index in "${selected_indexes[@]}"; do if test "${selected[index]}" = 'yes'; then - render+="${style__result_line}${style__icon_selected}${items[index]}${style__end__result_line}"$'\n' + item="${items[index]}" + item_rendered="${item//$'\n'/$'\n'"$style__blockquote"}" # re-add the necessary indentation + render+="${style__result_line}${style__icon_selected}${item_rendered}${style__end__result_line}"$'\n' fi done if test "$selected_count" -eq 0; then diff --git a/commands/command-working b/commands/command-working index 54b0e89f3..935ac2e23 100755 --- a/commands/command-working +++ b/commands/command-working @@ -57,6 +57,7 @@ function command_working() ( # ===================================== # Helpers + # exceptions are all commands that do not respond to a version or help query # https://github.com/greymd/teip/issues/29 # https://trunkcommunity.slack.com/archives/C0205B6KK8X/p1661601215325159 local failures=() exceptions=( @@ -66,6 +67,7 @@ function command_working() ( ssh-askpass sshd teip + trash trunk ) function check_status { diff --git a/commands/echo-mkdir b/commands/echo-mkdir index 7284e47eb..01b04ef3a 100755 --- a/commands/echo-mkdir +++ b/commands/echo-mkdir @@ -36,12 +36,14 @@ function echo_mkdir() ( } # process our own arguments, delegate everything else to stdinargs - local item option_sudo='no' option_args=() + local item option_sudo='no' option_args=() option_quiet + option_quiet="$(echo-quiet-enabled -- "$@")" while test "$#" -ne 0; do item="$1" shift case "$item" in '--help' | '-h') help ;; + '--no-quiet'* | '--quiet'* | '--no-verbose'* | '--verbose'*) ;; # handled by echo-quiet-enabled '--no-sudo'* | '--sudo'*) option_sudo="$(get-flag-value --affirmative --fallback="$option_sudo" -- "$item")" ;; @@ -56,27 +58,24 @@ function echo_mkdir() ( done # construct command - local cmd=() if test "$option_sudo" = 'yes'; then - cmd=( - 'sudo-helper' - '--' - 'mkdir' - '-p' - ) + function __mkdir { + sudo-helper --reason="Your sudo/root/login password is required to make the directory: $*" -- mkdir -p "$@" + } else - cmd=( - 'mkdir' - '-p' - ) + function __mkdir { + mkdir -p + } fi # ===================================== # Action function on_line { - if test -d "$1" || ("${cmd[@]}" "$1"); then - fs-absolute -- "$1" + if test -d "$1" || __mkdir "$1"; then + if test "$option_quiet" != 'yes'; then + fs-absolute -- "$1" + fi return 0 else return 1 diff --git a/commands/eval-helper b/commands/eval-helper index 90ef1940b..b68204948 100755 --- a/commands/eval-helper +++ b/commands/eval-helper @@ -25,13 +25,18 @@ function eval_helper() ( Disabled by default. --[no-]quiet - If enabled, only latest message will be kept, and command output will be cleared if successful. - If disabled, all messages and command output will be kept. + If empty, if successful only the pending message, command status, and success message will be kept. If failure, the pending message, command output and status, and failure message will be kept. + If enabled, if successful only the success message will be kept. If failure, the pending message, command output and status, and failure message will be kept. + If disabled, the pending message, command output and status, and success or failure message will be kept. The command stdout will also be available to stdout. --[no-]shapeshifter Workaround for commands that clear themselves. Disabled by default. + --[no-]trim + Trim output of the command. + Disabled by default. + --pending= Message to display while the command is executing. @@ -41,12 +46,14 @@ function eval_helper() ( --failure= Message to display if the command failed. - --title= - If provided, uses this as the title inside the wrappers, instead of the escaped command. + --command=<command-string> + By default when wrapping the command status and/or output, the escaped <command> is used. + Providing a <command-string> will use the <command-string> instead of the escaped <command>. Intended only for use by [sudo-helper]. QUIRKS: - Unless [--quiet=no] is used, then stdout+stderr will be merged into stderr. + Unless [--no-quiet] is used, then stdout+stderr will be merged into stderr. + Messages are output to TTY if available, otherwise stderr. EOF if test "$#" -ne 0; then echo-error "$@" @@ -55,15 +62,8 @@ function eval_helper() ( } # process - local item option_cmd=() option_wrap option_confirm option_quiet option_pending option_success option_failure - option_wrap='yes' - option_confirm='no' + local item option_quiet option_cmd=() option_wrap='yes' option_trim='no' option_confirm='no' option_shapeshifter='no' option_pending='' option_success='' option_failure='' option_command_string='' option_quiet="$(echo-quiet-enabled -- "$@")" - option_shapeshifter='no' - option_pending='' - option_success='' - option_failure='' - option_title='' while test "$#" -ne 0; do item="$1" shift @@ -73,7 +73,10 @@ function eval_helper() ( '--pending='*) option_pending="${item#*=}" ;; '--success='*) option_success="${item#*=}" ;; '--failure='*) option_failure="${item#*=}" ;; - '--title='*) option_title="${item#*=}" ;; + '--command='*) option_command_string="${item#*=}" ;; + '--no-trim'* | '--trim'*) + option_trim="$(get-flag-value --affirmative --fallback="$option_trim" -- "$item")" + ;; '--no-confirm'* | '--confirm'*) option_confirm="$(get-flag-value --affirmative --fallback="$option_confirm" -- "$item")" ;; @@ -96,17 +99,20 @@ function eval_helper() ( # ===================================== # Action - # prepare - local element_open tty_target - if test -z "$option_title"; then - option_title="$(echo-escape-command -- "${option_cmd[@]}")" + # tty + local tty_target + tty_target="$(is-tty --fallback)" + + # element + local element_open='' + if test -z "$option_command_string"; then + option_command_string="$(echo-escape-command -- "${option_cmd[@]}")" fi if test "$option_wrap" = 'yes'; then - element_open="$(echo-style --element="$option_title")" + element_open="$(echo-style --element="$option_command_string")" else - element_open="$(echo-style --code="$option_title")" + element_open="$(echo-style --code="$option_command_string")" fi - tty_target="$(is-tty --fallback)" # confirm if test "$option_confirm" = 'yes' && ! confirm --positive --ppid=$$ -- 'Confirm execution of the command that is below:' "$element_open"; then @@ -114,48 +120,63 @@ function eval_helper() ( return 0 fi - # output everything if already inside a revolving door, or if in verbose mode - local cmd_status=0 - if test "${INSIDE_REVOLVING_DOOR-}" = 'yes' -o "$option_quiet" = 'no'; then - # headers - if test -n "$option_pending"; then - __print_lines "$option_pending" >"$tty_target" - fi - if test "$option_wrap" = 'yes'; then - __print_lines "$element_open" >"$tty_target" + # headers + local header='' + if test -n "$option_pending"; then + header+="$option_pending"$'\n' + fi + if test "$option_wrap" = 'yes'; then + header+="$element_open"$'\n' + fi + if test -n "$header"; then + __print_string "$header" >"$tty_target" + fi + + # output + local output='' + function flush_output { + if test -n "$output"; then + __print_string "$output" >"$tty_target" + output='' fi + } + # output everything if already inside a revolving door, or if in verbose mode + local cmd_status=0 + if test "${INSIDE_REVOLVING_DOOR-}" = 'yes' || test "$option_quiet" = 'no'; then # body - eval_capture --statusvar=cmd_status -- "${option_cmd[@]}" + if test "$option_trim" = 'yes'; then + eval_capture --statusvar=cmd_status -- "${option_cmd[@]}" \ + > >(echo-trim-padding --stdin) \ + 2> >(echo-trim-padding --stdin >/dev/stderr) + else + eval_capture --statusvar=cmd_status -- "${option_cmd[@]}" + fi - # footers + # add close if test "$option_wrap" = 'yes'; then - echo-style --/element="$option_title" --status="$cmd_status" >"$tty_target" + output+="$(echo-style --/element="$option_command_string" --status="$cmd_status")"$'\n' fi + + # add success or failure if test "$cmd_status" -eq 0; then if test -n "$option_success"; then - __print_lines "$option_success" >"$tty_target" + output+="$option_success"$'\n' fi else if test -n "$option_failure"; then - __print_lines "$option_failure" >"$tty_target" + output+="$option_failure"$'\n' fi fi + + # output + flush_output else # not inside a revolving door, and not in verbose mode - local columns headers body footer='' + local columns body_file # trunk-ignore(shellcheck/SC2015) columns="$(is-tty && tput cols || :)" - headers="$(mktemp)" - body="$(mktemp)" - - # headers - if test -n "$option_pending"; then - __print_lines "$option_pending" | tee -a "$headers" >"$tty_target" - fi - if test "$option_wrap" = 'yes'; then - __print_lines "$element_open" | tee -a "$headers" >"$tty_target" - fi + body_file="$(mktemp)" # body # NOTE |& is bash v4 only, and this script must work on Bash v3, which uses 2>&1 | @@ -166,63 +187,113 @@ function eval_helper() ( # this is used if the command writes to TTY # in which case echo-revolving-door fails to clear tty_start - cat "$headers" >"$tty_target" # redo headers inside alt tty while its active - eval_capture --statusvar=cmd_status -- "${option_cmd[@]}" > >(tee -a "$body") 2> >(tee -a "$body" >/dev/stderr) + __print_string "$header" >"$tty_target" # redo header inside alt tty while its active + eval_capture --statusvar=cmd_status -- "${option_cmd[@]}" \ + > >(tee -a "$body_file") \ + 2> >(tee -a "$body_file" >/dev/stderr) tty_finish else - eval_capture --statusvar=cmd_status -- "${option_cmd[@]}" > >(tee -a "$body" | echo-revolving-door --columns="$columns") 2> >(tee -a "$body" | echo-revolving-door --columns="$columns" >/dev/stderr) + eval_capture --statusvar=cmd_status -- "${option_cmd[@]}" \ + > >(tee -a "$body_file" | echo-revolving-door --columns="$columns") \ + 2> >(tee -a "$body_file" | echo-revolving-door --columns="$columns" >/dev/stderr) # we cannot detect shapeshifting after the fact, as it occurs in the TTY, not stdout, nor stderr fi export INSIDE_REVOLVING_DOOR="$INSIDE_REVOLVING_DOOR_original" - # clear headers, we can re-add them later if needed - echo-clear-lines --stdin <"$headers" >"$tty_target" + # clear header, we can re-add them later if needed + echo-clear-lines -- "$header" >"$tty_target" - # generate footer - if test "$cmd_status" -eq 0; then - if test -n "$option_success"; then - footer+="$option_success"$'\n' + # generate output + if test "$option_quiet" = 'no' -o "$cmd_status" -ne 0; then + # pending message, command output and status, and success or failure message + + # add pending + if test -n "$option_pending"; then + output+="$option_pending"$'\n' fi - else - if test -n "$option_failure"; then - footer+="$option_failure"$'\n' + + # add body + local body + if test -s "$body_file"; then + if test "$option_trim" = 'yes'; then + body="$(echo-trim-padding --stdin <"$body_file")" + else + body="$(cat "$body_file")" + fi + else + body='' + fi + if test "$option_wrap" = 'no'; then + if test -n "$body"; then + flush_output + __print_lines "$body" >/dev/stderr + fi + else + if test -n "$body"; then + output+="$(echo-style --element="$option_command_string")"$'\n' + flush_output + __print_lines "$body" >/dev/stderr + output+="$(echo-style --/element="$option_command_string" --status="$cmd_status")"$'\n' + else + output+="$(echo-style --element="$option_command_string" --/fragment='' --status="$cmd_status")"$'\n' + fi fi - fi - # if quiet and successful, dump footer and exit - if test "$option_quiet" = 'yes' -a "$cmd_status" -eq 0; then - if test -n "$footer"; then - __print_string "$footer" >"$tty_target" + # add success or failure + if test "$cmd_status" -eq 0; then + if test -n "$option_success"; then + output+="$option_success"$'\n' + fi + else + if test -n "$option_failure"; then + output+="$option_failure"$'\n' + fi fi - return "$cmd_status" - fi - # if it didn't output anything, output self closing wrap, then footer, then exit - if test -z "$(cat "$body")"; then - echo-style --element/="$option_title" --status="$cmd_status" >"$tty_target" - if test -n "$footer"; then - __print_string "$footer" >"$tty_target" + elif test -z "$option_quiet"; then + # pending message, command status, and success message + + # add pending + if test -n "$option_pending"; then + output+="$option_pending"$'\n' fi - return "$cmd_status" - fi - # the command outputted things - # output wrap header - if test "$option_wrap" = 'yes'; then - __print_lines "$element_open" >"$tty_target" - fi - # if verbose, or failure, output body - if test "$option_quiet" = 'no' -o "$cmd_status" -ne 0; then - cat "$body" >/dev/stderr - fi - # outpout wrap footer - if test "$option_wrap" = 'yes'; then - echo-style --/element="$option_title" --status="$cmd_status" >"$tty_target" - fi - # output footer - if test -n "$footer"; then - __print_string "$footer" >"$tty_target" + # truncate body if wrapping + if test "$option_wrap" = 'yes'; then + local body + if test -s "$body_file"; then + if test "$option_trim" = 'yes'; then + body="$(echo-trim-padding --stdin <"$body_file")" + else + body="$(cat "$body_file")" + fi + else + body='' + fi + if test -n "$body"; then + # self close as we are truncating + output+="$(echo-style --/element="$option_command_string" --status="$cmd_status")"$'\n' + else + # close fragment as there is no data + output+="$(echo-style --element="$option_command_string" --/fragment --status="$cmd_status")"$'\n' + fi + fi + + # add success + if test -n "$option_success"; then + output+="$option_success"$'\n' + fi + elif test "$option_quiet" = 'yes'; then + # only success message + + # add success + if test -n "$option_success"; then + output+="$option_success"$'\n' + fi fi + + # output + flush_output fi # done diff --git a/commands/get-macos-release-name b/commands/get-macos-release-name index 55dadc8e6..310b3e48c 100755 --- a/commands/get-macos-release-name +++ b/commands/get-macos-release-name @@ -40,12 +40,13 @@ function get_macos_release_name() ( IFS=. read -r macos_major_version _ < <(sw_vers -productVersion) case "$macos_major_version" in - 14) __print_line sonoma ;; - 13) __print_line ventura ;; - 12) __print_line monterey ;; - 11) __print_line big_sur ;; + 15) __print_line 'sequoia' ;; + 14) __print_line 'sonoma' ;; + 13) __print_line 'ventura' ;; + 12) __print_line 'monterey' ;; + 11) __print_line 'big_sur' ;; *) - echo-error 'macOS version is too old to identify release name.' >/dev/stderr + echo-error 'Unable to detect the release name of this macOS version. Please send a PR to: ' --code='https://github.com/bevry/dorothy/blob/master/commands/get-macos-release-name' return 1 ;; esac diff --git a/commands/git-helper b/commands/git-helper index dbe1ee529..f6d106719 100755 --- a/commands/git-helper +++ b/commands/git-helper @@ -537,7 +537,7 @@ function git_helper() ( if ! git remote &>"$BODY"; then echo-error "$0: No git repository at $option_path" return 1 - elif test -z "$(cat "$BODY")"; then + elif test ! -s "$BODY"; then echo-error "$0: No git remotes found at $option_path" return 1 fi diff --git a/commands/is-dns-working b/commands/is-dns-working new file mode 100755 index 000000000..3d9971c75 --- /dev/null +++ b/commands/is-dns-working @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +# fresh macos without brew has no ping +function is_dns_working() ( + source "$DOROTHY/sources/bash.bash" + + # ===================================== + # Arguments + + function help { + cat <<-EOF >/dev/stderr + ABOUT: + Test if DNS is working. + + USAGE: + is-dns-working [--quiet] <hostname> + # returns 0 if DNS is working, 1 if not + + OPTIONS: + <hostname> + The hostname to test if the DNS is working. + Defaults to: cloudflare.com + + --quiet + Toggle verbosity by having this empty (the default), enabled (quiet mode), and disabled (verbose mode). + EOF + if test "$#" -ne 0; then + echo-error "$@" + fi + return 22 # EINVAL 22 Invalid argument + } + + # process + local item option_url='' option_quiet + option_quiet="$(echo-quiet-enabled -- "$@")" + while test "$#" -ne 0; do + item="$1" + shift + case "$item" in + '--help' | '-h') help ;; + '--no-quiet'* | '--quiet'* | '--no-verbose'* | '--verbose'*) ;; # handled by echo-quiet-enabled + '--'*) help "An unrecognised flag was provided: $item" ;; + *) + if test -z "$option_url"; then + option_url="$item" + else + help "An unrecognised argument was provided: $item" + fi + ;; + esac + done + + # ensure url + if test -z "$option_url"; then + option_url='cloudflare.com' + fi + + # ===================================== + # Act + + # `dig -x cloudflare.com` times out for some reason, nslookup works though + # eval-helper -- what-is-listening dns + # open 'https://www.cloudflare.com/en-au/ssl/encrypted-sni/' + # open 'https://1.1.1.1/help' + + local cmd=(nslookup "$option_url") + eval-helper --wrap --trim --quiet="$option_quiet" \ + --pending="$(echo-style --notice2='Checking your DNS connection...')" \ + --success="$(echo-style --good2='DNS is connected.')" \ + --failure="$( + echo-style --error1='DNS is not working.' + )" \ + -- "${cmd[@]}" + return $? +) + +# fire if invoked standalone +if test "$0" = "${BASH_SOURCE[0]}"; then + is_dns_working "$@" +fi diff --git a/commands/is-internet-working b/commands/is-internet-working index ec94b553e..9861d75f5 100755 --- a/commands/is-internet-working +++ b/commands/is-internet-working @@ -22,7 +22,7 @@ function is_internet_working() ( Defaults to: cloudflare.com --quiet - If provided, there will be no output, only the return code. + Toggle verbosity by having this empty (the default), enabled (quiet mode), and disabled (verbose mode). EOF if test "$#" -ne 0; then echo-error "$@" @@ -58,59 +58,22 @@ function is_internet_working() ( # ===================================== # Act - local log='/dev/null' connected='no' - - # prepare body - if test "$option_quiet" = 'no'; then - log="$(mktemp)" - fi - - # header - if test "$option_quiet" != 'yes'; then - echo-style --h1='Verify Internet Connection' - fi - - # check + local cmd if command-exists -- ping; then - if ping -c 1 "$option_url" &>"$log"; then - connected='yes' - fi + cmd=(ping -c 1 "$option_url") elif command-exists -- whois; then - if whois "$option_url" &>"$log"; then - connected='yes' - fi - else - if fetch --ok "$option_url" &>"$log"; then - connected='yes' - fi - fi - - # body - if test "$option_quiet" = 'no'; then - echo-style --dim="$(cat "$log")" - fi - - # footer - if test "$connected" = 'yes'; then - if test "$option_quiet" = 'no'; then - echo-style --green="It appears you are connected to the internet." - fi - if test "$option_quiet" != 'yes'; then - echo-style --g1='Verify Internet Connection' - fi - return 0 + cmd=(whois "$option_url") else - if test "$option_quiet" != 'yes'; then - echo-style --red="$( - cat <<-EOF - It appears you are disconnected from the internet. - It could be a DNS issue in which [setup-dns] may work. - EOF - )" - echo-style --e1='Verify Internet Connection' - fi - return 1 + cmd=(fetch --ok "$option_url") fi + eval-helper --wrap --quiet="$option_quiet" \ + --pending="$(echo-style --notice2='Checking your internet connection...')" \ + --success="$(echo-style --good2='Internet is connected.')" \ + --failure="$( + echo-style --error1='Internet is not working. If it is a DNS issue, then ' --code-error1='setup-dns' --error1=' may help.' + )" \ + -- "${cmd[@]}" + return $? ) # fire if invoked standalone diff --git a/commands/service-helper b/commands/service-helper index ea9ad3b85..469d5bd36 100755 --- a/commands/service-helper +++ b/commands/service-helper @@ -95,7 +95,7 @@ function service_helper() ( # process, @todo rewrite with option_ prefix # don't use empty quiet, as that only outputs wraps - local item option_quiet services=() ignore='no' supported='' start='' stop='' restart='' unmask='' load='' unload='' enable='' disable='' reenable='' status='' logs='' running='' remove='' exists='' edit='' + local item option_quiet option_services=() option_ignore='no' option_supported='' option_actions=() option_quiet="$(echo-quiet-enabled --fallback=no -- "$@")" while test "$#" -ne 0; do item="$1" @@ -103,61 +103,18 @@ function service_helper() ( case "$item" in '--help' | '-h') help ;; '--no-quiet'* | '--quiet'* | '--no-verbose'* | '--verbose'*) ;; # handled by echo-quiet-enabled - '--supported') supported='yes' ;; - '--ignore-missing' | '--ignore') ignore='yes' ;; - # service options - '--no-start'* | '--start'*) - start="$(get-flag-value --affirmative --fallback="$start" -- "$item")" - ;; - '--no-stop'* | '--stop'*) - stop="$(get-flag-value --affirmative --fallback="$stop" -- "$item")" - ;; - '--no-restart'* | '--restart'*) - restart="$(get-flag-value --affirmative --fallback="$restart" -- "$item")" - ;; - '--no-unmask'* | '--unmask'*) - unmask="$(get-flag-value --affirmative --fallback="$unmask" -- "$item")" - ;; - '--no-load'* | '--load'*) - load="$(get-flag-value --affirmative --fallback="$load" -- "$item")" - ;; - '--no-unload'* | '--unload'*) - unload="$(get-flag-value --affirmative --fallback="$unload" -- "$item")" - ;; - '--no-enable'* | '--enable'*) - enable="$(get-flag-value --affirmative --fallback="$enable" -- "$item")" - ;; - '--no-disable'* | '--disable'*) - disable="$(get-flag-value --affirmative --fallback="$disable" -- "$item")" - ;; - '--no-reenable'* | '--reenable'*) - reenable="$(get-flag-value --affirmative --fallback="$reenable" -- "$item")" - ;; - '--no-status'* | '--status'*) - status="$(get-flag-value --affirmative --fallback="$status" -- "$item")" - ;; - '--no-logs'* | '--logs'*) - logs="$(get-flag-value --affirmative --fallback="$logs" -- "$item")" - ;; - '--no-running'* | '--running'*) - running="$(get-flag-value --affirmative --fallback="$running" -- "$item")" - ;; - '--no-remove'* | '--remove'*) - remove="$(get-flag-value --affirmative --fallback="$remove" -- "$item")" - ;; - '--no-exists'* | '--exists'*) - exists="$(get-flag-value --affirmative --fallback="$exists" -- "$item")" - ;; - '--no-edit'* | '--edit'*) - edit="$(get-flag-value --affirmative --fallback="$edit" -- "$item")" + '--supported') option_supported='yes' ;; + '--ignore-missing' | '--ignore') option_ignore='yes' ;; + '--start' | '--stop' | '--restart' | '--unmask' | '--load' | '--unload' | '--enable' | '--enable-now' | '--disable' | '--disable-now' | '--reenable' | '--status' | '--logs' | '--running' | '--remove' | '--exists' | '--edit') + option_actions+=("${item#--}") ;; '--') - services+=("$@") + option_services+=("$@") shift $# break ;; '--'*) help "An unrecognised flag was provided: $item" ;; - *) services+=("$item") ;; + *) option_services+=("$item") ;; esac done @@ -193,8 +150,10 @@ function service_helper() ( # operations will result in a service which launchd tracks but # cannot be launched or discovered in any way. # ... + local plist + plist="$(do_find "$1")" sudo-helper --reason="Unloading the service $1 requires your sudo/root/login password." \ - -- launchctl unload -w "$1" + -- launchctl unload -w "$plist" } function do_start { local id="$1" @@ -460,7 +419,7 @@ function service_helper() ( test "${#paths[@]}" -ne 0 } else - if test "$supported" = 'yes'; then + if test "$option_supported" = 'yes'; then # if --supported, no error message return 1 else @@ -470,7 +429,7 @@ function service_helper() ( fi # if --supported, then we are done - if test "$supported" = 'yes'; then + if test "$option_supported" = 'yes'; then return 0 fi @@ -488,10 +447,10 @@ function service_helper() ( # https://man.archlinux.org/man/systemctl.1.en # https://wiki.gentoo.org/wiki/OpenRC_to_systemd_Cheatsheet function handle { - local this_service="$1" this_unmask="$unmask" this_load="$load" this_unload="$unload" this_start="$start" this_stop="$stop" this_restart="$restart" this_enable="$enable" this_disable="$disable" this_reenable="$reenable" this_status="$status" this_logs="$logs" this_running="$running" this_remove="$remove" this_exists="$exists" this_edit="$edit" + local this_service="$1" this_action='' # ignore? - if test "$ignore" = 'yes' && ! __do_exists "$this_service"; then + if test "$option_ignore" = 'yes' && ! __do_exists "$this_service"; then # is missing, and desire to ignore, so skip this one if test "$option_quiet" = 'no'; then echo-style \ @@ -501,155 +460,48 @@ function service_helper() ( return 0 fi - # exists? - if test "$this_exists" = 'yes'; then - this_exists='' - __do_exists "$this_service" - fi - - # edit? - if test "$this_edit" = 'yes'; then - this_edit='' - do_edit "$this_service" - fi - - # remove? - if test "$this_remove" = 'yes'; then - this_remove='' - do_remove "$this_service" - fi - - # status - if test "$this_status" = 'yes'; then - this_status='maybe' - do_status "$this_service" - fi - - # unmask, do before load - if test "$this_unmask" = 'yes'; then - this_unmask='' - do_unmask "$this_service" - - # renable status - if test "$this_status" = 'maybe'; then - this_status='yes' - fi - fi - - # load - if test "$this_load" = 'yes'; then - this_load='' - do_load "$this_service" - - # renable status - if test "$this_status" = 'maybe'; then - this_status='yes' - fi - fi - - # split restart if autostart is being modified - if test "$this_restart" = 'yes' && test "$this_reenable" = 'yes' -o "$this_enable" = 'yes' -o "$this_disable" = 'yes'; then - this_restart='' - this_stop='yes' - this_start='yes' - fi - - # stop - if test "$this_stop" = 'yes'; then - this_stop='' - do_stop "$this_service" - - # renable status - if test "$this_status" = 'maybe'; then - this_status='yes' - fi - fi - - # autostart - if test "$this_reenable" = 'yes'; then - this_reenable='' - do_reenable "$this_service" - - # renable status - if test "$this_status" = 'maybe'; then - this_status='yes' - fi - elif test "$this_enable" = 'yes'; then - this_enable='' - if test "$this_start" = 'yes'; then - this_start='' - do_enable_now "$this_service" - else - do_enable "$this_service" - fi - - # renable status - if test "$this_status" = 'maybe'; then - this_status='yes' - fi - elif test "$this_disable" = 'yes'; then - this_disable='' - if test "$this_stop" = 'yes'; then - this_stop='' - do_disable_no "$this_service" - else - do_disable "$this_service" - fi - - # renable status - if test "$this_status" = 'maybe'; then - this_status='yes' - fi - fi - - # start / restart - if test "$this_start" = 'yes'; then - this_start='' - do_start "$this_service" - - # renable status - if test "$this_status" = 'maybe'; then - this_status='yes' - fi - fi - if test "$this_restart" = 'yes'; then - this_restart='' - do_restart "$this_service" - - # renable status - if test "$this_status" = 'maybe'; then - this_status='yes' - fi - fi - - # unload - if test "$this_unload" = 'yes'; then - this_unload='' - do_unload "$this_service" - fi - - # status - if test "$this_status" = 'yes'; then - this_status='' - do_status "$this_service" - fi - - # logs - if test "$this_logs" = 'yes'; then - this_logs='' - do_logs "$this_service" - fi - - # running - if test "$this_running" = 'yes'; then - this_running='' - do_running "$this_service" - fi + # actions + for this_action in "${option_actions[@]}"; do + case "$this_action" in + 'exists') + if test "$option_ignore" = 'yes'; then + return 0 # we already know from earlier + else + __do_exists "$this_service" + fi + ;; + 'edit') do_edit "$this_service" ;; + 'remove') do_remove "$this_service" ;; + 'status') do_status "$this_service" ;; + # unmask before load + 'unmask') do_unmask "$this_service" ;; + # load before stops + 'load') do_load "$this_service" ;; + # do stop, starts, before unload + 'stop') do_stop "$this_service" ;; + 'reenable') do_reenable "$this_service" ;; + 'enable') do_enable "$this_service" ;; + 'disable') do_disable "$this_service" ;; + # do enable/disable starts stops + 'enable-now') do_enable_now "$this_service" ;; + 'disable-now') do_disable_now "$this_service" ;; + # do starts + 'start') do_start "$this_service" ;; + # if any of reenable/disable/enable, then restart should be changed to stop and start + 'restart') do_restart "$this_service" ;; + # do unload + 'unload') do_unload "$this_service" ;; + 'logs') do_logs "$this_service" ;; + # do running + 'running') do_running "$this_service" ;; + *) help "An unrecognised action was provided: $this_action" ;; + esac + done } # cycle through local service - for service in "${services[@]}"; do + for service in "${option_services[@]}"; do handle "$service" done ) diff --git a/commands/setup-dns b/commands/setup-dns index 7331f53e6..f5a651d9d 100755 --- a/commands/setup-dns +++ b/commands/setup-dns @@ -1,160 +1,86 @@ #!/usr/bin/env bash +# Since 2020 iOS 14 and macOS Big Sur v11 have encryption but requires manual .mobileconfig profile installation which expire; as such it is not worth implementing compared to recommending alternative encrypted DNS services on macOS +# https://developer.apple.com/videos/play/wwdc2020/10047/ +# https://docs.quad9.net/Setup_Guides/MacOS/Big_Sur_and_later_%28Encrypted%29/ +# https://github.com/paulmillr/encrypted-dns + +# dot = dns over tls +# doh = dns over https +# sdns = dnscrypt +# quic = preferred + +# Cloudflare Warp doesn't yet support any of the platforms I use, so no support in here yet + function setup_dns() ( source "$DOROTHY/sources/bash.bash" __require_array 'mapfile' 'empty' - # check for support - if ! (is-mac || is-ubuntu); then - echo-style --warning="[$0] is implemented for macOS and Ubuntu systems..." >/dev/stderr - if ! confirm --negative --ppid=$$ -- 'Proceed at your own risk?'; then - return 0 - fi - fi - # ===================================== - # Detection + # Compatibility - # helper - function __die_unknown_os { - echo-style --error='Unknown Operating System' >/dev/stderr || return - return 19 # ENODEV 19 Operation not supported by device - } - - # prepare the paths - local BIN_DIR CONF_DIR DATA_DIR STATE_DIR SERVICE_DIR - BIN_DIR="$(echo-mkdir --sudo -- /usr/local/bin)" - CONF_DIR="$(echo-mkdir --sudo -- /usr/local/etc)" - DATA_DIR="$(echo-mkdir --sudo -- /usr/local/share)" - STATE_DIR="$(echo-mkdir --sudo -- /usr/local/lib)" + # check for support if is-wsl; then - echo-style --notice="[$0] is not applicable on WSL, skipping." >/dev/stderr - return 0 + echo-style --code-error1="$0" --error1=' is not applicable on WSL.' >/dev/stderr + return 19 # ENODEV 19 Operation not supported by device elif is-mac; then - SERVICE_DIR='/Library/LaunchDaemons' - elif is-linux; then - SERVICE_DIR='/etc/systemd/system' - else - __die_unknown_os - fi - - # prepare the installers - # https://github.com/AdguardTeam/AdGuardHome/releases - # https://github.com/cloudflare/cloudflared/releases - # https://github.com/DNSCrypt/dnscrypt-proxy/releases - local arch aghome_installer='' cloudflared_installer='' dnscrypt_installer='' cloudflared_archive_filter='' - arch="$(get-arch)" - if is-mac; then - cloudflared_archive_filter='cloudflared' - if test "$arch" = 'a64'; then - aghome_installer='AdGuardHome_darwin_arm64.zip' - cloudflared_installer='cloudflared-darwin-amd64.tgz' # rosetta - dnscrypt_installer='dnscrypt-proxy-macos_arm64' # ... - elif test "$arch" = 'x64'; then - aghome_installer='AdGuardHome_darwin_amd64.zip' - cloudflared_installer='cloudflared-darwin-amd64.tgz' - dnscrypt_installer='dnscrypt-proxy-macos_x86_64' # ... + if command-missing -- networksetup; then + echo-style --code-error1="$0" --error1=' is not applicable on this macOS environment, as ' --code-error1='networksetup' --error1=' was not available.' >/dev/stderr + return 19 # ENODEV 19 Operation not supported by device fi - elif is-linux; then - if test "$arch" = 'a64'; then - aghome_installer='AdGuardHome_linux_arm64.tar.gz' - cloudflared_installer='cloudflared-linux-arm64' - dnscrypt_installer='dnscrypt-proxy-linux_arm64' # ... - elif test "$arch" = 'x64'; then - aghome_installer='AdGuardHome_linux_amd64.tar.gz' - cloudflared_installer='cloudflared-linux-amd64' - dnscrypt_installer='dnscrypt-proxy-linux_x86_64' # ... - elif test "$arch" = 'x32'; then - aghome_installer='AdGuardHome_linux_386.tar.gz' - cloudflared_installer='cloudflared-linux-386' - dnscrypt_installer='dnscrypt-proxy-linux_i386' # ... + elif is-ubuntu; then + if command-missing -- systemctl resolvectl; then + echo-style --code-error1="$0" --error1=' is not applicable on this Linux environment, as ' --code-error1='systemctl' ' and/or ' --code-error1='resolvectl' --error1=' were not available.' >/dev/stderr + return 19 # ENODEV 19 Operation not supported by device fi else - __die_unknown_os + echo-style --code-error1="$0" --error1=' is not applicable on this unknown system.' >/dev/stderr + return 19 # ENODEV 19 Operation not supported by device fi - # determine available services - local services=('system') - if command-exists -- nordvpn; then - echo-style --notice='NordVPN installation detected, only permitting system DNS service.' - elif is-internet-working; then - if test -n "$aghome_installer"; then - services+=(aghome) - fi - if test -n "$cloudflared_installer"; then - services+=(cloudflared) - fi - if test -n "$dnscrypt_installer"; then - services+=(dnscrypt) + # ensure the paths are defined, they will be created later + local path_vars=('BIN_DIR' 'CONF_DIR' 'DATA_DIR' 'LIB_DIR' 'STATE_DIR' 'SERVICE_DIR' 'LOGS_DIR') + function make_system_paths { + local values=() + for path_var in "${path_vars[@]}"; do + values+=("${!path_var}") + done + echo-mkdir --sudo --quiet -- "${values[@]}" + } + function __check_env_vars { + local env_var missing_env_vars=() + for env_var in "$@"; do + if test -z "${!env_var-}"; then + missing_env_vars+=("$env_var") + fi + done + if test "${#missing_env_vars[@]}" -ne 0; then + echo-style --error1='Missing these requried environment variables: ' --code-error1="${missing_env_vars[*]}" >/dev/stderr + return 2 # ENOENT 2 No such file or directory fi - fi - - # helper - function die_incompatible_service { - local service_title - service_title="$1" - echo-style \ - --error="This platform [$arch] does not yet support: $service_title" $'\n' \ - --notice='Supported DNS services for this platform are:' $'\n' \ - "${services[*]}" - return 19 # ENODEV 19 Operation not supported by device } + __check_env_vars "${path_vars[@]}" # ===================================== - # Arguments - - # @todo move this later so that it also outputs providers - function help { - cat <<-EOF >/dev/stderr - ABOUT: - Setup the DNS service for your system. - - USAGE: - setup-dns [...options] <service> - - SERVICES: - $(__print_lines "${services[@]}") + # Configuration - CONFIGURATION: - dns.bash - EOF - if test "$#" -ne 0; then - echo-error "$@" - fi - return 22 # EINVAL 22 Invalid argument - } + # ------------------------------------- + # Locals - # process - local item action='' option_service='' option_vpn_reconnect='' - while test "$#" -ne 0; do - item="$1" - shift - case "$item" in - '--help' | '-h') help ;; - '--no-vpn-reconnect'* | '--vpn-reconnect'*) - option_vpn_reconnect="$(get-flag-value --affirmative --fallback="$option_vpn_reconnect" -- "$item")" - ;; - '--no-quiet'* | '--quiet'* | '--no-verbose'* | '--verbose'*) ;; # handled by echo-quiet-enabled - '--'*) help "An unrecognised flag was provided: $item" ;; - *) - if test -z "$option_service"; then - option_service="$item" - else - help "An unrecognised argument was provided: $item" - fi - ;; - esac - done + local \ + service provider \ + ipv4_servers ipv6_servers dot_servers doh_servers sdns_servers quic_servers dnscrypt_names \ + local_ipv4_servers=('127.0.0.1') local_ipv6_servers=('::1') \ + properties=('ipv4_servers' 'ipv6_servers' 'doh_servers' 'dot_servers' 'quic_servers' 'sdns_servers' 'dnscrypt_names') - # ===================================== - # Configuration + # ------------------------------------- + # Fetch Configuration source "$DOROTHY/sources/config.sh" # dns.bash provides: local DNS_SERVICE='' local DNS_PROVIDER='' - local DNS_BACKUP_PROVIDER='' local DNS_IPV4_SERVERS=() local DNS_IPV6_SERVERS=() local DNS_DOH_SERVERS=() @@ -162,28 +88,112 @@ function setup_dns() ( local DNS_QUIC_SERVERS=() local DNS_SDNS_SERVERS=() local DNS_DNSCRYPT_NAMES=() - local CLOUDFLARED_TUNNELS=() load_dorothy_config 'dns.bash' + # ------------------------------------- + # Fetch Services + + local services_id=() services_about=() services_url=() + + # load id + mapfile -t services_id < <(jq -r '.services | keys_unsorted[]' "$DOROTHY/config/dns.json") + + # load about + mapfile -t services_about < <(jq -r '.services[].about' "$DOROTHY/config/dns.json") + if test "${#services_id[@]}" -ne "${#services_about[@]}"; then + help 'A DNS service is missing an about, or an about is multiline when it should be single line.' + fi + + # load url + mapfile -t services_url < <(jq -r '.services[].url' "$DOROTHY/config/dns.json") + if test "${#services_id[@]}" -ne "${#services_url[@]}"; then + help 'A DNS service is missing a URL.' + fi + + # ------------------------------------- + # Fetch Providers + + local providers_id=() providers_url=() providers_about=() + + # load id + mapfile -t providers_id < <(jq -r '.providers | keys_unsorted[]' "$DOROTHY/config/dns.json") + + # load about + mapfile -t providers_about < <(jq -r '.providers[].about' "$DOROTHY/config/dns.json") + if test "${#providers_id[@]}" -ne "${#providers_about[@]}"; then + help 'A DNS provider is missing an about, or an about is multiline when it should be single line.' + fi + + # load url + mapfile -t providers_url < <(jq -r '.providers[].url' "$DOROTHY/config/dns.json") + if test "${#providers_id[@]}" -ne "${#providers_url[@]}"; then + help 'A DNS provider is missing a URL.' + fi + # ===================================== # Helpers - function verify_dns_generic { - local attempt="${1:-1}" status=0 - nslookup cloudflare.com | echo-trim-padding --stdin || status=$? - if test "$status" -ne 0; then - echo-style --notice="DNS service failed to verify, trying again in 10 seconds." - sleep 10 - attempt="$((attempt + 1))" - echo-style --notice="Attempt $attempt..." - verify_dns_generic "$attempt" - return + # @todo only change necessary vpn properties; unnecessary: `firewall` + # local nordvpn_connect='' nordvpn_killswitch='' nordvpn_firewall='' + function __is_vpn_interface { + local interface="$1" + if [[ $interface =~ (nordlynx|vpn|tun|tap) ]]; then # regex fuzzy match + return 0 fi - return 0 - # `dig -x cloudflare.com` times out for some reason, nslookup works though - # eval-helper -- what-is-listening dns - # open 'https://www.cloudflare.com/en-au/ssl/encrypted-sni/' - # open 'https://1.1.1.1/help' + return 1 + } + function disconnect_vpn { + if command-exists -- nordvpn; then + nordvpn set connect off || : + nordvpn set killswitch off || : + nordvpn disconnect || : + waiter 5 + fi + } + function reconnect_vpn { + if command-exists -- nordvpn && nordvpn account; then + if nordvpn connect; then + # @todo apply dns, apply backups of killswitch and connect options + # nordvpn set dns "..." + nordvpn set killswitch on || : + nordvpn set connect on || : + waiter 5 + fi + __verify_connection_and_wait + fi + } + + function __fail_if_dns_service_is_installed_but_not_intended { + local id="$1" bin="$2" exists + exists="$(type -P "$id" || :)" + if test -n "$exists" -a "$exists" != "$bin"; then + echo-style --error1='The DNS service ' --code-error1="$id" ' has a non-standard installation at: ' --code-error1="$exists" $'\n' \ + --error1='Remove it and try again...' >/dev/stderr + return 75 # EPROGMISMATCH 75 Program version wrong + fi + } + + function __fail_if_dns_serice_is_not_installed { + local id="$1" bin="$2" + if test ! -x "$bin"; then + echo-style --error1='The DNS service ' --code-error1="$id" ' failed to install to: ' --code-error1="$bin" >/dev/stderr + return 9 # EBADF 9 Bad file descriptor + fi + } + + function __verify_connection { + is-dns-working --no-quiet || return + is-internet-working || return + } + + function __verify_connection_and_wait { + local attempt=1 + while ! __verify_connection; do + echo-style --notice1='Failed to verify connection, trying again in 10 seconds.' + waiter 10 + attempt="$((attempt + 1))" + echo-style --notice1="Attempt $attempt..." + done } function verify_dns_resolvectl { @@ -192,8 +202,8 @@ function setup_dns() ( resolvectl status --no-pager || status=$? resolvectl statistics --no-pager || status=$? if test "$status" -ne 0; then - echo-style --notice="DNS service failed to verify, trying again in 10 seconds." - sleep 10 + echo-style --notice='DNS service failed to verify, trying again in 10 seconds.' + waiter 10 attempt="$((attempt + 1))" echo-style --notice="Attempt $attempt..." verify_dns_resolvectl "$attempt" @@ -202,23 +212,10 @@ function setup_dns() ( return 0 } - function check_installation { - local id="$1" bin="$2" exists - exists="$(type -P "$id" || :)" - if test -n "$exists" -a "$exists" != "$bin"; then - { - echo-style --error="There is a non-standard installation at:" - type -P "$id" - echo-style --warning="Remove it and try again..." - } >/dev/stderr - return 75 # EPROGMISMATCH 75 Program version wrong - fi - } - # https://support.apple.com/en-au/guide/terminal/apdc6c1077b-5d5d-4d35-9c19-60f2397b2369/mac # https://apple.stackexchange.com/a/366388/15131 # stop the service (if it exists) temporarily - function service_stop { + function dns_service_stop { local file="$1" id # check if the service file exists @@ -238,7 +235,7 @@ function setup_dns() ( } # stops, uninstalls, and deletes the service (if it exists) - function service_disable { + function dns_service_disable_and_stop { local file="$1" id # check if the service file exists @@ -258,16 +255,15 @@ function setup_dns() ( service-helper --disable -- "system/$id" || : service-helper --stop -- "$id" || : service-helper --unload -- "$file" || : - do_remove -- /Library/Logs/*"$id"*.log + dns_service_remove -- "$LOGS_DIR/"*"$id"*.log else - service-helper --disable --stop -- "$id" || : - service-helper --status -- "$id" || : + service-helper --disable --stop --status -- "$id" || : fi - do_remove --reload -- "$file" + dns_service_remove --reload -- "$file" } # loads/installs the service, then enables and starts it - function service_enable { + function dns_service_enable { local file="$1" id id="$(fs-filename --basename -- "$file")" if is-mac; then @@ -278,15 +274,17 @@ function setup_dns() ( service-helper --load -- "$file" || : # service-helper --start -- "system/$id" || : service-helper --start -- "$id" || : - sleep 3 - eval-helper --no-quiet -- cat /Library/Logs/*"$id"*.log || : + waiter 5 else - service-helper --enable --start -- "$id" || : - service-helper --status -- "$id" || : + service-helper --enable --start --status -- "$id" || : fi } - function service_reload { + function dns_service_logs { + eval-helper --no-quiet -- cat "$LOGS_DIR/"*"$id"*.log || : + } + + function dns_service_reload { # after service additions/removals # systemctl needs to be reloaded apparently if command-exists -- systemctl; then @@ -296,7 +294,7 @@ function setup_dns() ( fi } - function do_remove { + function dns_service_remove { # process local item service='' flags=() reload='no' remove_paths=() while test "$#" -ne 0; do @@ -331,7 +329,7 @@ function setup_dns() ( "$BIN_DIR/$service" "$CONF_DIR/$service" "$DATA_DIR/$service" - "$STATE_DIR/$service" + "$LIB_DIR/$service" # xdg "$XDG_BIN_HOME/$service" @@ -346,6 +344,7 @@ function setup_dns() ( "/usr/local/lib/$service" # services + "$SERVICE_DIR/"*"$service"* /Library/LaunchAgents/*"$service"* # user /Library/LaunchDaemons/*"$service"* # everyone /etc/init.d/*"$service"* # alt @@ -353,7 +352,7 @@ function setup_dns() ( /etc/systemd/system/*"$service"* # desired # logs - /Library/Logs/*"$service"*.log + "$LOGS_DIR/"*"$service"*.log ) fi @@ -367,7 +366,7 @@ function setup_dns() ( ls -la "$remove_path" )" )" - if confirm "${flags[@]}" --ppid=$$ -- "$question" "$details"; then + if test "$option_confirm" = 'no' || confirm "${flags[@]}" --ppid=$$ -- "$question" "$details"; then fs-rm --quiet --no-confirm --sudo -- "$remove_path" removed='yes' fi @@ -376,359 +375,14 @@ function setup_dns() ( # reload? if test "$reload" = 'yes' -a "$removed" = 'yes'; then - service_reload - fi - } - - # ===================================== - # Select DNS Provider/Servers - - # providers that are user selectable - # excludes local - # excludes backup - local providers=() - # add env, if available - if test \ - "${#DNS_IPV4_SERVERS[@]}" -ne 0 -o \ - "${#DNS_IPV6_SERVERS[@]}" -ne 0 -o \ - "${#DNS_DOH_SERVERS[@]}" -ne 0 -o \ - "${#DNS_DOT_SERVERS[@]}" -ne 0 -o \ - "${#DNS_QUIC_SERVERS[@]}" -ne 0 -o \ - "${#DNS_SDNS_SERVERS[@]}" -ne 0; then - providers+=('env') - fi - # add backup, if available - # if test -n "$DNS_BACKUP_PROVIDER"; then - # providers+=('backup') - # fi - # add standard options - local providers+=( - 'quad9' - 'cloudflare' - 'cloudflare-security' - 'cloudflare-family' - 'cloudflare-teams' - 'adguard' - 'adguard-unfiltered' - 'adguard-family' - 'google' - 'opendns' - ) - - # prepare local vars - local local_ipv4_servers=( - '127.0.0.1' - ) - local local_ipv6_servers=( - '::1' - ) - - # prepare selection vars - local provider='' - local ipv4_servers=() - local ipv6_servers=() - local dot_servers=() # dns over tls - local doh_servers=() # dns over https - local sdns_servers=() # dnscrypt - local quic_servers=() # preferred - local dnscrypt_names=() - - # helper - function fetch_provider { - local provider - provider="${1:-"$DNS_PROVIDER"}" - - # if backup was provided, use it - if test "$provider" = 'backup'; then - if test -n "${DNS_BACKUP_PROVIDER}"; then - fetch_provider "$DNS_BACKUP_PROVIDER" - return - else - echo-style --warning="Backup DNS Provider Missing" - __print_lines "The backup DNS provider was requested, however it has not yet been configured. Set DNS_BACKUP_PROVIDER in your dns.bash configuration file to your desired backup provider of these: ${providers[*]}" - return 1 - fi - elif test "$provider" != 'local'; then - # if non-local provider was provided, confirm or ask for it - provider="$( - choose --required --confirm \ - --question='Which DNS provider to use?' \ - --skip-default --default="$provider" -- "${providers[@]}" - )" + dns_service_reload fi - - # reset GLOBAL vars - ipv4_servers=() - ipv6_servers=() - dot_servers=() # dns over tls - doh_servers=() # dns over https - sdns_servers=() # dnscrypt - quic_servers=() # preferred - dnscrypt_names=() - - # turn provider into servers - case "$provider" in - 'local') - ipv4_servers=( - "${local_ipv4_servers[@]}" - ) - ipv6_servers=( - "${local_ipv6_servers[@]}" - ) - ;; - 'env') - ipv4_servers=( - "${DNS_IPV4_SERVERS[@]}" - ) - ipv6_servers=( - "${DNS_IPV6_SERVERS[@]}" - ) - doh_servers=( - "${DNS_DOH_SERVERS[@]}" - ) - dot_servers=( - "${DNS_DOT_SERVERS[@]}" - ) - quic_servers=( - "${DNS_QUIC_SERVERS[@]}" - ) - sdns_servers=( - "${DNS_SDNS_SERVERS[@]}" - ) - dnscrypt_names=( - "${DNS_DNSCRYPT_NAMES[@]}" - ) - ;; - 'adguard') - # https://adguard-dns.com/en/public-dns.html - # If you want to block ads and trackers. - ipv4_servers=( - '94.140.14.14' - '94.140.15.15' - ) - ipv6_servers=( - '2a10:50c0::ad1:ff' - '2a10:50c0::ad2:ff' - ) - doh_servers=( - 'https://dns.adguard-dns.com/dns-query' - ) - dot_servers=( - 'tls://dns.adguard-dns.com' - ) - quic_servers=( - 'quic://dns.adguard-dns.com' - ) - sdns_servers=( - 'sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20' - ) - dnscrypt_names=( - 'adguard-dns' - 'adguard-dns-doh' - 'adguard-dns-ipv6' - ) - ;; - 'adguard-unfiltered') - # https://adguard-dns.com/en/public-dns.html - # If you don't want AdGuard DNS to block ads and trackers, or any other DNS requests. - ipv4_servers=( - '94.140.14.140' - '94.140.14.141' - ) - ipv6_servers=( - '2a10:50c0::1:ff' - '2a10:50c0::2:ff' - ) - doh_servers=( - 'https://unfiltered.adguard-dns.com/dns-query' - ) - dot_servers=( - 'tls://unfiltered.adguard-dns.com' - ) - quic_servers=( - 'quic://unfiltered.adguard-dns.com' - ) - sdns_servers=( - 'sdns://AQMAAAAAAAAAEjk0LjE0MC4xNC4xNDA6NTQ0MyC16ETWuDo-PhJo62gfvqcN48X6aNvWiBQdvy7AZrLa-iUyLmRuc2NyeXB0LnVuZmlsdGVyZWQubnMxLmFkZ3VhcmQuY29t' - ) - dnscrypt_names=( - 'adguard-dns-unfiltered' - 'adguard-dns-unfiltered-ipv6' - ) - ;; - 'adguard-family') - # If you want to block adult content, enable safe search and safe mode options wherever possible, and also block ads and trackers. - ipv4_servers=( - '94.140.14.15' - '94.140.15.16' - ) - ipv6_servers=( - '2a10:50c0::bad1:ff' - '2a10:50c0::bad2:ff' - ) - doh_servers=( - 'https://family.adguard-dns.com/dns-query' - ) - dot_servers=( - 'tls://family.adguard-dns.com' - ) - quic_servers=( - 'quic://family.adguard-dns.com' - ) - sdns_servers=( - 'sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNTo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ' - ) - dnscrypt_names=( - 'adguard-dns-family' - 'adguard-dns-family-doh' - 'adguard-dns-family-ipv6' - ) - ;; - 'cloudflare') - # https://developers.cloudflare.com/1.1.1.1/setting-up-1.1.1.1 - # https://developers.cloudflare.com/1.1.1.1/encrypted-dns/dns-over-https/make-api-requests - # https://developers.cloudflare.com/1.1.1.1/encrypted-dns/dns-over-tls - ipv4_servers=( - '1.1.1.1' - '1.0.0.1' - ) - ipv6_servers=( - '2606:4700:4700::1111' - '2606:4700:4700::1001' - ) - doh_servers=( - 'https://cloudflare-dns.com/dns-query' - ) - dot_servers=( - 'tls://one.one.one.one' - ) - dnscrypt_names=( - 'cloudflare' - 'cloudflare-ipv6' - ) - ;; - 'cloudflare-security' | 'cloudflare-malware') - # https://developers.cloudflare.com/1.1.1.1/1.1.1.1-for-families/setup-instructions/router/ - ipv4_servers=( - '1.1.1.2' - '1.0.0.2' - ) - ipv6_servers=( - '2606:4700:4700::1112' - '2606:4700:4700::1002' - ) - doh_servers=( - 'https://security.cloudflare-dns.com/dns-query' - ) - dot_servers=( - 'tls://security.cloudflare-dns.com' - ) - dnscrypt_names=( - 'cloudflare-security' - 'cloudflare-security-ipv6' - ) - ;; - 'cloudflare-family') - # https://developers.cloudflare.com/1.1.1.1/1.1.1.1-for-families/setup-instructions/router/ - # https://developers.cloudflare.com/1.1.1.1/1.1.1.1-for-families - ipv4_servers=( - '1.1.1.3' - '1.0.0.3' - ) - ipv6_servers=( - '2606:4700:4700::1113' - '2606:4700:4700::1003' - ) - doh_servers=( - 'https://family.cloudflare-dns.com/dns-query' - ) - dot_servers=( - 'tls://family.cloudflare-dns.com' - ) - dnscrypt_names=( - 'cloudflare-family' - 'cloudflare-family-ipv6' - ) - ;; - 'cloudflare-teams') - # https://developers.cloudflare.com/1.1.1.1/1.1.1.1-for-families/setup-instructions/router/ - ipv4_servers=( - '172.64.36.1' - '172.64.36.2' - ) - ipv6_servers=() - ;; - 'google') - ipv4_servers=( - '8.8.8.8' - '8.8.4.4' - ) - ipv6_servers=( - '2001:4860:4860::8888' - '2001:4860:4860::8844' - ) - dnscrypt_names=( - 'google' - 'google-ipv6' - ) - ;; - 'opendns') - # https://support.opendns.com/hc/en-us/articles/227986667-Does-OpenDNS-support-IPv6- - ipv4_servers=( - '208.67.222.222' - '208.67.220.220' - ) - ipv6_servers=( - '2620:0:ccc::2' - '2620:0:ccd::2' - ) - ;; - 'quad9') - # https://www.quad9.net/service/service-addresses-and-features/ - ipv4_servers=( - '9.9.9.9' - '149.112.112.112' - ) - ipv6_servers=( - # '2620:fe::fe' cloudflared fails - '2620:fe::9' - ) - doh_servers=( - 'https://dns.quad9.net/dns-query' - ) - dot_servers=( - 'tls://dns.quad9.net' - ) - ;; - *) - help "Invalid provider: $provider" - ;; - esac } # ===================================== - # DNS Service: System: macOS + # DNS Service: System DNS - function __system_exists { - if is-mac; then - command-exists -- networksetup || return - elif is-linux; then - command-exists -- systemctl resolvectl || return - else - __die_unknown_os || return - fi - } - function system_install { - if ! __system_exists; then - eval_capture -- __die_unknown_os # use this message, but keep the custom return code - return 29 # Illegal seek - fi - } - function system_uninstall { - eval_capture -- __die_unknown_os # use this message, but keep the custom return code - return 29 # Illegal seek - } - function system_configure { + function system_dns__configure { # determine servers and action local servers action action_title action="$1" @@ -744,6 +398,10 @@ function setup_dns() ( "${local_ipv6_servers[@]}" ) fi + if test "${#servers[@]}" -eq 0; then + echo-error 'servers is empty, something is misconfigured' + return 1 + fi # handle the os changes if is-mac; then @@ -752,7 +410,7 @@ function setup_dns() ( # https://support.apple.com/en-us/HT202516 # log - echo-style --h1="Configure and $action_title macOS" + echo-style --h2="$action_title $system_dns__title" # apply local interface @@ -761,7 +419,7 @@ function setup_dns() ( done # log - echo-style --g1="Configure and $action_title macOS" + echo-style --g2="$action_title $system_dns__title" else # ------------------------------------- # DNS Service: Systemd (aka systemd-resolved, systemd-resolve, resolvectl, resolv) @@ -794,21 +452,11 @@ function setup_dns() ( # To improve compatibility, /etc/resolv.conf is read in order to discover configured system DNS servers, but only if it is not a symlink to /run/systemd/resolve/stub-resolv.conf, /usr/lib/systemd/resolv.conf or /run/systemd/resolve/resolv.conf (see below). # log - local systemd_title='Systemd-resolved' - echo-style --h1="Configure and $action_title $systemd_title" + echo-style --h2="$action_title $system_dns__title" # dump the data for where we are sudo-helper -- resolvectl status --no-pager || : - # if there is nordvpn, we have to disconnect - if command-exists -- nordvpn; then - nordvpn set autoconnect off || : - nordvpn set killswitch off || : - nordvpn set firewall off || : - nordvpn disconnect || : - sleep 3 - fi - # temporarily disable these services while we redo the configuration files # https://wiki.archlinux.org/title/systemd-networkd # resolved handles dns, networkd handles network interfaces @@ -873,14 +521,14 @@ function setup_dns() ( # apply the changes to their interfaces mapfile -t interfaces < <(network-interface list) if is-array-empty -- "${interfaces[@]}"; then - echo-style --error='No interfaces were found.' >/dev/stderr + echo-style --error1='No interfaces were found.' >/dev/stderr return 1 fi local default_route for interface in "${interfaces[@]}"; do # skip vpn interfaces if __is_vpn_interface "$interface"; then - echo-style --dim="Skipping VPN interface: $interface" + echo-style --dim="Skipping VPN interface: " --code="$interface" continue fi @@ -903,24 +551,13 @@ function setup_dns() ( sudo-helper -- resolvectl dns "$interface" "${servers[@]}" # reboot the interface if it isn't a vpn - sleep 3 + waiter 5 network-interface restart "$interface" done # dump the status now that we are done sudo-helper -- resolvectl status --no-pager || : - # if there is nordvpn, we have to reconnect - if command-exists -- nordvpn; then - if test "$option_vpn_reconnect" = 'yes' || (test -z "$option_vpn_reconnect" && confirm --linger --positive --ppid="$$" -- 'Reconnect to NordVPN?'); then - if nordvpn connect; then - nordvpn set firewall on || : - nordvpn set killswitch on || : - nordvpn set autoconnect on || : - fi - fi - fi - # now that it is all done, enable or disable if test "$action" = 'enable'; then # verify @@ -934,143 +571,205 @@ function setup_dns() ( # sudo-helper -- systemctl reset-failed # log - echo-style --g1="Configure and $action_title $systemd_title" - fi - } - function __is_vpn_interface { - local interface="$1" - if [[ $interface =~ (nordlynx|vpn|tun|tap) ]]; then # regex fuzzy match - return 0 + echo-style --g2="$action_title $system_dns__title" fi - return 1 } # ===================================== - # DNS Service: Custom: AdGuard Home - - # https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#command-line - local aghome_title="AdGuard Home" - local aghome_id='AdGuardHome' - local aghome_bin_file="$BIN_DIR/$aghome_id" - local aghome_conf_dir="$CONF_DIR/$aghome_id" - local aghome_conf_file="$aghome_conf_dir/$aghome_id.yaml" - local aghome_data_dir="$DATA_DIR/$aghome_id" - local aghome_state_dir="$STATE_DIR/$aghome_id" - local aghome_state_pid_file="$aghome_state_dir/$aghome_id.pid" - local aghome_state_log_file="$aghome_state_dir/$aghome_id.log" - local aghome_service_file - if is-mac; then - aghome_service_file="$SERVICE_DIR/$aghome_id.plist" # @todo unknown - else - aghome_service_file="$SERVICE_DIR/$aghome_id.service" - fi - local aghome_bin_cmd=( - "$aghome_bin_file" - '--config' "$aghome_conf_file" - '--work-dir' "$aghome_data_dir" - '--pidfile' "$aghome_state_pid_file" - '--logfile' "$aghome_state_log_file" - ) - function __aghome_exists { - test -x "$aghome_bin_file" - } - function aghome_install { - local action action_title temp_bin_file - action='install' - if __aghome_exists; then - action='upgrade' - fi - action_title="$(__uppercase_first_letter "$action")" + # DNS Services - # check - if test -z "$aghome_installer"; then - die_incompatible_service "$aghome_title" - fi + local arch + arch="$(get-arch)" - # log - echo-style --h1="$action_title $aghome_title" + local system_dns__title='System DNS' + + local adguard_home__asset_url adguard_home__asset_file adguard_home__archive_filter='AdGuardHome/AdGuardHome' + local adguard_home__title="AdGuard Home" adguard_home__bin_id='AdGuardHome' + local adguard_home__bin_file="$BIN_DIR/$adguard_home__bin_id" + local adguard_home__conf_dir="$CONF_DIR/$adguard_home__bin_id" + local adguard_home__conf_file="$adguard_home__conf_dir/$adguard_home__bin_id.yaml" + local adguard_home__data_dir="$DATA_DIR/$adguard_home__bin_id" + local adguard_home__state_dir="$STATE_DIR/$adguard_home__bin_id" + local adguard_home__state_pid_file="$adguard_home__state_dir/$adguard_home__bin_id.pid" + local adguard_home__state_log_file="$adguard_home__state_dir/$adguard_home__bin_id.log" + local adguard_home__bin_cmd=( + # https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#command-line + "$adguard_home__bin_file" + '--config' "$adguard_home__conf_file" + '--work-dir' "$adguard_home__data_dir" + '--pidfile' "$adguard_home__state_pid_file" + '--logfile' "$adguard_home__state_log_file" + ) + local adguard_home__service_file + + local cloudflared__slug='cloudflare/cloudflared' cloudflared__asset_regexp cloudflared__archive_filter='' + local cloudflared__title='Cloudflared' cloudflared__bin_id='cloudflared' cloudflared__brew_id='cloudflared' + local cloudflared__bin_file="$BIN_DIR/$cloudflared__bin_id" + local cloudflared__proxy_service_id cloudflared__proxy_service_file + + local dnscrypt_proxy__slug='DNSCrypt/dnscrypt-proxy' dnscrypt_proxy__asset_regexp dnscrypt_proxy__archive_filter='**/dnscrypt-proxy' + local dnscrypt_proxy__title='DNSCrypt Proxy' dnscrypt_proxy__bin_id='dnscrypt-proxy' dnscrypt_proxy__brew_id='dnscrypt-proxy' + local dnscrypt_proxy__bin_file="$BIN_DIR/$dnscrypt_proxy__bin_id" + local dnscrypt_proxy__conf_dir="$CONF_DIR/$dnscrypt_proxy__bin_id" + local dnscrypt_proxy__conf_id="$dnscrypt_proxy__bin_id.toml" dnscrypt_proxy__conf_pathname='dnscrypt-proxy/example-dnscrypt-proxy.toml' + local dnscrypt_proxy__conf_file="$dnscrypt_proxy__conf_dir/$dnscrypt_proxy__conf_id" + local dnscrypt_proxy__bin_cmd=( + "$dnscrypt_proxy__bin_file" + '-config' "$dnscrypt_proxy__conf_file" + ) + local dnscrypt_proxy__service_file - # check we are the right one - check_installation "$aghome_id" "$aghome_bin_file" + if is-mac; then + # launchctl + adguard_home__service_file="$SERVICE_DIR/$adguard_home__bin_id.plist" + cloudflared__archive_filter='cloudflared' + cloudflared__proxy_service_id='com.cloudflare.cloudflared-proxy' + cloudflared__proxy_service_file="$SERVICE_DIR/$cloudflared__proxy_service_id.plist" + dnscrypt_proxy__service_file="$SERVICE_DIR/$dnscrypt_proxy__bin_id.plist" + if test "$arch" = 'a64'; then + adguard_home__asset_file='AdGuardHome_darwin_arm64.zip' + cloudflared__asset_regexp='cloudflared-darwin-arm64[.]tgz$' + dnscrypt_proxy__asset_regexp='dnscrypt-proxy-macos_arm64.*?[.]zip$' + elif test "$arch" = 'x64'; then + adguard_home__asset_file='AdGuardHome_darwin_amd64.zip' + cloudflared__asset_regexp='cloudflared-darwin-amd64[.]tgz$' + dnscrypt_proxy__asset_regexp='dnscrypt-proxy-macos_x86_64.*?[.]zip$' + fi + elif is-linux; then + # systemctl / resolvectl / systemd-resolved + adguard_home__service_file="$SERVICE_DIR/$adguard_home__bin_id.service" + cloudflared__proxy_service_id='cloudflared-proxy' + cloudflared__proxy_service_file="$SERVICE_DIR/$cloudflared__proxy_service_id.service" + dnscrypt_proxy__service_file="$SERVICE_DIR/$dnscrypt_proxy__bin_id.service" + if test "$arch" = 'a64'; then + adguard_home__asset_file='AdGuardHome_linux_arm64.tar.gz' + cloudflared__asset_regexp='cloudflared-linux-arm64$' + dnscrypt_proxy__asset_regexp='dnscrypt-proxy-linux_arm64.*?[.]zip$' + elif test "$arch" = 'x64'; then + adguard_home__asset_file='AdGuardHome_linux_amd64.tar.gz' + cloudflared__asset_regexp='cloudflared-linux-amd64$' + dnscrypt_proxy__asset_regexp='dnscrypt-proxy-linux_x86_64.*?[.]zip$' + elif test "$arch" = 'x32'; then + adguard_home__asset_file='AdGuardHome_linux_386.tar.gz' + cloudflared__asset_regexp='cloudflared-linux-386$' + dnscrypt_proxy__asset_regexp='dnscrypt-proxy-linux_i386.*?[.]zip$' + fi + else + echo-error 'Unknown Operating System' + return 19 # ENODEV 19 Operation not supported by device + fi + adguard_home__asset_url="https://static.adguard.com/adguardhome/release/${adguard_home__asset_file-}" + + function die_incompatible_dns_service { + local service_title="${item#*=}" + echo-style --error1='This platform architecture ' --code-error1="$arch" --error1=' does not yet support: ' --code-error1="$service_title" >/dev/stderr + return 19 # ENODEV 19 Operation not supported by device + } + + # ===================================== + # DNS Service: AdGuard Home + + function __adguard_home__available { + test -n "${adguard_home__asset_file-}" + } + function __adguard_home__installed { + test -x "$adguard_home__bin_file" + } + function adguard_home__install { + local action action_title temp__bin_file + if __adguard_home__installed; then + action='upgrade' + action_title='Stop & Upgrade' + else + action='install' + action_title='Install' + fi + + # check + if ! __adguard_home__available; then + die_incompatible_dns_service "$adguard_home__title" + return $? + fi + + # log + echo-style --h2="$action_title $adguard_home__title" # ensure directories - sudo-helper -- mkdir -p "$aghome_conf_dir" "$aghome_data_dir" "$aghome_state_dir" + echo-mkdir --sudo --quiet -- "$adguard_home__conf_dir" "$adguard_home__data_dir" "$adguard_home__state_dir" # download the installer, prior to disabling - temp_bin_file="$( + temp__bin_file="$( fs-temp \ --directory='setup-dns' \ - --file='AdGuardHome' + --file="$adguard_home__bin_id" )" - down "https://static.adguard.com/adguardhome/release/${aghome_installer}" \ - --archive-glob="AdGuardHome/AdGuardHome" \ - --filepath="$temp_bin_file" || : - # ^ allow failures, in case dns is botched - - # if downloaded, stop and install - if test -f "$temp_bin_file"; then - if test -x "$aghome_bin_file"; then - # only tell prior cmd to stop if the prior cmd exists - sudo-helper -- "${aghome_bin_cmd[@]}" --service stop || : - fi - sudo-helper -- mv "$temp_bin_file" "$aghome_bin_file" || : - sudo-helper -- chmod +x "$aghome_bin_file" || : - # ^ allow failure,s as we will check this later - fi + down "$adguard_home__asset_url" \ + --archive-glob="$adguard_home__archive_filter" \ + --filepath="$temp__bin_file" - # confirm success - if test -x "$aghome_bin_file"; then - echo-style --g1="$action_title $aghome_title" - else - echo-style --error="Unable to make executable: $aghome_bin_file" >/dev/stderr - echo-style --e1="$action_title $aghome_title" - return 1 + # now that we have downloaded everything, uninstall any unsupported installations, and check there are no remaining unsupported installations + __fail_if_dns_service_is_installed_but_not_intended "$adguard_home__bin_id" "$adguard_home__bin_file" + + # now that we know if there is an installation, it is supported, check if it is installed, if so stop it + if __adguard_home__installed; then + sudo-helper -- "${adguard_home__bin_cmd[@]}" --service stop || : fi + + # now that the service is intended, installed, and stopped, install/upgrade it + sudo-helper -- mv "$temp__bin_file" "$adguard_home__bin_file" || : + sudo-helper -- chmod +x "$adguard_home__bin_file" || : + # ^ allow failure,s as we will check this later + + # now check the install/upgrade worked + __fail_if_dns_serice_is_not_installed "$adguard_home__bin_id" "$adguard_home__bin_file" + + # success + echo-style --g2="$action_title $adguard_home__title" } - function aghome_uninstall { + function adguard_home__uninstall { # check - if ! __aghome_exists; then + if ! __adguard_home__installed; then return 0 fi # log - echo-style --h1="Uninstall $aghome_title" + echo-style --h2="Uninstall $adguard_home__title" # stop and uninstall the service - sudo-helper -- "${aghome_bin_cmd[@]}" --service stop || : - sudo-helper -- "${aghome_bin_cmd[@]}" --service uninstall || : + sudo-helper -- "${adguard_home__bin_cmd[@]}" --service stop || : + sudo-helper -- "${adguard_home__bin_cmd[@]}" --service uninstall || : # ensure adguard home has stopped - killall "$aghome_id" || : + killall "$adguard_home__bin_id" || : # clean it all up - do_remove --reload --service="$aghome_id" -- "$aghome_bin_file" "$aghome_conf_dir" "$aghome_data_dir" "$aghome_state_dir" "$aghome_service_file" + dns_service_remove --reload --service="$adguard_home__bin_id" -- "$adguard_home__bin_file" "$adguard_home__conf_dir" "$adguard_home__data_dir" "$adguard_home__state_dir" "$adguard_home__service_file" # log - echo-style --g1="Uninstall $aghome_title" + echo-style --g2="Uninstall $adguard_home__title" } - function aghome_configure { + function adguard_home__configure { local action action_title upstream_servers server pattern replace action="$1" # enable/disable action_title="$(__uppercase_first_letter "$action")" # check - if ! __aghome_exists; then + if ! __adguard_home__installed; then return 0 fi # log - echo-style --h1="Configure and $action_title $aghome_title" + echo-style --h2="$action_title $adguard_home__title" # this is here, because we need to seed the configuration file before we can do changes to it - if test ! -f "$aghome_conf_file" -a "$action" = 'enable'; then - sudo-helper -- "${aghome_bin_cmd[@]}" --service install || : - sudo-helper -- "${aghome_bin_cmd[@]}" --service start || : - confirm --ppid=$$ -- "Press <enter> once you have completed the initial $aghome_title setup..." + if test ! -s "$adguard_home__conf_file" -a "$action" = 'enable'; then + sudo-helper -- "${adguard_home__bin_cmd[@]}" --service install || : + sudo-helper -- "${adguard_home__bin_cmd[@]}" --service start || : + confirm --ppid=$$ -- "Press <enter> once you have completed the initial $adguard_home__title setup at http://127.0.0.1:3000" fi # stop before config update - sudo-helper -- "${aghome_bin_cmd[@]}" --service stop || : + sudo-helper -- "${adguard_home__bin_cmd[@]}" --service stop || : # prepare desired providers upstream_servers=( @@ -1093,321 +792,114 @@ function setup_dns() ( replace+=" - $server"$'\n' done sudo-helper --inherit \ - -- sd "$pattern" "$replace" "$aghome_conf_file" + -- sd "$pattern" "$replace" "$adguard_home__conf_file" sudo-helper -- \ - "${aghome_bin_cmd[@]}" --check-config + "${adguard_home__bin_cmd[@]}" --check-config # enable or disable # Service control action: status, install, uninstall, start, stop, restart, reload (configuration). if test "$action" = 'enable'; then - sudo-helper -- "${aghome_bin_cmd[@]}" --service install || : - sudo-helper -- "${aghome_bin_cmd[@]}" --service reload || sudo-helper -- "${aghome_bin_cmd[@]}" --service start || : - sudo-helper -- "${aghome_bin_cmd[@]}" --service status || : + sudo-helper -- "${adguard_home__bin_cmd[@]}" --service install || : + sudo-helper -- "${adguard_home__bin_cmd[@]}" --service reload || : + sudo-helper -- "${adguard_home__bin_cmd[@]}" --service start || : + sudo-helper -- "${adguard_home__bin_cmd[@]}" --service status || : else - sudo-helper -- "${aghome_bin_cmd[@]}" --service uninstall || : + sudo-helper -- "${adguard_home__bin_cmd[@]}" --service uninstall || : fi # log - echo-style --g1="Configure and $action_title $aghome_title" + echo-style --g2="$action_title $adguard_home__title" } # ===================================== - # DNS Service: Custom: DNSCrypt Proxy - # https://github.com/DNSCrypt/dnscrypt-proxy/wiki/Installation-linux - - local dnscrypt_title='DNSCrypt Proxy' - local dnscrypt_id='dnscrypt-proxy' - local dnscrypt_brew_id="$dnscrypt_id" - local dnscrypt_bin_file="${BIN_DIR}/$dnscrypt_id" - local dnscrypt_conf_dir="${CONF_DIR}/$dnscrypt_id" - local dnscrypt_conf_file="$dnscrypt_conf_dir/$dnscrypt_id.toml" - local dnscrypt_bin_cmd=( - "$dnscrypt_bin_file" - '-config' "$dnscrypt_conf_file" - ) - local dnscrypt_service_id dnscrypt_service_file - if is-mac; then - # launchctl - dnscrypt_service_id="$dnscrypt_id" # @todo assumed - dnscrypt_service_file="$SERVICE_DIR/$dnscrypt_service_id.plist" - else - # systemctl - dnscrypt_service_id="$dnscrypt_id" - dnscrypt_service_file="$SERVICE_DIR/$dnscrypt_service_id.service" - fi - function __dnscrypt_exists { - test -x "$dnscrypt_bin_file" - } - function dnscrypt_install { - local action action_title temp_bin_file - action='install' - if __dnscrypt_exists; then - action='upgrade' - fi - action_title="$(__uppercase_first_letter "$action")" - - # check - if test -z "$dnscrypt_installer"; then - die_incompatible_service "$dnscrypt_title" - fi - - # prepare and log - echo-style --h1="$action_title $dnscrypt_title" - - # download the upgrade, prior to disabling - temp_bin_file="$( - fs-temp \ - --directory='setup-dns' \ - --file='dnscrypt-proxy' - )" - github-download \ - --slug='DNSCrypt/dnscrypt-proxy' \ - --latest \ - --asset-regexp="$dnscrypt_installer" \ - --archive-glob='**/dnscrypt-proxy' \ - --filepath="$temp_bin_file" - - # don't use brew for this, as we want complete control - brew uninstall "$dnscrypt_brew_id" &>/dev/null || : - check_installation "$dnscrypt_id" "$dnscrypt_bin_file" - - # ensure directories - sudo-helper -- mkdir -p "$dnscrypt_conf_dir" - - # if downloaded, stop and install - if test -f "$temp_bin_file"; then - if test -x "$dnscrypt_bin_file"; then - # only tell prior cmd to stop if the prior cmd exists - sudo-helper -- "${dnscrypt_bin_cmd[@]}" --service stop || : - fi - sudo-helper -- mv "$temp_bin_file" "$dnscrypt_bin_file" || : - sudo-helper -- chmod +x "$dnscrypt_bin_file" || : - # ^ allow failure,s as we will check this later - fi - - # download the configuration if it doesn't exist - # https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-dnscrypt-proxy.toml - if test ! -f "$dnscrypt_conf_file"; then - temp_conf_file="$( - fs-temp \ - --directory='setup-dns' \ - --file='dnscrypt-proxy.toml' - )" - github-download \ - --slug='DNSCrypt/dnscrypt-proxy' \ - --head \ - --pathname='dnscrypt-proxy/example-dnscrypt-proxy.toml' \ - --filepath="$temp_conf_file" - sudo-helper -- mv "$temp_conf_file" "$dnscrypt_conf_file" - fi - - # confirm success - if test -x "$dnscrypt_bin_file"; then - echo-style --g1="$action_title $dnscrypt_title" - else - echo-style --error="Unable to make executable: $dnscrypt_bin_file" >/dev/stderr - echo-style --e1="$action_title $dnscrypt_title" - return 1 - fi - } - function dnscrypt_uninstall { - # check - if ! __dnscrypt_exists; then - return 0 - fi - - # log - echo-style --h1="Uninstall $dnscrypt_title" - - # stop and uninstall the service - sudo-helper -- "${dnscrypt_bin_cmd[@]}" --service stop || : - sudo-helper -- "${dnscrypt_bin_cmd[@]}" --service uninstall || : - - # ensure adguard home has stopped - killall "$dnscrypt_id" || : - - # clean it all up - do_remove --reload --service="$dnscrypt_id" -- "$dnscrypt_bin_file" "$dnscrypt_conf_dir" "$dnscrypt_service_file" - - # log - echo-style --g1="Uninstall $dnscrypt_title" - } - function dnscrypt_configure { - local action action_title temp_conf_file dnscrypt_options - action="$1" # enable/disable - action_title="$(__uppercase_first_letter "$action")" - - # check - if ! __dnscrypt_exists; then - return 0 - fi - - # log - echo-style --h1="Configure and $action_title $dnscrypt_title" - - # if the configuration doesn't exist - if test ! -f "$dnscrypt_conf_file"; then - # then give up, as the internet is disabled in this mode - # as we deactivated the prior service - echo-style --error="Missing configuration file: $dnscrypt_conf_file" >/dev/stderr - echo-style --warning='You should attempt reinstallation then try again.' >/dev/stderr - echo-style --e1="Configure and $action_title $dnscrypt_title" - fi - - # if [dnscrypt_names] is empty, get the user to decide - # but only go through the trouble if we are actually intending - # to use dns-crypt - if test "${#dnscrypt_names[@]}" -eq 0 -a "$action" = 'enable'; then - mapfile -t dnscrypt_options < <( - # trunk-ignore(shellcheck/SC2016) - fetch 'https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md' | echo-regexp -ongm --regexp='^## (.+)$' --replace='$1' - ) - mapfile -t dnscrypt_names < <( - choose --required --multi --question="Which DNSCrypt Server names do you wish to use?" -- - "${dnscrypt_options[@]}" - ) - fi - - # stop before config update - sudo-helper -- "${dnscrypt_bin_cmd[@]}" --service stop || : - sudo-helper -- "${dnscrypt_bin_cmd[@]}" --service disable || : - - # update the configuration with the new [dnscrypt_names], if any - if test "${#dnscrypt_names[@]}" -ne 0; then - sudo-helper --inherit \ - -- config-helper --file="$dnscrypt_conf_file" -- \ - --field='ipv4_servers' --no-quote --value='true' \ - --field='ipv6_servers' --no-quote --value='true' \ - --field='dnscrypt_servers' --no-quote --value='true' \ - --field='doh_servers' --no-quote --value='true' \ - --field='server_names' --no-quote --value="[$( - echo-join ', ' -- "${dnscrypt_names[@]@Q}" - )]" - sudo-helper -- "${dnscrypt_bin_cmd[@]}" --check -config "$dnscrypt_conf_file" - fi + # DNS Service: Cloudflared Proxy - # enable or disable - if test "$action" = 'enable'; then - sudo-helper -- "${dnscrypt_bin_cmd[@]}" --service install || : - sudo-helper -- "${dnscrypt_bin_cmd[@]}" --service enable || : - sudo-helper -- "${dnscrypt_bin_cmd[@]}" --service start || : - else - sudo-helper -- "${dnscrypt_bin_cmd[@]}" --service stop || : - sudo-helper -- "${dnscrypt_bin_cmd[@]}" --service disable || : - sudo-helper -- "${dnscrypt_bin_cmd[@]}" --service uninstall || : - fi - - # log - echo-style --g1="Configure and $action_title $dnscrypt_title" + function __cloudflared__available { + test -n "${cloudflared__asset_regexp-}" } - - # ===================================== - # DNS Service: Custom: Cloudflare Warp - - # doesn't yet support any of the platforms I use, so no support in here yet - - # ===================================== - # DNS Service: Custom: Cloudflared Proxy - - local cloudflared_title='Cloudflared' - local cloudflared_id='cloudflared' - local cloudflared_brew_id='cloudflare/cloudflare/cloudflared' - local cloudflared_bin_file="$BIN_DIR/$cloudflared_id" - local cloudflared_proxy_service_id - local cloudflared_proxy_service_file - if is-mac; then - # launchctl - cloudflared_proxy_service_id='com.cloudflare.cloudflared-proxy' - cloudflared_proxy_service_file="$SERVICE_DIR/$cloudflared_proxy_service_id.plist" - else - # systemctl - cloudflared_proxy_service_id='cloudflared-proxy' - cloudflared_proxy_service_file="$SERVICE_DIR/$cloudflared_proxy_service_id.service" - fi - - function __cloudflared_exists { - test -x "$cloudflared_bin_file" + function __cloudflared__installed { + test -x "$cloudflared__bin_file" } - function cloudflared_install { - local action action_title temp_bin_file - action='install' # install/upgrade - if __cloudflared_exists; then + function cloudflared__install { + local action action_title temp__bin_file + if __cloudflared__installed; then action='upgrade' + action_title='Stop & Upgrade' + else + action='install' + action_title='Install' fi - action_title="$(__uppercase_first_letter "$action")" # check - if test -z "$cloudflared_installer"; then - die_incompatible_service "$cloudflared_title" + if ! __cloudflared__available; then + die_incompatible_dns_service "$cloudflared__title" + return $? fi # log - echo-style --h1="$action_title $cloudflared_title" + echo-style --h2="$action_title $cloudflared__title" # download the upgrade, prior to disabling # setup-util --cli=cloudflared APT_KEY='https://pkg.cloudflare.com/cloudflare-main.gpg' APT_REPO='deb [arch={ARCH} signed-by={KEY}] https://pkg.cloudflare.com/cloudflared jammy main' APT='cloudflared' # setup-util --cli=cloudflared APT_KEY='https://pkg.cloudflare.com/cloudflare-main.gpg' APT_REPO='deb [arch={ARCH} signed-by={KEY}] https://pkg.cloudflare.com/cloudflared {RELEASE} main' APT='cloudflared' - temp_bin_file="$( + temp__bin_file="$( fs-temp \ --directory='setup-dns' \ - --file='cloudflared' + --file="$cloudflared__bin_id" )" github-download \ - --slug='cloudflare/cloudflared' \ + --slug="$cloudflared__slug" \ --latest \ - --asset-regexp="$cloudflared_installer" \ - --archive-glob="$cloudflared_archive_filter" \ - --filepath="$temp_bin_file" + --asset-regexp="$cloudflared__asset_regexp" \ + --archive-glob="$cloudflared__archive_filter" \ + --filepath="$temp__bin_file" + + # now that we have downloaded everything, uninstall any unsupported installations, and check there are no remaining unsupported installations + if is-brew; then + # don't use brew for this, as we want complete control + brew uninstall "$cloudflared__brew_id" &>/dev/null || : + fi + __fail_if_dns_service_is_installed_but_not_intended "$cloudflared__bin_id" "$cloudflared__bin_file" - # disable/uninstall the service if it exists - service_disable "$cloudflared_proxy_service_file" + # now that we know if there is an installation, it is supported, check if it is installed, if so stop it + if __cloudflared__installed; then + dns_service_stop "$cloudflared__proxy_service_file" + fi - # don't use brew for this, as we want complete control - brew uninstall "$cloudflared_brew_id" &>/dev/null || : - check_installation "$cloudflared_id" "$cloudflared_bin_file" + # now that the service is intended, installed, and stopped, install/upgrade it + sudo-helper -- mv "$temp__bin_file" "$cloudflared__bin_file" + sudo-helper -- chmod +x "$cloudflared__bin_file" - # if downloaded, stop and install - if test -f "$temp_bin_file"; then - service_stop "$cloudflared_proxy_service_file" - sudo-helper -- mv "$temp_bin_file" "$cloudflared_bin_file" - sudo-helper -- chmod +x "$cloudflared_bin_file" - fi + # now check the install/upgrade worked + __fail_if_dns_serice_is_not_installed "$cloudflared__bin_id" "$cloudflared__bin_file" - # confirm success - if test -x "$cloudflared_bin_file"; then - echo-style --g1="$action_title $cloudflared_title" - else - echo-style --error="Unable to make executable: $cloudflared_bin_file" >/dev/stderr - echo-style --e1="$action_title $cloudflared_title" - return 1 - fi + # success + echo-style --g2="$action_title $cloudflared__title" } - function cloudflared_uninstall { + function cloudflared__uninstall { # check - if ! __cloudflared_exists; then + if ! __cloudflared__installed; then return 0 fi # log - echo-style --h1="Uninstall $cloudflared_title" + echo-style --h2="Uninstall $cloudflared__title" # stop, disable, uninstall the service if it exists - service_disable "$cloudflared_proxy_service_file" + dns_service_disable_and_stop "$cloudflared__proxy_service_file" - # if we are using tunnels - if test "${#CLOUDFLARED_TUNNELS[@]}" -ne 0; then - # only remove the service file - # as tunnels uses the same binary - do_remove --reload -- "$cloudflared_proxy_service_file" - else - # ensure everything related to the proxy is removed - do_remove --reload --service="$cloudflared_id" -- "$cloudflared_proxy_service_file" - fi + # # if we are using tunnels + # if test "${#CLOUDFLARED__TUNNELS[@]}" -ne 0; then + # # only remove the service file, as tunnels uses the same binary + # dns_service_remove --reload -- "$cloudflared__proxy_service_file" + # else + # ensure everything related to the proxy is removed + dns_service_remove --reload --service="$cloudflared__bin_id" -- "$cloudflared__proxy_service_file" # log - echo-style --g1="Uninstall $cloudflared_title" + echo-style --g2="Uninstall $cloudflared__title" } - function cloudflared_configure { + function cloudflared__configure { local action action_title upstream_servers upstream_section upstream_args server action="$1" # enable/disable action_title="$(__uppercase_first_letter "$action")" @@ -1415,24 +907,27 @@ function setup_dns() ( upstream_args='' # check - if ! __cloudflared_exists; then + if ! __cloudflared__installed; then return 0 fi # log - echo-style --h1="Configure and $action_title $cloudflared_title" + echo-style --h2="$action_title $cloudflared__title" # check - check_installation "$cloudflared_id" "$cloudflared_bin_file" + __fail_if_dns_service_is_installed_but_not_intended "$cloudflared__bin_id" "$cloudflared__bin_file" # stop, disable, uninstall the old service if it exists - service_disable "$cloudflared_proxy_service_file" + dns_service_disable_and_stop "$cloudflared__proxy_service_file" # only update the configuration, if we are [enable] action # as the SERVICE IS THE CONFIGURATION for [cloudflared proxy-dns] # as it doesn't support a configuration file # so we have to configure it via CLI args in the service definition if test "$action" = 'enable'; then + # check it is installed + __fail_if_dns_serice_is_not_installed "$cloudflared__bin_id" "$cloudflared__bin_file" + # prepare upstreams (despite docs, cloudflared doesn't support tls) upstream_servers=( "${doh_servers[@]}" @@ -1456,24 +951,24 @@ function setup_dns() ( # create service with custom upstreams # <string>--address</string> # <string>0.0.0.0</string> - sudo-helper -- tee "$cloudflared_proxy_service_file" >/dev/null <<EOF + sudo-helper -- tee "$cloudflared__proxy_service_file" >/dev/null <<EOF <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> - <string>${cloudflared_proxy_service_id}</string> + <string>${cloudflared__proxy_service_id}</string> <key>ProgramArguments</key> <array> - <string>${cloudflared_bin_file}</string> + <string>${cloudflared__bin_file}</string> <string>proxy-dns</string>${upstream_section} </array> <key>RunAtLoad</key> <true/> <key>StandardOutPath</key> - <string>/Library/Logs/${cloudflared_proxy_service_id}.out.log</string> + <string>${LOGS_DIR}/${cloudflared__proxy_service_id}.out.log</string> <key>StandardErrorPath</key> - <string>/Library/Logs/${cloudflared_proxy_service_id}.err.log</string> + <string>${LOGS_DIR}/${cloudflared__proxy_service_id}.err.log</string> <key>KeepAlive</key> <dict> <key>SuccessfulExit</key> @@ -1492,9 +987,9 @@ EOF done # create service with custom upstreams - sudo-helper -- tee "$cloudflared_proxy_service_file" >/dev/null <<-EOF + sudo-helper -- tee "$cloudflared__proxy_service_file" >/dev/null <<-EOF [Unit] - Description=${cloudflared_proxy_service_id} + Description=${cloudflared__proxy_service_id} Wants=network-online.target nss-lookup.target Before=nss-lookup.target @@ -1502,7 +997,7 @@ EOF AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE DynamicUser=yes - ExecStart=${cloudflared_bin_file} proxy-dns --address 0.0.0.0 ${upstream_args} + ExecStart=${cloudflared__bin_file} proxy-dns --address 0.0.0.0 ${upstream_args} [Install] WantedBy=multi-user.target @@ -1510,411 +1005,756 @@ EOF fi # enable the service - service_enable "$cloudflared_proxy_service_file" + dns_service_enable "$cloudflared__proxy_service_file" fi # log - echo-style --g1="Configure and $action_title $cloudflared_title" + echo-style --g2="$action_title $cloudflared__title" } # ===================================== - # DNS Service: Special: Cloudflared Tunnels + # DNS Service: DNSCrypt Proxy + # https://github.com/DNSCrypt/dnscrypt-proxy/wiki/Installation-linux - local tunnel_user_dir="$HOME/.cloudflared" - local tunnel_user_pem="$tunnel_user_dir/cert.pem" - # ^ cloudflared always places login cert here - local tunnel_conf_dir="$CONF_DIR/$cloudflared_id" - # ^ store tunnel data into cloudflared conf dir - # ^ ironically, [cloudflared proxy-dns] doesn't actually have conf - # ^ only the tunnel stuff does + function validate_dnscrypt_names { + local dnscrypt_names_file available_dnscrypt_names=() a b found - function tunnel_install { - # ensure cloudflared is installed - if ! __cloudflared_exists; then - cloudflared_install + # prep the cache file + dnscrypt_names_file="$( + fs-temp \ + --directory='setup-dns' \ + --file='dnscrypt-names' + )" + + # write the names to the file if it is empty + if test ! -s "$dnscrypt_names_file"; then + # trunk-ignore(shellcheck/SC2016) + fetch 'https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md' | echo-regexp -ongm --regexp='^## (.+)$' --replace='$1' >"$dnscrypt_names_file" fi - # make tunnel directories - mkdir -p "$tunnel_user_dir" - sudo-helper -- mkdir -p "$tunnel_conf_dir" + # read the file into an array + mapfile -t available_dnscrypt_names <"$dnscrypt_names_file" + if test "${#dnscrypt_names[@]}" -ne 0; then + for a in "${dnscrypt_names[@]}"; do + found='no' + for b in "${available_dnscrypt_names[@]}"; do + if test "$a" = "$b"; then + found='yes' + break + fi + done + if test "$found" = 'no'; then + echo-style --error1='Invalid dnscrypt-proxy configuration, the dnscrypt-name ' --code-error1="$a" --error1=' is invalid. Find a list of valid names at: ' --code-error1="$dnscrypt_names_file" >/dev/stderr + return 3 # ESRCH 3 No such process + fi + done + elif test -n "${provider-}"; then + echo-style --error1='Invalid dnscrypt-proxy configuration, no dnscrypt-names were provided for the provider ' --code-error1="$provider" >/dev/stderr + return 6 # ENXIO 6 Device not configured + fi + + # success + return 0 } - function tunnel_uninstall { - local service_file + function __dnscrypt_proxy__available { + test -n "${dnscrypt_proxy__asset_regexp-}" + } + function __dnscrypt_proxy__installed { + test -x "$dnscrypt_proxy__bin_file" + } + function dnscrypt_proxy__install { + local action action_title temp__bin_file + if __dnscrypt_proxy__installed; then + action='upgrade' + action_title='Stop & Upgrade' + else + action='install' + action_title='Install' + fi - # disable all the tunnels - for service_file in "$SERVICE_DIR/"*'cloudflared-tunnel'*; do - service_disable "$service_file" - done + # check + if ! __dnscrypt_proxy__available; then + die_incompatible_dns_service "$dnscrypt_proxy__title" + return $? + fi + + # prepare and log + echo-style --h2="$action_title $dnscrypt_proxy__title" + + # ensure directories + echo-mkdir --sudo --quiet -- "$dnscrypt_proxy__conf_dir" + + # validate dnscrypt names + validate_dnscrypt_names + + # download the upgrade, prior to disabling + temp__bin_file="$( + fs-temp \ + --directory='setup-dns' \ + --file="$dnscrypt_proxy__bin_id" + )" + github-download \ + --slug="$dnscrypt_proxy__slug" \ + --latest \ + --asset-regexp="$dnscrypt_proxy__asset_regexp" \ + --archive-glob="$dnscrypt_proxy__archive_filter" \ + --filepath="$temp__bin_file" + + # download the configuration if it doesn't exist + # https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-dnscrypt-proxy.toml + if test ! -s "$dnscrypt_proxy__conf_file"; then + temp_conf_file="$( + fs-temp \ + --directory='setup-dns' \ + --file="$dnscrypt_proxy__conf_id" + )" + github-download \ + --slug="$dnscrypt_proxy__slug" \ + --head \ + --pathname="$dnscrypt_proxy__conf_pathname" \ + --filepath="$temp_conf_file" + sudo-helper -- mv "$temp_conf_file" "$dnscrypt_proxy__conf_file" + fi + + # now that we have downloaded everything, uninstall any unsupported installations, and check there are no remaining unsupported installations + if is-brew; then + brew uninstall "$dnscrypt_proxy__brew_id" &>/dev/null || : + fi + __fail_if_dns_service_is_installed_but_not_intended "$dnscrypt_proxy__bin_id" "$dnscrypt_proxy__bin_file" + + # now that we know if there is an installation, it is supported, check if it is installed, if so stop it + if __dnscrypt_proxy__installed; then + # only tell prior cmd to stop if the prior cmd exists + sudo-helper -- "${dnscrypt_proxy__bin_cmd[@]}" --service stop || : + fi + + # now that the service is intended, installed, and stopped, install/upgrade it + sudo-helper -- mv "$temp__bin_file" "$dnscrypt_proxy__bin_file" + sudo-helper -- chmod +x "$dnscrypt_proxy__bin_file" - # remove files for all tunnels - do_remove --service='cloudflared-tunnel' -- "$tunnel_user_dir" "$tunnel_conf_dir" - service_reload + # now check the install/upgrade worked + __fail_if_dns_serice_is_not_installed "$dnscrypt_proxy__bin_id" "$dnscrypt_proxy__bin_file" + + # success + echo-style --g2="$action_title $dnscrypt_proxy__title" } - function tunnel_configure_single { - local item tunnel hostnames url ingress - local tunnel_service_id tunnel_service_file tunnel_conf_file tunnel_cred_file - local hostname temp_cred_file - tunnel='' - hostnames=() - url='' - ingress='' + function dnscrypt_proxy__uninstall { + # check + if ! __dnscrypt_proxy__installed; then + return 0 + fi + + # log + echo-style --h2="Uninstall $dnscrypt_proxy__title" + + # stop and uninstall the service + sudo-helper -- "${dnscrypt_proxy__bin_cmd[@]}" --service stop || : + sudo-helper -- "${dnscrypt_proxy__bin_cmd[@]}" --service uninstall || : + + # ensure adguard home has stopped + killall "$dnscrypt_proxy__bin_id" || : + + # clean it all up + dns_service_remove --reload --service="$dnscrypt_proxy__bin_id" -- "$dnscrypt_proxy__bin_file" "$dnscrypt_proxy__conf_dir" "$dnscrypt_proxy__service_file" + + # log + echo-style --g2="Uninstall $dnscrypt_proxy__title" + } + function dnscrypt_proxy__configure { + local action action_title temp_conf_file + action="$1" # enable/disable + action_title="$(__uppercase_first_letter "$action")" # check - if ! __cloudflared_exists; then + if ! __dnscrypt_proxy__installed; then return 0 fi - # log, tunnels are always enable - echo-style --h1="Configure and Enable Cloudflared Tunnel" + # log + echo-style --h2="$action_title $dnscrypt_proxy__title" + + # validate dnscrypt names + validate_dnscrypt_names - # process args - while test "$#" -ne 0; do - item="$1" - shift - case "$item" in - '--tunnel='*) tunnel="${item#*=}" ;; - '--hostname='*) hostnames+=("${item#*=}") ;; - '--url='*) url="${item#*=}" ;; - '--ingress='*) ingress="${item#*=}" ;; - --) break ;; - *) - echo-style --error="Unknown tunnel argument: $item" >/dev/stderr - return 22 # EINVAL 22 Invalid argument - ;; - esac - done + # if the configuration doesn't exist + if test ! -s "$dnscrypt_proxy__conf_file"; then + # then give up, as the internet is disabled in this mode + # as we deactivated the prior service + echo-style --error1='Missing configuration file: ' --code-error1="$dnscrypt_proxy__conf_file" >/dev/stderr + echo-style --notice1='You should attempt reinstallation then try again.' >/dev/stderr + echo-style --e2="$action_title $dnscrypt_proxy__title" + fi - # ask if required args missing - if test -z "$tunnel"; then - tunnel="$( - ask --required --linger \ - --question="What will be name identifier of the tunnel?" - )" + # stop the service if it is installed, before config update + if test -s "$dnscrypt_proxy__service_file"; then + sudo-helper -- "${dnscrypt_proxy__bin_cmd[@]}" --service stop || : fi - if test "${#hostnames[@]}" -eq 0; then - hostnames+=( - "$( - ask --required --linger \ - --question="What will be the hostname to access the tunnel? E.g. ${tunnel}.domain.com" - )" - ) + + # update the configuration with the new [dnscrypt_names], if any + if test "${#dnscrypt_names[@]}" -ne 0; then + sudo-helper --inherit \ + -- config-helper --file="$dnscrypt_proxy__conf_file" -- \ + --field='ipv4_servers' --no-quote --value='true' \ + --field='ipv6_servers' --no-quote --value='true' \ + --field='dnscrypt_servers' --no-quote --value='true' \ + --field='doh_servers' --no-quote --value='true' \ + --field='server_names' --no-quote --value="[$( + echo-quote -- "${dnscrypt_names[@]}" | echo-join ', ' --stdin + )]" + sudo-helper -- "${dnscrypt_proxy__bin_cmd[@]}" --check -config "$dnscrypt_proxy__conf_file" fi - if test -z "$ingress"; then - if test -z "$url"; then - url="$( - ask --required --linger \ - --question="What will be local URL the tunnel will expose?" - )" - fi - ingress="url: $url" + + # enable or disable + if test "$action" = 'enable'; then + sudo-helper -- "${dnscrypt_proxy__bin_cmd[@]}" --service uninstall || : # Needed before install, otherwise: Failed to install DNSCrypt client proxy: Init already exists: /Library/LaunchDaemons/dnscrypt-proxy.plist + sudo-helper -- "${dnscrypt_proxy__bin_cmd[@]}" --service install || : + sudo-helper -- "${dnscrypt_proxy__bin_cmd[@]}" --service start || : + elif test -s "$dnscrypt_proxy__service_file"; then + sudo-helper -- "${dnscrypt_proxy__bin_cmd[@]}" --service stop || : + sudo-helper -- "${dnscrypt_proxy__bin_cmd[@]}" --service uninstall || : fi - # prepare specific files - if is-mac; then - # launchctl - tunnel_service_id="com.cloudflare.cloudflared-tunnel-$tunnel" - tunnel_service_file="$SERVICE_DIR/$tunnel_service_id.plist" - else - # systemctl - tunnel_service_id="cloudflared-tunnel-$tunnel" - tunnel_service_file="$SERVICE_DIR/$tunnel_service_id.service" - fi - tunnel_conf_file="$tunnel_conf_dir/$tunnel_service_id-$tunnel.yml" - tunnel_cred_file="$tunnel_conf_dir/$tunnel_service_id-$tunnel.json" - - # cleanup old tunnel service if it exists - service_disable "$tunnel_service_file" - - # cleanup old login details and login again - do_remove --negative -- "$tunnel_user_pem" - eval-helper --quiet --shapeshifter \ - -- "$cloudflared_bin_file" tunnel login - chmod 600 "$tunnel_user_pem" - # creates $tunnel_user_pem no need to move it though - - # cleanup the old tunnel remote connections and registration - eval-helper --quiet \ - -- "$cloudflared_bin_file" tunnel cleanup "$tunnel" || : # cleanup connections - eval-helper --quiet \ - -- "$cloudflared_bin_file" tunnel delete "$tunnel" || : # delete tunnel - - # cleanup the old tunnel files - do_remove --reload --service="cloudflared-tunnel-$tunnel" -- "$tunnel_conf_file" "$tunnel_cred_file" - - # create the new tunnel again - temp_cred_file="$( - fs-temp --no-touch \ - --directory='setup-dns' \ - --directory="cloudflared-tunnel-$tunnel" \ - --file="$tunnel_service_id-$tunnel.json" - )" - eval-helper --quiet \ - -- "$cloudflared_bin_file" tunnel create \ - --credentials-file "$temp_cred_file" \ - "$tunnel" - chmod 660 "$temp_cred_file" - sudo-helper -- mv "$temp_cred_file" "$tunnel_cred_file" - - # route create - sudo-helper -- tee "$tunnel_conf_file" >/dev/null <<-EOF - tunnel: ${tunnel} - credentials-file: ${tunnel_cred_file} - ${ingress} - EOF + # log + echo-style --g2="$action_title $dnscrypt_proxy__title" + } + + # --------------------------------- + # Availability and Installation Detection + + local available_services_id=() installed_services_id=() - # tunnel route - for hostname in "${hostnames[@]}"; do - eval-helper --no-quiet \ - -- "$cloudflared_bin_file" tunnel route dns \ - --overwrite-dns "$tunnel" "$hostname" + function __check_dns_service_helpers { + local id="$1" fn_id + fn_id="${id//-/_}" # replace dashes with underscores + local fn fns=( + "__${fn_id}__available" + "__${fn_id}__installed" + "${fn_id}__install" + "${fn_id}__uninstall" + "${fn_id}__configure" + ) + for fn in "${fns[@]}"; do + command -v "$fn" >/dev/null || { + echo-style --error1='The DNS service ' --code-error1="$fn_id" --error1=' is missing its helper: ' --code-error1="$fn" >/dev/stderr + return 1 + } done + return 0 + } - # service create - if is-mac; then - sudo-helper -- tee "$tunnel_service_file" >/dev/null <<EOF -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> -<plist version="1.0"> - <dict> - <key>Label</key> - <string>${tunnel_service_id}</string> - <key>ProgramArguments</key> - <array> - <string>${cloudflared_bin_file}</string> - <string>tunnel</string> - <string>--config</string> - <string>${tunnel_conf_file}</string> - <string>run</string> - </array> - <key>RunAtLoad</key> - <true/> - <key>StandardOutPath</key> - <string>/Library/Logs/${tunnel_service_id}.out.log</string> - <key>StandardErrorPath</key> - <string>/Library/Logs/${tunnel_service_id}.err.log</string> - <key>KeepAlive</key> - <dict> - <key>SuccessfulExit</key> - <false/> - </dict> - <key>ThrottleInterval</key> - <integer>5</integer> - </dict> -</plist> -EOF + function fetch_available_and_installed_services { + # reset + available_services_id=() + installed_services_id=() + # update + local id fn_id + for id in "${services_id[@]}"; do + __check_dns_service_helpers "$id" + fn_id="${id//-/_}" # replace dashes with underscores + if "__${fn_id}__available"; then # eval + available_services_id+=("$id") + if "__${fn_id}__installed"; then # eval + installed_services_id+=("$id") + fi + fi + done + } + + # ===================================== + # Arguments + + # @todo move this later so that it also outputs providers + function help { + fetch_available_and_installed_services + cat <<-EOF >/dev/stderr + ABOUT: + Setup the DNS service for your system. + + USAGE: + setup-dns [...options] <service> + + OPTIONS: + --installed | --uninstalled + If provided, will check if the service is installed or uninstalled. + + --install | --uninstall + If provided, will install the service, or uninstall and disable the service. + + --enable | --disable + If provided, will install and enable the service, or disable the service. + + SERVICES: + system + $(__print_lines "${services_id[@]}") + + AVAILABLE SERVICES: + system + $(__print_lines "${available_services_id[@]}") + + INSTALLED SERVICES: + system + $(__print_lines "${installed_services_id[@]}") + + PROVIDERS: + $(__print_lines "${providers_id[@]}") + + CONFIGURATION: + DNS_SERVICE=$(echo-quote -- "$DNS_SERVICE") + DNS_PROVIDER=$(echo-quote -- "$DNS_PROVIDER") + DNS_IPV4_SERVERS=($(echo-quote -- "${DNS_IPV4_SERVERS[@]}" | echo-join --stdin)) + DNS_IPV6_SERVERS=($(echo-quote -- "${DNS_IPV6_SERVERS[@]}" | echo-join --stdin)) + DNS_DOH_SERVERS=($(echo-quote -- "${DNS_DOH_SERVERS[@]}" | echo-join --stdin)) + DNS_DOT_SERVERS=($(echo-quote -- "${DNS_DOT_SERVERS[@]}" | echo-join --stdin)) + DNS_QUIC_SERVERS=($(echo-quote -- "${DNS_QUIC_SERVERS[@]}" | echo-join --stdin)) + DNS_SDNS_SERVERS=($(echo-quote -- "${DNS_SDNS_SERVERS[@]}" | echo-join --stdin)) + DNS_DNSCRYPT_NAMES=($(echo-quote -- "${DNS_DNSCRYPT_NAMES[@]}" | echo-join --stdin)) + + CONFIGURATION FILES: + dns.bash + dns.json + EOF + if test "$#" -ne 0; then + echo-error "$@" + fi + return 22 # EINVAL 22 Invalid argument + } + + # process + local item option_installed='' option_install='' option_enable='' option_confirm='' option_service='' option_provider='' option_ipv4_servers=() option_ipv6_servers=() option_doh_servers=() option_dot_servers=() option_quic_servers=() option_sdns_servers=() option_dnscrypt_names=() + while test "$#" -ne 0; do + item="$1" + shift + case "$item" in + '--help' | '-h') help ;; + '--no-installed'* | '--installed'*) + option_installed="$(get-flag-value --affirmative --fallback="$option_installed" -- "$item")" + ;; + '--no-uninstalled'* | '--uninstalled'*) + option_installed="$(get-flag-value --non-affirmative --fallback="$option_installed" -- "$item")" + ;; + '--no-install'* | '--install'*) + option_install="$(get-flag-value --affirmative --fallback="$option_install" -- "$item")" + ;; + '--no-uninstall'* | '--uninstall'*) + option_install="$(get-flag-value --non-affirmative --fallback="$option_install" -- "$item")" + ;; + '--no-enable'* | '--enable'*) + option_enable="$(get-flag-value --affirmative --fallback="$option_enable" -- "$item")" + ;; + '--no-disable'* | '--disable'*) + option_enable="$(get-flag-value --non-affirmative --fallback="$option_enable" -- "$item")" + ;; + '--no-confirm'* | '--confirm'*) + option_confirm="$(get-flag-value --affirmative --fallback="$option_confirm" -- "$item")" + ;; + '--service='*) option_service="${item#*=}" ;; + '--provider='*) option_provider="${item#*=}" ;; + '--ipv4='*) option_ipv4_servers+=("${item#*=}") ;; + '--ipv6='*) option_ipv6_servers+=("${item#*=}") ;; + '--doh='*) option_doh_servers+=("${item#*=}") ;; + '--dot='*) option_dot_servers+=("${item#*=}") ;; + '--quic='*) option_quic_servers+=("${item#*=}") ;; + '--sdns='*) option_sdns_servers+=("${item#*=}") ;; + '--dnscrypt='*) option_dnscrypt_names+=("${item#*=}") ;; + '--'*) help "An unrecognised flag was provided: $item" ;; + *) help "An unrecognised argument was provided: $item" ;; + esac + done - # update macos service permissions - sudo-helper -- chown root:admin "$tunnel_service_file" - sudo-helper -- chmod +t "$tunnel_service_file" + # --------------------------------- + # Select DNS Provider/Servers + + function render_service_or_provider { + # prefix with an empty dim to disable the default dimming in results + local id="$1" about="$2" url="$3" + if is-value -- "$about" "$url"; then + echo-style --dim='' --bold+underline="$id" $':\n' "$about" $'\n' --dim="$url" + elif is-value -- "$about"; then + echo-style --dim='' --bold+underline="$id" $':\n' "$about" + elif is-value -- "$url"; then + echo-style --dim='' --bold+underline="$id" $':\n' --dim="$url" else - sudo-helper -- tee "$tunnel_service_file" >/dev/null <<-EOF - [Unit] - Description=${tunnel_service_id} - After=network.target + echo-style --dim='' --bold+underline="$id" + fi + } + + function select_service { + local id index about url options=() + if command-exists -- nordvpn; then + service='system' + echo-style --notice='NordVPN installation detected, only permitting system DNS service.' + else + # add system + id='system' + about="Use the system's default DNS service. Since 2018, Linux's systemd-resolved v239 includes encryption — https://github.com/systemd/systemd/blob/04c00944d8494b88b29fd432189cf575dd0de0db/NEWS#L9866-L9870 — macOS to this day still requires an alternative service/profile for encryption." + url='' + options+=( + "$id" + "$(render_service_or_provider "$id" "$about" "$url")" + ) - [Service] - TimeoutStartSec=0 - Type=notify - ExecStart=${cloudflared_bin_file} tunnel --config ${tunnel_conf_file} run - Restart=on-failure - RestartSec=5s + # render service options + for id in "${available_services_id[@]}"; do + for index in "${!services_id[@]}"; do + if test "${services_id[index]}" = "$id"; then + about="${services_about[index]}" + url="${services_url[index]}" + break + fi + done + options+=( + "$id" + "$(render_service_or_provider "$id" "$about" "$url")" + ) + done - [Install] - WantedBy=multi-user.target - EOF + # prompt the user which dns service they wish to use + service="$( + choose --required --linger --confirm="$option_confirm" --label \ + --question='Which DNS service do you wish to be your primary service?' \ + --default="${option_service:-"$DNS_SERVICE"}" -- "${options[@]}" + )" fi + } - # enable the new tunnel - service_enable "$tunnel_service_file" + function __has_arg_provider { + test \ + "${#option_ipv4_servers[@]}" -ne 0 -o \ + "${#option_ipv6_servers[@]}" -ne 0 -o \ + "${#option_doh_servers[@]}" -ne 0 -o \ + "${#option_dot_servers[@]}" -ne 0 -o \ + "${#option_quic_servers[@]}" -ne 0 -o \ + "${#option_sdns_servers[@]}" -ne 0 -o \ + "${#option_dnscrypt_names[@]}" -ne 0 + } - # log - echo-style --g1="Configure and Enable Cloudflared Tunnel" + function __has_env_provider { + test \ + "${#DNS_IPV4_SERVERS[@]}" -ne 0 -o \ + "${#DNS_IPV6_SERVERS[@]}" -ne 0 -o \ + "${#DNS_DOH_SERVERS[@]}" -ne 0 -o \ + "${#DNS_DOT_SERVERS[@]}" -ne 0 -o \ + "${#DNS_QUIC_SERVERS[@]}" -ne 0 -o \ + "${#DNS_SDNS_SERVERS[@]}" -ne 0 -o \ + "${#DNS_DNSCRYPT_NAMES[@]}" -ne 0 } - function tunnel_configure { - local args arg - args=() - # check - if ! __cloudflared_exists; then + function select_provider { + local index id url about options=() property + + # only run once + if test -n "${provider-}"; then return 0 fi - # if no args - if test "$#" -eq 0; then - # check env - if test "${#CLOUDFLARED_TUNNELS[@]}" -eq 0; then - return 0 # @todo not currently implemented - # if no env, call single without args so it pompts - tunnel_configure_single - # and ask again until the user is done adding tunnels - while confirm --linger --negative --ppid=$$ -- "Do you wish to add another tunnel?"; do - tunnel_configure_single - done + # reset shared vars + for property in "${properties[@]}"; do + eval "$property=()" + done + + # render arg provider if applicable + if __has_arg_provider; then + id='arg' + about='Use the provided DNS servers specified in the CLI arguments' + url='' + options+=( + "$id" + "$(render_service_or_provider "$id" "$about" "$url")" + ) + fi + + # render env provider if applicable + if __has_env_provider; then + id='env' + about='Use the provided DNS servers specified in your dns.bash user configuration' + url='' + options+=( + "$id" + "$(render_service_or_provider "$id" "$about" "$url")" + ) + fi + + # render provider options + for index in "${!providers_id[@]}"; do + id="${providers_id[index]}" + about="${providers_about[index]}" + url="${providers_url[index]}" + options+=( + "$id" + "$(render_service_or_provider "$id" "$about" "$url")" + ) + done + + # select provider + provider="$( + choose --required --linger --confirm="$option_confirm" --label \ + --question='Which DNS provider to use?' \ + --default="${option_provider:-"$DNS_PROVIDER"}" -- "${options[@]}" + )" + + # apply + if test "$provider" = 'arg'; then + ipv4_servers=("${option_ipv4_servers[@]}") + ipv6_servers=("${option_ipv6_servers[@]}") + doh_servers=("${option_doh_servers[@]}") + dot_servers=("${option_dot_servers[@]}") + quic_servers=("${option_quic_servers[@]}") + sdns_servers=("${option_sdns_servers[@]}") + dnscrypt_names=("${option_dnscrypt_names[@]}") + elif test "$provider" = 'env'; then + ipv4_servers=("${DNS_IPV4_SERVERS[@]}") + ipv6_servers=("${DNS_IPV6_SERVERS[@]}") + doh_servers=("${DNS_DOH_SERVERS[@]}") + dot_servers=("${DNS_DOT_SERVERS[@]}") + quic_servers=("${DNS_QUIC_SERVERS[@]}") + sdns_servers=("${DNS_SDNS_SERVERS[@]}") + dnscrypt_names=("${DNS_DNSCRYPT_NAMES[@]}") + else + for property in "${properties[@]}"; do + mapfile -t "$property" < <( + jq -r \ + --arg provider "$provider" \ + --arg property "$property" \ + '.providers[$provider][$property][]' \ + "$DOROTHY/config/dns.json" 2>/dev/null || : + ) + done + fi + } + + # --------------------------------- + # Action + + function check_installed { + fetch_available_and_installed_services + local want="${option_service:-"$DNS_SERVICE"}" + + # Check any installed, or specific is installed + if test -z "$want"; then + if test "${#installed_services_id[@]}" -eq 0; then + echo-style --error1='No custom DNS is installed.' + return 1 else - # use env - tunnel_configure "${CLOUDFLARED_TUNNELS[@]}" - return + echo-style --good1='The following custom DNS is installed: ' --code-good1="${installed_services_id[*]}" + return 0 + fi + else + local id found='no' + for id in "${installed_services_id[@]}"; do + if test "$want" = "$id"; then + found='yes' + break + fi + done + if test "$found" = 'no'; then + echo-style --error1='The custom DNS is not installed: ' --code-error1="$want" + return 1 + else + echo-style --good1='The custom DNS is installed: ' --code-good1="$want" + return 0 fi fi + } - # cycle through the tunnels - while test "$#" -ne 0; do - arg="$1" - args+=("$arg") - shift - # if we have args and are a new tunnel or are at the end, then run configure and reset - if test "$arg" = '--' -o "$#" -eq 0 && test "${#args[@]}" -ne 0; then - tunnel_configure_single "${args[@]}" - args=() + function check_uninstalled { + fetch_available_and_installed_services + local want="${option_service:-"$DNS_SERVICE"}" + + # Check all uninstalled, or specific is uninstalled + if test -z "$want"; then + if test "${#installed_services_id[@]}" -eq 0; then + echo-style --good1='All custom DNS is uninstalled.' + return 1 + else + echo-style --error1='The following custom DNS are still installed: ' --code-error1="${installed_services_id[*]}" + return 0 fi - done + else + local id found='no' + for id in "${installed_services_id[@]}"; do + if test "$want" = "$id"; then + found='yes' + break + fi + done + if test "$found" = 'yes'; then + echo-style --error1='The custom DNS is still installed: ' --code-error1="$want" + return 1 + else + echo-style --good1='The custom DNS is uninstalled: ' --code-good1="$want" + return 0 + fi + fi } - # ===================================== - # Simple Actions + function install_only { + fetch_available_and_installed_services + make_system_paths + if test -z "${service-}"; then + select_service + fi + local id fn_id service_fn_id="${service//-/_}" # replace dashes with underscores - # if test "$action" = 'get-config-paths'; then - # __print_lines "$aghome_conf_file" - # __print_lines "$tunnel_user_pem" - # __print_lines "$HOME/.secrets/certbot" - # __print_lines '/etc/letsencrypt' - # return 0 - # fi + # Install then configure the service if not system + if test "$service" = 'system'; then + return 0 + elif ! __verify_connection; then + # If the internet is not working, disable other services and enable system, then install and enable our desired service + disconnect_vpn + for id in "${installed_services_id[@]}"; do + fn_id="${id//-/_}" # replace dashes with underscores + "${fn_id}__configure" disable # eval + done + select_provider + system_dns__configure enable + __verify_connection_and_wait - # ===================================== - # Action + # Install and enable our desired service + reconnect_vpn + "${service_fn_id}__install" # eval + else + # If the internet is working, install the desired service, disable alternative services, then enable the desired service + "${service_fn_id}__install" # eval + fi + } - echo-style --h1='Setup DNS' + function install_and_enable { + fetch_available_and_installed_services + make_system_paths + select_service + select_provider + local id fn_id service_fn_id="${service//-/_}" # replace dashes with underscores + + # Install then configure the service if not system + if test "$service" = 'system'; then + # Disable other services and enable system + disconnect_vpn + for id in "${installed_services_id[@]}"; do + fn_id="${id//-/_}" # replace dashes with underscores + "${fn_id}__configure" disable # eval + done + system_dns__configure enable + __verify_connection_and_wait + reconnect_vpn + elif ! __verify_connection; then + # If the internet is not working, disable other services and enable system, then install and enable our desired service + disconnect_vpn + for id in "${installed_services_id[@]}"; do + fn_id="${id//-/_}" # replace dashes with underscores + "${fn_id}__configure" disable # eval + done + system_dns__configure enable + __verify_connection_and_wait + + # Install and enable our desired service + reconnect_vpn + "${service_fn_id}__install" # eval + disconnect_vpn + system_dns__configure disable + "${service_fn_id}__configure" enable # eval + __verify_connection_and_wait + reconnect_vpn + else + # If the internet is working, install the desired service, disable alternative services, then enable the desired service + "${service_fn_id}__install" # eval + disconnect_vpn + for id in "${installed_services_id[@]}"; do + fn_id="${id//-/_}" # replace dashes with underscores + if test "$fn_id" != "$service_fn_id"; then + "${fn_id}__configure" disable # eval + fi + done + system_dns__configure disable + "${service_fn_id}__configure" enable # eval + __verify_connection_and_wait + reconnect_vpn + fi + } - # ------------------------------------- - # Prompts - - # Prompt the user which dns service they wish to use - local service="${option_service:-"$DNS_SERVICE"}" - service="$( - choose --required --confirm \ - --question="Which DNS service do you wish to be your primary service?" \ - --skip-default --default="$DNS_SERVICE" -- "${services[@]}" - )" - - # Prompt the user if they intend to use cloudflared tunnels - local cloudflared_tunnels - cloudflared_tunnels='no' - if test "${#CLOUDFLARED_TUNNELS[@]}" -ne 0; then - cloudflared_tunnels='yes' - elif confirm --linger --negative --ppid=$$ -- "Do you wish to create a Cloudflare tunnel?"; then - cloudflared_tunnels='yes' - fi + function uninstall_and_disable { + fetch_available_and_installed_services + make_system_paths + local id fn_id service_fn_id - # Prompt the user which dns provider they wish to use - fetch_provider + # uninstall all services + if test -z "${service-}"; then + for id in "${installed_services_id[@]}"; do + fn_id="${id//-/_}" # replace dashes with underscores + "${fn_id}__uninstall" # eval + done + else + # uninstall specific service + service_fn_id="${service//-/_}" # replace dashes with underscores + "${service_fn_id}__uninstall" # eval + fi + service='' + service_fn_id='' + + # go back to system + select_provider + system_dns__configure enable + __verify_connection_and_wait + reconnect_vpn + } - # ------------------------------------- - # Installations + function disable_only { + fetch_available_and_installed_services + make_system_paths + local id fn_id service_fn_id - # Install the provider - ("$service"_install) + # uninstall all services + if test -z "${service-}"; then + for id in "${installed_services_id[@]}"; do + fn_id="${id//-/_}" # replace dashes with underscores + "${fn_id}__configure" disable # eval + done - # Install the tunnel provider - if test "$cloudflared_tunnels" = 'yes'; then - tunnel_install - else - tunnel_uninstall - fi + else + # uninstall specific service + service_fn_id="${service//-/_}" # replace dashes with underscores + "${service_fn_id}__configure" disable # eval + fi + service='' + service_fn_id='' + + # go back to system + select_provider + system_dns__configure enable + __verify_connection_and_wait + reconnect_vpn + } - # Detect installed custom services - local installed_custom_services - installed_custom_services=() - if __aghome_exists; then - installed_custom_services+=(aghome) - fi - if __cloudflared_exists; then - installed_custom_services+=(cloudflared) - fi - if __dnscrypt_exists; then - installed_custom_services+=(dnscrypt) + # verify that installed install and enable options are not used together + if test -n "$option_installed"; then + if test -n "$option_install" -o -n "$option_enable"; then + help '--installed, --install, and --enable are mutually exclusive' + fi + elif test -n "$option_install"; then + if test -n "$option_installed" -o -n "$option_enable"; then + help '--installed, --install, and --enable are mutually exclusive' + fi + elif test -n "$option_enable"; then + if test -n "$option_installed" -o -n "$option_install"; then + help '--installed, --install, and --enable are mutually exclusive' + fi fi - # Disable all custom services, reconfigure the system (system on linux must be temporarily enabled to be configured), and then enable the chosen service - local installed_custom_service - for installed_custom_service in "${installed_custom_services[@]}"; do - "$installed_custom_service"_configure disable # eval - done - if test "$service" = 'system'; then - system_configure enable + echo-style --h1='Setup DNS' + if test "$option_installed" = 'yes'; then + check_installed + elif test "$option_installed" = 'no'; then + check_uninstalled + elif test "$option_install" = 'yes'; then + install_only + elif test "$option_install" = 'no'; then + uninstall_and_disable + elif test "$option_enable" = 'yes'; then + install_and_enable + elif test "$option_enable" = 'no'; then + disable_only else - system_configure disable - "$service"_configure enable # eval - fi - - # ------------------------------------- - # Verification - - # Verify DNS - echo-style --h1='Verify DNS' - verify_dns_generic - echo-style --g1='Verify DNS' - - # Prompt the user which hosts they want to use - # This is here, as we need internet to be working. - setup-hosts - - # echo - # __print_lines 'Testing that the system is now using the custom DNS service...' - # if ! (dig -x cloudflare.com | grep --quiet --fixed-strings --regexp=';; SERVER: 127.0.0.1'); then - # cat <<-EOF >/dev/stderr - # FAILURE - # Custom DNS configuration has failed. - # The domain failed to resolve or did not resolve with the local DNS service. - # You can debug further by running [debug-network]. - # EOF - # return 1 - # fi - # __print_lines "DNS service setup succesfully ✅" - - # 7. Configure the tunnel - if test "$cloudflared_tunnels" = 'yes' && confirm --linger --negative --ppid=$$ -- "Setup cloudflare tunnels?"; then - tunnel_configure - # verify tunnels works + install_and_enable fi - - # letsencrypt cert - # https://certbot.eff.org/instructions?ws=other&os=ubuntufocal - # https://eff-certbot.readthedocs.io/en/stable/using.html - # https://certbot-dns-cloudflare.readthedocs.io/en/stable/ - # function letsencrypt { - # sudo-helper -- snap install core - # sudo-helper -- snap refresh core - # sudo-helper -- apt-get remove certbot - - # sudo-helper -- snap install --classic certbot - # sudo-helper -- snap set certbot trust-plugin-with-root=ok - # sudo-helper -- snap install certbot-dns-cloudflare - - # mkdir -p ~/.secrets/certbot - # despite docs, this is not needed: dns_cloudflare_email = user@domain.tld - # cat <<-EOF > ~/.secrets/certbot/cloudflare.ini - # dns_cloudflare_api_token = $CLOUDFLARE_API_TOKEN - # EOF - - # chmod -R 700 ~/.secrets - # chmod 600 ~/.secrets/certbot/cloudflare.ini - - # if exposes a dns server, you can include --preferred-challenges dns-01 - # however it is optional and not needed - # https://eff-certbot.readthedocs.io/en/stable/contributing.html?highlight=challenge#authenticators - # https://eff-certbot.readthedocs.io/en/stable/using.html#certbot-command-line-options - # sudo-helper -- certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini -d domain.tld,*.domain.tld - - # sudo-helper -- certbot renew --dry-run - # } - - # update configuration - # dorothy-config 'dns.bash' --prefer=local -- \ - # --field='DNS_SERVICE' --value="$DNS_SERVICE" \ - # --field='DNS_PROVIDER' --value="$DNS_PROVIDER" - - # done echo-style --g1='Setup DNS' ) diff --git a/commands/setup-environment-commands b/commands/setup-environment-commands index 904e6297e..5c9503c1d 100755 --- a/commands/setup-environment-commands +++ b/commands/setup-environment-commands @@ -62,6 +62,10 @@ source "$DOROTHY/sources/env.bash" # ===================================== # Prepare +# PATH vars contain multiple paths +# _DIR vars contain a single universal path +# _HOME vars contain a single user path + # ensure editor vars are exported export LANG LC_ALL EDITOR @@ -87,6 +91,15 @@ if test -z "${HOME-}"; then fi fi +# Local directories +# https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch04s09.html +export BIN_DIR CONF_DIR DATA_DIR LIB_DIR STATE_DIR SERVICE_DIR LOGS_DIR +BIN_DIR='/usr/local/bin' +CONF_DIR='/usr/local/etc' +DATA_DIR='/usr/local/share' +LIB_DIR='/usr/local/lib' +STATE_DIR='/var/local/state' + # XDG # https://wiki.archlinux.org/title/XDG_Base_Directory # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html @@ -261,9 +274,11 @@ if test -d '/usr/lib/pkgconfig'; then PKG_CONFIG_PATH="/usr/lib/pkgconfig:$PKG_CONFIG_PATH" fi -# Homebrew +# macOS if is-mac; then - # macos + SERVICE_DIR='/Library/LaunchDaemons' + LOGS_DIR='/var/log' + # Homebrew export HOMEBREW_ARCH HOMEBREW_PREFIX HOMEBREW_CELLAR HOMEBREW_REPOSITORY HOMEBREW_SHELLENV_PREFIX HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_ENV_HINTS=1 if test -z "${HOMEBREW_ARCH-}"; then if test "$(uname -p)" = 'arm' -o "$(uname -m)" = 'arm64'; then @@ -297,6 +312,10 @@ if is-mac; then HOMEBREW_SHELLENV_PREFIX="$HOMEBREW_PREFIX" fi elif is-linux; then + # Linux + SERVICE_DIR='/etc/systemd/system' + LOGS_DIR='/var/log' + # Homebrew # https://docs.brew.sh/Homebrew-on-Linux export HOMEBREW_PREFIX="${HOMEBREW_PREFIX:-"$HOME/.linuxbrew"}" HOMEBREW_NO_ENV_HINTS=1 fi @@ -587,8 +606,7 @@ if command-exists -- go; then # if we were able to find one if test -n "${GOPATH-}"; then # then create its parts and add it to PATH - mkdir -p "$GOPATH/bin" - mkdir -p "$GOPATH/libexec" + mkdir -p "$GOPATH/bin" "$GOPATH/libexec" PATH="$GOPATH/bin:$PATH" fi fi diff --git a/commands/setup-mac-brew b/commands/setup-mac-brew index 4f91d6da2..6724e0846 100755 --- a/commands/setup-mac-brew +++ b/commands/setup-mac-brew @@ -528,6 +528,7 @@ function setup_mac_brew() ( # update packages echo-style --h2="Upgrade Homebrew" + echo-style --dim='Brew upgrades can fail sporadically due to changes in the environment (such as when the environment depended on a now upgraded package, in which the environment must be reloaded to make use of the upgraded package). If it fails, open a new terminal and try again which will continue this incremental upgrade process.' update_brew echo-style --g2="Upgrade Homebrew" diff --git a/commands/setup-util b/commands/setup-util index ab34edbe0..21def8477 100755 --- a/commands/setup-util +++ b/commands/setup-util @@ -191,6 +191,11 @@ function setup_util() ( Returns [0] if quiet and no action is necessary. Returns [1] if an action is necessary. + --installed | --uninstalled + If used, just check if an installation or uninstallation is necessary. + Returns [0] if quiet and no action is necessary. + Returns [1] if an action is necessary. + ... uppercase arguments are options for our installers. INSTALLERS: @@ -277,6 +282,14 @@ function setup_util() ( '--uninstall') option_action='uninstall' ;; '--check='*) option_check="${item#*=}" ;; '--check') option_check='yes' ;; + '--installed') + option_check='yes' + option_action='install' + ;; + '--uninstalled') + option_check='yes' + option_action='uninstall' + ;; '--no-check') option_check='no' ;; '--action='*) option_action="${item#*=}" ;; '--order='*) mapfile -t option_order < <(echo-split ' ' -- "${item#*=}") ;; @@ -2209,7 +2222,7 @@ function setup_util() ( pip2 "$@" || return elif python2 -m pip --version &>/dev/null; then python -m pip "$@" || return - elif command-exists /usr/local/bin/pip; then + elif command-exists -- /usr/local/bin/pip; then /usr/local/bin/pip "$@" || return elif test -n "${HOMEBREW_PREFIX-}" -a -x "${HOMEBREW_PREFIX-}/bin/pip"; then "${HOMEBREW_PREFIX}/bin/pip" "$@" || return diff --git a/commands/setup-util-adguard-home b/commands/setup-util-adguard-home new file mode 100755 index 000000000..e164a2721 --- /dev/null +++ b/commands/setup-util-adguard-home @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# adguard-home +# https://github.com/AdguardTeam/AdGuardHome/releases +# AdGuardHome_darwin_amd64.zip +# AdGuardHome_darwin_arm64.zip +# AdGuardHome_freebsd_386.tar.gz +# AdGuardHome_freebsd_amd64.tar.gz +# AdGuardHome_freebsd_arm64.tar.gz +# AdGuardHome_freebsd_armv5.tar.gz +# AdGuardHome_freebsd_armv6.tar.gz +# AdGuardHome_freebsd_armv7.tar.gz +# AdGuardHome_frontend.tar.gz +# AdGuardHome_linux_386.tar.gz +# AdGuardHome_linux_amd64.tar.gz +# AdGuardHome_linux_arm64.tar.gz +# AdGuardHome_linux_armv5.tar.gz +# AdGuardHome_linux_armv6.tar.gz +# AdGuardHome_linux_armv7.tar.gz +# AdGuardHome_linux_mips64le_softfloat.tar.gz +# AdGuardHome_linux_mips64_softfloat.tar.gz +# AdGuardHome_linux_mipsle_softfloat.tar.gz +# AdGuardHome_linux_mips_softfloat.tar.gz +# AdGuardHome_linux_ppc64le.tar.gz +# AdGuardHome_openbsd_amd64.tar.gz +# AdGuardHome_openbsd_arm64.tar.gz +# AdGuardHome_windows_386.zip +# AdGuardHome_windows_amd64.zip +# AdGuardHome_windows_arm64.zip + +function setup_util_adguard_home() ( + source "$DOROTHY/sources/bash.bash" + setup-dns --service='adguard-home' "$@" + return $? +) + +# fire if invoked standalone +if test "$0" = "${BASH_SOURCE[0]}"; then + setup_util_adguard_home "$@" +fi diff --git a/commands/setup-util-bash b/commands/setup-util-bash index 3291d31da..d70885ac0 100755 --- a/commands/setup-util-bash +++ b/commands/setup-util-bash @@ -63,9 +63,12 @@ function setup_util_bash() ( } arch="$(get-arch)" if is-mac; then + # fetch the bottle urls via: brew info --json bash | jq -r ".[].bottle.stable.files" macos_release="$(get-macos-release-name)" if test "$arch" = 'a64'; then - if test "$macos_release" = 'sonoma'; then + if test "$macos_release" = 'sequoia'; then + bottle_url='https://ghcr.io/v2/homebrew/core/bash/blobs/sha256:066b7eba204091b70860d2f17d0dd65201900b3e3ca32de87a746ed1baf13332' + elif test "$macos_release" = 'sonoma'; then bottle_url='https://ghcr.io/v2/homebrew/core/bash/blobs/sha256:bd484090760c2736fa30e29a7861aaf115330bfb10178ce398e1f927a056a047' elif test "$macos_release" = 'ventura'; then bottle_url='https://ghcr.io/v2/homebrew/core/bash/blobs/sha256:f3a42b9282e6779504034485634a2f3e6e3bddfc70b9990e09e66e3c8c926b7d' diff --git a/commands/setup-util-cloudflared b/commands/setup-util-cloudflared new file mode 100755 index 000000000..b6258d47e --- /dev/null +++ b/commands/setup-util-cloudflared @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# cloudflared +# https://github.com/cloudflare/cloudflared/releases +# cloudflared-amd64.pkg +# cloudflared-arm64.pkg +# cloudflared-darwin-amd64.tgz +# cloudflared-darwin-arm64.tgz +# cloudflared-fips-linux-amd64 +# cloudflared-fips-linux-amd64.deb +# cloudflared-fips-linux-x86_64.rpm +# cloudflared-linux-386 +# cloudflared-linux-386.deb +# cloudflared-linux-386.rpm +# cloudflared-linux-aarch64.rpm +# cloudflared-linux-amd64 +# cloudflared-linux-amd64.deb +# cloudflared-linux-arm +# cloudflared-linux-arm.deb +# cloudflared-linux-arm.rpm +# cloudflared-linux-arm64 +# cloudflared-linux-arm64.deb +# cloudflared-linux-armhf +# cloudflared-linux-armhf.deb +# cloudflared-linux-armhf.rpm +# cloudflared-linux-x86_64.rpm +# cloudflared-windows-386.exe +# cloudflared-windows-386.msi +# cloudflared-windows-amd64.exe +# cloudflared-windows-amd64.msi + +function setup_util_cloudflared() ( + source "$DOROTHY/sources/bash.bash" + setup-dns --service='cloudflared' "$@" + return $? +) + +# fire if invoked standalone +if test "$0" = "${BASH_SOURCE[0]}"; then + setup_util_cloudflared "$@" +fi diff --git a/commands/setup-util-dnscrypt-proxy b/commands/setup-util-dnscrypt-proxy new file mode 100755 index 000000000..1ad00038d --- /dev/null +++ b/commands/setup-util-dnscrypt-proxy @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# dnscrypt-proxy +# https://github.com/DNSCrypt/dnscrypt-proxy/releases +# dnscrypt-proxy-android_arm-2.1.5.zip +# dnscrypt-proxy-android_arm64-2.1.5.zip +# dnscrypt-proxy-android_i386-2.1.5.zip +# dnscrypt-proxy-android_x86_64-2.1.5.zip +# dnscrypt-proxy-dragonflybsd_amd64-2.1.5.tar.gz +# dnscrypt-proxy-freebsd_amd64-2.1.5.tar.gz +# dnscrypt-proxy-freebsd_arm-2.1.5.tar.gz +# dnscrypt-proxy-freebsd_i386-2.1.5.tar.gz +# dnscrypt-proxy-linux_arm-2.1.5.tar.gz +# dnscrypt-proxy-linux_arm64-2.1.5.tar.gz +# dnscrypt-proxy-linux_i386-2.1.5.tar.gz +# dnscrypt-proxy-linux_mips-2.1.5.tar.gz +# dnscrypt-proxy-linux_mips64-2.1.5.tar.gz +# dnscrypt-proxy-linux_mips64le-2.1.5.tar.gz +# dnscrypt-proxy-linux_mipsle-2.1.5.tar.gz +# dnscrypt-proxy-linux_riscv64-2.1.5.tar.gz +# dnscrypt-proxy-linux_x86_64-2.1.5.tar.gz +# dnscrypt-proxy-macos_arm64-2.1.5.zip +# dnscrypt-proxy-macos_x86_64-2.1.5.zip +# dnscrypt-proxy-netbsd_amd64-2.1.5.tar.gz +# dnscrypt-proxy-netbsd_i386-2.1.5.tar.gz +# dnscrypt-proxy-openbsd_amd64-2.1.5.tar.gz +# dnscrypt-proxy-openbsd_i386-2.1.5.tar.gz +# dnscrypt-proxy-solaris_amd64-2.1.5.tar.gz +# dnscrypt-proxy-win32-2.1.5.zip +# dnscrypt-proxy-win64-2.1.5.zip + +function setup_util_dnscrypt_proxy() ( + source "$DOROTHY/sources/bash.bash" + setup-dns --service='dnscrypt-proxy' "$@" + return $? +) + +# fire if invoked standalone +if test "$0" = "${BASH_SOURCE[0]}"; then + setup_util_dnscrypt_proxy "$@" +fi diff --git a/commands/setup-util-gsed b/commands/setup-util-gsed index abe6d0514..ce117cc8d 100755 --- a/commands/setup-util-gsed +++ b/commands/setup-util-gsed @@ -55,7 +55,9 @@ function setup_util_gsed() ( # fetch the bottle urls via: brew info --json gnu-sed | jq -r ".[].bottle.stable.files" macos_release="$(get-macos-release-name)" if test "$arch" = 'a64'; then - if test "$macos_release" = 'sonoma'; then + if test "$macos_release" = 'sequoia'; then + bottle_url='https://ghcr.io/v2/homebrew/core/gnu-sed/blobs/sha256:7bf9b1bc4e946b0f316cfe1aeacc5fbf418d0045588d381f35439d96dba64f4c' + elif test "$macos_release" = 'sonoma'; then bottle_url='https://ghcr.io/v2/homebrew/core/gnu-sed/blobs/sha256:5ffd49517ed790e52a088e720de77f1dd4de4e88816fb6a1d244be3f6b01314d' elif test "$macos_release" = 'ventura'; then bottle_url='https://ghcr.io/v2/homebrew/core/gnu-sed/blobs/sha256:3770e9098033bc1f32427d3b6502a1ab10082b3945e204286c87060d82d03d19' diff --git a/commands/setup-util-plex b/commands/setup-util-plex-media-server similarity index 97% rename from commands/setup-util-plex rename to commands/setup-util-plex-media-server index feb402fb2..7d27f9cf4 100755 --- a/commands/setup-util-plex +++ b/commands/setup-util-plex-media-server @@ -1,6 +1,6 @@ #!/usr/bin/env bash -function setup_util_plex() ( +function setup_util_plex_media_server() ( source "$DOROTHY/sources/bash.bash" # ===================================== @@ -112,5 +112,5 @@ function setup_util_plex() ( # fire if invoked standalone if test "$0" = "${BASH_SOURCE[0]}"; then - setup_util_plex "$@" + setup_util_plex_media_server "$@" fi diff --git a/commands/sudo-helper b/commands/sudo-helper index fc44a131c..a5ad737ec 100755 --- a/commands/sudo-helper +++ b/commands/sudo-helper @@ -224,7 +224,7 @@ function sudo_helper() ( # the main thing here though, is that any failure should be detected and cancel in the caller # perhaps requiring --ppid=$$ to be passed to sudo-helper is the way to go, as we do for [confirm] if test "$option_wrap" = 'yes' -o "$option_confirm" = 'yes' -o "$option_quiet" = 'yes'; then - eval-helper --title="$(echo-style --sudo="$(echo-escape-command -- "${option_cmd[@]}")")" --wrap="$option_wrap" --quiet="$option_quiet" --confirm="$option_confirm" -- "${run[@]}" + eval-helper --command="$(echo-style --sudo="$(echo-escape-command -- "${option_cmd[@]}")")" --wrap="$option_wrap" --quiet="$option_quiet" --confirm="$option_confirm" -- "${run[@]}" else "${run[@]}" # eval fi diff --git a/config/dns.json b/config/dns.json new file mode 100644 index 000000000..afaf5a8d9 --- /dev/null +++ b/config/dns.json @@ -0,0 +1,146 @@ +{ + "services": { + "cloudflared": { + "about": "Closed-source encrypted DNS and tunnels", + "url": "https://github.com/cloudflare/cloudflared" + }, + "adguard-home": { + "about": "Open-source and full-featured Pi-Hole alternative with builtin encrypted DNS, client filtering, advert and tracker blocking", + "url": "https://github.com/AdguardTeam/AdGuardHome" + }, + "dnscrypt-proxy": { + "about": "Open-source and lightweight DNS service for the DNSCrypt protocol", + "url": "https://github.com/DNSCrypt/dnscrypt-proxy" + } + }, + "providers": { + "adguard": { + "about": "Block ads and trackers.", + "url": "https://adguard-dns.io/en/public-dns.html", + "ipv4_servers": ["94.140.14.14", "94.140.15.15"], + "ipv6_servers": ["2a10:50c0::ad1:ff", "2a10:50c0::ad2:ff"], + "doh_servers": ["https://dns.adguard-dns.com/dns-query"], + "dot_servers": ["tls://dns.adguard-dns.com"], + "quic_servers": ["quic://dns.adguard-dns.com"], + "sdns_servers": [ + "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20" + ], + "dnscrypt_names": ["adguard-dns", "adguard-dns-doh", "adguard-dns-ipv6"] + }, + "adguard-unfiltered": { + "about": "Will not block ads, trackers, or any other DNS requests.", + "url": "https://adguard-dns.io/en/public-dns.html", + "ipv4_servers": ["94.140.14.140", "94.140.14.141"], + "ipv6_servers": ["2a10:50c0::1:ff", "2a10:50c0::2:ff"], + "doh_servers": ["https://unfiltered.adguard-dns.com/dns-query"], + "dot_servers": ["tls://unfiltered.adguard-dns.com"], + "quic_servers": ["quic://unfiltered.adguard-dns.com"], + "sdns_servers": [ + "sdns://AQMAAAAAAAAAEjk0LjE0MC4xNC4xNDA6NTQ0MyC16ETWuDo-PhJo62gfvqcN48X6aNvWiBQdvy7AZrLa-iUyLmRuc2NyeXB0LnVuZmlsdGVyZWQubnMxLmFkZ3VhcmQuY29t" + ], + "dnscrypt_names": [ + "adguard-dns-unfiltered", + "adguard-dns-unfiltered-ipv6" + ] + }, + "adguard-family": { + "about": "Block ads, trackers, adult content, and enable Safe Search and Safe Mode, where possible.", + "url": "https://adguard-dns.io/en/public-dns.html", + "ipv4_servers": ["94.140.14.15", "94.140.15.16"], + "ipv6_servers": ["2a10:50c0::bad1:ff", "2a10:50c0::bad2:ff"], + "doh_servers": ["https://family.adguard-dns.com/dns-query"], + "dot_servers": ["tls://family.adguard-dns.com"], + "quic_servers": ["quic://family.adguard-dns.com"], + "sdns_servers": [ + "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNTo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ" + ], + "dnscrypt_names": [ + "adguard-dns-family", + "adguard-dns-family-doh", + "adguard-dns-family-ipv6" + ] + }, + "quad9": { + "about": "Recommended: Malware Blocking, DNSSEC Validation (this is the most typical configuration)", + "url": "https://www.quad9.net/service/service-addresses-and-features/", + "ipv4_servers": ["9.9.9.9", "149.112.112.112"], + "ipv6_servers": ["2620:fe::fe", "2620:fe::9"], + "doh_servers": ["https://dns.quad9.net/dns-query"], + "dot_servers": ["tls://dns.quad9.net"], + "dnscrypt_names": [ + "quad9-dnscrypt-ip4-filter-pri", + "quad9-dnscrypt-ip6-filter-pri", + "quad9-doh-ip4-port443-filter-pri", + "quad9-doh-ip4-port5053-filter-pri", + "quad9-doh-ip6-port443-filter-pri", + "quad9-doh-ip6-port5053-filter-pri" + ] + }, + "quad9-ecs": { + "about": "Secured w/ECS: Malware blocking, DNSSEC Validation, ECS enabled", + "url": "https://www.quad9.net/service/service-addresses-and-features/", + "ipv4_servers": ["9.9.9.11", "149.112.112.11"], + "ipv6_servers": ["2620:fe::11", "2620:fe::fe:11"], + "doh_servers": ["https://dns11.quad9.net/dns-query"], + "dot_servers": ["tls://dns11.quad9.net"], + "dnscrypt_names": [ + "quad9-dnscrypt-ip4-filter-ecs-pri", + "quad9-dnscrypt-ip6-filter-ecs-pri", + "quad9-doh-ip4-port443-filter-ecs-pri", + "quad9-doh-ip4-port5053-filter-ecs-pri", + "quad9-doh-ip6-port443-filter-ecs-pri", + "quad9-doh-ip6-port5053-filter-ecs-pri" + ] + }, + "quad9-unsecured": { + "about": "Unsecured: No Malware blocking, no DNSSEC validation (for experts only!)", + "url": "https://www.quad9.net/service/service-addresses-and-features/", + "ipv4_servers": ["9.9.9.10", "149.112.112.10"], + "ipv6_servers": ["2620:fe::10", "2620:fe::fe:10"], + "doh_servers": ["https://dns10.quad9.net/dns-query"], + "dot_servers": ["tls://dns10.quad9.net"], + "dnscrypt_names": [ + "quad9-dnscrypt-ip4-nofilter-pri", + "quad9-dnscrypt-ip6-nofilter-pri", + "quad9-doh-ip4-port443-nofilter-pri", + "quad9-doh-ip4-port5053-nofilter-pri", + "quad9-doh-ip6-port443-nofilter-pri", + "quad9-doh-ip6-port5053-nofilter-pri" + ] + }, + "cloudflare": { + "about": "Provides speed and resilience", + "url": "https://developers.cloudflare.com/1.1.1.1/setup/", + "ipv4_servers": ["1.1.1.1", "1.0.0.1"], + "ipv6_servers": ["2606:4700:4700::1111", "2606:4700:4700::1001"], + "doh_servers": ["https://cloudflare-dns.com/dns-query"], + "dot_servers": ["tls://one.one.one.one"], + "dnscrypt_names": ["cloudflare", "cloudflare-ipv6"] + }, + "cloudflare-security": { + "about": "Block malicious content", + "url": "https://developers.cloudflare.com/1.1.1.1/setup/#1111-for-families", + "ipv4_servers": ["1.1.1.2", "1.0.0.2"], + "ipv6_servers": ["2606:4700:4700::1112", "2606:4700:4700::1002"], + "doh_servers": ["https://security.cloudflare-dns.com/dns-query"], + "dot_servers": ["tls://security.cloudflare-dns.com"], + "dnscrypt_names": ["cloudflare-security", "cloudflare-security-ipv6"] + }, + "cloudflare-family": { + "about": "Block malware and adult content", + "url": "https://developers.cloudflare.com/1.1.1.1/setup/#1111-for-families", + "ipv4_servers": ["1.1.1.3", "1.0.0.3"], + "ipv6_servers": ["2606:4700:4700::1113", "2606:4700:4700::1003"], + "doh_servers": ["https://family.cloudflare-dns.com/dns-query"], + "dot_servers": ["tls://family.cloudflare-dns.com"], + "dnscrypt_names": ["cloudflare-family", "cloudflare-family-ipv6"] + }, + "google": { + "about": "A recursive DNS resolver, similar to other publicly available services", + "url": "https://developers.google.com/speed/public-dns", + "ipv4_servers": ["8.8.8.8", "8.8.4.4"], + "ipv6_servers": ["2001:4860:4860::8888", "2001:4860:4860::8844"], + "dnscrypt_names": ["google", "google-ipv6"] + } + } +} diff --git a/config/styles.bash b/config/styles.bash index 88c7e9a41..41af335fc 100644 --- a/config/styles.bash +++ b/config/styles.bash @@ -178,8 +178,12 @@ style__color__success="${style__color__foreground_green}${style__color__bold}" style__color_end__success="${style__color_end__foreground}${style__color_end__intensity}" style__color__positive="${style__color__foreground_green}${style__color__bold}" style__color_end__positive="${style__color_end__foreground}${style__color_end__intensity}" + style__color__good1="${style__color__background_intense_green}${style__color__foreground_black}" style__color_end__good1="${style__color_end__background}${style__color_end__foreground}" +style__color__code_good1="${style__color__background_intense_green}${style__color__foreground_intense_blue}" +style__color_end__code_good1="${style__color_end__background}${style__color_end__foreground}" + style__color__good2="${style__color__bold}${style__color__underline}${style__color__foreground_green}" style__color_end__good2="${style__color_end__intensity}${style__color_end__underline}${style__color_end__foreground}" style__color__good3="${style__color__bold}${style__color__foreground_green}" @@ -346,6 +350,13 @@ style__nocolor_end__element_slash=' />' style__color__element_slash="${style__color__dim}${style__color__bold}< ${style__color_end__intensity}" style__color_end__element_slash="${style__color__dim}${style__color__bold} />${style__color_end__intensity}" +# fragment +style__nocolor__fragment='<>' +style__color__fragment="${style__color__dim}${style__color__bold}<>${style__color_end__intensity}" + +style__nocolor__slash_fragment='</>' +style__color__slash_fragment="${style__color__dim}${style__color__bold}</>${style__color_end__intensity}" + # the style__color__resets allow these to work: # echo-style --h1_begin --h1='Setup Python' --h1_end $'\n' --g1_begin --g1='Setup Python' --g1_end # echo-style --element_slash_begin --h3="this should not be dim" --element_slash_end "$status" @@ -485,8 +496,8 @@ style__key_key_spacer=' ' style__indent_bar=' ' style__indent_active='⏵ ' style__indent_inactive=' ' -style__nocolor__indent_subsequent=' │ ' -style__color__indent_subsequent=" ${style__color__dim}│ ${style__color_end__dim}" +style__nocolor__blockquote='│ ' +style__color__blockquote="${style__color__dim}│ ${style__color_end__dim}" style__nocolor__count_spacer=' ∙ ' style__color__count_spacer=" ${style__color__foreground_intense_black}∙${style__color_end__foreground} " diff --git a/sources/bash.bash b/sources/bash.bash index 095d01d18..dcea08892 100644 --- a/sources/bash.bash +++ b/sources/bash.bash @@ -116,7 +116,7 @@ if test -z "${BASH_VERSION_CURRENT-}"; then # e.g. 5.2.15(1)-release => 5.2.15 IFS=. read -r BASH_VERSION_MAJOR BASH_VERSION_MINOR BASH_VERSION_PATCH <<<"${BASH_VERSION%%(*}" BASH_VERSION_CURRENT="${BASH_VERSION_MAJOR}.${BASH_VERSION_MINOR}.${BASH_VERSION_PATCH}" - BASH_VERSION_LATEST='5.2.32' # https://ftp.gnu.org/gnu/bash/?C=M;O=D + BASH_VERSION_LATEST='5.2.37' # https://ftp.gnu.org/gnu/bash/?C=M;O=D # any v5 version is supported by dorothy if test "$BASH_VERSION_MAJOR" -eq 5; then IS_BASH_VERSION_OUTDATED='no'