From ababb6048b155e85c6e323595dd3d5d840716530 Mon Sep 17 00:00:00 2001 From: Ruben Date: Tue, 4 Mar 2025 14:53:24 +0100 Subject: [PATCH] Initial commit --- .gitignore | 1 + README.md | 98 ++++++++++++ dnsdrone.rsc | 345 +++++++++++++++++++++++++++++++++++++++++ dnsdrone.sh | 426 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 870 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 dnsdrone.rsc create mode 100755 dnsdrone.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..55768a1 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# 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. + +## 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) + +## Prerequisites + +- curl +- jq +- dig +- nsupdate +- [ipcalc or ipcalc-ng](https://gitlab.com/ipcalc/ipcalc) ("ng" for Debian-based distros) + +## Configuration + +The script looks for a `.env` file in the following locations (in order): +1. Current working directory (`.env`) +2. Script's real path directory (`.env`) +3. Script's real path directory (`.env.`) +4. Script's symlink path directory (`.env.`) +5. `/opt/skyfritt-tools-env/.env.` + +### Required Variables + +| Variable | Description | +|----------|-------------| +| `SERVER` | DNS server hostname or IP address | +| `ZONE_NAME` | DNS zone name (e.g., "example.com") | +| `RECORD_NAME` | Hostname to update (without domain). Skip to update zone apex | + +### Authentication Variables +Either USER/PASSWORD or TSIG authentication must be configured: + +#### FreeIPA Authentication +| Variable | Description | +|----------|-------------| +| `USER` | FreeIPA username | +| `PASSWORD` | FreeIPA password | + +**IMPORTANT!** When using FreeIPA's API, the records has to exist *before* the script can be used. It can only modify, not add, records. + +#### TSIG Authentication +| Variable | Description | +|----------|-------------| +| `TSIG_KEYNAME` | Name of the TSIG key | +| `TSIG_KEY_TYPE` | Type of TSIG key (e.g., "hmac-sha256") | +| `TSIG_KEY` | The TSIG key value | + +### Optional Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `RECORD_TTL` | Time-to-live value for DNS records in seconds | 60 | +| `RECORD_A` | Enable IPv4 (A) record updates | false | +| `RECORD_AAAA` | Enable IPv6 (AAAA) record updates | false | +| `RECORD_PTR4` | Enable IPv4 PTR record updates | false | +| `RECORD_PTR6` | Enable IPv6 PTR record updates | false | +| `RECORD_TXT_PREFIX` | Enable TXT record updates for IPv6 prefix | false | +| `ISP_PREFIX_LENGHT` | IPv6 prefix length for TXT record updates | Required if RECORD_TXT_PREFIX is true | +| `API_VERSION` | FreeIPA API version | 2.253 | + +### Example Configuration + +```env +# Server Configuration +SERVER=dns.example.com +ZONE_NAME=example.com +RECORD_NAME=myhost +RECORD_TTL=300 + +# FreeIPA Authentication +USER=dnsuser +PASSWORD=secretpassword + +# Alternative: TSIG Authentication +# TSIG_KEYNAME=mykey +# TSIG_KEY_TYPE=hmac-sha256 +# TSIG_KEY="base64encodedkey==" + +# Record Configuration +RECORD_A=true +RECORD_AAAA=true +RECORD_PTR4=false +RECORD_PTR6=false +RECORD_TXT_PREFIX=true +ISP_PREFIX_LENGHT=56 diff --git a/dnsdrone.rsc b/dnsdrone.rsc new file mode 100755 index 0000000..fed9bff --- /dev/null +++ b/dnsdrone.rsc @@ -0,0 +1,345 @@ +#!rsc by RouterOS +# DNSDrone for Mikrotik - DNS Record Updater for FreeIPA + +# This script: +# 1. Consolidates all configuration into a single CONFIG dictionary +# 2. Includes a function for making FreeIPA API calls +# 3. Gets current IP addresses from DHCP clients +# 4. Compares with existing DNS records +# 5. Updates records only when they differ +# 6. Handles both IPv4 and IPv6 records +# 7. Updates the prefix TXT record +# +# To use this: +# 1. Replace the configuration values in the CONFIG dictionary +# 2. Save it as a script in your Mikrotik router +# 3. Schedule it to run periodically or trigger it manually +# +# Note: You might need to adjust the HTTP headers and API calls based on your specific FreeIPA version and requirements. Also, ensure your Mikrotik router has proper SSL/TLS # certificates installed for HTTPS communication with the FreeIPA server. + +# Configuration Variables +:local CONFIG { + "wanInterface"="VLAN666_Altibox"; + "IpaServer"="ipa.demo1.freeipa.org"; + "dnsZone"="demo1.freeipa.org"; + "IpaUser"="admin"; + "IpaPassword"="Secret123"; + "apiVersion"="2.253"; + "recordTTL"="300"; + +# Curlproxy + "curlproxyserver"="pubdns.dmz.skyfritt.net"; + "curlproxyuser"="curluser"; + "cookiefile"="cookie_output.txt"; + +# Record Names (without zone) + "dualStackName"="tik-test"; + "ipv4OnlyName"="tik-test-v4"; + "ipv6OnlyName"="tik-test-v6"; + "prefixTxtName"="tik-test-prefix"; + +# Resolver Config + "DNSserver"="ipa.demo1.freeipa.org"; + "DoHserver"="one.one.one.one"; + +# Script options (true/false) + "debug"="true" + +} + +# Functions: + +:local GetIPACookie do={ + +# Import config + :local CONFIG $1; + +# Save Cookie-information in tempoary local file via ugly ssh hack + + /system ssh "$($CONFIG->"curlproxyserver")" user="$($CONFIG->"curlproxyuser")" \ + command="curl -s -k -i -H 'Accept: text/plain' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -H 'Referer: https://$($CONFIG->"IpaServer")/ipa' \ + -H 'X-IPA-API-Version: $($CONFIG->"apiVersion")' \ + --data-urlencode 'user=$($CONFIG->"IpaUser")' \ + --data-urlencode 'password=$($CONFIG->"IpaPassword")' \ + 'https://$($CONFIG->"IpaServer")/ipa/session/login_password'" \ + output-to-file="$($CONFIG->"cookiefile")" + +# Needs time to save the cookiefile + + /delay 2s + +# Extract cookie-information from temporary filr + + + :local cookie; + :local fileContents [/file get "$($CONFIG->"cookiefile")" contents] + + :local startIndex [:find $fileContents "Set-Cookie: ipa_session="] + :if ($startIndex != -1) do={ + :set startIndex ($startIndex + 12) + + :local endIndex [:find $fileContents "\n" $startIndex] + :if ($endIndex = -1) do={ + :set endIndex [:len $fileContents] + } + + :set cookie [:pick $fileContents $startIndex $endIndex] + } + +# Cleaning up the cookiefile + /file remove "$($CONFIG->"cookiefile")" + + :put "DEBUG (function: GetIPACookie):; + :put Ipakjeks: $cookie"; + +# Output + :return "$cookie"; + +} + + +:local GetLocalDynamicConfig do={ + +# Import config + :local CONFIG $1; + +# Valid options: ipv4address, ipv6address, ipv6prefix + :local ConfigType $2; + +# result variable + :local resultRAW; + :local resultClean; + + :if ($ConfigType = "ipv4address" ) do={ + +# Retrive information + :set resultRAW [/ip dhcp-client get [/ip dhcp-client find interface="$($CONFIG->"wanInterface")"] address]; + + +# Clean information + :set resultClean [:pick "$resultRAW" 0 [:find "$resultRAW" "/"]]; + +# :put "DEBUG: resultClean: $resultClean"; + + + } + + :if ($ConfigType = "ipv6address") do={ + +# Retrive information + :set resultRAW [/ipv6 dhcp-client get [/ipv6 dhcp-client find interface="$($CONFIG->"wanInterface")"] address]; + +# Clean information + :set resultClean [:pick "$resultRAW" 0 [:find "$resultRAW" ","]]; + +# :put "DEBUG: resultClean: $resultClean"; + + } + + :if ($ConfigType = "ipv6prefix") do={ + +# Retrive information + :set resultRAW [/ipv6 dhcp-client get [/ipv6 dhcp-client find interface="$($CONFIG->"wanInterface")"] prefix]; + +# Clean information + :set resultClean [:pick "$resultRAW" 0 [:find "$resultRAW" ","]]; + +# :put "DEBUG: resultClean: $resultClean"; + + } + +# Output + :return "$resultClean"; + +} + + +:local GetDynamicDNSRecords do={ + +# Import config + :local CONFIG $1; + +# Valid options: ipv4address, ipv6address, ipv6prefix + :local ConfigType $2; + +# Record name + :local RecordName $3; + +# result variable + :local resultRAW; + :local resultClean; + + :if ($ConfigType = "ipv4address" ) do={ + +# Retrive information + :set resultRAW [/resolve "$RecordName" server="$($CONFIG->"DNSserver")" type=ipv4] + +# Clean information + :set resultClean "$resultRAW"; + +# :put "DEBUG: resultClean: $resultClean"; + + } + + :if ($ConfigType = "ipv6address" ) do={ + +# Retrive information + :set resultRAW [/resolve "$RecordName" server="$($CONFIG->"DNSserver")" type=ipv6] + +# Clean information + :set resultClean "$resultRAW"; + +# :put "DEBUG: resultClean: $resultClean"; + + } + + + :if ($ConfigType = "ipv6prefix") do={ + +# Fetch prefix TXT-record via DoH server: + :set resultRAW [/tool fetch url="https://$($CONFIG->"DoHserver")/dns-query?name=$RecordName&type=TXT" http-method=get output=user http-header-field="accept: application/dns-json" as-value]; + +# :put "DEBUG: resultRAW: $resultRAW"; + +# Get just the data content + :local resultJSON ($resultRAW -> "data"); + +# :put "DEBUG: resultJSON: $resultJSON"; + +# Extracting TXT-record-data + :local startIndex [:find $resultJSON "data\":\"" 0]; + :local startPos ($startIndex + 7); + :local endIndex [:find $resultJSON "\"}" $startPos]; + :local resultTXT [:pick $resultJSON $startPos $endIndex]; + +# Cleaning the prefix-variable (remove escaped double quotes) + :set resultClean [:pick $resultTXT 2 ([:len $resultTXT] - 2)]; + +# :put "DEBUG: resultClean: $resultClean"; + + } + +# Output + :return "$resultClean"; + +} + + + + + + +:local UpdateDynamicDNSRecord do={ + +# Import config + :local CONFIG $1; + +# Import IPACookie + :local IPACookie $2; + +# Valid options: ipv4address, ipv6address, ipv6prefix + :local ConfigType $3; + +# Record name + :local RecordName $4; + +# Record content + :local RecordContent $5; + +# result variable + :local resultRAW; + :local resultClean; + + put "DEBUG: IPACookie: $IPACookie"; + put "DEBUG: ConfigType: $ConfigType"; + put "DEBUG: RecordName: $RecordName"; + put "DEBUG: RecordContent: $RecordContent"; + + :if ($ConfigType = "ipv4address" ) do={ + +# Construct valid json-data: +# :local httpheader "\"Accept: application/json,Content-Type: application/json, Referer: https://$($CONFIG->"IpaServer")/ipa, X-IPA-API-Version: $($CONFIG->"apiVersion"), Cookie: $IPACookie \"\""; + :local httpdata "{\"method\":\"dnsrecord_mod\",\"params\":[[\"$($CONFIG->"dnsZone")\",\"$RecordName.\"],{\"arecord\":\"$RecordContent\",\"dnsttl\":$($CONFIG->"recordTTL")}]}"; + + :local IPACookieData "$cookie"; +# :local IPACookieData "ipa_session=MagBearerToken=4ZTumSEI1zR%2fcPkbvlzM2lkT7CR9ojGOsvOzji0MO8ee61xUjZqXX3Ecs9n1FCHl0Vyn1U0EEooAAQk2DZ9GWT1Wt43Zx06sqDjDd5Ku8OJMa0W5SzkQuIQs%2f2hqkFoREevjwefgXurIlSxvDyVofXzJM726ZUAZsGICL9UawnWmo%2f7IVIqPRnNkb5g2NeMJKgThT0xuJ1G4nRY8w0CuIJV%2fJnVk9%2fjK%2bg%2bEjxhE3Lo%3d;path=/ipa;httponly;secure;"; + + + put "DEBUG: (NEW)IPACookie = $IPACookie"; + put "DEBUG: IPACookieData = $IPACookieData"; + put "DEBUG: http-data=$httpdata"; + + /tool fetch url="https://$($CONFIG->"IpaServer")/ipa/session/json" \ + http-method=post \ + http-header-field="Accept: application/json,Content-Type: application/json,\ + Referer: https://$($CONFIG->"IpaServer")/ipa,\ + X-IPA-API-Version: $($CONFIG->"apiVersion"),\ + Cookie: $IPACookieData" \ + http-data="$httpdata" \ + keep-result=no + +# http-data="{\"method\":\"dnsrecord_mod\",\"params\":[[\"$($CONFIG->"dnsZone")\",\"$RecordName.\"],{\"arecord\":\"$RecordContent\",\"dnsttl\":$($CONFIG->"recordTTL")}]}" \ +# Cookie: $IPACookie" \ + + } + + +# Output + :return "$resultClean"; + +} + + + + + + + + + + +# Main Runtime + +#:local IPACookie "drytest"; +:local IPACookie [$GetIPACookie $CONFIG]; +:put "DEBUG: IPACookie: $IPACookie"; + +:local CurrentIPv4address [$GetLocalDynamicConfig $CONFIG ipv4address]; +:put "DEBUG CurrentIPv4address: $CurrentIPv4address"; + +:local CurrentIPv6address [$GetLocalDynamicConfig $CONFIG ipv6address]; +:put "DEBUG: CurrentIPV6address: $CurrentIPv6address"; + +:local CurrentIPv6prefix [$GetLocalDynamicConfig $CONFIG ipv6prefix]; +:put "DEBUG: CurrentIPv6prefix: $CurrentIPv6prefix"; + + + + +# "$($CONFIG->"dualStackName").$($CONFIG->"dnsZone")" + +:local CurrentDualStackRecordName "$($CONFIG->"dualStackName").$($CONFIG->"dnsZone")"; +:local CurrentSingleStackIPv4RecordName "$($CONFIG->"ipv4OnlyName").$($CONFIG->"dnsZone")"; +:local CurrentSingleStackIPv6RecordName "$($CONFIG->"ipv6OnlyName").$($CONFIG->"dnsZone")"; +:local CurrentTXTRecordIPv6PrefixName "$($CONFIG->"prefixTxtName").$($CONFIG->"dnsZone")"; + +:local CurrentDualStackRecordv4 [$GetDynamicDNSRecords $CONFIG ipv4address $CurrentDualStackRecordName ]; +:put "DEBUG: Dual-stack: $CurrentDualStackRecordName ipv4-resolves as $CurrentDualStackRecordv4"; + +:local CurrentDualStackRecordv6 [$GetDynamicDNSRecords $CONFIG ipv6address $CurrentDualStackRecordName ]; +:put "DEBUG: Dual-stack: $CurrentDualStackRecordName ipv6-resolves as $CurrentDualStackRecordv6"; + +:local CurrentSingleStackRecordv4 [$GetDynamicDNSRecords $CONFIG ipv4address $CurrentSingleStackIPv4RecordName ]; +:put "DEBUG: Single-stack-IPv4: $CurrentSingleStackIPv4RecordName ipv4-resolves as $CurrentSingleStackRecordv4"; + +:local CurrentSingleStackRecordv6 [$GetDynamicDNSRecords $CONFIG ipv6address $CurrentSingleStackIPv6RecordName ]; +:put "DEBUG: Single-stack-IPv6: $CurrentSingleStackIPv6RecordName ipv6-resolves as $CurrentSingleStackRecordv6"; + +:local CurrentTXTRecordIPv6Prefix [$GetDynamicDNSRecords $CONFIG ipv6prefix $CurrentTXTRecordIPv6PrefixName ]; +:put "DEBUG: $CurrentTXTRecordIPv6PrefixName resolves as $CurrentTXTRecordIPv6Prefix"; + +:local DebugUpdate [$UpdateDynamicDNSRecord $CONFIG $IPACookie ipv4address $CurrentSingleStackIPv4RecordName $CurrentIPv4address ]; +:put "DEBUG: $DebugUpdate"; + diff --git a/dnsdrone.sh b/dnsdrone.sh new file mode 100755 index 0000000..df9fa31 --- /dev/null +++ b/dnsdrone.sh @@ -0,0 +1,426 @@ +#!/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}" <