Add version check and auto-update

This commit is contained in:
Ruben 2025-03-04 18:27:44 +01:00
parent 49a4d16951
commit 94942a1ad8
2 changed files with 156 additions and 71 deletions

View file

@ -1,19 +1,15 @@
# DNSDrone
DNSDrone is a Bash script that automatically updates DNS records (A, AAAA, and PTR) in FreeIPA or standard DNS servers using nsupdate. It detects your current public IPv4 and IPv6 addresses and updates the configured DNS records when changes are detected.
DNSDrone is a Bash script that automatically updates DNS records (A, AAAA, PTR, and TXT) in FreeIPA or standard DNS servers using nsupdate. It detects your current public IPv4 and IPv6 addresses and updates the configured DNS records when changes are detected.
## Features
- Supports both IPv4 (A) and IPv6 (AAAA) records
- Optional PTR record management for both IPv4 and IPv6
- Optional TXT record management for IPv6 prefix tracking
- Supports authentication via FreeIPA user/password or TSIG keys
- Configurable through environment variables
- Debug mode for troubleshooting
- Dry run mode for testing
- Flexible .env file location detection
- Multiple IP address providers for redundancy
- Support for updating zone apex (root domain)
- Full DNS record management (A, AAAA, PTR, and TXT records with IPv6 prefix tracking)
- Dual authentication support (FreeIPA user/password or TSIG keys)
- Flexible configuration with environment variables and multiple .env file locations
- Advanced operational modes (debug, dry-run, auto-update)
- Multiple IP provider fallback for reliable address detection
- Support for zone apex (root domain) updates
## Prerequisites
@ -96,3 +92,4 @@ RECORD_PTR4=false
RECORD_PTR6=false
RECORD_TXT_PREFIX=true
ISP_PREFIX_LENGHT=56
```

View file

@ -1,40 +1,35 @@
#!/usr/bin/env bash
# TODO: Make hierarchy of where to check for .env.* files.
#
# Exit on any error
# set -e
set -u # Exit on undefined variable
set -o pipefail # Exit on pipe failures
# Debug flag
DEBUG=0
DRY_RUN=0
CURRENT_VERSION="v1.1"
FORGEJO_API_URL=${FORGEJO_API_URL:-"https://forge.dmz.skyfritt.net"}
FORGEJO_ORG=${FORGEJO_ORG:-"Skyfritt"}
FORGEJO_REPO=${FORGEJO_REPO:-"dnsdrone"}
AUTO_UPDATE=${AUTO_UPDATE:-0}
CHECK_UPDATE=${CHECK_UPDATE:-0}
# Default values
RECORD_A=${RECORD_A:-false}
RECORD_AAAA=${RECORD_AAAA:-false}
RECORD_PTR4=${RECORD_PTR4:-false}
RECORD_PTR6=${RECORD_PTR6:-false}
ISP_PREFIX_LENGHT="${ISP_PREFIX_LENGHT:-false}"
# TXT_PREFIX_NAME="${TXT_PREFIX_NAME:-false}"
API_VERSION=${API_VERSION:-2.253}
IPCALC_CMD=${IPCALC_CMD:-false}
RECORD_NAME=${RECORD_NAME:-false}
# Error handling function
error() {
echo "[ERROR] $1" >&2
exit 1
}
# Function to handle debug output
debug() {
[[ $DEBUG -eq 1 ]] && echo "[DEBUG] $1"
}
# Function to check dependencies
check_dependencies() {
local missing=0
for cmd in curl jq dig nsupdate; do
@ -44,7 +39,6 @@ check_dependencies() {
fi
done
# Check for ipcalc-ng first, fall back to ipcalc
if command -v ipcalc-ng >/dev/null 2>&1; then
debug "Using ipcalc-ng"
IPCALC_CMD="ipcalc-ng"
@ -60,40 +54,69 @@ check_dependencies() {
}
load_env() {
local linkName realName envFile=""
linkName="$(basename "$0")"
realName="$(basename "$(realpath "$0")")"
local -r skyfrittEnvPath="/opt/skyfritt-tools-env"
local -a searchPaths=(
"$(pwd)/.env"
"$(dirname "$(realpath "$0")")/.env"
"$(dirname "$(realpath "$0")")/.env.${realName}"
"$(dirname "$(readlink "$0")")/.env.${linkName}"
"${skyfrittEnvPath}/.env.${linkName}"
)
local linkName realName envFile=""
linkName="$(basename "$0")"
realName="$(basename "$(realpath "$0")")"
local -r skyfrittEnvPath="/opt/skyfritt-tools-env"
local -a searchPaths=(
"$(pwd)/.env"
"$(dirname "$(realpath "$0")")/.env"
"$(dirname "$(realpath "$0")")/.env.${realName}"
"$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")/.env.${linkName}"
"${skyfrittEnvPath}/.env.${linkName}"
)
debug "Checking for .env file in multiple locations"
debug "Checking for .env file in multiple locations"
for path in "${searchPaths[@]}"; do
if [[ -f "$path" ]]; then
envFile="$path"
debug "Found .env file: ${envFile}"
break
for path in "${searchPaths[@]}"; do
if [[ -f "$path" ]]; then
envFile="$path"
debug "Found .env file: ${envFile}"
break
fi
done
[[ -z "$envFile" ]] && error "No .env file found in search paths"
[[ ! -r "$envFile" ]] && error "Found .env file at ${envFile} but it's not readable"
# shellcheck source=/dev/null
if ! source "$envFile"; then
error "Failed to source .env file: ${envFile}"
fi
done
# shellcheck source=/dev/null
source "$envFile" || error "Failed to source .env file"
# Validate required environment variables
[[ -z "${SERVER:-}" ]] && error "SERVER not set in .env"
[[ -z "${ZONE_NAME:-}" ]] && error "ZONE_NAME not set in .env"
# [[ -z "${RECORD_NAME:-}" ]] && error "RECORD_NAME not set in .env"
local -a required_vars=("SERVER" "ZONE_NAME")
local missing_vars=()
for var in "${required_vars[@]}"; do
if [[ -z "${!var:-}" ]]; then
missing_vars+=("$var")
fi
done
# Check for missing required variables
if [[ ${#missing_vars[@]} -gt 0 ]]; then
error "Missing required variables in .env file: ${missing_vars[*]}"
fi
# Validate authentication method
if [[ -z "${USER:-}" || -z "${PASSWORD:-}" ]] && [[ -z "${TSIG_KEYNAME:-}" || -z "${TSIG_KEY:-}" ]]; then
error "Either USER/PASSWORD or TSIG_KEYNAME/TSIG_KEY must be set"
error "Either USER/PASSWORD or TSIG_KEYNAME/TSIG_KEY must be set in .env file"
fi
# Validate values if present
[[ -n "${RECORD_TTL:-}" ]] && [[ ! "${RECORD_TTL}" =~ ^[0-9]+$ ]] && \
error "RECORD_TTL must be a positive integer"
# Additional validation for specific record types
if [[ "${RECORD_TXT_PREFIX:-false}" == "true" ]]; then
[[ -z "${ISP_PREFIX_LENGHT:-}" ]] && \
error "ISP_PREFIX_LENGHT must be set when RECORD_TXT_PREFIX is true"
[[ ! "${ISP_PREFIX_LENGHT}" =~ ^[0-9]+$ ]] && \
error "ISP_PREFIX_LENGHT must be a positive integer"
fi
debug "Environment loaded successfully"
}
# Function to get current IPs
@ -113,7 +136,6 @@ get_current_ips() {
"v6.ident.me"
)
# Try IPv4 providers
for provider in "${ipv4_providers[@]}"; do
local temp_ipv4
temp_ipv4=$(curl -s4 --connect-timeout 5 "$provider" 2>/dev/null)
@ -123,7 +145,6 @@ get_current_ips() {
fi
done
# Try IPv6 providers
for provider in "${ipv6_providers[@]}"; do
local temp_ipv6
temp_ipv6=$(curl -s6 --connect-timeout 5 "$provider" 2>/dev/null)
@ -133,7 +154,6 @@ get_current_ips() {
fi
done
# Only error if both stacks are unavailable
if [[ -z "$ipv4" && -z "$ipv6" ]]; then
error "Failed to get any IP addresses from providers"
fi
@ -141,7 +161,6 @@ get_current_ips() {
echo "$ipv4:$ipv6"
}
# Function to get DNS records
get_dns_records() {
local record_type=$1
local result
@ -149,7 +168,6 @@ get_dns_records() {
echo "$result"
}
# Function to get PTR record
get_ptr_record() {
local ip=$1
local result
@ -157,8 +175,6 @@ get_ptr_record() {
echo "$result"
}
# Function to update FreeIPA DNS record
# IMPORTANT! The records have to exists, since we are using the api call dnsrecord_mod.
update_freeipa_record() {
local record_type=$1
local ip=$2
@ -174,7 +190,6 @@ update_freeipa_record() {
return
fi
# Validate inputs
[[ -z "$record_type" ]] && error "Record type cannot be empty"
[[ -z "$ip" ]] && error "IP address cannot be empty"
@ -219,7 +234,7 @@ update_freeipa_record() {
ip="\\\"${ip}\\\""
fi
# Now make the API call with the cookie
# Make the API call with the cookie
curl_output=$(curl -s -k \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
@ -231,7 +246,6 @@ update_freeipa_record() {
"https://${SERVER}/ipa/session/json" 2>&1)
curl_exit_code=$?
# Cleanup cookie file
rm -f "${cookie_file}"
# Parse HTTP status code from last line
@ -242,12 +256,10 @@ update_freeipa_record() {
debug "Curl exit code: $curl_exit_code"
debug "HTTP status code: $http_code"
# Check curl execution
if [[ $curl_exit_code -ne 0 ]]; then
error "Curl command failed with exit code $curl_exit_code: $data"
fi
# Check HTTP status
if [[ $http_code -ne 200 ]]; then
error "HTTP request failed with status $http_code: $data"
fi
@ -263,7 +275,6 @@ update_freeipa_record() {
debug "DNS record update completed successfully"
}
# Function to update nsupdate record
update_nsupdate_record() {
local record_type=$1
local ip=$2
@ -296,7 +307,6 @@ send
EOF
}
# Main update function
update_record() {
local record_type=$1
local current_ip=$2
@ -319,47 +329,126 @@ update_record() {
fi
}
# Main execution
version_gt() {
test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"
}
check_for_updates() {
local latest_version
local api_endpoint="${FORGEJO_API_URL}/api/v1/repos/${FORGEJO_ORG}/${FORGEJO_REPO}/releases/latest"
debug "Checking for updates from: $api_endpoint"
latest_version=$(curl -s "$api_endpoint" | jq -r '.tag_name' 2>/dev/null)
if [[ -z "$latest_version" ]]; then
error "Failed to fetch latest version information"
fi
debug "Current version: $CURRENT_VERSION"
debug "Latest version: $latest_version"
if version_gt "$latest_version" "$CURRENT_VERSION"; then
echo "Update available: $CURRENT_VERSION$latest_version"
return 0
else
echo "You are running the latest version ($CURRENT_VERSION)"
return 1
fi
}
perform_update() {
local latest_version
local api_endpoint="${FORGEJO_API_URL}/api/v1/repos/${FORGEJO_ORG}/${FORGEJO_REPO}/releases/latest"
local raw_download_url
local current_script_path
local temp_file
latest_version=$(curl -s "$api_endpoint" | jq -r '.tag_name' 2>/dev/null)
if [[ -z "$latest_version" ]]; then
error "Failed to get latest version information. Is the repo public?"
fi
raw_download_url="${FORGEJO_API_URL}/${FORGEJO_ORG}/${FORGEJO_REPO}/raw/tag/${latest_version}/dnsdrone.sh"
debug "Downloading from: $raw_download_url"
temp_file=$(mktemp)
current_script_path=$(realpath "$0")
echo "Downloading update..."
if ! curl -s -L "$raw_download_url" -o "$temp_file"; then
rm -f "$temp_file"
error "Failed to download update"
fi
if ! bash -n "$temp_file"; then
rm -f "$temp_file"
error "Downloaded file appears to be invalid"
fi
cp "$current_script_path" "${current_script_path}.backup"
if ! mv "$temp_file" "$current_script_path"; then
cp "${current_script_path}.backup" "$current_script_path"
rm -f "$temp_file"
error "Failed to install update"
fi
chmod +x "$current_script_path"
echo "Successfully updated to version $latest_version"
echo "Previous version backed up to ${current_script_path}.backup"
exit 0
}
main() {
# Process flags
while getopts "dn" opt; do
while getopts "dnac" opt; do
case $opt in
d) DEBUG=1 ;;
n) DRY_RUN=1 ;;
a) AUTO_UPDATE=1 ;;
c) CHECK_UPDATE=1 ;;
*) error "Invalid option: -$OPTARG" ;;
esac
done
if [[ $CHECK_UPDATE -eq 1 ]]; then
check_for_updates
exit $?
fi
if [[ $AUTO_UPDATE -eq 1 ]]; then
if check_for_updates; then
perform_update
fi
fi
trap 'error "Script interrupted"' INT TERM
check_dependencies
load_env
# Construct the full domain name
if [[ $RECORD_NAME = false ]]; then
local FQDN="${ZONE_NAME}."
else
local FQDN="${RECORD_NAME}.${ZONE_NAME}."
fi
# Get current IPs
IFS=':' read -r current_ipv4 current_ipv6 <<< "$(get_current_ips)" || true
# Update A record if IPv4 is available
if [[ -n "$current_ipv4" && "${RECORD_A:-false}" == "true" ]]; then
update_record "A" "$current_ipv4"
elif [[ "${RECORD_A:-false}" == "true" ]]; then
debug "Skipping A record update - no IPv4 address available"
fi
# Update AAAA record if IPv6 is available
if [[ -n "$current_ipv6" && "${RECORD_AAAA:-false}" == "true" ]]; then
update_record "AAAA" "$current_ipv6"
elif [[ "${RECORD_AAAA:-false}" == "true" ]]; then
debug "Skipping AAAA record update - no IPv6 address available"
fi
# Check and update PTR records if configured and IP is available
if [[ "${RECORD_PTR4:-false}" == "true" ]]; then
if [[ -n "$current_ipv4" ]]; then
debug "Processing PTR record for IPv4"
@ -390,7 +479,6 @@ main() {
fi
fi
# Check for, and update TXT record with IPv6 prefix if IPv6 is available
if [[ "${RECORD_TXT_PREFIX:-false}" == true ]]; then
if [[ -n "$current_ipv6" ]]; then
local expected_network_address expected_network_address_without_prefix current_network_address current_network_address_quotes