PKG_NAME:=ddns-scripts
PKG_VERSION:=2.8.2
-PKG_RELEASE:=29
+PKG_RELEASE:=30
PKG_LICENSE:=GPL-2.0
endef
+define Package/ddns-scripts-gcp
+ $(call Package/ddns-scripts/Default)
+ TITLE:=Extension for Google Cloud DNS API v1
+ DEPENDS:=ddns-scripts +curl +openssl-util
+endef
+
+define Package/ddns-scripts-gcp/description
+ Dynamic DNS Client scripts extension for Google Cloud DNS API v1 (requires curl)
+endef
+
+
define Package/ddns-scripts-freedns
$(call Package/ddns-scripts/Default)
TITLE:=Extension for freedns.42.pl
# Remove special services
rm $(1)/usr/share/ddns/default/cloudflare.com-v4.json
+ rm $(1)/usr/share/ddns/default/cloud.google.com-v1.json
rm $(1)/usr/share/ddns/default/freedns.42.pl.json
rm $(1)/usr/share/ddns/default/godaddy.com-v1.json
rm $(1)/usr/share/ddns/default/digitalocean.com-v2.json
endef
+define Package/ddns-scripts-gcp/install
+ $(INSTALL_DIR) $(1)/usr/lib/ddns
+ $(INSTALL_BIN) ./files/usr/lib/ddns/update_gcp_v1.sh \
+ $(1)/usr/lib/ddns
+
+ $(INSTALL_DIR) $(1)/usr/share/ddns/default
+ $(INSTALL_DATA) ./files/usr/share/ddns/default/cloud.google.com-v1.json \
+ $(1)/usr/share/ddns/default/
+endef
+
+define Package/ddns-scripts-gcp/prerm
+#!/bin/sh
+if [ -z "$${IPKG_INSTROOT}" ]; then
+ /etc/init.d/ddns stop
+fi
+exit 0
+endef
+
+
define Package/ddns-scripts-freedns/install
$(INSTALL_DIR) $(1)/usr/lib/ddns
$(INSTALL_BIN) ./files/usr/lib/ddns/update_freedns_42_pl.sh \
$(eval $(call BuildPackage,ddns-scripts))
$(eval $(call BuildPackage,ddns-scripts-services))
$(eval $(call BuildPackage,ddns-scripts-cloudflare))
+$(eval $(call BuildPackage,ddns-scripts-gcp))
$(eval $(call BuildPackage,ddns-scripts-freedns))
$(eval $(call BuildPackage,ddns-scripts-godaddy))
$(eval $(call BuildPackage,ddns-scripts-digitalocean))
--- /dev/null
+#!/bin/sh
+#
+#.Distributed under the terms of the GNU General Public License (GPL) version 2.0
+#.2022 Chris Barrick <chrisbarrick@google.com>
+#
+# This script sends DDNS updates using the Google Cloud DNS REST API.
+# See: https://cloud.google.com/dns/docs/reference/v1
+#
+# This script uses a GCP service account. The user is responsible for creating
+# the service account, ensuring it has permission to update DNS records, and
+# for generating a service account key to be used by this script. The records
+# to be updated must already exist.
+#
+# Arguments:
+#
+# - $username: The service account name.
+# Example: ddns-service-account@my-dns-project.iam.gserviceaccount.com
+#
+# - $password: The service account key. You can paste the key directly into the
+# "password" field or upload the key file to the router and set the field
+# equal to the file path. This script supports JSON keys or the raw private
+# key as a PEM file. P12 keys are not supported. File names must end with
+# `*.json` or `*.pem`.
+#
+# - $domain: The domain to update.
+#
+# - $param_enc: The additional required arguments, as form-urlencoded data,
+# i.e. `key1=value1&key2=value2&...`. The required arguments are:
+# - project: The name of the GCP project that owns the DNS records.
+# - zone: The DNS zone in the GCP API.
+# - Example: `project=my-dns-project&zone=my-dns-zone`
+#
+# - $param_opt: Optional TTL for the records, in seconds. Defaults to 3600 (1h).
+#
+# Dependencies:
+# - ddns-scripts (for the base functionality)
+# - openssl-util (for the authentication flow)
+# - curl (for the GCP REST API)
+
+. /usr/share/libubox/jshn.sh
+
+
+# Authentication
+# ---------------------------------------------------------------------------
+# The authentication flow works like this:
+#
+# 1. Construct a JWT claim for access to the DNS readwrite scope.
+# 2. Sign the JWT with the service accout key, proving we have access.
+# 3. Exchange the JWT for an access token, valid for 5m.
+# 4. Use the access token for API calls.
+#
+# See https://developers.google.com/identity/protocols/oauth2/service-account
+
+# A URL-safe variant of base64 encoding, used by JWTs.
+base64_urlencode() {
+ openssl base64 | tr '/+' '_-' | tr -d '=\n'
+}
+
+# Prints the service account private key in PEM format.
+get_service_account_key() {
+ # The "password" field provides us with the service account key.
+ # We allow the user to provide it to us in a few different formats.
+ #
+ # 1. If $password is a string ending in `*.json`, it is a file path,
+ # pointing to a JSON service account key as downloaded from GCP.
+ #
+ # 2. If $password is a string ending with `*.pem`, it is a PEM private
+ # key, extracted from the JSON service account key.
+ #
+ # 3. If $password starts with `{`, then the JSON service account key
+ # was pasted directly into the password field.
+ #
+ # 4. If $password starts with `---`, then the PEM private key was pasted
+ # directly into the password field.
+ #
+ # We do not support P12 service account keys.
+ case "${password}" in
+ (*".json")
+ jsonfilter -i "${password}" -e @.private_key
+ ;;
+ (*".pem")
+ cat "${password}"
+ ;;
+ ("{"*)
+ jsonfilter -s "${password}" -e @.private_key
+ ;;
+ ("---"*)
+ printf "%s" "${password}"
+ ;;
+ (*)
+ write_log 14 "Could not parse the service account key."
+ ;;
+ esac
+}
+
+# Sign stdin using the service account key. Prints the signature.
+# The input is the JWT header-payload. Used to construct a signed JWT.
+sign() {
+ # Dump the private key to a tmp file so openssl can get to it.
+ local tmp_keyfile="$(mktemp -t gcp_dns_sak.pem.XXXXXX)"
+ chmod 600 ${tmp_keyfile}
+ get_service_account_key > ${tmp_keyfile}
+ openssl dgst -binary -sha256 -sign ${tmp_keyfile}
+ rm ${tmp_keyfile}
+}
+
+# Print the JWT header in JSON format.
+# Currently, Google only supports RS256.
+jwt_header() {
+ json_init
+ json_add_string "alg" "RS256"
+ json_add_string "typ" "JWT"
+ json_dump
+}
+
+# Prints the JWT claim-set in JSON format.
+# The claim is for 5m of readwrite access to the Cloud DNS API.
+jwt_claim_set() {
+ local iat=$(date -u +%s) # Current UNIX time, UTC.
+ local exp=$(( iat + 300 )) # Expiration is 5m in the future.
+
+ json_init
+ json_add_string "iss" "${username}"
+ json_add_string "scope" "https://www.googleapis.com/auth/ndev.clouddns.readwrite"
+ json_add_string "aud" "https://oauth2.googleapis.com/token"
+ json_add_string "iat" "${iat}"
+ json_add_string "exp" "${exp}"
+ json_dump
+}
+
+# Generate a JWT signed by the service account key, which can be exchanged for
+# a Google Cloud access token, authorized for Cloud DNS.
+get_jwt() {
+ local header=$(jwt_header | base64_urlencode)
+ local payload=$(jwt_claim_set | base64_urlencode)
+ local header_payload="${header}.${payload}"
+ local signature=$(printf "%s" ${header_payload} | sign | base64_urlencode)
+ echo "${header_payload}.${signature}"
+}
+
+# Request an access token for the Google Cloud service account.
+get_access_token_raw() {
+ local grant_type="urn:ietf:params:oauth:grant-type:jwt-bearer"
+ local assertion=$(get_jwt)
+
+ ${CURL} -v https://oauth2.googleapis.com/token \
+ --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \
+ --data-urlencode "assertion=${assertion}" \
+ | jsonfilter -e @.access_token
+}
+
+# Get the access token, stripping the trailing dots.
+get_access_token() {
+ # Since tokens may contain internal dots, we only trim the suffix if it
+ # starts with at least 8 dots. (The access token has *many* trailing dots.)
+ local access_token="$(get_access_token_raw)"
+ echo "${access_token%%........*}"
+}
+
+
+# Google Cloud DNS API
+# ---------------------------------------------------------------------------
+# Cloud DNS offers a straight forward RESTful API.
+#
+# - The main class is a ResourceRecordSet. It's a collection of DNS records
+# that share the same domain, type, TTL, etc. Within a record set, the only
+# difference between the records are their values.
+#
+# - The record sets live under a ManagedZone, which in turn lives under a
+# Project. All we need to know about these are their names.
+#
+# - This implementation only makes PATCH requests to update existing record
+# sets. The user must have already created at least one A or AAAA record for
+# the domain they are updating. It's fine to start with a dummy, like 0.0.0.0.
+#
+# - The API requires SSL, and this implementation uses curl.
+
+# Prints a ResourceRecordSet in JSON format.
+format_record_set() {
+ local domain="$1"
+ local record_type="$2"
+ local ttl="$3"
+ shift 3 # The remaining arguments are the IP addresses for this record set.
+
+ json_init
+ json_add_string "kind" "dns#resourceRecordSet"
+ json_add_string "name" "${domain}." # trailing dot on the domain
+ json_add_string "type" "${record_type}"
+ json_add_string "ttl" "${ttl}"
+ json_add_array "rrdatas"
+ for value in $@; do
+ json_add_string "" "${value}"
+ done
+ json_close_array
+ json_dump
+}
+
+# Makes an HTTP PATCH request to the Cloud DNS API.
+patch_record_set() {
+ local access_token="$1"
+ local project="$2"
+ local zone="$3"
+ local domain="$4"
+ local record_type="$5"
+ local ttl="$6"
+ shift 6 # The remaining arguments are the IP addresses for this record set.
+
+ # Note the trailing dot after the domain name. It's fully qualified.
+ local url="https://dns.googleapis.com/dns/v1/projects/${project}/managedZones/${zone}/rrsets/${domain}./${record_type}"
+ local record_set=$(format_record_set ${domain} ${record_type} ${ttl} $@)
+
+ ${CURL} -v ${url} \
+ -X PATCH \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer ${access_token}" \
+ -d "${record_set}"
+}
+
+
+# Main entrypoint
+# ---------------------------------------------------------------------------
+
+# Parse the $param_enc into project and zone variables.
+# The arguments are the names for those variables.
+parse_project_zone() {
+ local project_var=$1
+ local zone_var=$2
+
+ IFS='&'
+ for entry in $param_enc
+ do
+ case "${entry}" in
+ ('project='*)
+ local project_val=$(echo "${entry}" | cut -d'=' -f2)
+ eval "${project_var}=${project_val}"
+ ;;
+ ('zone='*)
+ local zone_val=$(echo "${entry}" | cut -d'=' -f2)
+ eval "${zone_var}=${zone_val}"
+ ;;
+ esac
+ done
+ unset IFS
+}
+
+main() {
+ local access_token project zone ttl record_type
+
+ # Dependency checking
+ [ -z "${CURL_SSL}" ] && write_log 14 "Google Cloud DNS requires cURL with SSL support"
+ [ -z "$(openssl version)" ] && write_log 14 "Google Cloud DNS update requires openssl-utils"
+
+ # Argument parsing
+ [ -z ${param_opt} ] && ttl=3600 || ttl="${param_opt}"
+ [ $use_ipv6 -ne 0 ] && record_type="AAAA" || record_type="A"
+ parse_project_zone project zone
+
+ # Sanity checks
+ [ -z "${username}" ] && write_log 14 "Config is missing 'username' (service account name)"
+ [ -z "${password}" ] && write_log 14 "Config is missing 'password' (service account key)"
+ [ -z "${domain}" ] && write_log 14 "Config is missing 'domain'"
+ [ -z "${project}" ] && write_log 14 "Could not parse project name from 'param_enc'"
+ [ -z "${zone}" ] && write_log 14 "Could not parse zone name from 'param_enc'"
+ [ -z "${ttl}" ] && write_log 14 "Could not parse TTL from 'param_opt'"
+ [ -z "${record_type}" ] && write_log 14 "Could not determine the record type"
+
+ # Push the record!
+ access_token="$(get_access_token)"
+ patch_record_set "${access_token}" "${project}" "${zone}" "${domain}" "${record_type}" "${ttl}" "${__IP}"
+}
+
+main $@