1525 lines
36 KiB
Bash
1525 lines
36 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# OpenVPN helper to add DHCP information into systemd-resolved via DBus.
|
|
# Copyright (C) 2016, Jonathan Wright <jon@than.io>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
# This script will parse DHCP options set via OpenVPN (dhcp-option) to update
|
|
# systemd-resolved directly via DBus, instead of updating /etc/resolv.conf. To
|
|
# install, set as the 'up' and 'down' script in your OpenVPN configuration file
|
|
# or via the command-line arguments, alongside setting the 'down-pre' option to
|
|
# run the 'down' script before the device is closed. For example:
|
|
#
|
|
# script-security 2
|
|
# up /usr/local/libexec/openvpn/update-systemd-resolved
|
|
# up-restart
|
|
# down /usr/local/libexec/openvpn/update-systemd-resolved
|
|
# down-pre
|
|
|
|
# Define what needs to be called via DBus
|
|
DBUS_DEST="org.freedesktop.resolve1"
|
|
DBUS_NODE="/org/freedesktop/resolve1"
|
|
|
|
SCRIPT_NAME="${BASH_SOURCE[0]##*/}"
|
|
|
|
if [[ -S /dev/log ]] && command -v logger &> /dev/null; then
|
|
if [[ -t 2 ]]; then
|
|
log() {
|
|
logger -s -t "$SCRIPT_NAME" "$@"
|
|
}
|
|
else
|
|
# Suppress output on stderr when not attached to a (p|t)ty.
|
|
# https://github.com/jonathanio/update-systemd-resolved/issues/81
|
|
log() {
|
|
logger -t "$SCRIPT_NAME" "$@"
|
|
}
|
|
fi
|
|
|
|
for level in err warning info debug; do
|
|
printf -v functext -- '%s() { log -p user.%s -- "$@" ; }' "$level" "$level"
|
|
eval "$functext"
|
|
done
|
|
else
|
|
log() {
|
|
printf 1>&2 -- '%s: %s\n' "$SCRIPT_NAME" "$*"
|
|
}
|
|
|
|
for level in err warning info debug; do
|
|
printf -v functext -- '%s() { log "%s:" "$@" ; }' "$level" "${level^^}"
|
|
eval "$functext"
|
|
done
|
|
fi
|
|
|
|
usage() {
|
|
err "${1:?${1}. }. Usage: ${SCRIPT_NAME} up|down|print-polkit-rules [<options>]."
|
|
}
|
|
|
|
busctl_status() {
|
|
busctl status "$DBUS_DEST"
|
|
}
|
|
|
|
busctl_call() {
|
|
# Preserve busctl's exit status
|
|
busctl call "$DBUS_DEST" "$DBUS_NODE" "${DBUS_DEST}.Manager" "$@" || {
|
|
local -i status=$?
|
|
err "'busctl' exited with status $status"
|
|
print_polkit_rules_command_for_current_user | err
|
|
return $status
|
|
}
|
|
}
|
|
|
|
get_link_info() {
|
|
dev="$1"
|
|
shift
|
|
|
|
link=''
|
|
link="$(ip link show dev "$dev")" || return $?
|
|
|
|
echo "$dev" "${link%%:*}"
|
|
}
|
|
|
|
each_dhcp_setting() {
|
|
local foreign_option foreign_option_value setting_type setting_value
|
|
|
|
for foreign_option in "${!foreign_option_@}"; do
|
|
foreign_option_value="${!foreign_option}"
|
|
|
|
# Matches:
|
|
#
|
|
# dhcp-option SOME-SETTING a-value
|
|
# dhcp-option ANOTHER-SETTING
|
|
#
|
|
# In the second case, the setting value is the empty string.
|
|
if [[ $foreign_option_value =~ ^[[:space:]]*dhcp-option[[:space:]]+([^[:space:]]+)([[:space:]]+(.*))?$ ]]; then
|
|
"$@" "${BASH_REMATCH[1]}" "${BASH_REMATCH[3]-}" || return
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Check that a function was supplied the expected number of arguments. If not,
|
|
# issue a diagnostic message and return nonzero status.
|
|
usage_for() {
|
|
if [[ ${FUNCNAME[1]} != "${FUNCNAME[0]}" ]]; then
|
|
usage_for 4 - "$#" '<funcname> <argc-min> <argc-max> <argc-actual> <usage-string> [<usage-string> ...]' || return
|
|
fi
|
|
|
|
local caller="${FUNCNAME[1]}"
|
|
|
|
local -i argc_min
|
|
|
|
case "$1" in
|
|
-)
|
|
argc_min=0
|
|
;;
|
|
*)
|
|
argc_min="$1"
|
|
;;
|
|
esac
|
|
|
|
shift
|
|
|
|
local have_argc_max
|
|
local -i argc_max
|
|
|
|
case "$1" in
|
|
-) ;;
|
|
*)
|
|
have_argc_max=yes
|
|
argc_max="$1"
|
|
;;
|
|
esac
|
|
|
|
shift
|
|
|
|
local -i argc="$1"
|
|
shift
|
|
|
|
if ((argc < argc_min)) || { [[ -n ${have_argc_max-} ]] && ((argc > argc_max)); }; then
|
|
local expectation
|
|
if [[ -n ${have_argc_max-} ]]; then
|
|
if ((argc_min == argc_max)); then
|
|
expectation="exactly ${argc_min}"
|
|
else
|
|
expectation="from ${argc_min} to ${argc_max}"
|
|
fi
|
|
else
|
|
expectation="at least ${argc_min}"
|
|
fi
|
|
|
|
err "${caller}: got ${argc} argument(s); expected ${expectation}"
|
|
err "usage: ${caller} $*"
|
|
|
|
return 64 # EX_USAGE
|
|
fi
|
|
}
|
|
|
|
# mapfile wrapper that (unlike "mapfile -t somevar < <(some command)") bubbles
|
|
# up the exit status of the command used to generate the output read into the
|
|
# mapfile'd variable.
|
|
mapfile_from_command() {
|
|
usage_for 2 - "$#" '<varname> <command> [<arg> ...]' || return
|
|
|
|
local -a passthru
|
|
|
|
while (("$#" > 0)); do
|
|
case "$1" in
|
|
-d | -n | -O | -s | -u | -C | -c)
|
|
passthru+=("$1" "$2")
|
|
shift
|
|
;;
|
|
-t)
|
|
passthru+=("$1")
|
|
;;
|
|
--)
|
|
shift
|
|
break
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
|
|
shift
|
|
done
|
|
|
|
var="$1"
|
|
shift
|
|
|
|
local out
|
|
out="$("$@")" || return
|
|
|
|
# "printf" rather than herestring ("<<<"); avoids introducing a newline
|
|
mapfile "${passthru[@]}" "$var" < <(printf -- '%s' "$out")
|
|
}
|
|
|
|
# Work around this:
|
|
#
|
|
# $ IFS=$'.' read -r -a octets <<<192.168.1.1. # note trailing "."
|
|
# $ echo "${#octets[@]}"
|
|
# 4
|
|
# $ echo "${octets[-1]}"
|
|
# 1
|
|
# $ mapfile -d $'.' -t octets < <(printf -- '192.168.2.1.')
|
|
# $ echo "${#octets[@]}"
|
|
# 4
|
|
# $ echo "${octets[-1]}"
|
|
# 1
|
|
#
|
|
# This function is like "read -r -a" or "mapfile -t", except that it adds an
|
|
# empty string in the final spot in the generated array if the source string
|
|
# ends with the separator sequence.
|
|
#
|
|
# NOTE: uses "declare -n", so requires Bash >= 4.3
|
|
split_on_separator_into() {
|
|
usage_for 3 3 "$#" '<separator> <varname> <string>' || return
|
|
|
|
local sep="$1"
|
|
shift
|
|
|
|
local -n rvar="$1"
|
|
shift
|
|
|
|
# "printf" rather than herestring ("<<<"); avoids introducing a newline.
|
|
# Cannot count on "mapfile -d", which was released in the relatively-recent
|
|
# Bash 5.0, so use a workaround that handles only a single line of input.
|
|
IFS="$sep" read -r -a rvar < <(printf -- '%s' "$1") || :
|
|
|
|
if [[ $1 == *"$sep" ]]; then
|
|
rvar+=('')
|
|
fi
|
|
}
|
|
|
|
# Print the supplied arguments as a string joined with the specified separator
|
|
print_with_separator() {
|
|
usage_for 1 - "$#" '<separator> [<arg> <arg> ...]' || return
|
|
|
|
local sep="$1"
|
|
shift
|
|
|
|
printf -- '%s' "$1"
|
|
shift
|
|
|
|
if (("$#" < 1)); then
|
|
return
|
|
fi
|
|
|
|
printf -- "${sep}%s" "$@"
|
|
}
|
|
|
|
# Like "print_with_separator", but adds a final newline
|
|
puts_with_separator() {
|
|
usage_for 1 - "$#" '<separator> [<arg> <arg> ...]' || return
|
|
print_with_separator "$@" || return
|
|
printf -- '\n'
|
|
}
|
|
|
|
with_openvpn_script_handling() {
|
|
if (("$#" == 0)); then
|
|
usage 'No script type specified'
|
|
return 1
|
|
fi
|
|
|
|
local func="$1"
|
|
shift || :
|
|
|
|
local dev="${1:-${dev-}}"
|
|
shift || :
|
|
|
|
if [[ -z ${dev-} ]]; then
|
|
usage 'No device name specified'
|
|
return 1
|
|
fi
|
|
|
|
if ! read -r link if_index _ < <(get_link_info "$dev"); then
|
|
usage "Invalid device name: '$dev'"
|
|
return 1
|
|
fi
|
|
|
|
busctl_status &> /dev/null || {
|
|
local -i status="$?"
|
|
err << ERR
|
|
systemd-resolved DBus interface (${DBUS_DEST}) is not available.
|
|
$SCRIPT_NAME requires systemd version 229 or above.
|
|
ERR
|
|
return "$status"
|
|
}
|
|
|
|
if ! "$func" "$link" "$if_index" "$@"; then
|
|
err 'Unable to configure systemd-resolved.'
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
_up() {
|
|
local link="$1"
|
|
shift
|
|
local if_index="$1"
|
|
shift
|
|
|
|
info "Link '$link' coming up"
|
|
|
|
# Preset values for processing -- will be altered in the various process_*
|
|
# functions.
|
|
local -a dns_servers=() dns_ex_servers=() dns_domain=() dns_search=() dns_routed=() dnssec_negative_trust_anchors=()
|
|
local -i dns_server_count=0 dns_ex_server_count=0
|
|
local flush_caches=yes
|
|
local dns_sec reset_statistics reset_server_features default_route
|
|
local llmnr multicast_dns dns_over_tls
|
|
|
|
# This function is called indirectly below (via `each_dhcp_setting`); disable
|
|
# check for unreachable commands.
|
|
# shellcheck disable=SC2317
|
|
_dispatch_dhcp_setting() {
|
|
local setting_type="${1?}"
|
|
local setting_value="${2?}"
|
|
|
|
process_setting_function="${setting_type,,}"
|
|
process_setting_function="process_${process_setting_function//-/_}"
|
|
|
|
if declare -f "$process_setting_function" &> /dev/null; then
|
|
"$process_setting_function" "$setting_value" || return $?
|
|
else
|
|
warning "Not a recognized DHCP setting: '${setting_type}'"
|
|
fi
|
|
}
|
|
|
|
each_dhcp_setting _dispatch_dhcp_setting || return
|
|
|
|
if [[ ${reset_statistics-} == yes ]]; then
|
|
info "ResetStatistics()"
|
|
busctl_call ResetStatistics || return $?
|
|
fi
|
|
|
|
if [[ ${reset_server_features-} == yes ]]; then
|
|
info 'ResetServerFeatures()'
|
|
busctl_call ResetServerFeatures || return $?
|
|
fi
|
|
|
|
if [[ -n ${dns_sec+x} ]]; then
|
|
info "SetLinkDNSSEC(${if_index} '${dns_sec}')"
|
|
busctl_call SetLinkDNSSEC 'is' "$if_index" "${dns_sec}" || return
|
|
fi
|
|
|
|
if [[ ${#dns_servers[*]} -gt 0 ]]; then
|
|
busctl_params=("$if_index" "$dns_server_count" "${dns_servers[@]}")
|
|
info "SetLinkDNS(${busctl_params[*]})"
|
|
busctl_call SetLinkDNS 'ia(iay)' "${busctl_params[@]}" || return $?
|
|
fi
|
|
|
|
if [[ ${#dns_ex_servers[*]} -gt 0 ]]; then
|
|
busctl_params=("$if_index" "$dns_ex_server_count" "${dns_ex_servers[@]}")
|
|
info "SetLinkDNSEx(${busctl_params[*]})"
|
|
busctl_call SetLinkDNSEx 'ia(iayqs)' "${busctl_params[@]}" || return $?
|
|
fi
|
|
|
|
# Divide by two to account for the boolean second argument
|
|
dns_count="$(((${#dns_domain[*]} + ${#dns_search[*]} + ${#dns_routed[*]}) / 2))"
|
|
if ((dns_count > 0)); then
|
|
busctl_params=(
|
|
"$if_index"
|
|
"$dns_count"
|
|
|
|
# Hack to work around pre-4.4 Bash `empty array == unset` bug
|
|
${dns_domain:+"${dns_domain[@]}"}
|
|
${dns_search:+"${dns_search[@]}"}
|
|
${dns_routed:+"${dns_routed[@]}"}
|
|
)
|
|
info "SetLinkDomains(${busctl_params[*]})"
|
|
busctl_call SetLinkDomains 'ia(sb)' "${busctl_params[@]}" || return $?
|
|
fi
|
|
|
|
if [[ -n ${default_route-} ]]; then
|
|
info "SetLinkDefaultRoute(${if_index} ${default_route})"
|
|
busctl_call SetLinkDefaultRoute 'ib' "$if_index" "$default_route" || return $?
|
|
fi
|
|
|
|
if [[ -n ${llmnr+x} ]]; then
|
|
info "SetLinkLLMNR(${if_index} '${llmnr}')"
|
|
busctl_call SetLinkLLMNR 'is' "$if_index" "$llmnr"
|
|
fi
|
|
|
|
if [[ -n ${multicast_dns+x} ]]; then
|
|
info "SetLinkMulticastDNS(${if_index} '${multicast_dns}')"
|
|
busctl_call SetLinkMulticastDNS 'is' "$if_index" "$multicast_dns"
|
|
fi
|
|
|
|
if [[ -n ${dns_over_tls+x} ]]; then
|
|
info "SetLinkDNSOverTLS(${if_index} '${dns_over_tls}')"
|
|
busctl_call SetLinkDNSOverTLS 'is' "$if_index" "$dns_over_tls"
|
|
fi
|
|
|
|
if (("${#dnssec_negative_trust_anchors[*]}" > 0)); then
|
|
busctl_params=(
|
|
"$if_index"
|
|
"${#dnssec_negative_trust_anchors[*]}"
|
|
"${dnssec_negative_trust_anchors[@]}"
|
|
)
|
|
|
|
info "SetLinkDNSSECNegativeTrustAnchors(${busctl_params[*]})"
|
|
busctl_call SetLinkDNSSECNegativeTrustAnchors ias "${busctl_params[@]}"
|
|
fi
|
|
|
|
if [[ -n ${flush_caches-} ]]; then
|
|
info 'FlushCaches()'
|
|
busctl_call FlushCaches || return
|
|
fi
|
|
}
|
|
|
|
up() {
|
|
with_openvpn_script_handling _up "$@"
|
|
}
|
|
|
|
down() {
|
|
with_openvpn_script_handling _down "$@"
|
|
}
|
|
|
|
_down() {
|
|
local link="$1"
|
|
shift
|
|
local if_index="$1"
|
|
shift
|
|
|
|
info "Link '$link' going down"
|
|
|
|
if ! busctl_call RevertLink i "$if_index"; then
|
|
info 'Calling RevertLink failed; this can happen if privileges were dropped in the OpenVPN client.'
|
|
print_polkit_rules_command_for_current_user | info
|
|
fi
|
|
}
|
|
|
|
# Run sipcalc and extract a single line matching the provided prefix
|
|
match_sipcalc_output() {
|
|
usage_for 2 2 "$#" '<sipcalc-output-prefix-match> <address>' || return
|
|
|
|
local prefix="$1"
|
|
shift
|
|
|
|
local out
|
|
out="$(sipcalc "$@" 2> >(err))" || return
|
|
|
|
while read -r line; do
|
|
if [[ $line == "$prefix"* ]]; then
|
|
printf -- '%s\n' "${line##*- }"
|
|
return
|
|
fi
|
|
done <<< "$out"
|
|
|
|
return 1
|
|
}
|
|
|
|
# Expand an IPv4 or IPv6 address using Python's "ipaddress" module
|
|
expand_ip_python() {
|
|
usage_for 2 2 "$#" '{IPv4,IPv6} <address>' || return
|
|
|
|
local type="$1"
|
|
shift
|
|
|
|
case "$type" in
|
|
IPv4 | IPv6) ;;
|
|
*)
|
|
err "${FUNCNAME[0]}: not a valid IP version type: ${type}"
|
|
return 64
|
|
;;
|
|
esac
|
|
|
|
python -c "
|
|
import ipaddress
|
|
import sys
|
|
|
|
# Abort if we're on an older Python; the backported 'ipaddress' module requires
|
|
# IPs to be unicode, and properly decoding sys.argv is problematic on Python 2
|
|
# (see https://bugs.python.org/issue2128).
|
|
if sys.version_info < (3, 0):
|
|
majmin = '.'.join([str(v) for v in sys.version_info[0:2]])
|
|
sys.stderr.write('${type} address expansion is not supported for Python {0}\\n'.format(majmin))
|
|
sys.exit(1)
|
|
|
|
try:
|
|
print(ipaddress.${type}Address(sys.argv[1]).exploded)
|
|
except Exception as e:
|
|
sys.stderr.write(\"'{0}' is not a valid ${type} address: {1}\\n\".format(sys.argv[1], e))
|
|
sys.exit(1)
|
|
" "${1?}" 2> >(err)
|
|
}
|
|
|
|
# Very light check to see if a string looks vaguely in the vicinity of an IPv4
|
|
# address; more robust validation occurs in (the course of executing)
|
|
# "parse_ipv4".
|
|
#
|
|
# NOTE that we include the "! looks_like_ipv6" condition in order return a
|
|
# nonzero status when provided an IPv4-in-IPv6 address (e.g. "::ffff:1.2.3.4").
|
|
# This check comes after the check for dotted-quad so that we can do
|
|
#
|
|
# if looks_like_ipv6 "$address"; then
|
|
# process_dns_ipv6 "$address" || return $?
|
|
# elif looks_like_ipv4 "$address"; then
|
|
# process_dns_ipv4 "$address" || return $?
|
|
# else
|
|
#
|
|
# without repeating work.
|
|
looks_like_ipv4() {
|
|
[[ ${1-} =~ ^([^.]+\.){3}[^.]+$ ]] && ! looks_like_ipv6 "${1-}"
|
|
}
|
|
|
|
# Read the components of a dotted-quad IPv4 into the specified array variable
|
|
read_ipv4_segments_into() {
|
|
usage_for 2 2 "$#" '<varname> <string>' || return
|
|
|
|
split_on_separator_into $'.' "$@"
|
|
}
|
|
|
|
each_ipv4_segment() {
|
|
looks_like_ipv4 "$@" || return
|
|
|
|
local -a segments
|
|
read_ipv4_segments_into segments "$@" || return
|
|
|
|
((${#segments[@]} == 4)) || return
|
|
|
|
local segment
|
|
for segment in "${segments[@]}"; do
|
|
printf -- '%s\n' "$segment"
|
|
done
|
|
}
|
|
|
|
expand_ipv4_native() {
|
|
local address="$1"
|
|
|
|
local -a segments
|
|
mapfile_from_command -t segments each_ipv4_segment "$address" || return
|
|
|
|
log_invalid_ipv4() {
|
|
local message="'$address' is not a valid IPv4 address"
|
|
err "${message}: $*"
|
|
unset -f "${FUNCNAME[0]:-log_invalid_ipv4}"
|
|
return 1
|
|
}
|
|
|
|
local segment
|
|
local -i decimal_segment
|
|
|
|
for segment in "${segments[@]}"; do
|
|
printf -v decimal_segment -- '%d' "$segment" 2> /dev/null || {
|
|
local -i status="$?"
|
|
log_invalid_ipv4 "cannot interpret '${segment}' as a decimal number"
|
|
return "$status"
|
|
}
|
|
|
|
if ((decimal_segment < 0)) || ((decimal_segment > 255)); then
|
|
log_invalid_ipv4 "'${segment}' is not a decimal number from 0 to 255, inclusive"
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
puts_with_separator $'.' "${segments[@]}"
|
|
}
|
|
|
|
expand_ipv4_sipcalc() {
|
|
match_sipcalc_output 'Host address' "$@"
|
|
}
|
|
|
|
expand_ipv4_python() {
|
|
expand_ip_python IPv4 "$@"
|
|
}
|
|
|
|
parse_ipv4() {
|
|
local expanded
|
|
expanded="$(expand_ipv4 "$@")" || return
|
|
each_ipv4_segment "$expanded"
|
|
}
|
|
|
|
# Very light check to see if a string looks vaguely in the vicinity of an IPv6
|
|
# address; more robust validation occurs in (the course of executing)
|
|
# "parse_ipv6".
|
|
looks_like_ipv6() {
|
|
[[ ${1-} == *:*:* ]]
|
|
}
|
|
|
|
read_ipv6_segments_into() {
|
|
usage_for 2 2 "$#" '<varname> <address>' || return
|
|
|
|
split_on_separator_into $':' "$@"
|
|
}
|
|
|
|
each_ipv6_segment() {
|
|
looks_like_ipv6 "$@" || return
|
|
|
|
local -a segments
|
|
read_ipv6_segments_into segments "$@" || return
|
|
|
|
((${#segments[@]} == 8)) || return
|
|
|
|
local segment
|
|
for segment in "${segments[@]}"; do
|
|
printf -- '%s\n' "$segment"
|
|
done
|
|
}
|
|
|
|
expand_ipv6_native() {
|
|
local orig_address="${1-}"
|
|
|
|
log_invalid_ipv6() {
|
|
local message="'$orig_address' is not a valid IPv6 address"
|
|
err "${message}: $*"
|
|
unset -f "${FUNCNAME[0]:-log_invalid_ipv6}"
|
|
return 1
|
|
}
|
|
|
|
local -a orig_segments
|
|
read_ipv6_segments_into orig_segments "$orig_address" || {
|
|
local -i status="$?"
|
|
log_invalid_ipv6 'failed to read address segments'
|
|
return "$status"
|
|
}
|
|
|
|
if (("${#orig_segments[@]}" < 3)); then
|
|
log_invalid_ipv6 "expected at least 3 address segments; got ${#orig_segments[@]}"
|
|
return 1
|
|
fi
|
|
|
|
if looks_like_ipv4 "${orig_segments[-1]-}"; then
|
|
local -a ipv4_segments
|
|
|
|
mapfile_from_command -t ipv4_segments parse_ipv4 "${orig_segments[-1]}" || {
|
|
local -i status="$?"
|
|
log_invalid_ipv6 "failed to parse embedded IPv4 address '${orig_segments[-1]}'"
|
|
return "$status"
|
|
}
|
|
|
|
printf -v 'orig_segments[-1]' -- '%0.2x%0.2x' "${ipv4_segments[@]:0:2}"
|
|
printf -v "orig_segments[${#orig_segments[@]}]" -- '%0.2x%0.2x' "${ipv4_segments[@]:2:4}"
|
|
fi
|
|
|
|
local -i expected_len=8
|
|
local -i orig_len="${#orig_segments[@]}"
|
|
|
|
# "expected_len + 1" to account for addresses like "::1:1:1:1:1:1:1"
|
|
if ((orig_len > (expected_len + 1))); then
|
|
log_invalid_ipv6 "at most ${expected_len} colons permitted; got $((orig_len - 1))"
|
|
return 1
|
|
fi
|
|
|
|
local -a final_segments
|
|
local final_segment
|
|
local -i orig_idx
|
|
local saw_compressed_group
|
|
local -i zero_segments_needed_count
|
|
|
|
for ((orig_idx = 0; orig_idx < orig_len; orig_idx++)); do
|
|
orig_segment="${orig_segments[orig_idx]}"
|
|
|
|
if [[ -z $orig_segment ]]; then
|
|
if [[ -n ${saw_compressed_group-} ]]; then
|
|
log_invalid_ipv6 "at most one '::' permitted"
|
|
return 1
|
|
fi
|
|
|
|
saw_compressed_group=yes
|
|
|
|
if ((orig_idx == 0)); then
|
|
# ::1:2:3:4
|
|
if [[ -z ${orig_segments[$((orig_idx + 1))]-} ]]; then
|
|
zero_segments_needed_count="$(((expected_len - orig_len) + 2))"
|
|
((orig_idx++))
|
|
# :1:2:3:4
|
|
else
|
|
log_invalid_ipv6 "leading ':' without '::'"
|
|
return 1
|
|
fi
|
|
# 1:2:3:4::
|
|
elif {
|
|
((orig_idx == (orig_len - 2))) &&
|
|
[[ -z ${orig_segments[$((orig_idx + 1))]-} ]]
|
|
}; then
|
|
zero_segments_needed_count="$(((expected_len - orig_len) + 2))"
|
|
((orig_idx++))
|
|
# 1:2:3:4:
|
|
elif ((orig_idx == (orig_len - 1))); then
|
|
log_invalid_ipv6 "trailing ':' without '::'"
|
|
return 1
|
|
# 1:2::3:4
|
|
else
|
|
zero_segments_needed_count="$(((expected_len - orig_len) + 1))"
|
|
fi
|
|
|
|
if ((zero_segments_needed_count < 1)); then
|
|
log_invalid_ipv6 "cannot expand '::'; address already has 8 or more segments"
|
|
return 1
|
|
fi
|
|
|
|
local -i zero_segment_counter
|
|
for ((\
|
|
zero_segment_counter = 0; \
|
|
zero_segment_counter < zero_segments_needed_count; \
|
|
zero_segment_counter++)); do
|
|
final_segments+=(0000)
|
|
done
|
|
elif (("${#orig_segment}" > 4)); then
|
|
log_invalid_ipv6 "'$orig_segment' is longer than 4 characters"
|
|
return 1
|
|
else
|
|
printf -v final_segment -- '%0.4x' "0x${orig_segment}" 2> /dev/null || {
|
|
local -i status="$?"
|
|
log_invalid_ipv6 "cannot interpret '${orig_segment}' as a hexadecimal number"
|
|
return "$status"
|
|
}
|
|
|
|
final_segments+=("$final_segment")
|
|
fi
|
|
done
|
|
|
|
if (("${#final_segments[@]}" != expected_len)); then
|
|
log_invalid_ipv6 "expected ${expected_len} segments; got ${#final_segments[@]}"
|
|
return 1
|
|
fi
|
|
|
|
puts_with_separator $':' "${final_segments[@]}"
|
|
}
|
|
|
|
expand_ipv6_sipcalc() {
|
|
match_sipcalc_output 'Expanded Address' "$@"
|
|
}
|
|
|
|
expand_ipv6_python() {
|
|
expand_ip_python IPv6 "$@"
|
|
}
|
|
|
|
test_ipv4_expansion_func() {
|
|
local expanded
|
|
expanded="$("${1?}" 127.0.0.1)" && [[ $expanded == '127.0.0.1' ]]
|
|
}
|
|
|
|
test_ipv6_expansion_func() {
|
|
local expanded
|
|
expanded="$("${1?}" ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff)" &&
|
|
[[ $expanded == ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff ]]
|
|
}
|
|
|
|
each_ip_expansion_func() {
|
|
local cb="$1"
|
|
shift
|
|
|
|
local type name
|
|
|
|
case "${1,,}" in
|
|
ipv4)
|
|
type="${1,,}"
|
|
name=IPv4
|
|
;;
|
|
ipv6)
|
|
type="${1,,}"
|
|
name=IPv6
|
|
;;
|
|
*)
|
|
err "unrecognized IP version type '${1}'"
|
|
return 64
|
|
;;
|
|
esac
|
|
|
|
local expansion_func="expand_${type}"
|
|
local expansion_func_impl
|
|
local impl
|
|
|
|
for impl in python sipcalc native; do
|
|
expansion_func_impl="${expansion_func}_${impl}"
|
|
|
|
# Run in subshell with `logger` defined as a NOP to avoid issuing useless
|
|
# messages about (say) not being able to find the `python` or `sipcalc`
|
|
# programs.
|
|
# `log` is called indirectly; disable warning about unreachable command.
|
|
# shellcheck disable=SC2317
|
|
if (
|
|
log() { :; }
|
|
"test_${type}_expansion_func" "$expansion_func_impl"
|
|
); then
|
|
if "$cb" "$expansion_func_impl" 1; then
|
|
return
|
|
fi
|
|
elif "$cb" "$expansion_func_impl" 0; then
|
|
return
|
|
fi
|
|
done
|
|
}
|
|
|
|
set_up_ip_expansion_func() {
|
|
local type name
|
|
|
|
case "${1,,}" in
|
|
ipv4)
|
|
type="${1,,}"
|
|
name=IPv4
|
|
;;
|
|
ipv6)
|
|
type="${1,,}"
|
|
name=IPv6
|
|
;;
|
|
*)
|
|
err "unrecognized IP version type '${1}'"
|
|
return 64
|
|
;;
|
|
esac
|
|
|
|
local preference_var="UPDATE_SYSTEMD_RESOLVED_PREFERRED_${type^^}_EXPANSION_IMPLEMENTATION"
|
|
|
|
local preference
|
|
if [[ -v $preference_var ]]; then
|
|
preference="${!preference_var}"
|
|
fi
|
|
|
|
preference="${preference:-${UPDATE_SYSTEMD_RESOLVED_PREFERRED_IP_EXPANSION_IMPLEMENTATION-}}"
|
|
|
|
local expansion_func="expand_${type}"
|
|
|
|
local expansion_func_impl
|
|
|
|
if [[ -n $preference ]]; then
|
|
expansion_func_impl="${expansion_func}_${preference}"
|
|
|
|
if declare -f "$expansion_func_impl" &> /dev/null; then
|
|
eval "${expansion_func}() { $expansion_func_impl \"\$@\"; }"
|
|
else
|
|
err "${preference} is not a valid ${name} address expansion implementation"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if ! declare -f "$expansion_func" &> /dev/null; then
|
|
# This function is called indirectly below (via `each_ip_expansion_func`);
|
|
# disable check for unreachable commands.
|
|
# shellcheck disable=SC2317
|
|
choose_expansion_func_impl() {
|
|
expansion_func_impl="$1"
|
|
|
|
if (("$2" == 1)); then
|
|
eval "${expansion_func}() { $expansion_func_impl \"\$@\"; }"
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
each_ip_expansion_func choose_expansion_func_impl "$type"
|
|
|
|
unset -f choose_expansion_func_impl
|
|
fi
|
|
|
|
if ! declare -f "$expansion_func" &> /dev/null; then
|
|
err "no usable ${name} expansion implementations"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# "builtin exit" because the test suite overrides "exit". If we cannot handle
|
|
# IP addresses, no sense in continuing.
|
|
set_up_ip_expansion_func ipv4 || builtin exit
|
|
set_up_ip_expansion_func ipv6 || builtin exit
|
|
|
|
parse_ipv6() {
|
|
local expanded
|
|
expanded="$(expand_ipv6 "$@")" || return
|
|
each_ipv6_segment "$expanded"
|
|
}
|
|
|
|
parse_dns_spec() {
|
|
usage_for 1 - "$#" '<dns-server-spec> [<address-var> <port-var> <server-name-var>]' || return
|
|
|
|
local spec="$1"
|
|
shift
|
|
|
|
local -n address_ref="${1:-address}"
|
|
shift
|
|
|
|
local -n port_ref="${1:-port}"
|
|
shift
|
|
|
|
local -n server_name_ref="${1:-server_name}"
|
|
shift
|
|
|
|
local cursor="$spec"
|
|
while [[ -n ${cursor-} ]]; do
|
|
case "$cursor" in
|
|
*'#'?*)
|
|
server_name_ref="${cursor#*'#'}"
|
|
cursor="${cursor%%'#'*}"
|
|
;;
|
|
*:?*)
|
|
if looks_like_ipv6 "$cursor" &> /dev/null; then
|
|
address_ref="$cursor"
|
|
break
|
|
else
|
|
case "$cursor" in
|
|
'['*']'*)
|
|
case "$cursor" in
|
|
'['*']:'?*)
|
|
address_ref="${cursor#[}"
|
|
address_ref="${address_ref#]}"
|
|
port_ref="${cursor#*:}"
|
|
break
|
|
;;
|
|
*)
|
|
err "invalid DNS server specification '${spec}'"
|
|
return 1
|
|
;;
|
|
esac
|
|
;;
|
|
*)
|
|
address_ref="${cursor%%:*}"
|
|
port_ref="${cursor#*:}"
|
|
break
|
|
;;
|
|
esac
|
|
fi
|
|
;;
|
|
*)
|
|
address_ref="$cursor"
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Ensure that port variable is defined if server name variable is. The
|
|
# default value is `0`, meaning "default port 53" when passed to
|
|
# `SetLinkDNSEx`.
|
|
#
|
|
# NOTE that we do not do any further input validation here; instead we let
|
|
# `SetLinkDNSEx` complain if the port is anything other than an unsigned
|
|
# integer < 2 ** 16.
|
|
if [[ -n ${server_name_ref-} ]]; then
|
|
port_ref="${port_ref:-0}"
|
|
fi
|
|
|
|
# Ensure that server name variable is defined if port variable is. The
|
|
# default value is the empty string, meaning "no server name" when passed to
|
|
# `SetLinkDNSEx`.
|
|
if [[ -n ${port_ref-} ]]; then
|
|
server_name_ref="${server_name_ref-}"
|
|
fi
|
|
}
|
|
|
|
process_dns() {
|
|
local spec="$1"
|
|
shift
|
|
|
|
local address port server_name
|
|
parse_dns_spec "$spec" address port server_name || return
|
|
|
|
local -a args=()
|
|
if [[ -n ${port-} ]] || [[ -n ${server_name-} ]]; then
|
|
args=("${port:-0}" "${server_name-}")
|
|
fi
|
|
|
|
if looks_like_ipv6 "$address"; then
|
|
process_dns_ipv6 "$address" "${args[@]}" || return
|
|
elif looks_like_ipv4 "$address"; then
|
|
process_dns_ipv4 "$address" "${args[@]}" || return
|
|
else
|
|
err "Not a valid IPv6 or IPv4 address: '${address}' (full specification: '${spec}')"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
process_dns6() {
|
|
process_dns "$@"
|
|
}
|
|
|
|
process_dns_ipv4() {
|
|
usage_for 1 3 "$#" '<address> [<port> <server-name>]' || return
|
|
|
|
local address="$1"
|
|
shift
|
|
|
|
info "Adding IPv4 DNS Server ${address}"
|
|
|
|
local -a segments
|
|
mapfile_from_command -t segments parse_ipv4 "$address" || return
|
|
|
|
if (("$#" > 0)); then
|
|
dns_ex_servers+=(2 4 "${segments[@]}" "${1:-0}" "${2-}")
|
|
((dns_ex_server_count += 1))
|
|
else
|
|
dns_servers+=(2 4 "${segments[@]}")
|
|
((dns_server_count += 1))
|
|
fi
|
|
}
|
|
|
|
process_dns_ipv6() {
|
|
usage_for 1 3 "$#" '<address> [<port> <server-name>]' || return
|
|
|
|
local address="$1"
|
|
shift
|
|
|
|
info "Adding IPv6 DNS Server ${address}"
|
|
|
|
local -a segments
|
|
mapfile_from_command -t segments parse_ipv6 "$address" || return
|
|
|
|
if (("$#" > 0)); then
|
|
# Add AF_INET6 and byte count
|
|
dns_ex_servers+=(10 16)
|
|
for segment in "${segments[@]}"; do
|
|
dns_ex_servers+=("$((16#${segment:0:2}))" "$((16#${segment:2:2}))")
|
|
done
|
|
|
|
dns_ex_servers+=("${1:-0}" "${2-}")
|
|
|
|
((dns_ex_server_count += 1))
|
|
else
|
|
# Add AF_INET6 and byte count
|
|
dns_servers+=(10 16)
|
|
for segment in "${segments[@]}"; do
|
|
dns_servers+=("$((16#${segment:0:2}))" "$((16#${segment:2:2}))")
|
|
done
|
|
|
|
((dns_server_count += 1))
|
|
fi
|
|
}
|
|
|
|
process_domain() {
|
|
local domain="$1"
|
|
shift
|
|
|
|
info "Adding DNS Domain ${domain}"
|
|
|
|
# Make sure the first domain specified with "dhcp-option DOMAIN <domain>"
|
|
# appears at the head of the list we pass to SetLinkDNS.
|
|
if (("${#dns_domain[*]}" == 0)); then
|
|
dns_domain+=("${domain}" false)
|
|
else
|
|
dns_search+=("${domain}" false)
|
|
fi
|
|
}
|
|
|
|
process_adapter_domain_suffix() {
|
|
# This enables support for ADAPTER_DOMAIN_SUFFIX which is a Microsoft standard
|
|
# which works in the same way as DOMAIN to set the primary search domain on
|
|
# this specific link.
|
|
process_domain "$@"
|
|
}
|
|
|
|
process_domain_search() {
|
|
local domain="$1"
|
|
shift
|
|
|
|
info "Adding DNS Search Domain ${domain}"
|
|
dns_search+=("${domain}" false)
|
|
}
|
|
|
|
process_domain_route() {
|
|
local domain="$1"
|
|
shift
|
|
|
|
info "Adding DNS Routed Domain ${domain}"
|
|
dns_routed+=("${domain}" true)
|
|
}
|
|
|
|
process_dnssec() {
|
|
case "${1,,}" in
|
|
yes | true)
|
|
dns_sec=yes
|
|
;;
|
|
no | false)
|
|
dns_sec=no
|
|
;;
|
|
allow-downgrade)
|
|
dns_sec=allow-downgrade
|
|
;;
|
|
default)
|
|
dns_sec=""
|
|
;;
|
|
*)
|
|
err "'$1' is not a valid DNSSEC option"
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
info "Setting DNSSEC to ${dns_sec:-default}"
|
|
}
|
|
|
|
process_reset_statistics() {
|
|
case "${1,,}" in
|
|
yes | true)
|
|
reset_statistics=yes
|
|
;;
|
|
no | false)
|
|
reset_statistics=""
|
|
;;
|
|
*)
|
|
err "'$1' is not a valid value for RESET-STATISTICS"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
process_flush_caches() {
|
|
case "${1,,}" in
|
|
yes | true)
|
|
flush_caches=yes
|
|
;;
|
|
no | false)
|
|
flush_caches=""
|
|
;;
|
|
*)
|
|
err "'$1' is not a valid value for FLUSH-CACHES"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
process_reset_server_features() {
|
|
case "${1,,}" in
|
|
yes | true)
|
|
reset_server_features=yes
|
|
;;
|
|
no | false)
|
|
reset_server_features=""
|
|
;;
|
|
*)
|
|
err "'$1' is not a valid value for RESET-SERVER-FEATURES"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
process_default_route() {
|
|
case "${1,,}" in
|
|
yes | true)
|
|
default_route=true
|
|
;;
|
|
no | false)
|
|
default_route=false
|
|
;;
|
|
*)
|
|
err "'$1' is not a valid value for DEFAULT-ROUTE"
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
info "Setting DEFAULT-ROUTE to ${default_route}"
|
|
}
|
|
|
|
process_llmnr() {
|
|
case "${1,,}" in
|
|
yes | true)
|
|
llmnr=yes
|
|
;;
|
|
no | false)
|
|
llmnr=no
|
|
;;
|
|
resolve)
|
|
llmnr=resolve
|
|
;;
|
|
default)
|
|
llmnr=""
|
|
;;
|
|
*)
|
|
err "'$1' is not a valid value for LLMNR"
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
info "Setting LLMNR to ${llmnr:-default}"
|
|
}
|
|
|
|
process_multicast_dns() {
|
|
case "${1,,}" in
|
|
yes | true)
|
|
multicast_dns=yes
|
|
;;
|
|
no | false)
|
|
multicast_dns=no
|
|
;;
|
|
resolve)
|
|
multicast_dns=resolve
|
|
;;
|
|
default)
|
|
multicast_dns=""
|
|
;;
|
|
*)
|
|
err "'$1' is not a valid value for MULTICAST-DNS"
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
info "Setting MULTICAST-DNS to ${multicast_dns:-default}"
|
|
}
|
|
|
|
process_dns_over_tls() {
|
|
case "${1,,}" in
|
|
yes | true)
|
|
dns_over_tls=yes
|
|
;;
|
|
no | false)
|
|
dns_over_tls=no
|
|
;;
|
|
opportunistic)
|
|
dns_over_tls=opportunistic
|
|
;;
|
|
default)
|
|
dns_over_tls=""
|
|
;;
|
|
*)
|
|
err "'$1' is not a valid value for DNS-OVER-TLS"
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
info "Setting DNS-OVER-TLS to ${dns_over_tls:-default}"
|
|
}
|
|
|
|
process_dnssec_negative_trust_anchors() {
|
|
local domain="$1"
|
|
shift
|
|
|
|
info "Adding DNSSEC negative trust anchor ${domain}"
|
|
dnssec_negative_trust_anchors+=("$domain")
|
|
}
|
|
|
|
to_json_array_jq() {
|
|
jq --compact-output --null-input '$ARGS.positional' --args -- "$@"
|
|
}
|
|
|
|
to_json_array_perl() {
|
|
perl -MModule::Load -wle '
|
|
foreach my $mod ( qw(Cpanel::JSON::XS JSON::MaybeXS JSON::XS JSON::PP JSON) ) {
|
|
if ( eval { load $mod; $mod->import(qw(encode_json)); 1 } ) {
|
|
print encode_json(\@ARGV);
|
|
last;
|
|
}
|
|
}
|
|
' -- "$@"
|
|
}
|
|
|
|
to_json_array_python() {
|
|
python -c "
|
|
import sys
|
|
|
|
try:
|
|
import json
|
|
except ImportError:
|
|
import simplejson as json
|
|
|
|
print(json.dumps(sys.argv[1:]))
|
|
" "$@"
|
|
}
|
|
|
|
to_json_array_native() {
|
|
printf -- '['
|
|
|
|
while (("$#" > 0)); do
|
|
printf -- '"%s"' "${1//\"/\\\"}"
|
|
shift
|
|
if (("$#" > 0)); then
|
|
printf -- ','
|
|
fi
|
|
done
|
|
|
|
printf -- ']'
|
|
}
|
|
|
|
test_to_json_array_func() {
|
|
local expanded
|
|
expanded="$("${1?}" foo bar baz)" && [[ $expanded =~ ^\['"foo",'[[:space:]]*'"bar",'[[:space:]]*'"baz"'\]$ ]]
|
|
}
|
|
|
|
set_up_to_json_array_func() {
|
|
local expansion_func_impl
|
|
local impl
|
|
|
|
for impl in jq perl python native; do
|
|
expansion_func_impl="to_json_array_${impl}"
|
|
if test_to_json_array_func "$expansion_func_impl" 2> /dev/null; then
|
|
eval "to_json_array() { $expansion_func_impl \"\$@\"; }"
|
|
return
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
if ! set_up_to_json_array_func; then
|
|
to_json_array_func() {
|
|
printf -- 'Unable to serialize arguments to a JSON array'
|
|
return 127
|
|
}
|
|
fi
|
|
|
|
require_optarg() {
|
|
local opt="$1"
|
|
shift
|
|
|
|
local argc="$1"
|
|
shift
|
|
|
|
if ((argc < 2)); then
|
|
err "missing required argument for option \"$opt\""
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# shellcheck disable=SC2120
|
|
print_polkit_rules() {
|
|
local -A allowed_users_map=() allowed_groups_map=() systemd_openvpn_units_map=()
|
|
|
|
while (("$#" > 0)); do
|
|
case "$1" in
|
|
--polkit-allowed-user)
|
|
require_optarg "$1" "$#" || return
|
|
allowed_users_map["${2?}"]=1
|
|
shift
|
|
;;
|
|
--polkit-allowed-user=?*)
|
|
allowed_users_map["${1#*=}"]=1
|
|
;;
|
|
--polkit-allowed-group)
|
|
require_optarg "$1" "$#" || return
|
|
allowed_groups_map["${2?}"]=1
|
|
shift
|
|
;;
|
|
--polkit-allowed-group=?*)
|
|
allowed_groups_map["${1#*=}"]=1
|
|
;;
|
|
--polkit-systemd-openvpn-unit)
|
|
require_optarg "$1" "$#" || return
|
|
systemd_openvpn_units_map["${2?}"]=1
|
|
shift
|
|
;;
|
|
--polkit-systemd-openvpn-unit=?*)
|
|
systemd_openvpn_units_map["${1#*=}"]=1
|
|
;;
|
|
*)
|
|
err "unrecognized option: $1"
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
shift
|
|
done
|
|
|
|
if {
|
|
(("${#systemd_openvpn_units_map[@]}" < 1)) &&
|
|
(("${#allowed_users_map[@]}" < 1)) &&
|
|
(("${#allowed_groups_map[@]}" < 1))
|
|
}; then
|
|
# NOTE that we cannot use the template unit "openvpn-client@.service"
|
|
# itself:
|
|
#
|
|
# $ systemctl show -p User openvpn-client@.service
|
|
# Failed to get properties: Unit name openvpn-client@.service is neither a valid invocation ID nor unit name.
|
|
# $ systemctl show -p User openvpn-client@utterly-bogus.service
|
|
# User=openvpn
|
|
#
|
|
systemd_openvpn_units_map["openvpn-client@totally-made-up-to-avoid-collisions-${RANDOM:-12345}.service"]=1
|
|
fi
|
|
|
|
local allowed_user
|
|
while read -r allowed_user; do
|
|
if [[ -n ${allowed_user-} ]]; then
|
|
allowed_users_map["$allowed_user"]=1
|
|
fi
|
|
done < <(systemctl show -P User "${!systemd_openvpn_units_map[@]}" 2> /dev/null)
|
|
|
|
if ((${#allowed_users_map[@]} < 1)); then
|
|
warning 'unable to determine the value(s) of "User=..." for OpenVPN client systemd units; assuming "root".'
|
|
allowed_users_map[root]=1
|
|
fi
|
|
|
|
local allowed_group
|
|
while read -r allowed_group; do
|
|
if [[ -n ${allowed_group-} ]]; then
|
|
allowed_groups_map["$allowed_group"]=1
|
|
fi
|
|
done < <(systemctl show -P Group "${!systemd_openvpn_units_map[@]}" 2> /dev/null)
|
|
|
|
if ((${#allowed_groups_map[@]} < 1)); then
|
|
err 'unable to determine the value(s) of "Group=..." for OpenVPN client systemd units; assuming "root".'
|
|
allowed_groups_map[root]=1
|
|
fi
|
|
|
|
local allowed_users allowed_groups
|
|
allowed_users="$(to_json_array "${!allowed_users_map[@]}")" || return
|
|
allowed_groups="$(to_json_array "${!allowed_groups_map[@]}")" || return
|
|
|
|
printf -- \
|
|
'/*
|
|
* Allow OpenVPN client services to update systemd-resolved settings.
|
|
* Added by %s.
|
|
*/
|
|
|
|
function listToBoolMap(list) {
|
|
var result = {};
|
|
|
|
for (var i = 0; i < list.length; i++) {
|
|
var item = list[i];
|
|
result[item] = true;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
const updateSystemdResolved = {
|
|
allowedUsers: listToBoolMap(%s),
|
|
|
|
allowedGroups: %s,
|
|
|
|
allowedSubactions: listToBoolMap([
|
|
"set-dns-servers",
|
|
"set-domains",
|
|
"set-default-route",
|
|
"set-llmnr",
|
|
"set-mdns",
|
|
"set-dns-over-tls",
|
|
"set-dnssec",
|
|
"set-dnssec-negative-trust-anchors",
|
|
"revert"
|
|
]),
|
|
|
|
actionIsAllowed: function(action) {
|
|
if ( !action.id.startsWith("org.freedesktop.resolve1.") ) {
|
|
return false;
|
|
}
|
|
|
|
var ns = action.id.split(".");
|
|
var subaction = ns[ns.length - 1];
|
|
|
|
return this.allowedSubactions[subaction];
|
|
},
|
|
|
|
subjectIsAllowed: function(subject) {
|
|
if ( this.allowedUsers[subject.user] ) {
|
|
return true;
|
|
}
|
|
|
|
return this.allowedGroups.some(function(group) {
|
|
subject.isInGroup(group);
|
|
});
|
|
},
|
|
|
|
isAllowed: function(action, subject) {
|
|
return this.actionIsAllowed(action) && this.subjectIsAllowed(subject);
|
|
}
|
|
};
|
|
|
|
polkit.addRule(function(action, subject) {
|
|
if ( updateSystemdResolved.isAllowed(action, subject) ) {
|
|
return polkit.Result.YES;
|
|
} else {
|
|
return polkit.Result.NOT_HANDLED;
|
|
}
|
|
});
|
|
' "$SCRIPT_NAME" "$allowed_users" "$allowed_groups"
|
|
}
|
|
|
|
print_polkit_rules_command_for_current_user() {
|
|
local current_user current_group
|
|
|
|
local format='You may wish to add the output of the following command'
|
|
format+=' to your polkit rules in order to authorize your user to access'
|
|
format+=' the systemd-resolved DBus interface:'
|
|
format+='\n%q print-polkit-rules'
|
|
|
|
local -a args=("$SCRIPT_NAME")
|
|
|
|
if current_user="$(id -u -n 2> /dev/null)" && [[ -n ${current_user-} ]]; then
|
|
format+=' --polkit-allowed-user %q'
|
|
args+=("$current_user")
|
|
fi
|
|
|
|
if current_group="$(id -g -n 2> /dev/null)" && [[ -n ${current_group-} ]]; then
|
|
format+=' --polkit-allowed-group %q'
|
|
args+=("$current_group")
|
|
fi
|
|
|
|
format+='\nPlease see %s for additional details on configuring polkit.\n'
|
|
args+=('https://github.com/tomeon/update-systemd-resolved/tree/polkit-rules-definition#policykit-rules')
|
|
|
|
# shellcheck disable=SC2059
|
|
printf -- "$format" "${args[@]}"
|
|
}
|
|
|
|
main() {
|
|
local action
|
|
while (("$#" > 0)); do
|
|
case "$1" in
|
|
up | down | print-polkit-rules)
|
|
action="$1"
|
|
;;
|
|
--)
|
|
shift
|
|
break
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
|
|
shift
|
|
done
|
|
|
|
action="${action:-${script_type:-down}}"
|
|
action="${action//-/_}"
|
|
|
|
if ! declare -f "${action}" &> /dev/null; then
|
|
usage "Invalid script type: '${action}'"
|
|
return 1
|
|
fi
|
|
|
|
"$action" "$@"
|
|
}
|
|
|
|
if [[ ${BASH_SOURCE[0]} == "$0" ]] || [[ ${AUTOMATED_TESTING-} == 1 ]]; then
|
|
set -o nounset
|
|
|
|
main "$@"
|
|
fi
|