Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mock DNS server using Twisted

I'm trying to write a Twisted-based mock DNS server to do some tests.

Taking inspiration from this guide I wrote a very simple server that just resolves everything to 127.0.0.1:

from twisted.internet import defer, reactor
from twisted.names import dns, error, server


class MockDNSResolver:

    def _doDynamicResponse(self, query):
        name = query.name.name
        record = dns.Record_A(address=b"127.0.0.1")
        answer = dns.RRHeader(name=name, payload=record)
        authority = []
        additional = []
        return [answer], authority, additional

    def query(self, query, timeout=None):
        print("Incoming query for:", query.name)
        if query.type == dns.A:
            return defer.succeed(self._doDynamicResponse(query))
        else:
            return defer.fail(error.DomainError())


if __name__ == "__main__":
    clients = [MockDNSResolver()]
    factory = server.DNSServerFactory(clients=clients)
    protocol = dns.DNSDatagramProtocol(controller=factory)
    reactor.listenUDP(10053, protocol)
    reactor.listenTCP(10053, factory)
    reactor.run()

The above is working just fine with dig and nslookup (from a different terminal):

$ dig -p 10053 @localhost something.example.org A +short
127.0.0.1

$ nslookup something.else.example.org 127.0.0.1 -port=10053
Server:     127.0.0.1
Address:    127.0.0.1#10053

Non-authoritative answer:
Name:   something.else.example.org
Address: 127.0.0.1

I'm also getting the corresponding hits on the terminal that's running the server:

Incoming query for: something.example.org
Incoming query for: something.else.example.org

Then, I wrote the following piece of code, based on this section about making requests and this section about installing a custom resolver:

from twisted.internet import reactor
from twisted.names.client import createResolver
from twisted.web.client import Agent


d = Agent(reactor).request(b'GET', b'http://does.not.exist')
reactor.installResolver(createResolver(servers=[('127.0.0.1', 10053)]))


def callback(result):
    print('Result:', result)


d.addBoth(callback)
d.addBoth(lambda _: reactor.stop())

reactor.run()

But this fails (and I get no lines in the server terminal). It appears as if the queries are not going to the mock server, but to the system-defined server:

Result: [Failure instance: Traceback: <class 'twisted.internet.error.DNSLookupError'>: DNS lookup failed: no results for hostname lookup: does.not.exist.
/.../venv/lib/python3.6/site-packages/twisted/internet/_resolver.py:137:deliverResults
/.../venv/lib/python3.6/site-packages/twisted/internet/endpoints.py:921:resolutionComplete
/.../venv/lib/python3.6/site-packages/twisted/internet/defer.py:460:callback
/.../venv/lib/python3.6/site-packages/twisted/internet/defer.py:568:_startRunCallbacks
--- <exception caught here> ---
/.../venv/lib/python3.6/site-packages/twisted/internet/defer.py:654:_runCallbacks
/.../venv/lib/python3.6/site-packages/twisted/internet/endpoints.py:975:startConnectionAttempts
]

I'm using:

  • macOS 10.14.6 Python 3.6.6, Twisted 18.9.0
  • Linux Mint 19.1, Python 3.6.9, Twisted 19.7.0

I appreciate any help, please let me know if additional information is required.

Thanks!

like image 527
elacuesta Avatar asked Jan 24 '20 18:01

elacuesta


1 Answers

The solution was:

  • (Client) swap the order of the lines that install the resolver and create the deferred for the request, as suggested by @Hadus. I thought this didn't matter, since the reactor was not running yet.
  • (Server) implement lookupAllRecords, reusing the existing _doDynamicResponse method.
# server
from twisted.internet import defer, reactor
from twisted.names import dns, error, server


class MockDNSResolver:
    """
    Implements twisted.internet.interfaces.IResolver partially
    """

    def _doDynamicResponse(self, name):
        print("Resolving name:", name)
        record = dns.Record_A(address=b"127.0.0.1")
        answer = dns.RRHeader(name=name, payload=record)
        return [answer], [], []

    def query(self, query, timeout=None):
        if query.type == dns.A:
            return defer.succeed(self._doDynamicResponse(query.name.name))
        return defer.fail(error.DomainError())

    def lookupAllRecords(self, name, timeout=None):
        return defer.succeed(self._doDynamicResponse(name))


if __name__ == "__main__":
    clients = [MockDNSResolver()]
    factory = server.DNSServerFactory(clients=clients)
    protocol = dns.DNSDatagramProtocol(controller=factory)
    reactor.listenUDP(10053, protocol)
    reactor.listenTCP(10053, factory)
    reactor.run()
# client
from twisted.internet import reactor
from twisted.names.client import createResolver
from twisted.web.client import Agent


reactor.installResolver(createResolver(servers=[('127.0.0.1', 10053)]))
d = Agent(reactor).request(b'GET', b'http://does.not.exist:8000')


def callback(result):
    print('Result:', result)


d.addBoth(callback)
d.addBoth(lambda _: reactor.stop())

reactor.run()
$ python client.py
Result: <twisted.web._newclient.Response object at 0x101077f98>

(I'm running a simple web server with python3 -m http.server in another terminal, otherwise I get a reasonable twisted.internet.error.ConnectionRefusedError exception).

like image 168
elacuesta Avatar answered Oct 23 '22 06:10

elacuesta