123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- #!/bin/bash
- # requirements:
- # jq to parse json
- # curl
- # coreutils: sort, cat...
- me="${0##*/}"
- usage() {
- [[ "$@" != "" ]] && printf "%s\n" "$@"
- cat <<EOF
- This script will fetch a current list of DNS servers from $host,
- filter them by countries if desired, sort by percentage, and write the result
- to files readable by various resolvers.
- It will then restart some systemd services if they are active. If the systemctl
- executable is not found in PATH, the script will gracefully exit instead.
- The script takes no command line options (except "-h" which produces this
- output, everything else is discarded), but searches for a config file in
- "\$HOME/.config/opennic-resolve/opennic-resolve.conf"
- "/etc/opennic-resolve/opennic-resolve.conf"
- "\$0.conf" i.e. the full path to the script + .conf
- "\$PWD/opennic-resolve.conf" i.e. in the present working directory
- Whichever is encountered first will be the only one to parse.
- Defaults and possible configuration:
- countries= Provide a comma-separated list of 2-letter country codes.
- Nameservers from other countries won't be added to the file.
- Empty by default - no filtering by country.
- max= Maximum number of entries to write to a file. Default: $max
- Should be between $minmax and $maxmax for the API query, although it
- appears that it rarely returns more than 20 results anyhow.
- min= Minimum number of entries to write to a file. Default: $min
- If fewer nameservers are found no file will be overwritten.
- ipv= Specify which class of IP addresses to return.
- 4 = Only list IPv4 servers
- 6 = Only list IPv6 servers
- all = Include both IPv4 and IPv6 servers
- 64 = Include all server, but list IPv6 address when both exist
- (unspecified by default)
- timeout= Timeout in s for data fetching from $host. default: $timeout
- out= Comma-separated list of actions. Possible values:
- resolvconf
- each line is either a comment starting with "#" or
- "nameserver " followed by the IP of the nameserver
- resolved
- starts with "[Resolve]", after which each line is either a
- comment starting with "#" or "DNS=" followed by the IP
- of the nameserver
- networkd
- starts with "[Network]", after which each line is either a
- comment starting with "#" or "DNS=" followed by the IP
- of the nameserver
- Overrides the default, which is "resolvconf,resolved"
- resolvconf= Write to this file instead of $resolvconf
- networkd= Write networkd syntax to this file (no default)
- resolved= Write to this file instead of $resolved
- services= Comma-separated list of systemd services to restart, if active.
- Overrides the default, which is:
- "$services"
- More information:
- EOF
- echo "https://"{notabug,framagit}".org/ohnonot/opennic"
- exit 1
- }
- rs_format() {
- # $1: prefix - "DNS=" for systemd files, "nameserver " for /etc/resolv.conf
- # reverse sort by percentage, split into 2 lines for each nameserver:
- # line1: prefix ip
- # line2: # percentage # location
- i=0
- printf "$rs"| sort -r | while read -a line && (( i++ < max )); do
- printf "# %s %s %s%%\n%s%s\n" "${line[2]}" "${line[3]}" "${line[0]}" "$1" "${line[1]}"
- done
- }
- rs_grow() {
- # add entries to results string rs
- local i="$1"
- ip="$(jq -Mr ".[$i] .ip" <<<"$data")"
- c="$(jq -Mj ".[$i] .loc" <<<"$data")"
- c="${c##* }"
- rs="${rs}$(jq -Mj ".[$i] .stat" <<<"$data") $ip $c $(jq -Mj ".[$i] .host" <<<"$data")\n"
- ((ns++))
- }
- # to be filled with namserver entries for /etc/resolv.conf
- rs=""
- # a counter for chosen nameserver results
- ns=0
- countries=""
- out=""
- services="systemd-networkd.service,systemd-resolved.service"
- config=''
- config_paths=( "$HOME/.config/$me/$me.conf" "/etc/$me/$me.conf" "$0.conf" "$PWD/$me.conf" )
- resolvconf="/etc/resolv.conf"
- # This one is just an example:
- # networkd="/etc/systemd/network/20-wired.network.d/dns.conf"
- networkd=""
- resolved="/etc/systemd/resolved.conf.d/dns.conf"
- do_resolvconf=1
- do_resolved=1
- do_networkd=0
- # maximum of results written to file
- max=5
- # limits for the above:
- minmax=2
- maxmax=50
- # minimum of results written to file
- # i.e., if there's less nameservers than this, no file will be overwritten
- min=2
- # timeout for curl data fetching
- timeout=10
- # no preference for IP version. user configurable
- ipv=
- host="api.opennicproject.org"
- #~ api_ip=161.97.219.82
- # json format, ipv6 over 4, min. 99% (doesn't seem to work), 20 results
- url="https://$host/geoip/?json&pct=99&res=$maxmax"
- # just give us 20 of whatever you think is best, in json:
- #~ url="https://$host/geoip/?json&res=$maxmax"
- [[ "$1" == "-h" || "$1" == "--help" ]] && usage
- # dependency checks
- for dep in jq curl sort; do
- type -f $dep >/dev/null || exit 1
- done
- ####################### USER CONF #############################################
- for config in "${config_paths[@]}"; do
- if [ -r "$config" ]; then
- echo "Reading configuration from $config" >&2
- break
- fi
- done
- ##### Parse config file #####
- shopt -s extglob
- if [ -r "$config" ]; then
- while IFS='= ' read -r lhs rhs; do
- if [[ ! $lhs =~ ^\ *# && -n $lhs ]]; then
- rhs="${rhs%%\#*}" # Del in line right comments
- rhs="${rhs%%*( )}" # Del trailing spaces
- rhs="${rhs%\"*}" # Del opening string quotes
- rhs="${rhs#\"*}" # Del closing string quotes
- declare $lhs="$rhs"
- echo "${lhs}=${!lhs}"
- fi
- done <"$config"
- fi
- unset config_paths config rhs lhs
- echo
- ##### Quality Check for user config #####
- [ "$max" -ge "$minmax" ] && [ "$max" -le "$maxmax" ] || usage "max must be larger than or equal to $minmax, and smaller than or equal to $maxmax."
- [ "$min" -gt 0 ] || usage "min must be larger than 0."
- [ "$timeout" -gt 0 ] || usage "timeout must be larger than 0."
- if [ -n "$ipv" ]; then
- [[ "$ipv" == 6 || "$ipv" == 4 || "$ipv" == "all" || "$ipv" == 64 ]]\
- || usage "wrong value for ipv: $ipv"
- fi
- (( min > max )) && usage "Minimum $min cannot be larger than maximum $max."
- if [[ "$out" != "" ]]; then
- out=( ${out//,/ } )
- do_resolvconf=0
- do_resolved=0
- do_networkd=0
- for i in "${out[@]}"; do
- case "$i" in
- resolvconf) do_resolvconf=1
- ;;
- resolved) do_resolved=1
- ;;
- networkd) do_networkd=1
- ;;
- esac
- done
- fi
- unset out
- if [[ "$countries" != "" ]]; then
- countries=( ${countries//,/ } )
- for ((i=0;i<${#countries[@]};i++)); do
- countries[i]="${countries[i],,}"
- [[ "${countries[i]}" == [a-z][a-z] ]] || usage "Invalid country ${countries[i]}"
- done
- else
- unset countries
- fi
- services=( ${services//,/ } )
- for i in "${services[@]}"; do
- [[ "$i" == *\.service ]] || usage "Invalid service $i"
- done
- ####################### END USER CONF #########################################
- curlopts=(
- #~ --resolve "${host}:443:${api_ip}"
- --connect-timeout "$timeout"
- --max-time "$timeout"
- )
- # json data of nameservers, obtained from opennic
- printf "Fetching %s\n" "$url"
- data="$(curl "${curlopts[@]}" "$url")"
- # fill array with all nameservers locations
- oldifs="$IFS";IFS=$'\n'
- loc=( $(jq -Mr '.[] .loc' <<<"$data") )
- IFS="$oldifs"
- # check locations against whitelist of allowed countries...
- for (( i=0 ; i<${#loc[@]} ; i++ )); do
- if (( ${#countries[@]} > 0 )); then
- # if countries are restricted, filter for suitable results
- for (( j=0 ; j<${#countries[@]} ; j++ )); do
- loc[i]="${loc[i],,}" # lowercase for comparison
- if [[ "${loc[i]##* }" == "${countries[j]}" ]]; then
- rs_grow "$i"
- break
- fi
- done
- else
- rs_grow "$i"
- fi
- done
- echo -e "\nRaw list: $ns results.\n$rs" >&2
- # how many results did we get?
- (( ns < min )) && echo "Not enough IPs for this configuration. Nothing changed." && exit 1
- if [[ "$do_resolvconf" == 1 ]] && [ -d "${resolvconf%/*}" ]; then
- rs_format "nameserver " > "$resolvconf"
- fi
- if [[ "$do_networkd" == 1 ]] && [ -d "${networkd%/*}" ]; then
- echo "[Network]" > "$networkd"
- rs_format "DNS=" >> "$networkd"
- fi
- if [[ "$do_resolved" == 1 ]] && [ -d "${resolved%/*}" ]; then
- echo "[Resolve]" > "$resolved"
- rs_format "DNS=" >> "$resolved"
- fi
- type -f systemctl >/dev/null || exit 0
- # Restart systemd services if active
- # According to wiki.archlinux.org/index.php/Domain_name_resolution, dhcpcd uses
- # the glibc resolver which reads /etc/resolv.conf for every name resolution
- # Thus, a dhcpcd.service restart should not be required.
- for s in "${services[@]}"; do
- systemctl --quiet is-active "$s" && systemctl restart "$s"
- done
- exit 0
|