diff --git a/README.md b/README.md index 4cbd1f1..8cc2d91 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,260 @@ -# ddns-powerdns-updater +# PowerDNS DDNS Updater -Hybrid DDNS service for PowerDNS setups. Synchronizes external Infomaniak updates with PowerDNS internal A record management via systemd. \ No newline at end of file +Hybrid DDNS service for PowerDNS environments. Automatically synchronizes external Infomaniak DNS updates with PowerDNS internal A record management via systemd service and timer. + +## Features + +- **Dual DNS Management**: Updates both external (Infomaniak) and internal (PowerDNS) DNS records +- **Automatic IP Detection**: Fetches current external IP using `ifconfig.me` +- **Smart Updates**: Only updates records when IP address changes +- **Comprehensive Logging**: Tracks IP changes and DNS updates with timestamps +- **Systemd Integration**: Runs automatically as background service with configurable intervals +- **Multi-Domain Support**: Manages multiple internal PowerDNS domains from single configuration +- **Error Handling**: Validates IP addresses and handles API failures gracefully + +## Architecture + +The service consists of four main components: + +1. **Main Script** (`pdns-ddns`) - Core logic for IP detection and DNS updates +2. **Configuration** (`ddns.conf`) - API credentials and domain settings +3. **Systemd Service** (`pdns-ddns.service`) - Service definition for one-shot execution +4. **Systemd Timer** (`pdns-ddns.timer`) - Scheduler running every minute + +## Installation + +### 1. Clone and Setup Files + +```bash +# Create directories +sudo mkdir -p /etc/powerdns + +# Copy files to system locations +sudo cp pdns-ddns /usr/local/bin/ +sudo cp ddns.conf /etc/powerdns/ +sudo cp pdns-ddns.service /etc/systemd/system/ +sudo cp pdns-ddns.timer /etc/systemd/system/ + +# Make script executable +sudo chmod +x /usr/local/bin/pdns-ddns +``` + +### 2. Configure Settings + +Edit the configuration file: + +```bash +sudo nano /etc/powerdns/ddns.conf +``` + +Update the following variables: + +```bash +# Infomaniak API credentials +INFOMANIAK_TOKEN="your-infomaniak-api-token" +INFOMANIAK_DOMAIN="your-external-domain.com" + +# Internal PowerDNS domains (space-separated) +POWERDNS_DOMAINS="internal1.com internal2.xyz" + +# DNS record TTL in seconds +TTL=300 +``` + +### 3. Enable and Start Service + +```bash +# Reload systemd configuration +sudo systemctl daemon-reload + +# Enable and start the timer +sudo systemctl enable pdns-ddns.timer +sudo systemctl start pdns-ddns.timer + +# Check status +sudo systemctl status pdns-ddns.timer +``` + +## Configuration + +### Infomaniak API Setup + +1. Log into your Infomaniak account +2. Navigate to API settings +3. Generate an API token with DNS management permissions +4. Add the token to `INFOMANIAK_TOKEN` in the configuration + +### PowerDNS Requirements + +The script requires PowerDNS with the following tools installed: +- `pdnsutil` - PowerDNS utility for zone management +- Proper permissions for the script to modify DNS zones + +### Supported Domains + +- **External**: Single Infomaniak-managed domain +- **Internal**: Multiple PowerDNS zones (space-separated in config) + +## Usage + +### Manual Execution + +Run the script manually for testing: + +```bash +sudo /usr/local/bin/pdns-ddns +``` + +### Service Management + +```bash +# Check timer status +sudo systemctl status pdns-ddns.timer + +# View recent service runs +sudo systemctl status pdns-ddns.service + +# Check logs +sudo journalctl -u pdns-ddns.service -f + +# Stop/start the timer +sudo systemctl stop pdns-ddns.timer +sudo systemctl start pdns-ddns.timer +``` + +### Changing Update Interval + +Edit the timer configuration: + +```bash +sudo nano /etc/systemd/system/pdns-ddns.timer +``` + +Modify the `OnCalendar` setting: +- Every minute: `OnCalendar=*:*` +- Every 5 minutes: `OnCalendar=*:*/5` +- Every hour: `OnCalendar=hourly` + +Then reload and restart: + +```bash +sudo systemctl daemon-reload +sudo systemctl restart pdns-ddns.timer +``` + +## Logging + +### IP Change History + +The service maintains an IP change log at `/var/log/ddns-ip-history.log`: + +```bash +# View recent IP changes +sudo tail -f /var/log/ddns-ip-history.log + +# View all changes +sudo cat /var/log/ddns-ip-history.log +``` + +### System Logs + +View detailed execution logs: + +```bash +# Follow live logs +sudo journalctl -u pdns-ddns.service -f + +# View recent logs +sudo journalctl -u pdns-ddns.service --since "1 hour ago" + +# View logs with timestamps +sudo journalctl -u pdns-ddns.service -o short-iso +``` + +## Troubleshooting + +### Common Issues + +**Script fails with permission errors:** +```bash +# Ensure proper ownership and permissions +sudo chown root:root /usr/local/bin/pdns-ddns +sudo chmod 755 /usr/local/bin/pdns-ddns +``` + +**Infomaniak API errors:** +- Verify API token has DNS management permissions +- Check domain name spelling in configuration +- Ensure API token hasn't expired + +**PowerDNS errors:** +- Verify `pdnsutil` is installed and accessible +- Check PowerDNS service is running: `sudo systemctl status pdns` +- Ensure script has permissions to modify DNS zones + +**IP detection fails:** +- Check internet connectivity +- Verify `curl` is installed +- Test manually: `curl -s ifconfig.me` + +### Debug Mode + +Run the script manually to see detailed output: + +```bash +sudo bash -x /usr/local/bin/pdns-ddns +``` + +### Logs Analysis + +Check for specific error patterns: + +```bash +# API errors +sudo journalctl -u pdns-ddns.service | grep -i error + +# IP changes +sudo journalctl -u pdns-ddns.service | grep "IP changed" + +# PowerDNS operations +sudo journalctl -u pdns-ddns.service | grep "PowerDNS" +``` + +## Security Considerations + +- Store API tokens securely in the configuration file +- Limit file permissions: `sudo chmod 600 /etc/powerdns/ddns.conf` +- Regularly rotate Infomaniak API tokens +- Monitor logs for unauthorized access attempts +- Consider firewall rules for PowerDNS management access + +## Dependencies + +### Required Packages + +```bash +# Ubuntu/Debian +sudo apt update +sudo apt install curl jq + +# CentOS/RHEL +sudo yum install curl jq + +# PowerDNS (if not already installed) +sudo apt install pdns-server pdns-tools +``` + +### System Requirements + +- Linux system with systemd +- PowerDNS server with `pdnsutil` +- Internet connectivity for IP detection and Infomaniak API +- Root or sudo access for DNS modifications + +## License + +This project is open source. Feel free to modify and distribute according to your needs. + +## Contributing + +Contributions are welcome! Please ensure any changes maintain compatibility with existing PowerDNS and Infomaniak setups. diff --git a/ddns.conf b/ddns.conf new file mode 100644 index 0000000..fb7e84d --- /dev/null +++ b/ddns.conf @@ -0,0 +1,10 @@ + +# Infomaniak API +INFOMANIAK_TOKEN="YOUR-API-KEY" +INFOMANIAK_DOMAIN="YOUR-DOMAIN" + +# PowerDNS Domains (space separated) +POWERDNS_DOMAINS="INTERNAL-DOMAIN1.com INTERNAL-DOMAIN2.com" + +# TTL for DNS records +TTL=300 diff --git a/pdns-ddns b/pdns-ddns new file mode 100755 index 0000000..188bad3 --- /dev/null +++ b/pdns-ddns @@ -0,0 +1,131 @@ +#!/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" + +# 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 + echo "ERROR: Invalid IP address: $EXTERNAL_IP" + exit 1 +fi + +echo "$(date): External IP: $EXTERNAL_IP" + +# Read last known IP +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 + +# Log IP change if different +if [[ -n "$LAST_IP" && "$LAST_IP" != "$EXTERNAL_IP" ]]; then + echo "$(date): IP changed from $LAST_IP to $EXTERNAL_IP" | tee -a "$IP_HISTORY_FILE" + echo "$(date): 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/' || echo "No previous changes found" +elif [[ -z "$LAST_IP" ]]; then + echo "$(date): IP unchanged: $EXTERNAL_IP (first run or no history)" +else + echo "$(date): IP unchanged: $EXTERNAL_IP" +fi + +# Check and update Infomaniak (external) +echo "$(date): Checking Infomaniak A record for $INFOMANIAK_DOMAIN..." +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 + echo "$(date): Infomaniak A record already correct: $EXTERNAL_IP" +else + if [[ -n "$CURRENT_RECORD" ]]; then + echo "$(date): Infomaniak A record needs update: $CURRENT_RECORD -> $EXTERNAL_IP" + echo "$(date): Infomaniak change: $CURRENT_RECORD -> $EXTERNAL_IP" >> "$IP_HISTORY_FILE" + else + echo "$(date): Infomaniak A record does not exist, creating with IP: $EXTERNAL_IP" + echo "$(date): Infomaniak created: -> $EXTERNAL_IP" >> "$IP_HISTORY_FILE" + fi + + 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 + echo "$(date): 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 + echo "$(date): SUCCESS: Infomaniak A record created" + else + echo "$(date): ERROR: Failed to create Infomaniak A record" + fi + else + echo "$(date): Updating 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 + echo "$(date): SUCCESS: Infomaniak A record updated" + else + echo "$(date): ERROR: Failed to update Infomaniak A record" + fi + fi +fi + +# Check and update PowerDNS (internal) +echo "$(date): Checking PowerDNS A records..." +for domain in $POWERDNS_DOMAINS; do + echo "$(date): Checking zone $domain..." + + # Check if zone exists + if ! pdnsutil list-zone "$domain" >/dev/null 2>&1; then + echo "$(date): Zone $domain does not exist, creating..." + echo "$(date): PowerDNS $domain created: -> $EXTERNAL_IP" >> "$IP_HISTORY_FILE" + pdnsutil create-zone "$domain" + NEEDS_UPDATE=true + else + # Check current A record - match actual PowerDNS output format + 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) + + if [[ "$CURRENT_IP" == "$EXTERNAL_IP" ]]; then + echo "$(date): PowerDNS A record for $domain already correct: $EXTERNAL_IP" + NEEDS_UPDATE=false + else + if [[ -n "$CURRENT_IP" ]]; then + echo "$(date): PowerDNS A record for $domain needs update: $CURRENT_IP -> $EXTERNAL_IP" + echo "$(date): PowerDNS $domain change: $CURRENT_IP -> $EXTERNAL_IP" >> "$IP_HISTORY_FILE" + else + echo "$(date): PowerDNS A record for $domain does not exist, creating with IP: $EXTERNAL_IP" + echo "$(date): PowerDNS $domain created: -> $EXTERNAL_IP" >> "$IP_HISTORY_FILE" + fi + NEEDS_UPDATE=true + fi + fi + + # Update if needed + if [[ "$NEEDS_UPDATE" == "true" ]]; then + echo "$(date): Updating PowerDNS A record for $domain..." + if pdnsutil replace-rrset "$domain" @ A "$TTL" "$EXTERNAL_IP" 2>/dev/null; then + echo "$(date): SUCCESS: PowerDNS A record updated for $domain" + pdnsutil rectify-zone "$domain" 2>/dev/null + pdnsutil increase-serial "$domain" 2>/dev/null + else + echo "$(date): ERROR: Failed to update PowerDNS A record for $domain" + fi + fi +done + +echo "$(date): DDNS check/update completed" + +# Show recent IP history summary +if [[ -f "$IP_HISTORY_FILE" ]]; then + echo "$(date): Recent changes (last 5):" + tail -5 "$IP_HISTORY_FILE" | grep -E "(change:|created:)" || echo "No recent changes" +fi diff --git a/pdns-ddns.service b/pdns-ddns.service new file mode 100644 index 0000000..8c0be62 --- /dev/null +++ b/pdns-ddns.service @@ -0,0 +1,10 @@ +[Unit] +Description=PowerDNS DDNS Updater +After=network.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/pdns-ddns +User=root +StandardOutput=journal +StandardError=journal diff --git a/pdns-ddns.timer b/pdns-ddns.timer new file mode 100644 index 0000000..346d943 --- /dev/null +++ b/pdns-ddns.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Run PowerDNS DDNS update every minute +Requires=pdns-ddns.service + +[Timer] +OnCalendar=*:* +Persistent=true + +[Install] +WantedBy=timers.target