A hack to provide some privacy to DNS queries.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

214 lines
6.2 KiB

  1. #!/usr/bin/env python
  2. #
  3. # Copyright 2019 John-Mark Gurney.
  4. # All rights reserved.
  5. #
  6. # Redistribution and use in source and binary forms, with or without
  7. # modification, are permitted provided that the following conditions
  8. # are met:
  9. # 1. Redistributions of source code must retain the above copyright
  10. # notice, this list of conditions and the following disclaimer.
  11. # 2. Redistributions in binary form must reproduce the above copyright
  12. # notice, this list of conditions and the following disclaimer in the
  13. # documentation and/or other materials provided with the distribution.
  14. #
  15. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  16. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  17. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  18. # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  19. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  20. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  21. # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  22. # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  23. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  24. # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  25. # SUCH DAMAGE.
  26. #
  27. __author__ = 'John-Mark Gurney'
  28. __copyright__ = 'Copyright 2019 John-Mark Gurney. All rights reserved.'
  29. __license__ = '2-clause BSD license'
  30. __version__ = '0.1.0.dev'
  31. import asyncio
  32. import dns
  33. import socket
  34. import unittest
  35. from dnslib import DNSRecord, DNSQuestion, RR, QTYPE, A, digparser
  36. from ntunnel import async_test, parsesockstr
  37. class DNSCache(object):
  38. def __init__(self):
  39. self._data = {}
  40. # preload some values:
  41. self._data.update({
  42. ('.', 'DS'): [ ('.', 'IN', 'DS', 20326, 8, 2,
  43. bytes.fromhex('E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D'))],
  44. })
  45. def get(self, name, rtype):
  46. return self._data[(name, rtype)]
  47. class Resolver(object):
  48. '''Resolve DNS names to records.
  49. Either serve it from the cache, or do a query. If the query
  50. can be DNSSEC validated, do it via the privacy querier if not,
  51. via the trusted one.'''
  52. def _writepkt(wrr, bts):
  53. wrr.write(len(bts).to_bytes(2, 'big'))
  54. wrr.write(bts)
  55. async def _readpkt(rdr):
  56. nbytes = int.from_bytes(await rdr.readexactly(2), 'big')
  57. return await rdr.readexactly(nbytes)
  58. class ServerResolver(object):
  59. '''Ask a specific server a question, and return the result.'''
  60. def __init__(self, rdrwrr):
  61. '''Pass in the reader and writer pair that is connected
  62. to the server.'''
  63. self._reader, self._writer = rdrwrr
  64. async def resolve(self, q):
  65. pkt = DNSRecord(questions=[q])
  66. pktbytes = pkt.pack()
  67. _writepkt(self._writer, pktbytes)
  68. pkt = await _readpkt(self._reader)
  69. resp = DNSRecord.parse(pkt)
  70. return resp.rr
  71. class DNSProc(asyncio.DatagramProtocol):
  72. def connection_made(self, transport):
  73. #print('cm')
  74. self.transport = transport
  75. def datagram_received(self, data, addr):
  76. #print('dr')
  77. pkt = DNSRecord.parse(data)
  78. #print(repr((pkt, addr)))
  79. d = pkt.reply()
  80. d.add_answer(RR("xxx.abc.com",QTYPE.A,rdata=A("1.2.3.4")))
  81. data = d.pack()
  82. self.transport.sendto(data, addr)
  83. async def dnsprocessor(sockstr):
  84. proto, args = parsesockstr(sockstr)
  85. if proto == 'udp':
  86. loop = asyncio.get_event_loop()
  87. #print('pre-cde')
  88. trans, protocol = await loop.create_datagram_endpoint(DNSProc,
  89. local_addr=(args.get('host', '127.0.0.1'), args['port']))
  90. #print('post-cde', repr((trans, protocol)), trans is protocol.transport)
  91. else:
  92. raise ValueError('unknown protocol: %s' % repr(proto))
  93. def _asyncsockpair():
  94. '''Create a pair of sockets that are bound to each other.
  95. The function will return a tuple of two coroutine's, that
  96. each, when await'ed upon, will return the reader/writer pair.'''
  97. socka, sockb = socket.socketpair()
  98. return (asyncio.open_connection(sock=socka),
  99. asyncio.open_connection(sock=sockb))
  100. class Tests(unittest.TestCase):
  101. @async_test
  102. async def test_processdnsfailures(self):
  103. port = 5
  104. with self.assertRaises(ValueError):
  105. dnsproc = await dnsprocessor(
  106. 'tcp:host=127.0.0.1,port=%d' % port)
  107. #print('post-dns')
  108. @async_test
  109. async def test_processdns(self):
  110. # start the dns processor
  111. port = 38394
  112. dnsproc = await dnsprocessor(
  113. 'udp:host=127.0.0.1,port=%d' % port)
  114. # submit a query to it
  115. digproc = await asyncio.create_subprocess_exec('dig',
  116. '@127.0.0.1', '-p', str(port), '+dnssec', '+time=1',
  117. 'www.funkthat.com.',
  118. stdout=asyncio.subprocess.PIPE)
  119. # make sure it exits successfully
  120. self.assertEqual(await digproc.wait(), 0)
  121. stdout, stderr = await digproc.communicate()
  122. rep = list(digparser.DigParser(stdout))
  123. self.assertEqual(len(rep), 1)
  124. #print('x', repr(list(rep)))
  125. def test_cache(self):
  126. # test that the cache
  127. cache = DNSCache()
  128. # has the root trust anchors in it
  129. dsrs = cache.get('.', 'DS')
  130. self.assertEqual(dsrs, [ ('.', 'IN', 'DS', 20326, 8, 2,
  131. bytes.fromhex('E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D')) ])
  132. @async_test
  133. async def xtest_query(self):
  134. # test that the query function
  135. q = await query_record('com.', 'DS')
  136. @async_test
  137. async def test_realresolver(self): # pragma: no cover
  138. # This is to test against a real resolver.
  139. host = 'gold'
  140. res = ServerResolver(await asyncio.open_connection(host, 53))
  141. resquestion = DNSQuestion('example.com')
  142. print(repr(await res.resolve(resquestion)))
  143. @async_test
  144. async def test_questionresolver(self):
  145. client, server = _asyncsockpair()
  146. res = ServerResolver(await client)
  147. rdr, wrr = await server
  148. # constants
  149. resquestion = DNSQuestion('example.com')
  150. resanswer = RR.fromZone('example.com. A 192.0.2.10\nexample.com. A 192.0.2.11')
  151. # start the query
  152. ans = asyncio.create_task(res.resolve(resquestion))
  153. # Fetch the question
  154. question = await _readpkt(rdr)
  155. dnsrec = DNSRecord.parse(question)
  156. # Make sure we got the correct question
  157. self.assertEqual(dnsrec.get_q(), resquestion)
  158. # Generate the reply
  159. rep = dnsrec.reply()
  160. rep.add_answer(*resanswer)
  161. repbytes = rep.pack()
  162. # Send the reply
  163. _writepkt(wrr, repbytes)
  164. wrr.write_eof()
  165. # veryify the received answer
  166. ans = await ans
  167. self.assertEqual(ans, resanswer)