diff --git a/circuitmatter/__init__.py b/circuitmatter/__init__.py index 68ffb72..e27b8be 100644 --- a/circuitmatter/__init__.py +++ b/circuitmatter/__init__.py @@ -1,8 +1,10 @@ """Pure Python implementation of the Matter IOT protocol.""" +import binascii import enum import pathlib import json +import os import struct import time @@ -10,21 +12,6 @@ __version__ = "0.0.0" -# descriminator = 3840 -# avahi = subprocess.Popen(["avahi-publish-service", "-v", f"--subtype=_L{descriminator}._sub._matterc._udp", "--subtype=_CM._sub._matterc._udp", "FA93546B21F5FB54", "_matterc._udp", "5540", "PI=", "PH=33", "CM=1", f"D={descriminator}", "CRI=3000", "CRA=4000", "T=1", "VP=65521+32769"]) - -# # Define the UDP IP address and port -# UDP_IP = "::" # Listen on all available network interfaces -# UDP_PORT = 5540 - -# # Create the UDP socket -# sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) - -# # Bind the socket to the IP and port -# sock.bind((UDP_IP, UDP_PORT)) - -# print(f"Listening on UDP port {UDP_PORT}") - # Section 4.11.2 MSG_COUNTER_WINDOW_SIZE = 32 MSG_COUNTER_SYNC_REQ_JITTER_MS = 500 @@ -243,11 +230,54 @@ def process_counter(self, counter) -> bool: return False +class Exchange: + def __init__(self, initiator: bool, exchange_id: int, protocols): + self.initiator = initiator + self.exchange_id = exchange_id + self.protocols = protocols + + self.pending_acknowledgement = None + self.next_retransmission_time = None + self.pending_retransmission = None + + def send(self, message): + pass + + def receive(self, message) -> bool: + """Process the message and return if the packet should be dropped.""" + if message.protocol_id not in self.protocols: + # Drop messages that don't match the protocols we're waiting for. + return True + + # Section 4.10.5.2.1 + if message.exchange_flags & ExchangeFlags.A: + if message.acknowledged_message_counter is None: + # Drop messages that are missing an acknowledgement counter. + return True + if self.pending_acknowledgement is None: + # Drop messages that are not waiting for an acknowledgement. + return True + if message.acknowledged_message_counter != self.pending_acknowledgement: + # Drop messages that have the wrong acknowledgement counter. + return True + self.pending_acknowledgement = None + self.pending_retransmission = None + self.next_retransmission_time = None + + # Section 4.10.5.2.2 + # if message.exchange_flags & ExchangeFlags.R: + # if message + if message.duplicate: + return True + return False + + class UnsecuredSessionContext: def __init__(self, initiator, ephemeral_initiator_node_id): self.initiator = initiator self.ephemeral_initiator_node_id = ephemeral_initiator_node_id self.message_reception_state = None + self.exchanges = {} class SecureSessionContext: @@ -283,6 +313,7 @@ def __init__(self, local_session_id): self.session_idle_interval = None self.session_active_interval = None self.session_active_threshold = None + self.exchanges = {} @property def peer_active(self): @@ -295,6 +326,7 @@ def __init__(self, buffer): self.flags, self.session_id, self.security_flags, self.message_counter = ( struct.unpack_from("> 4) != 0: raise RuntimeError("Incorrect version") - self.secure_session = self.security_flags & 0x3 != 0 or self.session_id != 0 + self.secure_session = not ( + not (self.security_flags & SecurityFlags.GROUP) and self.session_id == 0 + ) if not self.secure_session: self.payload = memoryview(buffer)[offset:] - - context = UnsecuredSessionContext(False, self.source_node_id) - self.unsecured_session_context[self.source_node_id] = context else: self.payload = None - def _parse_protocol_header(self): + self.duplicate = None + + def parse_protocol_header(self): self.exchange_flags, self.protocol_opcode, self.exchange_id = ( struct.unpack_from(" memoryview: reply = bytearray(1280) offset = 0 @@ -375,12 +410,11 @@ def __init__(self): def _increment(self, value): return (value + 1) % 0xFFFFFFFF - def counter_ok(self, message): - """Implements 4.6.7""" + def get_session(self, message): if message.secure_session: if message.security_flags & SecurityFlags.GROUP: if message.source_node_id is None: - return False + return None # TODO: Get MRS for source node id and message type else: session_context = self.secure_session_contexts[message.session_id] @@ -393,6 +427,11 @@ def counter_ok(self, message): ) ) session_context = self.unsecured_session_context[message.source_node_id] + return session_context + + def mark_duplicate(self, message): + """Implements 4.6.7""" + session_context = self.get_session(message) if session_context.message_reception_state is None: session_context.message_reception_state = MessageReceptionState( @@ -400,9 +439,10 @@ def counter_ok(self, message): rollover=False, encrypted=message.secure_session, ) - return True + message.duplicate = False + return - return session_context.message_reception_state.process_counter( + message.duplicate = session_context.message_reception_state.process_counter( message.message_counter ) @@ -443,3 +483,207 @@ def new_context(self): self.secure_session_contexts[session_id] = SecureSessionContext(session_id) return self.secure_session_contexts[session_id] + + def process_exchange(self, message): + session = self.get_session(message) + if session is None: + return None + # Step 1 of 4.12.5.2 + if ( + message.exchange_flags & (ExchangeFlags.R | ExchangeFlags.A) + and not message.security_flags & SecurityFlags.C + and message.security_flags & SecurityFlags.GROUP + ): + # Drop illegal combination of flags. + return None + if message.exchange_id not in session.exchanges: + # Section 4.10.5.2 + initiator = message.exchange_flags & ExchangeFlags.I + if initiator and not message.duplicate: + session.exchanges[message.exchange_id] = Exchange( + not initiator, message.exchange_id, [message.protocol_id] + ) + # Drop because the message isn't from an initiator. + elif message.exchange_flags & ExchangeFlags.R: + # Send a bare acknowledgement back. + raise NotImplementedError("Send a bare acknowledgement back") + return None + else: + # Just drop it. + return None + + exchange = session.exchanges[message.exchange_id] + if exchange.receive(message): + # If we want to drop the message, then return None. + return None + + return exchange + + +class CircuitMatter: + def __init__(self, socketpool, mdns_server, state_filename, record_to=None): + self.socketpool = socketpool + self.mdns_server = mdns_server + self.avahi = None + self.record_to = record_to + if self.record_to: + self.recorded_packets = [] + else: + self.recorded_packets = None + self.manager = SessionManager() + + with open(state_filename, "r") as state_file: + self.nonvolatile = json.load(state_file) + + for key in ["descriminator", "salt", "iteration-count"]: + if key not in self.nonvolatile: + raise RuntimeError(f"Missing key {key} in state file") + + commission = "fabrics" not in self.nonvolatile + + self.packet_buffer = memoryview(bytearray(1280)) + + # Define the UDP IP address and port + UDP_IP = "::" # Listen on all available network interfaces + self.UDP_PORT = 5540 + + # Create the UDP socket + self.socket = self.socketpool.socket( + self.socketpool.AF_INET6, self.socketpool.SOCK_DGRAM + ) + + # Bind the socket to the IP and port + self.socket.bind((UDP_IP, self.UDP_PORT)) + self.socket.setblocking(False) + + print(f"Listening on UDP port {self.UDP_PORT}") + + if commission: + self.start_commissioning() + + def start_commissioning(self): + descriminator = self.nonvolatile["descriminator"] + txt_records = { + "PI": "", + "PH": "33", + "CM": "1", + "D": str(descriminator), + "CRI": "3000", + "CRA": "4000", + "T": "1", + "VP": "65521+32769", + } + self.mdns_server.advertise_service( + "_matterc", + "_udp", + self.UDP_PORT, + txt_records=txt_records, + instance_name="FA93546B21F5FB54", + subtypes=[ + f"_L{descriminator}._sub._matterc._udp", + "_CM._sub._matterc._udp", + ], + ) + + def process_packets(self): + while True: + try: + nbytes, addr = self.socket.recvfrom_into( + self.packet_buffer, len(self.packet_buffer) + ) + except BlockingIOError: + break + if nbytes == 0: + break + if self.recorded_packets is not None: + self.recorded_packets.append( + ( + "receive", + time.monotonic_ns(), + addr, + binascii.b2a_base64( + self.packet_buffer[:nbytes], newline=False + ).decode("utf-8"), + ) + ) + + self.process_packet(addr, self.packet_buffer[:nbytes]) + + def process_packet(self, address, data): + # Print the received data and the address of the sender + # This is section 4.7.2 + message = Message(data) + if message.secure_session: + # Decrypt the payload + pass + message.parse_protocol_header() + self.manager.mark_duplicate(message) + + exchange = self.manager.process_exchange(message) + if exchange is None: + print(f"Dropping message {message.message_counter}") + return + + print(f"Received packet from {address}:") + print(f"{data.hex(' ')}") + print(f"Message counter {message.message_counter}") + protocol_id = message.protocol_id + protocol_opcode = message.protocol_opcode + + if protocol_id == ProtocolId.SECURE_CHANNEL: + if protocol_opcode == SecureProtocolOpcode.MSG_COUNTER_SYNC_REQ: + print("Received Message Counter Synchronization Request") + elif protocol_opcode == SecureProtocolOpcode.MSG_COUNTER_SYNC_RSP: + print("Received Message Counter Synchronization Response") + elif protocol_opcode == SecureProtocolOpcode.PBKDF_PARAM_REQUEST: + print("Received PBKDF Parameter Request") + # This is Section 4.14.1.2 + request = PBKDFParamRequest(message.application_payload[1:-1]) + if request.passcodeId == 0: + pass + # Send back failure + # response = StatusReport() + # response.GeneralCode + print(request) + response = PBKDFParamResponse() + response.initiatorRandom = request.initiatorRandom + + # Generate a random number + response.responderRandom = os.urandom(32) + session_context = self.manager.new_context() + response.responderSessionId = session_context.local_session_id + session_context.peer_session_id = request.initiatorSessionId + if not request.hasPBKDFParameters: + params = Crypto_PBKDFParameterSet() + params.iterations = self.nonvolatile["iteration-count"] + params.salt = binascii.a2b_base64(self.nonvolatile["salt"]) + response.pbkdf_parameters = params + print(response) + + elif protocol_opcode == SecureProtocolOpcode.PBKDF_PARAM_RESPONSE: + print("Received PBKDF Parameter Response") + elif protocol_opcode == SecureProtocolOpcode.PASE_PAKE1: + print("Received PASE PAKE1") + elif protocol_opcode == SecureProtocolOpcode.PASE_PAKE2: + print("Received PASE PAKE2") + elif protocol_opcode == SecureProtocolOpcode.PASE_PAKE3: + print("Received PASE PAKE3") + elif protocol_opcode == SecureProtocolOpcode.CASE_SIGMA1: + print("Received CASE Sigma1") + elif protocol_opcode == SecureProtocolOpcode.CASE_SIGMA2: + print("Received CASE Sigma2") + elif protocol_opcode == SecureProtocolOpcode.CASE_SIGMA3: + print("Received CASE Sigma3") + elif protocol_opcode == SecureProtocolOpcode.CASE_SIGMA2_RESUME: + print("Received CASE Sigma2 Resume") + elif protocol_opcode == SecureProtocolOpcode.STATUS_REPORT: + print("Received Status Report") + elif protocol_opcode == SecureProtocolOpcode.ICD_CHECK_IN: + print("Received ICD Check-in") + + def __del__(self): + if self.avahi: + self.avahi.kill() + if self.recorded_packets and self.record_to: + with open(self.record_to, "w") as record_file: + json.dump(self.recorded_packets, record_file) diff --git a/circuitmatter/__main__.py b/circuitmatter/__main__.py index 52626e8..4ee6491 100644 --- a/circuitmatter/__main__.py +++ b/circuitmatter/__main__.py @@ -1,127 +1,120 @@ """Pure Python implementation of the Matter IOT protocol.""" -import os +import binascii +import json +import socket +import subprocess import circuitmatter as cm -# descriminator = 3840 -# avahi = subprocess.Popen(["avahi-publish-service", "-v", f"--subtype=_L{descriminator}._sub._matterc._udp", "--subtype=_CM._sub._matterc._udp", "FA93546B21F5FB54", "_matterc._udp", "5540", "PI=", "PH=33", "CM=1", f"D={descriminator}", "CRI=3000", "CRA=4000", "T=1", "VP=65521+32769"]) - -# # Define the UDP IP address and port -# UDP_IP = "::" # Listen on all available network interfaces -# UDP_PORT = 5540 - -# # Create the UDP socket -# sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) - -# # Bind the socket to the IP and port -# sock.bind((UDP_IP, UDP_PORT)) - -# print(f"Listening on UDP port {UDP_PORT}") - -unsecured_session_context = {} -secure_session_contexts = ["reserved"] - -# while True: -# # Receive data from the socket (1280 is the minimum ipv6 MTU and the max UDP matter packet size.) -# data, addr = sock.recvfrom(1280) -data = b"\x04\x00\x00\x00\x0b\x06\xb7\t)\xad\x07\xd9\xae\xa1\xee\xa0\x05 j\x15\x00\x00\x150\x01 \x97\x064#\x1c\xd1E7H\x0b|\xc2G\xa7\xc38\xe9\xce3\x11\xb2@M\x86\xd7\xb5{)\xaa`\xddb%\x02\xc2\x86$\x03\x00(\x045\x05%\x01\xf4\x01%\x02,\x01%\x03\xa0\x0f$\x04\x11$\x05\x0b&\x06\x00\x00\x03\x01$\x07\x01\x18\x18" -addr = None - - -# pathlib.Path("data.bin").write_bytes(data) - -bookmarks = [] - - -def add_bookmark(start, length, name, color=0x0000FF): - bookmarks.append( - { - "color": 0x4F000000 | color, - "comment": "\n", - "id": len(bookmarks), - "locked": True, - "name": name, - "region": {"address": start, "size": length}, - } +class ReplaySocket: + def __init__(self, replay_data): + self.replay_data = replay_data + + def bind(self, address): + print("bind to", address) + + def setblocking(self, value): + print("setblocking", value) + + def recvfrom_into(self, buffer, nbytes=None): + if nbytes is None: + nbytes = len(buffer) + direction = "send" + while direction == "send": + direction, _, address, data_b64 = self.replay_data.pop(0) + decoded = binascii.a2b_base64(data_b64) + if len(decoded) > nbytes: + raise RuntimeError("Next replay packet is larger than buffer to read into") + buffer[: len(decoded)] = decoded + return len(decoded), address + + +class ReplaySocketPool: + AF_INET6 = 0 + SOCK_DGRAM = 1 + + def __init__(self, replay_file): + with open(replay_file, "r") as f: + self.replay_data = json.load(f) + self._socket_created = False + + def socket(self, *args, **kwargs): + if self._socket_created: + raise RuntimeError("Only one socket can be created") + self._socket_created = True + return ReplaySocket(self.replay_data) + + +class DummyMDNS: + def advertise_service( + self, + service_type, + protocol, + port, + txt_records=[], + subtypes=[], + instance_name="", + ): + print(f"Advertise service {service_type} {protocol} {port} {txt_records}") + + +class MDNSServer(DummyMDNS): + def __init__(self): + self.active_services = {} + + def advertise_service( + self, + service_type, + protocol, + port, + txt_records={}, + subtypes=[], + instance_name="", + ): + subtypes = [f"--subtype={subtype}" for subtype in subtypes] + txt_records = [f"{key}={value}" for key, value in txt_records.items()] + if service_type in self.active_services: + self.active_services[service_type].kill() + del self.active_services[service_type] + self.active_services[service_type] = subprocess.Popen( + [ + "avahi-publish-service", + *subtypes, + instance_name, + f"{service_type}.{protocol}", + str(port), + *txt_records, + ] + ) + + def __del__(self): + for active_service in self.active_services.values(): + active_service.kill() + + +def run(replay_file=None): + if replay_file: + socketpool = ReplaySocketPool(replay_file) + mdns_server = DummyMDNS() + record_file = None + else: + socketpool = socket + mdns_server = MDNSServer() + record_file = "test_data/recorded_packets.json" + matter = cm.CircuitMatter( + socketpool, mdns_server, "test_data/device_state.json", record_file ) - # Write every time in case we crash - # pathlib.Path("parsed.hexbm").write_text(json.dumps({"bookmarks": bookmarks})) - - -def run(): - manager = cm.SessionManager() - # Print the received data and the address of the sender - # This is section 4.7.2 - print(f"Received packet from {addr}: {data}") - message = cm.Message(data) - if message.secure_session: - # Decrypt the payload - pass - if not manager.counter_ok(message): - print("Dropping message due to counter error") - return - # if not manager.rmp_ok(message): - # print("Dropping message due to RMP") - # continue - - protocol_id = message.protocol_id - protocol_opcode = message.protocol_opcode - - if protocol_id == cm.ProtocolId.SECURE_CHANNEL: - if protocol_opcode == cm.SecureProtocolOpcode.MSG_COUNTER_SYNC_REQ: - print("Received Message Counter Synchronization Request") - elif protocol_opcode == cm.SecureProtocolOpcode.MSG_COUNTER_SYNC_RSP: - print("Received Message Counter Synchronization Response") - elif protocol_opcode == cm.SecureProtocolOpcode.PBKDF_PARAM_REQUEST: - print("Received PBKDF Parameter Request") - # This is Section 4.14.1.2 - request = cm.PBKDFParamRequest(message.payload) - if request.passcodeID == 0: - pass - # Send back failure - # response = StatusReport() - # response.GeneralCode - print(request) - response = cm.PBKDFParamResponse() - response.initiatorRandom = request.initiatorRandom - - # Generate a random number - response.responderRandom = os.urandom(32) - session_context = manager.new_context(response.responderSessionId) - - session_context.peer_session_id = request.initiatorSessionId - if not request.hasPBKDFParameters: - params = cm.Crypto_PBKDFParameterSet() - params.iterations = 1000 - params.salt = b"\x00" * 32 - response.pbkdf_parameters = params - print(response) - - elif protocol_opcode == cm.SecureProtocolOpcode.PBKDF_PARAM_RESPONSE: - print("Received PBKDF Parameter Response") - elif protocol_opcode == cm.SecureProtocolOpcode.PASE_PAKE1: - print("Received PASE PAKE1") - elif protocol_opcode == cm.SecureProtocolOpcode.PASE_PAKE2: - print("Received PASE PAKE2") - elif protocol_opcode == cm.SecureProtocolOpcode.PASE_PAKE3: - print("Received PASE PAKE3") - elif protocol_opcode == cm.SecureProtocolOpcode.CASE_SIGMA1: - print("Received CASE Sigma1") - elif protocol_opcode == cm.SecureProtocolOpcode.CASE_SIGMA2: - print("Received CASE Sigma2") - elif protocol_opcode == cm.SecureProtocolOpcode.CASE_SIGMA3: - print("Received CASE Sigma3") - elif protocol_opcode == cm.SecureProtocolOpcode.CASE_SIGMA2_RESUME: - print("Received CASE Sigma2 Resume") - elif protocol_opcode == cm.SecureProtocolOpcode.STATUS_REPORT: - print("Received Status Report") - elif protocol_opcode == cm.SecureProtocolOpcode.ICD_CHECK_IN: - print("Received ICD Check-in") - - # avahi.kill() + while True: + matter.process_packets() if __name__ == "__main__": - run() + import sys + + print(sys.argv) + replay_file = None + if len(sys.argv) > 1: + replay_file = sys.argv[1] + run(replay_file=replay_file) diff --git a/circuitmatter/tlv.py b/circuitmatter/tlv.py index c0964ce..387c298 100644 --- a/circuitmatter/tlv.py +++ b/circuitmatter/tlv.py @@ -74,21 +74,15 @@ def encode_into(self, buffer, offset=0): def scan_until(self, tag): if self.buffer is None: return - print(bytes(self.buffer[self._offset :])) - print(f"Looking for {tag}") while self._offset < len(self.buffer): control_octet = self.buffer[self._offset] tag_control = control_octet >> 5 element_type = control_octet & 0x1F - print( - f"Control 0x{control_octet:x} tag_control {tag_control} element_type {element_type:x}" - ) this_tag = None if tag_control == 0: # Anonymous this_tag = None elif tag_control == 1: # Context specific - print("context specific tag") this_tag = self.buffer[self._offset + 1] else: vendor_id = None @@ -113,11 +107,9 @@ def scan_until(self, tag): this_tag = (vendor_id, profile_number, tag_number) else: this_tag = tag_number - print(f"found tag {this_tag}") length_offset = self._offset + 1 + TAG_LENGTH[tag_control] element_category = element_type >> 2 - print(f"element_category {element_category}") if element_category == 0 or element_category == 1: # ints value_offset = length_offset value_length = 1 << (element_type & 0x3) @@ -133,11 +125,8 @@ def scan_until(self, tag): elif ( element_category == 3 or element_category == 4 ): # UTF-8 String or Octet String - print(f"element_type {element_type:x}", bin(element_type)) power_of_two = element_type & 0x3 - print(f"power_of_two {power_of_two}") length_length = 1 << power_of_two - print(f"length_length {length_length}") value_offset = length_offset + length_length value_length = struct.unpack_from( INT_SIZE[power_of_two], self.buffer, length_offset @@ -238,9 +227,7 @@ def encode_into(self, obj: TLVStructure, buffer: bytearray, offset: int) -> int: buffer[offset] = self.tag offset += 1 if value is not None: - print("enconding value into", offset) new_offset = self.encode_value_into(value, buffer, offset) - print("new offset", new_offset) return new_offset return offset @@ -262,13 +249,10 @@ def __init__(self, tag, _format, optional=False): ElementType.SIGNED_INT if self.signed else ElementType.UNSIGNED_INT ) self._element_type |= int(math.log(self.max_value_length, 2)) - print(f"{self._element_type:x}") else: - print("float") self._element_type = ElementType.FLOAT if self.max_value_length == 8: self._element_type |= 1 - print(f"{self._element_type:x}") super().__init__(tag, optional) def __set__(self, obj, value): @@ -303,7 +287,6 @@ def _print(self, value): def encode_element_type(self, value): # We don't adjust our encoding based on value size. We always use the bytes needed for the # format. - print("encode", self._element_type) return self._element_type def encode_value_into(self, value, buffer, offset) -> int: diff --git a/test_data/device_state.json b/test_data/device_state.json new file mode 100644 index 0000000..bb9ac67 --- /dev/null +++ b/test_data/device_state.json @@ -0,0 +1,5 @@ +{ + "descriminator": 2207, + "iteration-count": 10000, + "salt": "5uCP0ITHYzI9qBEe6hfU4HfY3y7VopSk0qNvhvznhiQ=" +} diff --git a/test_data/recorded_packets.json b/test_data/recorded_packets.json new file mode 100644 index 0000000..090a5c7 --- /dev/null +++ b/test_data/recorded_packets.json @@ -0,0 +1 @@ +[["receive", 187233115992533, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAGseHgbBzXVNGzbjyQUgEGgAABUwASAJdoCKgQAHZ79xJe7S19yKoOuRS6KxZfHx5ENGbqNdZyUCHh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187233489386972, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAGseHgbBzXVNGzbjyQUgEGgAABUwASAJdoCKgQAHZ79xJe7S19yKoOuRS6KxZfHx5ENGbqNdZyUCHh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187233977348856, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAGseHgbBzXVNGzbjyQUgEGgAABUwASAJdoCKgQAHZ79xJe7S19yKoOuRS6KxZfHx5ENGbqNdZyUCHh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187234418371874, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAGseHgbBzXVNGzbjyQUgEGgAABUwASAJdoCKgQAHZ79xJe7S19yKoOuRS6KxZfHx5ENGbqNdZyUCHh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187235348087967, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAGseHgbBzXVNGzbjyQUgEGgAABUwASAJdoCKgQAHZ79xJe7S19yKoOuRS6KxZfHx5ENGbqNdZyUCHh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187243571244985, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAGweHgbb5WaAXI2fcAUgEWgAABUwASDVKzSj/fL0caSIfcD4UyOqVX97C/jpz2SV2+hyrJWQIyUCHx4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187243921960752, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAGweHgbb5WaAXI2fcAUgEWgAABUwASDVKzSj/fL0caSIfcD4UyOqVX97C/jpz2SV2+hyrJWQIyUCHx4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187244315499581, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAGweHgbb5WaAXI2fcAUgEWgAABUwASDVKzSj/fL0caSIfcD4UyOqVX97C/jpz2SV2+hyrJWQIyUCHx4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187244883197242, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAGweHgbb5WaAXI2fcAUgEWgAABUwASDVKzSj/fL0caSIfcD4UyOqVX97C/jpz2SV2+hyrJWQIyUCHx4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187245846277711, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAGweHgbb5WaAXI2fcAUgEWgAABUwASDVKzSj/fL0caSIfcD4UyOqVX97C/jpz2SV2+hyrJWQIyUCHx4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187263138437928, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAG0eHgajX68X+QJCiwUgEmgAABUwASB4ptkLUberS0X8bAUY59xRU86mWq4whxIugVko1P6ZrCUCIB4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187263491537451, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAG0eHgajX68X+QJCiwUgEmgAABUwASB4ptkLUberS0X8bAUY59xRU86mWq4whxIugVko1P6ZrCUCIB4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187263835625962, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAG0eHgajX68X+QJCiwUgEmgAABUwASB4ptkLUberS0X8bAUY59xRU86mWq4whxIugVko1P6ZrCUCIB4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187264476478812, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAG0eHgajX68X+QJCiwUgEmgAABUwASB4ptkLUberS0X8bAUY59xRU86mWq4whxIugVko1P6ZrCUCIB4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187265439975626, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAG0eHgajX68X+QJCiwUgEmgAABUwASB4ptkLUberS0X8bAUY59xRU86mWq4whxIugVko1P6ZrCUCIB4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187273604022843, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAG4eHgbfnRsl6Zf6zAUgE2gAABUwASBFpAjTtgirRicfNPQCzG8LVmyz0eBZumdjg7N1rRCY+yUCIR4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187273990040253, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAG4eHgbfnRsl6Zf6zAUgE2gAABUwASBFpAjTtgirRicfNPQCzG8LVmyz0eBZumdjg7N1rRCY+yUCIR4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187274341720181, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAG4eHgbfnRsl6Zf6zAUgE2gAABUwASBFpAjTtgirRicfNPQCzG8LVmyz0eBZumdjg7N1rRCY+yUCIR4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187274967509748, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAG4eHgbfnRsl6Zf6zAUgE2gAABUwASBFpAjTtgirRicfNPQCzG8LVmyz0eBZumdjg7N1rRCY+yUCIR4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187275818446360, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAG4eHgbfnRsl6Zf6zAUgE2gAABUwASBFpAjTtgirRicfNPQCzG8LVmyz0eBZumdjg7N1rRCY+yUCIR4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187293145236067, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAG8eHgYslRF/GjOAtQUgFGgAABUwASACaHIy6kcrGizLne86xgIlyX7nzu8omlOFnlLLLUtsryUCIh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187293543855120, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAG8eHgYslRF/GjOAtQUgFGgAABUwASACaHIy6kcrGizLne86xgIlyX7nzu8omlOFnlLLLUtsryUCIh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187293944763592, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAG8eHgYslRF/GjOAtQUgFGgAABUwASACaHIy6kcrGizLne86xgIlyX7nzu8omlOFnlLLLUtsryUCIh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187294558009767, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAG8eHgYslRF/GjOAtQUgFGgAABUwASACaHIy6kcrGizLne86xgIlyX7nzu8omlOFnlLLLUtsryUCIh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187295601697894, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAG8eHgYslRF/GjOAtQUgFGgAABUwASACaHIy6kcrGizLne86xgIlyX7nzu8omlOFnlLLLUtsryUCIh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187323159623203, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAHAeHgaxiiI5/PrLHwUgFWgAABUwASCplZXkPbPkg7xOTDqDeEeFdU4boaoFeQDD5izCkDXzxSUCIx4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187323544270296, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAHAeHgaxiiI5/PrLHwUgFWgAABUwASCplZXkPbPkg7xOTDqDeEeFdU4boaoFeQDD5izCkDXzxSUCIx4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187323916393380, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAHAeHgaxiiI5/PrLHwUgFWgAABUwASCplZXkPbPkg7xOTDqDeEeFdU4boaoFeQDD5izCkDXzxSUCIx4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187324494830447, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAHAeHgaxiiI5/PrLHwUgFWgAABUwASCplZXkPbPkg7xOTDqDeEeFdU4boaoFeQDD5izCkDXzxSUCIx4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187325452128813, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAHAeHgaxiiI5/PrLHwUgFWgAABUwASCplZXkPbPkg7xOTDqDeEeFdU4boaoFeQDD5izCkDXzxSUCIx4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187333597945812, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAHEeHgYpUsoTNc+IPwUgFmgAABUwASAIlwcrQLeKRcpNGWFmMY+6MCixVcHaEQ7sOlwXFcJgSSUCJB4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187334015063020, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAHEeHgYpUsoTNc+IPwUgFmgAABUwASAIlwcrQLeKRcpNGWFmMY+6MCixVcHaEQ7sOlwXFcJgSSUCJB4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187334421138957, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAHEeHgYpUsoTNc+IPwUgFmgAABUwASAIlwcrQLeKRcpNGWFmMY+6MCixVcHaEQ7sOlwXFcJgSSUCJB4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187334994459386, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAHEeHgYpUsoTNc+IPwUgFmgAABUwASAIlwcrQLeKRcpNGWFmMY+6MCixVcHaEQ7sOlwXFcJgSSUCJB4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187335928292007, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAHEeHgYpUsoTNc+IPwUgFmgAABUwASAIlwcrQLeKRcpNGWFmMY+6MCixVcHaEQ7sOlwXFcJgSSUCJB4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187353158825640, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAHIeHgbia/xA80W1wQUgF2gAABUwASCqj40OAAqOQLJH2sKlLFLEys8AtJA5/5VlWWeKkGcFmSUCJR4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187353549094661, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAHIeHgbia/xA80W1wQUgF2gAABUwASCqj40OAAqOQLJH2sKlLFLEys8AtJA5/5VlWWeKkGcFmSUCJR4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187353954130472, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAHIeHgbia/xA80W1wQUgF2gAABUwASCqj40OAAqOQLJH2sKlLFLEys8AtJA5/5VlWWeKkGcFmSUCJR4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187354743258981, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAHIeHgbia/xA80W1wQUgF2gAABUwASCqj40OAAqOQLJH2sKlLFLEys8AtJA5/5VlWWeKkGcFmSUCJR4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187355487537401, ["fd98:bbab:bd61:8040:14ae:fe13:c814:3bc2", 58700, 0, 0], "BAAAAHIeHgbia/xA80W1wQUgF2gAABUwASCqj40OAAqOQLJH2sKlLFLEys8AtJA5/5VlWWeKkGcFmSUCJR4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187363612372888, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAHMeHgbilpflS1S8gAUgGGgAABUwASBH7Y2amv7rpqkUrCRC0FzvRYjlRa5HNldkAQVVfSpbjSUCJh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187364007417532, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAHMeHgbilpflS1S8gAUgGGgAABUwASBH7Y2amv7rpqkUrCRC0FzvRYjlRa5HNldkAQVVfSpbjSUCJh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187364381658926, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAHMeHgbilpflS1S8gAUgGGgAABUwASBH7Y2amv7rpqkUrCRC0FzvRYjlRa5HNldkAQVVfSpbjSUCJh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187364946983063, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAHMeHgbilpflS1S8gAUgGGgAABUwASBH7Y2amv7rpqkUrCRC0FzvRYjlRa5HNldkAQVVfSpbjSUCJh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="], ["receive", 187365790064355, ["::ffff:192.168.0.37", 53917, 0, 0], "BAAAAHMeHgbilpflS1S8gAUgGGgAABUwASBH7Y2amv7rpqkUrCRC0FzvRYjlRa5HNldkAQVVfSpbjSUCJh4kAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="]]