Self hosted Dynamic DNS server using PHP and BIND

Are you looking to setup your own Dynamic DNS server then this guide is for you. I use Debian Linux so this guide is based on Debian 12 at the time of this writing.

Domain registrar DNS records setup

First add a new DNS record for your dns server, for example dns.example.com AAAA 2001:0db8:a55b:42df:5d01:2359:a67e:737d or / and dns.example.com A 203.0.113.9 A/AAAA record with your server IP where you will serve your BIND9 DNS server.

Now create a NS record in your domain registrar, for example.
NS record dns.example.com pointing to dns.example.com

Server Setup

Install BIND9 DNS server apt install bind9 dnsutils

Now we start configuring BIND9 server.

/etc/bind/named.conf.local
//
// Do any local configuration here
//

// Consider adding the 1918 zones here, if they are not used in your
// organization
//include "/etc/bind/zones.rfc1918";

zone "dns.example.com" IN {
    type master;
    file "/var/lib/bind/dns.example.com.zone";
    allow-update { ::1; 127.0.0.1;};
    notify no;
};

Now configure named.conf.options file

/etc/bind/named.conf.options
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 {
	// 	0.0.0.0;
	// };

	//========================================================================
	// 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 auto;
	allow-transfer {none;};
	allow-notify {none;};
	allow-recursion {none;};
	allow-query-cache {none;};
	recursion no;
	minimal-any yes;
	minimal-responses yes;

	listen-on { 127.0.0.1; };
	listen-on-v6 { ::1; };
};

Now create a new zone file listed above (/var/lib/bind/dns.example.com.zone), replace the values accordingly.

Last line in this file must be a blank line.

/var/lib/bind/dns.example.com.zone
$ORIGIN .
$TTL 0	; 0 minutes
dns.example.com IN SOA dns.example.com. email.example.com. (
				100        ; serial
				3600       ; refresh (1 hour)
				3600       ; retry (1 hour)
				3600       ; expire (1 hour)
				0          ; minimum (0 seconds)
				)
$TTL 900	; 15 minutes
		IN	NS	dns.example.com.
; IP records for name servers
dns.example.com.             IN      AAAA       2001:0db8:a55b:42df:5d01:2359:a67e:737d
dns.example.com.             IN      A          203.0.113.9

Now check the zone file by running named-checkzone dns.example.com. /var/lib/bind/dns.example.com.zone

If you get no error after checking the zone file restart BIND server with your new settings systemctl restart bind9

PHP setup

Now we will use PHP script to update our dynamic DNS server. Create a new PHP file dns.php on your server with the content below.

https://example.com/dns.php
<?php
header('Content-Type: text/plain');

$hostname = $_GET['hostname'] ?? null;
$ipV4Address = $_GET['myip'] ?? null;
$ipV6Address = $_GET['myipv6'] ?? null;
$dnsKey = $_GET['password'] ?? null;
$action = $_GET['action'] ?? null;
// DNS Time to live in seconds
$dnsttl = $_GET['ttl'] ?? '300';

if (isset($dnsKey) && preg_match('/[^a-zA-Z0-9]/', $dnsKey))
{
	echo 'badauth';
	exit;
}

if (isset($hostname) && preg_match('/[^a-z0-9\.\-\_]/', $hostname))
{
	echo 'notfqdn';
	exit;
}

if (isset($ipV4Address) && preg_match('/[^0-9\.]/', $ipV4Address))
{
	echo 'badsys';
	exit;
}

if (isset($ipV6Address) && preg_match('/[^a-f0-9\:]/', $ipV6Address))
{
	echo 'badsys';
	exit;
}

// Credintials format: hostname (homevpn.dns.example.com), password (rUOrbg8R2RvVmOhcVoOGnIRIgWCY0W5x), name of DNS zone to update (dns.example.com)
$login = false;
$user_info=[
'homevpn.dns.example.com'=>['rUOrbg8R2RvVmOhcVoOGnIRIgWCY0W5x','dns.example.com'],
'plexserver.dns.example.com'=>['vxV4uh77QhKlUZV7woenHogCGTZbENBf','dns.example.com'],
'fileserver.dns.example.com'=>['kYwi06FqLXDI77IJpZtM9N8VqiALymPR','dns.example.com'],
'gameserver.dns.example.com'=>['53o8DkXYCDB54mcL5l5x5C8OVPk8XRXM','dns.example.com']
];
foreach ($user_info as $key => $value) {
	$login = ($key == $hostname && $value[0] == $dnsKey) ? true : false;
	if($login) break;
}
if(!$login) {
	echo 'badauth';
	exit;
}

if ($hostname == null) {
	header('HTTP/1.1 400 Bad Request');
	echo 'notfqdn';
	exit;
}

if ($action == 'delete') {
$descriptorspec = array(
	0 => array('pipe', 'r'),
	1 => array('pipe', 'w')
);
$process = proc_open('nsupdate', $descriptorspec, $pipes, NULL, NULL);
fwrite($pipes[0], "server 127.0.0.1\n");
fwrite($pipes[0], "zone $value[1]\n");
fwrite($pipes[0], "update delete $hostname.\n");
fwrite($pipes[0], "send\n");
fclose($pipes[0]);
echo stream_get_contents($pipes[1]);
proc_close($process);
echo 'good';
exit;
}

if (!$ipV4Address and !$ipV6Address) {
$ip = $_SERVER['REMOTE_ADDR'];
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
// Use client IP address if no IPv4 given
$ipV4Address = $ip;
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// Use client IP address if no IPv6 given
$ipV6Address = $ip;
}
}

// Check whether the given IPv4 address is valid
if ($ipV4Address and !filter_var($ipV4Address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
	header('HTTP/1.1 400 Bad Request');
	echo 'badsys';
	exit;
}

// Check whether the given IPv6 address is valid
if ($ipV6Address and !filter_var($ipV6Address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
	header('HTTP/1.1 400 Bad Request');
	echo 'badsys';
	exit;
}


if ($ipV4Address && $ipV6Address) {
	ipv4_update();
	ipv6_update();
}
elseif ($ipV6Address && !$ipV4Address) {
	ipv6_update();
	$descriptorspec = array(
		0 => array('pipe', 'r'),
							1 => array('pipe', 'w')
	);
	$process = proc_open('nsupdate', $descriptorspec, $pipes, NULL, NULL);
	fwrite($pipes[0], "server ::1\n");
	fwrite($pipes[0], "zone $value[1]\n");
	fwrite($pipes[0], "update delete $hostname. A\n");
	fwrite($pipes[0], "send\n");
	fclose($pipes[0]);
	echo stream_get_contents($pipes[1]);
	proc_close($process);
}
elseif ($ipV4Address && !$ipV6Address) {
	ipv4_update();
	$descriptorspec = array(
		0 => array('pipe', 'r'),
							1 => array('pipe', 'w')
	);
	$process = proc_open('nsupdate', $descriptorspec, $pipes, NULL, NULL);
	fwrite($pipes[0], "server ::1\n");
	fwrite($pipes[0], "zone $value[1]\n");
	fwrite($pipes[0], "update delete $hostname. AAAA\n");
	fwrite($pipes[0], "send\n");
	fclose($pipes[0]);
	echo stream_get_contents($pipes[1]);
	proc_close($process);
}

function ipv4_update()
{
global $hostname, $ipV4Address, $dnsttl, $value;
$descriptorspec = array(
	0 => array('pipe', 'r'),
	1 => array('pipe', 'w')
);
$process = proc_open('nsupdate', $descriptorspec, $pipes, NULL, NULL);
fwrite($pipes[0], "server 127.0.0.1\n");
fwrite($pipes[0], "zone $value[1]\n");
fwrite($pipes[0], "update delete $hostname. A\n");
fwrite($pipes[0], "update add $hostname. $dnsttl A $ipV4Address\n\n");
fclose($pipes[0]);
echo stream_get_contents($pipes[1]);
proc_close($process);
}

function ipv6_update()
{
global $hostname, $ipV6Address, $dnsttl, $value;
$descriptorspec = array(
	0 => array('pipe', 'r'),
	1 => array('pipe', 'w')
);
$process = proc_open('nsupdate', $descriptorspec, $pipes, NULL, NULL);
fwrite($pipes[0], "server 127.0.0.1\n");
fwrite($pipes[0], "zone $value[1]\n");
fwrite($pipes[0], "update delete $hostname. AAAA\n");
fwrite($pipes[0], "update add $hostname. $dnsttl AAAA $ipV6Address\n\n");
fclose($pipes[0]);
echo stream_get_contents($pipes[1]);
proc_close($process);
}

echo 'good';
?>

Examples to update the DNS server

Updating using the default IP used by the client
https://www.example.com/dns.php?password=rUOrbg8R2RvVmOhcVoOGnIRIgWCY0W5x&hostname=homevpn.dns.example.com
Updating by manually specifying IPv6 address
https://www.example.com/dns.php?password=rUOrbg8R2RvVmOhcVoOGnIRIgWCY0W5x&hostname=homevpn.dns.example.com&myipv6=fd22::2
Updating by manually specifying IPv4 address
https://www.example.com/dns.php?password=rUOrbg8R2RvVmOhcVoOGnIRIgWCY0W5x&hostname=homevpn.dns.example.com&myip=10.10.10.0
Updating my manually specifying both IPv6 and IPv4 address
https://www.example.com/dns.php?password=rUOrbg8R2RvVmOhcVoOGnIRIgWCY0W5x&hostname=homevpn.dns.example.com&myipv6=fd22::2&myip=10.10.10.0
Setting custom TTL (in seconds)
https://www.example.com/dns.php?password=rUOrbg8R2RvVmOhcVoOGnIRIgWCY0W5x&hostname=homevpn.dns.example.com&myipv6=fd22::2&myip=10.10.10.0&ttl=60
Deleting the hostname
https://www.example.com/dns.php?password=rUOrbg8R2RvVmOhcVoOGnIRIgWCY0W5x&hostname=homevpn.dns.example.com&action=delete

Let me know if you have any comments or if there is any error in this guide.