def handle_natneg_preinit_ack(nn, recv_data, addr, socket): """Command: 0x10 - NN_PREINIT_ACK. Reply by the server for record NN_PREINIT (0x0F). Example: fd fc 1e 66 6a b2 04 10 b5 e0 95 2a 00 00 00 00 00 00 After receiving other client's PREINIT: fd fc 1e 66 6a b2 04 10 b5 e0 95 2a 01 02 00 00 00 00 Description: fd fc 1e 66 6a b2 - NATNEG magic 04 - NATNEG version 10 - NATNEG record type b5 e0 95 2a - Session id 00 - Client index (0x00 - Client, 0x01 - Host) 00 - State (0x00 - Waiting for client, 0x01 - Waiting for matchup, 0x02 - Ready) 00 00 00 00 - Other client's session id (or empty) """ logger.log( logging.WARNING, "Received server record type command NN_PREINIT_ACK (0x10)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
def handle_natneg_initack(nn, recv_data, addr, socket): """Command: 0x01 - NN_INITACK. Reply by the server for record NN_INIT (0x00). Example: fd fc 1e 66 6a b2 03 01 3d f1 00 71 00 00 ff ff 6d 16 b5 7d ea Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 01 - NATNEG record type 3d f1 00 71 - Session id 00 - Port type (between 0x00 and 0x03) 00 - Client index (0x00 - Client, 0x01 - Host) ff - Use game port (-1)? Dummy value? ff 6d 16 b5 - Local IP? Dummy value? 7d ea - Local port? Hex speak of "Idea"? Dummy value? """ logger.log( logging.WARNING, "Received server record type command NN_INITACK (0x01)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
def handle_natneg_preinit_ack(nn, recv_data, addr, socket): """Command: 0x10 - NN_PREINIT_ACK. Reply by the server for record NN_PREINIT (0x0F). Example: fd fc 1e 66 6a b2 04 10 b5 e0 95 2a 00 00 00 00 00 00 After receiving other client's PREINIT: fd fc 1e 66 6a b2 04 10 b5 e0 95 2a 01 02 00 00 00 00 Description: fd fc 1e 66 6a b2 - NATNEG magic 04 - NATNEG version 10 - NATNEG record type b5 e0 95 2a - Session id 00 - Client index (0x00 - Client, 0x01 - Host) 00 - State (0x00 - Waiting for client, 0x01 - Waiting for matchup, 0x02 - Ready) 00 00 00 00 - Other client's session id (or empty) """ logger.log(logging.WARNING, "Received server record type command NN_PREINIT_ACK (0x10)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
def handle_natneg_address_reply(nn, recv_data, addr, socket): """Command: 0x0B - NN_ADDRESS_REPLY. Reply by the server for record NN_ADDRESS_CHECK (0x0A). Example: fd fc 1e 66 6a b2 03 0b 00 00 00 03 01 00 00 25 c9 e2 8a 91 e4 Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 0b - NATNEG record type 00 00 00 03 - Session id 01 - Port type (between 0x00 and 0x03) 00 - Client index (0x00 - Client, 0x01 - Host) 00 - Use game port 25 c9 e2 8a - Public IP 91 e4 - Public port """ logger.log( logging.WARNING, "Received server record type command NN_ADDRESS_REPLY (0x0B)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))
def handle_natneg_natify_request(nn, recv_data, addr, socket): """Command: 0x0C - NN_NATIFY_REQUEST. Send by the client during connection test. Example: fd fc 1e 66 6a b2 03 0c 00 00 03 09 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 0c - NATNEG record type 00 00 03 09 - Session id 01 - Port type (between 0x00 and 0x03) - 60 bytes padding? 00 - Client index (0x00 - Client, 0x01 - Host) 00 - NATNEG result? 00 00 00 00 - NAT type? 00 00 00 00 - NAT mapping scheme? 00 (x50) - Game name? """ port_type = "%02x" % ord(recv_data[12]) logger.log(logging.DEBUG, "Received natify command from %s:%d...", *addr) output = bytearray(recv_data) output[7] = 0x02 # ERT Test nn.write_queue.put((output, addr, socket)) logger.log(logging.DEBUG, "Sent natify response to %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))
def find_server(self, query_game, filter, fields, max_servers, game_name, challenge): # Get dictionary from master server list server. self.log(logging.DEBUG, "Searching for server matching '%s' with the fields '%s'" % (filter, fields)) self.server_list = self.server_manager.find_servers(query_game, filter, fields, max_servers)._getvalue() self.log(logging.DEBUG, "Found server(s):") self.log(logging.DEBUG, self.server_list) if self.server_list == []: self.server_list.append({}) for _server in self.server_list: server = _server if len(server) > 0 and len(fields) > 0 and server['requested'] == {}: # If the requested fields weren't found then don't return a server. # This fixes a bug with Mario Kart DS. #print "Requested was empty" server = {} if "__console__" in server: self.console = int(server['__console__']) # Generate binary server list data data = self.generate_server_list_data(self.address, fields, server) self.log(logging.DEBUG, utils.pretty_print_hex(data)) # Encrypt data enc = gs_utils.EncTypeX() data = enc.encrypt(self.secret_key_list[game_name], challenge, data) # Send to client self.transport.write(bytes(data)) self.log(logging.DEBUG, "Sent server list message to %s:%s..." % (self.address.host, self.address.port))
def handle_natneg_erttest(nn, recv_data, addr, socket): """Command: 0x02 - NN_ERTTEST. Reply by the server for record NN_NATIFY_REQUEST (0x0C). Example: fd fc 1e 66 6a b2 03 02 00 00 03 09 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 02 - NATNEG record type 00 00 03 09 - Session id 02 - Port type (between 0x00 and 0x03) - 60 bytes padding? 00 - Client index (0x00 - Client, 0x01 - Host) 00 - NATNEG result? 00 00 00 00 - NAT type? 00 00 00 00 - NAT mapping scheme? 00 (x50) - Game name? """ logger.log(logging.WARNING, "Received server record type command NN_ERTTEST (0x02)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
def handle_natneg_report_ack(nn, recv_data, addr, socket): """Command: 0x0E - NN_REPORT_ACK. Reply by the server for record NN_REPORT (0x0D). Example: fd fc 1e 66 6a b2 03 0e 3d f1 00 71 00 00 00 00 00 00 06 00 00 Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 0e - NATNEG record type 3d f1 00 71 - Session id 00 - Port type (0x00, 0x80 or 0x90) 00 - Client index (0x00 - Client, 0x01 - Host) 00 - NATNEG result (0x00 - Error, 0x01 - Success) 00 00 00 06 - NAT type (0x00 - No NAT, 0x01 - Firewall only, 0x02 - Full cone, 0x03 - Restricted cone, 0x04 - Port restricted cone, 0x05 - Symmetric, 0x06 - Unknown) """ logger.log(logging.WARNING, "Received server record type command NN_REPORT_ACK (0x0E)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
def handle_natneg_initack(nn, recv_data, addr, socket): """Command: 0x01 - NN_INITACK. Reply by the server for record NN_INIT (0x00). Example: fd fc 1e 66 6a b2 03 01 3d f1 00 71 00 00 ff ff 6d 16 b5 7d ea Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 01 - NATNEG record type 3d f1 00 71 - Session id 00 - Port type (between 0x00 and 0x03) 00 - Client index (0x00 - Client, 0x01 - Host) ff - Use game port (-1)? Dummy value? ff 6d 16 b5 - Local IP? Dummy value? 7d ea - Local port? Hex speak of "Idea"? Dummy value? """ logger.log(logging.WARNING, "Received server record type command NN_INITACK (0x01)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
def handle_natneg_connect_ping(nn, recv_data, addr, socket): """Command: 0x07 - NN_CONNECT_PING. Looks like NN_CONNECT but between clients. The server shouldn't be involved. Example: fd fc 1e 66 6a b2 03 07 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 07 - NATNEG record type ?? ?? ?? ?? - Session id ?? ?? ?? ?? - Remote IP ?? ?? - Remote port ?? - Sequence counter (0 or 1) ?? - Error """ logger.log( logging.WARNING, "Received unimplemented command NN_CONNECT_PING (0x07)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
def handle_natneg_report_ack(nn, recv_data, addr, socket): """Command: 0x0E - NN_REPORT_ACK. Reply by the server for record NN_REPORT (0x0D). Example: fd fc 1e 66 6a b2 03 0e 3d f1 00 71 00 00 00 00 00 00 06 00 00 Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 0e - NATNEG record type 3d f1 00 71 - Session id 00 - Port type (0x00, 0x80 or 0x90) 00 - Client index (0x00 - Client, 0x01 - Host) 00 - NATNEG result (0x00 - Error, 0x01 - Success) 00 00 00 06 - NAT type (0x00 - No NAT, 0x01 - Firewall only, 0x02 - Full cone, 0x03 - Restricted cone, 0x04 - Port restricted cone, 0x05 - Symmetric, 0x06 - Unknown) """ logger.log( logging.WARNING, "Received server record type command NN_REPORT_ACK (0x0E)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
def handle_natneg_erttest(nn, recv_data, addr, socket): """Command: 0x02 - NN_ERTTEST. Reply by the server for record NN_NATIFY_REQUEST (0x0C). Example: fd fc 1e 66 6a b2 03 02 00 00 03 09 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 02 - NATNEG record type 00 00 03 09 - Session id 02 - Port type (between 0x00 and 0x03) - 60 bytes padding? 00 - Client index (0x00 - Client, 0x01 - Host) 00 - NATNEG result? 00 00 00 00 - NAT type? 00 00 00 00 - NAT mapping scheme? 00 (x50) - Game name? """ logger.log( logging.WARNING, "Received server record type command NN_ERTTEST (0x02)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
def handle_natneg_address_reply(nn, recv_data, addr, socket): """Command: 0x0B - NN_ADDRESS_REPLY. Reply by the server for record NN_ADDRESS_CHECK (0x0A). Example: fd fc 1e 66 6a b2 03 0b 00 00 00 03 01 00 00 25 c9 e2 8a 91 e4 Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 0b - NATNEG record type 00 00 00 03 - Session id 01 - Port type (between 0x00 and 0x03) 00 - Client index (0x00 - Client, 0x01 - Host) 00 - Use game port 25 c9 e2 8a - Public IP 91 e4 - Public port """ logger.log(logging.WARNING, "Received server record type command NN_ADDRESS_REPLY (0x0B)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))
def forward_data_to_client(self, data, forward_client): # Find session id of server # Iterate through the list of servers sent to the client and match by IP and port. # Is there a better way to determine this information? if self.forward_client == None or len(self.forward_client) != 2: return server, ip = self.find_server_in_cache(self.forward_client[0], self.forward_client[1], self.console) if server == None: if self.console == 0: server, ip = self.find_server_in_cache(self.forward_client[0], self.forward_client[1], 1) # Try Wii elif self.console == 1: server, ip = self.find_server_in_cache(self.forward_client[0], self.forward_client[1], 0) # Try DS self.log(logging.DEBUG, "find_server_in_cache returned: %s" % server) self.log( logging.DEBUG, "Trying to send message to %s:%d..." % (self.forward_client[0], self.forward_client[1])) self.log(logging.DEBUG, utils.pretty_print_hex(bytearray(data))) if server == None: return self.log(logging.DEBUG, "%s %s" % (ip, server['publicip'])) if server['publicip'] == ip and server['publicport'] == str( self.forward_client[1]): # Send command to server to get it to connect to natneg natneg_session = int( utils.generate_random_hex_str(8), 16 ) # Quick and lazy way to get a random 32bit integer. Replace with something else later output = bytearray([0xfe, 0xfd, 0x06]) output += utils.get_bytes_from_int(server['__session__']) output += bytearray(utils.get_bytes_from_int(natneg_session)) output += bytearray(data) if self.qr != None: self.qr.socket.sendto(output, forward_client) self.log( logging.DEBUG, "Forwarded data to %s:%s through QR server..." % (forward_client[0], forward_client[1])) else: # In case we can't contact the QR server, just try sending the packet directly. # This isn't standard behavior but it can work in some instances. client_s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) client_s.sendto(output, forward_client) self.log( logging.DEBUG, "Forwarded data to %s:%s directly (potential error occurred)..." % (forward_client[0], forward_client[1]))
def forward_data_to_client(self, data, forward_client): # Find session id of server # Iterate through the list of servers sent to the client and match by IP and port. # Is there a better way to determine this information? if self.forward_client == None or len(self.forward_client) != 2: return server, ip = self.find_server_in_cache(self.forward_client[0], self.forward_client[1], self.console) if server == None: if self.console == 0: server, ip = self.find_server_in_cache(self.forward_client[0], self.forward_client[1], 1) # Try Wii elif self.console == 1: server, ip = self.find_server_in_cache(self.forward_client[0], self.forward_client[1], 0) # Try DS self.log(logging.DEBUG, "find_server_in_cache returned: %s" % server) self.log(logging.DEBUG, "Trying to send message to %s:%d..." % (self.forward_client[0], self.forward_client[1])) self.log(logging.DEBUG, utils.pretty_print_hex(bytearray(data))) if server == None: return self.log(logging.DEBUG, "%s %s" % (ip, server['publicip'])) if server['publicip'] == ip and server['publicport'] == str(self.forward_client[1]): if self.forward_client[1] == 0 and 'localport' in server: # No public port returned from client, try contacting on the local port. self.forward_client = (self.forward_client[0], int(server['localport'])) # Send command to server to get it to connect to natneg cookie = int(utils.generate_random_hex_str(8), 16) # Quick and lazy way to get a random 32bit integer. Replace with something else later if len(data) == 10 and bytearray(data)[0:6] == bytearray([0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2]): natneg_session = utils.get_int(data,6) self.log(logging.DEBUG, "Adding %d to natneg server list: %s" % (natneg_session, server)) self.server_manager.add_natneg_server(natneg_session, server) # Store info in backend so we can get it later in natneg # if self.qr != None: # own_server = self.qr.get_own_server() # # self.log(logging.DEBUG, "Adding %d to natneg server list: %s" % (natneg_session, own_server)) # self.server_manager.add_natneg_server(natneg_session, own_server) # Store info in backend so we can get it later in natneg output = bytearray([0xfe, 0xfd, 0x06]) output += utils.get_bytes_from_int(server['__session__']) output += bytearray(utils.get_bytes_from_int(cookie)) output += bytearray(data) if self.qr != None: self.log(logging.DEBUG, "Forwarded data to %s:%s through QR server..." % (forward_client[0], forward_client[1])) self.qr.socket.sendto(output, forward_client) else: # In case we can't contact the QR server, just try sending the packet directly. # This isn't standard behavior but it can work in some instances. self.log(logging.DEBUG, "Forwarded data to %s:%s directly (potential error occurred)..." % (forward_client[0], forward_client[1])) client_s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) client_s.sendto(output, forward_client)
def send_encrypted_data(self, challenge, data): self.log(logging.DEBUG, "Sent server list message to %s:%s..." % (self.address.host, self.address.port)) self.log(logging.DEBUG, utils.pretty_print_hex(data)) # Encrypt data enc = gs_utils.EncTypeX() data = enc.encrypt(self.secret_key_list[game_name], challenge, data) # Send to client self.transport.write(bytes(data))
def handle_natneg_address_check(nn, recv_data, addr, socket): """Command: 0x0A - NN_ADDRESS_CHECK. Send by the client during connection test. Example: fd fc 1e 66 6a b2 03 0a 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 0a - NATNEG record type 00 00 00 00 - Session id 01 - Port type (between 0x00 and 0x03) - 60 bytes padding? 00 - Client index (0x00 - Client, 0x01 - Host) 00 - NATNEG result? 00 00 00 00 - NAT type? 00 00 00 00 - NAT mapping scheme? 00 (x50) - Game name? """ client_id = "%02x" % ord(recv_data[13]) logger.log(logging.DEBUG, "Received address check command from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data)) output = bytearray(recv_data[0:15]) output += utils.get_bytes_from_ip_str(addr[0]) output += utils.get_bytes_from_short(addr[1], True) output += bytearray(recv_data[len(output):]) output[7] = 0x0b # NN_ADDRESS_REPLY nn.write_queue.put((output, addr, socket)) logger.log(logging.DEBUG, "Sent address check response to %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))
def handle_natneg_preinit(nn, recv_data, addr, socket): """Command: 0x0F - NN_PREINIT. Natneg v4 command thanks to Pipian. Only seems to be used in very few DS games, namely, Pokemon Black/White/Black 2/White 2. Example: fd fc 1e 66 6a b2 04 0f b5 e0 95 2a 00 24 38 b2 b3 5e Description: fd fc 1e 66 6a b2 - NATNEG magic 04 - NATNEG version 0f - NATNEG record type b5 e0 95 2a - Session id 00 - Client index (0x00 - Client, 0x01 - Host) 24 - State (0x00 - Waiting for client, 0x01 - Waiting for matchup, 0x02 - Ready) 38 b2 b3 5e - Other client's session id """ logger.log(logging.DEBUG, "Received pre-init command from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data)) session = utils.get_int(recv_data[-4:], 0) # Report response output = bytearray(recv_data[:-4]) + bytearray([0, 0, 0, 0]) output[7] = 0x10 # Pre-init response if not session: # What's the correct behavior when session == 0? output[13] = 2 elif session in nn.natneg_preinit_session: # Should this be sent to both clients or just the one that # connected most recently? # I can't tell from a one sided packet capture of Pokemon. # For the time being, send to both clients just in case. output[13] = 2 nn.write_queue.put((output, nn.natneg_preinit_session[session], socket)) output[12] = (1, 0)[output[12]] # Swap the index del nn.natneg_preinit_session[session] else: output[13] = 0 nn.natneg_preinit_session[session] = addr nn.write_queue.put((output, addr, socket))
def handle_natneg_preinit(nn, recv_data, addr, socket): """Command: 0x0F - NN_PREINIT. Natneg v4 command thanks to Pipian. Only seems to be used in very few DS games, namely, Pokemon Black/White/Black 2/White 2. Example: fd fc 1e 66 6a b2 04 0f b5 e0 95 2a 00 24 38 b2 b3 5e Description: fd fc 1e 66 6a b2 - NATNEG magic 04 - NATNEG version 0f - NATNEG record type b5 e0 95 2a - Session id 00 - Client index (0x00 - Client, 0x01 - Host) 24 - State (0x00 - Waiting for client, 0x01 - Waiting for matchup, 0x02 - Ready) 38 b2 b3 5e - Other client's session id """ logger.log(logging.DEBUG, "Received pre-init command from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data)) session = utils.get_int(recv_data[-4:], 0) # Report response output = bytearray(recv_data[:-4]) + bytearray([0, 0, 0, 0]) output[7] = 0x10 # Pre-init response if not session: # What's the correct behavior when session == 0? output[13] = 2 elif session in nn.natneg_preinit_session: # Should this be sent to both clients or just the one that # connected most recently? # I can't tell from a one sided packet capture of Pokemon. # For the time being, send to both clients just in case. output[13] = 2 nn.write_queue.put( (output, nn.natneg_preinit_session[session], socket)) output[12] = (1, 0)[output[12]] # Swap the index del nn.natneg_preinit_session[session] else: output[13] = 0 nn.natneg_preinit_session[session] = addr nn.write_queue.put((output, addr, socket))
def handle_natneg_stateupdate(nn, recv_data, addr, socket): """Command: 0x04 - NN_STATEUPDATE. TODO Example: TODO Description: TODO """ logger.log( logging.WARNING, "Received unimplemented command NN_STATEUPDATE (0x04)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))
def handle_natneg_stateupdate(nn, recv_data, addr, socket): """Command: 0x04 - NN_STATEUPDATE. TODO Example: TODO Description: TODO """ logger.log(logging.WARNING, "Received unimplemented command NN_STATEUPDATE (0x04)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))
def handle_natneg_backup_ack(nn, recv_data, addr, socket): """Command: 0x09 - NN_BACKUP_ACK. Reply by the server for record NN_BACKUP_TEST (0x08). Only the record type is changed. Example: TODO Description: TODO """ logger.log(logging.WARNING, "Received server record type command NN_BACKUP_ACK (0x09)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))
def handle_natneg_backup_ack(nn, recv_data, addr, socket): """Command: 0x09 - NN_BACKUP_ACK. Reply by the server for record NN_BACKUP_TEST (0x08). Only the record type is changed. Example: TODO Description: TODO """ logger.log( logging.WARNING, "Received server record type command NN_BACKUP_ACK (0x09)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))
def handle_natneg_backup_test(nn, recv_data, addr, socket): """Command: 0x08 - NN_BACKUP_TEST. Send by the client. Example: TODO Description: Untested """ logger.log(logging.DEBUG, "Received backup command from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data)) # Backup response output = bytearray(recv_data) output[7] = 0x09 # NN_BACKUP_ACK nn.write_queue.put((output, addr, socket))
def handle_natneg_report(nn, recv_data, addr, socket): """Command: 0x0D - NN_REPORT. Send by the client. Example: fd fc 1e 66 6a b2 03 0d 3d f1 00 71 00 00 01 00 00 00 06 00 00 00 00 6d 61 72 69 6f 6b 61 72 74 77 69 69 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 0d - NATNEG record type 3d f1 00 71 - Session id 00 - Port type (0x00, 0x80 or 0x90) 00 - Client index (0x00 - Client, 0x01 - Host) 01 - NATNEG result (0x00 - Error, 0x01 - Success) 00 00 00 06 - NAT type (0x00 - No NAT, 0x01 - Firewall only, 0x02 - Full cone, 0x03 - Restricted cone, 0x04 - Port restricted cone, 0x05 - Symmetric, 0x06 - Unknown) 00 00 00 00 - NAT mapping scheme (0x00 - Unknown, 0x01 - Private same as public, 0x02 - Consistent port, 0x03 - Incremental, 0x04 - Mixed) GAME_NAME 00 - Game name (GAME_NAME is 49 bytes length) """ logger.log(logging.DEBUG, "Received report command from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data)) # Report response output = bytearray(recv_data[:21]) output[7] = 0x0e # Report response output[14] = 0 # Clear byte to match real server's response nn.write_queue.put((output, addr, socket))
def handle(self): """Handle NAT Negotiation request.""" recv_data, socket = self.request addr = self.client_address logger.log(logging.DEBUG, "Connection from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data)) # Make sure it's a legal packet if not recv_data.startswith(self.nn_magics): logger.log(logging.ERROR, "Aborted due to illegal packet!") return # Handle commands try: command = self.nn_commands.get(recv_data[7], handle_natneg) command(self.server, recv_data, addr, socket) except: logger.log(logging.ERROR, "Failed to handle command!") logger.log(logging.ERROR, "%s", traceback.format_exc())
def forward_data_to_client(self, data, forward_client): # Find session id of server # Iterate through the list of servers sent to the client and match by IP and port. # Is there a better way to determine this information? if self.forward_client == None or len(self.forward_client) != 2: return server, ip = self.find_server_in_cache(self.forward_client[0], self.forward_client[1], self.console) if server == None: if self.console == 0: server, ip = self.find_server_in_cache(self.forward_client[0], self.forward_client[1], 1) # Try Wii elif self.console == 1: server, ip = self.find_server_in_cache(self.forward_client[0], self.forward_client[1], 0) # Try DS self.log(logging.DEBUG, "find_server_in_cache returned: %s" % server) self.log(logging.DEBUG, "Trying to send message to %s:%d..." % (self.forward_client[0], self.forward_client[1])) self.log(logging.DEBUG, utils.pretty_print_hex(bytearray(data))) if server == None: return self.log(logging.DEBUG, "%s %s" % (ip, server['publicip'])) if server['publicip'] == ip and server['publicport'] == str(self.forward_client[1]): # Send command to server to get it to connect to natneg natneg_session = int(utils.generate_random_hex_str(8), 16) # Quick and lazy way to get a random 32bit integer. Replace with something else later output = bytearray([0xfe, 0xfd, 0x06]) output += utils.get_bytes_from_int(server['__session__']) output += bytearray(utils.get_bytes_from_int(natneg_session)) output += bytearray(data) if self.qr != None: self.qr.socket.sendto(output, forward_client) self.log(logging.DEBUG, "Forwarded data to %s:%s through QR server..." % (forward_client[0], forward_client[1])) else: # In case we can't contact the QR server, just try sending the packet directly. # This isn't standard behavior but it can work in some instances. client_s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) client_s.sendto(output, forward_client) self.log(logging.DEBUG, "Forwarded data to %s:%s directly (potential error occurred)..." % (forward_client[0], forward_client[1]))
def handle_natneg_connect(nn, recv_data, addr, socket): """Command: 0x05 - NN_CONNECT. Reply by the server for record NN_INIT (0x00). Example: fd fc 1e 66 6a b2 03 05 3d f1 00 71 18 ab ed 7a da 00 42 00 Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 05 - NATNEG record type 3d f1 00 71 - Session id 18 ab ed 7a - Remote IP da 00 - Remote port 42 - Got remote data 00 - Finished """ logger.log(logging.WARNING, "Received server record type command NN_CONNECT (0x05)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
def handle_natneg_connect_ping(nn, recv_data, addr, socket): """Command: 0x07 - NN_CONNECT_PING. Looks like NN_CONNECT but between clients. The server shouldn't be involved. Example: fd fc 1e 66 6a b2 03 07 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 07 - NATNEG record type ?? ?? ?? ?? - Session id ?? ?? ?? ?? - Remote IP ?? ?? - Remote port ?? - Sequence counter (0 or 1) ?? - Error """ logger.log(logging.WARNING, "Received unimplemented command NN_CONNECT_PING (0x07)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
def handle_natneg_connect(nn, recv_data, addr, socket): """Command: 0x05 - NN_CONNECT. Reply by the server for record NN_INIT (0x00). Example: fd fc 1e 66 6a b2 03 05 3d f1 00 71 18 ab ed 7a da 00 42 00 Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 05 - NATNEG record type 3d f1 00 71 - Session id 18 ab ed 7a - Remote IP da 00 - Remote port 42 - Got remote data 00 - Finished """ logger.log( logging.WARNING, "Received server record type command NN_CONNECT (0x05)" " from %s:%d...", *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
def forward_data_to_client(self, data, forward_client): # Find session id of server # Iterate through the list of servers sent to the client and match by # IP and port. Is there a better way to determine this information? if forward_client is None or len(forward_client) != 2: return server, ip = self.find_server_in_cache(forward_client[0], forward_client[1], self.console) if server is None: if self.console == 0: server, ip = self.find_server_in_cache(forward_client[0], forward_client[1], 1) # Try Wii elif self.console == 1: server, ip = self.find_server_in_cache(forward_client[0], forward_client[1], 0) # Try DS self.log(logging.DEBUG, "find_server_in_cache returned: %s", server) self.log(logging.DEBUG, "Trying to send message to %s:%d...", forward_client[0], forward_client[1]) self.log(logging.DEBUG, "%s", utils.pretty_print_hex(bytearray(data))) if server is None: return self.log(logging.DEBUG, "%s %s", ip, server['publicip']) if server['publicip'] == ip and \ server['publicport'] == str(forward_client[1]): if forward_client[1] == 0 and 'localport' in server: # No public port returned from client, try contacting on # the local port. forward_client = (forward_client[0], int(server['localport'])) # Send command to server to get it to connect to natneg # Quick and lazy way to get a random 32bit integer. Replace with # something else later cookie = int(utils.generate_random_hex_str(8), 16) # if (len(data) == 24 and bytearray(data)[0:10] == \ # bytearray([0x53, 0x42, 0x43, 0x4d, 0x03, # 0x00, 0x00, 0x00, 0x01, 0x04])) or \ # (len(data) == 40 and bytearray(data)[0:10] == \ # bytearray([0x53, 0x42, 0x43, 0x4d, # 0x0b, 0x00, 0x00, 0x00, # 0x01, 0x04])): if self.own_server is None and len(data) >= 16 and \ bytearray(data)[0:4] in (bytearray([0xbb, 0x49, 0xcc, 0x4d]), bytearray([0x53, 0x42, 0x43, 0x4d])): # Is the endianness the same between the DS and Wii here? # It seems so but I'm not positive. # Note to self: Port is little endian here. self_port = utils.get_short(bytearray(data[10:12]), 0, False) self_ip = '.'.join(["%d" % x for x in bytearray(data[12:16])]) self.own_server, _ = self.find_server_in_cache(self_ip, self_port, self.console) if self.own_server is None: if self.console == 0: # Try Wii self.own_server, _ = self.find_server_in_cache( self_ip, self_port, 1 ) elif self.console == 1: # Try DS self.own_server, _ = self.find_server_in_cache( self_ip, self_port, 0 ) if self.own_server is None: self.log(logging.DEBUG, "Could not find own server: %s:%d", self_ip, self_port) else: self.log(logging.DEBUG, "Found own server: %s", self.own_server) elif len(data) == 10 and \ bytearray(data)[0:6] == \ bytearray([0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2]): natneg_session = utils.get_int_signed(data, 6) self.log(logging.DEBUG, "Adding %d to natneg server list: %s", natneg_session, server) # Store info in backend so we can get it later in natneg self.server_manager.add_natneg_server(natneg_session, server) if self.own_server is not None: self.log(logging.DEBUG, "Adding %d to natneg server list: %s (self)", natneg_session, self.own_server) # Store info in backend so we can get it later in natneg self.server_manager.add_natneg_server(natneg_session, self.own_server) # if self.qr is not None: # own_server = self.qr.get_own_server() # # self.log(logging.DEBUG, # "Adding %d to natneg server list: %s", # natneg_session, own_server) # self.server_manager.add_natneg_server(natneg_session, # own_server) output = bytearray([0xfe, 0xfd, 0x06]) output += utils.get_bytes_from_int(server['__session__']) output += bytearray(utils.get_bytes_from_int(cookie)) output += bytearray(data) if self.qr is not None: self.log(logging.DEBUG, "Forwarded data to %s:%s through QR server...", forward_client[0], forward_client[1]) self.qr.socket.sendto(output, forward_client) else: # In case we can't contact the QR server, just try sending # the packet directly. This isn't standard behavior but it # can work in some instances. self.log(logging.DEBUG, "Forwarded data to %s:%s directly" " (potential error occurred)...", forward_client[0], forward_client[1]) client_s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) client_s.sendto(output, forward_client)
def handle_natneg_init(nn, recv_data, addr, socket): """Command: 0x00 - NN_INIT. Send by the client to initialize the connection. Example: fd fc 1e 66 6a b2 03 00 3d f1 00 71 00 00 01 0a 00 01 e2 00 00 6d 61 72 69 6f 6b 61 72 74 77 69 69 00 Description: fd fc 1e 66 6a b2 - NATNEG magic 03 - NATNEG version 00 - NATNEG record type 3d f1 00 71 - Session id 00 - Port type (between 0x00 and 0x03) 00 - Client index (0x00 - Client, 0x01 - Host) 01 - Use game port 0a 00 01 e2 - Local IP 00 00 - Local port GAME_NAME 00 - Game name """ logger.log(logging.DEBUG, "Received initialization from %s:%d...", *addr) session_id = utils.get_int(recv_data, 8) output = bytearray(recv_data[0:14]) # Checked with Tetris DS, Mario Kart DS, and Metroid Prime # Hunters, and this seems to be the standard response to 0x00 output += bytearray([0xff, 0xff, 0x6d, 0x16, 0xb5, 0x7d, 0xea]) output[7] = 0x01 # Initialization response nn.write_queue.put((output, addr, socket)) # Try to connect to the server gameid = utils.get_string(recv_data, 0x15) client_id = "%02x" % ord(recv_data[13]) localaddr = utils.get_local_addr(recv_data, 15) nn.session_list \ .setdefault(session_id, {}) \ .setdefault(client_id, { 'connected': False, 'addr': '', 'localaddr': None, 'serveraddr': None, 'gameid': None }) # In fact, it's a pointer client_id_session = nn.session_list[session_id][client_id] client_id_session['gameid'] = gameid client_id_session['addr'] = addr client_id_session['localaddr'] = localaddr for client in nn.session_list[session_id]: # Another pointer client_session = nn.session_list[session_id][client] if client_session['connected'] or client == client_id: continue # --- Send to requesting client # Get server info serveraddr = nn.get_server_addr(gameid, session_id, client) client_session['serveraddr'] = serveraddr logger.log(logging.DEBUG, "Found server from local ip/port: %s from %d", serveraddr, session_id) # Get public port if client_session['serveraddr'] is not None: publicport = int(client_session['serveraddr']['publicport']) else: publicport = \ client_session['localaddr'][1] or \ client_session['addr'][1] output = bytearray(recv_data[0:12]) output += utils.get_bytes_from_ip_str(client_session['addr'][0]) output += utils.get_bytes_from_short(publicport, True) # Unknown, always seems to be \x42\x00 output += bytearray([0x42, 0x00]) output[7] = 0x05 # NN_CONNECT nn.write_queue.put((output, client_id_session['addr'], socket)) logger.log(logging.DEBUG, "Sent connection request to %s:%d...", *client_id_session['addr']) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output)) # --- Send to other client # Get server info serveraddr = nn.get_server_addr(gameid, session_id, client_id) client_id_session['serveraddr'] = serveraddr logger.log(logging.DEBUG, "Found server 2 from local ip/port: %s from %d", serveraddr, session_id) # Get public port if client_id_session['serveraddr'] is not None: publicport = int(client_id_session['serveraddr']['publicport']) else: publicport = \ client_id_session['localaddr'][1] or \ client_id_session['addr'][1] output = bytearray(recv_data[0:12]) output += utils.get_bytes_from_ip_str(client_id_session['addr'][0]) output += utils.get_bytes_from_short(publicport, True) # Unknown, always seems to be \x42\x00 output += bytearray([0x42, 0x00]) output[7] = 0x05 # NN_CONNECT nn.write_queue.put((output, client_session['addr'], socket)) logger.log(logging.DEBUG, "Sent connection request to %s:%d...", *client_session['addr']) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))
def handle_packet(self, recv_data, addr): logger.log(logging.DEBUG, "Connection from %s:%d..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data)) # Make sure it's a legal packet if recv_data[0:6] != bytearray([0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2]): return session_id = struct.unpack("<I", recv_data[8:12])[0] session_id_raw = recv_data[8:12] # Handle commands if recv_data[7] == '\x00': logger.log( logging.DEBUG, "Received initialization from %s:%s..." % (addr[0], addr[1])) output = bytearray(recv_data[0:14]) output += bytearray( [0xff, 0xff, 0x6d, 0x16, 0xb5, 0x7d, 0xea] ) # Checked with Tetris DS, Mario Kart DS, and Metroid Prime Hunters, and this seems to be the standard response to 0x00 output[7] = 0x01 # Initialization response self.write_queue.put((output, addr)) # Try to connect to the server gameid = utils.get_string(recv_data, 0x15) client_id = "%02x" % ord(recv_data[13]) localip_raw = recv_data[15:19] localip_int_le = utils.get_ip(recv_data, 15) localip_int_be = utils.get_ip(recv_data, 15, True) localip = '.'.join(["%d" % ord(x) for x in localip_raw]) localport_raw = recv_data[19:21] localport = utils.get_short(localport_raw, 0, True) localaddr = (localip, localport, localip_int_le, localip_int_be) self.session_list.setdefault(session_id, {}).setdefault( client_id, { 'connected': False, 'addr': '', 'localaddr': None, 'serveraddr': None, 'gameid': None }) self.session_list[session_id][client_id]['gameid'] = gameid self.session_list[session_id][client_id]['addr'] = addr self.session_list[session_id][client_id]['localaddr'] = localaddr clients = len(self.session_list[session_id]) for client in self.session_list[session_id]: if self.session_list[session_id][client][ 'connected'] == False: # and self.session_list[session_id][client]['localaddr'][1] != 0: if client == client_id: continue #if self.session_list[session_id][client]['serveraddr'] == None: serveraddr = self.get_server_info(gameid, session_id, client) if serveraddr == None: serveraddr = self.get_server_info_alt( gameid, session_id, client) self.session_list[session_id][client][ 'serveraddr'] = serveraddr logger.log( logging.DEBUG, "Found server from local ip/port: %s from %d" % (serveraddr, session_id)) publicport = self.session_list[session_id][client]['addr'][ 1] if self.session_list[session_id][client]['localaddr'][ 1] != 0: publicport = self.session_list[session_id][client][ 'localaddr'][1] if self.session_list[session_id][client][ 'serveraddr'] != None: publicport = int(self.session_list[session_id][client] ['serveraddr']['publicport']) # Send to requesting client output = bytearray(recv_data[0:12]) output += bytearray([ int(x) for x in self.session_list[session_id][client] ['addr'][0].split('.') ]) output += utils.get_bytes_from_short(publicport, True) output += bytearray( [0x42, 0x00]) # Unknown, always seems to be \x42\x00 output[7] = 0x05 #self.write_queue.put((output, (self.session_list[session_id][client_id]['addr']))) self.write_queue.put((output, ( self.session_list[session_id][client_id]['addr'][0], self.session_list[session_id][client_id]['addr'][1]))) logger.log( logging.DEBUG, "Sent connection request to %s:%d..." % (self.session_list[session_id][client_id]['addr'][0], self.session_list[session_id][client_id]['addr'][1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) # Send to other client #if self.session_list[session_id][client_id]['serveraddr'] == None: serveraddr = self.get_server_info(gameid, session_id, client_id) if serveraddr == None: serveraddr = self.get_server_info_alt( gameid, session_id, client_id) self.session_list[session_id][client_id][ 'serveraddr'] = serveraddr logger.log( logging.DEBUG, "Found server 2 from local ip/port: %s from %d" % (serveraddr, session_id)) publicport = self.session_list[session_id][client_id][ 'addr'][1] if self.session_list[session_id][client_id]['localaddr'][ 1] != 0: publicport = self.session_list[session_id][client_id][ 'localaddr'][1] if self.session_list[session_id][client_id][ 'serveraddr'] != None: publicport = int( self.session_list[session_id][client_id] ['serveraddr']['publicport']) output = bytearray(recv_data[0:12]) output += bytearray([ int(x) for x in self.session_list[session_id] [client_id]['addr'][0].split('.') ]) output += utils.get_bytes_from_short(publicport, True) output += bytearray( [0x42, 0x00]) # Unknown, always seems to be \x42\x00 output[7] = 0x05 #self.write_queue.put((output, (self.session_list[session_id][client]['addr']))) self.write_queue.put( (output, (self.session_list[session_id][client]['addr'][0], self.session_list[session_id][client]['addr'][1]))) logger.log( logging.DEBUG, "Sent connection request to %s:%d..." % (self.session_list[session_id][client]['addr'][0], self.session_list[session_id][client]['addr'][1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) elif recv_data[7] == '\x06': # Was able to connect client_id = "%02x" % ord(recv_data[13]) logger.log( logging.DEBUG, "Received connected command from %s:%s..." % (addr[0], addr[1])) if session_id in self.session_list and client_id in self.session_list[ session_id]: self.session_list[session_id][client_id]['connected'] = True elif recv_data[7] == '\x0a': # Address check. Note: UNTESTED! client_id = "%02x" % ord(recv_data[13]) logger.log( logging.DEBUG, "Received address check command from %s:%s..." % (addr[0], addr[1])) output = bytearray(recv_data[0:15]) output += bytearray([int(x) for x in addr[0].split('.')]) output += utils.get_bytes_from_short(addr[1], True) output += bytearray(recv_data[len(output):]) output[7] = 0x0b self.write_queue.put((output, addr)) logger.log( logging.DEBUG, "Sent address check response to %s:%d..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) elif recv_data[7] == '\x0c': # Natify port_type = "%02x" % ord(recv_data[12]) logger.log( logging.DEBUG, "Received natify command from %s:%s..." % (addr[0], addr[1])) output = bytearray(recv_data) output[7] = 0x02 # ERT Test self.write_queue.put((output, addr)) logger.log(logging.DEBUG, "Sent natify response to %s:%d..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) elif recv_data[7] == '\x0d': # Report logger.log( logging.DEBUG, "Received report command from %s:%s..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data)) # Report response output = bytearray(recv_data[:21]) output[7] = 0x0e # Report response output[14] = 0 # Clear byte to match real server's response self.write_queue.put((output, addr)) elif recv_data[7] == '\x0f': # Natneg v4 command thanks to Pipian. # Only seems to be used in very few DS games (namely, Pokemon Black/White/Black 2/White 2). logger.log( logging.DEBUG, "Received pre-init command from %s:%s..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data)) session = utils.get_int(recv_data[-4:], 0) # Report response output = bytearray(recv_data[:-4]) + bytearray([0, 0, 0, 0]) output[7] = 0x10 # Pre-init response if session == 0: # What's the correct behavior when session == 0? output[13] = 2 elif session in self.natneg_preinit_session: # Should this be sent to both clients or just the one that connected most recently? # I can't tell from a one sided packet capture of Pokemon. # For the time being, send to both clients just in case. output[13] = 2 self.write_queue.put( (output, self.natneg_preinit_session[session])) output[12] = (1, 0)[output[12]] # Swap the index del self.natneg_preinit_session[session] else: output[13] = 0 self.natneg_preinit_session[session] = addr self.write_queue.put((output, addr)) else: # Was able to connect logger.log( logging.DEBUG, "Received unknown command %02x from %s:%s..." % (ord(recv_data[7]), addr[0], addr[1]))
def rawDataReceived(self, data): # First 2 bytes are the packet size. # # Third byte is the command byte. # According to Openspy-Core: # 0x00 - Server list request # 0x01 - Server info request # 0x02 - Send message request # 0x03 - Keep alive reply # 0x04 - Map loop request (?) # 0x05 - Player search request # # For Tetris DS, at the very least 0x00 and 0x02 need to be implemented. if self.forward_to_client: self.forward_to_client = False # Find session id of server # Iterate through the list of servers sent to the client and match by IP and port. # Is there a better way to determine this information? if self.console != 0: ip = str(ctypes.c_int32(utils.get_int_be(bytearray([int(x) for x in self.forward_client[0].split('.')]), 0)).value) # Wii else: ip = str(ctypes.c_int32(utils.get_int(bytearray([int(x) for x in self.forward_client[0].split('.')]), 0)).value) # DS logger.log(logging.DEBUG, "Trying to send message to %s:%d..." % (self.forward_client[0], self.forward_client[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(bytearray(data))) # Get server based on ip/port server = self.server_manager.find_server_by_address(ip, self.forward_client[1])._getvalue() logger.log(logging.DEBUG, "find_server_by_address returned: %s" % server) if server == None: pass logger.log(logging.DEBUG, "%s %s" % (ip, server['publicip'])) if server['publicip'] == ip and server['publicport'] == str(self.forward_client[1]): # Send command to server to get it to connect to natneg natneg_session = int(utils.generate_random_hex_str(8), 16) # Quick and lazy way to get a random 32bit integer. Replace with something else late.r output = bytearray([0xfe, 0xfd, 0x06]) output += utils.get_bytes_from_int(server['__session__']) output += bytearray(utils.get_bytes_from_int(natneg_session)) output += bytearray(data) client_s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) client_s.sendto(output, self.forward_client) logger.log(logging.DEBUG, "Forwarded data to %s:%s..." % (self.forward_client[0], self.forward_client[1])) return if data[2] == '\x00': # Server list request logger.log(logging.DEBUG, "Received server list request from %s:%s..." % (self.address.host, self.address.port)) # This code is so... not python. The C programmer in me is coming out strong. # TODO: Rewrite this section later? idx = 3 list_version = ord(data[idx]) idx += 1 encoding_version = ord(data[idx]) idx += 1 game_version = utils.get_int(data, idx) idx += 4 query_game = utils.get_string(data, idx) idx += len(query_game) + 1 game_name = utils.get_string(data, idx) idx += len(game_name) + 1 challenge = data[idx:idx+8] idx += 8 filter = utils.get_string(data, idx) idx += len(filter) + 1 fields = utils.get_string(data, idx) idx += len(fields) + 1 options = utils.get_int_be(data, idx) idx += 4 source_ip = 0 max_servers = 0 ALTERNATE_SOURCE_IP = 0x08 LIMIT_RESULT_COUNT = 0x80 if (options & LIMIT_RESULT_COUNT): max_servers = utils.get_int(data, idx) elif (options & ALTERNATE_SOURCE_IP): source_ip = utils.get_int(data, idx) if '\\' in fields: fields = [x for x in fields.split('\\') if x and not x.isspace()] #print "%02x %02x %08x" % (list_version, encoding_version, game_version) #print "%s" % query_game #print "%s" % game_name #print "%s" % challenge #print "%s" % filter #print "%s" % fields #print "%08x" % options #print "%d %08x" % (max_servers, source_ip) logger.log(logging.DEBUG, "list version: %02x / encoding version: %02x / game version: %08x / query game: %s / game name: %s / challenge: %s / filter: %s / fields: %s / options: %08x / max servers: %d / source ip: %08x" % (list_version, encoding_version, game_version, query_game, game_name, challenge, filter, fields, options, max_servers, source_ip)) # Requesting ip and port of client, not server if filter == "" or fields == "": output = bytearray([int(x) for x in self.address.host.split('.')]) output += utils.get_bytes_from_short_be(self.address.port) self.transport.write(bytes(output)) logger.log(logging.DEBUG, "Responding with own IP and port...") logger.log(logging.DEBUG, utils.pretty_print_hex(output)) else: self.find_server(query_game, filter, fields, max_servers, game_name, challenge) elif data[2] == '\x02': # Send message request dest_addr = '.'.join(["%d" % ord(x) for x in data[3:7]]) dest_port = utils.get_short_be(data, 7) # What's the pythonic way to do this? unpack? dest = (dest_addr, dest_port) logger.log(logging.DEBUG, "Received send message request from %s:%s to %s:%d..." % (self.address.host, self.address.port, dest_addr, dest_port)) logger.log(logging.DEBUG, utils.pretty_print_hex(bytearray(data))) self.forward_to_client = True self.forward_client = dest elif data[2] == '\x03': # Keep alive reply logger.log(logging.DEBUG, "Received keep alive from %s:%s..." % (self.address.host, self.address.port)) else: logger.log(logging.DEBUG, "Received unknown command (%02x) from %s:%s..." % (ord(data[2]), self.address.host, self.address.port)) logger.log(logging.DEBUG, utils.pretty_print_hex(bytearray(data))) logger.log(logging.DEBUG, utils.pretty_print_hex(data))
def handle_natneg(nn, recv_data, addr, socket): """Command: Unknown.""" logger.log(logging.DEBUG, "Received unknown command %02x from %s:%d...", ord(recv_data[7]), *addr) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
)) elif recv_data[0] == '\x0a': # Client Registered # Only sent to client, never received? self.log(logging.WARNING, address, session_id, "NOT IMPLEMENTED! Received client registered" " from %s:%s... %s", address[0], address[1], recv_data[5:]) else: self.log(logging.ERROR, address, session_id, "Unknown request from %s:%s:", address[0], address[1]) self.log(logging.DEBUG, address, session_id, "%s", utils.pretty_print_hex(recv_data)) def keepalive_check(self): # self.log(logging.DEBUG, None, session_id, # "Keep alive check on %d sessions", # len(self.sessions)) pruned = [] now = int(time.time()) for session_id in self.sessions: delta = now - self.sessions[session_id].keepalive timeout = 61 # Remove clients that haven't responded in x seconds if delta < 0 or delta >= timeout: pruned.append(session_id)
def rawDataReceived(self, data): try: # First 2 bytes are the packet size. # # Third byte is the command byte. # According to Openspy-Core: # 0x00 - Server list request # 0x01 - Server info request # 0x02 - Send message request # 0x03 - Keep alive reply # 0x04 - Map loop request (?) # 0x05 - Player search request # # For Tetris DS, at the very least 0x00 and 0x02 need to be # implemented. self.buffer += data while len(self.buffer) > 0: packet_len = utils.get_short(self.buffer, 0, True) packet = None if len(self.buffer) >= packet_len: packet = self.buffer[:packet_len] self.buffer = self.buffer[packet_len:] if packet is None: # Don't have enough for the entire packet, break. break if packet[2] == '\x00': # Server list request self.log(logging.DEBUG, "Received server list request from %s:%s...", self.address.host, self.address.port) # This code is so... not python. The C programmer in me is # coming out strong. # TODO: Rewrite this section later? idx = 3 list_version = ord(packet[idx]) idx += 1 encoding_version = ord(packet[idx]) idx += 1 game_version = utils.get_int(packet, idx) idx += 4 query_game = utils.get_string(packet, idx) idx += len(query_game) + 1 game_name = utils.get_string(packet, idx) idx += len(game_name) + 1 challenge = ''.join(packet[idx:idx + 8]) idx += 8 filter = utils.get_string(packet, idx) idx += len(filter) + 1 fields = utils.get_string(packet, idx) idx += len(fields) + 1 options = utils.get_int(packet, idx, True) idx += 4 source_ip = 0 max_servers = 0 NO_SERVER_LIST = 0x02 ALTERNATE_SOURCE_IP = 0x08 LIMIT_RESULT_COUNT = 0x80 send_ip = False if (options & LIMIT_RESULT_COUNT): max_servers = utils.get_int(packet, idx) elif (options & ALTERNATE_SOURCE_IP): source_ip = utils.get_ip(packet, idx) elif (options & NO_SERVER_LIST): send_ip = True if '\\' in fields: fields = [ x for x in fields.split('\\') if x and not x.isspace() ] # print "%02x %02x %08x" % \ # (list_version, encoding_version, game_version) # print "%s" % query_game # print "%s" % game_name # print "%s" % challenge # print "%s" % filter # print "%s" % fields # print "%08x" % options # print "%d %08x" % (max_servers, source_ip) self.log( logging.DEBUG, "list version: %02x / encoding version: %02x /" " game version: %08x / query game: %s /" " game name: %s / challenge: %s / filter: %s /" " fields: %s / options: %08x / max servers: %d /" " source ip: %08x" % (list_version, encoding_version, game_version, query_game, game_name, challenge, filter, fields, options, max_servers, source_ip)) # Requesting ip and port of client, not server if not filter and not fields or send_ip: output = bytearray( [int(x) for x in self.address.host.split('.')]) # Does this ever change? output += utils.get_bytes_from_short(6500, True) enc = gs_utils.EncTypeX() output_enc = enc.encrypt( self.secret_key_list[game_name], challenge, output) self.transport.write(bytes(output_enc)) self.log(logging.DEBUG, "%s", "Responding with own IP and game port...") self.log(logging.DEBUG, "%s", utils.pretty_print_hex(output)) else: self.find_server(query_game, filter, fields, max_servers, game_name, challenge) elif packet[2] == '\x02': # Send message request packet_len = utils.get_short(packet, 0, True) dest_addr = '.'.join(["%d" % ord(x) for x in packet[3:7]]) # What's the pythonic way to do this? unpack? dest_port = utils.get_short(packet, 7, True) dest = (dest_addr, dest_port) self.log( logging.DEBUG, "Received send message request from %s:%s to" " %s:%d... expecting %d byte packet.", self.address.host, self.address.port, dest_addr, dest_port, packet_len) self.log(logging.DEBUG, "%s", utils.pretty_print_hex(bytearray(packet))) if packet_len == len(packet): # Contains entire packet, send immediately. self.forward_data_to_client(packet[9:], dest) else: self.log(logging.ERROR, "%s", "ERROR: Could not find entire packet.") elif packet[2] == '\x03': # Keep alive reply self.log(logging.DEBUG, "Received keep alive from %s:%s...", (self.address.host, self.address.port)) else: self.log(logging.DEBUG, "Received unknown command (%02x) from %s:%s...", ord(packet[2]), self.address.host, self.address.port) self.log(logging.DEBUG, "%s", utils.pretty_print_hex(bytearray(packet))) except: self.log(logging.ERROR, "Unknown exception: %s", traceback.format_exc())
def wait_loop(self): while 1: recv_data, address = self.socket.recvfrom(2048) # Tetris DS overlay 10 @ 02144184 - Handle responses back to server # Tetris DS overlay 10 @ 02144184 - Handle responses back to server # # After some more packet inspection, it seems the format goes something like this: # - All server messages seem to always start with \xfe\xfd. # - The first byte from the client (or third byte from the server) is a command. # - Bytes 2 - 5 from the client is some kind of ID. This will have to be inspected later. I believe it's a # session-like ID because the number changes between connections. Copying the client's ID might be enough. # # The above was as guessed. # The code in Tetris DS (overlay 10) @ 0216E974 handles the network command creation. # R1 contains the command to be sent to the server. # R2 contains a pointer to some unknown integer that gets written after the command. # # - Commands # Commands range from 0x00 to 0x09 (for client only at least?) (Tetris DS overlay 10 @ 0216DDCC) # # CLIENT: # 0x01 - Response (Tetris DS overlay 10 @ 216DCA4) # Sends back base64 of RC4 encrypted string that was gotten from the server's 0x01. # # 0x03 - Send client state? (Tetris DS overlay 10 @ 216DA30) # Data sent: # 1) Loop for each localip available on the system, write as localip%d\x00(local ip) # 2) localport\x00(local port) # 3) natneg (either 0 or 1) # 4) ONLY IF STATE CHANGED: statechanged\x00(state) (Possible values: 0, 1, 2, 3) # 5) gamename\x00(game name) # 6) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE: publicip\x00(public ip) # 7) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE: publicport\x00(public port) # # if statechanged != 2: # Write various other data described here: http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Implementation # # 0x07 - Unknown, related to server's 0x06 (returns value sent from server) # # 0x08 - Keep alive? Sent after 0x03 # # 0x09 - Availability check # # SERVER: # 0x01 - Unknown # Data sent: # 8 random ASCII characters (?) followed by the public IP and port of the client as a hex string # # 0x06 - Unknown # First 4 bytes is some kind of id? I believe it's a unique identifier for the data being sent, # seeing how the server can send the same IP information many times in a row. If the IP information has # already been parsed then it doesn't waste time handling it. # # After that is a "SBCM" section which is 0x14 bytes in total. # SBCM information gets parsed at 2141A0C in Tetris DS overlay 10. # Seems to contain IP address information. # # The SBCM seems to contain a little information that must be parsed before. # After the SBCM: # \x03\x00\x00\x00 - Always the same? # \x01 - Found player? # \x04 - Unknown # (2 bytes) - Unknown. Port? # (4 bytes) - Player's IP # (4 bytes) - Unknown. Some other IP? Remote server IP? # \x00\x00\x00\x00 - Unknown but seems to get checked # # Another SBCM, after a player has been found and attempting to start a game: # \x03\x00\x00\x00 - Always the same? # \x05 - Connecting to player? # \x00 - Unknown # (2 bytes) - Unknown. Port? Same as before. # (4 bytes) - Player's IP # (4 bytes) - Unknown. Some other IP? Remote server IP? # # 0x0a - Response to 0x01 # Gets sent after receiving 0x01 from the client. So, server 0x01 -> client 0x01 -> server 0x0a. # Has no other data besides the client ID. # # - \xfd\xfc commands get passed directly between the other player(s)? # # # Open source version of GameSpy found here: https://github.com/sfcspanky/Openspy-Core/tree/master/qr # Use as reference. if recv_data[0] != '\x09': # Don't add a session if the client is trying to check if the game is available or not session_id = struct.unpack("<I", recv_data[1:5])[0] session_id_raw = recv_data[1:5] if session_id not in self.sessions: # Found a new session, add to session list self.sessions[session_id] = self.Session(address) self.sessions[session_id].session = session_id self.sessions[session_id].keepalive = int(time.time()) # Handle commands if recv_data[0] == '\x00': # Query self.log(logging.DEBUG, address, "NOT IMPLEMENTED! Received query from %s:%s... %s" % (address[0], address[1], recv_data[5:])) elif recv_data[0] == '\x01': # Challenge self.log(logging.DEBUG, address, "Received challenge from %s:%s... %s" % (address[0], address[1], recv_data[5:])) # Prepare the challenge sent from the server to be compared challenge = gs_utils.prepare_rc4_base64(self.sessions[session_id].secretkey, self.sessions[session_id].challenge) # Compare challenge client_challenge = recv_data[5:-1] if client_challenge == challenge: # Challenge succeeded # Send message back to client saying it was accepted packet = bytearray([0xfe, 0xfd, 0x0a]) # Send client registered command packet.extend(session_id_raw) # Get the session ID self.socket.sendto(packet, address) self.log(logging.DEBUG, address, "Sent client registered to %s:%s..." % (address[0], address[1])) else: # Failed the challenge, request another during the next heartbeat self.sessions[session_id].sent_challenge = False elif recv_data[0] == '\x02': # Echo self.log(logging.DEBUG, address, "NOT IMPLEMENTED! Received echo from %s:%s... %s" % (address[0], address[1], recv_data[5:])) elif recv_data[0] == '\x03': # Heartbeat data = recv_data[5:] self.log(logging.DEBUG, address, "Received heartbeat from %s:%s... %s" % (address[0], address[1], data)) # Parse information from heartbeat here d = data.rstrip('\0').split('\0') # It may be safe to ignore "unknown" keys because the proper key names get filled in later... k = {} for i in range(0, len(d), 2): #self.log(logging.DEBUG, address, "%s = %s" % (d[i], d[i+1])) k[d[i]] = d[i+1] if "gamename" in k: self.sessions[session_id].secretkey = self.secret_key_list[k['gamename']] #print "Got secret key %s for %s" % (self.sessions[session_id].secretkey, k['gamename']) if self.sessions[session_id].playerid == 0 and "dwc_pid" in k: # Get the player's id and then query the profile to figure out what console they are on. # The endianness of some server data depends on the endianness of the console, so we must be able # to account for that. self.sessions[session_id].playerid = int(k['dwc_pid']) profile = self.db.get_profile_from_profileid(self.sessions[session_id].playerid) if "console" in profile: self.sessions[session_id].console = profile['console'] if self.sessions[session_id].sent_challenge == False: addr_hex = ''.join(["%02X" % int(x) for x in address[0].split('.')]) port_hex = "%04X" % int(address[1]) server_challenge = utils.generate_random_str(8) + addr_hex + port_hex self.sessions[session_id].challenge = server_challenge packet = bytearray([0xfe, 0xfd, 0x01]) # Send challenge command packet.extend(session_id_raw) # Get the session ID packet.extend(server_challenge) packet.extend('\x00') self.socket.sendto(packet, address) self.log(logging.DEBUG, address, "Sent challenge to %s:%s..." % (address[0], address[1])) self.sessions[session_id].sent_challenge = True if 'publicip' in k and k['publicip'] == "0": #and k['dwc_hoststate'] == "2": # When dwc_hoststate == 2 then it doesn't send an IP, so calculate it ourselves if self.sessions[session_id].console != 0: k['publicip'] = str(ctypes.c_int32(utils.get_int_be(bytearray([int(x) for x in address[0].split('.')]), 0)).value) # Wii else: k['publicip'] = str(ctypes.c_int32(utils.get_int(bytearray([int(x) for x in address[0].split('.')]), 0)).value) # DS if "statechanged" in k: if k['statechanged'] == "1": # Create server #if k['publicport'] != "0" and k['publicip'] != "0": # dwc_mtype controls what kind of server query we're looking for. # dwc_mtype = 0 is used when looking for a matchmaking game. # dwc_mtype = 1 is unknown. # dwc_mtype = 2 is used when hosting a friends only game (possibly other uses too). # dwc_mtype = 3 is used when looking for a friends only game (possibly other uses too). # Some memory could be saved by clearing out any unwanted fields from k before sending. self.server_manager.update_server_list(k['gamename'], session_id, k, self.sessions[session_id].console)._getvalue() if session_id in self.sessions: self.sessions[session_id].gamename = k['gamename'] elif k['statechanged'] == "2": # Close server self.server_manager.delete_server(k['gamename'] , session_id) if session_id in self.sessions: self.sessions.pop(session_id) elif recv_data[0] == '\x04': # Add Error self.log(logging.WARNING, address, "NOT IMPLEMENTED! Received add error from %s:%s... %s" % (address[0], address[1], recv_data[5:])) elif recv_data[0] == '\x05': # Echo Response self.log(logging.WARNING, address, "NOT IMPLEMENTED! Received echo response from %s:%s... %s" % (address[0], address[1], recv_data[5:])) elif recv_data[0] == '\x06': # Client Message self.log(logging.WARNING, address, "NOT IMPLEMENTED! Received echo from %s:%s... %s" % (address[0], address[1], recv_data[5:])) elif recv_data[0] == '\x07': # Client Message Ack #self.log(logging.WARNING, address, "NOT IMPLEMENTED! Received client message ack from %s:%s... %s" % (address[0], address[1], recv_data[5:])) self.log(logging.DEBUG, address, "Received client message ack from %s:%s..." % (address[0], address[1])) elif recv_data[0] == '\x08': # Keep Alive self.log(logging.DEBUG, address, "Received keep alive from %s:%s..." % (address[0], address[1])) self.sessions[session_id].keepalive = int(time.time()) elif recv_data[0] == '\x09': # Available # Availability check only sent to *.available.gs.nintendowifi.net self.log(logging.DEBUG, address, "Received availability request for '%s' from %s:%s..." % (recv_data[5: -1], address[0], address[1])) self.socket.sendto(bytearray([0xfe, 0xfd, 0x09, 0x00, 0x00, 0x00, 0x00]), address) elif recv_data[0] == '\x0a': # Client Registered # Only sent to client, never received? self.log(logging.WARNING, address, "NOT IMPLEMENTED! Received client registered from %s:%s... %s" % (address[0], address[1], recv_data[5:])) else: self.log(logging.ERROR, address, "Unknown request from %s:%s:" % (address[0], address[1])) self.log(logging.DEBUG, address, utils.pretty_print_hex(recv_data))
def start(self): # Start natneg server address = ('0.0.0.0', 27901) # accessible to outside connections (use this if you don't know what you're doing) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(address) logger.log(logging.INFO, "Server is now listening on %s:%s..." % (address[0], address[1])) while 1: recv_data, addr = s.recvfrom(2048) logger.log(logging.DEBUG, "Connection from %s:%d..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data)) # Make sure it's a legal packet if recv_data[0:6] != bytearray([0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2]): continue session_id = struct.unpack("<I", recv_data[8:12])[0] session_id_raw = recv_data[8:12] # Handle commands if recv_data[7] == '\x00': logger.log(logging.DEBUG, "Received initialization from %s:%s..." % (addr[0], addr[1])) output = bytearray(recv_data[0:14]) output += bytearray([0xff, 0xff, 0x6d, 0x16, 0xb5, 0x7d, 0xea ]) # Checked with Tetris DS, Mario Kart DS, and Metroid Prime Hunters, and this seems to be the standard response to 0x00 output[7] = 0x01 # Initialization response s.sendto(output, addr) # Try to connect to the server gameid = utils.get_string(recv_data, 0x16) client_id = "%02x" % ord(recv_data[13]) if gameid not in self.session_list: self.session_list[gameid] = {} if session_id not in self.session_list[gameid]: self.session_list[gameid][session_id] = {} if client_id not in self.session_list[gameid][session_id]: self.session_list[gameid][session_id][client_id] = { 'connected': False, 'addr': '' } self.session_list[gameid][session_id][client_id]['addr'] = addr clients = len(self.session_list[gameid][session_id]) for client in self.session_list[gameid][session_id]: if self.session_list[gameid][session_id][client]['connected'] == False: if client == client_id: continue # Send to requesting client output = bytearray(recv_data[0:12]) output += bytearray([int(x) for x in self.session_list[gameid][session_id][client]['addr'][0].split('.')]) output += utils.get_bytes_from_short_be(self.session_list[gameid][session_id][client]['addr'][1]) output += bytearray([0x42, 0x00]) # Unknown, always seems to be \x42\x00 output[7] = 0x05 s.sendto(output, (self.session_list[gameid][session_id][client_id]['addr'])) logger.log(logging.DEBUG, "Sent connection request to %s:%d..." % (self.session_list[gameid][session_id][client_id]['addr'][0], self.session_list[gameid][session_id][client_id]['addr'][1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) logger.log(logging.DEBUG, "") # Send to other client output = bytearray(recv_data[0:12]) output += bytearray([int(x) for x in self.session_list[gameid][session_id][client_id]['addr'][0].split('.')]) output += utils.get_bytes_from_short_be(self.session_list[gameid][session_id][client_id]['addr'][1]) output += bytearray([0x42, 0x00]) # Unknown, always seems to be \x42\x00 output[7] = 0x05 s.sendto(output, (self.session_list[gameid][session_id][client]['addr'])) logger.log(logging.DEBUG, "Sent connection request to %s:%d..." % (self.session_list[gameid][session_id][client]['addr'][0], self.session_list[gameid][session_id][client]['addr'][1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) logger.log(logging.DEBUG, "") elif recv_data[7] == '\x06': # Was able to connect client_id = "%02x" % ord(recv_data[13]) logger.log(logging.DEBUG, "Received connected command from %s:%s..." % (addr[0], addr[1])) if gameid not in self.session_list: pass if session_id not in self.session_list[gameid]: pass if client_id not in self.session_list[gameid][session_id]: pass #self.session_list[gameid][session_id][client_id]['connected'] = True elif recv_data[7] == '\x0a': # Address check. Note: UNTESTED! client_id = "%02x" % ord(recv_data[13]) logger.log(logging.DEBUG, "Received address check command from %s:%s..." % (addr[0], addr[1])) output = bytearray(recv_data[0:15]) output += bytearray([int(x) for x in addr[0].split('.')]) output += utils.get_bytes_from_short_be(addr[1]) output += bytearray(recv_data[len(output):]) output[7] = 0x0b s.sendto(output, addr) logger.log(logging.DEBUG, "Sent address check response to %s:%d..." % (self.session_list[gameid][session_id][client]['addr'][0], self.session_list[gameid][session_id][client]['addr'][1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) elif recv_data[7] == '\x0d': client_id = "%02x" % ord(recv_data[13]) logger.log(logging.DEBUG, "Received report command from %s:%s..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data)) output = bytearray(recv_data) output[7] = 0x0e # Report response s.sendto(recv_data, addr) else: # Was able to connect logger.log(logging.DEBUG, "Received unknown command %02x from %s:%s..." % (ord(recv_data[7]), addr[0], addr[1]))
def handle_packet(self, recv_data, addr): logger.log(logging.DEBUG, "Connection from %s:%d..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data)) # Make sure it's a legal packet if recv_data[0:6] != bytearray([0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2]): return session_id = struct.unpack("<I", recv_data[8:12])[0] session_id_raw = recv_data[8:12] # Handle commands if recv_data[7] == '\x00': logger.log(logging.DEBUG, "Received initialization from %s:%s..." % (addr[0], addr[1])) output = bytearray(recv_data[0:14]) output += bytearray([0xff, 0xff, 0x6d, 0x16, 0xb5, 0x7d, 0xea ]) # Checked with Tetris DS, Mario Kart DS, and Metroid Prime Hunters, and this seems to be the standard response to 0x00 output[7] = 0x01 # Initialization response self.write_queue.put((output, addr)) # Try to connect to the server gameid = utils.get_string(recv_data, 0x15) client_id = "%02x" % ord(recv_data[13]) localip_raw = recv_data[15:19] localip_int_le = utils.get_int(recv_data, 15) localip_int_be = utils.get_int_be(recv_data, 15) localip = '.'.join(["%d" % ord(x) for x in localip_raw]) localport_raw = recv_data[19:21] localport = utils.get_short_be(localport_raw, 0) localaddr = (localip, localport, localip_int_le, localip_int_be) if session_id not in self.session_list: self.session_list[session_id] = {} if client_id not in self.session_list[session_id]: self.session_list[session_id][client_id] = { 'connected': False, 'addr': '', 'localaddr': None, 'serveraddr': None, 'gameid': None } self.session_list[session_id][client_id]['gameid'] = gameid self.session_list[session_id][client_id]['addr'] = addr self.session_list[session_id][client_id]['localaddr'] = localaddr clients = len(self.session_list[session_id]) for client in self.session_list[session_id]: if self.session_list[session_id][client]['connected'] == False: # and self.session_list[session_id][client]['localaddr'][1] != 0: if client == client_id: continue #if self.session_list[session_id][client]['serveraddr'] == None: serveraddr = self.get_server_info(gameid, session_id, client) if serveraddr == None: serveraddr = self.get_server_info_alt(gameid, session_id, client) self.session_list[session_id][client]['serveraddr'] = serveraddr logger.log(logging.DEBUG, "Found server from local ip/port: %s from %d" % (serveraddr, session_id)) publicport = self.session_list[session_id][client]['addr'][1] if self.session_list[session_id][client]['localaddr'][1] != 0: publicport = self.session_list[session_id][client]['localaddr'][1] if self.session_list[session_id][client]['serveraddr'] != None: publicport = int(self.session_list[session_id][client]['serveraddr']['publicport']) # Send to requesting client output = bytearray(recv_data[0:12]) output += bytearray([int(x) for x in self.session_list[session_id][client]['addr'][0].split('.')]) output += utils.get_bytes_from_short_be(publicport) output += bytearray([0x42, 0x00]) # Unknown, always seems to be \x42\x00 output[7] = 0x05 #self.write_queue.put((output, (self.session_list[session_id][client_id]['addr']))) self.write_queue.put((output, (self.session_list[session_id][client_id]['addr'][0], self.session_list[session_id][client_id]['addr'][1]))) logger.log(logging.DEBUG, "Sent connection request to %s:%d..." % (self.session_list[session_id][client_id]['addr'][0], self.session_list[session_id][client_id]['addr'][1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) # Send to other client #if self.session_list[session_id][client_id]['serveraddr'] == None: serveraddr = self.get_server_info(gameid, session_id, client_id) if serveraddr == None: serveraddr = self.get_server_info_alt(gameid, session_id, client) self.session_list[session_id][client_id]['serveraddr'] = serveraddr logger.log(logging.DEBUG, "Found server 2 from local ip/port: %s from %d" % (serveraddr, session_id)) publicport = self.session_list[session_id][client_id]['addr'][1] if self.session_list[session_id][client_id]['localaddr'][1] != 0: publicport = self.session_list[session_id][client_id]['localaddr'][1] if self.session_list[session_id][client_id]['serveraddr'] != None: publicport = int(self.session_list[session_id][client_id]['serveraddr']['publicport']) output = bytearray(recv_data[0:12]) output += bytearray([int(x) for x in self.session_list[session_id][client_id]['addr'][0].split('.')]) output += utils.get_bytes_from_short_be(publicport) output += bytearray([0x42, 0x00]) # Unknown, always seems to be \x42\x00 output[7] = 0x05 #self.write_queue.put((output, (self.session_list[session_id][client]['addr']))) self.write_queue.put((output, (self.session_list[session_id][client]['addr'][0], self.session_list[session_id][client]['addr'][1]))) logger.log(logging.DEBUG, "Sent connection request to %s:%d..." % (self.session_list[session_id][client]['addr'][0], self.session_list[session_id][client]['addr'][1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) elif recv_data[7] == '\x06': # Was able to connect client_id = "%02x" % ord(recv_data[13]) logger.log(logging.DEBUG, "Received connected command from %s:%s..." % (addr[0], addr[1])) if session_id not in self.session_list: return if client_id not in self.session_list[session_id]: return self.session_list[session_id][client_id]['connected'] = True elif recv_data[7] == '\x0a': # Address check. Note: UNTESTED! client_id = "%02x" % ord(recv_data[13]) logger.log(logging.DEBUG, "Received address check command from %s:%s..." % (addr[0], addr[1])) output = bytearray(recv_data[0:15]) output += bytearray([int(x) for x in addr[0].split('.')]) output += utils.get_bytes_from_short_be(addr[1]) output += bytearray(recv_data[len(output):]) output[7] = 0x0b self.write_queue.put((output, addr)) logger.log(logging.DEBUG, "Sent address check response to %s:%d..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) elif recv_data[7] == '\x0c': # Natify port_type = "%02x" % ord(recv_data[12]) logger.log(logging.DEBUG, "Received natify command from %s:%s..." % (addr[0], addr[1])) output = bytearray(recv_data) output[7] = 0x02 # ERT Test self.write_queue.put((output, addr)) logger.log(logging.DEBUG, "Sent natify response to %s:%d..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) elif recv_data[7] == '\x0d': client_id = "%02x" % ord(recv_data[13]) logger.log(logging.DEBUG, "Received report command from %s:%s..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data)) output = bytearray(recv_data) output[7] = 0x0e # Report response self.write_queue.put((recv_data, addr)) else: # Was able to connect logger.log(logging.DEBUG, "Received unknown command %02x from %s:%s..." % (ord(recv_data[7]), addr[0], addr[1]))
def rawDataReceived(self, data): # First 2 bytes are the packet size. # # Third byte is the command byte. # According to Openspy-Core: # 0x00 - Server list request # 0x01 - Server info request # 0x02 - Send message request # 0x03 - Keep alive reply # 0x04 - Map loop request (?) # 0x05 - Player search request # # For Tetris DS, at the very least 0x00 and 0x02 need to be implemented. if self.forward_to_client: if self.forward_packet == None: self.forward_packet = data else: self.forward_packet += data if self.header_length + len(self.forward_packet) >= self.expected_packet_length: # Is it possible that multiple packets will need to be waited for? # Is it possible that more data will be in the last packet than expected? self.forward_data_to_client(self.forward_packet, self.forward_client) self.forward_to_client = False self.forward_client = () self.header_length = 0 self.expected_packet_length = 0 self.forward_packet = None return if data[2] == '\x00': # Server list request self.log(logging.DEBUG, "Received server list request from %s:%s..." % (self.address.host, self.address.port)) # This code is so... not python. The C programmer in me is coming out strong. # TODO: Rewrite this section later? idx = 3 list_version = ord(data[idx]) idx += 1 encoding_version = ord(data[idx]) idx += 1 game_version = utils.get_int(data, idx) idx += 4 query_game = utils.get_string(data, idx) idx += len(query_game) + 1 game_name = utils.get_string(data, idx) idx += len(game_name) + 1 challenge = data[idx:idx+8] idx += 8 filter = utils.get_string(data, idx) idx += len(filter) + 1 fields = utils.get_string(data, idx) idx += len(fields) + 1 options = utils.get_int_be(data, idx) idx += 4 source_ip = 0 max_servers = 0 NO_SERVER_LIST = 0x02 ALTERNATE_SOURCE_IP = 0x08 LIMIT_RESULT_COUNT = 0x80 send_ip = False if (options & LIMIT_RESULT_COUNT): max_servers = utils.get_int(data, idx) elif (options & ALTERNATE_SOURCE_IP): source_ip = utils.get_int(data, idx) elif (options & NO_SERVER_LIST): send_ip = True if '\\' in fields: fields = [x for x in fields.split('\\') if x and not x.isspace()] #print "%02x %02x %08x" % (list_version, encoding_version, game_version) #print "%s" % query_game #print "%s" % game_name #print "%s" % challenge #print "%s" % filter #print "%s" % fields #print "%08x" % options #print "%d %08x" % (max_servers, source_ip) self.log(logging.DEBUG, "list version: %02x / encoding version: %02x / game version: %08x / query game: %s / game name: %s / challenge: %s / filter: %s / fields: %s / options: %08x / max servers: %d / source ip: %08x" % (list_version, encoding_version, game_version, query_game, game_name, challenge, filter, fields, options, max_servers, source_ip)) # Requesting ip and port of client, not server if filter == "" or fields == "" or send_ip == True: output = bytearray([int(x) for x in self.address.host.split('.')]) output += utils.get_bytes_from_short_be(6500) # Does this ever change? enc = gs_utils.EncTypeX() output_enc = enc.encrypt(self.secret_key_list[game_name], challenge, output) self.transport.write(bytes(output_enc)) self.log(logging.DEBUG, "Responding with own IP and game port...") self.log(logging.DEBUG, utils.pretty_print_hex(output)) else: self.find_server(query_game, filter, fields, max_servers, game_name, challenge) elif data[2] == '\x02': # Send message request packet_len = utils.get_short_be(data, 0) dest_addr = '.'.join(["%d" % ord(x) for x in data[3:7]]) dest_port = utils.get_short_be(data, 7) # What's the pythonic way to do this? unpack? dest = (dest_addr, dest_port) self.log(logging.DEBUG, "Received send message request from %s:%s to %s:%d... expecting %d byte packet." % (self.address.host, self.address.port, dest_addr, dest_port, packet_len)) self.log(logging.DEBUG, utils.pretty_print_hex(bytearray(data))) if packet_len == len(data): # Contains entire packet, send immediately. self.forward_data_to_client(data[3:], dest) self.forward_to_client = False self.forward_client = () self.header_length = 0 self.expected_packet_length = 0 self.forward_packet = None else: self.forward_to_client = True self.forward_client = dest self.header_length = len(data) self.expected_packet_length = packet_len elif data[2] == '\x03': # Keep alive reply self.log(logging.DEBUG, "Received keep alive from %s:%s..." % (self.address.host, self.address.port)) else: self.log(logging.DEBUG, "Received unknown command (%02x) from %s:%s..." % (ord(data[2]), self.address.host, self.address.port)) self.log(logging.DEBUG, utils.pretty_print_hex(bytearray(data))) self.log(logging.DEBUG, utils.pretty_print_hex(data))
elif recv_data[0] == '\x08': # Keep Alive self.log(logging.DEBUG, address, session_id, "Received keep alive from %s:%s..." % (address[0], address[1])) self.sessions[session_id].keepalive = int(time.time()) elif recv_data[0] == '\x09': # Available # Availability check only sent to *.available.gs.nintendowifi.net self.log(logging.DEBUG, address, session_id, "Received availability request for '%s' from %s:%s..." % (recv_data[5: -1], address[0], address[1])) self.write_queue.put((bytearray([0xfe, 0xfd, 0x09, 0x00, 0x00, 0x00, 0x00]), address)) elif recv_data[0] == '\x0a': # Client Registered # Only sent to client, never received? self.log(logging.WARNING, address, session_id, "NOT IMPLEMENTED! Received client registered from %s:%s... %s" % (address[0], address[1], recv_data[5:])) else: self.log(logging.ERROR, address, session_id, "Unknown request from %s:%s:" % (address[0], address[1])) self.log(logging.DEBUG, address, session_id, utils.pretty_print_hex(recv_data)) def keepalive_check(self): #self.log(logging.DEBUG, None, session_id, "Keep alive check on %d sessions" % (len(self.sessions))) pruned = [] now = int(time.time()) for session_id in self.sessions: delta = now - self.sessions[session_id].keepalive timeout = 61 # Remove clients that haven't responded in x seconds if delta < 0 or delta >= timeout: pruned.append(session_id) self.server_manager.delete_server(self.sessions[session_id].gamename, self.sessions[session_id].session) self.log(logging.DEBUG, None, session_id, "Keep alive check removed %s:%s for game %s. Client hasn't responded in %d seconds." % (self.sessions[session_id].address[0], self.sessions[session_id].address[1], self.sessions[session_id].gamename, delta))
def handle_packet(self, recv_data, addr): """Handle NATNEG. TODO: Pointer to methods for recv_data[7].""" logger.log(logging.DEBUG, "Connection from %s:%d...", addr[0], addr[1]) logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data)) # Make sure it's a legal packet if recv_data[0:6] != bytearray([0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2]): return session_id = struct.unpack("<I", recv_data[8:12])[0] session_id_raw = recv_data[8:12] # Handle commands if recv_data[7] == '\x00': logger.log(logging.DEBUG, "Received initialization from %s:%s...", addr[0], addr[1]) output = bytearray(recv_data[0:14]) # Checked with Tetris DS, Mario Kart DS, and Metroid Prime # Hunters, and this seems to be the standard response to 0x00 output += bytearray([0xff, 0xff, 0x6d, 0x16, 0xb5, 0x7d, 0xea]) output[7] = 0x01 # Initialization response self.write_queue.put((output, addr)) # Try to connect to the server gameid = utils.get_string(recv_data, 0x15) client_id = "%02x" % ord(recv_data[13]) localip_raw = recv_data[15:19] localip_int_le = utils.get_ip(recv_data, 15) localip_int_be = utils.get_ip(recv_data, 15, True) localip = '.'.join(["%d" % ord(x) for x in localip_raw]) localport_raw = recv_data[19:21] localport = utils.get_short(localport_raw, 0, True) localaddr = (localip, localport, localip_int_le, localip_int_be) self.session_list \ .setdefault(session_id, {}) \ .setdefault(client_id, { 'connected': False, 'addr': '', 'localaddr': None, 'serveraddr': None, 'gameid': None }) # In fact, it's a pointer (cf. shallow copy) client_id_session = self.session_list[session_id][client_id] client_id_session['gameid'] = gameid client_id_session['addr'] = addr client_id_session['localaddr'] = localaddr clients = len(self.session_list[session_id]) # Unused? for client in self.session_list[session_id]: # Another shallow copy client_session = self.session_list[session_id][client] if client_session['connected'] or client == client_id: continue # if client_session['serveraddr'] \ # is None: serveraddr = self.get_server_info(gameid, session_id, client) if serveraddr is None: serveraddr = self.get_server_info_alt( gameid, session_id, client ) client_session['serveraddr'] = serveraddr logger.log(logging.DEBUG, "Found server from local ip/port: %s from %d", serveraddr, session_id) publicport = client_session['addr'][1] if client_session['localaddr'][1]: publicport = client_session['localaddr'][1] if client_session['serveraddr'] is not None: publicport = int( client_session['serveraddr']['publicport'] ) # Send to requesting client output = bytearray(recv_data[0:12]) output += bytearray([ int(x) for x in client_session['addr'][0].split('.') ]) output += utils.get_bytes_from_short(publicport, True) # Unknown, always seems to be \x42\x00 output += bytearray([0x42, 0x00]) output[7] = 0x05 # self.write_queue.put(( # output, # (client_id_session['addr']) # )) self.write_queue.put(( output, (client_id_session['addr'][0], client_id_session['addr'][1]) )) logger.log(logging.DEBUG, "Sent connection request to %s:%d...", client_id_session['addr'][0], client_id_session['addr'][1]) logger.log(logging.DEBUG, '%s', utils.pretty_print_hex(output)) # Send to other client # if client_id_session['serveraddr'] is None: serveraddr = self.get_server_info( gameid, session_id, client_id ) if serveraddr is None: serveraddr = self.get_server_info_alt( gameid, session_id, client_id ) client_id_session['serveraddr'] = serveraddr logger.log(logging.DEBUG, "Found server 2 from local ip/port: %s from %d", serveraddr, session_id) publicport = client_id_session['addr'][1] if client_id_session['localaddr'][1]: publicport = client_id_session['localaddr'][1] if client_id_session['serveraddr'] is not None: publicport = int( client_id_session['serveraddr']['publicport'] ) output = bytearray(recv_data[0:12]) output += bytearray( [int(x) for x in client_id_session['addr'][0].split('.')] ) output += utils.get_bytes_from_short(publicport, True) # Unknown, always seems to be \x42\x00 output += bytearray([0x42, 0x00]) output[7] = 0x05 # self.write_queue.put((output, (client_session['addr']))) self.write_queue.put((output, (client_session['addr'][0], client_session['addr'][1]))) logger.log(logging.DEBUG, "Sent connection request to %s:%d...", client_session['addr'][0], client_session['addr'][1]) logger.log(logging.DEBUG, '%s', utils.pretty_print_hex(output)) elif recv_data[7] == '\x06': # Was able to connect client_id = "%02x" % ord(recv_data[13]) logger.log(logging.DEBUG, "Received connected command from %s:%s...", addr[0], addr[1]) if session_id in self.session_list and \ client_id in self.session_list[session_id]: self.session_list[session_id][client_id]['connected'] = True elif recv_data[7] == '\x0a': # Address check. Note: UNTESTED! client_id = "%02x" % ord(recv_data[13]) logger.log(logging.DEBUG, "Received address check command from %s:%s...", addr[0], addr[1]) output = bytearray(recv_data[0:15]) output += bytearray([int(x) for x in addr[0].split('.')]) output += utils.get_bytes_from_short(addr[1], True) output += bytearray(recv_data[len(output):]) output[7] = 0x0b self.write_queue.put((output, addr)) logger.log(logging.DEBUG, "Sent address check response to %s:%d...", addr[0], addr[1]) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output)) elif recv_data[7] == '\x0c': # Natify port_type = "%02x" % ord(recv_data[12]) logger.log(logging.DEBUG, "Received natify command from %s:%s...", addr[0], addr[1]) output = bytearray(recv_data) output[7] = 0x02 # ERT Test self.write_queue.put((output, addr)) logger.log(logging.DEBUG, "Sent natify response to %s:%d...", addr[0], addr[1]) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output)) elif recv_data[7] == '\x0d': # Report logger.log(logging.DEBUG, "Received report command from %s:%s...", addr[0], addr[1]) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data)) # Report response output = bytearray(recv_data[:21]) output[7] = 0x0e # Report response output[14] = 0 # Clear byte to match real server's response self.write_queue.put((output, addr)) elif recv_data[7] == '\x0f': # Natneg v4 command thanks to Pipian. # Only seems to be used in very few DS games (namely, # Pokemon Black/White/Black 2/White 2). logger.log(logging.DEBUG, "Received pre-init command from %s:%s...", addr[0], addr[1]) logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data)) session = utils.get_int(recv_data[-4:], 0) # Report response output = bytearray(recv_data[:-4]) + bytearray([0, 0, 0, 0]) output[7] = 0x10 # Pre-init response if not session: # What's the correct behavior when session == 0? output[13] = 2 elif session in self.natneg_preinit_session: # Should this be sent to both clients or just the one that # connected most recently? # I can't tell from a one sided packet capture of Pokemon. # For the time being, send to both clients just in case. output[13] = 2 self.write_queue.put((output, self.natneg_preinit_session[session])) output[12] = (1, 0)[output[12]] # Swap the index del self.natneg_preinit_session[session] else: output[13] = 0 self.natneg_preinit_session[session] = addr self.write_queue.put((output, addr)) else: # Was able to connect logger.log(logging.DEBUG, "Received unknown command %02x from %s:%s...", ord(recv_data[7]), addr[0], addr[1])
self.write_queue.put( (bytearray([0xfe, 0xfd, 0x09, 0x00, 0x00, 0x00, 0x00]), address)) elif recv_data[0] == '\x0a': # Client Registered # Only sent to client, never received? self.log( logging.WARNING, address, session_id, "NOT IMPLEMENTED! Received client registered from %s:%s... %s" % (address[0], address[1], recv_data[5:])) else: self.log(logging.ERROR, address, session_id, "Unknown request from %s:%s:" % (address[0], address[1])) self.log(logging.DEBUG, address, session_id, utils.pretty_print_hex(recv_data)) def keepalive_check(self): #self.log(logging.DEBUG, None, session_id, "Keep alive check on %d sessions" % (len(self.sessions))) pruned = [] now = int(time.time()) for session_id in self.sessions: delta = now - self.sessions[session_id].keepalive timeout = 61 # Remove clients that haven't responded in x seconds if delta < 0 or delta >= timeout: pruned.append(session_id) self.server_manager.delete_server( self.sessions[session_id].gamename,
def wait_loop(self): while 1: recv_data, address = self.socket.recvfrom(2048) # Tetris DS overlay 10 @ 02144184 - Handle responses back to server # Tetris DS overlay 10 @ 02144184 - Handle responses back to server # # After some more packet inspection, it seems the format goes something like this: # - All server messages seem to always start with \xfe\xfd. # - The first byte from the client (or third byte from the server) is a command. # - Bytes 2 - 5 from the client is some kind of ID. This will have to be inspected later. I believe it's a # session-like ID because the number changes between connections. Copying the client's ID might be enough. # # The above was as guessed. # The code in Tetris DS (overlay 10) @ 0216E974 handles the network command creation. # R1 contains the command to be sent to the server. # R2 contains a pointer to some unknown integer that gets written after the command. # # - Commands # Commands range from 0x00 to 0x09 (for client only at least?) (Tetris DS overlay 10 @ 0216DDCC) # # CLIENT: # 0x01 - Response (Tetris DS overlay 10 @ 216DCA4) # Sends back base64 of RC4 encrypted string that was gotten from the server's 0x01. # # 0x03 - Send client state? (Tetris DS overlay 10 @ 216DA30) # Data sent: # 1) Loop for each localip available on the system, write as localip%d\x00(local ip) # 2) localport\x00(local port) # 3) natneg (either 0 or 1) # 4) ONLY IF STATE CHANGED: statechanged\x00(state) (Possible values: 0, 1, 2, 3) # 5) gamename\x00(game name) # 6) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE: publicip\x00(public ip) # 7) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE: publicport\x00(public port) # # if statechanged != 2: # Write various other data described here: http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Implementation # # 0x07 - Unknown, related to server's 0x06 (returns value sent from server) # # 0x08 - Keep alive? Sent after 0x03 # # 0x09 - Availability check # # SERVER: # 0x01 - Unknown # Data sent: # 8 random ASCII characters (?) followed by the public IP and port of the client as a hex string # # 0x06 - Unknown # First 4 bytes is some kind of id? I believe it's a unique identifier for the data being sent, # seeing how the server can send the same IP information many times in a row. If the IP information has # already been parsed then it doesn't waste time handling it. # # After that is a "SBCM" section which is 0x14 bytes in total. # SBCM information gets parsed at 2141A0C in Tetris DS overlay 10. # Seems to contain IP address information. # # The SBCM seems to contain a little information that must be parsed before. # After the SBCM: # \x03\x00\x00\x00 - Always the same? # \x01 - Found player? # \x04 - Unknown # (2 bytes) - Unknown. Port? # (4 bytes) - Player's IP # (4 bytes) - Unknown. Some other IP? Remote server IP? # \x00\x00\x00\x00 - Unknown but seems to get checked # # Another SBCM, after a player has been found and attempting to start a game: # \x03\x00\x00\x00 - Always the same? # \x05 - Connecting to player? # \x00 - Unknown # (2 bytes) - Unknown. Port? Same as before. # (4 bytes) - Player's IP # (4 bytes) - Unknown. Some other IP? Remote server IP? # # 0x0a - Response to 0x01 # Gets sent after receiving 0x01 from the client. So, server 0x01 -> client 0x01 -> server 0x0a. # Has no other data besides the client ID. # # - \xfd\xfc commands get passed directly between the other player(s)? # # # Open source version of GameSpy found here: https://github.com/sfcspanky/Openspy-Core/tree/master/qr # Use as reference. session_id = struct.unpack("<I", recv_data[1:5])[0] session_id_raw = recv_data[1:5] if session_id not in self.sessions: # Found a new session, add to session list self.sessions[session_id] = self.Session(address) # Handle commands if recv_data[0] == '\x00': # Query self.log(logging.DEBUG, address, "NOT IMPLEMENTED! Received query from %s:%s... %s" % (address[0], address[1], recv_data[5:])) elif recv_data[0] == '\x01': # Challenge self.log(logging.DEBUG, address, "Received challenge from %s:%s... %s" % (address[0], address[1], recv_data[5:])) # Prepare the challenge sent from the server to be compared challenge = gs_utils.prepare_rc4_base64(self.sessions[session_id].secretkey, self.sessions[session_id].challenge) # Compare challenge client_challenge = recv_data[5:-1] if client_challenge == challenge: # Challenge succeeded self.sessions[session_id].sent_challenge = True # Handle successful challenge stuff here packet = bytearray([0xfe, 0xfd, 0x0a]) # Send client registered command packet.extend(session_id_raw) # Get the session ID self.socket.sendto(packet, address) self.log(logging.DEBUG, address, "Sent client registered to %s:%s..." % (address[0], address[1])) elif recv_data[0] == '\x02': # Echo self.log(logging.DEBUG, address, "NOT IMPLEMENTED! Received echo from %s:%s... %s" % (address[0], address[1], recv_data[5:])) elif recv_data[0] == '\x03': # Heartbeat data = recv_data[5:] self.log(logging.DEBUG, address, "Received heartbeat from %s:%s... %s" % (address[0], address[1], data)) # Parse information from heartbeat here d = data.rstrip('\0').split('\0') # It may be safe to ignore "unknown" keys because the proper key names get filled in later... k = {} for i in range(0, len(d), 2): self.log(logging.DEBUG, address, "%s = %s" % (d[i], d[i+1])) k[d[i]] = d[i+1] if "gamename" in k: self.sessions[session_id].secretkey = self.secret_key_list[k['gamename']] #print "Got secret key %s for %s" % (self.sessions[session_id].secretkey, k['gamename']) if self.sessions[session_id].playerid == 0 and "dwc_pid" in k: # Get the player's id and then query the profile to figure out what console they are on. # The endianness of some server data depends on the endianness of the console, so we must be able # to account for that. self.sessions[session_id].playerid = int(k['dwc_pid']) profile = self.db.get_profile_from_profileid(self.sessions[session_id].playerid) if "console" in profile: self.sessions[session_id].console = profile['console'] if self.sessions[session_id].sent_challenge == False: addr_hex = ''.join(["%02X" % int(x) for x in address[0].split('.')]) port_hex = "%04X" % int(address[1]) server_challenge = utils.generate_random_str(8) + addr_hex + port_hex self.sessions[session_id].challenge = server_challenge packet = bytearray([0xfe, 0xfd, 0x01]) # Send challenge command packet.extend(session_id_raw) # Get the session ID packet.extend(server_challenge) packet.extend('\x00') self.socket.sendto(packet, address) self.log(logging.DEBUG, address, "Sent challenge to %s:%s..." % (address[0], address[1])) if "statechanged" in k: if k['statechanged'] == "1": # Create server if k['publicport'] != "0" and k['publicip'] != "0" and k['maxplayers'] != "0": # dwc_mtype controls what kind of server query we're looking for. # dwc_mtype = 0 is used when looking for a matchmaking game. # dwc_mtype = 1 is unknown. # dwc_mtype = 2 is used when hosting a friends only game (possibly other uses too). # dwc_mtype = 3 is used when looking for a friends only game (possibly other uses too). # Some memory could be saved by clearing out any unwanted fields from k before sending. self.server_manager.update_server_list(k['gamename'] , session_id, k, self.sessions[session_id].console) elif k['statechanged'] == "2": # Close server self.server_manager.delete_server(k['gamename'] , session_id) #self.sessions.pop(session_id) elif recv_data[0] == '\x04': # Add Error self.log(logging.WARNING, address, "NOT IMPLEMENTED! Received add error from %s:%s... %s" % (address[0], address[1], recv_data[5:])) elif recv_data[0] == '\x05': # Echo Response self.log(logging.WARNING, address, "NOT IMPLEMENTED! Received echo response from %s:%s... %s" % (address[0], address[1], recv_data[5:])) elif recv_data[0] == '\x06': # Client Message self.log(logging.WARNING, address, "NOT IMPLEMENTED! Received echo from %s:%s... %s" % (address[0], address[1], recv_data[5:])) elif recv_data[0] == '\x07': # Client Message Ack self.log(logging.WARNING, address, "NOT IMPLEMENTED! Received client message ack from %s:%s... %s" % (address[0], address[1], recv_data[5:])) elif recv_data[0] == '\x08': # Keep Alive self.log(logging.DEBUG, address, "Received keep alive from %s:%s..." % (address[0], address[1])) elif recv_data[0] == '\x09': # Available # Availability check only sent to *.available.gs.nintendowifi.net self.log(logging.DEBUG, address, "Received availability request for '%s' from %s:%s..." % (recv_data[5: -1], address[0], address[1])) self.socket.sendto(bytearray([0xfe, 0xfd, 0x09, 0x00, 0x00, 0x00, 0x00]), address) elif recv_data[0] == '\x0a': # Client Registered # Only sent to client, never received? self.log(logging.WARNING, address, "NOT IMPLEMENTED! Received client registered from %s:%s... %s" % (address[0], address[1], recv_data[5:])) else: self.log(logging.ERROR, address, "Unknown request from %s:%s:" % (address[0], address[1])) self.log(logging.DEBUG, address, utils.pretty_print_hex(recv_data))
def start(self): # Start natneg server address = ( '0.0.0.0', 27901 ) # accessible to outside connections (use this if you don't know what you're doing) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(address) logger.log( logging.INFO, "Server is now listening on %s:%s..." % (address[0], address[1])) while 1: recv_data, addr = s.recvfrom(2048) logger.log(logging.DEBUG, "Connection from %s:%d..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data)) # Make sure it's a legal packet if recv_data[0:6] != bytearray( [0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2]): continue session_id = struct.unpack("<I", recv_data[8:12])[0] session_id_raw = recv_data[8:12] # Handle commands if recv_data[7] == '\x00': logger.log( logging.DEBUG, "Received initialization from %s:%s..." % (addr[0], addr[1])) output = bytearray(recv_data[0:14]) output += bytearray( [0xff, 0xff, 0x6d, 0x16, 0xb5, 0x7d, 0xea] ) # Checked with Tetris DS, Mario Kart DS, and Metroid Prime Hunters, and this seems to be the standard response to 0x00 output[7] = 0x01 # Initialization response s.sendto(output, addr) # Try to connect to the server gameid = utils.get_string(recv_data, 0x16) client_id = "%02x" % ord(recv_data[13]) localip_raw = recv_data[15:19] localip = '.'.join(["%d" % ord(x) for x in localip_raw]) localport_raw = recv_data[19:21] localport = utils.get_short_be(localport_raw, 0) localaddr = (localip, localport) if gameid not in self.session_list: self.session_list[gameid] = {} if session_id not in self.session_list[gameid]: self.session_list[gameid][session_id] = {} if client_id not in self.session_list[gameid][session_id]: self.session_list[gameid][session_id][client_id] = { 'connected': False, 'addr': '', 'localaddr': None, 'serveraddr': None, 'gameid': None } self.session_list[gameid][session_id][client_id][ 'gameid'] = utils.get_string(recv_data[21:], 0) self.session_list[gameid][session_id][client_id]['addr'] = addr self.session_list[gameid][session_id][client_id][ 'localaddr'] = localaddr clients = len(self.session_list[gameid][session_id]) for client in self.session_list[gameid][session_id]: if self.session_list[gameid][session_id][client][ 'connected'] == False and self.session_list[gameid][ session_id][client]['localaddr'][1] != 0: if client == client_id: continue if self.session_list[gameid][session_id][client][ 'serveraddr'] == None: serveraddr = self.get_server_info( gameid, session_id, client) self.session_list[gameid][session_id][client][ 'serveraddr'] = serveraddr logger.log( logging.DEBUG, "Found server from local ip/port: %s" % serveraddr) publicport = 0 if self.session_list[gameid][session_id][client][ 'serveraddr'] != None: publicport = int( self.session_list[gameid][session_id][client] ['serveraddr']['publicport']) # Send to requesting client output = bytearray(recv_data[0:12]) output += bytearray([ int(x) for x in self.session_list[gameid] [session_id][client]['addr'][0].split('.') ]) output += utils.get_bytes_from_short_be(publicport) output += bytearray( [0x42, 0x00]) # Unknown, always seems to be \x42\x00 output[7] = 0x05 #s.sendto(output, (self.session_list[gameid][session_id][client_id]['addr'])) s.sendto( output, (self.session_list[gameid][session_id][client_id] ['addr'][0], self.session_list[gameid][session_id] [client_id]['addr'][1])) logger.log( logging.DEBUG, "Sent connection request to %s:%d..." % (self.session_list[gameid][session_id][client_id] ['addr'][0], self.session_list[gameid][session_id] [client_id]['addr'][1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) # Send to other client if self.session_list[gameid][session_id][client_id][ 'serveraddr'] == None: serveraddr = self.get_server_info( gameid, session_id, client_id) self.session_list[gameid][session_id][client_id][ 'serveraddr'] = serveraddr logger.log( logging.DEBUG, "Found server 2 from local ip/port: %s" % serveraddr) publicport = 0 if self.session_list[gameid][session_id][client_id][ 'serveraddr'] != None: publicport = int( self.session_list[gameid][session_id] [client_id]['serveraddr']['publicport']) output = bytearray(recv_data[0:12]) output += bytearray([ int(x) for x in self.session_list[gameid] [session_id][client_id]['addr'][0].split('.') ]) output += utils.get_bytes_from_short_be(publicport) output += bytearray( [0x42, 0x00]) # Unknown, always seems to be \x42\x00 output[7] = 0x05 #s.sendto(output, (self.session_list[gameid][session_id][client]['addr'])) s.sendto(output, (self.session_list[gameid][session_id][client] ['addr'][0], self.session_list[gameid] [session_id][client]['addr'][1])) logger.log( logging.DEBUG, "Sent connection request to %s:%d..." % (self.session_list[gameid][session_id][client] ['addr'][0], self.session_list[gameid][session_id] [client]['addr'][1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) elif recv_data[7] == '\x06': # Was able to connect client_id = "%02x" % ord(recv_data[13]) logger.log( logging.DEBUG, "Received connected command from %s:%s..." % (addr[0], addr[1])) if gameid not in self.session_list: pass if session_id not in self.session_list[gameid]: pass if client_id not in self.session_list[gameid][session_id]: pass self.session_list[gameid][session_id][client_id][ 'connected'] = True elif recv_data[7] == '\x0a': # Address check. Note: UNTESTED! client_id = "%02x" % ord(recv_data[13]) logger.log( logging.DEBUG, "Received address check command from %s:%s..." % (addr[0], addr[1])) output = bytearray(recv_data[0:15]) output += bytearray([int(x) for x in addr[0].split('.')]) output += utils.get_bytes_from_short_be(addr[1]) output += bytearray(recv_data[len(output):]) output[7] = 0x0b s.sendto(output, addr) logger.log( logging.DEBUG, "Sent address check response to %s:%d..." % (self.session_list[gameid][session_id][client]['addr'][0], self.session_list[gameid][session_id][client]['addr'][1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) elif recv_data[7] == '\x0d': client_id = "%02x" % ord(recv_data[13]) logger.log( logging.DEBUG, "Received report command from %s:%s..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data)) output = bytearray(recv_data) output[7] = 0x0e # Report response s.sendto(recv_data, addr) else: # Was able to connect logger.log( logging.DEBUG, "Received unknown command %02x from %s:%s..." % (ord(recv_data[7]), addr[0], addr[1]))
def forward_data_to_client(self, data, forward_client): # Find session id of server # Iterate through the list of servers sent to the client and match by # IP and port. Is there a better way to determine this information? if forward_client is None or len(forward_client) != 2: return server, ip = self.find_server_in_cache(forward_client[0], forward_client[1], self.console) if server is None: if self.console == 0: server, ip = self.find_server_in_cache(forward_client[0], forward_client[1], 1) # Try Wii elif self.console == 1: server, ip = self.find_server_in_cache(forward_client[0], forward_client[1], 0) # Try DS self.log(logging.DEBUG, "find_server_in_cache returned: %s", server) self.log(logging.DEBUG, "Trying to send message to %s:%d...", forward_client[0], forward_client[1]) self.log(logging.DEBUG, "%s", utils.pretty_print_hex(bytearray(data))) if server is None: return self.log(logging.DEBUG, "%s %s", ip, server['publicip']) if server['publicip'] == ip and \ server['publicport'] == str(forward_client[1]): if forward_client[1] == 0 and 'localport' in server: # No public port returned from client, try contacting on # the local port. forward_client = (forward_client[0], int(server['localport'])) # Send command to server to get it to connect to natneg # Quick and lazy way to get a random 32bit integer. Replace with # something else later cookie = int(utils.generate_random_hex_str(8), 16) # if (len(data) == 24 and bytearray(data)[0:10] == \ # bytearray([0x53, 0x42, 0x43, 0x4d, 0x03, # 0x00, 0x00, 0x00, 0x01, 0x04])) or \ # (len(data) == 40 and bytearray(data)[0:10] == \ # bytearray([0x53, 0x42, 0x43, 0x4d, # 0x0b, 0x00, 0x00, 0x00, # 0x01, 0x04])): if self.own_server is None and len(data) >= 16 and \ bytearray(data)[0:4] in (bytearray([0xbb, 0x49, 0xcc, 0x4d]), bytearray([0x53, 0x42, 0x43, 0x4d])): # Is the endianness the same between the DS and Wii here? # It seems so but I'm not positive. # Note to self: Port is little endian here. self_port = utils.get_short(bytearray(data[10:12]), 0, False) self_ip = '.'.join(["%d" % x for x in bytearray(data[12:16])]) self.own_server, _ = self.find_server_in_cache( self_ip, self_port, self.console) if self.own_server is None: if self.console == 0: # Try Wii self.own_server, _ = self.find_server_in_cache( self_ip, self_port, 1) elif self.console == 1: # Try DS self.own_server, _ = self.find_server_in_cache( self_ip, self_port, 0) if self.own_server is None: self.log(logging.DEBUG, "Could not find own server: %s:%d", self_ip, self_port) else: self.log(logging.DEBUG, "Found own server: %s", self.own_server) elif len(data) == 10 and \ bytearray(data)[0:6] == \ bytearray([0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2]): natneg_session = utils.get_int_signed(data, 6) self.log(logging.DEBUG, "Adding %d to natneg server list: %s", natneg_session, server) # Store info in backend so we can get it later in natneg self.server_manager.add_natneg_server(natneg_session, server) if self.own_server is not None: self.log(logging.DEBUG, "Adding %d to natneg server list: %s (self)", natneg_session, self.own_server) # Store info in backend so we can get it later in natneg self.server_manager.add_natneg_server( natneg_session, self.own_server) # if self.qr is not None: # own_server = self.qr.get_own_server() # # self.log(logging.DEBUG, # "Adding %d to natneg server list: %s", # natneg_session, own_server) # self.server_manager.add_natneg_server(natneg_session, # own_server) output = bytearray([0xfe, 0xfd, 0x06]) output += utils.get_bytes_from_int(server['__session__']) output += bytearray(utils.get_bytes_from_int(cookie)) output += bytearray(data) if self.qr is not None: self.log(logging.DEBUG, "Forwarded data to %s:%s through QR server...", forward_client[0], forward_client[1]) self.qr.socket.sendto(output, forward_client) else: # In case we can't contact the QR server, just try sending # the packet directly. This isn't standard behavior but it # can work in some instances. self.log( logging.DEBUG, "Forwarded data to %s:%s directly" " (potential error occurred)...", forward_client[0], forward_client[1]) client_s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) client_s.sendto(output, forward_client)
def handle_packet(self, s, recv_data, addr): logger.log(logging.DEBUG, "Connection from %s:%d..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data)) time.sleep(0.05) # Make sure it's a legal packet if recv_data[0:6] != bytearray([0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2]): return session_id = struct.unpack("<I", recv_data[8:12])[0] session_id_raw = recv_data[8:12] # Handle commands if recv_data[7] == '\x00': logger.log( logging.DEBUG, "Received initialization from %s:%s..." % (addr[0], addr[1])) output = bytearray(recv_data[0:14]) output += bytearray( [0xff, 0xff, 0x6d, 0x16, 0xb5, 0x7d, 0xea] ) # Checked with Tetris DS, Mario Kart DS, and Metroid Prime Hunters, and this seems to be the standard response to 0x00 output[7] = 0x01 # Initialization response s.sendto(output, addr) # Try to connect to the server gameid = utils.get_string(recv_data, 0x15) client_id = "%02x" % ord(recv_data[13]) localip_raw = recv_data[15:19] localip_int_le = utils.get_int(recv_data, 15) localip_int_be = utils.get_int_be(recv_data, 15) localip = '.'.join(["%d" % ord(x) for x in localip_raw]) localport_raw = recv_data[19:21] localport = utils.get_short_be(localport_raw, 0) localaddr = (localip, localport, localip_int_le, localip_int_be) if session_id not in self.session_list: self.session_list[session_id] = {} if client_id not in self.session_list[session_id]: self.session_list[session_id][client_id] = { 'connected': False, 'addr': '', 'localaddr': None, 'serveraddr': None, 'gameid': None } self.session_list[session_id][client_id]['gameid'] = gameid self.session_list[session_id][client_id]['addr'] = addr self.session_list[session_id][client_id]['localaddr'] = localaddr clients = len(self.session_list[session_id]) for client in self.session_list[session_id]: if self.session_list[session_id][client][ 'connected'] == False: # and self.session_list[session_id][client]['localaddr'][1] != 0: if client == client_id: continue #if self.session_list[session_id][client]['serveraddr'] == None: serveraddr = self.get_server_info(gameid, session_id, client) if serveraddr == None: serveraddr = self.get_server_info_alt( gameid, session_id, client) self.session_list[session_id][client][ 'serveraddr'] = serveraddr logger.log( logging.DEBUG, "Found server from local ip/port: %s from %d" % (serveraddr, session_id)) publicport = self.session_list[session_id][client]['addr'][ 1] if self.session_list[session_id][client]['localaddr'][ 1] != 0: publicport = self.session_list[session_id][client][ 'localaddr'][1] if self.session_list[session_id][client][ 'serveraddr'] != None: publicport = int(self.session_list[session_id][client] ['serveraddr']['publicport']) # Send to requesting client output = bytearray(recv_data[0:12]) output += bytearray([ int(x) for x in self.session_list[session_id][client] ['addr'][0].split('.') ]) output += utils.get_bytes_from_short_be(publicport) output += bytearray( [0x42, 0x00]) # Unknown, always seems to be \x42\x00 output[7] = 0x05 #s.sendto(output, (self.session_list[session_id][client_id]['addr'])) s.sendto( output, (self.session_list[session_id][client_id]['addr'][0], self.session_list[session_id][client_id]['addr'][1])) logger.log( logging.DEBUG, "Sent connection request to %s:%d..." % (self.session_list[session_id][client_id]['addr'][0], self.session_list[session_id][client_id]['addr'][1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) # Send to other client #if self.session_list[session_id][client_id]['serveraddr'] == None: serveraddr = self.get_server_info(gameid, session_id, client_id) if serveraddr == None: serveraddr = self.get_server_info_alt( gameid, session_id, client) self.session_list[session_id][client_id][ 'serveraddr'] = serveraddr logger.log( logging.DEBUG, "Found server 2 from local ip/port: %s from %d" % (serveraddr, session_id)) publicport = self.session_list[session_id][client_id][ 'addr'][1] if self.session_list[session_id][client_id]['localaddr'][ 1] != 0: publicport = self.session_list[session_id][client_id][ 'localaddr'][1] if self.session_list[session_id][client_id][ 'serveraddr'] != None: publicport = int( self.session_list[session_id][client_id] ['serveraddr']['publicport']) output = bytearray(recv_data[0:12]) output += bytearray([ int(x) for x in self.session_list[session_id] [client_id]['addr'][0].split('.') ]) output += utils.get_bytes_from_short_be(publicport) output += bytearray( [0x42, 0x00]) # Unknown, always seems to be \x42\x00 output[7] = 0x05 #s.sendto(output, (self.session_list[session_id][client]['addr'])) s.sendto( output, (self.session_list[session_id][client]['addr'][0], self.session_list[session_id][client]['addr'][1])) logger.log( logging.DEBUG, "Sent connection request to %s:%d..." % (self.session_list[session_id][client]['addr'][0], self.session_list[session_id][client]['addr'][1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) elif recv_data[7] == '\x06': # Was able to connect client_id = "%02x" % ord(recv_data[13]) logger.log( logging.DEBUG, "Received connected command from %s:%s..." % (addr[0], addr[1])) if session_id not in self.session_list: return if client_id not in self.session_list[session_id]: return self.session_list[session_id][client_id]['connected'] = True elif recv_data[7] == '\x0a': # Address check. Note: UNTESTED! client_id = "%02x" % ord(recv_data[13]) logger.log( logging.DEBUG, "Received address check command from %s:%s..." % (addr[0], addr[1])) output = bytearray(recv_data[0:15]) output += bytearray([int(x) for x in addr[0].split('.')]) output += utils.get_bytes_from_short_be(addr[1]) output += bytearray(recv_data[len(output):]) output[7] = 0x0b s.sendto(output, addr) logger.log( logging.DEBUG, "Sent address check response to %s:%d..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) elif recv_data[7] == '\x0c': # Natify port_type = "%02x" % ord(recv_data[12]) logger.log( logging.DEBUG, "Received natify command from %s:%s..." % (addr[0], addr[1])) output = bytearray(recv_data) output[7] = 0x02 # ERT Test s.sendto(output, addr) logger.log(logging.DEBUG, "Sent natify response to %s:%d..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(output)) elif recv_data[7] == '\x0d': client_id = "%02x" % ord(recv_data[13]) logger.log( logging.DEBUG, "Received report command from %s:%s..." % (addr[0], addr[1])) logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data)) output = bytearray(recv_data) output[7] = 0x0e # Report response s.sendto(recv_data, addr) else: # Was able to connect logger.log( logging.DEBUG, "Received unknown command %02x from %s:%s..." % (ord(recv_data[7]), addr[0], addr[1]))