I’ve recently been working on a simple domain monitoring tool that includes a module for retrieving TLS certificate data. Part of the tools functionality is that the user can specify the DNS server(s) handling resolution of the domains to be monitored. Using the dnspython library, the tool can set the desired resolvers, make queries, and avoid falling through to the system resolver. However, the module for querying TLS certificates makes use of the ssl std lib which uses the socket library. socket lib relies on the getaddrinfo or gethostbyname methods which are dependent on the system hosts file and resolver configuration. There is no native method in socket to override the resolver which would invalidate the intended function of my tool.
The dnspython lib offers a nice work around through its override_system_resolver method. This function redefines the various relevant socket methods used for resolving a host address and takes in a dns.Resolver object which can be configured with your desired resolvers. In my tool, I make use of the ssl standard lib to query for server certificates, which uses the socket library to connect. There is a reset_default_resolver function also available from dnspython and, as the name implies, restores the std lib methods to socket. The resolver for the socket request made by ssl can be overridden like so:
import ssl
from dns import resolver
from dns.resolver import override_system_resolver, reset_default_resolver
res = resolver.Resolver()
res.nameservers = ["8.8.8.8", "9.9.9.9"]
override_system_resolver(res)
ssl.get_server_certificate(("example.com", 443))
reset_default_resolver()
This smells of an around/template pattern of setup -> action -> teardown/revert which usually hints in Python that we can wrap it in a context manager.
import ssl
from contextlib import contextmanager
from dns import resolver
from dns.resolver import override_system_resolver, reset_default_resolver
@contextmanager
def override_dns(resolvers: list):
res = resolver.Resolver()
res.nameservers = resolvers
override_system_resolver(res)
try:
yield
finally:
reset_default_resolver()
my_resolvers = ["8.8.8.8", "9.9.9.9"]
with override_dns(my_resolvers):
cert = ssl.get_server_certificate(("example.com", 443))
The underlying socket connection of the get_server_certificate method will resolve using the desired resolvers and, when the context exits after leaving the with block, the system resolver will take precedence again. This makes for a re-usable resolver switch that will conveniently override DNS resolution for any call that relies on the socket resolver methods.