Backup DHCP and DNS services in a small home environment

 

Summary

Goal:  Create both primary and (backup) secondary DHCP and DNS services in a small home network.
Needed: one Linux server, one Windows machine and one DHCP capable router with a telnet interface.
Assumption: some programming experience.
Contra indication: not necessary when two Linux servers are available.
Alternative option: Dual DHCP DNS server , http://sourceforge.net/projects/dhcp-dns-server/files/
Note: IP addresses, domain names, user names & passwords used here, do not reflect the actual setup.

Overview

My home network consists of one server running Linux, a number of wired clients running on Microsoft or Apple operating systems and an seemingly ever growing number of wireless clients running on a variety of platforms.  The network infrastructure consists of a managed switch, a dual WAN router with wireless AP, a separate second wireless AP, a cable modem and a ADSL modem. The server is running both ISC DHCPD and ISC Bind providing crucial DHCP and DNS service to clients in the network.

The information entered in the dhcp configuration file on the server located at /etc/dhcp/dhcpd.conf is authoritative for the network. Using the built-in Dynamic DNS mechanism, the DHCP server updates the relevant zone files of the DNS server automatically. This way domain name and IP address associations are entered and maintained in one place: the file dhcpd.conf. See the section on DDNS for the details.

The goal is to design the network in such a way that when the server is down (for whatever reason) Internet service to known clients is not interrupted. Specifically, known clients are still able to receive DHCP leases and resolve private domain names to IP addresses and vice versa.  Obviously other services the server may provide, e.g. smb shares, are interrupted.

To achieve the stated goal it is necessary to have secondary DHCP and DNS services in case the primary services on the Linux server fail. See Figure 1

All clients in the home network are grouped into three separate categories: infrastructure, known clients and unknown clients:

Infrastructure clients form the first category. These are all clients with a fixed IP address, i.e. they do not use DHCP. Examples of clients in this category are: the Linux server and the dual WAN router.

Known clients form the second category. These can be either wired of wireless clients. They get their static IP addresses using the DHCP service and their names need to be resolved in the DNS.

Unknown clients form the third category. These can be either wired of wireless clients. They get their IP addresses using the dynamic pool of the DHCP service and their names need to be resolved in the DNS. The dynamic address pool is the only such pool on the network. When the Linux server fails, unknown clients will not get an IP address and will not be able to access the Internet. This is a design decision. 

 

 

 

 

dhcp-dns v1.0.jpg

Figure 1 DHCP and DNS services

DDNS on the server

The following article http://www.semicomplete.com/articles/dynamic-dns-with-dhcp/ lists basic setup of Dynamic DNS. Read this to setup the necessary secret key file which is omitted here.

The domain name and IP address of all infrastructure clients are manually seeded into the DNS server, using the privatezone.db.green and revprivatezone.db.green zone files.

The domain name and IP address associations of known clients are automatically pushed to the DNS server, using Dynamic DNS, on first usage. The privatezone.db and revprivatezone.db zone files are automatically updated. The /etc/dhcp/dhcpd.conf file is the source for this information.

The domain name and IP address associations of unknown clients are automatically pushed to the dns server, using Dynamic DNS, on first usage. The privatezone.db and revprivatezone.db zone files are automatically updated. The /etc/dhcp/dhcpd.conf file is the source for this information.

Custom scripts and the dual WAN router

Unfortunately there is no Windows version of the ISC DHCPD software. There are plenty of other Windows DHCP servers, but none, that I could find,  offers the ability to synchronize itself with ISC DHCPD on the Linux server. Several options were investigated and considered. Finally a custom solution was chosen in which the dual WAN router functions as a secondary DHCP server. Using custom scripts the IP address- MAC address associations in the DHCP configuration file on the server are copied automatically to the internal DHCP table on the dual WAN router whenever  /etc/dhcp/dhcpd.conf  is changed. For this to work the router needs to be accessible using a telnet command interface.

Master/slave and the Windows 7 semi server

The Internet Systems Consortium offers  a Windows version of its Bind software, it is called NT Bind. It is easy to set up NT Bind on a Windows 7 client and configure it as a secondary, slave, server.  Using the built-in master/slave mechanism of ISC Bind all relevant zone files are copied to the secondary DNS server. This Windows semi server needs to be up 24/7 in order for this to work reliably.

DDNS on the server detailed

The /etc/named.conf configuration file on the Linux server, the “green” zone files and a portion of the /etc/hdcp/dhcpd.conf configuration file are listed below. Secret key files are omitted.

# Begin of file /etc/dhcp/dhcpd.conf

server-identifier 192.168.1.100;

server-name server

authoritative;

ddns-update-style interim;

update-static-leases on;

deny client-updates;

deny bootp;

 

include "/var/named/server.key";

 

zone privatezone. {

  primary server.privatezone;

  key server-key;

}

 

zone 1.168.192.in-addr.arpa. {

  primary server.privatezone;

  key server-key;

}

# etc….


 

# Begin of file /etc/named.conf

acl localip { 127.0.0.1; 192.168.1.0/24;  };

acl servers { 192.168.1.100; 192.168.1.200; };

 

options {

        pid-file                                            "/var/run/named/named.pid";

        directory                                       "/var/named";

        dump-file                                      "/var/named/data/cache_dump.db";

        statistics-file                               "/var/named/data/named_stats.txt";

        memstatistics-file                   "/var/named/data/named_mem_stats.txt";

        empty-zones-enable yes;

        disable-empty-zone "1.168.192.in-addr.arpa";

        disable-empty-zone "0.0.127.in-addr.arpa";

        allow-query     { localip; };

        allow-transfer {  none;   };

        also-notify { 192.168.1.200; };

        notify yes;

        allow-recursion {  localip;};

        forwarders { 208.67.222.222; 208.67.220.220;  };

};

 

include "rndc.key";

include "server.key";

 

logging {

        category "lame-servers" { null; };

        category "edns-disabled" { null; };

        channel default_debug {

                    file "data/named.run";

                    severity dynamic;

        };

};

 

controls {  inet 192.168.1.100 port 953 allow {servers; } keys { "rndc-key"; }; };

 

zone "localdomain" {

        type master;

        file "localdomain.db";

        allow-update { none; };

        notify no;

};

zone "0.0.127.in-addr.arpa" {

        type master;

        file "revlocaldomain.db";

        allow-update { none; };

        notify no;

};

 

zone "." IN {

        type hint;

        file "named.ca";

};

 

zone "privatezone" {

        type master;

        file "privatezone.db";

        allow-transfer { servers; };

        allow-update { key server-key; };

};

zone "1.168.192.in-addr.arpa" {

        type master;

        file "revprivatezone.db";

        allow-transfer { servers; };

        allow-update { key server-key; };

 

};


 

; Begin of file /var/named/privatezone.db

$ORIGIN .

$TTL 43200      ; 12 hours

privatezone.                  IN      SOA             server.privatezone . postmaster.server.privatezone. (

                                                        2012030401 ; serial

                                                        300        ; refresh (5 minutes)

                                                        60         ; retry (1 minute)

                                                        1209600    ; expire (2 weeks)

                                                        43200      ; minimum (12 hours)

                                                )

                        IN      NS              server.privatezone.

                        IN      NS              winserver.privatezone.

                        IN      MX      10   server.privatezone.

$ORIGIN privatezone.

router         IN      A               192.168.1.1

b                     IN      A               192.168.1.2

c                     IN      A               192.168.1.3

server          IN      A               192.168.1.100

winserver IN       A               192.168.1.200


 

; Begin of file /var/named/revprivatezone.db

$ORIGIN .

$TTL 43200      ; 12 hours

1.168.192.in-addr.arpa. IN      SOA    server.privatezone. postmaster.server.privatezone. (

                                                2012030401 ; serial

                                                300        ; refresh (5 minutes)

                                                60         ; retry (1 minute)

                                                1209600    ; expire (2 weeks)

                                                43200      ; minimum (12 hours)

                                        )

                        IN      NS      server.privatezone.

                        IN      NS      winserver.privatezone.

$ORIGIN 1.168.192.in-addr.arpa.

1                       IN      PTR     router.privatezone.

2                       IN      PTR     b.privatezone.

3                       IN      PTR     c.privatezone.

100                   IN      PTR     server.privatezone.

200                   IN      PTR     winserver.privatezone.


 

 

Custom scripts and the dual WAN router detailed

There are four separate scripts:

Name

Type

Purpose

dhcp-status.sh

Shell script

Has dhcpd.conf changed?

dhcp-ddns.pl

Perl  script

List active ddns host names

dhcp-update.sh

Shell script

Update dhcp table of router

router.exp

Expect script

Execute command on router

 

The first script dhcp-status.sh  is run as a cron job on the Linux server every five minutes, using the following schedule:

#

2,7,12,17,22,27,32,37,42,47,52,57 * * * * root /etc/dhcp/dhcp-status.sh


 

This shell script takes no arguments on the command line. When run, it first checks if the configuration file /etc/dhcp/dhcpd.conf exists. If it exists it calculates a MD5 checksum of the file. It then looks for a previous checksum in a specific temporary file. If the two checksum are identical or if the old checksum in the temporary file does not yet exist, the configuration file was not changed. If the two checksums differ, the configuration file was changed, possibly containing new host entries. In the latter case, the dhcp-ddns.pl script is called to generate a new list of host entries and its output is piped to the dhcp-update.sh script, which actually updates the DHCP tables of the router.  In both cases the new MD5 checksum is written to the temporary file. These series of events create a secondary DHCP server in the router containing exactly the same host information as the primary DHCP server.   

#!/bin/bash

DHCP_CONF=/etc/dhcp/dhcpd.conf

MD5FILE=/tmp/.dhcpd.md5

if [ ! -f $DHCP_CONF ]

then

        exit 1

fi

MD5=`md5sum $DHCP_CONF | cut -d " " -f 1`

if [ -z $MD5 ]

then

        exit 1

fi

if [ -f $MD5FILE ]

then

        OLDMD5=`cat $MD5FILE`

        if [ -z $OLDMD5 ]

        then

                exit 1

        fi

        if [ "$MD5" != "$OLDMD5" ]

        then

                CHECK=`perl /etc/dhcp/dhcp-ddns.pl | /etc/dhcp/dhcp-update.sh`

        fi

fi

echo $MD5 > $MD5FILE


 

The second script dhcp-ddns.pl is written in the Perl scripting language. It takes no arguments on the command line. It opens the configuration file /etc/dhcp/dhcpd.conf  for reading and strips all comments. It then looks for host entries with a very distinct pattern:

host  <name>  {

hardware ethernet  <valid MAC address>;
fixed-address  <valid IP address>;
ddns-hostname  "<name>";
option host-name "<name>";

}

In this pattern <name> can be any string, <MAC address> can be any valid MAC address, <IP address> can be any valid IP address. If an entry matching this pattern is found, three parameters are saved :

<IP address> <MAC address>  <name>

The <name> parameter is taken from the line ddns-hostname “<name>”; Strictly speaking the other two <name> parameters can be different, because they are discarded in this process.  Actually the ddns-hostname “<name>” line is key here, the <name> parameter should be the hostname that will be used in setting  up  the client's A and PTR records using the dynamic dns mechanism. When the configuration file is completely processed in this way, one line for all host entries matching this pattern is printed on standard output. Each line contains the three parameters, separated by white space.

Please note that host entries for infrastructure clients look like:

host  <name>  {

hardware ethernet  <valid MAC address>;
fixed-address  <valid IP address>;

}

They do not match the above pattern and their names will not be dynamically updated in the DNS. This means the names of infrastructure clients must be manually seeded into the relevant zone files of the DNS.

use warnings;

use strict;

 

# Location of the ISC DHCPD configuration file

use constant CONFIGFILE => '/etc/dhcp/dhcpd.conf';

 

my @ddnshosts = ();

my $config = '';

 

# Open and read the ISC DHCPD configuration file

# strip all comments and load result into array $config

open(DHCPDCONF, '<', CONFIGFILE);

while (<DHCPDCONF>) {

    s/^([^#]*).*$/$1/;

    $config .= $_;

}

close DHCPDCONF;

 

while ( $config =~ m/

        ^\s*host\s+([[:alnum:]-]+)\s*{

        \s*hardware\ ethernet\s+((?:[[:xdigit:]]{2}:){5}[[:xdigit:]]{2})\s*;

        \s*fixed-address\s+(\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b)\s*;

        \s*ddns-hostname\s+"([[:alnum:]-]+)"\s*;

        \s*option\ host-name\s+"([[:alnum:]-]+)"\s*;

        \s*}\s*$

      /mxgo ) {

    push @ddnshosts, { ip => $3, mac => lc($2), hostname => $4 };

}

 

# List the DDNS entries, if any, on standard output

# entry: IP-ADDRESS, MAC-ADDRESS, DDNS-HOSTNAME

$~ = 'DDNS';

write;

 

# End of Program

 

# Format statement, NOT executed.

format DDNS =

@<<<<<<<<<<<<<<    @<<<<<<<<<<<<<<<<  @<<<<<<<<<<<<<<<<<<<< ~~

{

  my $tmp = shift @ddnshosts;

  if ($tmp) {

    ( $$tmp{ip}, $$tmp{mac}, $$tmp{hostname} );

  }

  else {

    ( "", "", "" );

 }

}

.

# Please note the DOT on the previous line


 

The third script dhcp-update.sh is a shell script. Please note that this script is tailored to the specific telnet commands of the router. This will need customization for different routers.  The script reads lines from the standard input containing three parameters: <IP address> <MAC address> <name>. It checks for a valid IP address, a valid MAC address and a non empty name. If these checks are passed and if it is the first line read then the DHCP table on the router is completely erased, a new leasetime parameter is sent to the router and a fake host entry is sent to the router, containing the line 192.168.1.0 00:00:00:00:00:00 <timestamp>. The purpose of this fake entry is to easily check on the router at what time the DHCP table was last replaced. After this all other lines are read and valid host entries are added to the DHCP table of the router. This script uses the fourth script router.exp to actually send the proper commands to the router.

#!/bin/bash

 

SCRIPT=/etc/dhcp/router.exp

 

# Formats of valid IP and MAC addresses

ipformat="\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b"

macformat="^([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}$"

 

ONCE=0

timestamp=`date +%Y%m%d%H%M`

 

# Set time to live of lease in seconds (set to 4 hours)

ttl=14400

 

while read ip mac name

do

        CHECK=$(echo $ip | grep –P $ipformat)

        if [[ "$?" -eq 0 ]]

        then

                CHECK=$(echo $mac | grep –P  $macformat)

                if [[ "$?" -eq 0 ]]

                then

                        if test $name

                        then

                                # At this point we have at least one valid entry...

                                if [[ $ONCE == 0 ]]

                                then

                                        # Execute this command only once, first time around

                                        $SCRIPT "ip bindmac del all"

                                        # Set time to live here ...

                                        $SCRIPT "srv dhcp leasetime $ttl"

                                        $SCRIPT "ip bindmac add 192.168.1.0 00:00:00:00:00:00 $timestamp"

                                        ONCE=1

                                fi

                                # add an entry to bindmac table of router

                                $SCRIPT "ip bindmac add $ip $mac $name"

                        fi

                fi

        fi

done


 

The fourth script router.exp is an expect script. Please note that this script needs to be tailored to the specifics of the router. Three things need to be changed in this script for it to work at all: the user name, the matching password and the name of the router. This script takes one argument: a single router command. It then uses the telnet command to log in on the router. If successful, it executes the one command entered as an argument to this script.

Caveats

·         The /etc/dhcp/dhcpd.conf configuration file is not formally parsed in this process.

·         There are no security measures in use, other than the username and password of the router.

·         On the router the DHCP server must be enabled and active

·         On the router dynamic address pools must be disabled, or chaos ensues.

#!/usr/bin/expect -f

 

set force_conservative 0 ;

if {$force_conservative} {

        set send_slow {1 .1}

        proc send {ignore arg} {

                sleep .1

                exp_send -s -- $arg

        }

}

 

# Set account details on next two lines. Make sure this file is not group or world readable.

set account "<user name>"

set pass "<password>"

# Read command as arg to this script

set routercmd [lindex $argv 0]

 

set timeout -1

# Set name of router on next line.

spawn telnet <name-of-router>

match_max 100000

expect -exact "Account:"

send -- "$account\r"

expect -exact "Password: "

send -- "$pass\r"

expect -exact "> "

send -- "$routercmd\r"

expect -exact "> "

send -- "quit\r"

expect eof


 

Master/slave and the Windows 7 semi server detailed

Installation of the ISC Bind program on Windows is pretty straightforward. It operates as a windows service. The D:\NTbind\dns\etc\named.conf configuration file is listed below and is all that’s needed. When started the zone files for which this secondary server is the slave are automatically transferred from the Linux server. Magic!

 

acl localip { 127.0.0.1; 192.168.1.0/24; };

acl servers { 192.168.1.100; 192.168.1.200; };

 

options {

                        directory "D:\NTbind\dns\etc";

                        empty-zones-enable yes;

                        disable-empty-zone "1.168.192.in-addr.arpa";

                        disable-empty-zone "0.0.127.in-addr.arpa";

                        allow-query                     {

                                                localip;

                        };

                        allow-transfer {

                                                none;

                        };

                        allow-recursion            {

                                                localip;

                        };

                    forwarders {

                                                                208.67.222.222; 208.67.220.220;

                    };

};

 

include "rndc.key";

 

logging {

        category "lame-servers" { null; };

        category "edns-disabled" { null; };

        channel default_debug {

                file "named.run";

                severity dynamic;

        };

};

 

controls {

                        inet 192.168.1.200 port 953 allow { 192.168.1.28; 192.168.1.200; } keys { "rndc-key"; };

};

 

zone "localdomain" {

                        type master;

                        file "localdomain.db";

                        notify no;

};

 

zone "0.0.127.in-addr.arpa" {

                        type master;

                        file "revlocaldomain.db";

                        notify no;

};

 

zone "." IN {

        type hint;

        file "named.ca";

};

 

zone "privatezone" {

                        type slave;

                        file "privatezone.db";

                        masters { 192.168.1.100; };

                        allow-transfer { servers; };

};

 

zone "1.168.192.in-addr.arpa" {

        type slave;

        file "revprivatezone.db";

                        masters { 192.168.1.100; };

                        allow-transfer { servers; };

};