Configuring Bind/CentOS7 for Builtin DNS

I am trying to configure apiscp on CentOS7 with Bind as DNS Server.

I configured Bind on CentOS7 - all functional. I installed apiscp and Added Site with FQDN.

Do I have to modify apnscp…\Builtin\module.php for BuiltinDNS to work?

I continue to receive error:

Action failed

Cannot fetch zone information for `xxx.xxx.com': no master nameserver configured in config.ini
Non-authoritative for zone xxx.xxx.com

I wouldn’t recommend using BIND for DNS. It scales horribly and, from that I’ve seen on my C7 cluster, prone to randomly stop serving zones necessitating a restart.

PowerDNS with a MySQL or PostgreSQL backend is the way to do it. The “builtin” module was originally intended for my use with Hostineer. It still requires manually provisioning of zones on the master NS.

The surrogate I used for BIND is as follows,

<?php
declare(strict_types=1);
/**
 * Copyright (C) Apis Networks, Inc - All Rights Reserved.
 *
 * Unauthorized copying of this file, via any medium, is
 * strictly prohibited without consent. Any dissemination of
 * material herein is prohibited.
 *
 * For licensing inquiries email <licensing@apisnetworks.com>
 *
 * Written by Matt Saladna <matt@apisnetworks.com>, May 2017
 */

    /**
     * Class Dns_Module_Surrogate
     *
     * A named implementation for DNS
     */
    class Dns_Module_Surrogate extends Dns_Module
    {

        /** external MySQL connection to apnscp.domains */
        private static $domain_db;

		/**
		 * Legal DNS resource records permitted by provider
		 * A
		 * AAAA
		 * MX
		 * CNAME
		 * DNAME
		 * HINFO
		 * TXT
		 * NS
		 * SRV
		 *
		 * @var array
		 */
		protected static $permitted_records = array(
			'A',
			'AAAA',
			'CAA',
			'CNAME',
			'HINFO',
			'MX',
			'NAPTR',
			'NS',
			'SOA',
			'SRV',
			'TXT',
		);
	protected const HAS_CONTIGUOUS_LIMIT = true;

        private static function _connect_db()
        {
            if (null !== self::$domain_db) {
                @self::$domain_db->ping();
                return self::$domain_db;
            }
            $db = \MySQL::stub();
            if ((!$db->connect(DNS_DOMAIN_HOST, DNS_DOMAIN_USER, DNS_DOMAIN_PASSWORD, DNS_DOMAIN_DB) &&
                !$db->connect(DNS_DOMAIN_HOST_BACKUP, DNS_DOMAIN_USER_BACKUP, DNS_DOMAIN_PASSWORD_BACKUP, DNS_DOMAIN_DB)) ||
                !$db->select_db(DNS_DOMAIN_DB)
            ) {
                return error('Cannot connect to domain server at this time');
            }

            self::$domain_db = &$db;
            return $db;
        }

        private static function __parse($resp, $orig, $new = '', $nsCMD = '')
        {
            if (!$resp) {
                return true;
            }
            if (false !== strpos($resp, 'NXRRSET')) {
                return warn('Non-existent DNS record `' . $orig . "'");
            } else if (false !== strpos($resp, 'YXRRSET')) {
                return error('DNS record `' . $new . "' exists");
            } else if (false !== strpos($resp, 'NOTAUTH')) {
                return error("Non-authoritative response on zone `$orig'");
            } else if (false !== strpos($resp, 'REFUSED')) {
                return error("DNS server refused record `$orig'");
            } else if (false !== strpos($resp, 'invalid rdata format: not a valid number')) {
                if (false !== strpos($nsCMD, ' MX ')) {
                    return error('MX records must be of format {NUMERIC PRIORITY} {HOSTNAME}');
                } else {
                    return error('SRV records must be of format {NUMERIC PRIORITY} {WEIGHT} {PORT} {TARGET}');
                }
            } else if (false !== strpos($resp, ': unbalanced quotes')) {
                return error('missing opening or ending quotation (") mark');
            } else if (false !== strpos($resp, ': bad dotted quad')) {
                return error('DNS record must be of numeric format, e.g. 127.0.0.1');
            } else if (false !== strpos($resp, ': timed out') && $nsCMD) {
                usleep(5000);
                return self::__send($nsCMD) || fatal('cannot connect to DNS server');
            }
            Error_Reporter::report('Unknown DNS resp - ' . $resp);
            return error("DNS server refused record: `%s'", trim($resp));
        }

        private static function __send($cmd)
        {
            if (!self::MASTER_NAMESERVER) {
                return false;
            }
            $key = explode(':', self::$dns_key);
            $cmd = 'server ' . self::MASTER_NAMESERVER . "\n" .
                'key ' . $key[0] . ' ' . $key[1] . "\n" .
                $cmd . "\n" . 'send' . "\n";
            $file = tempnam('/tmp', 'dns');
            file_put_contents($file, $cmd, LOCK_EX);
            $status = Util_Process::exec('nsupdate ' . $file, array('mute_stderr' => true));
            unlink($file);
            usleep(5000);
            return $status;
        }


        /**
         * Lookup and compare nameservers for domain to host
         *
         * @param string $domain
         * @return bool
         */
        public function domain_uses_nameservers(string $domain): bool
        {
            $myns = [
                'ns1.apisnetworks.com',
                'ns2.apisnetworks.com',
                'ns1.hostineer.com',
                'ns2.hostineer.com'
            ];
            $ns = $this->get_authns_from_host($domain);
            if (null === $ns) {
                return false;
            }
            foreach ($ns as $n) {
                if (in_array($n, $myns, true)) {
                    return true;
                }
            }
            return parent::domain_uses_nameservers($domain);
        }

        /**
         * Query database for domain expiration
         *
         * A return of 0 indicates failure
         * null indicates unknown expiration
         *
         * @param string $domain domain owned by the account
         * @return int expiration as unix timestamp
         */
        public function domain_expiration(string $domain): ?int
        {
            if (!self::_connect_db()) {
                return 0;
            }
            if (!$this->owned_zone($domain)) {
                error($domain . ': not owned by account');
                return null;
            }

            $q = self::$domain_db->query("
				SELECT
					UNIX_TIMESTAMP(domain_expire) as expiration
				FROM
					domain_information
				WHERE
					domain = '" . self::$domain_db->escape_string($domain) . "'
					AND
					site_id = " . $this->site_id);
            if ($q->num_rows < 1) {
                return null;
            }

            return (int)$q->fetch_object()->expiration;
        }

        /**
         * Fetches all domains across all servers
         *
         * @return array
         */
        public function get_all_domains(): array
        {
            self::_connect_db();
            $q = self::$domain_db->query('SELECT domain FROM domain_information ORDER BY domain');
            $domains = array();
            while ($row = $q->fetch_object()) {
                $domains[] = $row->domain;
            }
            return $domains;

        }

	    /**
	     * Get server on which a domain is hosted
	     *
	     * @param string $domain
	     * @param bool   $all show all server matches
	     * @param        mixed  when $all = true: array of all servers, else server
	     * @return array|string
	     */
        public function get_server_from_domain(string $domain, bool $all = false)
        {
            self::_connect_db();
            $rs = self::$domain_db->query("SELECT server_name FROM domain_information WHERE domain = '" .
                self::$domain_db->escape_string($domain) . "'");
            $servers = array();
            while ($row = $rs->fetch_object()) {
                $servers[] = $row->server_name;
            }
            if (!$all && count($servers) > 1) {
                warn("domain `%s' present on `%d' servers",
                    $domain, count($servers));
            }
            if ($all) {
                return $servers;
            }
            return array_pop($servers);
        }

        /**
         *  Get primary domain affiliated with account
         *
         * @param string $domain
         * @return bool|string
         */
        public function get_parent_domain(string $domain)
        {
            self::_connect_db();
            $db = self::$domain_db;
            $domain = strtolower($domain);

            $stmt = $db->stmt_init();
            $stmt->prepare('SELECT COALESCE(parent_domain,domain) AS domain ' .
	            'FROM domain_information WHERE domain = ?');
            $stmt->bind_param('s', $domain);
            $rs = $stmt->execute();
            $stmt->store_result();
            if (!$rs || $stmt->num_rows < 1) {
                return false;
            }
            $parent = null;
            $stmt->bind_result($parent);
            $stmt->fetch();

            return $parent;
        }

        /**
         * Get DNS record(s)
         *
         * @param string $subdomain optional subdomain
         * @param string $rr        optional RR type
         * @param string $domain    optional domain
         * @return array|bool
         */
        public function get_records(?string $subdomain = '', string $rr = 'any', string $domain = null)
        {
            if (!$domain) {
                $domain = $this->domain;
            }
            if (!$this->owned_zone($domain)) {
                return error('cannot view DNS information for unaffiliated domain `' . $domain . "'");
            }
            $recs = $this->get_records_raw($subdomain, $rr, $domain);
            return (array)$recs;

        }

        /**
         * bool remove_record (string, string)
         * Removes a record from a zone.
         *
         * @param  string $zone      base domain
         * @param  string $subdomain subdomain, leave blank for base domain
         * @param  string $rr        resource record type, possible values:
         *                           [MX, TXT, A, AAAA, NS, CNAME, DNAME, SRV]
         * @param  string $param     record context
         * @return bool operation completed successfully or not
         *
         */
        public function remove_record(string $zone, string $subdomain, string $rr, string $param = ''): bool
        {
            $subdomain = rtrim($subdomain, '.');
            if (!$zone) {
                $zone = $this->domain;
            }
            if (!$this->owned_zone($zone)) {
                return error($zone . ': not owned by account');
            }
	    if (!$this->canonicalizeRecord($zone, $subdomain, $rr, $param)) {
                return false;
            }
            $rr = strtoupper($rr);
            if ($subdomain === '@') {
                $subdomain = '';
                warn("record `@' alias for domain - record stripped");
            }

            if (substr($subdomain, -strlen($zone)) == $zone) {
                $subdomain = substr($subdomain, 0, -strlen($zone));
            }
            if (!in_array($rr, self::$permitted_records, true)) {
                return error("`$rr' invalid resource record type");
            }
            $record = rtrim($subdomain, '.');
            if ($record !== $zone) {
                $record = trim(preg_replace('/\.' . $zone . '$/', '', $record) . '.' . $zone, '.') . '.';
            }
            // only supply parameter if parameter is provided
            /*if ($rr === 'TXT' && $param) {
$param = str_replace(' "" ', '', $param);
                $param = '"' . implode('" "', str_split(trim($param, '"'), 253)) . '"';
                if ($param[0] !== '"' && $param[strlen($param) - 1] !== '"') {
                   // $param = '"' . str_replace('"', '\\"', $param) . '"';
                }
            }*/
//dd($param);
            $nsUpdateCmd = 'zone ' . $zone . ".\n" .
                'prereq yxrrset ' . $record . ' ' . $rr . "\n" .
                'update delete ' . $record . ' ' . $rr . (null !== $param ? (' ' . $param) : '');
            $status = self::__send($nsUpdateCmd);
            if (!$status['success']) {
                return self::__parse($status['stderr'], $subdomain, $subdomain, $nsUpdateCmd);
            }

            return true;
        }

        /**
         * Modify a DNS record
         *
         * @param string $zone
         * @param string $subdomain
         * @param string $rr
         * @param string $parameter
         * @param array  $newdata new zone data (name, rr, ttl, parameter)
         * @return bool
         */
        public function modify_record(string $zone, string $subdomain, string $rr, string $parameter, array $newdata): bool
        {
            if (!$this->owned_zone($zone)) {
                return error($zone . ': not owned by account');
            }
            $ttl = (int)($newdata['ttl'] ?? self::DNS_TTL);
	    if (!$this->canonicalizeRecord($zone, $subdomain, $rr, $parameter, $ttl)) {
                return false;
            }

            if ($subdomain === '@') {
                $subdomain = '';
                warn("record `@' alias for domain - record stripped");
            }
            $rr = strtoupper($rr);
            $newdata = array_merge(
                array(
                    'name'      => $subdomain,
                    'rr'        => $rr,
                    'ttl'       => null,
                    'parameter' => $parameter
                ),
                $newdata);
            $newdata['rr'] = strtoupper($newdata['rr']);
            if (!$newdata['name'] && $newdata['rr'] === 'cname') {
                return error('CNAME record cannot coexist with zone root, see RFC 1034 section 3.6.2');
            }
            if (!in_array($rr, self::$permitted_records, true)) {
                return error("`%s': invalid resource record type", $rr);
            } else if (!in_array($newdata['rr'], self::$permitted_records, true)) {
                return error("`%s': invalid resource record type", $newdata['rr']);
            }

            if (false !== strpos($newdata['name'], ' ')) {
                return error("DNS record `%s' may not contain spaces", $newdata['name']);
            }

            if ($subdomain !== $zone . '.') {
                $subdomain = ltrim(preg_replace('/\.' . $zone . '$/', '', rtrim($subdomain, '.')) . '.' . $zone . '.',
                    '.');
            }
            if ($newdata['name'] !== $zone . '.') {
                $newdata['name'] = ltrim(preg_replace('/\.' . $zone . '$/', '',
                        rtrim($newdata['name'], '.')) . '.' . $zone . '.', '.');
            }

            if ($newdata['rr'] === 'MX' && preg_match('/(\S+) ([0-9]+)$/', $newdata['parameter'], $mx_flip)) {
                // user entered MX record in reverse, e.g. mail.apisnetworks.com 10
                $newdata['parameter'] = $mx_flip[2] . ' ' . $mx_flip[1];
            }/*
            if ($newdata['rr'] === 'TXT') {
                $newdata['parameter'] = '"' . implode('" "', str_split(trim($newdata['parameter'], '"'), 253)) . '"';
            }*
            if ($rr === 'TXT') {
                $parameter = '"' . implode('" "', str_split(trim($parameter, '"'), 253)) . '"';
            }*/

            $rectmp = preg_replace('/\.?' . $zone . '\.?$/', '', $newdata['name']);
            if ($newdata['name'] !== $subdomain && $newdata['rr'] !== $rr &&
                $this->record_exists($zone, $rectmp, $newdata['rr'], $parameter)
            ) {
                return error('Target record `' . $newdata['name'] . "' exists");
            }
            $nsUpdateCmd = 'zone ' . $zone . "\n" .
                'prereq yxrrset ' . $subdomain . ' ' . $rr . "\n" .
                'update delete ' . $subdomain . ' ' . $rr . ' ' . $parameter . "\n" .
                'update add ' . $newdata['name'] . ' ' . ( $newdata['ttl'] ?? 1800 ) . ' ' . $newdata['rr'] . ' ' . $newdata['parameter'];
            $resp = self::__send($nsUpdateCmd);
            $parseresp = self::__parse(
                $resp['stderr'],
                $subdomain,
                $newdata['name'],
                $nsUpdateCmd
            );
            if (!$parseresp) {
                // nsUpdate failed, rollback records
                warn('record update failed');
                return ($subdomain == $newdata['name'] && $rr == $newdata['rr'] ||
                        !$this->record_exists($zone, $subdomain, $rr, $parameter)) &&
                    $this->record_exists($zone, $newdata['name'], $newdata['rr'], $newdata['parameter']);
            }
            return true;

        }

        /**
         * Add a DNS record to a domain
         *
         * @param string $zone      zone name (normally domain name)
         * @param string $subdomain name of the record to add
         * @param string $rr        resource record type [MX, A, AAAA, CNAME, NS, TXT, DNAME]
         * @param string $param     parameter value
         * @param int    $ttl       TTL value, default value 86400
         *
         * @return bool
         */
        public function add_record(string $zone, string $subdomain, string $rr, string $param, int $ttl = self::DNS_TTL): bool
        {
            if (false && is_debug()) {
                return info('not setting DNS record in development mode');
            }
            if (!$this->canonicalizeRecord($zone, $subdomain, $rr, $param, $ttl)) {
                return false;
            }

            if (!$this->owned_zone($zone)) {
                return error($zone . ' not owned by account');
            }

            $rr = strtoupper($rr);
            if ($subdomain === '@') {
                $subdomain = '';
                warn("record `@' alias for domain - record stripped");
            }

            if ($rr === 'cname' && !$subdomain) {
                return error('CNAME record cannot coexist with zone root, see RFC 1034 section 3.6.2');
            } else if ($rr === 'ns' && !$subdomain) {
                return error('Set nameserver records for zone root through domain registrar');
            }

            if (false !== ($pos = strpos($subdomain, ' '))) {
                return error('hostname must not contain any spaces');
            }
            if (substr($subdomain, -strlen($zone)) == $zone) {
                $subdomain = substr($subdomain, 0, -strlen($zone));
            }
            if (!in_array($rr, self::$permitted_records, true)) {
                return error($rr . ': invalid resource record type');
            }
            if ($subdomain !== $zone . '.') {
                $subdomain = ltrim(preg_replace('/\.' . $zone . '$/', '', rtrim($subdomain, '.')) . '.' . $zone . '.',
                    '.');
            }
            if ($rr === 'MX' && preg_match('/(\S+) ([0-9]+)$/', $param, $mx_flip)) {
                // user entered MX record in reverse, e.g. mail.apisnetworks.com 10
                $param = $mx_flip[2] . ' ' . $mx_flip[1];
            }/*
            if ($rr === 'TXT') {
                $param = '"' . implode('" "', str_split(trim($param, '"'), 253)) . '"';
            }*/
            // zones are defined by hostname
            // each base host already has a NS and SOA record defined

            $nsUpdateCmd = 'zone ' . $zone . "\n";

            $nsUpdateCmd .= 'update add ' . $subdomain . ' ' . $ttl . ' ' . $rr . ' ' . $param;
            $status = self::__send($nsUpdateCmd);
            if (!$status['success']) {
                return self::__parse(
                    $status['stderr'],
                    $subdomain,
                    $subdomain,
                    $nsUpdateCmd
                );
            }
            return true;

        }

        /**
         * Check whether a domain is hosted on any server
         *
         * @param string $domain
         * @param bool   $ignore_on_account domains hosted on account ignored
         * @return bool
         */
        public function domain_hosted(string $domain, bool $ignore_on_account = false): bool
        {
            $ignore_on_account = $ignore_on_account;

            $domain = strtolower($domain);
            if (substr($domain, 0, 4) === 'www.') {
                $domain = substr($domain, 4);
            }

            $db = self::_connect_db();
            if (!$db) {
            	return false;
            }
            $q = 'SELECT di_invoice, server_name, site_id FROM domain_information WHERE domain = ?';

            $stmt = $db->stmt_init();
            if (!$stmt->prepare($q)) {
            	return error('failed to query domain database server');
            }
            $invoice = $server = $site_id = null;
            $stmt->bind_param('s', $domain);
            $stmt->bind_result($invoice, $server, $site_id);
            $stmt->execute();
            $stmt->store_result();
            $stmt->fetch();
            $hosted = $stmt->num_rows > 0;
            $stmt->close();
            if (!$hosted) {
                return false;
            }
            if (!$ignore_on_account) {
                return $hosted;
            }

            return !$this->domain_on_account($domain);
        }

        public function domain_on_account(string $domain): bool
        {
            $db = self::_connect_db();
			if (!$db) {
				return false;
			}
            if (0 === strpos($domain, 'www.')) {
                $domain = substr($domain, 4);
            }

            $q = 'select IF(d.di_invoice != "", d.di_invoice,d2.di_invoice) AS invoice ' .
	            'FROM domain_information d LEFT JOIN domain_information d2 ON ' .
	            'd.parent_domain = d2.domain WHERE d.domain = ?';

            $stmt = $db->prepare($q);

            $invoice = null;
            $stmt->bind_param('s', $domain);
            $stmt->bind_result($invoice);
            if (!$stmt->execute()) {
            	\Error_Reporter::report($db->error);
            }
            $stmt->store_result();
            $stmt->fetch();
            $hosted = $stmt->num_rows > 0;
            $stmt->close();
            return $hosted && $invoice == coalesce($this->getConfig('billing', 'invoice'),
                    $this->getConfig('billing', 'parent_invoice'));
        }

        /**
         * Get recently expiring domains
         *
         * @param int  $days        lookahead n days
         * @param bool $showExpired show domains expired within the last 10 days
         *
         * @return array
         */
        public function get_pending_expirations(int $days = 30, bool $showExpired = true): array
        {
            $db = self::_connect_db();
            if (!$db) {
            	return [];
            }
            $days = intval($days);
            if ($days > 365 || $days < 1) {
                $days = 30;
            }
            $server = substr(SERVER_NAME, 0, strpos(SERVER_NAME, '.'));
            $q = 'SELECT domain, unix_timestamp(domain_expire) as expire ' .
	            'FROM domain_information WHERE ' .
                "site_id = '" . $this->site_id . "' AND server_name = '" . $server . "' AND " .
	            'domain_expire <= DATE_ADD(NOW(), INTERVAL ' . $days . ' DAY) AND domain_expire >= ' .
	            'DATE_SUB(NOW(), INTERVAL ' . ($showExpired ? '10' : '0') . ' DAY) ORDER BY domain_expire';

            $rs = $db->query($q);
            $domains = array();
            while (null !== ($row = $rs->fetch_object())) {
                $domains[] = array(
                    'domain' => $row->domain,
                    'ts'     => $row->expire
                );
            }
            return $domains;
        }

        public function remove_zone_backend(string $domain): bool
        {
            if (is_debug()) {
                return info("not removing zone `%s' in debug", $domain);
            }
            $nsCmd = 'zone apnscp.' . "\n" .
                'class IN' . "\n" .
                'update add delete.apnscp. ' . self::DNS_TTL . ' TXT ' . $domain;
            $resp = self::__send($nsCmd);
            if (!$resp['success']) {
                warn("Could not remove $domain - " . $resp['stderr']);
                return true;
            }
            info("Removed domain $domain");
            return true;
        }

        public function add_zone_backend(string $domain, string $ip): bool
        {
            /*if (is_debug()) {
                return info("not creating DNS zone for `%s' in development mode", $domain);
            } else */if (!self::MASTER_NAMESERVER) {
                return error("rndc not configured in conf/config.ini. Cannot add zone `%s'", $domain);
            }

            $buffer = Error_Reporter::flush_buffer();
            $res = parent::zoneAxfr($domain);
            if (null !== $res) {
                Error_Reporter::set_buffer($buffer);
                warn("DNS for zone `%s' already exists, not overwriting", $domain);
                return true;
            }
            // make sure DNS does not exist yet for the parent
            list($a, $b) = explode('.', $domain, 2);
            $res = parent::zoneAxfr($b);
            Error_Reporter::set_buffer($buffer);
            if (null !== $res) {
                warn("DNS for zone `%s' already exists, not overwriting", $b);
                return true;
            }

            if (is_array($ip)) {
                $ip = array_pop($ip);
            }

            if (ip2long($ip) === false) {
                return error("`%s': invalid address", $ip);
            }
            // nameserver zone replication service port
            $port = 25500;
            $msg = $err = null;
            $socket = fsockopen(self::MASTER_NAMESERVER, $port, $err, $msg);
            if (!$socket) {
                Error_Reporter::report("failed to add domain `%s' with `%s': (%x) %s",
                    $domain, $ip, $err, $msg);
            }
            $args = implode('|', array($domain, $ip));
            $len = fwrite($socket, $args);
            fclose($socket);
            if ($len < 1) {
                return error("error sending domain `%s' to nameserver", $domain);
            }
            info("Added domain `%s'", $domain);

            $revZone = \Opcenter\Dns\General::reverseIP($domain) . '.add.apnscp.';
            $nsCmd = 'zone apnscp.' . "\n" .
                'class IN' . "\n" .
                'update add add.apnscp. ' . self::DNS_TTL . ' TXT ' . $domain . "\n" .
                'prereq nxdomain ' . $revZone . "\n" .
                'update add ' . $revZone . ' ' . self::DNS_TTL . ' A ' . $ip;

            $resp = self::__send($nsCmd);
            if (!$resp['success']) {
                error("Could not add $domain - " . $resp['stderr']);
            }

            for ($i = 0 ; $i < 60; $i++) {
                if (null !== $this->zoneAxfr($domain)) {
                    break;
                }
                sleep(1);
            }
            return true;
        }

        /**
         * Release PTR assignment from an IP
         *
         * @param        $ip
         * @param string $domain confirm PTR rDNS matches domain
         * @return bool
         */
        protected function __deleteIP(string $ip, string $domain = null): bool
        {
            $rev = \Opcenter\Dns\General::reverseIP($ip);
            $node = substr($rev, 0, strpos($rev, '.'));
            $fqdn = $rev . '.in-addr.arpa.';
            if ($domain) {
                $domain = ' ' . rtrim($domain) . '.';
            }
            $cmd = 'prereq yxrrset ' . $fqdn . ' PTR' . $domain . "\n" .
                'update delete ' . $fqdn . ' PTR';
            $resp = self::__send($cmd);
            if (!is_debug() && !$resp['success']) {
                error("cannot release $ip - " . $resp[1]);
                return false;

            }
            info("Released $ip");
            return true;
        }

        /**
         * Change PTR name
         *
         * @param        $ip       IP address to alter
         * @param        $hostname new PTR name
         * @param string $chk      optional check hostname to verify
         * @return bool
         */
        protected function __changePTR(string $ip, string $hostname, string $chk = ''): bool
        {
            $rev = \Opcenter\Dns\General::reverseIP($ip);
            $fqdn = $rev . '.in-addr.arpa.';

            if ($chk) {
                $chk = ' ' . rtrim($chk, '.') . '.';
            }
            $cmd = 'prereq yxrrset ' . $fqdn . ' PTR' . $chk . "\n";
            'update delete ' . $fqdn . ' ' . self::DNS_TTL . ' PTR ' . "\n" .
            'update add ' . $fqdn . ' ' . self::DNS_TTL . ' PTR ' . $hostname;

            $resp = self::__send($cmd);
            if (!is_debug() && !$resp['success']) {
                error("cannot change PTR for $ip - " . $resp[1]);
                report($cmd, var_export($resp, true));
                return false;
            }
            // update ARP tables
            return true;
        }
public function t() {
return $this->__addIP('64.22.68.47', 'thesurlynerd.com');
}
        protected function __addIP(string $ip, string $hostname = ''): bool
        {
            $rev = \Opcenter\Dns\General::reverseIP($ip);
            $fqdn = $rev . '.in-addr.arpa.';

            if (!$hostname) {
                $hostname = $fqdn;
            } else {
                $hostname = rtrim($hostname, '.') . '.';
            }

            // no zone should be specificied
            $cmd = 'prereq nxrrset ' . $fqdn . ' PTR' . "\n" .
                'update add ' . $fqdn . ' ' . self::DNS_TTL . ' PTR ' . $hostname;
            $resp = self::__send($cmd);
            if (!$resp['success'] && !is_debug()) {
                error("cannot add $ip - " . $resp[1]);
                report($cmd, var_export($resp, true));
                return false;
            }
            // update ARP tables
            \Opcenter\Net\Ip4::announce($ip);
            info("Added $ip");
            return true;
        }
    }

Thank you for your assistance, I went with PowerDNS config.