Skip to content

Instantly share code, notes, and snippets.

@Tugzrida
Last active February 12, 2023 18:13
Show Gist options
  • Save Tugzrida/6fe83682157ead89875a76d065874973 to your computer and use it in GitHub Desktop.
Save Tugzrida/6fe83682157ead89875a76d065874973 to your computer and use it in GitHub Desktop.
Python CLI for dnsleaktest.com

DNS leak test CLI

A simple python-based CLI front end for dnsleaktest.com

Usage details available with -h

Should run on most platforms as all modules are included in the standard python install

How dnsleaktest.com works

When you visit dnsleaktest.com and run a test, your browser attempts to load resources from randomly generated subdomains of test.dnsleaktest.com.

As these domains are random and unique, it is guaranteed that no response will be cached in any DNS servers, so when your device looks up the domains, the query will make its way back to an authoritative name server for dnsleaktest.com.

As these authoritative name servers are under the website's control, it can see when queries for the test domains come in.

The origin of the queries, being the DNS resursor that queries from your device end up at, is logged and at the end of the test, your browser asks dnsleaktest.com what servers it saw queries from for the random subdomains used in your test.

There's more information about fixing DNS leaks and the sneaky tricks that ISPs can pull to force you to use their DNS at dnsleaktest.com

To-Do:

  • Nothing currently

Changelog:

v0.7

  • Fix python <3.6 compatibility

v0.6

  • Add python3 compatibility
  • Speed up tests
  • General tidy-up of code & docs

v0.5

  • Implement 'identifiers' endpoint to pre-send UUID's to dnsleaktest.com

v0.4

  • Change lookup url for domains to be http, as the test domains now resolve, but to a server with an invalid HTTPS certificate.

v0.3:

  • Implement new API-based dnsleaktest
  • Breaking change: JSON output does not use the same object/item names as previous version, as the result from the dnsleaktest API is just passed through

v0.2:

  • Add -j, -s, and -p options - JSON output support and DNS server specification via crude built-in DNS client
  • Improve resiliency and error-handling
  • Clean-up output
#!/usr/bin/env python3
# dnsleaktest v0.7 python CLI Created by Tugzrida(https://gist.github.com/Tugzrida)
from __future__ import print_function
version = "0.7"
import sys, socket, getopt, os.path
from uuid import uuid4
from json import loads, dumps
try:
from urllib.request import urlopen, Request
from urllib.error import HTTPError, URLError
except ImportError:
from urllib2 import urlopen, Request, HTTPError, URLError
def formatIPorName(address):
# Accepts an ip address or hostname, returns a formatted output (where the
# resolved part is in parentheses), and the ip address
try:
ip = socket.getaddrinfo(address, None)[0][4][0]
except (socket.herror, socket.gaierror):
sys.exit("DNS server address is invalid or does not resolve!")
if ip == address:
try:
return "{} ({})".format(address, socket.gethostbyaddr(address)[0]), address
except (socket.herror, socket.gaierror):
return "{} (No PTR)".format(address), address
else:
return "{} ({})".format(address, ip), ip
def dns_req(uuid, server, port):
# Accepts a 36-character uuid, DNS server and port through which to lookup {uuid}.test.dnsleaktest.com
req = b'\x42\x42\x01\x10\x00\x01\x00\x00\x00\x00\x00\x00\x24' + uuid.encode("utf-8") + b'\x04test\x0bdnsleaktest\x03com\x00\x00\x01\x00\x01'
try:
try:
skt = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
skt.sendto(req, (server, port))
except socket.gaierror:
skt = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
skt.sendto(req, (server, port))
skt.settimeout(5)
skt.recv(512)
except (socket.timeout, socket.error) as e:
sys.exit("Error connecting to DNS server: " + str(e))
testIDs = []
server = None
extended = False
port = 53
json = False
# Handle opts
try:
opts, args = getopt.getopt(sys.argv[1:],"ejs:p:")
except getopt.GetoptError:
sys.exit('''Usage: {} [-e] [-j] [-s server] [-p port]
-e: extended test, 36 lookups rather than the default 6
-j: json output
-s server: trace lookups through a specific DNS server, rather than the system defaults.
In this mode, the behaviour is less like a leak test and more like a tracer.
-p port: specify a non-standard port for the DNS server
v{} Created by Tugzrida(https://gist.github.com/Tugzrida)'''.format(os.path.basename(__file__), version))
for opt, arg in opts:
if opt == "-j": json = True
if opt == "-e": extended = True
if opt == "-s":
if arg:
formattedServer, server = formatIPorName(arg)
else:
sys.exit("DNS server address not provided!")
if opt == "-p":
if not any(i[0] == "-s" for i in opts): sys.exit("Must specify a server for port number to work!")
try:
if 0 <= int(arg) <= 65535:
port = int(arg)
else:
sys.exit("DNS server port number must be in the range 0-65535!")
except ValueError:
sys.exit("DNS server port number must be an integer!")
# Begin the tests
if not json: print("Starting {}DNS leak test via {}{}...".format("extended " if extended else "", formattedServer if server else "system resolver", " port " + str(port) if port != 53 else ""))
for _ in range(36 if extended else 6):
testIDs.append(str(uuid4()))
try:
urlopen(Request("https://www.dnsleaktest.com/api/v1/identifiers", headers={"User-Agent": "dnsleaktestcli/{} (https://gist.github.com/Tugzrida)".format(version), "Content-Type": "application/json;charset=UTF-8"}), dumps({"identifiers": testIDs}).encode("utf-8"), timeout=5)
except HTTPError:
pass # Endpoint always returns 400 Bad Request
except URLError:
sys.exit("Unable to reach dnsleaktest.com!")
for testCount in range(36 if extended else 6):
if server:
dns_req(testIDs[testCount], server, port)
else:
socket.gethostbyname("{}.test.dnsleaktest.com".format(testIDs[testCount]))
if not json: print("\rTest {}/{}".format(testCount+1, 36 if extended else 6), end="")
sys.stdout.flush()
# Get the results
res = loads(urlopen(Request("https://www.dnsleaktest.com/api/v1/servers-for-result", headers={"User-Agent": "dnsleaktestcli/{} (https://gist.github.com/Tugzrida)".format(version), "Content-Type": "application/json;charset=UTF-8"}), dumps({"queries": testIDs}).encode("utf-8"), timeout=5).read().decode("utf-8"))
if json:
print(dumps(res))
else:
print("\rDiscovered DNS recursors are:")
max_ip_len = max(len(x["ip_address"]) for x in res)
if max_ip_len == 0: sys.exit(' No recursors found!')
for server in res:
if server["hostname"] == "None": server["hostname"] = "No PTR"
print(" {srv[ip_address]:>{pad}} ({srv[hostname]}) hosted by {srv[isp]} in {srv[city]}, {srv[country]}".format(srv=server, pad=max_ip_len))
@sanyer
Copy link

sanyer commented Feb 22, 2021

Very nice. Exactly what I was just about to do myself :-)

btw, line 106 fails on python3.5 with this:

  File "dnsleaks.py", line 106, in <module>
    res = loads(urlopen(Request("https://www.dnsleaktest.com/api/v1/servers-for-result", headers={"User-Agent": "dnsleaktestcli/{} (https://gist.github.com/Tugzrida)".format(version), "Content-Type": "application/json;charset=UTF-8"}), dumps({"queries": testIDs}).encode("utf-8"), timeout=5).read())
  File "/usr/lib/python3.5/json/__init__.py", line 312, in loads
    s.__class__.__name__))
TypeError: the JSON object must be str, not 'bytes'

Adding .decode('utf-8') on 106 in the end after read() fixes this.
Python3.6+ would do this automatically.

@Tugzrida
Copy link
Author

@sanyer good catch - I must've only used this on >=3.6 so far! It's fixed now.

@sanyer
Copy link

sanyer commented Mar 4, 2021

Awesome! Thanks again :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment