Initial commit
This commit is contained in:
commit
ababb6048b
4 changed files with 870 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.env
|
98
README.md
Normal file
98
README.md
Normal 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
345
dnsdrone.rsc
Executable 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
426
dnsdrone.sh
Executable 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 "$@"
|
Loading…
Add table
Reference in a new issue