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.
 
 

345 lines
9.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. self._qs = {}
  65. self._replies = asyncio.create_task(self.procreplies(self._reader))
  66. def __del__(self):
  67. if self._replies:
  68. self._replies.cancel()
  69. self._replies = None
  70. self._writer.write_eof()
  71. def cleanup(self):
  72. while self._qs:
  73. i = self._qs.popitem()[1]
  74. i.set_exception(RuntimeError(
  75. 'server connection closed, '
  76. 'no response received'))
  77. async def procreplies(self, rdr):
  78. while True:
  79. # Get a response
  80. try:
  81. pkt = await _readpkt(self._reader)
  82. except asyncio.streams.IncompleteReadError:
  83. self.cleanup()
  84. return
  85. resp = DNSRecord.parse(pkt)
  86. # fetch where we should send it
  87. fut = self._qs.pop(resp.header.id, None)
  88. if fut is None:
  89. continue
  90. # send the result
  91. fut.set_result(resp.rr)
  92. async def resolve(self, q):
  93. # make DNS packet
  94. pkt = DNSRecord(questions=[q])
  95. pktbytes = pkt.pack()
  96. # Where the procreplies function will send the answer
  97. res = asyncio.Future()
  98. # Record it
  99. self._qs[pkt.header.id] = res
  100. # Send query
  101. _writepkt(self._writer, pktbytes)
  102. return await res
  103. class DNSProc(asyncio.DatagramProtocol):
  104. def connection_made(self, transport):
  105. #print('cm')
  106. self.transport = transport
  107. def datagram_received(self, data, addr):
  108. #print('dr')
  109. pkt = DNSRecord.parse(data)
  110. #print(repr((pkt, addr)))
  111. d = pkt.reply()
  112. d.add_answer(RR("xxx.abc.com",QTYPE.A,rdata=A("1.2.3.4")))
  113. data = d.pack()
  114. self.transport.sendto(data, addr)
  115. async def dnsprocessor(sockstr):
  116. proto, args = parsesockstr(sockstr)
  117. if proto == 'udp':
  118. loop = asyncio.get_event_loop()
  119. #print('pre-cde')
  120. trans, protocol = await loop.create_datagram_endpoint(DNSProc,
  121. local_addr=(args.get('host', '127.0.0.1'), args['port']))
  122. #print('post-cde', repr((trans, protocol)), trans is protocol.transport)
  123. else:
  124. raise ValueError('unknown protocol: %s' % repr(proto))
  125. def _asyncsockpair():
  126. '''Create a pair of sockets that are bound to each other.
  127. The function will return a tuple of two coroutine's, that
  128. each, when await'ed upon, will return the reader/writer pair.'''
  129. socka, sockb = socket.socketpair()
  130. return (asyncio.open_connection(sock=socka),
  131. asyncio.open_connection(sock=sockb))
  132. class Tests(unittest.TestCase):
  133. @async_test
  134. async def test_processdnsfailures(self):
  135. port = 5
  136. with self.assertRaises(ValueError):
  137. dnsproc = await dnsprocessor(
  138. 'tcp:host=127.0.0.1,port=%d' % port)
  139. #print('post-dns')
  140. @async_test
  141. async def test_processdns(self):
  142. # start the dns processor
  143. port = 38394
  144. dnsproc = await dnsprocessor(
  145. 'udp:host=127.0.0.1,port=%d' % port)
  146. # submit a query to it
  147. digproc = await asyncio.create_subprocess_exec('dig',
  148. '@127.0.0.1', '-p', str(port), '+dnssec', '+time=1',
  149. 'www.funkthat.com.',
  150. stdout=asyncio.subprocess.PIPE)
  151. # make sure it exits successfully
  152. self.assertEqual(await digproc.wait(), 0)
  153. stdout, stderr = await digproc.communicate()
  154. rep = list(digparser.DigParser(stdout))
  155. self.assertEqual(len(rep), 1)
  156. #print('x', repr(list(rep)))
  157. def test_cache(self):
  158. # test that the cache
  159. cache = DNSCache()
  160. # has the root trust anchors in it
  161. dsrs = cache.get('.', 'DS')
  162. self.assertEqual(dsrs, [ ('.', 'IN', 'DS', 20326, 8, 2,
  163. bytes.fromhex('E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D')) ])
  164. @async_test
  165. async def xtest_query(self): # pragma: no cover
  166. # test that the query function
  167. q = await query_record('com.', 'DS')
  168. @async_test
  169. async def xtest_realresolver(self): # pragma: no cover
  170. # This is to test against a real resolver.
  171. host = 'gold'
  172. res = ServerResolver(await asyncio.open_connection(host, 53))
  173. resquestion = DNSQuestion('example.com')
  174. @async_test
  175. async def test_resolvernoanswer(self):
  176. client, server = _asyncsockpair()
  177. res = ServerResolver(await client)
  178. rdr, wrr = await server
  179. # constants
  180. resquestion = DNSQuestion('example.com')
  181. # start the query
  182. ans = asyncio.create_task(res.resolve(resquestion))
  183. # Fetch the question
  184. question = await _readpkt(rdr)
  185. dnsrec = DNSRecord.parse(question)
  186. # Make sure we got the correct question
  187. self.assertEqual(dnsrec.get_q(), resquestion)
  188. # close the connection
  189. wrr.write_eof()
  190. # make sure it raises an error
  191. with self.assertRaises(RuntimeError):
  192. await ans
  193. @async_test
  194. async def test_questionresolver(self):
  195. client, server = _asyncsockpair()
  196. res = ServerResolver(await client)
  197. rdr, wrr = await server
  198. # constants
  199. resquestion = DNSQuestion('example.com')
  200. resanswer = RR.fromZone('example.com. A 192.0.2.10\nexample.com. A 192.0.2.11')
  201. # start the query
  202. ans = asyncio.create_task(res.resolve(resquestion))
  203. # Fetch the question
  204. question = await _readpkt(rdr)
  205. dnsrec = DNSRecord.parse(question)
  206. # Make sure we got the correct question
  207. self.assertEqual(dnsrec.get_q(), resquestion)
  208. # Generate the reply
  209. rep = dnsrec.reply()
  210. rep.add_answer(*resanswer)
  211. repbytes = rep.pack()
  212. # Send the reply
  213. _writepkt(wrr, repbytes)
  214. wrr.write_eof()
  215. # veryify the received answer
  216. ans = await ans
  217. self.assertEqual(ans, resanswer)
  218. @async_test
  219. async def test_outoforderanswer(self):
  220. client, server = _asyncsockpair()
  221. res = ServerResolver(await client)
  222. rdr, wrr = await server
  223. # constants
  224. resquestiona = DNSQuestion('example.com')
  225. resquestionb = DNSQuestion('example.net')
  226. resanswera = RR.fromZone('example.com. A 192.0.2.10\nexample.com. A 192.0.2.11')
  227. resanswerb = RR.fromZone('example.net. A 192.0.2.20\nexample.net. A 192.0.2.21')
  228. # start the first query
  229. ansa = asyncio.create_task(res.resolve(resquestiona))
  230. # Fetch the first question
  231. question = await _readpkt(rdr)
  232. dnsreca = DNSRecord.parse(question)
  233. # Make sure we got the correct question
  234. self.assertEqual(dnsreca.get_q(), resquestiona)
  235. # start the second query (before first has been answered)
  236. ansb = asyncio.create_task(res.resolve(resquestionb))
  237. # Fetch the second question
  238. question = await _readpkt(rdr)
  239. dnsrecb = DNSRecord.parse(question)
  240. # Make sure we got the correct question
  241. self.assertEqual(dnsrecb.get_q(), resquestionb)
  242. # Generate the second reply
  243. rep = dnsrecb.reply()
  244. rep.add_answer(*resanswerb)
  245. repbytes = rep.pack()
  246. # Send the reply
  247. _writepkt(wrr, repbytes)
  248. # Generate the first reply
  249. rep = dnsreca.reply()
  250. rep.add_answer(*resanswera)
  251. repbytes = rep.pack()
  252. # Send the reply
  253. _writepkt(wrr, repbytes)
  254. # Send a second reply, and make sure it is ignored
  255. _writepkt(wrr, repbytes)
  256. # close the connection
  257. wrr.write_eof()
  258. # veryify the first received answer
  259. ansa = await ansa
  260. self.assertEqual(ansa, resanswera)
  261. # veryify the second received answer
  262. ansb = await ansb
  263. self.assertEqual(ansb, resanswerb)