pytest_mdns_app.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. # SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
  2. # SPDX-License-Identifier: Apache-2.0
  3. import re
  4. import select
  5. import socket
  6. import struct
  7. import time
  8. from threading import Event, Thread
  9. import dpkt
  10. import dpkt.dns
  11. import pytest
  12. UDP_PORT = 5353
  13. MCAST_GRP = '224.0.0.251'
  14. # This service is created from esp board startup
  15. SERVICE_NAME = u'ESP32-WebServer._http._tcp.local'
  16. SUB_SERVICE_NAME = u'_server._sub._http._tcp.local'
  17. # This host name answer sent by host, when there is query from board
  18. HOST_NAME = u'tinytester.local'
  19. # This servce answer sent by host, when there is query from board
  20. MDNS_HOST_SERVICE = u'ESP32._http._tcp.local'
  21. # Number of retries to receive mdns answear
  22. RETRY_COUNT = 10
  23. stop_mdns_listener = Event()
  24. start_mdns_listener = Event()
  25. esp_service_answered = Event()
  26. esp_sub_service_answered = Event()
  27. esp_host_answered = Event()
  28. esp_delegated_host_answered = Event()
  29. @pytest.mark.skip
  30. # Get query of ESP32-WebServer._http._tcp.local service
  31. def get_mdns_service_query(service): # type:(str) -> dpkt.dns.Msg
  32. dns = dpkt.dns.DNS()
  33. dns.op = dpkt.dns.DNS_QR | dpkt.dns.DNS_AA
  34. dns.rcode = dpkt.dns.DNS_RCODE_NOERR
  35. arr = dpkt.dns.DNS.RR()
  36. arr.cls = dpkt.dns.DNS_IN
  37. arr.type = dpkt.dns.DNS_SRV
  38. arr.name = service
  39. arr.target = socket.inet_aton('127.0.0.1')
  40. arr.srvname = service
  41. dns.qd.append(arr)
  42. print('Created mdns service query: {} '.format(dns.__repr__()))
  43. return dns.pack()
  44. @pytest.mark.skip
  45. # Get query of _server_.sub._http._tcp.local sub service
  46. def get_mdns_sub_service_query(sub_service): # type:(str) -> dpkt.dns.Msg
  47. dns = dpkt.dns.DNS()
  48. dns.op = dpkt.dns.DNS_QR | dpkt.dns.DNS_AA
  49. dns.rcode = dpkt.dns.DNS_RCODE_NOERR
  50. arr = dpkt.dns.DNS.RR()
  51. arr.cls = dpkt.dns.DNS_IN
  52. arr.type = dpkt.dns.DNS_PTR
  53. arr.name = sub_service
  54. arr.target = socket.inet_aton('127.0.0.1')
  55. arr.ptrname = sub_service
  56. dns.qd.append(arr)
  57. print('Created mdns subtype service query: {} '.format(dns.__repr__()))
  58. return dns.pack()
  59. @pytest.mark.skip
  60. # Get query for host resolution
  61. def get_dns_query_for_esp(esp_host): # type:(str) -> dpkt.dns.Msg
  62. dns = dpkt.dns.DNS()
  63. arr = dpkt.dns.DNS.RR()
  64. arr.cls = dpkt.dns.DNS_IN
  65. arr.name = esp_host + u'.local'
  66. dns.qd.append(arr)
  67. print('Created query for esp host: {} '.format(dns.__repr__()))
  68. return dns.pack()
  69. @pytest.mark.skip
  70. # Get mdns answer for host resoloution
  71. def get_dns_answer_to_mdns(tester_host): # type:(str) -> dpkt.dns.Msg
  72. dns = dpkt.dns.DNS()
  73. dns.op = dpkt.dns.DNS_QR | dpkt.dns.DNS_AA
  74. dns.rcode = dpkt.dns.DNS_RCODE_NOERR
  75. arr = dpkt.dns.DNS.RR()
  76. arr.cls = dpkt.dns.DNS_IN
  77. arr.type = dpkt.dns.DNS_A
  78. arr.name = tester_host
  79. arr.ip = socket.inet_aton('127.0.0.1')
  80. dns.an.append(arr)
  81. print('Created answer to mdns query: {} '.format(dns.__repr__()))
  82. return dns.pack()
  83. @pytest.mark.skip
  84. # Get mdns answer for service query
  85. def get_dns_answer_to_service_query(
  86. mdns_service): # type:(str) -> dpkt.dns.Msg
  87. dns = dpkt.dns.DNS()
  88. dns.op = dpkt.dns.DNS_QR | dpkt.dns.DNS_AA
  89. dns.rcode = dpkt.dns.DNS_RCODE_NOERR
  90. arr = dpkt.dns.DNS.RR()
  91. arr.name = mdns_service
  92. arr.cls = dpkt.dns.DNS_IN
  93. arr.type = dpkt.dns.DNS_SRV
  94. arr.priority = 0
  95. arr.weight = 0
  96. arr.port = 100
  97. arr.srvname = mdns_service
  98. arr.ip = socket.inet_aton('127.0.0.1')
  99. dns.an.append(arr)
  100. print('Created answer to mdns query: {} '.format(dns.__repr__()))
  101. return dns.pack()
  102. @pytest.mark.skip
  103. def mdns_listener(esp_host): # type:(str) -> None
  104. print('mdns_listener thread started')
  105. UDP_IP = '0.0.0.0'
  106. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  107. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  108. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
  109. sock.setblocking(False)
  110. sock.bind((UDP_IP, UDP_PORT))
  111. mreq = struct.pack('4sl', socket.inet_aton(MCAST_GRP), socket.INADDR_ANY)
  112. sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
  113. last_query_timepoint = time.time()
  114. QUERY_TIMEOUT = 0.2
  115. while not stop_mdns_listener.is_set():
  116. try:
  117. start_mdns_listener.set()
  118. current_time = time.time()
  119. if current_time - last_query_timepoint > QUERY_TIMEOUT:
  120. last_query_timepoint = current_time
  121. timeout = max(
  122. 0, QUERY_TIMEOUT - (current_time - last_query_timepoint))
  123. read_socks, _, _ = select.select([sock], [], [], timeout)
  124. if not read_socks:
  125. continue
  126. data, _ = sock.recvfrom(1024)
  127. dns = dpkt.dns.DNS(data)
  128. # Receives queries from esp board and sends answers
  129. if len(dns.qd) > 0:
  130. if dns.qd[0].name == HOST_NAME:
  131. print('Received query: {} '.format(dns.__repr__()))
  132. sock.sendto(get_dns_answer_to_mdns(HOST_NAME),
  133. (MCAST_GRP, UDP_PORT))
  134. if dns.qd[0].name == HOST_NAME:
  135. print('Received query: {} '.format(dns.__repr__()))
  136. sock.sendto(get_dns_answer_to_mdns(HOST_NAME),
  137. (MCAST_GRP, UDP_PORT))
  138. if dns.qd[0].name == MDNS_HOST_SERVICE:
  139. print('Received query: {} '.format(dns.__repr__()))
  140. sock.sendto(
  141. get_dns_answer_to_service_query(MDNS_HOST_SERVICE),
  142. (MCAST_GRP, UDP_PORT))
  143. # Receives answers from esp board and sets event flags for python test cases
  144. if len(dns.an) == 1:
  145. if dns.an[0].name.startswith(SERVICE_NAME):
  146. print('Received answer to service query: {}'.format(
  147. dns.__repr__()))
  148. esp_service_answered.set()
  149. if len(dns.an) > 1:
  150. if dns.an[1].name.startswith(SUB_SERVICE_NAME):
  151. print('Received answer for sub service query: {}'.format(
  152. dns.__repr__()))
  153. esp_sub_service_answered.set()
  154. if len(dns.an) > 0 and dns.an[0].type == dpkt.dns.DNS_A:
  155. if dns.an[0].name == esp_host + u'.local':
  156. print('Received answer to esp32-mdns query: {}'.format(
  157. dns.__repr__()))
  158. esp_host_answered.set()
  159. if dns.an[0].name == esp_host + u'-delegated.local':
  160. print('Received answer to esp32-mdns-delegate query: {}'.
  161. format(dns.__repr__()))
  162. esp_delegated_host_answered.set()
  163. except socket.timeout:
  164. break
  165. except dpkt.UnpackError:
  166. continue
  167. @pytest.mark.skip
  168. def create_socket(): # type:() -> socket.socket
  169. UDP_IP = '0.0.0.0'
  170. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  171. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  172. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
  173. sock.setblocking(False)
  174. sock.bind((UDP_IP, UDP_PORT))
  175. mreq = struct.pack('4sl', socket.inet_aton(MCAST_GRP), socket.INADDR_ANY)
  176. sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
  177. return sock
  178. @pytest.mark.skip
  179. def test_query_dns_http_service(service): # type: (str) -> None
  180. print('SRV: Query {}'.format(service))
  181. sock = create_socket()
  182. packet = get_mdns_service_query(service)
  183. for _ in range(RETRY_COUNT):
  184. if esp_service_answered.wait(timeout=25):
  185. break
  186. sock.sendto(packet, (MCAST_GRP, UDP_PORT))
  187. else:
  188. raise RuntimeError(
  189. 'Test has failed: did not receive mdns answer within timeout')
  190. @pytest.mark.skip
  191. def test_query_dns_sub_service(sub_service): # type: (str) -> None
  192. print('PTR: Query {}'.format(sub_service))
  193. sock = create_socket()
  194. packet = get_mdns_sub_service_query(sub_service)
  195. for _ in range(RETRY_COUNT):
  196. if esp_sub_service_answered.wait(timeout=25):
  197. break
  198. sock.sendto(packet, (MCAST_GRP, UDP_PORT))
  199. else:
  200. raise RuntimeError(
  201. 'Test has failed: did not receive mdns answer within timeout')
  202. @pytest.mark.skip
  203. def test_query_dns_host(esp_host): # type: (str) -> None
  204. print('A: {}'.format(esp_host))
  205. sock = create_socket()
  206. packet = get_dns_query_for_esp(esp_host)
  207. for _ in range(RETRY_COUNT):
  208. if esp_host_answered.wait(timeout=25):
  209. break
  210. sock.sendto(packet, (MCAST_GRP, UDP_PORT))
  211. else:
  212. raise RuntimeError(
  213. 'Test has failed: did not receive mdns answer within timeout')
  214. @pytest.mark.skip
  215. def test_query_dns_host_delegated(esp_host): # type: (str) -> None
  216. print('A: {}'.format(esp_host))
  217. sock = create_socket()
  218. packet = get_dns_query_for_esp(esp_host + '-delegated')
  219. for _ in range(RETRY_COUNT):
  220. if esp_delegated_host_answered.wait(timeout=25):
  221. break
  222. sock.sendto(packet, (MCAST_GRP, UDP_PORT))
  223. else:
  224. raise RuntimeError(
  225. 'Test has failed: did not receive mdns answer within timeout')
  226. @pytest.mark.esp32
  227. @pytest.mark.esp32s2
  228. @pytest.mark.esp32c3
  229. @pytest.mark.generic
  230. def test_app_esp_mdns(dut):
  231. # 1. get the dut host name (and IP address)
  232. specific_host = dut.expect(
  233. re.compile(b'mdns hostname set to: \[([^\]]+)\]'), # noqa: W605
  234. timeout=30).group(1).decode()
  235. esp_ip = dut.expect(
  236. re.compile(
  237. b' IPv4 address: ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)'), # noqa: W605
  238. timeout=30).group(1).decode()
  239. print('Got IP={}'.format(esp_ip))
  240. mdns_responder = Thread(target=mdns_listener, args=(str(specific_host), ))
  241. def start_case(case, desc, result): # type: (str, str, str) -> None
  242. print('Starting {}: {}'.format(case, desc))
  243. dut.write(case)
  244. res = bytes(result, encoding='utf8')
  245. dut.expect(re.compile(res), timeout=30)
  246. try:
  247. # start dns listener thread
  248. mdns_responder.start()
  249. # wait untill mdns listener thred started
  250. if not start_mdns_listener.wait(timeout=5):
  251. raise ValueError(
  252. 'Test has failed: mdns listener thread did not start')
  253. # query dns service from host, answer should be received from esp board
  254. test_query_dns_http_service(SERVICE_NAME)
  255. # query dns sub-service from host, answer should be received from esp board
  256. test_query_dns_sub_service(SUB_SERVICE_NAME)
  257. # query dns host name, answer should be received from esp board
  258. test_query_dns_host(specific_host)
  259. # query dns host name delegated, answer should be received from esp board
  260. test_query_dns_host_delegated(specific_host)
  261. # query service from esp board, answer should be received from host
  262. start_case('CONFIG_TEST_QUERY_SERVICE',
  263. 'Query SRV ESP32._http._tcp.local', 'SRV:ESP32')
  264. # query dns-host from esp board, answer should be received from host
  265. start_case('CONFIG_TEST_QUERY_HOST', 'Query tinytester.local',
  266. 'tinytester.local resolved to: 127.0.0.1')
  267. # query dns-host aynchrounusely from esp board, answer should be received from host
  268. start_case('CONFIG_TEST_QUERY_HOST_ASYNC',
  269. 'Query tinytester.local async',
  270. 'Async query resolved to A:127.0.0.1')
  271. finally:
  272. stop_mdns_listener.set()
  273. mdns_responder.join()