This is tested to work on Dreamhost with a free SSL certificate.
Last active
September 1, 2025 11:01
-
-
Save gbraad/3c9f66d9906a97c97bed989c98b8f4fa to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| RewriteEngine On | |
| RewriteRule ^dns-query$ dns-query.php [L] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| // List of DOH servers | |
| $doh_servers = [ | |
| 'https://cloudflare-dns.com/dns-query', | |
| 'https://dns.google/dns-query', | |
| 'https://dns.quad9.net/dns-query', | |
| 'https://dns.adguard.com/dns-query', | |
| 'https://dns.nextdns.io/dns-query', | |
| 'https://doh.opendns.com/dns-query', | |
| 'https://dns.watch/dns-query', | |
| 'https://doh.cleanbrowsing.org/dns-query', | |
| 'https://public.dns.iij.jp/dns-query', | |
| 'https://dns.neustar.biz/dns-query' | |
| ]; | |
| $use_random = false; | |
| // Forward a POST DoH request | |
| function doPost($url, $postdata) { | |
| $ch = curl_init(); | |
| curl_setopt($ch, CURLOPT_URL, $url); | |
| curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
| curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20); | |
| curl_setopt($ch, CURLOPT_USERAGENT, 'Chrome'); | |
| curl_setopt($ch, CURLOPT_TIMEOUT, 30); | |
| curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); | |
| curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); | |
| curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); | |
| curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: application/dns-message", "Accept: application/dns-message")); | |
| curl_setopt($ch, CURLOPT_POST, true); | |
| curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata); | |
| $response = curl_exec($ch); | |
| $statuscode = curl_getinfo($ch, CURLINFO_HTTP_CODE); | |
| curl_close($ch); | |
| return [$statuscode, $response]; | |
| } | |
| // Forward a GET DoH request | |
| function doGet($url) { | |
| $ch = curl_init(); | |
| curl_setopt($ch, CURLOPT_URL, $url); | |
| curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
| curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20); | |
| curl_setopt($ch, CURLOPT_USERAGENT, 'Chrome'); | |
| curl_setopt($ch, CURLOPT_TIMEOUT, 30); | |
| curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); | |
| curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); | |
| curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); | |
| curl_setopt($ch, CURLOPT_HTTPHEADER, array("Accept: application/dns-message")); | |
| $response = curl_exec($ch); | |
| $statuscode = curl_getinfo($ch, CURLINFO_HTTP_CODE); | |
| curl_close($ch); | |
| return [$statuscode, $response]; | |
| } | |
| // Main: handle GET and POST | |
| $post_data = file_get_contents('php://input'); | |
| $get_data = isset($_REQUEST['dns']) ? $_REQUEST['dns'] : null; | |
| $server_count = count($doh_servers); | |
| if ($server_count < 1) { | |
| http_response_code(503); | |
| exit(); | |
| } | |
| // Select upstream DoH server | |
| $selected = $use_random ? rand(0, $server_count - 1) : 0; | |
| $upstream_url = $doh_servers[$selected]; | |
| // If POST with body: forward binary DNS message | |
| if ($post_data && strlen($post_data) > 0) { | |
| $doh_resp = doPost($upstream_url, $post_data); | |
| } | |
| // If GET with ?dns=...: forward GET | |
| else if ($get_data) { | |
| $doh_resp = doGet($upstream_url . '?dns=' . urlencode($get_data)); | |
| } else { | |
| http_response_code(400); | |
| exit(); | |
| } | |
| // Return upstream response | |
| http_response_code($doh_resp[0]); | |
| header('Content-Type: application/dns-message'); | |
| echo $doh_resp[1]; | |
| exit(); | |
| ?> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import requests | |
| import struct | |
| def create_dns_query(domain): | |
| # Create a DNS query in binary format | |
| query = b'' | |
| query += struct.pack('!H', 0x0001) # ID | |
| query += struct.pack('!H', 0x0100) # Flags (Standard query) | |
| query += struct.pack('!H', 0x0001) # QDCOUNT | |
| query += struct.pack('!H', 0x0000) # ANCOUNT | |
| query += struct.pack('!H', 0x0000) # NSCOUNT | |
| query += struct.pack('!H', 0x0000) # ARCOUNT | |
| # Add the query name | |
| parts = domain.split('.') | |
| for part in parts: | |
| query += struct.pack('B', len(part)) + part.encode() | |
| query += b'\x00' # Null terminator | |
| # Add the query type and class | |
| query += struct.pack('!H', 0x0001) # Type A | |
| query += struct.pack('!H', 0x0001) # Class IN | |
| return query | |
| def test_doh(dns_server, domain): | |
| url = f"https://{dns_server}/dns-query" | |
| headers = { | |
| "accept": "application/dns-message", | |
| "Content-Type": "application/dns-message" | |
| } | |
| query = create_dns_query(domain) | |
| response = requests.post(url, headers=headers, data=query) | |
| if response.status_code == 200: | |
| print("DOH query successful!") | |
| print(response.content) | |
| else: | |
| print(f"DOH query failed with status code {response.status_code}") | |
| print(response.text) | |
| # Example usage | |
| test_doh("cloudflare-dns.com", "google.com") | |
| test_doh("dns.google", "google.com") | |
| test_doh([YOUR_SERVER|, "google.com") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment