#!/bin/bash # Script: ddns-cert-sync.sh # Purpose: Monitor public IP changes and ensure Caddy DNS/TLS is synchronized # Usage: Run via cron (e.g., every 5 minutes) set -euo pipefail # Configuration LOCK_FILE="/tmp/caddy-ddns-cert-sync.lock" STATE_FILE="/var/lib/caddy/last_ip.txt" LOG_FILE="/var/log/caddy-ddns-cert-sync.log" IP_CHECK_URL="https://api.ipify.org?format=txt" CRITICAL_DOMAINS=("recept.gynther.se" "jellyfin.gynther.se" "wetty.gynther.se") DOCKER_CONTAINER_NAME="caddy" # Create directory for state file if it doesn't exist if [ ! -d "/var/lib/caddy" ]; then sudo mkdir -p /var/lib/caddy sudo touch "$STATE_FILE" sudo chown $USER:$USER "$STATE_FILE" fi # Create log file if it doesn't exist if [ ! -f "$LOG_FILE" ]; then sudo touch "$LOG_FILE" sudo chown $USER:$USER "$LOG_FILE" fi # Logging function log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } # Check if script is already running if [ -f "$LOCK_FILE" ]; then log "Lock file exists. Another instance is running. Exiting." exit 0 fi # Create lock file trap 'rm -f "$LOCK_FILE"; exit' INT TERM EXIT touch "$LOCK_FILE" # Get current public IP get_current_ip() { curl -s --max-time 10 "$IP_CHECK_URL" || { log "ERROR: Failed to fetch current IP from $IP_CHECK_URL" echo "" } } # Read last known IP from state file read_last_ip() { if [ -f "$STATE_FILE" ]; then cat "$STATE_FILE" 2>/dev/null || echo "" else echo "" fi } # Write current IP to state file write_current_ip() { echo "$1" > "$STATE_FILE" } # Test HTTPS connectivity test_https() { local domain="$1" if curl -s --max-time 10 -o /dev/null -w "%{http_code}" "https://$domain/" | grep -q "^200$"; then return 0 else return 1 fi } # Reload Caddy in Docker container reload_caddy_in_docker() { if docker ps | grep -q "$DOCKER_CONTAINER_NAME"; then log "Reloading Caddy in Docker container '$DOCKER_CONTAINER_NAME'..." if docker exec "$DOCKER_CONTAINER_NAME" caddy reload --config /etc/caddy/Caddyfile; then log "Caddy reloaded successfully in Docker." return 0 else log "ERROR: Failed to reload Caddy in Docker. Attempting container restart." docker restart "$DOCKER_CONTAINER_NAME" || { log "ERROR: Failed to restart Caddy container." return 1 } log "Caddy container restarted." return 0 fi else log "ERROR: Caddy container '$DOCKER_CONTAINER_NAME' is not running." return 1 fi } # Main execution log "Starting IP check and Caddy sync" CURRENT_IP=$(get_current_ip) if [ -z "$CURRENT_IP" ]; then log "ERROR: Could not determine current public IP. Retrying in next cycle." rm -f "$LOCK_FILE" exit 1 fi log "Current public IP: $CURRENT_IP" LAST_IP=$(read_last_ip) if [ -z "$LAST_IP" ]; then log "No previous IP found. Writing current IP to state file." write_current_ip "$CURRENT_IP" rm -f "$LOCK_FILE" exit 0 fi log "Last known IP: $LAST_IP" if [ "$CURRENT_IP" = "$LAST_IP" ]; then log "IP unchanged. No action needed." rm -f "$LOCK_FILE" exit 0 fi # IP has changed log "IP changed from $LAST_IP to $CURRENT_IP. Triggering Caddy reload and TLS verification." # Update state file write_current_ip "$CURRENT_IP" # Reload Caddy in Docker if ! reload_caddy_in_docker; then log "ERROR: Failed to reload/restart Caddy in Docker." fi # Verify HTTPS for critical domains log "Running HTTPS verification for critical domains..." for domain in "${CRITICAL_DOMAINS[@]}"; do if test_https "$domain"; then log "HTTPS test PASSED for $domain" else log "WARNING: HTTPS test FAILED for $domain (may be due to DNS propagation)" fi done log "IP change handling completed." # Cleanup rm -f "$LOCK_FILE" exit 0