petrified/petrified
2024-03-20 09:32:36 -05:00

282 lines
7.3 KiB
Bash
Executable file

#!/usr/bin/env bash
#
## petrified - bash client to update dynamic DNS at freedns.afraid.org
## Copyright (c) 2014 Troy Engel
## Version: 2.0.2
#
# 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, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
## One of these must exist - '-c <config>' ignores global/local entirely
CONF_GLOBAL=/etc/petrified.conf
CONF_LOCAL=~/.petrifiedrc
CONF_NAMED=""
# Check our arguments for a config file
while getopts ":c:" opt; do
case $opt in
c)
CONF_NAMED="$OPTARG"
;;
:)
echo "Option -$OPTARG requires the name of a config file." >&2
exit 1
;;
esac
done
## v2 API - How to update DNS - DDNS_KEY is unique per domain
DDNS_URL=""
DDNS_KEY=""
## From Josh, v1 key is variable but v2 is fixed length @24char. When set
## to 0 (auto) the length of the DDNS_KEY will be used to choose which API
## endpoint, otherwise specify 1 (API v1) or 2 (API v2) directly
DDNS_API=0
## API URL endpoints, no trailing slashes
DDNS_V1U=https://freedns.afraid.org/dynamic/update.php
DDNS_V2U=https://sync.afraid.org/u
## Which URL to use to get an IP
# IPv4 icanhazip (http://major.io/icanhazip-com-faq/)
DDNS_CHECK="http://4.icanhazip.com"
# IPv6 icanhazip (http://major.io/icanhazip-com-faq/)
#DDNS_CHECK="http://6.icanhazip.com"
## How many seconds should curl wait when either checking your IP or
## trying to update the remote DNS
CURL_WAIT=10
## Report an internal IP instead of the public IP - useful if you're
## using dynamic DNS for machines on the internal network
LOCAL_MODE=0
# Specify which interface
LOCAL_IF=eth0
# Should we use IPv4 or IPv6
LOCAL_IV=4
## How to log - multiple supported
USE_JOURNAL=1
USE_SYSLOG=0
USE_STDOUT=0
USE_LOGFILE=0
## If set, does not log if the IP has not changed
LOG_QUIET=0
## If USE_LOGFILE is 1, where to log
PET_LOG=/var/log/petrified.log
## Prevent race conditions, i.e. stuck crons piling up
USE_PID=1
PET_PID=/run/petrified.pid
## Save the IP from our last check
USE_LIP=1
PET_LIP=/var/cache/petrified/lastip.dat
## Dependencies:
# logger (util-linux)
# kill (util-linux)
# printf (coreutils)
# touch (coreutils)
# date (coreutils)
# stat (coreutils)
# cat (coreutils)
# rm (coreutils)
# bash (bash)
# ip (iproute2)
# curl (curl)
####
# Read in our configs
if [[ -z "${CONF_NAMED}" ]]; then
[[ -r "${CONF_GLOBAL}" ]] && source "${CONF_GLOBAL}"
[[ -r "${CONF_LOCAL}" ]] && source "${CONF_LOCAL}"
else
[[ -r "${CONF_NAMED}" ]] && source "${CONF_NAMED}"
fi
# Check that we have all the needed variables
if [[ -z "${DDNS_KEY}" ]]; then
echo "DDNS_KEY must be configured, exiting."
exit 1
else
# Set the API version endpoint, IP added later
case ${DDNS_API} in
0)
# From Josh, v2 is fixed @24 but v1 is variable (typically >24)
if [[ ${#DDNS_KEY} -ne 24 ]]; then
DDNS_URL="${DDNS_V1U}?${DDNS_KEY}&address="
else
DDNS_URL="${DDNS_V2U}/${DDNS_KEY}/?ip="
fi
;;
1)
DDNS_URL="${DDNS_V1U}?${DDNS_KEY}&address="
;;
2)
DDNS_URL="${DDNS_V2U}/${DDNS_KEY}/?ip="
;;
*)
echo "DDNS_API must be 0, 1 or 2; exiting."
exit 1
;;
esac
fi
# Make sure that PET_LOG will work if required
if (( ${USE_LOGFILE} == 1 )); then
if [[ -z "${PET_LOG}" ]]; then
echo "USE_LOGFILE=1 but PET_LOG is an empty string, exiting."
exit 1
elif (( $(touch "${PET_LOG}" 2>/dev/null; echo $?;) != 0 )); then
echo "USE_LOGFILE=1 but cannot write to ${PET_LOG}, exiting."
exit 1
fi
fi
# Logging
logmsg () {
LMSG=$1
if (( ${USE_JOURNAL} == 1 )); then
printf "%s\n%s\n" "SYSLOG_IDENTIFIER=petrified" "MESSAGE=${LMSG}" | \
logger --journald
fi
if (( ${USE_SYSLOG} == 1 )); then
echo "${LMSG}" | logger -t petrified
fi
if (( ${USE_STDOUT} == 1 )); then
DTS=$(date +"%Y-%m-%d %H:%M:%S")
echo "[${DTS}] [petrified] ${LMSG}"
fi
if (( ${USE_LOGFILE} == 1 )); then
DTS=$(date +"%Y-%m-%d %H:%M:%S")
echo "[${DTS}] [petrified] ${LMSG}" >> "${PET_LOG}"
fi
return
}
# Security warning
statchk () {
_CFILE=$1
if [[ -f "${_CFILE}" ]] && [[ -r "${_CFILE}" ]]; then
if [[ $(stat -c "%a" "${_CFILE}") != 600 ]]; then
logmsg "Security warning: ${_CFILE} is readable but not mode 0600"
fi
fi
}
[[ -n "${CONF_GLOBAL}" ]] && statchk "${CONF_GLOBAL}"
[[ -n "${CONF_LOCAL}" ]] && statchk "${CONF_LOCAL}"
[[ -n "${CONF_NAMED}" ]] && statchk "${CONF_NAMED}"
# Make sure that PET_PID will work if required
if (( ${USE_PID} == 1 )); then
if [[ -z "${PET_PID}" ]]; then
logmsg "USE_PID=1 but PET_PID is an empty string, exiting."
exit 1
elif (( $(touch "${PET_PID}" 2>/dev/null; echo $?;) != 0 )); then
logmsg "USE_PID=1 but cannot write to ${PET_PID}, exiting."
exit 1
fi
fi
# PID actions
if (( ${USE_PID} == 1 )); then
PIDNUM=$(cat "${PET_PID}" 2>/dev/null)
# check if we have a number
if [[ ${PIDMUM} =~ ^-?[0-9]+$ ]]; then
kill -0 ${PIDNUM}
if (( $? == 0 )); then
logmsg "Detected a running process ${PIDNUM}, exiting."
exit 1
else
logmsg "Stale PID ${PIDNUM} detected, overwriting."
fi
fi
echo ${BASHPID} > "${PET_PID}"
fi
# Cleanup actions
cleanup () {
(( ${USE_PID} == 1 )) && (rm -f "${PET_PID}" 1>/dev/null 2>&1)
(( ${USE_LIP} == 0 )) && (rm -f "${PET_LIP}" 1>/dev/null 2>&1)
return
}
# Trap the basic signals
sigexit () {
logmsg "Trapped a kill signal, exiting."
cleanup
exit 3
}
trap sigexit SIGHUP SIGINT SIGTERM
# Get our IP address
NEWIP=""
if (( ${LOCAL_MODE} == 1 )); then
NEWIP=$(ip -${LOCAL_IV} -o addr show dev ${LOCAL_IF} primary 2>/dev/null)
NEWIP=${NEWIP%%/*}
NEWIP=${NEWIP##* }
else
NEWIP=$(curl -m ${CURL_WAIT} -s ${DDNS_CHECK} 2>/dev/null)
fi
if [[ -z "${NEWIP}" ]]; then
logmsg "Error getting an IP address, exiting."
cleanup
exit 1
fi
# Check our saved IP against the new IP
OLDIP="0"
if (( ${USE_LIP} == 1 )); then
OLDIP=$(cat "${PET_LIP}" 2>/dev/null)
fi
# If they don't match, tell upstream
__UPDATED=0
if [[ "${OLDIP}" != "${NEWIP}" ]]; then
UPDURL="${DDNS_URL}${NEWIP}"
RESULT=$(curl -m ${CURL_WAIT} -sk "${UPDURL}" 2>/dev/null)
logmsg "${RESULT}"
# "Updated foo from 1.2.3.4 to 5.6.7.8"
# "No IP change detected for foo with IP 1.2.3.4, skipping update"
_RE1='^Updated'
_RE2='^No IP change detected'
if [[ ${RESULT} =~ ${_RE1} ]] || [[ ${RESULT} =~ ${_RE2} ]]; then
# if _RE2 matched, our PET_LIP is stale
__UPDATED=1
fi
else
if (( ${LOG_QUIET} == 0 )); then
logmsg "IP ${NEWIP} hasn't changed, not updating."
fi
fi
# Save the new IP if configured
if (( ${USE_LIP} == 1 )) && (( ${__UPDATED} == 1 )); then
if (( $(touch "${PET_LIP}" 2>/dev/null; echo $?;) != 0 )); then
logmsg "USE_LIP=1 but cannot write to ${PET_LIP}."
else
echo "${NEWIP}" > "${PET_LIP}"
fi
fi
# Take out the trash
cleanup
exit 0