ddns-scripts: add blazingfast.io Anycast DNS provider
authorFotios Kitsantas <redacted>
Sun, 10 May 2026 13:22:55 +0000 (14:22 +0100)
committerFlorian Eckert <redacted>
Mon, 18 May 2026 09:44:25 +0000 (11:44 +0200)
Add DDNS update support for blazingfast.io Anycast DNS via their
REST API. Authentication is performed via JWT token obtained from
the login endpoint. Zone records are fetched to verify the record
type before update, ensuring IPv4 services only target A records
and IPv6 services only target AAAA records.

Service, zone and record IDs are passed via param_opt as
space-separated key=value pairs:
  service_id=X zone_id=Y record_id=Z

curl --config file approach is used throughout to avoid eval and
shell injection from user-controlled values. Supports both IPv4
and IPv6. For dual-stack, create two separate DDNS service sections
with their respective record IDs.

Tested on GL.iNet MT5000 (Brume 3) running OpenWrt with
ddns-scripts 2.8.2.

Signed-off-by: Fotios Kitsantas <redacted>
net/ddns-scripts/Makefile
net/ddns-scripts/files/usr/lib/ddns/update_blazingfast_io.sh [new file with mode: 0644]
net/ddns-scripts/files/usr/share/ddns/default/blazingfast.io.json [new file with mode: 0644]

index 5a8ee109d1fcf9832a33655cdc6ed7de4836a723..5901dbddb965107799485122abc959ba03c9f24f 100644 (file)
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
 
 PKG_NAME:=ddns-scripts
 PKG_VERSION:=2.8.3
-PKG_RELEASE:=4
+PKG_RELEASE:=5
 
 PKG_LICENSE:=GPL-2.0
 
@@ -290,6 +290,22 @@ define Package/ddns-scripts-beget/description
   'option password' to be a valid API key for beget.com
 endef
 
+define Package/ddns-scripts-blazingfast
+  $(call Package/ddns-scripts/Default)
+  TITLE:=Extension for blazingfast.io Anycast DNS API
+  DEPENDS:=ddns-scripts +curl +ca-bundle
+  PROVIDES:=ddns-scripts_blazingfast.io
+  MAINTAINER:=Fotios Kitsantas <fkitsantas@icloud.com>
+endef
+
+define Package/ddns-scripts-blazingfast/description
+  Dynamic DNS Client scripts extension for blazingfast.io Anycast DNS API (requires curl)
+  It requires:
+  'option username'  to be a valid blazingfast.io client area username
+  'option password'  to be the matching blazingfast.io client area password
+  'option domain'    to contain the full DNS record name to update (e.g. uk.nightmare.gr)
+  'option param_opt' to contain service_id=X zone_id=Y record_id=Z
+endef
 
 define Package/ddns-scripts-pdns
   $(call Package/ddns-scripts/Default)
@@ -468,6 +484,7 @@ define Package/ddns-scripts-services/install
        rm $(1)/usr/share/ddns/default/dnspod.cn-v3.json
        rm $(1)/usr/share/ddns/default/no-ip.com.json
        rm $(1)/usr/share/ddns/default/bind-nsupdate.json
+       rm $(1)/usr/share/ddns/default/blazingfast.io.json
        rm $(1)/usr/share/ddns/default/route53-v1.json
        rm $(1)/usr/share/ddns/default/cnkuai.cn.json
        rm $(1)/usr/share/ddns/default/gandi.net.json
@@ -829,6 +846,23 @@ fi
 exit 0
 endef
 
+define Package/ddns-scripts-blazingfast/install
+       $(INSTALL_DIR) $(1)/usr/lib/ddns
+       $(INSTALL_BIN) ./files/usr/lib/ddns/update_blazingfast_io.sh \
+               $(1)/usr/lib/ddns
+
+       $(INSTALL_DIR) $(1)/usr/share/ddns/default
+       $(INSTALL_DATA) ./files/usr/share/ddns/default/blazingfast.io.json \
+               $(1)/usr/share/ddns/default/
+endef
+
+define Package/ddns-scripts-blazingfast/prerm
+#!/bin/sh
+if [ -z "$${IPKG_INSTROOT}" ]; then
+       /etc/init.d/ddns stop
+fi
+exit 0
+endef
 
 define Package/ddns-scripts-pdns/install
        $(INSTALL_DIR) $(1)/usr/lib/ddns
@@ -999,6 +1033,7 @@ $(eval $(call BuildPackage,ddns-scripts-route53))
 $(eval $(call BuildPackage,ddns-scripts-cnkuai))
 $(eval $(call BuildPackage,ddns-scripts-gandi))
 $(eval $(call BuildPackage,ddns-scripts-beget))
+$(eval $(call BuildPackage,ddns-scripts-blazingfast))
 $(eval $(call BuildPackage,ddns-scripts-pdns))
 $(eval $(call BuildPackage,ddns-scripts-scaleway))
 $(eval $(call BuildPackage,ddns-scripts-transip))
diff --git a/net/ddns-scripts/files/usr/lib/ddns/update_blazingfast_io.sh b/net/ddns-scripts/files/usr/lib/ddns/update_blazingfast_io.sh
new file mode 100644 (file)
index 0000000..9fa9fb1
--- /dev/null
@@ -0,0 +1,336 @@
+#!/bin/sh
+#
+# Distributed under the terms of the GNU General Public License (GPL) version 2.0
+#
+# Script for sending updates to blazingfast.io Anycast DNS
+# API documentation: https://my.blazingfast.io/api
+#
+# May, 2026 - Fotios Kitsantas <fkitsantas@icloud.com>
+#
+# This script is parsed by dynamic_dns_functions.sh inside send_update() function
+#
+# using following options from /etc/config/ddns
+# option username  - Your Blazingfast client area username
+# option password  - Your Blazingfast client area password
+# option domain    - Full DNS record name to update, e.g. hostname.yourdomain.com
+# option param_opt - Space-separated key=value pairs:
+#                    service_id=SERVICE_ID zone_id=ZONE_ID record_id=RECORD_ID
+#
+# For dual-stack IPv4 + IPv6, create two DDNS service sections:
+#   - one with option use_ipv6 '0' and the A record_id
+#   - one with option use_ipv6 '1' and the AAAA record_id
+#
+# The hostname may be the same for both records, for example:
+#   hostname.yourdomain.com A    x.x.x.x
+#   hostname.yourdomain.com AAAA xxxx:xxxx::xxxx
+#
+# Example /etc/config/ddns configuration
+#
+# IPv4 only, updates the A record:
+#
+# config service 'blazingfast_ipv4'
+#      option enabled '1'
+#      option service_name 'blazingfast.io'
+#      option use_ipv6 '0'
+#      option domain 'hostname.yourdomain.com'
+#      option username 'YOUR_USERNAME'
+#      option password 'YOUR_PASSWORD'
+#      option param_opt 'service_id=SERVICE_ID zone_id=ZONE_ID record_id=A_RECORD_ID'
+#
+# IPv6 only, updates the AAAA record:
+#
+# config service 'blazingfast_ipv6'
+#      option enabled '1'
+#      option service_name 'blazingfast.io'
+#      option use_ipv6 '1'
+#      option domain 'hostname.yourdomain.com'
+#      option username 'YOUR_USERNAME'
+#      option password 'YOUR_PASSWORD'
+#      option param_opt 'service_id=SERVICE_ID zone_id=ZONE_ID record_id=AAAA_RECORD_ID'
+#
+# Dual-stack IPv4 + IPv6:
+#
+# Use both sections above at the same time.
+# The domain can be the same for both A and AAAA records.
+# The record_id must be different:
+#   - A_RECORD_ID for the A record
+#   - AAAA_RECORD_ID for the AAAA record
+#
+# How to find your service_id, zone_id, record_id:
+#
+#   1. Get your token:
+#      TOKEN=$(curl -s -X POST 'https://my.blazingfast.io/api/login' \
+#        -d "username=USERNAME" \
+#        -d "password=PASSWORD" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
+#
+#   2. List services to get service_id:
+#      curl -s 'https://my.blazingfast.io/api/service' \
+#        -H "Authorization: Bearer $TOKEN" | python3 -m json.tool
+#
+#   3. List DNS zones to get zone_id (replace SERVICE_ID):
+#      curl -s 'https://my.blazingfast.io/api/service/SERVICE_ID/dns' \
+#        -H "Authorization: Bearer $TOKEN" | python3 -m json.tool
+#
+#   4. List records to get record_id values, including both A and AAAA records:
+#      curl -s 'https://my.blazingfast.io/api/service/SERVICE_ID/dns/ZONE_ID' \
+#        -H "Authorization: Bearer $TOKEN" | python3 -m json.tool
+#
+#   Then set param_opt to:
+#      service_id=SERVICE_ID zone_id=ZONE_ID record_id=RECORD_ID
+#
+# variable __IP already defined with the ip-address to use for update
+#
+
+. /usr/share/libubox/jshn.sh
+
+# check parameters
+[ -z "$CURL_SSL" ] && write_log 14 "Blazingfast communication requires cURL with SSL support. Please install"
+[ -z "$username" ] && write_log 14 "Service section not configured correctly! Missing 'username'"
+[ -z "$password" ] && write_log 14 "Service section not configured correctly! Missing 'password'"
+[ -z "$domain"   ] && write_log 14 "Service section not configured correctly! Missing 'domain'"
+[ "${use_https:-0}" -eq 0 ] && use_https=1     # force HTTPS
+
+# Always use the SSL-capable curl binary because the Blazingfast API is HTTPS-only.
+# $CURL may be unset if the framework only detected an SSL-enabled curl.
+local __CURLBIN="$CURL_SSL"
+
+# parse param_opt — expects: service_id=X zone_id=Y record_id=Z
+local __SERVICE_ID __ZONE_ID __RECORD_ID
+if [ -n "$param_opt" ]; then
+       for pair in $param_opt; do
+               case $pair in
+                       service_id=*) __SERVICE_ID=${pair#*=}; write_log 7 "service_id: $__SERVICE_ID" ;;
+                       zone_id=*)    __ZONE_ID=${pair#*=};    write_log 7 "zone_id: $__ZONE_ID" ;;
+                       record_id=*)  __RECORD_ID=${pair#*=};  write_log 7 "record_id: $__RECORD_ID" ;;
+                       *) ;;
+               esac
+       done
+fi
+
+[ -z "$__SERVICE_ID" ] && write_log 14 "param_opt missing service_id=VALUE"
+[ -z "$__ZONE_ID"    ] && write_log 14 "param_opt missing zone_id=VALUE"
+[ -z "$__RECORD_ID"  ] && write_log 14 "param_opt missing record_id=VALUE"
+
+# set record type based on use_ipv6 flag
+local __TYPE __IPVERSION
+if [ "${use_ipv6:-0}" -eq 0 ]; then
+       __TYPE="A"
+       __IPVERSION="4"
+else
+       __TYPE="AAAA"
+       __IPVERSION="6"
+fi
+
+local __URLBASE="https://my.blazingfast.io/api"
+local __TOKEN __DATA __RECTYPE __DEVICE __PAYLOAD
+local __CURLCFG="${DATFILE}.curl"
+local __CURLEXTRA="${DATFILE}.extra"
+
+# Explicit cleanup helper. We deliberately avoid `trap ... EXIT` because this
+# script is sourced into the long-running ddns runtime, where a global trap
+# would leak past this provider invocation and could clobber unrelated files
+# or override traps installed by the framework / other providers.
+blazingfast_cleanup() {
+       rm -f "$__CURLCFG" "$__CURLEXTRA"
+}
+
+# -------------------------------------------------------------------
+# blazingfast_transfer — invokes curl via config file
+# Avoids eval and shell injection from user-controlled values.
+# Call-specific options (method, url, headers, data) are written
+# to __CURLEXTRA before each call and appended to the base config.
+# -------------------------------------------------------------------
+blazingfast_transfer() {
+       local __CNT=0
+       local __ERR
+
+       while : ; do
+               # Build fresh curl config for this attempt
+               : > "$__CURLCFG"
+               echo "silent" >> "$__CURLCFG"
+               echo "show-error" >> "$__CURLCFG"
+               echo "remote-time" >> "$__CURLCFG"
+               echo "output = \"$DATFILE\"" >> "$__CURLCFG"
+               echo "stderr = \"$ERRFILE\"" >> "$__CURLCFG"
+
+               [ -n "$__DEVICE" ] && \
+                       echo "interface = \"$__DEVICE\"" >> "$__CURLCFG"
+
+               [ "${force_ipversion:-0}" -eq 1 ] && {
+                       [ "${use_ipv6:-0}" -eq 0 ] \
+                               && echo "ipv4" >> "$__CURLCFG" \
+                               || echo "ipv6" >> "$__CURLCFG"
+               }
+
+               if [ "$cacert" = "IGNORE" ]; then
+                       echo "insecure" >> "$__CURLCFG"
+               elif [ -f "$cacert" ]; then
+                       echo "cacert = \"$cacert\"" >> "$__CURLCFG"
+               elif [ -d "$cacert" ]; then
+                       echo "capath = \"$cacert\"" >> "$__CURLCFG"
+               elif [ -n "$cacert" ]; then
+                       write_log 14 "No valid certificate(s) found at '$cacert' for HTTPS communication"
+               fi
+
+               if [ -z "$proxy" ]; then
+                       echo "noproxy = \"*\"" >> "$__CURLCFG"
+               elif [ -z "$CURL_PROXY" ]; then
+                       write_log 13 "cURL: libcurl compiled without Proxy support"
+               else
+                       echo "proxy = \"$proxy\"" >> "$__CURLCFG"
+               fi
+
+               # append call-specific options
+               cat "$__CURLEXTRA" >> "$__CURLCFG"
+
+               write_log 7 "#> $__CURLBIN --config $__CURLCFG"
+
+               "$__CURLBIN" --config "$__CURLCFG"
+               __ERR=$?
+               [ "$__ERR" -eq 0 ] && break
+
+               write_log 3 "cURL Error: '$__ERR'"
+               write_log 7 "$(cat "$ERRFILE")"
+
+               [ "${VERBOSE_MODE:-0}" -gt 1 ] && {
+                       write_log 4 "Transfer failed - Verbose Mode: ${VERBOSE_MODE:-0} - NO retry on error"
+                       break
+               }
+
+               __CNT=$(( __CNT + 1 ))
+               [ "${retry_max_count:-0}" -gt 0 ] && [ "$__CNT" -gt "${retry_max_count:-0}" ] && \
+                       write_log 14 "Transfer failed after ${retry_max_count:-0} retries"
+
+               write_log 4 "Transfer failed - retry $__CNT/${retry_max_count:-0} in ${RETRY_SECONDS:-0} seconds"
+               sleep "${RETRY_SECONDS:-0}" &
+               PID_SLEEP=$!
+               wait $PID_SLEEP
+               PID_SLEEP=0
+       done
+}
+
+# resolve bind_network to device name if set
+if [ -n "$bind_network" ]; then
+       network_get_device __DEVICE "$bind_network" || \
+               write_log 13 "Cannot detect local device using 'network_get_device $bind_network' - Error: '$?'"
+       write_log 7 "Force communication via device '$__DEVICE'"
+fi
+
+# -------------------------------------------------------------------
+# Step 1 — Authenticate and obtain JWT token
+# -------------------------------------------------------------------
+write_log 7 "Authenticating with Blazingfast.io"
+
+: > "$__CURLEXTRA"
+echo "request = POST" >> "$__CURLEXTRA"
+echo "url = \"$__URLBASE/login\"" >> "$__CURLEXTRA"
+# Use data-urlencode so credentials containing reserved characters
+# (&, =, +, spaces, ...) are safely percent-encoded by curl.
+printf 'data-urlencode = "username=%s"\n' "$username" >> "$__CURLEXTRA"
+printf 'data-urlencode = "password=%s"\n' "$password" >> "$__CURLEXTRA"
+blazingfast_transfer
+
+__TOKEN=$(jsonfilter -i "$DATFILE" -e "@.token" 2>/dev/null)
+if [ -z "$__TOKEN" ]; then
+       # Do NOT dump $DATFILE: a partial/successful response may contain a token.
+       write_log 4 "Blazingfast authentication failed — check username/password"
+       blazingfast_cleanup
+       return 1
+fi
+write_log 7 "Authentication successful"
+
+# -------------------------------------------------------------------
+# Step 2 — Fetch all zone records and verify record type
+# The Blazingfast API does not support single-record GET requests.
+# All records for the zone are fetched and filtered by record_id.
+# Ensures IPv4 updates only target A records and IPv6 updates only
+# target AAAA records.
+# -------------------------------------------------------------------
+write_log 7 "Fetching zone records to verify record type is '$__TYPE'"
+
+: > "$__CURLEXTRA"
+echo "request = GET" >> "$__CURLEXTRA"
+echo "url = \"$__URLBASE/service/$__SERVICE_ID/dns/$__ZONE_ID\"" >> "$__CURLEXTRA"
+echo "header = \"Authorization: Bearer $__TOKEN\"" >> "$__CURLEXTRA"
+echo "header = \"Content-Type: application/json\"" >> "$__CURLEXTRA"
+blazingfast_transfer
+
+# record id may be returned as integer or string depending on endpoint
+__RECTYPE=$(jsonfilter -i "$DATFILE" -e "@.records[@.id=$__RECORD_ID].type" 2>/dev/null)
+[ -z "$__RECTYPE" ] && \
+       __RECTYPE=$(jsonfilter -i "$DATFILE" -e "@.records[@.id='$__RECORD_ID'].type" 2>/dev/null)
+
+if [ -z "$__RECTYPE" ]; then
+       write_log 4 "Could not retrieve DNS record type from Blazingfast API"
+       write_log 7 "$(cat "$DATFILE")"
+       blazingfast_cleanup
+       write_log 14 "Check service_id, zone_id and record_id"
+fi
+
+if [ "$__RECTYPE" != "$__TYPE" ]; then
+       write_log 4 "Record type mismatch: expected '$__TYPE' for IPv$__IPVERSION but record '$__RECORD_ID' is '$__RECTYPE'"
+       blazingfast_cleanup
+       write_log 14 "Use the correct Blazingfast record_id for the IPv$__IPVERSION DNS record"
+fi
+
+write_log 7 "DNS record type confirmed: '$__RECTYPE'"
+
+# -------------------------------------------------------------------
+# Step 3 — GET_REGISTERED_IP mode
+# Returns the IP currently stored in the DNS record,
+# used by ddns-scripts to compare before deciding to update.
+#
+# The zone records were already fetched during type verification,
+# so reuse the existing API response from $DATFILE.
+# -------------------------------------------------------------------
+if [ -n "$GET_REGISTERED_IP" ]; then
+       __DATA=$(jsonfilter -i "$DATFILE" -e "@.records[@.id=$__RECORD_ID].content" 2>/dev/null)
+       [ -z "$__DATA" ] && \
+               __DATA=$(jsonfilter -i "$DATFILE" -e "@.records[@.id='$__RECORD_ID'].content" 2>/dev/null)
+       if [ -n "$__DATA" ]; then
+               write_log 7 "Registered IP '$__DATA' detected via Blazingfast API"
+               REGISTERED_IP="$__DATA"
+               blazingfast_cleanup
+               return 0
+       else
+               write_log 4 "Could not extract IP from Blazingfast API response"
+               write_log 7 "$(cat "$DATFILE")"
+               blazingfast_cleanup
+               return 127
+       fi
+fi
+
+# -------------------------------------------------------------------
+# Step 4 — Update the DNS record
+# JSON payload is built with jshn.sh so values are properly escaped
+# and written inline to the curl config. This avoids any quoting or
+# escaping issues when domain or IP contain special characters.
+# -------------------------------------------------------------------
+json_init
+json_add_string "name"     "$domain"
+json_add_int    "ttl"      300
+json_add_int    "priority" 0
+json_add_string "type"     "$__TYPE"
+json_add_string "content"  "$__IP"
+__PAYLOAD=$(json_dump)
+
+: > "$__CURLEXTRA"
+echo "request = PUT" >> "$__CURLEXTRA"
+echo "url = \"$__URLBASE/service/$__SERVICE_ID/dns/$__ZONE_ID/records/$__RECORD_ID\"" >> "$__CURLEXTRA"
+echo "header = \"Authorization: Bearer $__TOKEN\"" >> "$__CURLEXTRA"
+echo "header = \"Content-Type: application/json\"" >> "$__CURLEXTRA"
+printf 'data = "%s"\n' "$__PAYLOAD" >> "$__CURLEXTRA"
+blazingfast_transfer
+
+# verify success from API response
+__DATA=$(jsonfilter -i "$DATFILE" -e "@.info[0]" 2>/dev/null)
+if echo "$__DATA" | grep -q "dnsrecordupdated"; then
+       write_log 7 "Record updated: $domain $__TYPE -> $__IP"
+       blazingfast_cleanup
+       return 0
+fi
+
+write_log 4 "Blazingfast API reported an error:"
+write_log 7 "$(cat "$DATFILE")"
+blazingfast_cleanup
+return 1
diff --git a/net/ddns-scripts/files/usr/share/ddns/default/blazingfast.io.json b/net/ddns-scripts/files/usr/share/ddns/default/blazingfast.io.json
new file mode 100644 (file)
index 0000000..16946cb
--- /dev/null
@@ -0,0 +1,9 @@
+{
+    "name": "blazingfast.io",
+    "ipv4": {
+        "url": "update_blazingfast_io.sh"
+    },
+    "ipv6": {
+        "url": "update_blazingfast_io.sh"
+    }
+}
git clone https://git.99rst.org/PROJECT