Vyos Script to Update DDNS Peers
Vyos Script to Update DDNS Peers
December 15, 2025
Vyos seems like a great project but it’s documentation gives you an odd feeling as if you are looking at Lorem Ipsum stances. According to T4930 it’s supposed to support dynamic dns for wireguard peers but sadly it still doesn’t. So here we are, this script goes through a given list of peers and updates the config. Tested on 2025-11 Stream of Vyos.
Revised Dec 16, 2025: Forgot to add the necessary exit at the end of the script. Without the exit statement, vyos keeps creating a new mountpoint on each script-template sourcing until you get the following warning:
fusermount: too many FUSE filesystems mounted; mount_max=N can be set in /etc/fuse.conf
#!/bin/vbash
# Script to dynamically update WireGuard peer IP addresses based on DNS resolution.
# Designed to run as a scheduled task (cron job) on a VyOS router.
# https://echoesofthings.com
# --- Installation ---
# save the script as /config/script/wireguard-ddns-update.sh
# chmod 0755 /config/script/wireguard-ddns-update.sh
# then setup the scheduler under config
# set system task-scheduler task wireguard-ddns-update crontab-spec "*/1 * * * *"
# set system task-scheduler task wireguard-ddns-update executable path /config/scripts/wireguard-ddns-update.sh
# commit
# save
# to check if it's scheduled, use
# show system task-scheduler
# --- Configuration Variables ---
# Set to 'true' to enable logs for successful IP resolution and no-change events.
# Set to 'false' to only log IP changes and errors (recommended for cron jobs).
VERBOSE_LOGGING="false"
# Define all peers in an array of triplets: "WG_INTERFACE PEER_HOSTNAME WG_PEER_NAME"
# Example:
# - Peer 1: on wg01, resolves office-a-ddns.otherdomain.net, peer name is office-peer-a
# - Peer 2: on wg02, resolves office-b-ddns.otherdomain.net, peer name is office-peer-b
# - add more triplets as needed
declare -a PEER_LIST=(
"wg01 office-a-ddns.otherdomain.net office-peer-a"
"wg01 office-b-ddns.otherdomain.net office-peer-b"
)
# Ensure the script is run under the correct group for configuration commands
if [ "$(id -g -n)" != 'vyattacfg' ] ; then
exec sg vyattacfg -c "/bin/vbash $(readlink -f $0) $@"
fi
source /opt/vyatta/etc/functions/script-template
# Function to log messages using the system 'logger' utility
log_message() {
local message="$1"
local level="$2"
local log_text="[WireGuard-DDNS] ${message}"
if [ "${VERBOSE_LOGGING}" = "true" ] || [ "${level}" = "CRITICAL" ]; then
# Use logger utility to write to system logs
logger -t "wireguard-ddns" "${log_text}"
fi
}
# Use a flag to track if any configuration changes were made, to avoid unnecessary 'save'
CONFIG_CHANGED="false"
# Start the main loop to process each peer entry
for peer_entry in "${PEER_LIST[@]}"; do
# Split the array entry into three separate variables
WG_INTERFACE=$(echo "$peer_entry" | awk '{print $1}')
PEER_HOSTNAME=$(echo "$peer_entry" | awk '{print $2}')
WG_PEER_NAME=$(echo "$peer_entry" | awk '{print $3}')
log_message "--- Starting check for peer: ${WG_PEER_NAME} on ${WG_INTERFACE} (${PEER_HOSTNAME}) ---"
# 1. Resolve the hostname to the new IP address
NEW_IP=$(getent hosts "${PEER_HOSTNAME}" | awk '{ print $1 }')
# 2. Check if IP resolution was successful and valid
if [[ -z "${NEW_IP}" ]] || ! [[ "${NEW_IP}" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
log_message "ERROR: Failed to resolve hostname ${PEER_HOSTNAME} or received invalid IP: ${NEW_IP}" "CRITICAL"
continue # Skip to the next peer in the list
fi
# 3. Get the currently configured IP address
# Primary method: parse the configuration command line.
OLD_CONFIG_LINE=$(run show configuration commands | egrep "interfaces wireguard ${WG_INTERFACE} peer ${WG_PEER_NAME} address\b")
if [ -n "${OLD_CONFIG_LINE}" ]; then
OLD_IP=$(echo "${OLD_CONFIG_LINE}" | awk '{ print $8 }' | tr -d '\'\" | xargs)
else
log_message "WARNING: Could not find old IP in configuration commands. Checking 'show interfaces' operational state." "CRITICAL"
# Fallback: Parse the operational status. Get the 2nd field, strip port, quotes, and whitespace.
OLD_IP=$(run show interfaces wireguard "${WG_INTERFACE}" endpoints | awk '{ print $2 }' | awk -F ':' '{ print $1 }' | tr -d '\'\" | xargs)
fi
log_message "Resolved IP: ${NEW_IP}"
log_message "Configured IP: ${OLD_IP}"
# 4. Compare the new IP with the old IP
if [ "${NEW_IP}" != "${OLD_IP}" ]
then
log_message "IP address change detected for ${WG_PEER_NAME} on ${WG_INTERFACE}: ${OLD_IP} -> ${NEW_IP}. Marking for commit..." "CRITICAL"
# Set the address change command
configure
# Check if the interface/peer path exists before applying
if run show configuration commands | grep "interfaces wireguard ${WG_INTERFACE} peer ${WG_PEER_NAME}" &> /dev/null; then
set interfaces wireguard "${WG_INTERFACE}" peer "${WG_PEER_NAME}" address "${NEW_IP}"
# This is a key change: we check for commit success inside the loop
# and set the flag to save the changes *after* the loop completes.
if commit; then
log_message "Configuration committed successfully for ${WG_PEER_NAME}." "CRITICAL"
CONFIG_CHANGED="true" # Mark that we need to save the config
else
log_message "ERROR: Commit failed for ${WG_PEER_NAME}. Rolling back..." "CRITICAL"
rollback
fi
else
log_message "ERROR: Interface ${WG_INTERFACE} or peer ${WG_PEER_NAME} not found in configuration." "CRITICAL"
fi
else
log_message "IP address for ${WG_PEER_NAME} is unchanged: ${NEW_IP}. No action required."
fi
done
# FINAL STEP: Save the configuration once, only if changes were successfully committed
if [ "${CONFIG_CHANGED}" = "true" ]; then
log_message "All necessary configuration changes committed. Saving running configuration." "CRITICAL"
save
fi
# the exit is necessary, every time we source the script-template it creates a
# FUSE mount point
exitLast updated on