Add version check and auto-update
This commit is contained in:
parent
49a4d16951
commit
94942a1ad8
2 changed files with 156 additions and 71 deletions
19
README.md
19
README.md
|
@ -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
|
||||
```
|
||||
|
|
208
dnsdrone.sh
208
dnsdrone.sh
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue