# Update #12 - Step-by-Step Breakdown of UFW DDNS Update Script

#### **Date:** May 11, 2025  
**Category:** Automation / Firewall  
**Backlink:** [Update #11 – Syncthing UFW DDNS Cron Recovery &amp; Long-Term Rule Persistence](https://docs.natenetworks.com/books/02-project-notes/page/update-11-syncthing-ufw-ddns-cron-recovery-long-term-rule-persistence)

### Overview

This update documents the full working version of the `update-syncthing-ufw.sh` script, designed to automatically update UFW rules for Syncthing ports based on the current IP address of a DDNS hostname.

### Script Location

```bash
/usr/local/bin/update-syncthing-ufw.sh
```

### Log File

```bash
/var/log/update-syncthing-ufw.log
```

### Full Script Breakdown

```bash
#!/bin/bash
```

Starts a bash shell script.

```bash
DDNS_HOST="<your-ddns-name"
```

Specifies your DDNS hostname to resolve dynamically.

```bash
LOGFILE="/var/log/update-syncthing-ufw.log"
```

Sets the path where all log entries will be stored.

```bash
PORTS=(
    "8384/tcp"
    "22000/tcp"
    "21027/udp"
)
```

Defines an array of Syncthing-related ports (Web UI, sync port, and discovery port).

```bash
timestamp() {
    date "+%Y-%m-%d %H:%M:%S"
}
```

Defines a helper function for timestamp formatting.

```bash
RESOLVED_IP=$(dig +short "$DDNS_HOST" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
```

Uses `dig` to resolve the IP for your DDNS and filter for valid IPv4 results.

```bash
if [[ -z "$RESOLVED_IP" ]]; then
    echo "$(timestamp) ❌ Failed to resolve IP for $DDNS_HOST" | tee -a "$LOGFILE"
    exit 1
else
    echo "$(timestamp) 🔍 Resolved IP for $DDNS_HOST: $RESOLVED_IP" | tee -a "$LOGFILE"
fi
```

Logs resolution status and aborts if the IP can't be resolved.

```bash
for PORT in "${PORTS[@]}"; do
    sudo ufw delete allow from any to any port "$PORT" comment 'Syncthing DDNS Access' > /dev/null 2>&1 || true
done
```

Deletes any prior rules with the comment 'Syncthing DDNS Access' silently. `|| true` ensures the script continues even if a rule doesn't exist.

```bash
ALL_ADDED=true
```

Tracks success status across all rule additions.

```bash
for PORT in "${PORTS[@]}"; do
```

Loops over each port.

```bash
    if sudo ufw status | grep -q "$PORT.*$RESOLVED_IP"; then
        echo "$(timestamp) ✳️ Rule already exists: $PORT from $RESOLVED_IP" | tee -a "$LOGFILE"
```

If the rule already exists, log it as a no-op.

```bash
    elif sudo ufw allow from "$RESOLVED_IP" to any port "$PORT" comment 'Syncthing DDNS Access' > /dev/null 2>&1; then
        echo "$(timestamp) ✅ Rule added: $PORT from $RESOLVED_IP" | tee -a "$LOGFILE"
```

If the rule doesn’t exist, add it and log success.

```bash
    else
        echo "$(timestamp) ❌ Failed to add rule: $PORT from $RESOLVED_IP" | tee -a "$LOGFILE"
        ALL_ADDED=false
    fi
done
```

If adding fails, log an error and flag the batch as partially failed.

```bash
if $ALL_ADDED; then
    echo "$(timestamp) ✅ All UFW rules successfully updated for Syncthing from $RESOLVED_IP" | tee -a "$LOGFILE"
else
    echo "$(timestamp) ⚠️ Partial failure updating UFW rules for Syncthing from $RESOLVED_IP" | tee -a "$LOGFILE"
fi
```

Provides a final summary log depending on success/failure of all rule additions.

### Fixes &amp; Adjustments Made

- Fixed `Permission denied` errors by ensuring the script runs with `sudo` when needed and logs are only written by root.
- Replaced silent failures with emoji-marked status outputs (`✅`, `❌`, `⚠️`, `✳️`) for readability.
- Confirmed logs rotate daily via `/etc/logrotate.d/update-syncthing-ufw`.

### Testing &amp; Verification

- Manual execution verified with:

```bash
sudo /usr/local/bin/update-syncthing-ufw.sh
```

- UFW rules confirmed using:

```bash
sudo ufw status verbose
```

- Log output tail:

```bash
sudo tail -n 20 /var/log/update-syncthing-ufw.log
```