Dobrev.EU Blog

Things I want to share

Dynamic Primary/backup DNS Resource Records Using BIND9 and Bash

| Comments

There is no sysadmin in the world that didn’t have to deal with dynamic DNS services like UltraDNS in order to automatically fall-back to alternative set of IPs in case of a network outage of the primary ISP line(s). Unlucky such services are not for free so I’ve created a small Bash script in order to achive similar functionality.

Problem description

Company A has a HA service that is available via multiple ISPs with variable bandwith capacity. For all the reasons one of the ISPs/incoming lines is considered primary and the rest standby. Although the HA service can be accessed over the standby IPs there is a DNS entry pointing to the primary provider as long as it is available. In case of a problem with the primary route one of the standbys will become primary thus DNS update is required.

dyndns.sh

dyndns.sh (dyndns.sh) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#!/bin/bash
#
# dyndns 0.5.9
# 
# This program is used to dynamicaly update BIND 
# records from a list of IPs. The first to be
# reachable via ICMP ping is set for the hostname
#
# Copyright (C) 2013 Martin Dobrev <martin.dobrev@unixsol.co.uk>
# Company: UNIXSOL LTD
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Chanegelog
# =========================================================
#
# v0.5.9 - Initial GNU GPL version
#
##################################################################
set +x

# Configuration
VERBOSE=0
LOGFILE=/var/log/dyndns.log
MAILTO="dnsadmin@dobrev.eu"
NSSERVER="10.0.0.1"
ZONE="lab.dobrev.eu"
KEY="/etc/named/Kdobrev.+125-342134.private"
TTL=60

# Do not chage below this line if you don't know what you do
# You're warned :)

PROG=$(basename $0)
VER="0.5.9"

function check_alive {
  ping -c 1 -w 2 -W 1 -q $1 > /dev/null
  return $?
}

function do_nsupdate {
  local lHOST=$1
  local lIPADDR=$2
  local lTTL=$3

  CURIP=$(dig +short @$NSSERVER $lHOST)

  if [ "$CURIP" = "$lIPADDR" ]; then
    return
  fi

cat <<EOF | nsupdate -k "$KEY"
server $NSSERVER
zone $ZONE
update delete $lHOST. A
update add $lHOST. $lTTL A $lIPADDR
send
EOF

  RC=$?

  if [ $RC != 0 ]; then
    eecho "FAILURE: Updating dynamic IP $lIPADDR on $NSSERVER failed (RC=$RC)"
    (
      echo "Subject: DDNS update failed"
      echo "To: $MAILTO"
      echo
      echo "Updating dynamic IP $lIPADDR on $NSSERVER failed (RC=$RC)"
    ) | /usr/sbin/sendmail -f noreply@dobrev.eu -F "DynDNS updater" "$MAILTO"
    return $RC
  else
    eecho "SUCCESS: Updating dynamic IP $lIPADDR on $NSSERVER succeeded"
    return $RC
  fi
}

function clear_host_entry {
  HOST=$1

cat <<EOF | nsupdate -k "$KEY"
server $NSSERVER
zone $ZONE
update delete $HOST. A
send
EOF

  RC=$?

  if [ $RC != 0 ]; then
    eecho "FAILURE: Removing hostname entry for $HOST on $NSSERVER failed (RC=$RC)"
    (
      echo "Subject: DDNS update failed"
      echo "To: $MAILTO"
      echo
      echo "Removing hostname entry for $HOST on $NSSERVER failed (RC=$RC)"
    ) | /usr/sbin/sendmail -f noreply@dobrev.eu -F "DynDNS updater" "$MAILTO"
    return $RC
  else
    eecho "SUCCESS: Removing hostname entry for $HOST on $NSSERVER succeeded"
    return $RC
  fi
}


function ddns_update {
  local stat=1

  check_alive $PRIIP
  if [[ $? != 0 ]]; then
    # Primary IP not reachable
    # Loop through the backup list and set the first available
    for i in $( seq 0 $((${#BKPIP[@]} - 1))); do
      SECIP=${BKPIP[$i]}

      check_alive $SECIP
      if [[ $? != 0 ]]; then
        eecho "WARN: $SECIP is not reachable."
      else
        eecho "INFO: $SECIP reachable"
        do_nsupdate $HOST $SECIP $TTL
        stat=0
        break;
      fi
    done

    if ! [[ $stat -eq 0 ]]; then
      # Secondary IP not reachable too
      # Huston - We have a problem
      clear_host_entry $HOST
      (
      echo "Subject: DDNS update failed"
      echo "To: $MAILTO"
      echo
      echo "WARNING: Can't find any available IPs for $HOST."
      echo "Please contact your provider for additional information"
      ) | /usr/sbin/sendmail -f noreply@dobrev.eu -F "DynDNS updater" $MAILTO
    fi
  else
    eecho "INFO: $PRIIP available"
    do_nsupdate $HOST $PRIIP $TTL
  fi
}

function eecho {
  echo "$(LANG=C date +'%b %e %X') $(hostname) $PROG[$$]: $@" >> $LOGFILE
  if [ $VERBOSE -eq 1 ]; then
    echo "$(LANG=C date +'%b %e %X'): $@"
  fi
}

function validate_hostname {
  HAYSTACK="$1"
  NEEDLE="^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$";

  if ! [[ "$HAYSTACK" =~ $NEEDLE ]]; then
    eecho "FAILURE: Invalid hostname! Please use FQDN format"
    exit 2
  fi
}

function valid_ip {
    local  ip=$1
    local  stat=1

    if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
      OIFS=$IFS
      IFS='.'
      ip=($ip)
      IFS=$OIFS
      [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \
          && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]]
      stat=$?
    fi
    
    return $stat
}

function usage {
cat << EOF
Usage: $PROG -o hostname -p primaryip -b secondaryip [-b secondaryip] [-k keyfile] [-s server ] [-z zone] [-t ttl] [-c] [-v] [-V]

OPTIONS:
  -p primaryip    Sets the primary IP
  -b secondaryip  Sets the backup IP. Multiple entries are possible
  -o hostname     Change DNS settings for hostname. FQDN format required.
  -s server       Sets the DNS server to be modified. (Default: $NSSERVER)
  -z zone         Make changes in zone. (Default: $ZONE)
  -t ttl          Sets the TTL for the record. (Default: $TTL)
  -k keyfile      Use keyfile for DNS connection. (Default: $KEY)
  -c              Clears the DNS entry for hostname. See -o
  -v              Be verbose
  -V              Version ($VER)
  -h              This screen


If primary IP is not available/reachable then the system will try to access the secondary one and update
the DNS settings accordingly. Once Primary IP comes back up the system will switch back to it.

For additional information and modifications contact Martin Dobrev (martin.dobrev@unixsol.co.uk)
EOF
}

CLEARHOSTENTRY=0
PRIIP=
declare -a BKPIP

while getopts "hs:z:t:p:b:co:k:vV" OPTION
do
     case $OPTION in
        h)
          usage
          exit 1
          ;;
        s)
          NSSERVER=$OPTARG
          ;;
        z)
          ZONE=$OPTARG
          ;;
        t)
          TTL=$OPTARG
          ;;
        v)
          VERBOSE=1
          ;;
        p)
          PRIIP=$OPTARG
          ;;
        b)
          BKPIP+=( $OPTARG )
          ;;
        c)
          CLEARHOSTENTRY=1
          ;;
        o)
          HOST=$OPTARG
          ;;
        k)
          KEY=$OPTARG
          ;;
        V)
          echo "$PROG version $VER"
          exit
          ;;
        ?)
          usage
          exit
          ;;
     esac
done

if [[ -n $HOST ]] && [[ $CLEARHOSTENTRY -eq 1 ]]
then
  clear_host_entry $HOST
  eecho "INFO: $HOST removed from server $NSSERVER (zone $ZONE)"
  exit 0
fi

if [[ -z $HOST ]] || [[ -z $PRIIP ]] || [[ ${#BKPIP[@]} -lt 1 ]]
then
  usage
  exit 99
fi

ddns_update

Example usage

The simplest way to use dyndns.sh is with one primary and one standby(backup) IP.

Simple Example
1
dyndns.sh -o puppetmaster -z lab.dobrev.eu -p 10.0.0.2 -b 10.0.0.3

As long as 10.0.0.2 is available puppetmaster.lab.dobrev.eu is going to point to it. Else 10.0.0.3 is going to be used.

Requirements

  • BIND9 server with a configured key for dynamic updates
  • External node to run this script from

Known issues

  • This script is unable to point a DNS record to multiple IPs but can be easily extended.

Comments