opennic-resolve 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. #!/bin/bash
  2. # requirements:
  3. # jq to parse json
  4. # curl
  5. # coreutils: sort, cat...
  6. me="${0##*/}"
  7. usage() {
  8. [[ "$@" != "" ]] && printf "%s\n" "$@"
  9. cat <<EOF
  10. This script will fetch a current list of DNS servers from $host,
  11. filter them by countries if desired, sort by percentage, and write the result
  12. to files readable by various resolvers.
  13. It will then restart some systemd services if they are active. If the systemctl
  14. executable is not found in PATH, the script will gracefully exit instead.
  15. The script takes no command line options (except "-h" which produces this
  16. output, everything else is discarded), but searches for a config file in
  17. "\$HOME/.config/opennic-resolve/opennic-resolve.conf"
  18. "/etc/opennic-resolve/opennic-resolve.conf"
  19. "\$0.conf" i.e. the full path to the script + .conf
  20. "\$PWD/opennic-resolve.conf" i.e. in the present working directory
  21. Whichever is encountered first will be the only one to parse.
  22. Defaults and possible configuration:
  23. countries= Provide a comma-separated list of 2-letter country codes.
  24. Nameservers from other countries won't be added to the file.
  25. Empty by default - no filtering by country.
  26. max= Maximum number of entries to write to a file. Default: $max
  27. Should be between $minmax and $maxmax for the API query, although it
  28. appears that it rarely returns more than 20 results anyhow.
  29. min= Minimum number of entries to write to a file. Default: $min
  30. If fewer nameservers are found no file will be overwritten.
  31. ipv= Specify which class of IP addresses to return.
  32. 4 = Only list IPv4 servers
  33. 6 = Only list IPv6 servers
  34. all = Include both IPv4 and IPv6 servers
  35. 64 = Include all server, but list IPv6 address when both exist
  36. (unspecified by default)
  37. timeout= Timeout in s for data fetching from $host. default: $timeout
  38. out= Comma-separated list of actions. Possible values:
  39. resolvconf
  40. each line is either a comment starting with "#" or
  41. "nameserver " followed by the IP of the nameserver
  42. resolved
  43. starts with "[Resolve]", after which each line is either a
  44. comment starting with "#" or "DNS=" followed by the IP
  45. of the nameserver
  46. networkd
  47. starts with "[Network]", after which each line is either a
  48. comment starting with "#" or "DNS=" followed by the IP
  49. of the nameserver
  50. Overrides the default, which is "resolvconf,resolved"
  51. resolvconf= Write to this file instead of $resolvconf
  52. networkd= Write networkd syntax to this file (no default)
  53. resolved= Write to this file instead of $resolved
  54. services= Comma-separated list of systemd services to restart, if active.
  55. Overrides the default, which is:
  56. "$services"
  57. More information:
  58. EOF
  59. echo "https://"{notabug,framagit}".org/ohnonot/opennic"
  60. exit 1
  61. }
  62. rs_format() {
  63. # $1: prefix - "DNS=" for systemd files, "nameserver " for /etc/resolv.conf
  64. # reverse sort by percentage, split into 2 lines for each nameserver:
  65. # line1: prefix ip
  66. # line2: # percentage # location
  67. i=0
  68. printf "$rs"| sort -r | while read -a line && (( i++ < max )); do
  69. printf "# %s %s %s%%\n%s%s\n" "${line[2]}" "${line[3]}" "${line[0]}" "$1" "${line[1]}"
  70. done
  71. }
  72. rs_grow() {
  73. # add entries to results string rs
  74. local i="$1"
  75. ip="$(jq -Mr ".[$i] .ip" <<<"$data")"
  76. c="$(jq -Mj ".[$i] .loc" <<<"$data")"
  77. c="${c##* }"
  78. rs="${rs}$(jq -Mj ".[$i] .stat" <<<"$data") $ip $c $(jq -Mj ".[$i] .host" <<<"$data")\n"
  79. ((ns++))
  80. }
  81. # to be filled with namserver entries for /etc/resolv.conf
  82. rs=""
  83. # a counter for chosen nameserver results
  84. ns=0
  85. countries=""
  86. out=""
  87. services="systemd-networkd.service,systemd-resolved.service"
  88. config=''
  89. config_paths=( "$HOME/.config/$me/$me.conf" "/etc/$me/$me.conf" "$0.conf" "$PWD/$me.conf" )
  90. resolvconf="/etc/resolv.conf"
  91. # This one is just an example:
  92. # networkd="/etc/systemd/network/20-wired.network.d/dns.conf"
  93. networkd=""
  94. resolved="/etc/systemd/resolved.conf.d/dns.conf"
  95. do_resolvconf=1
  96. do_resolved=1
  97. do_networkd=0
  98. # maximum of results written to file
  99. max=5
  100. # limits for the above:
  101. minmax=2
  102. maxmax=50
  103. # minimum of results written to file
  104. # i.e., if there's less nameservers than this, no file will be overwritten
  105. min=2
  106. # timeout for curl data fetching
  107. timeout=10
  108. # no preference for IP version. user configurable
  109. ipv=
  110. host="api.opennicproject.org"
  111. #~ api_ip=161.97.219.82
  112. # json format, ipv6 over 4, min. 99% (doesn't seem to work), 20 results
  113. url="https://$host/geoip/?json&pct=99&res=$maxmax"
  114. # just give us 20 of whatever you think is best, in json:
  115. #~ url="https://$host/geoip/?json&res=$maxmax"
  116. [[ "$1" == "-h" || "$1" == "--help" ]] && usage
  117. # dependency checks
  118. for dep in jq curl sort; do
  119. type -f $dep >/dev/null || exit 1
  120. done
  121. ####################### USER CONF #############################################
  122. for config in "${config_paths[@]}"; do
  123. if [ -r "$config" ]; then
  124. echo "Reading configuration from $config" >&2
  125. break
  126. fi
  127. done
  128. ##### Parse config file #####
  129. shopt -s extglob
  130. if [ -r "$config" ]; then
  131. while IFS='= ' read -r lhs rhs; do
  132. if [[ ! $lhs =~ ^\ *# && -n $lhs ]]; then
  133. rhs="${rhs%%\#*}" # Del in line right comments
  134. rhs="${rhs%%*( )}" # Del trailing spaces
  135. rhs="${rhs%\"*}" # Del opening string quotes
  136. rhs="${rhs#\"*}" # Del closing string quotes
  137. declare $lhs="$rhs"
  138. echo "${lhs}=${!lhs}"
  139. fi
  140. done <"$config"
  141. fi
  142. unset config_paths config rhs lhs
  143. echo
  144. ##### Quality Check for user config #####
  145. [ "$max" -ge "$minmax" ] && [ "$max" -le "$maxmax" ] || usage "max must be larger than or equal to $minmax, and smaller than or equal to $maxmax."
  146. [ "$min" -gt 0 ] || usage "min must be larger than 0."
  147. [ "$timeout" -gt 0 ] || usage "timeout must be larger than 0."
  148. if [ -n "$ipv" ]; then
  149. [[ "$ipv" == 6 || "$ipv" == 4 || "$ipv" == "all" || "$ipv" == 64 ]]\
  150. || usage "wrong value for ipv: $ipv"
  151. fi
  152. (( min > max )) && usage "Minimum $min cannot be larger than maximum $max."
  153. if [[ "$out" != "" ]]; then
  154. out=( ${out//,/ } )
  155. do_resolvconf=0
  156. do_resolved=0
  157. do_networkd=0
  158. for i in "${out[@]}"; do
  159. case "$i" in
  160. resolvconf) do_resolvconf=1
  161. ;;
  162. resolved) do_resolved=1
  163. ;;
  164. networkd) do_networkd=1
  165. ;;
  166. esac
  167. done
  168. fi
  169. unset out
  170. if [[ "$countries" != "" ]]; then
  171. countries=( ${countries//,/ } )
  172. for ((i=0;i<${#countries[@]};i++)); do
  173. countries[i]="${countries[i],,}"
  174. [[ "${countries[i]}" == [a-z][a-z] ]] || usage "Invalid country ${countries[i]}"
  175. done
  176. else
  177. unset countries
  178. fi
  179. services=( ${services//,/ } )
  180. for i in "${services[@]}"; do
  181. [[ "$i" == *\.service ]] || usage "Invalid service $i"
  182. done
  183. ####################### END USER CONF #########################################
  184. curlopts=(
  185. #~ --resolve "${host}:443:${api_ip}"
  186. --connect-timeout "$timeout"
  187. --max-time "$timeout"
  188. )
  189. # json data of nameservers, obtained from opennic
  190. printf "Fetching %s\n" "$url"
  191. data="$(curl "${curlopts[@]}" "$url")"
  192. # fill array with all nameservers locations
  193. oldifs="$IFS";IFS=$'\n'
  194. loc=( $(jq -Mr '.[] .loc' <<<"$data") )
  195. IFS="$oldifs"
  196. # check locations against whitelist of allowed countries...
  197. for (( i=0 ; i<${#loc[@]} ; i++ )); do
  198. if (( ${#countries[@]} > 0 )); then
  199. # if countries are restricted, filter for suitable results
  200. for (( j=0 ; j<${#countries[@]} ; j++ )); do
  201. loc[i]="${loc[i],,}" # lowercase for comparison
  202. if [[ "${loc[i]##* }" == "${countries[j]}" ]]; then
  203. rs_grow "$i"
  204. break
  205. fi
  206. done
  207. else
  208. rs_grow "$i"
  209. fi
  210. done
  211. echo -e "\nRaw list: $ns results.\n$rs" >&2
  212. # how many results did we get?
  213. (( ns < min )) && echo "Not enough IPs for this configuration. Nothing changed." && exit 1
  214. if [[ "$do_resolvconf" == 1 ]] && [ -d "${resolvconf%/*}" ]; then
  215. rs_format "nameserver " > "$resolvconf"
  216. fi
  217. if [[ "$do_networkd" == 1 ]] && [ -d "${networkd%/*}" ]; then
  218. echo "[Network]" > "$networkd"
  219. rs_format "DNS=" >> "$networkd"
  220. fi
  221. if [[ "$do_resolved" == 1 ]] && [ -d "${resolved%/*}" ]; then
  222. echo "[Resolve]" > "$resolved"
  223. rs_format "DNS=" >> "$resolved"
  224. fi
  225. type -f systemctl >/dev/null || exit 0
  226. # Restart systemd services if active
  227. # According to wiki.archlinux.org/index.php/Domain_name_resolution, dhcpcd uses
  228. # the glibc resolver which reads /etc/resolv.conf for every name resolution
  229. # Thus, a dhcpcd.service restart should not be required.
  230. for s in "${services[@]}"; do
  231. systemctl --quiet is-active "$s" && systemctl restart "$s"
  232. done
  233. exit 0