notify_out_test.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. # Copyright (C) 2010 Internet Systems Consortium.
  2. #
  3. # Permission to use, copy, modify, and distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. #
  7. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  8. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  9. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  10. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  11. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  12. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  13. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  14. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. import unittest
  16. import sys
  17. import os
  18. import tempfile
  19. import time
  20. import socket
  21. from isc.datasrc import sqlite3_ds
  22. from isc.notify import notify_out, SOCK_DATA
  23. import isc.log
  24. # our fake socket, where we can read and insert messages
  25. class MockSocket():
  26. def __init__(self):
  27. self._local_sock, self._remote_sock = socket.socketpair()
  28. def connect(self, to):
  29. pass
  30. def fileno(self):
  31. return self._local_sock.fileno()
  32. def close(self):
  33. self._local_sock.close()
  34. self._remote_sock.close()
  35. def sendto(self, data, flag, dst):
  36. return self._local_sock.send(data)
  37. def recvfrom(self, length):
  38. data = self._local_sock.recv(length)
  39. return (data, None)
  40. # provide a remote end which can write data to MockSocket for testing.
  41. def remote_end(self):
  42. return self._remote_sock
  43. # We subclass the ZoneNotifyInfo class we're testing here, only
  44. # to override the create_socket() method.
  45. class MockZoneNotifyInfo(notify_out.ZoneNotifyInfo):
  46. def create_socket(self, addrinfo):
  47. super().create_socket(addrinfo)
  48. # before replacing the underlying socket, remember the address family
  49. # of the original socket so that tests can check that.
  50. self.sock_family = self._sock.family
  51. self._sock.close()
  52. self._sock = MockSocket()
  53. return self._sock
  54. class TestZoneNotifyInfo(unittest.TestCase):
  55. def setUp(self):
  56. self.info = notify_out.ZoneNotifyInfo('example.net.', 'IN')
  57. def test_prepare_finish_notify_out(self):
  58. self.info.prepare_notify_out()
  59. self.assertNotEqual(self.info.notify_timeout, None)
  60. self.assertIsNone(self.info._notify_current)
  61. self.info.finish_notify_out()
  62. self.assertEqual(self.info._sock, None)
  63. self.assertEqual(self.info.notify_timeout, None)
  64. def test_set_next_notify_target(self):
  65. self.info.notify_slaves.append(('127.0.0.1', 53))
  66. self.info.notify_slaves.append(('1.1.1.1', 5353))
  67. self.info.prepare_notify_out()
  68. self.assertEqual(self.info.get_current_notify_target(), ('127.0.0.1', 53))
  69. self.info.set_next_notify_target()
  70. self.assertEqual(self.info.get_current_notify_target(), ('1.1.1.1', 5353))
  71. self.info.set_next_notify_target()
  72. self.assertIsNone(self.info.get_current_notify_target())
  73. temp_info = notify_out.ZoneNotifyInfo('example.com.', 'IN')
  74. temp_info.prepare_notify_out()
  75. self.assertIsNone(temp_info.get_current_notify_target())
  76. class TestNotifyOut(unittest.TestCase):
  77. def setUp(self):
  78. self._db_file = tempfile.NamedTemporaryFile(delete=False)
  79. sqlite3_ds.load(self._db_file.name, 'example.net.', self._example_net_data_reader)
  80. sqlite3_ds.load(self._db_file.name, 'example.com.', self._example_com_data_reader)
  81. self._notify = notify_out.NotifyOut(self._db_file.name)
  82. self._notify._notify_infos[('example.com.', 'IN')] = MockZoneNotifyInfo('example.com.', 'IN')
  83. self._notify._notify_infos[('example.com.', 'CH')] = MockZoneNotifyInfo('example.com.', 'CH')
  84. self._notify._notify_infos[('example.net.', 'IN')] = MockZoneNotifyInfo('example.net.', 'IN')
  85. self._notify._notify_infos[('example.org.', 'IN')] = MockZoneNotifyInfo('example.org.', 'IN')
  86. self._notify._notify_infos[('example.org.', 'CH')] = MockZoneNotifyInfo('example.org.', 'CH')
  87. net_info = self._notify._notify_infos[('example.net.', 'IN')]
  88. net_info.notify_slaves.append(('127.0.0.1', 53))
  89. net_info.notify_slaves.append(('1.1.1.1', 5353))
  90. com_info = self._notify._notify_infos[('example.com.', 'IN')]
  91. com_info.notify_slaves.append(('1.1.1.1', 5353))
  92. com_ch_info = self._notify._notify_infos[('example.com.', 'CH')]
  93. com_ch_info.notify_slaves.append(('1.1.1.1', 5353))
  94. def tearDown(self):
  95. self._db_file.close()
  96. os.unlink(self._db_file.name)
  97. def test_send_notify(self):
  98. notify_out._MAX_NOTIFY_NUM = 2
  99. self._notify._nonblock_event.clear()
  100. self._notify.send_notify('example.net')
  101. self.assertTrue(self._notify._nonblock_event.isSet())
  102. self.assertEqual(self._notify.notify_num, 1)
  103. self.assertEqual(self._notify._notifying_zones[0], ('example.net.', 'IN'))
  104. self._notify.send_notify('example.com')
  105. self.assertEqual(self._notify.notify_num, 2)
  106. self.assertEqual(self._notify._notifying_zones[1], ('example.com.', 'IN'))
  107. # notify_num is equal to MAX_NOTIFY_NUM, append it to waiting_zones list.
  108. self._notify._nonblock_event.clear()
  109. self._notify.send_notify('example.com', 'CH')
  110. # add waiting zones won't set nonblock_event.
  111. self.assertFalse(self._notify._nonblock_event.isSet())
  112. self.assertEqual(self._notify.notify_num, 2)
  113. self.assertEqual(1, len(self._notify._waiting_zones))
  114. # zone_id is already in notifying_zones list, append it to waiting_zones list.
  115. self._notify.send_notify('example.net')
  116. self.assertEqual(2, len(self._notify._waiting_zones))
  117. self.assertEqual(self._notify._waiting_zones[1], ('example.net.', 'IN'))
  118. # zone_id is already in waiting_zones list, skip it.
  119. self._notify.send_notify('example.net')
  120. self.assertEqual(2, len(self._notify._waiting_zones))
  121. # has no slave masters, skip it.
  122. self._notify.send_notify('example.org.', 'CH')
  123. self.assertEqual(self._notify.notify_num, 2)
  124. self.assertEqual(2, len(self._notify._waiting_zones))
  125. self._notify.send_notify('example.org.')
  126. self.assertEqual(self._notify.notify_num, 2)
  127. self.assertEqual(2, len(self._notify._waiting_zones))
  128. def test_wait_for_notify_reply(self):
  129. self._notify.send_notify('example.net.')
  130. self._notify.send_notify('example.com.')
  131. notify_out._MAX_NOTIFY_NUM = 2
  132. self._notify.send_notify('example.org.')
  133. replied_zones, timeout_zones = self._notify._wait_for_notify_reply()
  134. self.assertEqual(len(replied_zones), 0)
  135. self.assertEqual(len(timeout_zones), 2)
  136. # Trigger timeout events to "send" notifies via a mock socket
  137. for zone in timeout_zones:
  138. self._notify._zone_notify_handler(timeout_zones[zone],
  139. notify_out._EVENT_TIMEOUT)
  140. # Now make one socket be readable
  141. self._notify._notify_infos[('example.net.', 'IN')].notify_timeout = time.time() + 10
  142. self._notify._notify_infos[('example.com.', 'IN')].notify_timeout = time.time() + 10
  143. #Send some data to socket 12340, to make the target socket be readable
  144. self._notify._notify_infos[('example.net.', 'IN')]._sock.remote_end().send(b'data')
  145. replied_zones, timeout_zones = self._notify._wait_for_notify_reply()
  146. self.assertEqual(len(replied_zones), 1)
  147. self.assertEqual(len(timeout_zones), 1)
  148. self.assertTrue(('example.net.', 'IN') in replied_zones.keys())
  149. self.assertTrue(('example.com.', 'IN') in timeout_zones.keys())
  150. self.assertLess(time.time(), self._notify._notify_infos[('example.com.', 'IN')].notify_timeout)
  151. def test_wait_for_notify_reply_2(self):
  152. # Test the returned value when the read_side socket is readable.
  153. self._notify.send_notify('example.net.')
  154. self._notify.send_notify('example.com.')
  155. # Now make one socket be readable
  156. self._notify._notify_infos[('example.net.', 'IN')].notify_timeout = time.time() + 10
  157. self._notify._notify_infos[('example.com.', 'IN')].notify_timeout = time.time() + 10
  158. self._notify._read_sock, self._notify._write_sock = socket.socketpair()
  159. self._notify._write_sock.send(SOCK_DATA)
  160. replied_zones, timeout_zones = self._notify._wait_for_notify_reply()
  161. self.assertEqual(0, len(replied_zones))
  162. self.assertEqual(0, len(timeout_zones))
  163. def test_notify_next_target(self):
  164. self._notify.send_notify('example.net.')
  165. self._notify.send_notify('example.com.')
  166. notify_out._MAX_NOTIFY_NUM = 2
  167. # zone example.org. has no slave servers.
  168. self._notify.send_notify('example.org.')
  169. self._notify.send_notify('example.com.', 'CH')
  170. info = self._notify._notify_infos[('example.net.', 'IN')]
  171. self._notify._notify_next_target(info)
  172. self.assertEqual(0, info.notify_try_num)
  173. self.assertEqual(info.get_current_notify_target(), ('1.1.1.1', 5353))
  174. self.assertEqual(2, self._notify.notify_num)
  175. self.assertEqual(1, len(self._notify._waiting_zones))
  176. self._notify._notify_next_target(info)
  177. self.assertEqual(0, info.notify_try_num)
  178. self.assertIsNone(info.get_current_notify_target())
  179. self.assertEqual(2, self._notify.notify_num)
  180. self.assertEqual(0, len(self._notify._waiting_zones))
  181. example_com_info = self._notify._notify_infos[('example.com.', 'IN')]
  182. self._notify._notify_next_target(example_com_info)
  183. self.assertEqual(1, self._notify.notify_num)
  184. self.assertEqual(1, len(self._notify._notifying_zones))
  185. self.assertEqual(0, len(self._notify._waiting_zones))
  186. def test_handle_notify_reply(self):
  187. fake_address = ('192.0.2.1', 53)
  188. self.assertEqual(notify_out._BAD_REPLY_PACKET, self._notify._handle_notify_reply(None, b'badmsg', fake_address))
  189. example_com_info = self._notify._notify_infos[('example.com.', 'IN')]
  190. example_com_info.notify_msg_id = 0X2f18
  191. # test with right notify reply message
  192. data = b'\x2f\x18\xa0\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\03com\x00\x00\x06\x00\x01'
  193. self.assertEqual(notify_out._REPLY_OK, self._notify._handle_notify_reply(example_com_info, data, fake_address))
  194. # test with unright query id
  195. data = b'\x2e\x18\xa0\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\03com\x00\x00\x06\x00\x01'
  196. self.assertEqual(notify_out._BAD_QUERY_ID, self._notify._handle_notify_reply(example_com_info, data, fake_address))
  197. # test with unright query name
  198. data = b'\x2f\x18\xa0\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\03net\x00\x00\x06\x00\x01'
  199. self.assertEqual(notify_out._BAD_QUERY_NAME, self._notify._handle_notify_reply(example_com_info, data, fake_address))
  200. # test with unright opcode
  201. data = b'\x2f\x18\x80\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\03com\x00\x00\x06\x00\x01'
  202. self.assertEqual(notify_out._BAD_OPCODE, self._notify._handle_notify_reply(example_com_info, data, fake_address))
  203. # test with unright qr
  204. data = b'\x2f\x18\x10\x10\x00\x01\x00\x00\x00\x00\x00\x00\x07example\03com\x00\x00\x06\x00\x01'
  205. self.assertEqual(notify_out._BAD_QR, self._notify._handle_notify_reply(example_com_info, data, fake_address))
  206. def test_send_notify_message_udp_ipv4(self):
  207. example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
  208. example_com_info.prepare_notify_out()
  209. ret = self._notify._send_notify_message_udp(example_com_info,
  210. ('192.0.2.1', 53))
  211. self.assertTrue(ret)
  212. self.assertEqual(socket.AF_INET, example_com_info.sock_family)
  213. def test_send_notify_message_udp_ipv6(self):
  214. example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
  215. ret = self._notify._send_notify_message_udp(example_com_info,
  216. ('2001:db8::53', 53))
  217. self.assertTrue(ret)
  218. self.assertEqual(socket.AF_INET6, example_com_info.sock_family)
  219. def test_send_notify_message_with_bogus_address(self):
  220. example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
  221. # As long as the underlying data source validates RDATA this shouldn't
  222. # happen, but right now it's not actually the case. Even if the
  223. # data source does its job, it's prudent to confirm the behavior for
  224. # an unexpected case.
  225. ret = self._notify._send_notify_message_udp(example_com_info,
  226. ('invalid', 53))
  227. self.assertFalse(ret)
  228. def test_zone_notify_handler(self):
  229. old_send_msg = self._notify._send_notify_message_udp
  230. def _fake_send_notify_message_udp(va1, va2):
  231. pass
  232. self._notify._send_notify_message_udp = _fake_send_notify_message_udp
  233. self._notify.send_notify('example.net.')
  234. self._notify.send_notify('example.com.')
  235. notify_out._MAX_NOTIFY_NUM = 2
  236. self._notify.send_notify('example.org.')
  237. example_net_info = self._notify._notify_infos[('example.net.', 'IN')]
  238. example_net_info.prepare_notify_out()
  239. example_net_info.notify_try_num = 2
  240. self._notify._zone_notify_handler(example_net_info, notify_out._EVENT_TIMEOUT)
  241. self.assertEqual(3, example_net_info.notify_try_num)
  242. time1 = example_net_info.notify_timeout
  243. self._notify._zone_notify_handler(example_net_info, notify_out._EVENT_TIMEOUT)
  244. self.assertEqual(4, example_net_info.notify_try_num)
  245. self.assertGreater(example_net_info.notify_timeout, time1 + 2) # bigger than 2 seconds
  246. cur_tgt = example_net_info._notify_current
  247. example_net_info.notify_try_num = notify_out._MAX_NOTIFY_TRY_NUM
  248. self._notify._zone_notify_handler(example_net_info, notify_out._EVENT_NONE)
  249. self.assertNotEqual(cur_tgt, example_net_info._notify_current)
  250. cur_tgt = example_net_info._notify_current
  251. example_net_info.create_socket('127.0.0.1')
  252. # dns message, will result in bad_qid, but what we are testing
  253. # here is whether handle_notify_reply is called correctly
  254. example_net_info._sock.remote_end().send(b'\x2f\x18\xa0\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\03com\x00\x00\x06\x00\x01')
  255. self._notify._zone_notify_handler(example_net_info, notify_out._EVENT_READ)
  256. self.assertNotEqual(cur_tgt, example_net_info._notify_current)
  257. def _example_net_data_reader(self):
  258. zone_data = [
  259. ('example.net.', '1000', 'IN', 'SOA', 'a.dns.example.net. mail.example.net. 1 1 1 1 1'),
  260. ('example.net.', '1000', 'IN', 'NS', 'a.dns.example.net.'),
  261. ('example.net.', '1000', 'IN', 'NS', 'b.dns.example.net.'),
  262. ('example.net.', '1000', 'IN', 'NS', 'c.dns.example.net.'),
  263. ('a.dns.example.net.', '1000', 'IN', 'A', '1.1.1.1'),
  264. ('a.dns.example.net.', '1000', 'IN', 'AAAA', '2:2::2:2'),
  265. ('b.dns.example.net.', '1000', 'IN', 'A', '3.3.3.3'),
  266. ('b.dns.example.net.', '1000', 'IN', 'AAAA', '4:4::4:4'),
  267. ('b.dns.example.net.', '1000', 'IN', 'AAAA', '5:5::5:5'),
  268. ('c.dns.example.net.', '1000', 'IN', 'A', '6.6.6.6'),
  269. ('c.dns.example.net.', '1000', 'IN', 'A', '7.7.7.7'),
  270. ('c.dns.example.net.', '1000', 'IN', 'AAAA', '8:8::8:8')]
  271. for item in zone_data:
  272. yield item
  273. def _example_com_data_reader(self):
  274. zone_data = [
  275. ('example.com.', '1000', 'IN', 'SOA', 'a.dns.example.com. mail.example.com. 1 1 1 1 1'),
  276. ('example.com.', '1000', 'IN', 'NS', 'a.dns.example.com.'),
  277. ('example.com.', '1000', 'IN', 'NS', 'b.dns.example.com.'),
  278. ('example.com.', '1000', 'IN', 'NS', 'c.dns.example.com.'),
  279. ('a.dns.example.com.', '1000', 'IN', 'A', '1.1.1.1'),
  280. ('b.dns.example.com.', '1000', 'IN', 'A', '3.3.3.3'),
  281. ('b.dns.example.com.', '1000', 'IN', 'AAAA', '4:4::4:4'),
  282. ('b.dns.example.com.', '1000', 'IN', 'AAAA', '5:5::5:5')]
  283. for item in zone_data:
  284. yield item
  285. def test_get_notify_slaves_from_ns(self):
  286. records = self._notify._get_notify_slaves_from_ns('example.net.')
  287. self.assertEqual(6, len(records))
  288. self.assertEqual('8:8::8:8', records[5])
  289. self.assertEqual('7.7.7.7', records[4])
  290. self.assertEqual('6.6.6.6', records[3])
  291. self.assertEqual('5:5::5:5', records[2])
  292. self.assertEqual('4:4::4:4', records[1])
  293. self.assertEqual('3.3.3.3', records[0])
  294. records = self._notify._get_notify_slaves_from_ns('example.com.')
  295. self.assertEqual(3, len(records))
  296. self.assertEqual('5:5::5:5', records[2])
  297. self.assertEqual('4:4::4:4', records[1])
  298. self.assertEqual('3.3.3.3', records[0])
  299. def test_init_notify_out(self):
  300. self._notify._init_notify_out(self._db_file.name)
  301. self.assertListEqual([('3.3.3.3', 53), ('4:4::4:4', 53), ('5:5::5:5', 53)],
  302. self._notify._notify_infos[('example.com.', 'IN')].notify_slaves)
  303. def test_prepare_select_info(self):
  304. timeout, valid_fds, notifying_zones = self._notify._prepare_select_info()
  305. self.assertEqual(None, timeout)
  306. self.assertListEqual([], valid_fds)
  307. self._notify._notify_infos[('example.net.', 'IN')]._sock = 1
  308. self._notify._notify_infos[('example.net.', 'IN')].notify_timeout = time.time() + 5
  309. timeout, valid_fds, notifying_zones = self._notify._prepare_select_info()
  310. self.assertGreater(timeout, 0)
  311. self.assertListEqual([1], valid_fds)
  312. self._notify._notify_infos[('example.net.', 'IN')]._sock = 1
  313. self._notify._notify_infos[('example.net.', 'IN')].notify_timeout = time.time() - 5
  314. timeout, valid_fds, notifying_zones = self._notify._prepare_select_info()
  315. self.assertEqual(timeout, 0)
  316. self.assertListEqual([1], valid_fds)
  317. self._notify._notify_infos[('example.com.', 'IN')]._sock = 2
  318. self._notify._notify_infos[('example.com.', 'IN')].notify_timeout = time.time() + 5
  319. timeout, valid_fds, notifying_zones = self._notify._prepare_select_info()
  320. self.assertEqual(timeout, 0)
  321. self.assertListEqual([2, 1], valid_fds)
  322. def test_shutdown(self):
  323. thread = self._notify.dispatcher()
  324. self.assertTrue(thread.is_alive())
  325. # nonblock_event won't be setted since there are no notifying zones.
  326. self.assertFalse(self._notify._nonblock_event.isSet())
  327. # set nonblock_event manually
  328. self._notify._nonblock_event.set()
  329. # nonblock_event will be cleared soon since there are no notifying zones.
  330. while (self._notify._nonblock_event.isSet()):
  331. pass
  332. # send notify
  333. example_net_info = self._notify._notify_infos[('example.net.', 'IN')]
  334. example_net_info.notify_slaves = [('127.0.0.1', 53)]
  335. example_net_info.create_socket('127.0.0.1')
  336. self._notify.send_notify('example.net')
  337. self.assertTrue(self._notify._nonblock_event.isSet())
  338. # set notify_try_num to _MAX_NOTIFY_TRY_NUM, zone 'example.net' will be removed
  339. # from notifying zones soon and nonblock_event will be cleared since there is no
  340. # notifying zone left.
  341. example_net_info.notify_try_num = notify_out._MAX_NOTIFY_TRY_NUM
  342. while (self._notify._nonblock_event.isSet()):
  343. pass
  344. self.assertFalse(self._notify._nonblock_event.isSet())
  345. self._notify.shutdown()
  346. # nonblock_event should have been setted to stop waiting.
  347. self.assertTrue(self._notify._nonblock_event.isSet())
  348. self.assertFalse(thread.is_alive())
  349. if __name__== "__main__":
  350. isc.log.init("bind10")
  351. unittest.main()