verify-qlog.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. #!/usr/bin/env python3
  2. #
  3. # Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
  4. #
  5. # Licensed under the Apache License 2.0 (the "License"). You may not use
  6. # this file except in compliance with the License. You can obtain a copy
  7. # in the file LICENSE in the source distribution or at
  8. # https://www.openssl.org/source/license.html
  9. import sys, os, os.path, glob, json, re
  10. re_version = re.compile(r'''^OpenSSL/[0-9]+\.[0-9]\.[0-9](-[^ ]+)? ([^)]+)''')
  11. class Unexpected(Exception):
  12. def __init__(self, filename, msg):
  13. Exception.__init__(self, f"file {repr(filename)}: {msg}")
  14. class Malformed(Exception):
  15. def __init__(self, line, msg):
  16. Exception.__init__(self, f"{line}: {msg}")
  17. event_type_counts = {}
  18. frame_type_counts = {}
  19. def load_file(filename):
  20. objs = []
  21. with open(filename, 'r') as fi:
  22. for line in fi:
  23. if line[0] != '\x1e':
  24. raise Unexpected(filename, "expected JSON-SEQ leader")
  25. line = line[1:]
  26. try:
  27. objs.append(json.loads(line))
  28. except:
  29. fi.seek(0)
  30. fdata = fi.read()
  31. print(fdata)
  32. raise Malformed(line, "Malformed json input")
  33. return objs
  34. def check_header(filename, hdr):
  35. if not 'qlog_format' in hdr:
  36. raise Unexpected(filename, "must have qlog_format in header line")
  37. if not 'qlog_version' in hdr:
  38. raise Unexpected(filename, "must have qlog_version in header line")
  39. if not 'trace' in hdr:
  40. raise Unexpected(filename, "must have trace in header line")
  41. hdr_trace = hdr["trace"]
  42. if not 'common_fields' in hdr_trace:
  43. raise Unexpected(filename, "must have common_fields in header line")
  44. if not 'vantage_point' in hdr_trace:
  45. raise Unexpected(filename, "must have vantage_point in header line")
  46. if hdr_trace["vantage_point"].get('type') not in ('client', 'server'):
  47. raise Unexpected(filename, "unexpected vantage_point")
  48. vp_name = hdr_trace["vantage_point"].get('name')
  49. if type(vp_name) != str:
  50. raise Unexpected(filename, "expected vantage_point name")
  51. if not re_version.match(vp_name):
  52. raise Unexpected(filename, "expected correct vantage_point format")
  53. hdr_common_fields = hdr_trace["common_fields"]
  54. if hdr_common_fields.get("time_format") != "delta":
  55. raise Unexpected(filename, "must have expected time_format")
  56. if hdr_common_fields.get("protocol_type") != ["QUIC"]:
  57. raise Unexpected(filename, "must have expected protocol_type")
  58. if hdr["qlog_format"] != "JSON-SEQ":
  59. raise Unexpected(filename, "unexpected qlog_format")
  60. if hdr["qlog_version"] != "0.3":
  61. raise Unexpected(filename, "unexpected qlog_version")
  62. def check_event(filename, event):
  63. name = event.get("name")
  64. if type(name) != str:
  65. raise Unexpected(filename, "expected event to have name")
  66. event_type_counts.setdefault(name, 0)
  67. event_type_counts[name] += 1
  68. if type(event.get("time")) != int:
  69. raise Unexpected(filename, "expected event to have time")
  70. data = event.get('data')
  71. if type(data) != dict:
  72. raise Unexpected(filename, "expected event to have data")
  73. if "qlog_format" in event:
  74. raise Unexpected(filename, "event must not be header line")
  75. if name in ('transport:packet_sent', 'transport:packet_received'):
  76. check_packet_header(filename, event, data.get('header'))
  77. datagram_id = data.get('datagram_id')
  78. if type(datagram_id) != int:
  79. raise Unexpected(filename, "datagram ID must be integer")
  80. for frame in data.get('frames', []):
  81. check_frame(filename, event, frame)
  82. def check_packet_header(filename, event, header):
  83. if type(header) != dict:
  84. raise Unexpected(filename, "expected object for packet header")
  85. # packet type -> has frames?
  86. packet_types = {
  87. 'version_negotiation': False,
  88. 'retry': False,
  89. 'initial': True,
  90. 'handshake': True,
  91. '0RTT': True,
  92. '1RTT': True,
  93. }
  94. data = event['data']
  95. packet_type = header.get('packet_type')
  96. if packet_type not in packet_types:
  97. raise Unexpected(filename, f"unexpected packet type: {packet_type}")
  98. if type(header.get('dcid')) != str:
  99. raise Unexpected(filename, "expected packet event to have DCID")
  100. if packet_type != '1RTT' and type(header.get('scid')) != str:
  101. raise Unexpected(filename, "expected packet event to have SCID")
  102. if type(data.get('datagram_id')) != int:
  103. raise Unexpected(filename, "expected packet event to have datagram ID")
  104. if packet_types[packet_type]:
  105. if type(header.get('packet_number')) != int:
  106. raise Unexpected(filename, f"expected packet event to have packet number")
  107. if type(data.get('frames')) != list:
  108. raise Unexpected(filename, "expected packet event to have frames")
  109. def check_frame(filename, event, frame):
  110. frame_type = frame.get('frame_type')
  111. if type(frame_type) != str:
  112. raise Unexpected(filename, "frame must have frame_type field")
  113. frame_type_counts.setdefault(event['name'], {})
  114. counts = frame_type_counts[event['name']]
  115. counts.setdefault(frame_type, 0)
  116. counts[frame_type] += 1
  117. def check_file(filename):
  118. objs = load_file(filename)
  119. if len(objs) < 2:
  120. raise Unexpected(filename, "must have at least two objects")
  121. check_header(filename, objs[0])
  122. for event in objs[1:]:
  123. check_event(filename, event)
  124. def run():
  125. num_files = 0
  126. # Check each file for validity.
  127. qlogdir = os.environ['QLOGDIR']
  128. for filename in glob.glob(os.path.join(qlogdir, '*.sqlog')):
  129. check_file(filename)
  130. num_files += 1
  131. # Check that all supported events were generated.
  132. required_events = (
  133. "transport:parameters_set",
  134. "connectivity:connection_state_updated",
  135. "connectivity:connection_started",
  136. "transport:packet_sent",
  137. "transport:packet_received",
  138. "connectivity:connection_closed"
  139. )
  140. if num_files < 300:
  141. raise Unexpected(qlogdir, f"unexpectedly few output files: {num_files}")
  142. for required_event in required_events:
  143. count = event_type_counts.get(required_event, 0)
  144. if count < 100:
  145. raise Unexpected(qlogdir, f"unexpectedly low count of event '{required_event}': got {count}")
  146. # For each direction, ensure that at least one of the tests we run generated
  147. # a given frame type.
  148. required_frame_types = (
  149. "padding",
  150. "ping",
  151. "ack",
  152. "crypto",
  153. "handshake_done",
  154. "connection_close",
  155. "path_challenge",
  156. "path_response",
  157. "stream",
  158. "reset_stream",
  159. "stop_sending",
  160. "new_connection_id",
  161. "retire_connection_id",
  162. "max_streams",
  163. "streams_blocked",
  164. "max_stream_data",
  165. "stream_data_blocked",
  166. "max_data",
  167. "data_blocked",
  168. "new_token",
  169. )
  170. for required_frame_type in required_frame_types:
  171. sent_count = frame_type_counts.get('transport:packet_sent', {}).get(required_frame_type, 0)
  172. if sent_count < 1:
  173. raise Unexpected(qlogdir, f"unexpectedly did not send any '{required_frame_type}' frames")
  174. received_count = frame_type_counts.get('transport:packet_received', {}).get(required_frame_type, 0)
  175. if received_count < 1:
  176. raise Unexpected(qlogdir, f"unexpectedly did not receive any '{required_frame_type}' frames")
  177. return 0
  178. if __name__ == '__main__':
  179. sys.exit(run())