From d0f505afd57dc1da615977ae68d67575f5187dee Mon Sep 17 00:00:00 2001 From: Kim Vinberg aKa Uldtot Date: Sun, 25 Jan 2026 14:05:51 +0100 Subject: [PATCH] On branch main Initial commit Changes to be committed: new file: api.php new file: index.php new file: readme.md --- api.php | 709 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ index.php | 325 +++++++++++++++++++++++++ readme.md | 3 + 3 files changed, 1037 insertions(+) create mode 100644 api.php create mode 100644 index.php create mode 100644 readme.md diff --git a/api.php b/api.php new file mode 100644 index 0000000..3edbae8 --- /dev/null +++ b/api.php @@ -0,0 +1,709 @@ + true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT_MS => $timeoutMs, + CURLOPT_CONNECTTIMEOUT_MS => min(1000, $timeoutMs), + CURLOPT_HTTPHEADER => [ + 'Accept: application/dns-json', + 'User-Agent: MyDNSChecker/1.0' + ], + ]); + + $started = microtime(true); + $raw = curl_exec($ch); + $curlErr = curl_error($ch); + $httpCode = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + curl_close($ch); + $timeMs = (int)round((microtime(true) - $started) * 1000); + + if ($raw === false) { + return ['ok' => false, 'httpCode' => $httpCode, 'timeMs' => $timeMs, 'error' => $curlErr ?: 'curl error']; + } + + $json = json_decode($raw, true); + if (!is_array($json)) { + return ['ok' => false, 'httpCode' => $httpCode, 'timeMs' => $timeMs, 'error' => 'invalid json response']; + } + + return ['ok' => true, 'httpCode' => $httpCode, 'timeMs' => $timeMs, 'json' => $json]; +} + +/** + * Build DNS query wire (RFC1035) for DoH RFC8484 endpoints (?dns=base64url). + */ +function buildDnsQueryWire(string $qname, int $qtype): string +{ + $id = random_int(0, 65535); + $flags = 0x0100; // recursion desired + $qdcount = 1; + $ancount = 0; + $nscount = 0; + $arcount = 0; + + $header = pack('nnnnnn', $id, $flags, $qdcount, $ancount, $nscount, $arcount); + + $labels = explode('.', trim($qname, '.')); + $qnameBin = ''; + foreach ($labels as $label) { + $len = strlen($label); + if ($len < 1 || $len > 63) throw new RuntimeException('Invalid label length'); + $qnameBin .= chr($len) . $label; + } + $qnameBin .= "\x00"; + + $qclass = 1; // IN + $question = $qnameBin . pack('nn', $qtype, $qclass); + + return $header . $question; +} + +function base64url_encode(string $bin): string +{ + return rtrim(strtr(base64_encode($bin), '+/', '-_'), '='); +} + +function dnsSkipName(string $wire, int &$offset): void +{ + $n = strlen($wire); + while ($offset < $n) { + $len = ord($wire[$offset]); + if (($len & 0xC0) === 0xC0) { // pointer + $offset += 2; + return; + } + $offset++; + if ($len === 0) return; + $offset += $len; + } +} + +function dnsReadName(string $wire, int &$offset, int $depth = 0): string +{ + $n = strlen($wire); + if ($depth > 15) return '.'; // safety + + $labels = []; + while ($offset < $n) { + $len = ord($wire[$offset]); + + if (($len & 0xC0) === 0xC0) { // pointer + if ($offset + 1 >= $n) break; + $b2 = ord($wire[$offset + 1]); + $ptr = (($len & 0x3F) << 8) | $b2; + + $offset += 2; // consume pointer bytes in main stream + $ptrOffset = $ptr; + $labels[] = rtrim(dnsReadName($wire, $ptrOffset, $depth + 1), '.'); + break; + } + + $offset++; + if ($len === 0) break; + + if ($offset + $len > $n) break; + $labels[] = substr($wire, $offset, $len); + $offset += $len; + } + + $name = implode('.', array_filter($labels, fn ($x) => $x !== '')); + return ($name === '') ? '.' : ($name . '.'); +} + +function dnsTypeToString(int $type): string +{ + return match ($type) { + 1 => 'A', + 2 => 'NS', + 5 => 'CNAME', + 6 => 'SOA', + 12 => 'PTR', + 15 => 'MX', + 16 => 'TXT', + 28 => 'AAAA', + 33 => 'SRV', + 257 => 'CAA', + default => 'TYPE' . $type, + }; +} + +/** + * Parse DNS wire response answers (RFC1035). Handles name compression. + * Returns: [ ['type'=>'MX','ttl'=>300,'data'=>'10 mail.example.com.'], ... ] + */ +function parseDnsResponseAnswers(string $wire): array +{ + $n = strlen($wire); + if ($n < 12) return []; + + $hdr = unpack('nid/nflags/nqd/nan/nns/nar', substr($wire, 0, 12)); + $qd = (int)($hdr['qd'] ?? 0); + $an = (int)($hdr['an'] ?? 0); + + $offset = 12; + + // Skip questions + for ($i = 0; $i < $qd; $i++) { + dnsSkipName($wire, $offset); + if ($offset + 4 > $n) return []; + $offset += 4; // QTYPE + QCLASS + } + + $answers = []; + + for ($i = 0; $i < $an; $i++) { + dnsSkipName($wire, $offset); + if ($offset + 10 > $n) break; + + $rr = unpack('ntype/nclass/Nttl/nrdlen', substr($wire, $offset, 10)); + $offset += 10; + + $type = (int)$rr['type']; + $ttl = (int)$rr['ttl']; + $rdlen = (int)$rr['rdlen']; + + if ($offset + $rdlen > $n) break; + + $rdataOffset = $offset; + $rdata = substr($wire, $offset, $rdlen); + $offset += $rdlen; + + $typeStr = dnsTypeToString($type); + $data = null; + + if ($type === 1 && $rdlen === 4) { + $data = inet_ntop($rdata); + } elseif ($type === 28 && $rdlen === 16) { + $data = inet_ntop($rdata); + } elseif (in_array($type, [5, 2, 12], true)) { // CNAME/NS/PTR + $tmp = $rdataOffset; + $data = dnsReadName($wire, $tmp); + } elseif ($type === 15 && $rdlen >= 3) { // MX + $pref = unpack('n', substr($wire, $rdataOffset, 2))[1]; + $tmp = $rdataOffset + 2; + $exchange = dnsReadName($wire, $tmp); + $data = $pref . ' ' . $exchange; + } elseif ($type === 16 && $rdlen >= 1) { // TXT (one or more strings) + $tmp = $rdataOffset; + $parts = []; + $end = $rdataOffset + $rdlen; + while ($tmp < $end) { + $l = ord($wire[$tmp]); + $tmp++; + if ($tmp + $l > $end) break; + $parts[] = substr($wire, $tmp, $l); + $tmp += $l; + } + $data = implode(' ', $parts); + } elseif ($type === 6) { // SOA + $tmp = $rdataOffset; + $mname = dnsReadName($wire, $tmp); + $rname = dnsReadName($wire, $tmp); + if ($tmp + 20 <= $rdataOffset + $rdlen) { + $soa = unpack('Nserial/Nrefresh/Nretry/Nexpire/Nminimum', substr($wire, $tmp, 20)); + $data = sprintf( + "%s %s serial=%u refresh=%u retry=%u expire=%u minimum=%u", + $mname, + $rname, + $soa['serial'], + $soa['refresh'], + $soa['retry'], + $soa['expire'], + $soa['minimum'] + ); + } else { + $data = $mname . ' ' . $rname; + } + } elseif ($type === 33 && $rdlen >= 7) { // SRV + $prio = unpack('n', substr($wire, $rdataOffset, 2))[1]; + $weight = unpack('n', substr($wire, $rdataOffset + 2, 2))[1]; + $port = unpack('n', substr($wire, $rdataOffset + 4, 2))[1]; + $tmp = $rdataOffset + 6; + $target = dnsReadName($wire, $tmp); + $data = sprintf("%u %u %u %s", $prio, $weight, $port, $target); + } elseif ($type === 257 && $rdlen >= 3) { // CAA + $flags = ord($wire[$rdataOffset]); + $tagLen = ord($wire[$rdataOffset + 1]); + $tag = substr($wire, $rdataOffset + 2, $tagLen); + $value = substr($wire, $rdataOffset + 2 + $tagLen, ($rdlen - 2 - $tagLen)); + $data = sprintf("flags=%u tag=%s value=%s", $flags, $tag, $value); + } else { + $data = 'RDATA(' . $rdlen . ' bytes)'; + } + + $answers[] = [ + 'type' => $typeStr, + 'ttl' => $ttl, + 'data' => $data, + ]; + } + + return $answers; +} + +function dohWireQuery(string $endpoint, string $name, int $qtype, int $timeoutMs = 2500): array +{ + $wire = buildDnsQueryWire($name, $qtype); + $dnsParam = base64url_encode($wire); + $url = $endpoint . '?dns=' . rawurlencode($dnsParam); + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT_MS => $timeoutMs, + CURLOPT_CONNECTTIMEOUT_MS => min(1000, $timeoutMs), + CURLOPT_HTTPHEADER => [ + 'Accept: application/dns-message', + 'User-Agent: MyDNSChecker/1.0' + ], + ]); + + $started = microtime(true); + $raw = curl_exec($ch); + $curlErr = curl_error($ch); + $httpCode = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + curl_close($ch); + $timeMs = (int)round((microtime(true) - $started) * 1000); + + if ($raw === false) { + return ['ok' => false, 'httpCode' => $httpCode, 'timeMs' => $timeMs, 'error' => $curlErr ?: 'curl error']; + } + + $answers = parseDnsResponseAnswers($raw); + + return ['ok' => true, 'httpCode' => $httpCode, 'timeMs' => $timeMs, 'answers' => $answers]; +} + +function qtypeToInt(string $type): int +{ + return match ($type) { + 'A' => 1, + 'NS' => 2, + 'CNAME' => 5, + 'SOA' => 6, + 'PTR' => 12, + 'MX' => 15, + 'TXT' => 16, + 'AAAA' => 28, + 'SRV' => 33, + 'CAA' => 257, + default => 1, + }; +} + +function typeToStringFromGoogle($t): ?string +{ + if (is_int($t)) return dnsTypeToString($t); + if (is_string($t) && $t !== '') return $t; + return null; +} + +// ---- input ---- +$method = $_SERVER['REQUEST_METHOD']; +$input = []; + +if ($method === 'POST') { + $raw = file_get_contents('php://input'); + $decoded = json_decode($raw, true); + if (is_array($decoded)) $input = $decoded; +} else { + $input = $_GET; +} + +$domain = normalizeDomain((string)($input['domain'] ?? '')); +$type = strtoupper((string)($input['type'] ?? 'A')); +$expected = trim((string)($input['expected'] ?? '')); + +if ($domain === '') { + http_response_code(400); + echo json_encode(['error' => 'domain is required']); + exit; +} +if (!in_array($type, $VALID_TYPES, true)) { + http_response_code(400); + echo json_encode(['error' => 'invalid type']); + exit; +} + +// Resolver-liste (DNS over HTTPS) – med countryCode til flag i frontend +$resolvers = [ + [ + 'name' => 'Local server DNS', + 'country' => 'Germany', // Optional + 'country_code' => 'DE', // Optional + 'city' => 'Frankfurt', + 'kind' => 'system', + ], + // Germany + [ + 'name' => 'dnsforge', + 'country' => 'Germany', + 'country_code' => 'DE', + 'city' => 'Frankfurt', + 'kind' => 'wire', + 'endpoint' => 'https://dnsforge.de/dns-query', + ], + [ + 'name' => 'Digitale Gesellschaft', + 'country' => 'Switzerland', + 'country_code' => 'CH', + 'city' => 'Zurich', + 'kind' => 'wire', + 'endpoint' => 'https://dns.digitale-gesellschaft.ch/dns-query', + ], + + // USA + [ + 'name' => 'Google DNS', + 'country' => 'United States', + 'country_code' => 'US', + 'city' => 'Global', + 'kind' => 'json', + 'endpoint' => 'https://dns.google/resolve', + ], + [ + 'name' => 'Cloudflare', + 'country' => 'United States', + 'country_code' => 'US', + 'city' => 'Global', + 'kind' => 'wire', + 'endpoint' => 'https://cloudflare-dns.com/dns-query', + ], + [ + 'name' => 'OpenDNS (Cisco)', + 'country' => 'United States', + 'country_code' => 'US', + 'city' => 'Global', + 'kind' => 'wire', + 'endpoint' => 'https://doh.opendns.com/dns-query', + ], + + // Switzerland + [ + 'name' => 'Quad9', + 'country' => 'Switzerland', + 'country_code' => 'CH', + 'city' => 'Zurich', + 'kind' => 'wire', + 'endpoint' => 'https://dns.quad9.net/dns-query', + ], + [ + 'name' => 'Quad9 (ECS disabled)', + 'country' => 'Switzerland', + 'country_code' => 'CH', + 'city' => 'Zurich', + 'kind' => 'wire', + 'endpoint' => 'https://dns10.quad9.net/dns-query', + ], + + + + // France + [ + 'name' => 'FDN', + 'country' => 'France', + 'country_code' => 'FR', + 'city' => 'Paris', + 'kind' => 'wire', + 'endpoint' => 'https://dns.fdn.fr/dns-query', + ], + [ + 'name' => 'NextDNS', + 'country' => 'France', + 'country_code' => 'FR', + 'city' => 'EU', + 'kind' => 'wire', + 'endpoint' => 'https://dns.nextdns.io/dns-query', + ], + + // Netherlands + [ + 'name' => 'Mullvad DNS', + 'country' => 'Netherlands', + 'country_code' => 'NL', + 'city' => 'Amsterdam', + 'kind' => 'wire', + 'endpoint' => 'https://dns.mullvad.net/dns-query', + ], + [ + 'name' => 'NLnet Labs (Unbound)', + 'country' => 'Netherlands', + 'country_code' => 'NL', + 'city' => 'Amsterdam', + 'kind' => 'wire', + 'endpoint' => 'https://unbound.net/dns-query', + ], + + + // Global + [ + 'name' => 'AdGuard', + 'country' => 'Global', + 'country_code' => 'GL', + 'city' => 'Anycast', + 'kind' => 'wire', + 'endpoint' => 'https://dns.adguard.com/dns-query', + ], + [ + 'name' => 'CleanBrowsing (Security)', + 'country' => 'Global', + 'country_code' => 'GL', + 'city' => 'Anycast', + 'kind' => 'wire', + 'endpoint' => 'https://doh.cleanbrowsing.org/doh/security-filter/', + ], + [ + 'name' => 'DNS.SB', + 'country' => 'Global', + 'country_code' => 'GL', + 'city' => 'Anycast', + 'kind' => 'wire', + 'endpoint' => 'https://doh.dns.sb/dns-query', + ], +]; + +$qtypeInt = qtypeToInt($type); +$results = []; + +foreach ($resolvers as $r) { + $base = [ + 'resolver' => $r['name'], + 'country' => $r['country'] ?? null, + 'countryCode' => $r['country_code'] ?? null, + 'city' => $r['city'] ?? null, + ]; + + try { + + if ($r['kind'] === 'system') { + + $started = microtime(true); + + $map = [ + 'A' => DNS_A, + 'AAAA' => DNS_AAAA, + 'CNAME' => DNS_CNAME, + 'MX' => DNS_MX, + 'NS' => DNS_NS, + 'TXT' => DNS_TXT, + 'SOA' => DNS_SOA, + 'SRV' => DNS_SRV, + 'CAA' => DNS_CAA, + 'PTR' => DNS_PTR, + ]; + + $dnsType = $map[$type] ?? DNS_A; + $records = @dns_get_record($domain, $dnsType); + + $timeMs = (int) round((microtime(true) - $started) * 1000); + + if (!is_array($records) || count($records) === 0) { + $results[] = [ + 'resolver' => $r['name'], + 'country' => $r['country'], + 'city' => $r['city'], + 'status' => 'NO RESULT', + 'timeMs' => $timeMs, + 'answers' => [], + 'matched' => null, + ]; + continue; + } + + $answers = []; + foreach ($records as $rec) { + $data = null; + + switch ($type) { + case 'A': + $data = $rec['ip'] ?? null; + break; + case 'AAAA': + $data = $rec['ipv6'] ?? null; + break; + case 'CNAME': + $data = $rec['target'] ?? null; + break; + case 'MX': + $data = ($rec['pri'] ?? '') . ' ' . ($rec['target'] ?? ''); + break; + case 'NS': + case 'PTR': + $data = $rec['target'] ?? null; + break; + case 'TXT': + $data = $rec['txt'] ?? null; + break; + case 'SOA': + $data = ($rec['mname'] ?? '') . ' ' . ($rec['rname'] ?? ''); + break; + case 'SRV': + $data = ($rec['pri'] ?? '') . ' ' + . ($rec['weight'] ?? '') . ' ' + . ($rec['port'] ?? '') . ' ' + . ($rec['target'] ?? ''); + break; + case 'CAA': + $data = ($rec['flags'] ?? '') . ' ' + . ($rec['tag'] ?? '') . ' ' + . ($rec['value'] ?? ''); + break; + } + + $answers[] = [ + 'type' => $type, + 'ttl' => $rec['ttl'] ?? null, + 'data' => $data, + ]; + } + + $matched = null; + if ($expected !== '') { + $matched = false; + foreach ($answers as $a) { + if (is_string($a['data']) && strcasecmp(trim($a['data']), $expected) === 0) { + $matched = true; + break; + } + } + } + + $results[] = [ + 'resolver' => $r['name'], + 'country' => $r['country'], + 'city' => $r['city'], + 'status' => 'OK', + 'timeMs' => $timeMs, + 'answers' => $answers, + 'matched' => $matched, + ]; + + continue; + } + + + if ($r['kind'] === 'json') { + $resp = dohJsonQuery($r['endpoint'], $domain, $type); + + if (!$resp['ok']) { + $results[] = $base + [ + 'status' => 'ERROR', + 'timeMs' => $resp['timeMs'] ?? null, + 'error' => $resp['error'] ?? 'unknown error', + 'answers' => [], + 'matched' => null, + ]; + continue; + } + + $json = $resp['json']; + $answers = []; + foreach (($json['Answer'] ?? []) as $a) { + $answers[] = [ + 'ttl' => $a['TTL'] ?? null, + 'data' => $a['data'] ?? null, + 'type' => typeToStringFromGoogle($a['type'] ?? null), + ]; + } + + $matched = null; + if ($expected !== '') { + $matched = false; + foreach ($answers as $a) { + if (is_string($a['data']) && strcasecmp($a['data'], $expected) === 0) { + $matched = true; + break; + } + } + } + + $results[] = $base + [ + 'status' => 'OK', + 'timeMs' => $resp['timeMs'] ?? null, + 'rcode' => $json['Status'] ?? null, + 'answers' => $answers, + 'matched' => $matched, + ]; + } else { + $resp = dohWireQuery($r['endpoint'], $domain, $qtypeInt); + + if (!$resp['ok']) { + $results[] = $base + [ + 'status' => 'ERROR', + 'timeMs' => $resp['timeMs'] ?? null, + 'error' => $resp['error'] ?? 'unknown error', + 'answers' => [], + 'matched' => null, + ]; + continue; + } + + $answers = $resp['answers'] ?? []; + + $matched = null; + if ($expected !== '') { + $matched = false; + foreach ($answers as $a) { + if (is_string($a['data']) && strcasecmp($a['data'], $expected) === 0) { + $matched = true; + break; + } + } + } + + $results[] = $base + [ + 'status' => 'OK', + 'timeMs' => $resp['timeMs'] ?? null, + 'answers' => $answers, + 'matched' => $matched, + ]; + } + } catch (Throwable $e) { + $results[] = $base + [ + 'status' => 'ERROR', + 'error' => $e->getMessage(), + 'answers' => [], + 'matched' => null, + ]; + } +} + +echo json_encode([ + 'domain' => $domain, + 'type' => $type, + 'expected' => ($expected !== '' ? $expected : null), + 'results' => $results, +], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); diff --git a/index.php b/index.php new file mode 100644 index 0000000..deda072 --- /dev/null +++ b/index.php @@ -0,0 +1,325 @@ + + + + + + + DNS Propagation Checker + + + + + + + + +
+
+
+

+ DNS Propagation Checker +

+ +

+ This tool performs global DNS lookups using public DNS resolvers in multiple countries. + Results are shown in a compact overview with TTL and record data displayed directly, making it easy to compare DNS responses across locations. + A resolver is only marked as OK when it actually returns valid DNS answers. +

+ +
+ + 🔒 Privacy-friendly · No data logging + + +
+
+
+
+ + + +
+ + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+
+
+
+ + +
+ + +
+ + + + + + + + + + + + + + + + + +
ResolverStatusTimeTTLDataMatchDetaljer
Run lookup to see results…
+
+ +
+ + + + + + + + + + + \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..bf5b9e8 --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +# DNS Propagation Checker + +Nothing here yet... \ No newline at end of file