Initial commit

This commit is contained in:
Ruben 2025-03-04 14:53:24 +01:00
commit ababb6048b
4 changed files with 870 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.env

98
README.md Normal file
View file

@ -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.<script_name>`)
4. Script's symlink path directory (`.env.<symlink_name>`)
5. `/opt/skyfritt-tools-env/.env.<symlink_name>`
### 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

345
dnsdrone.rsc Executable file
View file

@ -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";

426
dnsdrone.sh Executable file
View file

@ -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}" <<EOF || error "nsupdate failed"
server ${SERVER}
zone ${ZONE_NAME}.
$(if [[ "$record_type" == "A" && "${RECORD_A:-false}" == "true" ]]; then
echo "update delete ${FQDN} A"
echo "update add ${FQDN} ${ttl} A ${ip}"
fi)
$(if [[ "$record_type" == "AAAA" && "${RECORD_AAAA:-false}" == "true" ]]; then
echo "update delete ${FQDN} AAAA"
echo "update add ${FQDN} ${ttl} AAAA ${ip}"
fi)
$(if [[ "$record_type" == "TXT" && "${RECORD_TXT_PREFIX:-false}" == "true" ]]; then
echo "update delete ${FQDN} TXT"
echo "update add ${FQDN} ${ttl} TXT ${ip}"
fi)
send
EOF
}
# Main update function
update_record() {
local record_type=$1
local current_ip=$2
local dns_ip
dns_ip=$(get_dns_records "$record_type")
debug "Checking $record_type record - Current DNS: '$dns_ip', New record: '$current_ip'"
if [[ "$dns_ip" != "$current_ip" ]]; then
debug "Updating $record_type record from '$dns_ip' to '$current_ip'"
if [[ -n "${USER:-}" && -n "${PASSWORD:-}" ]]; then
update_freeipa_record "$record_type" "$current_ip"
elif [[ -n "${TSIG_KEYNAME:-}" && -n "${TSIG_KEY:-}" ]]; then
update_nsupdate_record "$record_type" "$current_ip"
else
error "No valid authentication method available"
fi
else
debug "No update needed for $record_type record"
fi
}
# Main execution
main() {
# Process flags
while getopts "dn" opt; do
case $opt in
d) DEBUG=1 ;;
n) DRY_RUN=1 ;;
*) error "Invalid option: -$OPTARG" ;;
esac
done
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"
local expected_ptr="${RECORD_NAME}.${ZONE_NAME}."
local current_ptr4
current_ptr4=$(get_ptr_record "$current_ipv4")
debug "Current PTR4 record: $current_ptr4"
if [[ "$current_ptr4" != "$expected_ptr" ]]; then
update_record "PTR" "$expected_ptr"
fi
else
debug "Skipping PTR4 record update - no IPv4 address available"
fi
fi
if [[ "${RECORD_PTR6:-false}" == "true" ]]; then
if [[ -n "$current_ipv6" ]]; then
debug "Processing PTR record for IPv6"
local expected_ptr="${RECORD_NAME}.${ZONE_NAME}."
local current_ptr6
current_ptr6=$(get_ptr_record "$current_ipv6")
debug "Current PTR6 record: $current_ptr6"
if [[ "$current_ptr6" != "$expected_ptr" ]]; then
update_record "PTR" "$expected_ptr"
fi
else
debug "Skipping PTR6 record update - no IPv6 address available"
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
current_network_address_quotes=$(get_dns_records "TXT")
current_network_address=${current_network_address_quotes//\"/}
expected_network_address_without_prefix="$($IPCALC_CMD "$current_ipv6"/"$ISP_PREFIX_LENGHT" --network --no-decorate)"
expected_network_address="${expected_network_address_without_prefix}/${ISP_PREFIX_LENGHT}"
debug "Current network address: ${current_network_address}"
debug "Expected network address: ${expected_network_address}"
if [[ "$current_network_address" != "$expected_network_address" ]]; then
debug "Current network address differs from the expected. Updating."
update_record "TXT" "$expected_network_address"
else
debug "Current network address is the same as expected. Doing nothing."
fi
else
debug "Skipping TXT record update - no IPv6 address available"
fi
fi
}
main "$@"