#!/bin/bash # pdns-ddns - Updates both Infomaniak and PowerDNS with external IP # Speichern als: /usr/local/bin/pdns-ddns # Load config source /etc/powerdns/ddns.conf # IP History file IP_HISTORY_FILE="/var/log/ddns-ip-history.log" # ============================================================================ # LOGGING FUNCTIONS # ============================================================================ log_info() { echo "$(date): $1" } log_error() { echo "$(date): ERROR: $1" } log_success() { echo "$(date): SUCCESS: $1" } log_change() { local message="$1" echo "$(date): $message" echo "$(date): $message" >> "$IP_HISTORY_FILE" } log_history() { echo "$(date): $1" >> "$IP_HISTORY_FILE" } # ============================================================================ # IP CHANGE DETECTION # ============================================================================ check_ip_change() { # Get external IP EXTERNAL_IP=$(curl -s ifconfig.me) if [[ ! $EXTERNAL_IP =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then log_error "Invalid IP address: $EXTERNAL_IP" exit 1 fi # Read last known IP local last_ip="" if [[ -f "$IP_HISTORY_FILE" ]]; then last_ip=$(grep "IP changed from" "$IP_HISTORY_FILE" | tail -1 | sed 's/.*to \([0-9.]*\)$/\1/') fi # Check if IP changed if [[ -n "$last_ip" && "$last_ip" != "$EXTERNAL_IP" ]]; then log_info "External IP: $EXTERNAL_IP" log_change "IP changed from $last_ip to $EXTERNAL_IP" log_info "Previous IPs in last 10 changes:" grep "IP changed from" "$IP_HISTORY_FILE" | tail -10 | sed 's/.*from \([0-9.]*\) to \([0-9.]*\)$/Old: \1 -> New: \2/' || log_info "No previous changes found" return 1 # IP changed = update needed elif [[ -z "$last_ip" ]]; then log_info "External IP: $EXTERNAL_IP" log_info "First run or no history - checking DNS records" return 1 # First run = update needed else # IP unchanged - only return 1 if we need to check DNS consistency return 0 # No change needed fi } # ============================================================================ # EXTERNAL DNS (INFOMANIAK) FUNCTIONS # ============================================================================ check_external_dns() { local current_record=$(curl -s -H "Authorization: Bearer $INFOMANIAK_TOKEN" \ "https://api.infomaniak.com/2/zones/$INFOMANIAK_DOMAIN/records" | \ jq -r '.data[] | select(.type=="A" and .source==".") | .target' | head -1) if [[ "$current_record" == "$EXTERNAL_IP" ]]; then return 0 # No update needed - silent success else log_info "Checking Infomaniak A record for $INFOMANIAK_DOMAIN..." if [[ -n "$current_record" ]]; then log_change "Infomaniak A record needs update: $current_record -> $EXTERNAL_IP" else log_change "Infomaniak A record does not exist, creating with IP: $EXTERNAL_IP" fi return 1 # Update needed fi } update_external_dns() { log_info "Updating Infomaniak DNS..." local record_id=$(curl -s -H "Authorization: Bearer $INFOMANIAK_TOKEN" \ "https://api.infomaniak.com/2/zones/$INFOMANIAK_DOMAIN/records" | \ jq -r '.data[] | select(.type=="A" and .source==".") | .id' | head -1) if [[ -z "$record_id" ]]; then log_info "Creating new Infomaniak A record..." if curl -s -X POST "https://api.infomaniak.com/2/zones/$INFOMANIAK_DOMAIN/records" \ -H "Authorization: Bearer $INFOMANIAK_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"type\": \"A\", \"source\": \".\", \"target\": \"$EXTERNAL_IP\", \"ttl\": $TTL}" > /dev/null; then log_success "Infomaniak A record created" log_history "Infomaniak created: -> $EXTERNAL_IP" else log_error "Failed to create Infomaniak A record" return 1 fi else log_info "Updating existing Infomaniak A record..." if curl -s -X PUT "https://api.infomaniak.com/2/zones/$INFOMANIAK_DOMAIN/records/$record_id" \ -H "Authorization: Bearer $INFOMANIAK_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"target\": \"$EXTERNAL_IP\", \"ttl\": $TTL}" > /dev/null; then log_success "Infomaniak A record updated" log_history "Infomaniak change: -> $EXTERNAL_IP" else log_error "Failed to update Infomaniak A record" return 1 fi fi return 0 } # ============================================================================ # INTERNAL DNS (POWERDNS) FUNCTIONS # ============================================================================ ensure_zone_exists() { local domain="$1" if ! pdnsutil list-zone "$domain" >/dev/null 2>&1; then log_info "Zone $domain does not exist, creating..." log_history "PowerDNS $domain zone created" pdnsutil create-zone "$domain" return 1 # Zone was created, needs update fi return 0 # Zone exists } check_powerdns_record() { local domain="$1" local record_type="$2" # "@" for root, "*" for wildcard local display_name="$3" local current_ip if [[ "$record_type" == "@" ]]; then current_ip=$(pdnsutil list-zone "$domain" 2>/dev/null | grep -E "^${domain}\s+[0-9]+\s+IN\s+A\s+" | awk '{print $5}' | head -1) else current_ip=$(pdnsutil list-zone "$domain" 2>/dev/null | grep -E "^\*\.${domain}\s+[0-9]+\s+IN\s+A\s+" | awk '{print $5}' | head -1) fi if [[ "$current_ip" == "$EXTERNAL_IP" ]]; then return 0 # No update needed - silent success else if [[ -n "$current_ip" ]]; then log_change "PowerDNS A record for $display_name needs update: $current_ip -> $EXTERNAL_IP" else log_change "PowerDNS A record for $display_name does not exist, creating with IP: $EXTERNAL_IP" fi return 1 # Update needed fi } update_powerdns_record() { local domain="$1" local record_type="$2" # "@" for root, "*" for wildcard local display_name="$3" log_info "Updating PowerDNS A record for $display_name..." if pdnsutil replace-rrset "$domain" "$record_type" A "$TTL" "$EXTERNAL_IP" 2>/dev/null; then log_success "PowerDNS A record updated for $display_name" log_history "PowerDNS $display_name updated: -> $EXTERNAL_IP" pdnsutil rectify-zone "$domain" 2>/dev/null pdnsutil increase-serial "$domain" 2>/dev/null return 0 else log_error "Failed to update PowerDNS A record for $display_name" return 1 fi } check_internal_dns() { local needs_update=false local needs_logging=false for domain in $POWERDNS_DOMAINS; do # Ensure zone exists (this might need logging) if ! pdnsutil list-zone "$domain" >/dev/null 2>&1; then if [[ "$needs_logging" == "false" ]]; then log_info "Checking PowerDNS A records..." needs_logging=true fi log_info "Zone $domain does not exist, creating..." log_history "PowerDNS $domain zone created" pdnsutil create-zone "$domain" needs_update=true fi # Check root A record if ! check_powerdns_record "$domain" "@" "$domain"; then if [[ "$needs_logging" == "false" ]]; then log_info "Checking PowerDNS A records..." needs_logging=true fi needs_update=true fi # Check wildcard A record if ! check_powerdns_record "$domain" "*" "*.${domain}"; then if [[ "$needs_logging" == "false" ]]; then log_info "Checking PowerDNS A records..." needs_logging=true fi needs_update=true fi done if [[ "$needs_update" == "true" ]]; then return 1 # Update needed else return 0 # No update needed - was completely silent fi } update_internal_dns() { log_info "Updating PowerDNS records..." for domain in $POWERDNS_DOMAINS; do log_info "Processing zone $domain..." # Ensure zone exists ensure_zone_exists "$domain" # Update root A record if needed if ! check_powerdns_record "$domain" "@" "$domain"; then update_powerdns_record "$domain" "@" "$domain" fi # Update wildcard A record if needed if ! check_powerdns_record "$domain" "*" "*.${domain}"; then update_powerdns_record "$domain" "*" "*.${domain}" fi done } # ============================================================================ # MAIN EXECUTION # ============================================================================ main() { # Check if IP changed if ! check_ip_change; then # IP unchanged - check if DNS is consistent local external_needs_update=false local internal_needs_update=false if ! check_external_dns; then external_needs_update=true fi if ! check_internal_dns; then internal_needs_update=true fi # If everything is consistent, exit silently if [[ "$external_needs_update" == "false" && "$internal_needs_update" == "false" ]]; then exit 0 fi # If we reach here, something needs fixing log_info "External IP: $EXTERNAL_IP" log_info "No IP change detected, but DNS inconsistencies found" if [[ "$external_needs_update" == "true" ]]; then update_external_dns fi if [[ "$internal_needs_update" == "true" ]]; then update_internal_dns fi else # IP changed - do full update log_info "Starting DDNS check/update process" # Check and update external DNS (Infomaniak) if ! check_external_dns; then update_external_dns fi # Check and update internal DNS (PowerDNS) if ! check_internal_dns; then update_internal_dns fi fi log_info "DDNS check/update completed" # Show recent IP history summary if [[ -f "$IP_HISTORY_FILE" ]]; then log_info "Recent changes (last 5):" tail -5 "$IP_HISTORY_FILE" | grep -E "(change:|created:|updated:)" || log_info "No recent changes" fi } # Run main function main