Skip to content

Instantly share code, notes, and snippets.

@gbraad
Last active September 1, 2025 11:01
Show Gist options
  • Select an option

  • Save gbraad/3c9f66d9906a97c97bed989c98b8f4fa to your computer and use it in GitHub Desktop.

Select an option

Save gbraad/3c9f66d9906a97c97bed989c98b8f4fa to your computer and use it in GitHub Desktop.
RewriteEngine On
RewriteRule ^dns-query$ dns-query.php [L]

DOH Forwarder

This is tested to work on Dreamhost with a free SSL certificate.

<?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();
?>
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