From e92690a7111d1cd27399c3515bbd472624db205f Mon Sep 17 00:00:00 2001 From: Dani Guardiola Date: Mon, 12 Jan 2026 05:19:12 +0100 Subject: [PATCH] add support for updating multiple domains --- README.md | 19 +++++-- dns-sync.sh | 123 ++++++++++++++++++++++++++++++++++++--------- dns.config.example | 11 ++-- 3 files changed, 120 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index c6ce5c0..97be655 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Vercel Dynamic DNS Simple script for exposing a local server with [Vercel DNS](https://vercel.com/docs/custom-domains). -It runs on CRON, checking the current IP address and updating DNS records for your domain. +It runs on CRON, checking the current IP address and updating DNS records for your domains. ## Installation @@ -12,19 +12,30 @@ It runs on CRON, checking the current IP address and updating DNS records for yo 5. Open the cron settings using the command `crontab -e` 6. Add the following line to the cron job: `*/15 * * * * /path-to/vercel-ddns/dns-sync.sh` +### Configuration + +Set these values inside `dns.config`: + +- `VERCEL_TOKEN`: Personal token from https://vercel.com/account/tokens +- `DOMAINS`: Comma-separated list of fully qualified domains you want to keep in sync. Examples: `example.com`, `api.example.com, www.example.com`, `foo.bar.example.com, example.com`. The script treats the last two labels as the apex domain (e.g. `foo.bar.example.com` → apex `example.com`, subdomain `foo.bar`). + ## Usage example ```sh -# Creating +# Creating (multiple domains supported) ➜ ./dns-sync.sh Updating IP: x.x.x.x -Record for SUBDOMAIN does not exist. Creating... +Processing api.example.com +Record for api.example.com does not exist. Creating... +Processing example.com +Record for example.com already exists (id: rec_xxxxxxxxxxxxxxxxxxxxxxxx). Updating... 🎉 Done! # Updating ➜ ./dns-sync.sh Updating IP: x.x.x.x -Record for SUBDOMAIN already exists (id: rec_xxxxxxxxxxxxxxxxxxxxxxxx). Updating... +Processing api.example.com +Record for api.example.com already exists (id: rec_xxxxxxxxxxxxxxxxxxxxxxxx). Updating... 🎉 Done! ``` diff --git a/dns-sync.sh b/dns-sync.sh index a25b74d..a35ba3b 100755 --- a/dns-sync.sh +++ b/dns-sync.sh @@ -18,16 +18,21 @@ get_current_ip() { echo $ip } -# Function to check if subdomain exists +# Function to check if subdomain exists for a given domain check_subdomain_exists() { - local subdomain="$1" + local domain_name="$1" + local subdomain="$2" local response - response=$(curl -sX GET "https://api.vercel.com/v4/domains/$DOMAIN_NAME/records" \ + response=$(curl -sX GET "https://api.vercel.com/v4/domains/$domain_name/records" \ -H "Authorization: Bearer $VERCEL_TOKEN" \ -H "Content-Type: application/json") local record_id - record_id=$(echo "$response" | jq -r ".records[] | select(.name == \"$subdomain\") | .id") + if [[ -z "$subdomain" ]]; then + record_id=$(echo "$response" | jq -r '.records[] | select((.name == "") or (.name == null)) | .id') + else + record_id=$(echo "$response" | jq -r ".records[] | select(.name == \"$subdomain\") | .id") + fi if [[ -n "$record_id" ]]; then # Return record ID if exists echo "$record_id" @@ -39,15 +44,21 @@ check_subdomain_exists() { # Updates dns record update_dns_record() { - local ip="$1" - local record_id="$2" + local domain_name="$1" + local subdomain="$2" + local ip="$3" + local record_id="$4" + local host_display="$domain_name" + if [[ -n "$subdomain" ]]; then + host_display="$subdomain.$domain_name" + fi local response response=$(curl -sX PATCH "https://api.vercel.com/v1/domains/records/$record_id" \ -H "Authorization: Bearer $VERCEL_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "comment": "vercel-ddns", - "name": "'$SUBDOMAIN'", + "name": "'$subdomain'", "type": "A", "value": "'$ip'", "ttl": 60 @@ -57,21 +68,27 @@ update_dns_record() { if [[ "$response" == *"error"* ]]; then local error_message error_message=$(echo "$response" | jq -r '.error.message') - echo "⚠️ Error updating DNS record: $error_message" - exit 1 + echo "⚠️ Error updating DNS record for $host_display: $error_message" + return 1 fi } # Creates dns record create_dns_record() { - local ip="$1" + local domain_name="$1" + local subdomain="$2" + local ip="$3" + local host_display="$domain_name" + if [[ -n "$subdomain" ]]; then + host_display="$subdomain.$domain_name" + fi local response - response=$(curl -sX POST "https://api.vercel.com/v4/domains/$DOMAIN_NAME/records" \ + response=$(curl -sX POST "https://api.vercel.com/v4/domains/$domain_name/records" \ -H "Authorization: Bearer $VERCEL_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "comment": "vercel-ddns", - "name": "'$SUBDOMAIN'", + "name": "'$subdomain'", "type": "A", "value": "'$ip'", "ttl": 60 @@ -81,23 +98,81 @@ create_dns_record() { if [[ "$response" == *"error"* ]]; then local error_message error_message=$(echo "$response" | jq -r '.error.message') - echo "⚠️ Error creating DNS record: $error_message" - exit 1 + echo "⚠️ Error creating DNS record for $host_display: $error_message" + return 1 + fi +} + +# Split a fully qualified domain into apex and subdomain parts +split_domain() { + local full_domain="$1" + local IFS='.' + read -r -a parts <<< "$full_domain" + local total_parts=${#parts[@]} + if (( total_parts < 2 )); then + echo "⚠️ Invalid domain: $full_domain" >&2 + return 1 + fi + + local apex + apex="${parts[$((total_parts - 2))]}.${parts[$((total_parts - 1))]}" + + local subdomain_parts=() + if (( total_parts > 2 )); then + subdomain_parts=("${parts[@]:0:$((total_parts - 2))}") + fi + + local subdomain="" + if (( ${#subdomain_parts[@]} > 0 )); then + local IFS='.' + subdomain="${subdomain_parts[*]}" fi + + echo "$apex|$subdomain" } -# Get current IP +# Clean and split the DOMAINS list into an array +IFS=',' read -r -a DOMAIN_ENTRIES <<< "$DOMAINS" +IFS=$' \t\n' + +# Get current IP once for all domains ip=$(get_current_ip) echo "Updating IP: $ip" -# Check if subdomain exists -record_id=$(check_subdomain_exists "$SUBDOMAIN") -if [[ -n "$record_id" ]]; then - echo "Record for $SUBDOMAIN already exists (id: $record_id). Updating..." - update_dns_record "$ip" "$record_id" -else - echo "Record for $SUBDOMAIN does not exist. Creating..." - create_dns_record "$ip" -fi +for raw_domain in "${DOMAIN_ENTRIES[@]}"; do + domain_trimmed=$(echo "$raw_domain" | xargs) + + if [[ -z "$domain_trimmed" ]]; then + continue + fi + + split_result=$(split_domain "$domain_trimmed") || continue + domain_name=${split_result%%|*} + subdomain=${split_result#*|} + + # Log which host we are processing + if [[ -z "$subdomain" ]]; then + echo "Processing apex $domain_name" + else + echo "Processing $subdomain.$domain_name" + fi + + record_id=$(check_subdomain_exists "$domain_name" "$subdomain") + if [[ -n "$record_id" ]]; then + if [[ -z "$subdomain" ]]; then + echo "Record for $domain_name already exists (id: $record_id). Updating..." + else + echo "Record for $subdomain.$domain_name already exists (id: $record_id). Updating..." + fi + update_dns_record "$domain_name" "$subdomain" "$ip" "$record_id" + else + if [[ -z "$subdomain" ]]; then + echo "Record for $domain_name does not exist. Creating..." + else + echo "Record for $subdomain.$domain_name does not exist. Creating..." + fi + create_dns_record "$domain_name" "$subdomain" "$ip" + fi +done echo "🎉 Done!" diff --git a/dns.config.example b/dns.config.example index 90cc8e0..322f1ce 100644 --- a/dns.config.example +++ b/dns.config.example @@ -3,8 +3,9 @@ # Get the token from https://vercel.com/account/tokens VERCEL_TOKEN="" -# Domain name (e.g. example.com) -DOMAIN_NAME="" - -# Subdomain to update (note: use an empty string for the root record) -SUBDOMAIN="" +# Comma-separated list of full domains (with or without subdomains) +# Examples: +# "example.com" +# "api.example.com, www.example.com" +# "foo.bar.example.com, example.com" +DOMAINS=""