#!/usr/bin/perl # dynamic-dnsmasq.pl - update dnsmasq's internal dns entries dynamically # Copyright (C) 2004 Peter Willis # # 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 2 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, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # the purpose of this script is to be able to update dnsmasq's dns # records from a remote dynamic dns client. # # basic use of this script: # dynamic-dnsmasq.pl add testaccount 1234 testaccount.mydomain.com # dynamic-dnsmasq.pl listen & # # this script tries to emulate DynDNS.org's dynamic dns service, so # technically you should be able to use any DynDNS.org client to # update the records here. tested and confirmed to work with ddnsu # 1.3.1. just point the client's host to the IP of this machine, # port 9020, and include the hostname, user and pass, and it should # work. # # make sure "addn-hosts=/etc/dyndns-hosts" is in your /etc/dnsmasq.conf # file and "nopoll" is commented out. use strict; use IO::Socket; use MIME::Base64; use DB_File; use Fcntl; my $accountdb = "accounts.db"; my $recordfile = "/etc/dyndns-hosts"; my $dnsmasqpidfile = "/var/run/dnsmasq.pid"; # if this doesn't exist, will look for process in /proc my $listenaddress = "0.0.0.0"; my $listenport = 9020; # no editing past this point should be necessary if ( @ARGV < 1 ) { die "Usage: $0 ADD|DEL|LISTUSERS|WRITEHOSTSFILE|LISTEN\n"; } elsif ( lc $ARGV[0] eq "add" ) { die "Usage: $0 ADD USER PASS HOSTNAME\n" unless @ARGV == 4; add_acct($ARGV[1], $ARGV[2], $ARGV[3]); } elsif ( lc $ARGV[0] eq "del" ) { die "Usage: $0 DEL USER\n" unless @ARGV == 2; print "Are you sure you want to delete user \"$ARGV[1]\"? [N/y] "; my $resp = <STDIN>; chomp $resp; if ( lc substr($resp,0,1) eq "y" ) { del_acct($ARGV[1]); } } elsif ( lc $ARGV[0] eq "listusers" or lc $ARGV[0] eq "writehostsfile" ) { my $X = tie my %h, "DB_File", $accountdb, O_RDWR|O_CREAT, 0600, $DB_HASH; my $fh; if ( lc $ARGV[0] eq "writehostsfile" ) { open($fh, ">$recordfile") || die "Couldn't open recordfile \"$recordfile\": $!\n"; flock($fh, 2); seek($fh, 0, 0); truncate($fh, 0); } while ( my ($key, $val) = each %h ) { my ($pass, $domain, $ip) = split("\t",$val); if ( lc $ARGV[0] eq "listusers" ) { print "user $key, hostname $domain, ip $ip\n"; } else { if ( defined $ip ) { print $fh "$ip\t$domain\n"; } } } if ( lc $ARGV[0] eq "writehostsfile" ) { flock($fh, 8); close($fh); dnsmasq_rescan_configs(); } undef $X; untie %h; } elsif ( lc $ARGV[0] eq "listen" ) { listen_for_updates(); } sub listen_for_updates { my $sock = IO::Socket::INET->new(Listen => 5, LocalAddr => $listenaddress, LocalPort => $listenport, Proto => 'tcp', ReuseAddr => 1, MultiHomed => 1) || die "Could not open listening socket: $!\n"; $SIG{'CHLD'} = 'IGNORE'; while ( my $client = $sock->accept() ) { my $p = fork(); if ( $p != 0 ) { next; } $SIG{'CHLD'} = 'DEFAULT'; my @headers; my %cgi; while ( <$client> ) { s/(\r|\n)//g; last if $_ eq ""; push @headers, $_; } foreach my $header (@headers) { if ( $header =~ /^GET \/nic\/update\?([^\s].+) HTTP\/1\.[01]$/ ) { foreach my $element (split('&', $1)) { $cgi{(split '=', $element)[0]} = (split '=', $element)[1]; } } elsif ( $header =~ /^Authorization: basic (.+)$/ ) { unless ( defined $cgi{'hostname'} ) { print_http_response($client, undef, "badsys"); exit(1); } if ( !exists $cgi{'myip'} ) { $cgi{'myip'} = $client->peerhost(); } my ($user,$pass) = split ":", MIME::Base64::decode($1); if ( authorize($user, $pass, $cgi{'hostname'}, $cgi{'myip'}) == 0 ) { print_http_response($client, $cgi{'myip'}, "good"); update_dns(\%cgi); } else { print_http_response($client, undef, "badauth"); exit(1); } last; } } exit(0); } return(0); } sub add_acct { my ($user, $pass, $hostname) = @_; my $X = tie my %h, "DB_File", $accountdb, O_RDWR|O_CREAT, 0600, $DB_HASH; $X->put($user, join("\t", ($pass, $hostname))); undef $X; untie %h; } sub del_acct { my ($user, $pass, $hostname) = @_; my $X = tie my %h, "DB_File", $accountdb, O_RDWR|O_CREAT, 0600, $DB_HASH; $X->del($user); undef $X; untie %h; } sub authorize { my $user = shift; my $pass = shift; my $hostname = shift; my $ip = shift;; my $X = tie my %h, "DB_File", $accountdb, O_RDWR|O_CREAT, 0600, $DB_HASH; my ($spass, $shost) = split("\t", $h{$user}); if ( defined $h{$user} and ($spass eq $pass) and ($shost eq $hostname) ) { $X->put($user, join("\t", $spass, $shost, $ip)); undef $X; untie %h; return(0); } undef $X; untie %h; return(1); } sub print_http_response { my $sock = shift; my $ip = shift; my $response = shift; print $sock "HTTP/1.0 200 OK\n"; my @tmp = split /\s+/, scalar gmtime(); print $sock "Date: $tmp[0], $tmp[2] $tmp[1] $tmp[4] $tmp[3] GMT\n"; print $sock "Server: Peter's Fake DynDNS.org Server/1.0\n"; print $sock "Content-Type: text/plain; charset=ISO-8859-1\n"; print $sock "Connection: close\n"; print $sock "Transfer-Encoding: chunked\n"; print $sock "\n"; #print $sock "12\n"; # this was part of the dyndns response but i'm not sure what it is print $sock "$response", defined($ip)? " $ip" : "" . "\n"; } sub update_dns { my $hashref = shift; my @records; my $found = 0; # update the addn-hosts file open(FILE, "+<$recordfile") || die "Couldn't open recordfile \"$recordfile\": $!\n"; flock(FILE, 2); while ( <FILE> ) { if ( /^(\d+\.\d+\.\d+\.\d+)\s+$$hashref{'hostname'}\n$/si ) { if ( $1 ne $$hashref{'myip'} ) { push @records, "$$hashref{'myip'}\t$$hashref{'hostname'}\n"; $found = 1; } } else { push @records, $_; } } unless ( $found ) { push @records, "$$hashref{'myip'}\t$$hashref{'hostname'}\n"; } sysseek(FILE, 0, 0); truncate(FILE, 0); syswrite(FILE, join("", @records)); flock(FILE, 8); close(FILE); dnsmasq_rescan_configs(); return(0); } sub dnsmasq_rescan_configs { # send the HUP signal to dnsmasq if ( -r $dnsmasqpidfile ) { open(PID,"<$dnsmasqpidfile") || die "Could not open PID file \"$dnsmasqpidfile\": $!\n"; my $pid = <PID>; close(PID); chomp $pid; if ( kill(0, $pid) ) { kill(1, $pid); } else { goto LOOKFORDNSMASQ; } } else { LOOKFORDNSMASQ: opendir(DIR,"/proc") || die "Couldn't opendir /proc: $!\n"; my @dirs = grep(/^\d+$/, readdir(DIR)); closedir(DIR); foreach my $process (@dirs) { if ( open(FILE,"</proc/$process/cmdline") ) { my $cmdline = <FILE>; close(FILE); if ( (split(/\0/,$cmdline))[0] =~ /dnsmasq/ ) { kill(1, $process); } } } } return(0); }