#!/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 # 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 if ! command -v "$cmd" >/dev/null 2>&1; then echo "Error: $cmd is required but not installed." >&2 missing=1 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" elif command -v ipcalc >/dev/null 2>&1; then debug "Using ipcalc" IPCALC_CMD="ipcalc" else echo "Error: either ipcalc or ipcalc-ng is required but not installed." >&2 missing=1 fi [[ $missing -eq 1 ]] && error "Missing required dependencies" } load_env() { local fullRealPath fullLinkPath realPath linkPath realName linkName callDir linkName="$(basename $0)" fullLinkPath="$(readlink "$0")" fullRealPath="$(realpath "$0")" realName="$(basename "$fullRealPath")" realPath="$(dirname "$fullRealPath")" linkPath="$(dirname "$fullLinkPath")" callDir="$(pwd)" local skyfrittEnvPath="/opt/skyfritt-tools-env" local envFile="" debug "Checking for .env file in multiple locations" if [[ -f "${callDir}/.env" ]]; then # Check in call directory envFile="${callDir}/.env" debug "Found .env in call directory: ${envFile}" elif [[ -f "${realPath}/.env" ]]; then # Check in real path directory envFile="${realPath}/.env" debug "Found .env in real path directory: ${envFile}" elif [[ -f "${realPath}/.env.${realName}" ]]; then # Check for .env.realName in real path envFile="${realPath}/.env.${realName}" debug "Found .env.${realName} in real path: ${envFile}" elif [[ -f "${linkPath}/.env.${linkName}" ]]; then # Check for .env.linkName in link path envFile="${linkPath}/.env.${linkName}" debug "Found .env.${linkName} in link path: ${envFile}" elif [[ -f "${skyfrittEnvPath}/.env.${linkName}" ]]; then # Check in skyfritt env path envFile="${skyfrittEnvPath}/.env.${linkName}" debug "Found .env.${linkName} in skyfritt path: ${envFile}" fi [[ -z "$envFile" ]] && error ".env file not found in any location" # 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" # 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" fi } # Function to get current IPs get_current_ips() { local ipv4="" ipv6="" local ipv4_providers=( "ifconfig.me" "api.ipify.org" "icanhazip.com" "ipecho.net/plain" "checkip.amazonaws.com" ) local ipv6_providers=( "ifconfig.me" "api6.ipify.org" "icanhazip.com" "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) if [[ -n "$temp_ipv4" ]]; then ipv4=$temp_ipv4 break 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) if [[ -n "$temp_ipv6" ]]; then ipv6=$temp_ipv6 break 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 echo "$ipv4:$ipv6" } # Function to get DNS records get_dns_records() { local record_type=$1 local result result=$(dig +short "$record_type" "$FQDN" "@${SERVER}" 2>/dev/null || true) echo "$result" } # Function to get PTR record get_ptr_record() { local ip=$1 local result result=$(dig +short -x "$ip" "@${SERVER}" 2>/dev/null || true) 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 debug "Updating FreeIPA $record_type record to $ip" debug "Using server: ${SERVER}" debug "Zone: ${ZONE_NAME}" debug "Record: ${RECORD_NAME}" debug "API Version: ${API_VERSION}" if [[ $DRY_RUN -eq 1 ]]; then echo "[DRY RUN] Would update FreeIPA $record_type record for ${FQDN} to $ip" return fi # Validate inputs [[ -z "$record_type" ]] && error "Record type cannot be empty" [[ -z "$ip" ]] && error "IP address cannot be empty" local data local curl_exit_code local curl_output local cookie_file local record_name cookie_file=$(mktemp) if [[ $RECORD_NAME == false ]]; then record_name="@" # Empty string for root record else record_name="$RECORD_NAME" fi # First authenticate and get cookie curl_output=$(curl -s -k \ -H "Accept: text/plain" \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Referer: https://${SERVER}/ipa" \ -H "X-IPA-API-Version: ${API_VERSION}" \ --data-urlencode "user=${USER}" \ --data-urlencode "password=${PASSWORD}" \ -c "${cookie_file}" \ --write-out "\n%{http_code}" \ "https://${SERVER}/ipa/session/login_password" 2>&1) curl_exit_code=$? # Parse HTTP status code from login local login_code login_code=$(echo "$curl_output" | tail -n1) # Check login success if [[ $curl_exit_code -ne 0 || $login_code -ne 200 ]]; then rm -f "${cookie_file}" error "Login failed with status $login_code: $(echo "$curl_output" | sed '$d')" fi # If record type is TXT, wrap the IP in escaped quotes if [[ "${record_type}" == "TXT" ]]; then ip="\\\"${ip}\\\"" fi # Now make the API call with the cookie curl_output=$(curl -s -k \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "Referer: https://${SERVER}/ipa" \ -H "X-IPA-API-Version: ${API_VERSION}" \ -b "${cookie_file}" \ --write-out "\n%{http_code}" \ --data "{\"method\":\"dnsrecord_mod\",\"params\":[[\"${ZONE_NAME}\",\"${record_name}\"],{\"${record_type,,}record\":\"${ip}\", \"dnsttl\":${RECORD_TTL:-60}}]}" \ "https://${SERVER}/ipa/session/json" 2>&1) curl_exit_code=$? # Cleanup cookie file rm -f "${cookie_file}" # Parse HTTP status code from last line local http_code http_code=$(echo "$curl_output" | tail -n1) data=$(echo "$curl_output" | sed '$d') 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 # Check for error in JSON response if echo "$data" | jq -e '.error' >/dev/null 2>&1; then local error_msg error_msg=$(echo "$data" | jq -r '.error.message // "Unknown error"') error "FreeIPA API returned error: $error_msg" fi debug "FreeIPA response: $data" debug "DNS record update completed successfully" } # Function to update nsupdate record update_nsupdate_record() { local record_type=$1 local ip=$2 local ttl=${RECORD_TTL:-60} # Default TTL of 60 seconds if not set in .env debug "Updating nsupdate $record_type record to $ip with TTL $ttl" if [[ $DRY_RUN -eq 1 ]]; then echo "[DRY RUN] Would update nsupdate $record_type record for ${FQDN} to $ip with TTL $ttl" return fi # Create nsupdate commands based on record type nsupdate -y "${TSIG_KEY_TYPE}:${TSIG_KEYNAME}:${TSIG_KEY}" <