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