#!/usr/bin/env perl # @(#) DnsPixie.pl Update client for FreeDNS, DuckDNS and other popular # dynamic DNS services. Rev'd: 2023-01-14 # # Copyright (c) 2014 Graham Jenkins . All rights reserved. # This program is free software; you can redistribute it and/or modify it under # the same terms as Perl itself. use strict; use warnings; use File::Basename; use Getopt::Std; use Sys::Syslog; use Net::IP qw(:PROC); # Use whichever Curl module is available BEGIN { eval { require Net::Curl::Compat } } use WWW::Curl::Easy; use vars qw($VERSION); $VERSION=1.29; $Getopt::Std::STANDARD_HELP_VERSION=1; # Collect options, check usage, open configuration file my ($Mode,$BadOption,%opts)=("IPv4",,); getopts ('dxvm:l:',\%opts) or $BadOption="Y"; if ( ($#ARGV != 0) || defined($BadOption) || (defined($opts{'m'}) && ($opts{'m'} !~ m/^\d+$/)) ) { die "Usage: ".basename($0)." [-x] [-d] [-v] [-m N] [-l led] ". "config-file\n". "Refer: perldoc ".basename($0)."\n" } if ( defined($opts{'m'})&&($opts{'m'}==0) ){die "Zero interval not allowed!\n"} if ( ! open(CF,$ARGV[0]) ) { die "Can't read file: ".$ARGV[0]."\n" } # Read and store potentially meaningful records, then close the file my %Types=('FreeDNS',1,'Dynu',1,'DuckDNS',1,'ChangeIP',1,'DNSExit',1); my (%Type,%Pass,%OldAddr,%ChangeTime); while () { s/\s+$//; # Strip trailing white-space; my ($t,$h,$p,$junk) = split; # separate, check and store fields. if ( defined($p) && defined($Types{$t}) && (! defined($junk) ) ) { $Type{$h}=$t; $Pass{$h}=$p; $OldAddr{$h}="x"; $ChangeTime{$h}=time() } elsif ( defined($t) && ($t !~ /^\043/) ) { die "Illegal record in file: ".$ARGV[0]."\n>> $_\n" } } close(CF); if ( keys(%Pass) < 1 ) { die "No valid records found in file: ".$ARGV[0]."\n" } # Sources for current IP address; use these in sequence my @MyIps=("ifconfig.co/ip","ip1.dynupdate.no-ip.com","now-dns.com/ip", "dynamic.zoneedit.com/checkip.html","cpanel.com/showip.shtml","v4.ident.me"); if ( defined($opts{'x'}) ) { @MyIps=("ipv6.icanhazip.com","ydns.io/api/v1/ip", "my.ip.fi","now-dns.com/ip","ipv6.duia.ro","v6.ident.me"); $Mode="IPv6" } my $r=int(rand($#MyIps+1)); # Random-select first source for (my $i=0;$i<$r;$i++) { push(@MyIps,shift(@MyIps)) } # Process 'd' option if ( defined($opts{'d'}) ) { fork and exit } # Open log, then loop once if repeat-interval not set, else loop forever openlog("dnspixie",,"local7"); while (1) { # Select next IP-address source, get current address push(@MyIps,my $MyIp=shift(@MyIps)); my ($Status,$Address)=(0,); if ( ($Address=getMyAddress($MyIp,"$Mode")) ) { if ( defined($opts{'v'}) ) {syslog("info","$Mode Got $Address from $MyIp")} # Update each host address on first pass or if current address changed; # also force update if this program hasn't changed it during last 3 days. foreach my $h ( sort(keys(%Pass)) ) { if ( ($Address ne $OldAddr{$h}) || (time()-$ChangeTime{$h}>3*24*3600) ) { syslog("info","$Mode Attempting update for $h to: $Address"); my ($p,$Response,$Good)=($Pass{$h},,); if ( $Type{$h} eq "FreeDNS" ) { my $x=defined($opts{'x'}) ? "&address=$Address" : ""; if ( defined($Response=get( "https://freedns.afraid.org/dynamic/update.php?$p$x")) ) { if ( ($Response=~m/has not chang/) || ($Response=~m/^Updated/) ) { $Response=~s/^ERROR: //; $Good="Y" } } } if ( $Type{$h} eq "DuckDNS" ) { my $x=defined($opts{'x'}) ? "&ipv6=$Address" : "&ip=$Address"; if ( defined($Response=get( "https://www.duckdns.org/update?domains=$h&token=$p$x" . "&verbose=true")) ) { if ( $Response=~m/^OK/ ) { $Good="Y" } } } if ( $Type{$h} eq "DNSExit" ) { if ( defined($Response=get( "https://api.dnsexit.com/dns/ud/?apikey=$p&host=$h")) ) { if ( $Response=~m/:[0-1],/ ) { $Good="Y" } my ($n1,$n2)=( index($Response,"{"), index($Response,"}") ); $Response=substr($Response,$n1,$n2-$n1+1) } } if ( $Type{$h} eq "ChangeIP") { my ($x,$y)=split /:/,$p; if ( defined($Response=get( "https://nic.ChangeIP.com/nic/update?u=$x&p=$y&hostname=$h")) ) { if ( $Response=~m/200 Successful Update/ ) { $Good="Y" } } } if ( $Type{$h} eq "Dynu" ) { my ($x,$y)=split /:/,$p; my $m=defined($opts{'x'}) ? "myipv6=$Address" : "myip=$Address"; if ( defined($Response=get( "https://api.dynu.com/nic/update?hostname=$h&$m&". "username=$x&password=$y")) ) { if ( ($Response=~m/^good/) || ($Response=~m/^nochg/) ) { $Good="Y"} } } if ( defined($Good) ) { $OldAddr{$h}=$Address; $ChangeTime{$h}=time(); syslog("info","$Mode ==> $Response") } else { $Status=1; syslog("info","$Mode Update failed!") } } } } else { syslog("info","$Mode Can't get current address from: $MyIp"); $Status=1; } if ( defined($opts{'m'}) ) { if ( defined($opts{'l'}) && ($Status==0) && blink($opts{'l'},60*$opts{'m'}) ) {} else {sleep(60*$opts{'m'})} } else { exit($Status) } } sub get { # Usage: get($url[,$Mode]); my ($handle,$string,$m); # if $Mode is supplied, only use that mode. open($handle,'>',\$string); my $curl=new WWW::Curl::Easy; if ( defined($_[1])) { if($_[1] eq "IPv4") {$m=1} else {$m=2}; $curl->setopt(CURLOPT_IPRESOLVE,$m) } $curl->setopt(CURLOPT_TIMEOUT,30); $curl->setopt(CURLOPT_FOLLOWLOCATION,1); $curl->setopt(CURLOPT_WRITEDATA,$handle); $curl->setopt(CURLOPT_URL,shift); $curl->perform(); return($string) } sub getMyAddress { # Usage: getMyAddress($MyIp,$Mode) if ( defined(my $Address=get($_[0],$_[1])) ) { $Address=~s/^\s+|\s+$//g; # Trim white-space and ipv4/6 tags $Address=~s/^\//; $Address=~s/\<\/ipv(4|6)\>$//; if ( $_[1] eq "IPv6" ) { if ( ip_is_ipv6($Address) ) { return(ip_compress_address($Address,6)) } } # Return consistent forms of addresses else { if ( ip_is_ipv4($Address) ) { return(ip_expand_address ($Address,4)) } } } return(0) } sub blink { # Usage: blink(led,secs); my $led=shift; my $secs=shift; # blinks 'led' for 'secs' seconds. if ( open(TRIGGER,">/sys/class/leds/$led/trigger") ) { print TRIGGER "none\n"; close(TRIGGER); my $bright=1; # Set trigger mode, determine max brightness if ( open(MAX,") { chomp; if ( $_=~/^\d+$/) {$bright=$_} } close(MAX) } for (my $k=0;$k<$secs;$k++) {# Alternate brightness value each pass open(BRIGHT, ">/sys/class/leds/$led/brightness"); print BRIGHT ($k%2)*$bright,"\n"; close(BRIGHT); sleep(1) } return(1) } return(0) } __END__ =head1 NAME DnsPixie - update client for popular dynamic DNS services =head1 README DnsPixie will update one or more designated DNS records at a number of popular dynamic DNS services either once or periodically. =head1 DESCRIPTION C is a simple update client for the FreeDNS, Dynu, DuckDNS, DNSExit and ChangeIP DNS services. It will attempt an immediate update when called, then optionally loop at designated intervals, attempting further updates when necessary. =head1 USAGE =over 2 DnsPixie [-x] [-d] [-v] [-m repeat-interval] [-l led] config-file =back e.g.: DnsPixie -m 15 /usr/local/etc/DnsPixie.conf The repeat-interval value (where present) must be expressed as a positive integer number of minutes. If the '-d' option is used, DnsPixie will daemonize itself on startup. If the '-x' option is included, DnsPixie will update IPv6 addresses instead of IPv4 addresses. The '-v' option can be used to provide a more verbose log of activity. The config-file value must be the name of a configuration file which contains one or more 'service hostname password' records where each password relates to the owner of the corresponding hostname. Each record should appear on a separate line as follows. FreeDNS daisy.moo.com ABC7PqRsTUvwXYEjLLM2R7ST8uvWX92AA3BbCz Dynu mickey.dynu.net myuser:mypassword DuckDNS donald.duckdns.org 064a0540-864c-4f0f-8bf5-23857452b0c1 ChangeIP huwey.changeip.net myuser:mypassword DNSExit dewey.linkpc.net 5AD3eABC4r91PQR1234ZPyy5lllRSz .. etc. Lines which begin in "#" will be ignored. It is suggested that the configuration file should be readable only by the intended program-user. You may want to run separate IPv4 and IPv6 instances of DnsPixie, using different configuration files. Some services will allow hostnames to have either IPv4 or IPv6 records; others (e.g. Dynu, DuckDNS) will allow hostnames to have both. I will be used for logging; you might want to check your system's syslog configuration so that you can view log messages. Additional detail will be logged if the '-v' option is used. To start DnsPixie on a Linux or BSD machine, you can insert into your I file something like the following: /usr/local/sbin/DnsPixie.pl -dvm 10 /usr/local/etc/DnsPixie.conf If your nameserver(s) are likely to change or become available after DnsPixie starts, you should ensure that Dnsmasq (or something similar) has been installed so that DnsPixie can always access a name service. On a Windows machine, you can start DnsPixie as a scheduled task. On Linux machines which have a system LED, you can include the '-l' option with a name like 'led0' (for the Raspberry Pi); DnsPixie will then attempt to blink the designated led during successful execution. You should not specify the same LED device for separate instances of DnsPixie. =head1 SCRIPT CATEGORIES Networking UNIX/System_administration =head1 AUTHOR Graham Jenkins =head1 COPYRIGHT Copyright (c) 2014 Graham Jenkins. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut