#!/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 . 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 "$@"