Home | History | Annotate | Download | only in dynamic-dnsmasq
      1 #!/usr/bin/perl
      2 # dynamic-dnsmasq.pl - update dnsmasq's internal dns entries dynamically
      3 # Copyright (C) 2004  Peter Willis
      4 # 
      5 # This program is free software; you can redistribute it and/or modify
      6 # it under the terms of the GNU General Public License as published by
      7 # the Free Software Foundation; either version 2 of the License, or
      8 # (at your option) any later version.
      9 #
     10 # This program is distributed in the hope that it will be useful,
     11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 # GNU General Public License for more details.
     14 #
     15 # You should have received a copy of the GNU General Public License
     16 # along with this program; if not, write to the Free Software
     17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
     18 # 
     19 # the purpose of this script is to be able to update dnsmasq's dns
     20 # records from a remote dynamic dns client.
     21 # 
     22 # basic use of this script:
     23 # dynamic-dnsmasq.pl add testaccount 1234 testaccount.mydomain.com
     24 # dynamic-dnsmasq.pl listen &
     25 # 
     26 # this script tries to emulate DynDNS.org's dynamic dns service, so
     27 # technically you should be able to use any DynDNS.org client to
     28 # update the records here. tested and confirmed to work with ddnsu
     29 # 1.3.1. just point the client's host to the IP of this machine,
     30 # port 9020, and include the hostname, user and pass, and it should
     31 # work.
     32 # 
     33 # make sure "addn-hosts=/etc/dyndns-hosts" is in your /etc/dnsmasq.conf
     34 # file and "nopoll" is commented out.
     35 
     36 use strict;
     37 use IO::Socket;
     38 use MIME::Base64;
     39 use DB_File;
     40 use Fcntl;
     41 
     42 my $accountdb = "accounts.db";
     43 my $recordfile = "/etc/dyndns-hosts";
     44 my $dnsmasqpidfile = "/var/run/dnsmasq.pid"; # if this doesn't exist, will look for process in /proc
     45 my $listenaddress = "0.0.0.0";
     46 my $listenport = 9020;
     47 
     48 # no editing past this point should be necessary
     49 
     50 if ( @ARGV < 1 ) {
     51 	die "Usage: $0 ADD|DEL|LISTUSERS|WRITEHOSTSFILE|LISTEN\n";
     52 } elsif ( lc $ARGV[0] eq "add" ) {
     53 	die "Usage: $0 ADD USER PASS HOSTNAME\n" unless @ARGV == 4;
     54 	add_acct($ARGV[1], $ARGV[2], $ARGV[3]);
     55 } elsif ( lc $ARGV[0] eq "del" ) {
     56 	die "Usage: $0 DEL USER\n" unless @ARGV == 2;
     57 	print "Are you sure you want to delete user \"$ARGV[1]\"? [N/y] ";
     58 	my $resp = <STDIN>;
     59 	chomp $resp;
     60 	if ( lc substr($resp,0,1) eq "y" ) {
     61 		del_acct($ARGV[1]);
     62 	}
     63 } elsif ( lc $ARGV[0] eq "listusers" or lc $ARGV[0] eq "writehostsfile" ) {
     64 	my $X = tie my %h, "DB_File", $accountdb, O_RDWR|O_CREAT, 0600, $DB_HASH;
     65 	my $fh;
     66 	if ( lc $ARGV[0] eq "writehostsfile" ) {
     67         	open($fh, ">$recordfile") || die "Couldn't open recordfile \"$recordfile\": $!\n";
     68 	       	flock($fh, 2);
     69 	       	seek($fh, 0, 0);
     70 	       	truncate($fh, 0);
     71         }
     72 	while ( my ($key, $val) = each %h ) {
     73 		my ($pass, $domain, $ip) = split("\t",$val);
     74 		if ( lc $ARGV[0] eq "listusers" ) {
     75 			print "user $key, hostname $domain, ip $ip\n";
     76 		} else {
     77 			if ( defined $ip ) {
     78 				print $fh "$ip\t$domain\n";
     79 			}
     80 		}
     81 	}
     82 	if ( lc $ARGV[0] eq "writehostsfile" ) {
     83 		flock($fh, 8);
     84 		close($fh);
     85 		dnsmasq_rescan_configs();
     86 	}
     87 	undef $X;
     88 	untie %h;
     89 } elsif ( lc $ARGV[0] eq "listen" ) {
     90 	listen_for_updates();
     91 }
     92 
     93 sub listen_for_updates {
     94 	my $sock = IO::Socket::INET->new(Listen    => 5,
     95 		LocalAddr => $listenaddress, LocalPort => $listenport,
     96 		Proto     => 'tcp', ReuseAddr => 1,
     97 		MultiHomed => 1) || die "Could not open listening socket: $!\n";
     98 	$SIG{'CHLD'} = 'IGNORE';
     99 	while ( my $client = $sock->accept() ) {
    100 		my $p = fork();
    101 		if ( $p != 0 ) {
    102 			next;
    103 		}
    104 		$SIG{'CHLD'} = 'DEFAULT';
    105 		my @headers;
    106 		my %cgi;
    107 		while ( <$client> ) {
    108 			s/(\r|\n)//g;
    109 			last if $_ eq "";
    110 			push @headers, $_;
    111 		}
    112 		foreach my $header (@headers) {
    113 			if ( $header =~ /^GET \/nic\/update\?([^\s].+) HTTP\/1\.[01]$/ ) {
    114 				foreach my $element (split('&', $1)) {
    115 					$cgi{(split '=', $element)[0]} = (split '=', $element)[1];
    116 				}
    117 			} elsif ( $header =~ /^Authorization: basic (.+)$/ ) {
    118 				unless ( defined $cgi{'hostname'} ) {
    119 					print_http_response($client, undef, "badsys");
    120 					exit(1);
    121 				}
    122 				if ( !exists $cgi{'myip'} ) {
    123 					$cgi{'myip'} = $client->peerhost();
    124 				}
    125 				my ($user,$pass) = split ":", MIME::Base64::decode($1);
    126 				if ( authorize($user, $pass, $cgi{'hostname'}, $cgi{'myip'}) == 0 ) {
    127 					print_http_response($client, $cgi{'myip'}, "good");
    128 					update_dns(\%cgi);
    129 				} else {
    130 					print_http_response($client, undef, "badauth");
    131 					exit(1);
    132 				}
    133 				last;
    134 			}
    135 		}
    136 		exit(0);
    137 	}
    138 	return(0);
    139 }
    140 
    141 sub add_acct {
    142 	my ($user, $pass, $hostname) = @_;
    143 	my $X = tie my %h, "DB_File", $accountdb, O_RDWR|O_CREAT, 0600, $DB_HASH;
    144 	$X->put($user, join("\t", ($pass, $hostname)));
    145 	undef $X;
    146 	untie %h;
    147 }
    148 
    149 sub del_acct {
    150         my ($user, $pass, $hostname) = @_;
    151         my $X = tie my %h, "DB_File", $accountdb, O_RDWR|O_CREAT, 0600, $DB_HASH;
    152         $X->del($user);
    153         undef $X;
    154         untie %h;
    155 }
    156 
    157 
    158 sub authorize {
    159 	my $user = shift;
    160 	my $pass = shift;
    161 	my $hostname = shift;
    162 	my $ip = shift;;
    163 	my $X = tie my %h, "DB_File", $accountdb, O_RDWR|O_CREAT, 0600, $DB_HASH;
    164 	my ($spass, $shost) = split("\t", $h{$user});
    165 	if ( defined $h{$user} and ($spass eq $pass) and ($shost eq $hostname) ) {
    166 		$X->put($user, join("\t", $spass, $shost, $ip));
    167 		undef $X;
    168 		untie %h;
    169 		return(0);
    170 	}
    171 	undef $X;
    172 	untie %h;
    173 	return(1);
    174 }
    175 
    176 sub print_http_response {
    177 	my $sock = shift;
    178 	my $ip = shift;
    179 	my $response = shift;
    180 	print $sock "HTTP/1.0 200 OK\n";
    181 	my @tmp = split /\s+/, scalar gmtime();
    182 	print $sock "Date: $tmp[0], $tmp[2] $tmp[1] $tmp[4] $tmp[3] GMT\n";
    183 	print $sock "Server: Peter's Fake DynDNS.org Server/1.0\n";
    184 	print $sock "Content-Type: text/plain; charset=ISO-8859-1\n";
    185 	print $sock "Connection: close\n";
    186 	print $sock "Transfer-Encoding: chunked\n";
    187 	print $sock "\n";
    188 	#print $sock "12\n"; # this was part of the dyndns response but i'm not sure what it is
    189 	print $sock "$response", defined($ip)? " $ip" : "" . "\n";
    190 }
    191 
    192 sub update_dns {
    193 	my $hashref = shift;
    194 	my @records;
    195 	my $found = 0;
    196 	# update the addn-hosts file
    197 	open(FILE, "+<$recordfile") || die "Couldn't open recordfile \"$recordfile\": $!\n";
    198 	flock(FILE, 2);
    199 	while ( <FILE> ) {
    200 		if ( /^(\d+\.\d+\.\d+\.\d+)\s+$$hashref{'hostname'}\n$/si ) {
    201 			if ( $1 ne $$hashref{'myip'} ) {
    202 				push @records, "$$hashref{'myip'}\t$$hashref{'hostname'}\n";
    203 				$found = 1;
    204 			}
    205 		} else {
    206 			push @records, $_;
    207 		}
    208 	}
    209 	unless ( $found ) {
    210 		push @records, "$$hashref{'myip'}\t$$hashref{'hostname'}\n";
    211 	}
    212 	sysseek(FILE, 0, 0);
    213 	truncate(FILE, 0);
    214 	syswrite(FILE, join("", @records));
    215 	flock(FILE, 8);
    216 	close(FILE);
    217 	dnsmasq_rescan_configs();
    218 	return(0);
    219 }
    220 
    221 sub dnsmasq_rescan_configs {
    222 	# send the HUP signal to dnsmasq
    223 	if ( -r $dnsmasqpidfile ) {
    224 		open(PID,"<$dnsmasqpidfile") || die "Could not open PID file \"$dnsmasqpidfile\": $!\n";
    225 		my $pid = <PID>;
    226 		close(PID);
    227 		chomp $pid;
    228 		if ( kill(0, $pid) ) {
    229 			kill(1, $pid);
    230 		} else {
    231 			goto LOOKFORDNSMASQ;
    232 		}
    233 	} else {
    234 		LOOKFORDNSMASQ:
    235 		opendir(DIR,"/proc") || die "Couldn't opendir /proc: $!\n";
    236 		my @dirs = grep(/^\d+$/, readdir(DIR));
    237 		closedir(DIR);
    238 		foreach my $process (@dirs) {
    239 			if ( open(FILE,"</proc/$process/cmdline") ) {
    240 				my $cmdline = <FILE>;
    241 				close(FILE);
    242 				if ( (split(/\0/,$cmdline))[0] =~ /dnsmasq/ ) {
    243 					kill(1, $process);
    244 				}
    245 			}
    246 		}
    247 	}
    248 	return(0);
    249 }
    250