You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

170 lines
5.3 KiB

#!/usr/bin/env bash
# homeserverdns - https://ulrich.earth/code/homeserverdns
#
# Copyright (C) 2018 Christian Ulrich
#
# 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/>.
readonly required_config_options=("protocol" "auth_key" "domains")
readonly default_ttl=300
readonly default_public_ip4_hook="upnpc -s | grep ExternalIPAddress | cut -d ' ' -f3"
readonly default_update_hook="homeserverdns-update"
### () -> default_network_interface
function get_interface {
ip -o -6 route show to default | cut -d " " -f5
}
### (ip_address) -> bool
function is_unique_local_address {
prefix=$(echo $1 | cut -d ":" -f1)
[ "${prefix}" == "fd00" ] && echo true || echo false
}
### (ip_oneline_output) -> ip6_address
function extract_ip6 {
ip_address=$(echo $1 | tr -s " " | cut -d " " -f4 | cut -d "/" -f1)
[ "$(is_unique_local_address $ip_address)" == false ] && echo $ip_address || echo ""
}
### Returns an IPv6 address extracted from a oneline output of ip monitor if
### it's global, used for routing and still in use. Returns "" otherwise.
### (ip_monitor_output) -> ip6_address
function parse_ip_monitor_output {
filtered=$(echo $1 | grep -Ev '^Deleted|temporary|deprecated|noprefixroute|preferred_lft 0sec' | grep "scope global")
[ -n "${filtered}" ] && echo $(extract_ip6 "${filtered}") || echo ""
}
### (config) -> external_ip4_address
function lookup_ip4 {
eval "declare -A config="${1#*=}
local public_ip4_hook=${config["public_ip4_hook"]}
echo $(eval ${public_ip4_hook:-$default_public_ip4_hook})
}
### () -> global_ip6_address
function lookup_ip6 {
iface=$(get_interface)
ip_output=$(ip -6 -o address show dev $iface scope global -temporary -deprecated -noprefixroute | head -n1)
echo $(extract_ip6 "${ip_output}")
}
### (config_path) -> config
function parse_config {
declare -A config
while IFS= read -r line
do
if [ -n "${line}" ] && [[ $line != \#* ]]; then
local key value
IFS="=" read -r key value <<< $line
[ -n "${key}" ] && [ -n "${value}" ] && config[$key]=$value
fi
done < $1
declare -p config
}
### utility function for joining strings
### (delimiter, list) -> joined_list
function join_by {
local IFS=$1
shift
echo "$*"
}
### utility function for splitting a full-qualified domain name
### (fqdn) -> (record_name, second_level_domain)
function split_domain {
local tokens=(${1//./ })
local token_count=${#tokens[*]}
local second_level_domain=${tokens[@]:$((token_count - 2))}
if [ "${token_count}" -gt 2 ]; then
local record_name=${tokens[@]:0:$((token_count - 2))}
else
local record_name=("@")
fi
echo $(join_by . ${record_name[@]})" "$(join_by . ${second_level_domain[@]})
}
### (ip4, ip6, config) -> log_output
function update {
eval "declare -A config="${3#*=}
local domains=(${config["domains"]})
local update_hook=${config["update_hook"]}
export PROTOCOL=${config["protocol"]}
export API_ADDRESS=${config["api_address"]}
export USER=${config["user"]}
export AUTH_KEY=${config["auth_key"]}
#export IP4=$1
#export IP6=$2
export TTL=${config["ttl"]:-$default_ttl}
for domain in "${domains[@]}"; do
local record_name second_level_domain
read -r record_name second_level_domain <<< $(split_domain $domain)
export DOMAIN=$domain
export L2_DOMAIN=$second_level_domain
export RECORD_NAME=$record_name
echo "[$(date "+%Y-%m-%d %H:%M")] updating ${domain} ..."
if [ -n "$1" ]; then
echo -n "| setting A=$1 ... "
echo "status: "$(eval "IP4=$1 ${update_hook:-$default_update_hook}")
fi
if [ -n "$2" ]; then
echo -n "| setting AAAA=$2 ... "
echo "status: "$(eval "IP6=$2 ${update_hook:-$default_update_hook}")
fi
done
}
function main {
if [ "$#" -ne 1 ]; then
echo "usage: $0 CONFIG_FILE"
exit 1
fi
config=$(parse_config $1)
eval "declare -A config="${config#*=}
for key in "${required_config_options[@]}"; do
if [ -z "${config[$key]}" ]; then
echo "configuration is missing option \"${key}\"" >&2
exit 1
fi
done
local ip4=$(lookup_ip4 "$(declare -p config)")
local ip6=$(lookup_ip6)
update "${ip4}" "${ip6}" "$(declare -p config)"
ip -6 -o monitor address dev $(get_interface) |
while IFS= read -r line
do
local new_ip6=$(parse_ip_monitor_output "${line}")
#echo "ip-monitor output was ${line}"
if [ -n "${new_ip6}" ] && [ "${new_ip6}" != "${ip6}" ]; then
ip4=$(lookup_ip4 "$(declare -p config)")
ip6=$new_ip6
update "${ip4}" "${ip6}" "$(declare -p config)"
fi
done
}
main "$@"