Exploring DNS records using PHP

Camilo Herrera
winkhosting
Published in
13 min readDec 4, 2023

--

Photo by Alessandro Erbetta on Unsplash

Hello! The year is almost over, how are you doing with those goals you set? If you haven’t achieved them, be patient, rest, but don’t stop.

This will be one of the last posts of the year, and I want to propose to you this adventure of enlightenment and personal growth to analyze DNS records of a domain using PHP. This is something we do very often in our company, and we decided it would be very useful to have our own tool for these queries.

On the Internet, there are many tools to check all kinds of DNS zone records, with advanced functions and several years of operation. Our proposed solution will be inspired by a service that, apparently, has been abandoned but is still functional, intodns.

You can also visit the following sites which have very complete tools:

https://dnschecker.org/ This tool is very useful for determining the propagation status of a change in DNS records.

https://mxtoolbox.com/SuperTool.aspx This tool allows you to query all kinds of zone records and provides suggestions in case it finds errors.

Records to be queried

For our guide, we will query the following types of DNS records for a domain:

  1. A Records: This is the fundamental type of DNS record and it maps the domain address to a physical IP (IPv4), the equivalent for IPv6 is the AAAA record.
  2. MX Records: This record is used for mail exchange, meaning it indicates to mail servers where to deliver messages directed to mailboxes associated with the domain.
  3. NS (Nameserver) Records: This type of record is essential, containing the address of the server or servers authorized to manage the DNS zone records (MX, A, CNAME, PTR, SOA, etc.)
  4. PTR Records (Reverse DNS lookups): This record allows resolving a domain name from an IP, i.e., handling reverse IP to name resolution requests, usually used for email validation and to determine if a message’s originating server really is who it claims to be.
  5. SOA Records: “Start of Authority”, it contains basic information about the domain or associated DNS zone, including the administrator’s email address, last update, and refresh times between changes.
  6. CNAME Records: “Canonical Name”, it acts as an “alias” for a real domain name (A record) or a canonical domain (another CNAME record), for example, it’s common for a domain’s www. address to be a CNAME and point to the domain’s A record.

To query these records, we will use PHP’s integrated functionalities, which include the dns_get_record function that allows querying the listed record types.

Warning: dns_get_records allows for basic DNS queries. Keep in mind that if you require more advanced operations, such as querying a specific DNS server or debugging information, this would not be the best option and you are going to feel the pain!

Now, let’s implement our solution.

Requirements

The requirements for this project are as follows:

  1. A development environment for PHP 8.2+ with its respective web server
  2. Your favorite code editor
  3. A cup of coffee
  4. And that’s all. If you were expecting something more complex, go study about K8s (Kubernetes)

Our Scripts

In the root of our public directory of the web server, we will create a directory named “dns-scout”. Inside this directory, we will create the following files:

  1. index.html: Responsible for the GUI of our project
  2. requestmanager.php: This script will handle receiving the domain query request and returning the results in JSON format
  3. Scout.php: Our file that will contain the definition of the class responsible for DNS query operations.

The structure of the directory and files will be as follows:

Files

Now let’s go through each of the scripts to understand their source code and functions.

Scout.php

This class has eight methods, six of which are responsible for querying the types of DNS records listed in the initial part of this article (A, MX, NS, PTR, CNAME, and SOA), and two more functions, one which will be our starting point of execution “scoutDomain()”, and “invertIPv4()”, which is responsible for inverting an IP to perform PTR record queries.

The content of the file is as follows:

<?php

/**
* Class Scout
*/
class Scout
{
/**
* @var array For storing result.
*/
private array $result;

/**
* @var string PTR suffix.
*/
private string $PTRsuffix;

/**
* @var string CNAME Prefix.
*/
private string $CNAMEPreffix;

/**
* Constructing initial settings.
*/
public function __construct()
{
$this->result = array("dnsinfo" => array());
$this->PTRsuffix = ".in-addr.arpa";
$this->CNAMEPreffix = "www.";
}

/**
* Scout domain to get DNS details.
*
* @param string $domain
* @return array result
*/
public function scoutDomain(string $domain): array
{

$this->result["dnsinfo"]["NS"] = $this->getNameservers($domain);
$this->result["dnsinfo"]["SOA"] = $this->getSoaRecord($domain);
$this->result["dnsinfo"]["MX"] = $this->getMxRecords($domain);
$this->result["dnsinfo"]["A"] = $this->getARecord($domain);
$this->result["dnsinfo"]["WWW"] = $this->getCnameRecord($this->CNAMEPreffix . $domain);

foreach ($this->result["dnsinfo"]["MX"] as $mxReg) {

$aRecords = $this->getARecord($mxReg["target"]);

foreach ($aRecords as $aRec) {
$invertedIP = $this->invertIPv4($aRec["ip"]);
$this->result["dnsinfo"]["PTR"][] = $this->getPtrRecord($invertedIP . $this->PTRsuffix);
}
}

return $this->result;
}

/**
* Get Nameservers of a domain.
*
* @param string $domain
* @return array $nameservers
*/
private function getNameservers(string $domain)
{
$nameservers = array();
try {

$ns_records = dns_get_record($domain, DNS_NS);

if ($ns_records === false || count($ns_records) == 0) {
$this->result["errors"][] = "Failed to retrieve nameservers for the domain $domain.";
}

$nameservers = [];

foreach ($ns_records as $record) {

$ip_records = dns_get_record($record['target'], DNS_A);

if ($ip_records !== false && count($ip_records) > 0) {
$nameservers[] = [
'nameserver' => $record['target'],
'ipv4' => $ip_records[0]['ip']
];
} else {
$nameservers[] = [
'nameserver' => $record['target'],
'ipv4' => null
];
}
}
} catch (Exception $e) {
$this->result["errors"][] = "An error occurred: {$e->getMessage()}";
} finally {
return $nameservers;
}
}

/**
* Get the SOA record of a domain.
*
* @param string $domain
* @return array SOA record data
*/
private function getSoaRecord(string $domain): array
{
$result = array();

try {
$result = dns_get_record($domain, DNS_SOA);
if (!$result) {
$this->result["errors"][] = "Failed to retrieve SOA record for the domain $domain.";
}
} catch (Throwable $e) {
$this->result["errors"][] = "An error occurred: {$e->getMessage()}";
} finally {
return $result;
}
}

/**
* Get the MX records of a domain.
*
* @param string $domain
* @return array $records
*/
private function getMxRecords(string $domain): array
{
$records = array();

try {
$records = dns_get_record($domain, DNS_MX);
if (!$records) {
$this->result["errors"][] = "Failed to retrieve MX records for: $domain. If you see this message PTR records won't be shown.";
}
} catch (Throwable $e) {
$this->result["errors"][] = "An error occurred: {$e->getMessage()}";
} finally {
return $records;
}
}

/**
* Get PTR record of an IP.
*
* @param string $ip
* @return array $result
*/
private function getPtrRecord(string $ip): array
{
$result = array();
try {
$result = dns_get_record($ip, DNS_PTR);
if (!$result) {
$this->result["errors"][] = "Failed to retrieve PTR record for the IP $ip.";
}
} catch (Throwable $e) {
$this->result["errors"][] = "An error occurred: {$e->getMessage()}";
} finally {
return $result[0];
}
}

/**
* Get A record of a domain.
*
* @param string $domain
* @return array $result
*/
private function getARecord(string $domain): array
{
$result = array();
try {
$result = dns_get_record($domain, DNS_A);
if (!$result) {
$this->result["errors"][] = "Failed to retrieve A record for the domain $domain.";
}
} catch (Throwable $e) {
$this->result["errors"][] = "An error occurred: {$e->getMessage()}";
} finally {
return $result;
}
}

/**
* Get CNAME record of a domain.
*
* @param string $domain
* @return array $result
*/
private function getCnameRecord(string $domain): array
{
try {
$result = dns_get_record($domain, DNS_CNAME);

if (!$result) {
$this->result["errors"][] = "Failed to retrieve CNAME record for the domain $domain.";
}
} catch (Throwable $e) {
$this->result["errors"][] = "An error occurred: {$e->getMessage()}";
} finally {
return $result;
}
}

/**
* Reverse given IP address.
*
* @param string $ip
* @return string $result
*/
function invertIPv4(string $ip): string
{
$parts = explode(".", $ip);

// reverse the array
$parts = array_reverse($parts);

$result = implode(".", $parts);

return $result;
}
}

requestmanager.php

The “requestmanager.php” file will be responsible for executing DNS queries and returning their results.

Let's explore how it is structured:

// The class to be used in our query is included.
require("Scout.php");

// We decode the parameters received from the index.html file and store them in the $paramsFetch array
$paramsFetch = json_decode(
file_get_contents("php://input"),
true
);

// We create an instance of our class
$scout = new Scout();
// We send the domain name to retrieve its DNS records
$result = $scout->scoutDomain($paramsFetch["domain"]);

// Here we return the response in JSON format and end the execution.
$jsonResponse = json_encode($result);
echo $jsonResponse;
exit;

In it, we include the Scout class, also receive the query parameters through a fetch request from JavaScript in index.html, then create an instance of the class and make a call to the “scoutDomain()” method, sending the received domain name as a parameter.

Once the request is processed, we return the result in JSON format.

Now let’s go to the GUI file, index.html.

index.html

Our HTML interface will look like the following image:

DNS Scout GUI

The file content is as follows:

<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DNS Scout</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<script type="module">
window.addEventListener('load', (event) => {

document.querySelector(".search").addEventListener('click', (event) => {

event.currentTarget.classList.add('is-loading');
event.currentTarget.disabled = true;

document.querySelector(".result").parentElement.classList.add("is-hidden");
document.querySelector(".error").parentElement.classList.add("is-hidden");

const payload = JSON.stringify({
"domain": document.querySelector(".domain").value
});

fetch('requestmanager.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: payload,
})
.then(response => response.json())
.then(data => {

console.log(data.errors);
if (data.errors != undefined) {
document.querySelector(".error").parentElement.classList.remove("is-hidden");
document.querySelector(".error").innerText = "";

for (const item in data.errors) {
document.querySelector(".error").innerText += data.errors[item] + "\n";
}

}

document.querySelector(".result").parentElement.classList.remove("is-hidden");
//document.querySelector(".result").innerText = data.dnsinfo;

document.querySelector(".a-records").innerHTML = "";
document.querySelector(".mx-records").innerHTML = "";
document.querySelector(".ns-records").innerHTML = "";
document.querySelector(".ptr-records").innerHTML = "";
document.querySelector(".soa-records").innerHTML = "";
document.querySelector(".www-records").innerHTML = "";

for (const record in data.dnsinfo.A) {

document.querySelector(".a-records").innerHTML += `
<div class="columns is-mobile">
<div class="column pb-0 is-one-fifth">
<h5 class="title is-6">host:</h5>
</div>
<div class="column pb-0">
${data.dnsinfo.A[record].host}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">class:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.A[record].class}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">ttl:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.A[record].ttl}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">type:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.A[record].type}
</div>
</div>
<div class="columns is-mobile">
<div class="column pt-1 is-one-fifth">
<h5 class="title is-6">ip:</h5>
</div>
<div class="column pt-1">
${data.dnsinfo.A[record].ip}
</div>
</div>`;
}

for (const record in data.dnsinfo.MX) {

document.querySelector(".mx-records").innerHTML += `
<div class="columns is-mobile">
<div class="column pb-0 is-one-fifth">
<h5 class="title is-6">host:</h5>
</div>
<div class="column pb-0">
${data.dnsinfo.MX[record].host}
</div>

</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">priority:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.MX[record].pri}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">target:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.MX[record].target}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">ttl:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.MX[record].ttl}
</div>
</div>
<div class="columns is-mobile">
<div class="column pt-1 is-one-fifth">
<h5 class="title is-6">type:</h5>
</div>
<div class="column pt-1">
${data.dnsinfo.MX[record].type}
</div>
</div>`;
}

for (const record in data.dnsinfo.NS) {

document.querySelector(".ns-records").innerHTML += `
<div class="columns is-mobile">
<div class="column pb-0 is-one-fifth">
<h5 class="title is-6">nameserver:</h5>
</div>
<div class="column pb-0">
${data.dnsinfo.NS[record].nameserver}
</div>

</div>
<div class="columns is-mobile">
<div class="column pt-1 is-one-fifth">
<h5 class="title is-6">ipv4:</h5>
</div>
<div class="column pt-1">
${data.dnsinfo.NS[record].ipv4}
</div>
</div>`;
}


for (const record in data.dnsinfo.PTR) {

document.querySelector(".ptr-records").innerHTML += `
<div class="columns is-mobile">
<div class="column is-one-fifth pb-0">
<h5 class="title is-6">host:</h5>
</div>
<div class="column pb-0">
${data.dnsinfo.PTR[record].host}
</div>

</div>
<div class="columns is-mobile">
<div class="column is-one-fifth pb-0 pt-1">
<h5 class="title is-6">class:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.PTR[record].class}
</div>
</div>
<div class="columns is-mobile">
<div class="column is-one-fifth pb-0 pt-1">
<h5 class="title is-6">target:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.PTR[record].target}
</div>
</div>
<div class="columns is-mobile">
<div class="column is-one-fifth pb-0 pt-1">
<h5 class="title is-6">ttl:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.PTR[record].ttl}
</div>
</div>
<div class="columns is-mobile">
<div class="column is-one-fifth pt-1">
<h5 class="title is-6">type:</h5>
</div>
<div class="column pt-1">
${data.dnsinfo.PTR[record].type}
</div>
</div>`;
}


for (const record in data.dnsinfo.SOA) {

document.querySelector(".soa-records").innerHTML += `
<div class="columns is-mobile">
<div class="column pb-0 is-one-fifth">
<h5 class="title is-6">host:</h5>
</div>
<div class="column pb-0">
${data.dnsinfo.SOA[record].host}
</div>

</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">class:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.SOA[record].class}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">expire:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.SOA[record].expire}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">minimum-ttl:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.SOA[record]["minimum-ttl"]}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">mname:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.SOA[record].mname}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">refresh:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.SOA[record].refresh}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">retry:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.SOA[record].retry}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">rname:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.SOA[record].rname}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">serial:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.SOA[record].serial}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">ttl:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.SOA[record].ttl}
</div>
</div>
<div class="columns is-mobile">
<div class="column pt-1 is-one-fifth">
<h5 class="title is-6">type:</h5>
</div>
<div class="column pt-1">
${data.dnsinfo.SOA[record].type}
</div>
</div>`;
}


for (const record in data.dnsinfo.WWW) {

document.querySelector(".www-records").innerHTML += `
<div class="columns is-mobile">
<div class="column pb-0 is-one-fifth">
<h5 class="title is-6">host:</h5>
</div>
<div class="column pb-0">
${data.dnsinfo.WWW[record].host}
</div>

</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">class:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.WWW[record].class}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">target:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.WWW[record].target}
</div>
</div>
<div class="columns is-mobile">
<div class="column pb-0 pt-1 is-one-fifth">
<h5 class="title is-6">ttl:</h5>
</div>
<div class="column pb-0 pt-1">
${data.dnsinfo.WWW[record]["ttl"]}
</div>
</div>
<div class="columns is-mobile">
<div class="column pt-1 is-one-fifth">
<h5 class="title is-6">type:</h5>
</div>
<div class="column pt-1">
${data.dnsinfo.WWW[record].type}
</div>
</div>`;
}

})
.catch((error) => {
document.querySelector(".error").parentElement.classList.remove("is-hidden");
document.querySelector(".error").innerText = error;
console.error('Error:', error);
}).finally(() => {
document.querySelector(".search").classList.remove('is-loading');
document.querySelector(".search").disabled = false;
});
});

});
</script>
</head>

<body>
<section class="section">
<div class="container">
<div class="columns">
<div class="column">
<div class="columns">
<div class="column"></div>
<div class="column has-text-centered">
<h1 class="title">
DNS Scout
</h1>
</div>
<div class="column"></div>
</div>
<div class="columns">
<div class="column has-text-centered">
<div class="field is-grouped is-grouped-centered">
<div class="control">
<input class="input domain" type="text" placeholder="Domain">
</div>
<div class="control">
<button class="button is-info search">
Go!
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="columns is-hidden">
<div class="column result">

<div class="columns">
<div class="column">

<article class="message" style="height: 100%;">
<div class="message-header">
<p>A Records</p>
</div>
<div class="message-body">
<div class="columns">
<div class="column a-records">

</div>
</div>
</div>
</article>

</div>
<div class="column">

<article class="message" style="height: 100%;">
<div class="message-header">
<p>MX Records</p>
</div>
<div class="message-body">
<div class="columns">
<div class="column mx-records">

</div>
</div>
</div>
</article>

</div>

</div>

<div class="columns">
<div class="column">

<article class="message" style="height: 100%;">
<div class="message-header">
<p>NS Records</p>
</div>
<div class="message-body">
<div class="columns">
<div class="column ns-records">

</div>
</div>
</div>
</article>

</div>
<div class="column">

<article class="message" style="height: 100%;">
<div class="message-header">
<p>PTR Records</p>
</div>
<div class="message-body">
<div class="columns">
<div class="column ptr-records">

</div>
</div>
</div>
</article>

</div>

</div>

<div class="columns">
<div class="column">

<article class="message" style="height: 100%;">
<div class="message-header">
<p>SOA Records</p>
</div>
<div class="message-body">
<div class="columns">
<div class="column soa-records">

</div>
</div>
</div>
</article>

</div>
<div class="column">

<article class="message" style="height: 100%;">
<div class="message-header">
<p>WWW Records</p>
</div>
<div class="message-body">
<div class="columns">
<div class="column www-records">

</div>
</div>
</div>
</article>

</div>
</div>

</div>
</div>
<div class="columns box is-hidden">
<div class="column notification is-danger error has-text-centered">
</div>
</div>
</div>
</section>
</body>

</html>

As you can see, we make a fetch request via POST to the “requestmanager.php” file when clicking the “Go!” button on the form.

The returned response contains an associative array in the “data.dnsinfo” entry with the DNS records, using the following JSON structure:

{
"NS": [
{
"nameserver": "c.ns.instagram.com",
"ipv4": "185.89.218.12"
},
{
"nameserver": "d.ns.instagram.com",
"ipv4": "185.89.219.12"
},
{
"nameserver": "b.ns.instagram.com",
"ipv4": "129.134.31.12"
},
{
"nameserver": "a.ns.instagram.com",
"ipv4": "129.134.30.12"
}
],
"SOA": [
{
"host": "instagram.com",
"class": "IN",
"ttl": 1234,
"type": "SOA",
"mname": "a.ns.instagram.com",
"rname": "dns.facebook.com",
"serial": 1678373413,
"refresh": 14400,
"retry": 1800,
"expire": 604800,
"minimum-ttl": 3600
}
],
"MX": [
{
"host": "instagram.com",
"class": "IN",
"ttl": 1265,
"type": "MX",
"pri": 10,
"target": "mxb-00082601.gslb.pphosted.com"
},
{
"host": "instagram.com",
"class": "IN",
"ttl": 1265,
"type": "MX",
"pri": 10,
"target": "mxa-00082601.gslb.pphosted.com"
}
],
"A": [
{
"host": "instagram.com",
"class": "IN",
"ttl": 298,
"type": "A",
"ip": "157.240.6.174"
}
],
"WWW": [
{
"host": "www.instagram.com",
"class": "IN",
"ttl": 1916,
"type": "CNAME",
"target": "geo-p42.instagram.com"
}
],
"PTR": [
{
"host": "42.145.231.67.in-addr.arpa",
"class": "IN",
"ttl": 1297,
"type": "PTR",
"target": "mx0a-00082601.pphosted.com"
},
{
"host": "42.145.231.67.in-addr.arpa",
"class": "IN",
"ttl": 1297,
"type": "PTR",
"target": "mx0a-00082601.pphosted.com"
}
]
}

Additionally, an array is included in the position “data.errors” that contains error messages presented during the execution of the queries.

This response is processed by DNS record type and the elements are added to each section in the interface using template literals. Template literals are quite useful for injecting information into HTML elements from JavaScript. I recommend using them if you haven’t already.

And that’s all, remember that at Winkhosting.co we are much more than hosting.

I wish you a Merry Christmas, don’t forget to share your time with your loved ones and be thankful for every achievement you had this year. Live a life worth living!.

--

--