#!/bin/bash # -------------------------------------------- # DietPi-DDNS # Setup background jobs to regularly update dynamic IPs against DDNS providers # -------------------------------------------- # Created by MichaIng / micha@dietpi.com / dietpi.com # Content by ravenclaw900 / https://github.com/ravenclaw900 # License: https://github.com/MichaIng/DietPi/blob/master/LICENSE # -------------------------------------------- # Location: /boot/dietpi/dietpi-ddns { readonly USAGE=' Usage: dietpi-ddns [[...] []] Available commands: Interactive menu to setup dynamic DNS updates apply Apply or update DDNS updates for , using for setup details remove Remove any DDNS updates from this system Available options: -d Comma-separated list of domains that shall point to this system -u Username or identifier, depending on provider In combination with a custom provider, this is used for HTTP authentication. -p Password or token, depending on provider In combination with a custom provider, this is used for HTTP authentication. -t Duration between DDNS updates in minutes (optional, defaults to 10) -4and6 Update IPv4 and IPv6 addresses for your DDNS (optional, the default) -4 Update only the IPv4 address for your DDNS (optional) -6 Update only the IPv6 address for your DDNS (optional) Available providers: Full URL to update against a custom DDNS provider Use the "-u" and "-p" options if HTTP authentication is required. DuckDNS Read more: https://www.duckdns.org/about.jsp Use the "-d" and "-p" options to set domains and account token. No-IP Read more: https://www.noip.com/about Use the "-d", "-u" and "-p" options to set domains, username and password. Dynu Read more: https://www.dynu.com/DynamicDNS Use the "-d" and "-p" options to set domains and account password. FreeDNS Read more: https://freedns.afraid.org/ Use the "-p" option to set the account token. OVH Read more: https://docs.ovh.com/gb/en/domains/hosting_dynhost/ Use the "-d", "-u" and "-p" options to set domains, username and password. YDNS Read More https://ydns.io Use the "-d", "-u" and "-p" options to set the domain, username and password. ' # Load DietPi-Globals . /boot/dietpi/func/dietpi-globals readonly G_PROGRAM_NAME='DietPi-DDNS' G_CHECK_ROOT_USER "$@" G_CHECK_ROOTFS_RW G_INIT # Variables COMMAND= PROVIDER= DOMAINS= USERNAME= PASSWORD= TIMESPAN= IPFAMILY= # -------------------------------------------- # Functions # -------------------------------------------- # Process input: Requires script input to be passed Input() { while (( $# )) do case "$1" in '-d') shift; DOMAINS=$1;; '-u') shift; USERNAME=$1;; '-p') shift; PASSWORD=$1;; '-t') shift; TIMESPAN=$1;; '-'[46]|'-4and6') IPFAMILY=$1;; 'apply') COMMAND=$1; shift; PROVIDER=$1;; 'remove') COMMAND=$1;; *) G_DIETPI-NOTIFY 1 "Invalid input ($1). Aborting ...$USAGE"; exit 1;; esac shift done } # Read current settings from existing cURL command, if not set via input already Read() { [[ -f '/var/lib/dietpi/dietpi-ddns/update.sh' ]] || return local command=$(tail -1 /var/lib/dietpi/dietpi-ddns/update.sh) # DuckDNS if [[ $command == *'duckdns.org'* ]] then [[ $PROVIDER ]] || PROVIDER='DuckDNS' [[ $DOMAINS ]] || DOMAINS=${command#*domains=} DOMAINS=${DOMAINS%&token*} [[ $PASSWORD ]] || PASSWORD=${command#*token=} PASSWORD=${PASSWORD%\'*} # No-IP elif [[ $command == *'noip.com'* ]] then [[ $PROVIDER ]] || PROVIDER='No-IP' [[ $DOMAINS ]] || DOMAINS=${command#*hostname=} DOMAINS=${DOMAINS%\'*} [[ $USERNAME ]] || USERNAME=${command#*\'} USERNAME=${USERNAME%%:*} [[ $PASSWORD ]] || PASSWORD=${command#*:} PASSWORD=${PASSWORD%%\'*} # Dynu elif [[ $command == *'dynu.com'* ]] then [[ $PROVIDER ]] || PROVIDER='Dynu' [[ $DOMAINS ]] || DOMAINS=${command#*hostname=} DOMAINS=${DOMAINS%&password*} [[ $PASSWORD ]] || PASSWORD=${command#*password=} PASSWORD=${PASSWORD%\'*} # FreeDNS elif [[ $command == *'sync.afraid.org'* ]] then [[ $PROVIDER ]] || PROVIDER='FreeDNS' [[ $PASSWORD ]] || PASSWORD=${command%/\'*} PASSWORD=${PASSWORD##*/} # OVH elif [[ $command == *'www.ovh.com'* ]] then [[ $PROVIDER ]] || PROVIDER='OVH' [[ $DOMAINS ]] || DOMAINS=${command#*hostname=} DOMAINS=${DOMAINS%\'*} [[ $USERNAME ]] || USERNAME=${command#*\'} USERNAME=${USERNAME%%:*} [[ $PASSWORD ]] || PASSWORD=${command#*:} PASSWORD=${PASSWORD%%\'*} # YDNS elif [[ $command == *'www.ydns.io'* ]] then [[ $PROVIDER ]] || PROVIDER='YDNS' [[ $DOMAINS ]] || DOMAINS=${command#*host=} DOMAINS=${DOMAINS%\'*} [[ $USERNAME ]] || USERNAME=${command#*\'} USERNAME=${USERNAME%%:*} [[ $PASSWORD ]] || PASSWORD=${command#*:} PASSWORD=${PASSWORD%%\'*} # Custom else [[ $PROVIDER ]] || PROVIDER=${command%\'*} PROVIDER=${PROVIDER##*\'} # HTTP authentication if [[ $command == *' -u '* ]] then [[ $USERNAME ]] || USERNAME=${command#*\'} USERNAME=${USERNAME%%:*} [[ $PASSWORD ]] || PASSWORD=${command#*:} PASSWORD=${PASSWORD%%\'*} fi fi # IP family if [[ ! $IPFAMILY ]] then if grep -q 'curl -4 ' /var/lib/dietpi/dietpi-ddns/update.sh then if grep -q 'curl -6 ' /var/lib/dietpi/dietpi-ddns/update.sh then IPFAMILY='-4and6' else IPFAMILY='-4' fi elif grep -q 'curl -6 ' /var/lib/dietpi/dietpi-ddns/update.sh then IPFAMILY='-6' else IPFAMILY='-4and6' # Pre-v9.9 fi fi # Time span [[ ! $TIMESPAN && -f '/var/spool/cron/crontabs/dietpi-ddns' ]] || return TIMESPAN=$(tail -1 /var/spool/cron/crontabs/dietpi-ddns) TIMESPAN=${TIMESPAN#\*/} TIMESPAN=${TIMESPAN%% *} } # Apply chosen settings and create a Cron job for dynamic DNS updates Apply() { # Generate URL and set HTTP authentication flag based on provider local url http_auth=1 [[ $IPFAMILY ]] || IPFAMILY='-4and6' # - DuckDNS if [[ $PROVIDER == 'DuckDNS' ]] then url="https://www.duckdns.org/update?domains=$DOMAINS&token=$PASSWORD" http_auth= # - No-IP elif [[ $PROVIDER == 'No-IP' ]] then url="https://dynupdate.noip.com/nic/update?hostname=$DOMAINS" # - Dynu elif [[ $PROVIDER == 'Dynu' ]] then url="https://api.dynu.com/nic/update?hostname=$DOMAINS&password=$PASSWORD" http_auth= # - FreeDNS elif [[ $PROVIDER == 'FreeDNS' ]] then url="https://freedns.afraid.org/dynamic/update.php?$PASSWORD" http_auth= # - OVH elif [[ $PROVIDER == 'OVH' ]] then url="https://www.ovh.com/nic/update?system=dyndns&hostname=$DOMAINS" # - YDNS elif [[ $PROVIDER == 'YDNS' ]] then url="https://ydns.io/api/v1/update/?host=$DOMAINS" # - Custom else url=$PROVIDER # Assure that no HTTP authentication is attempted if username and password are not set [[ $USERNAME$PASSWORD ]] || http_auth= fi # Create DietPi-DDNS group G_DIETPI-NOTIFY 2 'Preparing unprivileged DietPi-DDNS UNIX group ...' if getent group dietpi-ddns > /dev/null then G_EXEC groupmod -p '!' dietpi-ddns else G_EXEC groupadd -r -p '!' dietpi-ddns fi # Create DietPi-DDNS user G_DIETPI-NOTIFY 2 'Preparing unprivileged DietPi-DDNS UNIX user ...' if getent passwd dietpi-ddns > /dev/null then G_EXEC usermod -g 'dietpi-ddns' -G '' -d '/nonexistent' -s '/usr/sbin/nologin' -p '!' dietpi-ddns else G_EXEC useradd -r -g 'dietpi-ddns' -G '' -d '/nonexistent' -s '/usr/sbin/nologin' -p '!' dietpi-ddns fi # Test DDNS update G_DIETPI-NOTIFY 2 'Testing DDNS update ...' local result # shellcheck disable=SC2086 if ! result=$(curl "${IPFAMILY%and6}" -sSfL ${http_auth:+ -u "$USERNAME:$PASSWORD"} "$url" 2>&1) || [[ $PROVIDER == 'DuckDNS' && $result == 'KO' ]] || [[ $PROVIDER == 'YDNS' && $result != 'good'* && $result != 'nochg'* ]] || [[ $PROVIDER == 'Dynu' && $result != 'good'* && $result != 'nochg'* ]] then STATUS="DDNS update test failed, please check your input${result:+:\n$result}" G_DIETPI-NOTIFY 1 "$STATUS" return 1 else STATUS="DDNS update test succeeded${result:+:\n$result}" G_DIETPI-NOTIFY 0 "$STATUS" # Test IPv6 as well if both are enabled if [[ $IPFAMILY == '-4and6' ]] then G_DIETPI-NOTIFY 2 'Testing IPv6 DDNS update ...' local status6 if ! result=$(curl "${IPFAMILY/4and}" -sSfL ${http_auth:+ -u "$USERNAME:$PASSWORD"} "$url" 2>&1) || [[ $PROVIDER == 'DuckDNS' && $result == 'KO' ]] || [[ $PROVIDER == 'YDNS' && $result != 'good'* && $result != 'nochg'* ]] || [[ $PROVIDER == 'Dynu' && $result != 'good'* && $result != 'nochg'* ]] then status6="IPv6 DDNS update test failed. Your server, network or DDNS provider might not support IPv6${result:+:\n$result}" G_DIETPI-NOTIFY 1 "$status6" else status6="IPv6 DDNS update test succeeded${result:+:\n$result}" G_DIETPI-NOTIFY 0 "$status6" fi STATUS+="\n\n$status6" fi fi # Check and in case remove obsolete No-IP client if command -v noip2 > /dev/null then G_DIETPI-NOTIFY 2 'Removing obsolete No-IP client from your system ...' if [[ -f '/etc/systemd/system/noip2.service' ]] then G_EXEC systemctl disable --now noip2 G_EXEC rm /etc/systemd/system/noip2.service fi if [[ -f '/etc/init.d/noip2.sh' ]] then G_EXEC systemctl unmask noip2 G_EXEC systemctl disable --now noip2 G_EXEC rm /etc/init.d/noip2.sh fi [[ -d '/etc/systemd/system/noip2.service.d' ]] && G_EXEC rm -R /etc/systemd/system/noip2.service.d [[ -f '/usr/local/bin/noip2' ]] && G_EXEC rm /usr/local/bin/noip2 [[ -f '/usr/local/etc/no-ip2.conf' ]] && G_EXEC rm /usr/local/etc/no-ip2.conf fi # Create update script G_DIETPI-NOTIFY 2 'Creating DietPi-DDNS update script...' G_EXEC mkdir -p /var/lib/dietpi/dietpi-ddns echo '#!/bin/dash' > /var/lib/dietpi/dietpi-ddns/update.sh G_EXEC chmod 0500 /var/lib/dietpi/dietpi-ddns/update.sh G_EXEC chown dietpi-ddns:dietpi-ddns /var/lib/dietpi/dietpi-ddns/update.sh # Shellcheck false positive: https://github.com/koalaman/shellcheck/issues/2168 # shellcheck disable=SC2016 echo "{ curl ${IPFAMILY%and6} -sSfL${http_auth:+ -u '$USERNAME:$PASSWORD'} '$url' | logger -t dietpi-ddns -p 6; } 2>&1 | logger -t dietpi-ddns -p 3" >> /var/lib/dietpi/dietpi-ddns/update.sh # shellcheck disable=SC2016 [[ $IPFAMILY == '-4and6' ]] && echo "{ curl ${IPFAMILY/4and} -sSfL${http_auth:+ -u '$USERNAME:$PASSWORD'} '$url' | logger -t dietpi-ddns -p 6; } 2>&1 | logger -t dietpi-ddns -p 3" >> /var/lib/dietpi/dietpi-ddns/update.sh # Apply Cron job G_DIETPI-NOTIFY 2 'Applying DietPi-DDNS Cron job ...' crontab -u dietpi-ddns - <<< "*/${TIMESPAN:-10} * * * * /var/lib/dietpi/dietpi-ddns/update.sh" } # Remove any DDNS updates from this system Remove() { # Remove Cron job [[ -f '/var/spool/cron/crontabs/dietpi-ddns' ]] && G_EXEC_DESC='Removing DietPi-DDNS Cron job' G_EXEC crontab -u dietpi-ddns -r [[ -d '/var/lib/dietpi/dietpi-ddns' ]] && G_EXEC_DESC='Removing DietPi-DDNS update script' G_EXEC rm -R /var/lib/dietpi/dietpi-ddns # Remove DietPi-DDNS user and group getent passwd dietpi-ddns > /dev/null && G_EXEC_DESC='Removing DietPi-DDNS UNIX user' G_EXEC userdel dietpi-ddns getent group dietpi-ddns > /dev/null && G_EXEC_DESC='Removing DietPi-DDNS UNIX group' G_EXEC groupdel dietpi-ddns # Unset variables unset -v USERNAME PASSWORD TIMESPAN PROVIDER IPFAMILY } # -------------------------------------------- # Menus # -------------------------------------------- Menu_Provider() { # No or known provider selected local custom_text='Use a custom provider and enter its URL manually' G_WHIP_DEFAULT_ITEM=${PROVIDER:-DuckDNS} # Custom provider selected if [[ $PROVIDER && $PROVIDER != 'DuckDNS' && $PROVIDER != 'No-IP' && $PROVIDER != 'Dynu' && $PROVIDER != 'FreeDNS' && $PROVIDER != 'OVH' && $PROVIDER != 'YDNS' ]] then G_WHIP_DEFAULT_ITEM='Custom' custom_text="[$PROVIDER]" fi G_WHIP_MENU_ARRAY=( 'DuckDNS' ': Read more: https://www.duckdns.org/about.jsp' 'No-IP' ': Read more: https://www.noip.com/about' 'Dynu' ': Read more: https://www.dynu.com/DynamicDNS' 'FreeDNS' ': Read more: https://freedns.afraid.org/' 'OVH' ': Read more: https://docs.ovh.com/gb/en/domains/hosting_dynhost/' 'YDNS' ': Read more: https://ydns.io' 'Custom' ": $custom_text" ) G_WHIP_MENU 'Please select your DDNS provider:' || return 1 case "$G_WHIP_RETURNED_VALUE" in 'Custom') G_WHIP_DEFAULT_ITEM=$PROVIDER G_WHIP_INPUTBOX 'Please enter the full URL used to update your dynamic IP against your custom DDNS provider:' && PROVIDER=$G_WHIP_RETURNED_VALUE || return 1 ;; *) PROVIDER=$G_WHIP_RETURNED_VALUE;; esac # Update credentials names [[ $PROVIDER == 'DuckDNS' || $PROVIDER == 'FreeDNS' ]] && password='Token' || password='Password' } Menu_Domains() { # Skip with FreeDNS/Custom providers [[ $PROVIDER == 'DuckDNS' || $PROVIDER == 'No-IP' || $PROVIDER == 'Dynu' || $PROVIDER == 'OVH' || $PROVIDER == 'YDNS' ]] || return 0 G_WHIP_DEFAULT_ITEM=$DOMAINS # Hint that YDNS only allows one domain if [[ $PROVIDER == 'YDNS' ]] then G_WHIP_INPUTBOX 'Please enter the domain that shall point to this system:' || return 1 else G_WHIP_INPUTBOX 'Please enter a comma-separated list of domains that shall point to this system:' || return 1 fi DOMAINS=$G_WHIP_RETURNED_VALUE } Menu_Username() { # Skip with these providers [[ $PROVIDER == 'DuckDNS' || $PROVIDER == 'Dynu' || $PROVIDER == 'FreeDNS' ]] && return 0 # Add note for custom provider local text="Please enter the $username to update your dynamic IP against your DDNS provider.\n - The colon character : is currently not supported!" [[ $PROVIDER == 'No-IP' || $PROVIDER == 'OVH' || $PROVIDER == 'YDNS' ]] || text+='\nThis is used for HTTP authentication. If no HTTP authentication is required, type in a \"0\" to skip the username.' G_WHIP_DEFAULT_ITEM=$USERNAME G_WHIP_INPUTBOX "$text" || return 1 USERNAME=$G_WHIP_RETURNED_VALUE # Unset with custom provider when "0" is given [[ $PROVIDER == 'No-IP' || $PROVIDER == 'OVH' || $PROVIDER == 'YDNS' || $USERNAME != 0 ]] || unset -v USERNAME } Menu_Password() { # Add note for custom provider local text="Please enter the $password to update your dynamic IP against your DDNS provider.\n - The single quote character ' is currently not supported!" [[ $PROVIDER == 'DuckDNS' || $PROVIDER == 'No-IP' || $PROVIDER == 'Dynu' || $PROVIDER == 'FreeDNS' || $PROVIDER == 'OVH' || $PROVIDER == 'YDNS' ]] || text+='\n\nThis is used for HTTP authentication. If no HTTP authentication is required, type in a \"0\" to skip the password.' G_WHIP_PASSWORD "$text" || return 1 PASSWORD=$result unset -v result # Unset with custom provider when "0" is given [[ $PROVIDER == 'DuckDNS' || $PROVIDER == 'No-IP' || $PROVIDER == 'Dynu' || $PROVIDER == 'FreeDNS' || $PROVIDER == 'OVH' || $PROVIDER == 'YDNS' || $PASSWORD != 0 ]] || unset -v PASSWORD } Menu_IPfamily() { G_WHIP_MENU_ARRAY=( 'IPv4and6' ': Update IPv4 and IPv6 addresses for your DDNS' 'IPv4' ': Update only the IPv4 address for your DDNS' 'IPv6' ': Update only the IPv6 address for your DDNS' ) G_WHIP_DEFAULT_ITEM=${IPFAMILY/-/IPv} G_WHIP_MENU 'Please select which IP family address to associate with your DDNS domain. \nUsually one would want to update both, the IPv4 and IPv6 address, if your server has one. \nIf you do not have a public IPv6 address, or want to keep the amount of DDNS requests low, update the IPv4 address only. \nIf you have a public IPv6 address, want to support only clients with IPv6 enabled, and connected via networks which support IPv6, then you could update the IPv6 address only. Choosing this could lower requests and attacks from random bots, which almost always try to connect via IPv4. But it can prevent your own/intended clients from being able to connect.' || return 1 IPFAMILY=${G_WHIP_RETURNED_VALUE/IPv/-} } Menu_Timespan() { G_WHIP_DEFAULT_ITEM=$TIMESPAN G_WHIP_INPUTBOX 'Please enter the duration between DDNS updates in minutes:' || return 1 TIMESPAN=$G_WHIP_RETURNED_VALUE } Menu_Main() { # Adjust credentials names local username='Username' password='Password' [[ $PROVIDER == 'DuckDNS' || $PROVIDER == 'FreeDNS' ]] && password='Token' # Loop through sub menus directly if no provider has been chosen yet, else show main menu [[ $PROVIDER ]] || { G_WHIP_BUTTON_CANCEL_TEXT='Exit' Menu_Provider || exit 0 && Menu_Domains && Menu_Username && Menu_Password && G_WHIP_DEFAULT_ITEM_NEXT='Apply'; } G_WHIP_MENU_ARRAY=('Provider' ": [$PROVIDER]") [[ $PROVIDER == 'DuckDNS' || $PROVIDER == 'No-IP' || $PROVIDER == 'Dynu' || $PROVIDER == 'OVH' || $PROVIDER == 'YDNS' ]] && G_WHIP_MENU_ARRAY+=('Domains' ": [$DOMAINS]") [[ $PROVIDER == 'DuckDNS' || $PROVIDER == 'Dynu' || $PROVIDER == 'FreeDNS' || $PROVIDER == 'YDNS' ]] || G_WHIP_MENU_ARRAY+=("$username" ": [$USERNAME]") G_WHIP_MENU_ARRAY+=( "$password" ": [${PASSWORD//?/*}]" 'IP family' ": [${IPFAMILY/-/IPv}]" 'Timespan' ": [${TIMESPAN:-10} minutes]" '' '●─ Apply ' 'Apply' ': Create or update Cron job with above settings' 'Remove' ': Remove any DDNS updates from this system' ) G_WHIP_BUTTON_CANCEL_TEXT='Exit' G_WHIP_DEFAULT_ITEM=$G_WHIP_DEFAULT_ITEM_NEXT G_WHIP_MENU "$STATUS" || exit 1 case "$G_WHIP_RETURNED_VALUE" in 'Provider') Menu_Provider && Menu_Domains && Menu_Username && Menu_Password && G_WHIP_DEFAULT_ITEM_NEXT='Apply';; 'Domains') Menu_Domains && Menu_Username && Menu_Password && G_WHIP_DEFAULT_ITEM_NEXT='Apply';; "$username") Menu_Username && Menu_Password && G_WHIP_DEFAULT_ITEM_NEXT='Apply';; "$password") Menu_Password && G_WHIP_DEFAULT_ITEM_NEXT='Apply';; 'IP family') Menu_IPfamily;; 'Timespan') Menu_Timespan;; 'Apply') Apply;; 'Remove') Remove;; *) :;; esac return 0 } # -------------------------------------------- # Main # -------------------------------------------- # CLI if (( $# )) then Input "$@" if [[ $COMMAND == 'apply' ]] then # Complement settings from existing Cron job Read Apply || exit 1 elif [[ $COMMAND == 'remove' ]] then Remove || exit 1 else G_DIETPI-NOTIFY 1 "Input found but no command. Aborting ...$USAGE" exit 1 fi # Menu else # Read current settings from existing Cron job Read [[ $IPFAMILY ]] || IPFAMILY='-4and6' # Read status of existing Cron job via last two journal lines: "dietpi-ddns" tag shows curl errors, "cron" tag with "dietpi-ddns" string shows Cron job execution STATUS='Manage DDNS settings to keep your dynamic IP with the static domain provided by your DDNS provider in sync' [[ $PROVIDER ]] && STATUS="Last DietPi-DDNS logs:\n$(journalctl -r -t dietpi-ddns -t CRON | grep -m4 dietpi-ddns | sort)" G_WHIP_DEFAULT_ITEM_NEXT='Provider' while Menu_Main; do :; done fi exit 0 }