Compare commits

..

7 Commits

Author SHA1 Message Date
d942833b06 Update with tech changes. 2023-05-12 15:55:55 +00:00
13d8b7f87e Update zone file generation
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-21 13:31:31 +02:00
5dd7a230fb Add more robust styling
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-21 12:11:24 +02:00
2d477bb77e Add content verification
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-21 11:22:40 +02:00
4b117a4743 Add support for longer records
This makes it possible to handle more content in a single round.

Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-21 11:17:50 +02:00
74619915ca Add default title to HTML
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-21 11:15:29 +02:00
727f2c671e Move zone to different NS
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
2021-08-21 11:14:41 +02:00
8 changed files with 1656 additions and 229 deletions

View File

@@ -22,7 +22,7 @@ fetch("https://cloudflare-dns.com/dns-query?ct=application/dns-json&type=TXT&nam
The content itself is served over DNS, using CoreDNS, with these contents:
```js
```hcl
hod.experiments.jacobkiers.net.:53 {
log
auto hod.experiments.jacobkiers.net. {
@@ -34,7 +34,7 @@ hod.experiments.jacobkiers.net.:53 {
This feeds into a zone file, which looks like this:
```js
```dns
$TTL 5m ; Default TTL
@ IN SOA experiments.jacobkiers.net. postmaster.jacobkiers.net. (
2021081612 ; serial
@@ -73,7 +73,7 @@ In short: just because I could. It was one of those ideas I was wondering idly a
### Has it any practical use?
It is not intended to have any. Since DNS records are fairly small, serving images or something would quickly start
consuming 100s of requests per second. I wouldn't want to do that to Cloudflare 😉
consuming 100s of requests per second. I wouldn't want to do that to Cloudflare.
It would be an interesting experiment to see how feasible that is.

View File

@@ -0,0 +1,21 @@
class Verifier {
/***
* @param content Content
*/
async verify(content) {
if (!window.isSecureContext) return;
if (typeof TextEncoder === "undefined") return;
if (typeof crypto === "undefined") return;
const encoder = new TextEncoder();
const digestBuffer = await crypto.subtle.digest(content.index.hashAlgorithm, encoder.encode(content.content));
const hashArray = Array.from(new Uint8Array(digestBuffer)); // convert buffer to byte array
const digest = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
if (content.index.hash === digest) {
document.getElementById("verification").innerHTML =
"The hash of the content is verified to correspond with the hash" +
" in the metadata. You are now reading exactly what was intended.";
}
}
}

View File

@@ -1,145 +0,0 @@
<!DOCTYPE html charset="UTF-8">
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.2.0/build/styles/default.min.css">
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.2.0/build/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
class Content {
content = "";
type = "";
metaData = {}
constructor(content, type, metaData = {})
{
this.content = content;
this.type = type;
this.metaData = metaData;
}
}
class Index
{
mimeType = "";
chunks = 0;
hash = "";
metaData = {};
constructor(mimeType, chunks, hash, metaData = {})
{
this.mimeType = mimeType;
this.chunks = chunks;
this.hash = hash;
this.metaData = metaData
}
}
const dohServer = "https://cloudflare-dns.com/dns-query?ct=application/dns-json&type=TXT&name=";
const baseDomain = "hod.experiments.jacobkiers.net";
async function readUrl(domain) {
var index = await fetchIndex(`${dohServer}${domain}.${baseDomain}`);
var chunk_promises = [];
for(i = 0; i < index.chunks; i++)
{
chunk_promises[i] = fetchChunk(i, index.hash);
}
var chunks = await Promise.all(chunk_promises);
var content = chunks.reduce((built, current) => built += current);
return handleContent(new Content(atob(content), index.mimeType, index.metaData));
}
async function fetchChunk(id, hash)
{
var domain = `${id}.${hash}.${baseDomain}`;
const json = await fetch(`${dohServer}${domain}`)
.then(response => response.json());
const data = json.Answer[0].data.slice(1, -1);
return data;
}
async function fetchIndex(domain)
{
const response = await fetch(`${dohServer}.${domain}`);
const json = await response.json();
const index = json.Answer[0].data.slice(1, -1);
let ret = {};
let items = index.split(';');
items.forEach(item => {
let md = item.split('=');
let key = md[0];
let value = md[1];
ret[key] = value;
});
const metadata = JSON.parse(atob(ret["m"]));
return new Index(ret["t"], ret["c"], ret["h"], metadata);
}
function handleContent(content)
{
if (!content instanceof Content) {
console.log("Not valid content in handleContent.")
return;
}
switch(content.type)
{
case "text/javascript":
return handleJavascript(content);
case "text/markdown":
return handleMarkdown(content);
default:
console.log(`handleContent() does not know how to parse ${content.type}`);
break;
}
}
function handleJavascript(content)
{
console.log("Got some javascript!", content.content);
}
async function handleMarkdown(content)
{
console.log("Got me some markdown!");
marked.setOptions({
highlight: function(code, lang) {
return hljs.highlight(lang, code).value;
},
// langPrefix: ''
});
if (content.metaData.title != undefined) document.title = content.metaData.title;
document.getElementById("post").innerHTML = marked(content.content);
let title = document.createElement("h1");
title.innerHTML = content.metaData.title;
document.getElementById("post").prepend(title)
document.getElementById("nojs").style.visibility = "hidden";
}
</script>
</head>
<body>
<script>
readUrl("posts-2021-08-17-serving-blog-content-over-dns-md");
</script>
<div id="post"></div>
<div id="nojs">
<h1>HTML over DNS</h1>
<p>The content of this page is fetched using DNS over HTTP. Since that requires Javascript, please enable that to see the content.</p>
</div>
</body>
</html>

7
public/equilibrium-light.min.css vendored Normal file
View File

@@ -0,0 +1,7 @@
/*!
Theme: Equilibrium Light
Author: Carlo Abelli
License: ~ MIT (or more permissive) [via base16-schemes-source]
Maintainer: @highlightjs/core-team
Version: 2021.05.0
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#43474e;background:#f5f0e7}.hljs ::selection{color:#d8d4cb}.hljs-comment{color:#73777f}.hljs-tag{color:#5a5f66}.hljs-operator,.hljs-punctuation,.hljs-subst{color:#43474e}.hljs-operator{opacity:.7}.hljs-bullet,.hljs-deletion,.hljs-name,.hljs-selector-tag,.hljs-template-variable,.hljs-variable{color:#d02023}.hljs-attr,.hljs-link,.hljs-literal,.hljs-number,.hljs-symbol,.hljs-variable.constant_{color:#bf3e05}.hljs-class .hljs-title,.hljs-title,.hljs-title.class_{color:#9d6f00}.hljs-strong{font-weight:700;color:#9d6f00}.hljs-addition,.hljs-code,.hljs-string,.hljs-title.class_.inherited__{color:#637200}.hljs-built_in,.hljs-doctag,.hljs-keyword.hljs-atrule,.hljs-quote,.hljs-regexp{color:#007a72}.hljs-attribute,.hljs-function .hljs-title,.hljs-section,.hljs-title.function_,.ruby .hljs-property{color:#0073b5}.diff .hljs-meta,.hljs-keyword,.hljs-template-tag,.hljs-type{color:#4e66b6}.hljs-emphasis{color:#4e66b6;font-style:italic}.hljs-meta,.hljs-meta .hljs-keyword,.hljs-meta .hljs-string{color:#c42775}.hljs-meta .hljs-keyword,.hljs-meta-keyword{font-weight:700}

1131
public/highlight.min.js vendored Normal file

File diff suppressed because one or more lines are too long

178
public/index.html Normal file
View File

@@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Blog over DNS</title>
<link rel="stylesheet" href="equilibrium-light.min.css">
<script src="highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
class Content {
content = "";
type = "";
metaData = {};
index;
/***
* @param content string
* @param type string
* @param index Index
*/
constructor(content, type, index)
{
this.content = content;
this.type = type;
this.index = index;
this.metaData = index.metaData;
}
}
class Index
{
mimeType = "";
chunks = 0;
hashAlgorithm = "";
hash = "";
metaData = {};
constructor(mimeType, chunks, hash, hashAlgorithm, metaData = {})
{
this.mimeType = mimeType;
this.chunks = chunks;
this.hashAlgorithm = hashAlgorithm;
this.hash = hash;
this.metaData = metaData
}
}
const dohServer = "https://cloudflare-dns.com/dns-query?ct=application/dns-json&type=TXT&name=";
const baseDomain = "hod.experiments.jacobkiers.net";
async function readUrl(domain) {
var index = await fetchIndex(`${domain}.${baseDomain}`);
var chunk_promises = [];
for(i = 0; i < index.chunks; i++)
{
chunk_promises[i] = fetchChunk(i, index.hash);
}
const chunks = await Promise.all(chunk_promises);
const base64 = chunks.reduce((built, current) => built += current);
const content = atob(base64);
return handleContent(new Content(content, index.mimeType, index));
}
async function fetchChunk(id, hash)
{
const domain = `${id}.${hash}.${baseDomain}`;
return await fetchData(domain);
}
async function fetchData(domain)
{
const json = await fetch(`${dohServer}${domain}`, {
method: "GET",
headers: {
"Accept": "application/dns-json"
}
})
.then(response => response.json());
const raw_data = json.Answer[0].data;
const data = raw_data.replaceAll(/[\s\"]/g, '');
return data;
}
async function fetchIndex(domain)
{
const index = await fetchData(domain);
let ret = {};
let items = index.split(';');
items.forEach(item => {
let md = item.split('=');
let key = md[0];
let value = md[1];
ret[key] = value;
});
const metadata = JSON.parse(atob(ret["m"]));
return new Index(ret["t"], ret["c"], ret["h"], ret["ha"], metadata);
}
function handleContent(content)
{
if (!content instanceof Content) {
console.log("Not valid content in handleContent.")
return;
}
switch(content.type)
{
case "text/javascript":
return handleJavascript(content);
case "text/markdown":
return handleMarkdown(content);
default:
console.log(`handleContent() does not know how to parse ${content.type}`);
break;
}
}
function handleJavascript(content)
{
console.log("Got some javascript!");
const scripts = document.getElementById("scripts");
const new_script = document.createElement("script");
new_script.text = content.content;
scripts.appendChild(new_script);
}
async function handleMarkdown(content)
{
console.log("Got me some markdown!");
marked.setOptions({
highlight: function(code, lang) {
const avialable_languages = hljs.listLanguages();
const has_language = hljs.getLanguage(lang);
if (typeof has_language === "undefined") return code;
const result = hljs.highlight(code, { language: lang, ignoreIllegals: true});
return result.value;
},
});
if (typeof content.metaData.title !== "undefined") document.title = content.metaData.title;
document.getElementById("post").innerHTML = marked.parse(content.content);
let title = document.createElement("h1");
title.innerHTML = content.metaData.title;
document.getElementById("post").prepend(title)
document.getElementById("nojs").remove();
if (typeof Verifier !== "undefined") {
await (new Verifier()).verify(content);
}
}
</script>
</head>
<body>
<div id="post"></div>
<div id="verification"></div>
<div id="nojs">
<h1>HTML over DNS</h1>
<p>The content of this page is fetched using DNS over HTTP. Since that requires Javascript, please enable that to see the content.</p>
</div>
<div id="scripts">
<script>
readUrl("scripts-verifier-js").then(
() => readUrl("posts-2021-08-17-serving-blog-content-over-dns-md")
);
</script>
</div>
</body>
</html>

View File

@@ -1,116 +1,325 @@
<?php declare(strict_types=1);
if (!defined('EOL')) {
define('EOL', "\n");
}
class Content
class ContentFile
{
public string $data;
public string $mimeType;
public string $fileName;
public string $hash;
private string $filePath;
private string $data;
private string $fileName;
private array $metadata = [];
const TTL = 60;
#const RECORD_TTL = 60 * 60 * 24 * 30; // 30 days.
const RECORD_TTL = 60; // Keep it short for now, so it is easier to experiment.
const METADATA_TTL = 60; // Keep it short for now, so it is easier to update the content.
const RECORD_LENGTH = 1536;
public function __construct(string $file)
{
$this->filePath = $file;
$this->fileName = basename(dirname($file)).'/'.basename($file);
$this->data = file_get_contents($file);
$this->hash = md5($this->data);
$this->parseMetadata();
}
public function toRecords(): string
/**
* @return Record[]
*/
private function toRecords(): array
{
$chunks = str_split(base64_encode($this->data), 250);
$chunks = $this->calculateChunks();
$hash = $this->calculateHash();
$records = [];
$records[] = "; {$this->getDnsName()}";
$records[] = $this->getDnsName().$this->buildMiddle().'"'.$this->buildIndexString(count($chunks)).'"';
foreach($chunks as $id => $chunk)
{
$records = [$this->buildMetadataRecord(count($chunks), $hash)];
foreach ($chunks as $id => $chunk) {
$records[] = $this->buildRecord($id, $chunk);
}
$result = "";
foreach ($records as $record)
return $records;
}
public function __toString(): string
{
$result .= $record . PHP_EOL;
$result = "";
foreach ($this->toRecords() as $record) {
$result .= $record.EOL;
}
return $result;
}
private function getDnsName(): string
public function getDnsName(): string
{
return str_replace(['/', '\\', '.'], '-', $this->fileName);
}
private function buildIndexString(int $chunkCount): string
private function buildMetadataRecord(int $chunkCount, string $hash): Record
{
return (new Record())
->setDnsName($this->getDnsName())
->setTtl(self::METADATA_TTL)
->setContent($this->buildIndexString($chunkCount, $hash));
}
private function buildIndexString(int $chunkCount, $hash): string
{
$metadata = base64_encode(json_encode($this->metadata));
return "t={$this->mimeType()};c={$chunkCount};h={$this->hash};m={$metadata}";
return '"'."t={$this->mimeType()};c={$chunkCount};ha=sha-1;h={$hash};m={$metadata}".'"';
}
private function buildMiddle(): string
private function buildRecord(int $id, string $data): Record
{
return "\t".self::TTL."\tIN\tTXT\t";
$name = "$id.{$this->calculateHash()}";
return (new Record())
->setContent($data)
->setDnsName($name)
->setTtl(self::RECORD_TTL);
}
private function buildRecord(int $id, string $data): string
{
$name = "$id.{$this->hash}";
return "$name".$this->buildMiddle().'"'.$data.'"';
}
private function mimeType()
private function mimeType(): string
{
$extension = pathinfo($this->fileName, PATHINFO_EXTENSION);
switch($extension)
{
switch ($extension) {
case "js":
return "text/javascript";
case "md":
return "text/markdown";
default:
return mime_content_type($this->file);
return mime_content_type($this->filePath);
}
}
private function parseMetadata()
private function parseMetadata(): void
{
if ($this->mimeType() != "text/markdown") return [];
$frontmatter_chars = "+++";
$end_of_frontmatter = strpos($this->data, $frontmatter_chars, 2);
if (false === $end_of_frontmatter) {
var_dump($end_of_frontmatter);
return [];
if ($this->mimeType() != "text/markdown") {
return;
}
$frontmatter = substr(
$front_matter_chars = "+++";
$front_matter_end = strpos($this->data, $front_matter_chars, 2);
if (false === $front_matter_end) {
return;
}
$front_matter = substr(
$this->data,
strlen($frontmatter_chars) + 1,
$end_of_frontmatter - strlen($frontmatter_chars) - 2
strlen($front_matter_chars) + 1,
$front_matter_end - strlen($front_matter_chars) - 2
);
$this->data = trim(substr($this->data, $end_of_frontmatter + strlen($frontmatter_chars))).PHP_EOL;
$this->data = trim(substr($this->data, $front_matter_end + strlen($front_matter_chars))).EOL;
$lines = explode(PHP_EOL, $frontmatter);
foreach($lines as $line) {
$lines = explode(EOL, $front_matter);
foreach ($lines as $line) {
$eq_pos = strpos($line, "=");
$key = trim(substr($line, 0, $eq_pos - 1));
$value = trim(substr($line, $eq_pos + 1));
$this->metadata[$key] = $value;
}
}
private function calculateHash(): string
{
return sha1($this->data);
}
private function calculateChunks(): array
{
return str_split(base64_encode($this->data), self::RECORD_LENGTH);
}
}
function update_serial_line(string $line)
class Record
{
private string $dnsName;
private int $ttl;
private string $content;
public function setDnsName(string $dnsName): self
{
$this->dnsName = $dnsName;
return $this;
}
public function setTtl(int $ttl): self
{
$this->ttl = $ttl;
return $this;
}
public function setContent(string $content): self
{
$this->content = $content;
return $this;
}
public function getRecord(): string
{
return $this->dnsName
."\t".$this->ttl
."\tIN"
."\tTXT\t"
.$this->content;
}
public function __toString(): string
{
return $this->getRecord();
}
}
class StartOfAuthority
{
public function __construct(
private string $origin,
private string $serial,
private string $masterDnsServer,
private string $domainContact,
private int $slaveRefreshInterval,
private int $slaveRetryInterval,
private int $slaveCopyExpireTime,
private int $nxDomainCacheTime
) {
}
public function doNotUserMasterDnsServer(): self
{
$servers = $this->dnsServers;
$this->dnsServers = array_filter(
$servers,
function ($server) {
return $server !== $this->masterDnsServer;
}
);
return $this;
}
public function increaseSerial(): self
{
$matches = [];
preg_match('#(\d{8})(\d{2})#', $this->serial, $matches);
list (, $day, $count) = $matches;
$today = date("Ymd");
if ($today === $day) {
$count++;
} else {
$count = 0;
}
$this->serial = sprintf('%1$s%2$02d', $today, $count);;
return $this;
}
public function __toString(): string
{
return <<<EOF
\$ORIGIN $this->origin ; The zone of this zone file
@ IN SOA $this->masterDnsServer $this->domainContact (
$this->serial ; serial
$this->slaveRefreshInterval ; slave refresh interval
$this->slaveRetryInterval ; slave retry interval
$this->slaveCopyExpireTime ; slave copy expire time
$this->nxDomainCacheTime ; NXDOMAIN cache time
)
;
; domain name servers
;
@ IN NS $this->masterDnsServer
EOF;
}
public static function fromString(string $zonefile): self
{
$matches = [];
$found = 1 === preg_match(
'#\$ORIGIN\s+(?P<origin>[a-z.]+)[^@]+@\s+IN\s+SOA\s+(?P<dns>[a-z.]+)\s+(?P<contact>[a-z.]+)\s+\(\D+\s+(?P<serial>\d+)\D+(?P<srefresh>\d+)\D+(?P<sretry>\d+)\D+(?P<sexpire>\d+)\D+(?P<nxcache>\d+)[^\)]+\)#im',
$zonefile,
$matches
);
if (!$found) {
throw new \Exception("Could not find the SOA record.");
}
return new self(
$matches['origin'],
$matches['serial'],
$matches['dns'],
$matches['contact'],
(int)$matches['srefresh'],
(int)$matches['sretry'],
(int)$matches['sexpire'],
(int)$matches['nxcache']
);
}
}
class ZoneFile
{
private StartOfAuthority $soa;
/** @var ContentFile[] */
private array $files = [];
private int $defaultTTL = 300;
public function __construct(
private string $file
) {
$this->soa = StartOfAuthority::fromString(file_get_contents($this->file));
}
public function addFile(ContentFile ...$files): self
{
foreach ($files as $file) {
$this->files[] = $file;
}
return $this;
}
public function setDefaultTTL(int $defaultTTL): self
{
$this->defaultTTL = $defaultTTL;
return $this;
}
public function __toString(): string
{
$this->soa->increaseSerial();
$zone_file = "\$TTL\t{$this->defaultTTL}\t; Default TTL".EOL.EOL;
$zone_file .= $this->soa->__toString().EOL.EOL.EOL;
$zone_file .= ";;; START BLOG RECORDS";
foreach ($this->files as $file) {
$zone_file .= EOL.EOL.EOL;
$zone_file .= '; '.$file->getDnsName().EOL;
$zone_file .= $file;
}
$zone_file .= EOL;
return $zone_file;
}
}
function update_serial_line(string $line): string
{
$matches = [];
preg_match("#(\s+)(\d{10})(.*)#", $line, $matches);
@@ -118,7 +327,7 @@ function update_serial_line(string $line)
$today = date("Ymd");
$day = substr($serial, 0, 8);
$nr = (int) substr($serial, -2);
$nr = (int)substr($serial, -2);
if ($day === $today) {
$nr++;
@@ -132,41 +341,68 @@ function update_serial_line(string $line)
return $start.$new_serial.$last;
}
$zone_file = "zones/db.hod.experiments.jacobkiers.net";
if (!file_exists($zone_file)) {
fwrite(STDERR, "The zone file {$zone_file} does not exist!");
$zone_file_path = "zones/db.hod.experiments.jacobkiers.net";
if (!file_exists($zone_file_path)) {
fwrite(STDERR, "The zone file {$zone_file_path} does not exist!");
exit(1);
}
/*
$lines = file($zone_file);
foreach($lines as $index => &$line)
{
$index = 0;
foreach ($lines as $index => &$line) {
if (str_contains($line, "; serial")) {
$matches = [];
preg_match("#(\s+)(\d{10})(.*)#", $line, $matches);
list($_, $start, $serial, $last) = $matches;
$line = update_serial_line($line).PHP_EOL;
$line = update_serial_line($line).EOL;
}
if (str_starts_with($line, ";; START BLOG RECORDS")) {
break;
}
if (str_starts_with($line, ";; START BLOG RECORDS")) break;
}
$zone_file_contents = implode("", array_slice($lines, 0, $index+1));
$zone_file_contents = implode("", array_slice($lines, 0, $index + 1));
echo $zone_file_contents;
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__."/content"));
foreach ($it as $file)
{
if ($file->isDir()) continue;
if (str_contains($file->getPathname(), "ignore")) continue;
foreach ($it as $file) {
if ($file->isDir()) {
continue;
}
if (str_contains($file->getPathname(), "ignore")) {
continue;
}
$bootstrap = new Content($file->getPathname());
$zone_file_contents .= PHP_EOL.$bootstrap->toRecords().PHP_EOL;
$content = new ContentFile($file->getPathname());
$zone_file_contents .= EOL.$content->__toString().EOL;
}
$zone_file_contents .= PHP_EOL;
$zone_file_contents .= EOL;
#echo $zone_file_contents;
echo $zone_file_contents;
file_put_contents($zone_file, $zone_file_contents);
#file_put_contents($zone_file, $zone_file_contents);
*/
$zone_file = new ZoneFile($zone_file_path);
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__."/content"));
foreach ($it as $file) {
if ($file->isDir()) {
continue;
}
if (str_contains($file->getPathname(), "ignore")) {
continue;
}
$content = new ContentFile($file->getPathname());
$zone_file->addFile($content);
}
$zone_file_contents = $zone_file->__toString();
echo $zone_file_contents;
file_put_contents($zone_file_path, $zone_file_contents);

View File

@@ -1,19 +1,18 @@
;
$TTL 5m ; Default TTL
@ IN SOA home.kie.rs. postmaster.kie.rs. (
2021081611 ; serial
1h ; slave refresh interval
15m ; slave retry interval
1w ; slave copy expire time
1h ; NXDOMAIN cache time
)
$TTL 300 ; Default TTL
$ORIGIN hod.experiments.jacobkiers.net.
$ORIGIN hod.experiments.jacobkiers.net. ; The zone of this zone file
@ IN SOA experiments.jacobkiers.net. postmaster.jacobkiers.net. (
2021082109 ; serial
3600 ; slave refresh interval
900 ; slave retry interval
604800 ; slave copy expire time
3600 ; NXDOMAIN cache time
)
;
; domain name servers
;
@ IN NS home.kie.rs.
@ IN NS experiments.jacobkiers.net.
;; START BLOG RECORDS