Dyndns api support for VestaCP [PHP] + client script [Python]

Section with additional software for Vesta
sq7lqw
Posts: 1
Joined: Tue Sep 06, 2016 8:17 pm

Dyndns api support for VestaCP [PHP] + client script [Python]

Postby sq7lqw » Tue Sep 06, 2016 11:19 pm

As this is my first post in this forum Id like to say Hi to everyone, and I also liked to say thanks to the authors of VestaCP panel.

I'd like to share a small simple but very usefull script which helped me change VestaCP into a dyndns server, where a client server can query a certain link from our VestaCP server and change chosen DNS records of a domain, whole records for a chosen domain, check its IP.

In order to achive this some backend script (1 file - php only) got to be placet in your VestaCP main www url.
For sake of this case, we will use example.com as a server address, %usr% and %pass% as user login and password.

Current behavior/limitations:
- user can list only domains/subdomain which he owns and are set up undr DNS section in VestaCP panel,
- updating mail subdomain will also change ip address under TXT record (two DNS entries at the same go)
(ip address will be extracted and replaced with new ip, no other modification will be done with TXT DNS entry)
- updating main domain entry will update all domain A records and TXT record
- once domain/subdomain is updated TTL time of the main domain will be reduced down to 300s [5m]
- changes are done only when new ip address is detected (will be different than in DNS entry)


I recomend two case scenario of usage,
First:
Detect IP address change on a dyndns client server and update when needed

Second:
Update your IP address every few minutes, only new ip address if detected will couse changes.

example use of the script in linux bash:

Case were we have no valid SSL certificates installed on server

Code: Select all

wget --no-check-certificate https://example.com:8083/api/dyndns/?do=ip


Case where domain certificate is valid and installed on server

Code: Select all

wget https://example.com:8083/api/dyndns/?do=ip


Full commands list is as follow:
Client IP address check

Code: Select all

https://example.com:8083/api/dyndns/?do=ip

Will simply give us an IP address from we are connecting

Code: Select all

1.1.1.1

All main domains listing

Code: Select all

https://example.com:8083/api/dyndns/?user=%usr%&pass=%pass%&do=list_domains

will answer us with a list of domains which are set under DNS section in VestaCP panel by our %usr%
An example:

Code: Select all

example.com/1.1.1.1
example.org/1.1.1.1
other.com/1.2.3.4

All main domains & subdomains

Code: Select all

https://example.com:8083/api/dyndns/?user=%usr%&pass=%pass%&do=list_subdomains

Will give us full list of domains, and all A records which are set under DNS in VestaCP panel by our %usr%
An example:

Code: Select all

example.com/1.1.1.1
@.example.com/1.1.1.1
mail.example.com/1.1.1.1
http://www.example.com/1.1.1.1
pop.example.com/1.1.1.1
ftp.example.com/1.1.1.1
example.org/1.1.1.1
@.example.org/1.1.1.1
mail.example.org/1.1.1.1
http://www.example.org/1.1.1.1
pop.example.org/1.1.1.1
ftp.example.org/1.1.1.1
other.com/1.2.3.4
@.other.com/1.2.3.4
mail.other.com/1.2.3.4
http://www.other.com/1.2.3.4
pop.other.com/1.2.3.4
ftp.other.com/1.2.3.4

Update a domain or subdomain with IP from which we are connecting

Code: Select all

https://example.com:8083/api/dyndns/?user=%usr%&pass=%pass%&do=update&domain=ftp.example.org

When main domain is used it will update all A records available under that domain, when subdomain is used it will update only that subdomain. It will be an IP address from which we are connecting

Update a domain or subdomain with selected IP

Code: Select all

https://example.com:8083/api/dyndns/?user=%usr%&pass=%pass%&do=update&domain=ftp.example.org&ip=3.3.3.3

When main domain is used it will update all A records available under that domain, when subdomain is used it will update only that subdomain. with an IP address specified under &ip=...



Possible all answers received from script

Code: Select all

OK - record updated and changes saved in name server
NOK - record already match with DNS entry, nothing to update
AUTH - incorrect user/password
DOMAIN_REQUIRED - there is no &domain=... specified
NOT_EXISTS - specified subdomain/domain was not found under choosen username
DO_UNKNOWN - unrecognized command specified under &do=...
DO_REQUIRED - there is no &do=...  specified



How/where to install
- create a directory named as dyndns in /usr/local/vesta/web/api
- create a file index.php in /usr/local/vesta/web/api/dyndns/index.php
- copy below code and save into this file:

Code: Select all

<?php
define('VESTA_CMD', '/usr/bin/sudo /usr/local/vesta/bin/');
$domains=array();
$domain_all=array();

function clear_spaces($txt)
{
   while(strpos($txt,'  ')!==false) $txt=str_replace('  ',' ',$txt);   
   return $txt;
}


function list_domains($v_user)
{
   GLOBAL $domains;
   if (count($domains)>=1) return $domains;
   exec(VESTA_CMD ."v-list-dns-domains ".$v_user,  $output, $auth_code);
   if (count($output)==2) return array();   
   $domains=array();
   unset($output[0]);
   unset($output[1]);
   foreach($output as $dom)
   {
      $dom=explode(' ',clear_spaces($dom));   
      $domains[$dom[0]]=$dom[0].'/'.$dom[1];
   }
   return $domains;
}

function domain_details($v_user,$domain)
{
   global $domain_all;
   if (isset($domain_all[$domain])) return $domain_all[$domain];
    exec(VESTA_CMD."v-list-dns-records ".$v_user." ".$domain." 'json'", $output, $return_var);
   $data = json_decode(implode('', $output), true);
   $domain_all[$domain]=array();
   if (count($data)<1) return array();   
   foreach($data as $id=>$sub) if ($sub['TYPE']=='A') $domain_all[$domain][$domain.'/'.$id]=$sub['RECORD'].".".$domain.'/'.$sub['VALUE'];
   return $domain_all[$domain];
}


function list_domain($v_user,$domain='all')
{
   $result=array();
      $domains=list_domains($v_user);
   if ($domain=='all') 
   {
      if (count($domains)<1) return false;
      foreach($domains as $dummy=>$dom)
      {
         $dom=explode('/',$dom);
         $result=array_merge($result,array($dom[0]=>$dom[0].'/'.$dom[1]),domain_details($v_user,$dom[0]));
      }
   } else
   {
      if (!isset($domains[$domain])) return false;
      $dom=$dom=explode('/',$domains[$domain]);
      $result=array_merge(array($dom[0]=>$dom[0].'/'.$dom[1]),domain_details($v_user,$domain));
   }
   return $result;   
}
   
function dnsid($v_user,$domain,$dns)
{
   $list=list_domain($v_user,$domain);
   if (count($list)<1) return 0;
   foreach ($list as $id=>$t)
   {
      list(,$id)=explode('/',$id);
      $id=(int)$id;
      list($t,)=explode('/',$t);
      $t=trim(str_replace('.'.$domain,'',$t));
      if ($dns==$t) return $id;
   }
   return 0;   
}


function dnstxtid($v_user,$domain)
{
   exec(VESTA_CMD ."v-list-dns-records ".$v_user." ".$domain,  $output, $auth_code);
   if (count($output)==1) return 0;
   unset($output[0]);
   unset($output[1]);
   foreach($output as $sub)
   {
      $sub=explode(' ',clear_spaces($sub));   
      if ($sub[2]=='TXT') return $sub[0];
   }
   return 0;
}

function getidvalue($v_user,$domain,$id)
{
    exec (VESTA_CMD."v-list-dns-records ".$v_user." ".$domain." 'json'", $output, $return_var);
   $data = json_decode(implode('', $output), true);
   if ($id=='all')
   {
      if (count($data)<1) return ''; else return $data;
   }
   else
   {
      if (!isset($data[$id])) return ''; else return $data[$id];
   }
}

function extract_txt($pattern,$t)
{
   if (!preg_match_all('/'.$pattern.'/',$t, $out, PREG_PATTERN_ORDER)) return '';
   return $out[1][0];
}


function update_txt($v_user,$domain,$ip,$named_restart=true)
{
   if ($named_restart) $reset=''; else $reset='no';
   $id=dnstxtid($v_user,$domain);
   if ($id==0)  return false;
   $old=getidvalue($v_user,$domain,$id);
   if ($old=='') return false;
   $old=$old['VALUE'];
   $old_ip=extract_txt('([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',$old);
   if ($old_ip=='') return false;
   if ($old_ip==$ip) return false;
   $old=str_replace($old_ip,$ip,$old);
   exec(VESTA_CMD ."v-change-dns-record '".$v_user."' '".$domain."' '".$id."' '".$old."' '".$reset."'",  $output, $auth_code);
   return true;
}


function update_domain($v_user,$domain,$v_ip_addr)
{
   global $named_restart;
   $named_restart=false;
   $change=false;
   $list=list_domain($v_user,$domain);   
   if (!isset($list[$domain])) die('NOT_EXISTS');
   list(,$d_ip)=explode('/',$list[$domain]);
   unset($list[$domain]);
   foreach($list as $id=>$ip)
   {
      list(,$id)=explode('/',$id);
      list(,$ip)=explode('/',$ip);
      if ($v_ip_addr!=$ip)
      {
         //updating all A entries
         exec(VESTA_CMD ."v-change-dns-record '".$v_user."' '".$domain."' '".$id."' '".$v_ip_addr."' 'no'",  $output, $auth_code);
         $change=true;
      }
   }
   if (update_txt($v_user,$domain,$v_ip_addr,false)) $change=true;
   if ($d_ip!=$v_ip_addr)
   {
      //updating main domain entry
      exec(VESTA_CMD ."v-change-dns-domain-ip '".$v_user."' '".$domain."' '".$v_ip_addr."' 'no'",  $output, $auth_code);
      $change=true;   
   }
   if ($change)
   {
      //if there were any changes made by now, we will queue a name server restart for refreshing and TTL reducing down  to 5 minutes - 300s
      exec(VESTA_CMD ."v-change-dns-domain-ttl '".$v_user."' '".$domain."' '300' 'no'",  $output, $auth_code);
      die('OK'); //let the client know that we have updated and saved the changes on our name server
   }
      else die('NOK'); //let the client know that all records are up to date and there are no changes needed
}


function update_subdomain($v_user,$domain,$val,$named_restart=false)
{
   if ($named_restart) $reset=''; else $reset='no';
   $list=list_domain($v_user);
   $domain_part=explode('.',$domain);
   $sub=array();
   foreach($domain_part as $id=>$part)
   {
      $sub[]=$domain_part[$id];
      unset($domain_part[$id]);
      $domain=implode('.',$domain_part);
      if (isset($list[$domain]))
      {
      $sub=@implode('.',$sub);   
      break;
      }
   }
   $id=dnsid($v_user,$domain,$sub);
   if ($id==0) die('NOT_EXISTS');
   $old=$list[$domain.'/'.$id];
   list(,$old)=explode('/',$old);
   $old=trim($old);
   if ($old!=$val)
   {
      exec(VESTA_CMD ."v-change-dns-record '".$v_user."' '".$domain."' '".$id."' '".$val."' '".$reset."'",  $output, $auth_code);
      exec(VESTA_CMD ."v-change-dns-domain-ttl '".$v_user."' '".$domain."' '300' 'no'",  $output, $auth_code);
      if ($sum=='mail') update_txt($v_user,$domain,$val,$named_restart);
      if ($named_restart) die ('OK');   
   } else
   {
      if ($named_restart) die('NOK');
   }
}

$v_ip_addr = $_SERVER["REMOTE_ADDR"];
$v_user=escapeshellarg($_GET['user']);
$v_password=escapeshellarg($_GET['pass']);
if (!isset($_GET['do'])) die('DO_REQUIRED');
if ($_GET['do']=='ip') die($_SERVER["REMOTE_ADDR"]);


exec(VESTA_CMD ."v-check-user-password ".$v_user." ".$v_password." '".$v_ip_addr."'",  $output, $auth_code);

if ($auth_code != 0 ) {
        echo 'AUTH';
        exit;
}

if ($_GET['do']==false) die('AUTH');
// used to be OK (as authorized ok, but decided to make it as athorizing error - dont want to create any backdors)
// when OK message in here,       it is possible to make bruteforce attach if login is compromised   

list_domain($v_user,'all'); // just loading all into variable that is kept in ram
if ($_GET['do']=='list_domains') $list=list_domains($v_user); // using variable loaded before
if ($_GET['do']=='list_subdomains') $list=list_domain($v_user,'all'); // using variable loaded before
if ($_GET['do']=='update')
{
   if (!isset($_GET['domain'])) die('DOMAIN_REQUIRED');
   $_GET['domain']=str_replace(' ','',$_GET['domain']);
   $list=list_domain($v_user);
   if (isset($_GET['ip'])) $v_ip_addr = str_replace("'",'',escapeshellarg($_GET['ip']));
   if (isset($list[$_GET['domain']])) update_domain($v_user,$_GET['domain'],$v_ip_addr);
   else update_subdomain($v_user,$_GET['domain'],$v_ip_addr,true);
   die('NOT_EXISTS');
}
if (count($list)<1) die('DO_UNKNOWN');
foreach ($list as $dom) echo $dom."\n";

exit;

?>



This script was created only for my private needs, I'm not responsible for any incorrect script usage, please be aware about changes that will take place when you use this script.





DynDNS client script written in python 2.7
Python script wrapper arround wget thanks to which our ip will be updated on VestaCP nameserver.

Functionality:
- multiple instances available - one for each domain (please do not try to update main domain, and subdomains separate as they will be overwritten)
- lock file usage - cannot have more than one client / domain active on same server
- selectable update interval
- for minimum VestaCP server usage IP addres is updated only when change is detected
- logging into /var/log/dyndns.log

Currently only linux support, wget installed is a must.

example usage:

Code: Select all

python dns_client.py vestaserver.com admin password subdomain.domainonvestaserver.com


example usage with /etc/rc.local:

Code: Select all

su - root -c "python /root/dns_client.py vestaserver.com admin password subdomain.domainonvestaserver.com" >/dev/null 2>&1 &


Code: Select all

#!/usr/bin/python


# DynDNS client for VestaCP dyndns "plugin" at https://forum.vestacp.com/viewtopic.php?f=19&t=12599
# Please use freely,

# usage: python file.py %server% %user% %password% %domain% [%refresh_time% - default 300s]
#
# example: python dns_client.py vestaserver.com admin password subdomain.domainonvestaserver.com 1000
# where:
#
# vestaserver.com - our VestaCP server address
# admin - user
# password - password
# subdomain.domainonvestaserver.com - (sub)domain which we want to update, must be already delegated to ns1.vestaserver.com
# 1000 - interval time (in seconds) how often our IP change will be checked and updated if needed, optional parameter - default: 300s [5 minutes]


# by Dariusz - admin@box24.info

import os
import time
import commands
import re
import sys

#set to '' if no log is required
log_file='/var/log/dyndns.log' 

#please set VestaCP admin port, default: 8083
default_port=8083

#How often in seconds script will query an ip and update our domain, default: 300
update=300


#when set to true, every update interval "xxx.xxx.xxx.xxx  - no change detected." will be saved in log
log_ip_no_change_detected=False

def run_cmd(command):
   return commands.getoutput(command)

def write_file(f, txt):
   if os.path.isfile(f):
      f = open(f, 'a')
   else:
      f = open(f, 'w')
   f.write(txt)
   f.close()


def log(txt):
   txt='['+sys.argv[4]+'] '+txt
   if log_file!='':
      s='['+time.strftime("%d %b %Y %H:%M:%S", time.gmtime())+']'
      txt=s+' '+txt
      write_file(log_file, txt+"\n")
   print txt

def update_lock_file():
   f = open('/run/lock/'+sys.argv[4], 'w')
   f.write('1')
   f.close()

if len(sys.argv)<5:
   log_file=''
   error='usage: $ python '+sys.argv[0]+' %server% %user% %password% %domain% [%refresh_time% - default 300s]'
else:
   error=''


if len(sys.argv)==6:
   if int(sys.argv[5])>0:
      update=int(sys.argv[5])



if os.path.isfile('/run/lock/'+sys.argv[4]):
   if (time.time()-os.path.getctime('/run/lock/'+sys.argv[4]))<6:
      error='Domain "'+sys.argv[4]+'" is already being updated!!!'
      
      
#internally used variables - do not modifie
if error=='':
   domain_update='"https://'+sys.argv[1]+':'+str(default_port)+'/api/dyndns/?user='+sys.argv[2]+'&pass='+sys.argv[3]+'&do=update&domain='+sys.argv[4]+'"'
   ip_query='"https://'+sys.argv[1]+':'+str(default_port)+'/api/dyndns/?do=ip"'
   log('Update interval is set to '+str(update)+'s')

last_ip='0.0.0.0'
current_ip='0.0.0.0'
last_update=0
last=0
err_count=0
connected=False

while error=='':
   now=int(time.time());

   if last!=now:
      last=now
      update_lock_file()

   if ((now-last_update)>update):
      last_update=now
      response = run_cmd('wget -qO- -T 1 -t 1 --no-check-certificate '+ip_query)
      if re.search(ur'([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})', response ):
         if connected==False:
            connected=True
            log('Connected to Host "'+sys.argv[1]+'" at port '+str(default_port))
         if response!=current_ip:
            if current_ip=='0.0.0.0':
               log('Ip detected: '+response)
            else:
               log('Ip change detected, New: '+response+', Old: '+current_ip)
            current_ip=response
            if current_ip!=last_ip:
               last_ip=current_ip
               response = run_cmd('wget -qO- -T 5 -t 1 --no-check-certificate '+domain_update)
               if response=='NOK':
                  log(current_ip+' - ip is already assign with this domain - nothing to update.')
               if response=='OK':
                  log(current_ip+' - updated.')
               if response=='AUTH':
                  error='Authorization error - please verify your user / password.'
               if response=='NOT_EXISTS':
                  error='Domain was not found on server under following username '+sys.argv[2]
         else:
            if log_ip_no_change_detected==True:
               log(current_ip+' - no change detected.')
      else:
         if connected==True:
            connected=False
         err_count+=1
         log('['+str(err_count)+']Host "'+sys.argv[1]+'" unreachable at port '+str(default_port)+'... Next retry in '+str(update)+'s')
   time.sleep(0.5)
log(error)
log('Client terminated.')



EXTRA - changing VestaCP into a public available DNS server
This will allow changes to act much faster than usual DNS propagation delays.
Simply point your server as a primary nameserver where ever you need to have changes to happend in instant:
(usefull with IOT devices - where i use this setting - it also allows you to use your own type of domains, but only when your VestaCP is se as a primary DNS server on a client computer/device)

Paste following content into /etc/bind/named.conf.options and restart service/server:

Code: Select all

options {
   directory "/var/cache/bind";

   // If there is a firewall between you and nameservers you want
   // to talk to, you may need to fix the firewall to allow multiple
   // ports to talk.  See http://www.kb.cert.org/vuls/id/800113

   // If your ISP provided one or more IP addresses for stable
   // nameservers, you probably want to use them as forwarders. 
   // Uncomment the following block, and insert the addresses replacing
   // the all-0's placeholder.

    forwarders {
        8.8.8.8; //google main
       8.8.4.4; //google secondary
       209.244.0.3; //Level3 main
       209.244.0.4; //Level3 secondary
       64.6.64.6; //Verisign main
       64.6.65.6; //Verisign secondary
       84.200.69.80; //dns watch main
       84.200.70.40; //dns watch secondary
       8.26.56.26; // comodo secure dns main
       8.20.247.20; // comodo secure dns secondary
       208.67.222.222; //open dns home main
       208.67.220.220; //open dns home secondary
       156.154.70.1; //DNS advantage main
       156.154.71.1; //DNS advantage secondary
       199.85.126.10; // Norton connect safe main
       199.85.127.10; // Norton connect safe secondary
       81.218.119.11; // Green Team main
       209.88.198.133; // Green Team secondary
       195.46.39.39; // Safe dns main
       195.46.39.40; // Safe dns secondary
       162.211.64.20; //OpenNIC main
       199.195.249.174; // OpenNIC secondary
       208.76.50.50; // SmartViper main
       208.76.51.51; //SmartViper secondary
       216.146.35.35; //dyn main
       216.146.36.36; //dyn secondary
       37.235.1.174; //FreeDNS main
       37.235.1.177; //FreeDNS secondary
       198.101.242.72; //Alternate DNS main
       23.253.163.53; //Alternate DNS secondary
       77.88.8.8; //Yandex main
       77.88.8.1; //Yandex secondary
       91.239.100.100; // censufridns main
       89.233.43.71; //censufridns secondary
       74.82.42.42; // Huricane electric
       109.69.8.51; // PuntCat
    };

   //========================================================================
   // If BIND logs error messages about the root key being expired,
   // you will need to update your keys.  See https://www.isc.org/bind-keys
   //========================================================================
   dnssec-validation no;
   recursion yes;
    allow-recursion { any; };
   //allow-query { any; };
   auth-nxdomain no;    # conform to RFC1035
   //listen-v6 { any; };
};



Sorry for any potential English mistakes.
I'm native Polish.

Have fun.

Cheers
Darek


EDIT:
Added DynDNS python script for linux client

ThA-LaN-LaW
Posts: 12
Joined: Mon Jan 25, 2016 2:25 pm

Re: Dyndns api support for VestaCP [PHP] + client script [Python]

Postby ThA-LaN-LaW » Fri Sep 16, 2016 11:01 am

Thanks Darek! Nice work!

i have to improvement suggestion for rainy sundays:
- fail2ban extension (block to many failure logins)
- Log File on Server (who updated, when, what)

Best Regards!

skurudo
VestaCP Team
Posts: 7798
Joined: Fri Dec 26, 2014 2:23 pm
Location: Moscow
Contact:

Re: Dyndns api support for VestaCP [PHP] + client script [Python]

Postby skurudo » Wed Sep 28, 2016 2:09 pm

Topic it "sticky" now.
Thanks a lot for this solution.
-> DigitalOcean competition - please, support us
-> fix for phpmyadmin - nice and sweet now


Return to “3rd Party Software”



Who is online

Users browsing this forum: No registered users and 5 guests