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
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 ## Features
- Supports both IPv4 (A) and IPv6 (AAAA) records - Full DNS record management (A, AAAA, PTR, and TXT records with IPv6 prefix tracking)
- Optional PTR record management for both IPv4 and IPv6 - Dual authentication support (FreeIPA user/password or TSIG keys)
- Optional TXT record management for IPv6 prefix tracking - Flexible configuration with environment variables and multiple .env file locations
- Supports authentication via FreeIPA user/password or TSIG keys - Advanced operational modes (debug, dry-run, auto-update)
- Configurable through environment variables - Multiple IP provider fallback for reliable address detection
- Debug mode for troubleshooting - Support for zone apex (root domain) updates
- Dry run mode for testing
- Flexible .env file location detection
- Multiple IP address providers for redundancy
- Support for updating zone apex (root domain)
## Prerequisites ## Prerequisites
@ -96,3 +92,4 @@ RECORD_PTR4=false
RECORD_PTR6=false RECORD_PTR6=false
RECORD_TXT_PREFIX=true RECORD_TXT_PREFIX=true
ISP_PREFIX_LENGHT=56 ISP_PREFIX_LENGHT=56
```

View file

@ -1,40 +1,35 @@
#!/usr/bin/env bash #!/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 -u # Exit on undefined variable
set -o pipefail # Exit on pipe failures set -o pipefail # Exit on pipe failures
# Debug flag
DEBUG=0 DEBUG=0
DRY_RUN=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_A=${RECORD_A:-false}
RECORD_AAAA=${RECORD_AAAA:-false} RECORD_AAAA=${RECORD_AAAA:-false}
RECORD_PTR4=${RECORD_PTR4:-false} RECORD_PTR4=${RECORD_PTR4:-false}
RECORD_PTR6=${RECORD_PTR6:-false} RECORD_PTR6=${RECORD_PTR6:-false}
ISP_PREFIX_LENGHT="${ISP_PREFIX_LENGHT:-false}" ISP_PREFIX_LENGHT="${ISP_PREFIX_LENGHT:-false}"
# TXT_PREFIX_NAME="${TXT_PREFIX_NAME:-false}"
API_VERSION=${API_VERSION:-2.253} API_VERSION=${API_VERSION:-2.253}
IPCALC_CMD=${IPCALC_CMD:-false} IPCALC_CMD=${IPCALC_CMD:-false}
RECORD_NAME=${RECORD_NAME:-false} RECORD_NAME=${RECORD_NAME:-false}
# Error handling function
error() { error() {
echo "[ERROR] $1" >&2 echo "[ERROR] $1" >&2
exit 1 exit 1
} }
# Function to handle debug output
debug() { debug() {
[[ $DEBUG -eq 1 ]] && echo "[DEBUG] $1" [[ $DEBUG -eq 1 ]] && echo "[DEBUG] $1"
} }
# Function to check dependencies
check_dependencies() { check_dependencies() {
local missing=0 local missing=0
for cmd in curl jq dig nsupdate; do for cmd in curl jq dig nsupdate; do
@ -44,7 +39,6 @@ check_dependencies() {
fi fi
done done
# Check for ipcalc-ng first, fall back to ipcalc
if command -v ipcalc-ng >/dev/null 2>&1; then if command -v ipcalc-ng >/dev/null 2>&1; then
debug "Using ipcalc-ng" debug "Using ipcalc-ng"
IPCALC_CMD="ipcalc-ng" IPCALC_CMD="ipcalc-ng"
@ -68,7 +62,7 @@ local -a searchPaths=(
"$(pwd)/.env" "$(pwd)/.env"
"$(dirname "$(realpath "$0")")/.env" "$(dirname "$(realpath "$0")")/.env"
"$(dirname "$(realpath "$0")")/.env.${realName}" "$(dirname "$(realpath "$0")")/.env.${realName}"
"$(dirname "$(readlink "$0")")/.env.${linkName}" "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")/.env.${linkName}"
"${skyfrittEnvPath}/.env.${linkName}" "${skyfrittEnvPath}/.env.${linkName}"
) )
@ -82,18 +76,47 @@ for path in "${searchPaths[@]}"; do
fi fi
done 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 # shellcheck source=/dev/null
source "$envFile" || error "Failed to source .env file" if ! source "$envFile"; then
error "Failed to source .env file: ${envFile}"
fi
# Validate required environment variables # Validate required environment variables
[[ -z "${SERVER:-}" ]] && error "SERVER not set in .env" local -a required_vars=("SERVER" "ZONE_NAME")
[[ -z "${ZONE_NAME:-}" ]] && error "ZONE_NAME not set in .env" local missing_vars=()
# [[ -z "${RECORD_NAME:-}" ]] && error "RECORD_NAME not set in .env"
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 # Validate authentication method
if [[ -z "${USER:-}" || -z "${PASSWORD:-}" ]] && [[ -z "${TSIG_KEYNAME:-}" || -z "${TSIG_KEY:-}" ]]; then 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 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 # Function to get current IPs
@ -113,7 +136,6 @@ get_current_ips() {
"v6.ident.me" "v6.ident.me"
) )
# Try IPv4 providers
for provider in "${ipv4_providers[@]}"; do for provider in "${ipv4_providers[@]}"; do
local temp_ipv4 local temp_ipv4
temp_ipv4=$(curl -s4 --connect-timeout 5 "$provider" 2>/dev/null) temp_ipv4=$(curl -s4 --connect-timeout 5 "$provider" 2>/dev/null)
@ -123,7 +145,6 @@ get_current_ips() {
fi fi
done done
# Try IPv6 providers
for provider in "${ipv6_providers[@]}"; do for provider in "${ipv6_providers[@]}"; do
local temp_ipv6 local temp_ipv6
temp_ipv6=$(curl -s6 --connect-timeout 5 "$provider" 2>/dev/null) temp_ipv6=$(curl -s6 --connect-timeout 5 "$provider" 2>/dev/null)
@ -133,7 +154,6 @@ get_current_ips() {
fi fi
done done
# Only error if both stacks are unavailable
if [[ -z "$ipv4" && -z "$ipv6" ]]; then if [[ -z "$ipv4" && -z "$ipv6" ]]; then
error "Failed to get any IP addresses from providers" error "Failed to get any IP addresses from providers"
fi fi
@ -141,7 +161,6 @@ get_current_ips() {
echo "$ipv4:$ipv6" echo "$ipv4:$ipv6"
} }
# Function to get DNS records
get_dns_records() { get_dns_records() {
local record_type=$1 local record_type=$1
local result local result
@ -149,7 +168,6 @@ get_dns_records() {
echo "$result" echo "$result"
} }
# Function to get PTR record
get_ptr_record() { get_ptr_record() {
local ip=$1 local ip=$1
local result local result
@ -157,8 +175,6 @@ get_ptr_record() {
echo "$result" 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() { update_freeipa_record() {
local record_type=$1 local record_type=$1
local ip=$2 local ip=$2
@ -174,7 +190,6 @@ update_freeipa_record() {
return return
fi fi
# Validate inputs
[[ -z "$record_type" ]] && error "Record type cannot be empty" [[ -z "$record_type" ]] && error "Record type cannot be empty"
[[ -z "$ip" ]] && error "IP address cannot be empty" [[ -z "$ip" ]] && error "IP address cannot be empty"
@ -219,7 +234,7 @@ update_freeipa_record() {
ip="\\\"${ip}\\\"" ip="\\\"${ip}\\\""
fi fi
# Now make the API call with the cookie # Make the API call with the cookie
curl_output=$(curl -s -k \ curl_output=$(curl -s -k \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@ -231,7 +246,6 @@ update_freeipa_record() {
"https://${SERVER}/ipa/session/json" 2>&1) "https://${SERVER}/ipa/session/json" 2>&1)
curl_exit_code=$? curl_exit_code=$?
# Cleanup cookie file
rm -f "${cookie_file}" rm -f "${cookie_file}"
# Parse HTTP status code from last line # Parse HTTP status code from last line
@ -242,12 +256,10 @@ update_freeipa_record() {
debug "Curl exit code: $curl_exit_code" debug "Curl exit code: $curl_exit_code"
debug "HTTP status code: $http_code" debug "HTTP status code: $http_code"
# Check curl execution
if [[ $curl_exit_code -ne 0 ]]; then if [[ $curl_exit_code -ne 0 ]]; then
error "Curl command failed with exit code $curl_exit_code: $data" error "Curl command failed with exit code $curl_exit_code: $data"
fi fi
# Check HTTP status
if [[ $http_code -ne 200 ]]; then if [[ $http_code -ne 200 ]]; then
error "HTTP request failed with status $http_code: $data" error "HTTP request failed with status $http_code: $data"
fi fi
@ -263,7 +275,6 @@ update_freeipa_record() {
debug "DNS record update completed successfully" debug "DNS record update completed successfully"
} }
# Function to update nsupdate record
update_nsupdate_record() { update_nsupdate_record() {
local record_type=$1 local record_type=$1
local ip=$2 local ip=$2
@ -296,7 +307,6 @@ send
EOF EOF
} }
# Main update function
update_record() { update_record() {
local record_type=$1 local record_type=$1
local current_ip=$2 local current_ip=$2
@ -319,47 +329,126 @@ update_record() {
fi 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() { main() {
# Process flags while getopts "dnac" opt; do
while getopts "dn" opt; do
case $opt in case $opt in
d) DEBUG=1 ;; d) DEBUG=1 ;;
n) DRY_RUN=1 ;; n) DRY_RUN=1 ;;
a) AUTO_UPDATE=1 ;;
c) CHECK_UPDATE=1 ;;
*) error "Invalid option: -$OPTARG" ;; *) error "Invalid option: -$OPTARG" ;;
esac esac
done 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 trap 'error "Script interrupted"' INT TERM
check_dependencies check_dependencies
load_env load_env
# Construct the full domain name
if [[ $RECORD_NAME = false ]]; then if [[ $RECORD_NAME = false ]]; then
local FQDN="${ZONE_NAME}." local FQDN="${ZONE_NAME}."
else else
local FQDN="${RECORD_NAME}.${ZONE_NAME}." local FQDN="${RECORD_NAME}.${ZONE_NAME}."
fi fi
# Get current IPs
IFS=':' read -r current_ipv4 current_ipv6 <<< "$(get_current_ips)" || true 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 if [[ -n "$current_ipv4" && "${RECORD_A:-false}" == "true" ]]; then
update_record "A" "$current_ipv4" update_record "A" "$current_ipv4"
elif [[ "${RECORD_A:-false}" == "true" ]]; then elif [[ "${RECORD_A:-false}" == "true" ]]; then
debug "Skipping A record update - no IPv4 address available" debug "Skipping A record update - no IPv4 address available"
fi fi
# Update AAAA record if IPv6 is available
if [[ -n "$current_ipv6" && "${RECORD_AAAA:-false}" == "true" ]]; then if [[ -n "$current_ipv6" && "${RECORD_AAAA:-false}" == "true" ]]; then
update_record "AAAA" "$current_ipv6" update_record "AAAA" "$current_ipv6"
elif [[ "${RECORD_AAAA:-false}" == "true" ]]; then elif [[ "${RECORD_AAAA:-false}" == "true" ]]; then
debug "Skipping AAAA record update - no IPv6 address available" debug "Skipping AAAA record update - no IPv6 address available"
fi fi
# Check and update PTR records if configured and IP is available
if [[ "${RECORD_PTR4:-false}" == "true" ]]; then if [[ "${RECORD_PTR4:-false}" == "true" ]]; then
if [[ -n "$current_ipv4" ]]; then if [[ -n "$current_ipv4" ]]; then
debug "Processing PTR record for IPv4" debug "Processing PTR record for IPv4"
@ -390,7 +479,6 @@ main() {
fi fi
fi fi
# Check for, and update TXT record with IPv6 prefix if IPv6 is available
if [[ "${RECORD_TXT_PREFIX:-false}" == true ]]; then if [[ "${RECORD_TXT_PREFIX:-false}" == true ]]; then
if [[ -n "$current_ipv6" ]]; then if [[ -n "$current_ipv6" ]]; then
local expected_network_address expected_network_address_without_prefix current_network_address current_network_address_quotes local expected_network_address expected_network_address_without_prefix current_network_address current_network_address_quotes