Page 1 of 1

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

Posted: Tue Sep 06, 2016 11:19 pm
by sq7lqw
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 serverShow

Code: Select all

wget --no-check-certificate https://example.com:8083/api/dyndns/?do=ip
Case where domain certificate is valid and installed on serverShow

Code: Select all

wget https://example.com:8083/api/dyndns/?do=ip
Full commands list is as follow:
Client IP address checkShow

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 listingShow

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 & subdomainsShow

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 connectingShow

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 IPShow

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 scriptShow

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 installShow
- 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.7Show
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 - [email protected]

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 serverShow
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

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

Posted: Fri Sep 16, 2016 11:01 am
by ThA-LaN-LaW
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!

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

Posted: Wed Sep 28, 2016 2:09 pm
by skurudo
Topic it "sticky" now.
Thanks a lot for this solution.