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.
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.
Figure 1 DHCP and DNS services
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.
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.
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.
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. |
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.
· 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 |
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; }; }; |