Last active
November 5, 2025 01:52
-
-
Save BluSyn/37670b7b9778489fb8394cb90de57294 to your computer and use it in GitHub Desktop.
updates unifi internal DNS with IPv6 addresses (needed for dual WAN failover scenario, where local IPv4 is same, but global IPv6 changes)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # run as sudo | |
| ln -sfT ${PWD}/unifi-dns-update.sh /usr/local/bin/unifi-dns-update | |
| ln -sfT ${PWD}/unifi-ipv6-monitor.sh /usr/local/bin/unifi-ipv6-monitor | |
| ln -sfT ${PWD}/unifi-ipv6-monitor.service /usr/lib/systemd/system/unifi-ipv6-monitor.service | |
| systemctl daemon-reload && systemctl enable --now unifi-ipv6-monitor.service |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Script to update UniFi DNS AAAA record with current IPv6 | |
| # --- Config --- | |
| CONTROLLER="https://192.168.1.1" # Your UniFi controller URL (include port if not 443) | |
| SITE="default" # Site name, usually "default" | |
| API_KEY="<KEY>" # UniFi api key (Settings > Control Plane > Integrations) | |
| HOSTNAMES="example.org *.example.org" # The DNS hostname to update (e.g., nas.mylan) | |
| INTERFACE="eth0" # Network interface with global IPv6 (check with ip -6 addr) | |
| # --- Get current global IPv6 --- | |
| IPV6=$(ip -6 addr show dev "$INTERFACE" scope global | awk '/inet6/ {print $2}' | cut -d/ -f1 | head -n1) | |
| if [ -z "$IPV6" ]; then | |
| echo "Error: No global IPv6 found on $INTERFACE" >&2 | |
| exit 1 | |
| fi | |
| ENDPOINT="$CONTROLLER/proxy/network/v2/api/site/$SITE/static-dns" | |
| CURRENT="/tmp/unifi_dns.json" | |
| # --- Get current DNS settings --- | |
| curl --silent -k -H "X-API-KEY: $API_KEY" "$ENDPOINT" > "$CURRENT" | |
| if [ ! -s "$CURRENT" ]; then | |
| echo "Error: Failed to get DNS settings" >&2 | |
| exit 1 | |
| fi | |
| # --- Loop over each hostname --- | |
| for HOSTNAME in $HOSTNAMES; do | |
| # Find existing record | |
| EXISTING=$(jq -r --arg h "$HOSTNAME" 'map(select(.key == $h and .record_type == "AAAA")) | .[0]' "$CURRENT") | |
| EXISTING_ID=$(echo "$EXISTING" | jq -r '._id // ""') | |
| EXISTING_VALUE=$(echo "$EXISTING" | jq -r '.value // ""') | |
| # Skip if exists and value matches | |
| if [ -n "$EXISTING_ID" ] && [ "$EXISTING_VALUE" = "$IPV6" ]; then | |
| echo "No change needed for $HOSTNAME (already $IPV6)" | |
| continue | |
| fi | |
| # Build payload | |
| PAYLOAD=$(jq -n --arg h "$HOSTNAME" --arg v "$IPV6" '{ | |
| enabled: true, | |
| key: $h, | |
| port: 0, | |
| priority: 0, | |
| record_type: "AAAA", | |
| ttl: 0, | |
| value: $v, | |
| weight: 0 | |
| }') | |
| # Update or add | |
| if [ -n "$EXISTING_ID" ]; then | |
| # Update existing | |
| curl -k -H "X-API-KEY: $API_KEY" -H "Content-Type: application/json" -X PUT "$ENDPOINT/$EXISTING_ID" -d "$PAYLOAD" >/dev/null 2>&1 | |
| echo "Updated $HOSTNAME to $IPV6" | |
| else | |
| # Add new | |
| curl -k -H "X-API-KEY: $API_KEY" -H "Content-Type: application/json" -X POST "$ENDPOINT" -d "$PAYLOAD" >/dev/null 2>&1 | |
| echo "Added $HOSTNAME as $IPV6" | |
| fi | |
| done | |
| # --- Cleanup --- | |
| rm -f "$CURRENT" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Script to monitor IPv6 changes and update UniFi DNS only on actual IP change | |
| # Run as systemd service | |
| INTERFACE="eth0" # Your interface with global IPv6 | |
| UPDATE_SCRIPT="/usr/local/bin/unifi-dns-update" # Path to the script from previous messages | |
| # Function to get current primary global IPv6 (non-temporary) | |
| get_ipv6() { | |
| ip -6 addr show dev "$INTERFACE" scope global | grep -v "temporary" | awk '/inet6/ {print $2}' | cut -d/ -f1 | head -n1 | |
| } | |
| # Get initial IPv6 | |
| LAST_IPV6=$(get_ipv6) | |
| if [ -z "$LAST_IPV6" ]; then | |
| echo "Error: No initial global IPv6 found on $INTERFACE" >&2 | |
| exit 1 | |
| fi | |
| echo "Starting monitor on $INTERFACE, initial IPv6: $LAST_IPV6" | |
| # call update in init to keep in sync | |
| "$UPDATE_SCRIPT" | |
| # Monitor address events | |
| ip -o -6 monitor address dev "$INTERFACE" | while read line; do | |
| # Parse event | |
| if [[ "$line" == "Deleted "* ]]; then | |
| action="del" | |
| rest="${line#Deleted }" | |
| else | |
| action="add" | |
| rest="$line" | |
| fi | |
| # Extract addr and scope | |
| addr=$(echo "$rest" | awk '{print $4}' | cut -d/ -f1) | |
| scope=$(echo "$rest" | awk '{for(i=1;i<=NF;i++) if ($i=="scope") print $(i+1)}') | |
| # Filter for global non-temporary | |
| if [[ "$scope" == "global" && "$rest" != *"temporary"* && -n "$addr" ]]; then | |
| echo "IPv6 $action event: $addr" | |
| # Re-fetch current IPv6 after event | |
| NEW_IPV6=$(get_ipv6) | |
| # Only trigger if actual change and new IP is present | |
| if [ -n "$NEW_IPV6" ] && [ "$NEW_IPV6" != "$LAST_IPV6" ]; then | |
| echo "Actual IPv6 change detected: $LAST_IPV6 -> $NEW_IPV6 - Running update" | |
| "$UPDATE_SCRIPT" | |
| LAST_IPV6="$NEW_IPV6" | |
| fi | |
| fi | |
| done |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [Unit] | |
| Description=Monitor IPv6 changes and update UniFi DNS | |
| After=network.target | |
| [Service] | |
| Type=simple | |
| ExecStart=/usr/local/bin/unifi-ipv6-monitor | |
| Restart=always | |
| RestartSec=10 | |
| [Install] | |
| WantedBy=multi-user.target |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment