mirror of
https://github.com/barronwaffles/dwc_network_server_emulator.git
synced 2026-03-21 17:34:21 -05:00
818 lines
28 KiB
Python
818 lines
28 KiB
Python
"""DWC Network Server Emulator
|
|
|
|
Copyright (C) 2014 polaris-
|
|
Copyright (C) 2014 ToadKing
|
|
Copyright (C) 2016 Sepalani
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
Server emulator for *.available.gs.nintendowifi.net
|
|
and *.master.gs.nintendowifi.net
|
|
Query and Reporting:
|
|
http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Overview
|
|
http://wiki.tockdom.com/wiki/Server_NATNEG
|
|
"""
|
|
|
|
import logging
|
|
import SocketServer
|
|
import threading
|
|
import time
|
|
import Queue
|
|
import gamespy.gs_utility as gs_utils
|
|
import other.utils as utils
|
|
import traceback
|
|
|
|
from multiprocessing.managers import BaseManager
|
|
import dwc_config
|
|
|
|
logger = dwc_config.get_logger('GameSpyNatNegServer')
|
|
|
|
|
|
class GameSpyServerDatabase(BaseManager):
|
|
pass
|
|
|
|
GameSpyServerDatabase.register("get_server_list")
|
|
GameSpyServerDatabase.register("modify_server_list")
|
|
GameSpyServerDatabase.register("find_servers")
|
|
GameSpyServerDatabase.register("find_server_by_address")
|
|
GameSpyServerDatabase.register("find_server_by_local_address")
|
|
GameSpyServerDatabase.register("add_natneg_server")
|
|
GameSpyServerDatabase.register("get_natneg_server")
|
|
GameSpyServerDatabase.register("delete_natneg_server")
|
|
|
|
|
|
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))
|
|
|
|
|
|
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_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_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_ertack(nn, recv_data, addr, socket):
|
|
"""Command: 0x03 - NN_ERTACK.
|
|
|
|
Reply by the client for record NN_ERTTEST (0x02).
|
|
Only the record type is changed.
|
|
|
|
Example:
|
|
fd fc 1e 66 6a b2 03 03 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
|
|
03 - 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.INFO, "Received ERT acknowledgement from %s:%d", *addr)
|
|
|
|
|
|
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_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_ack(nn, recv_data, addr, socket):
|
|
"""Command: 0x06 - NN_CONNECT_ACK.
|
|
|
|
Reply by the client for record NN_CONNECT (0x05).
|
|
|
|
Example:
|
|
fd fc 1e 66 6a b2 03 06 3d f1 00 71 90 00 cd a0
|
|
80 00 00 00 90
|
|
|
|
Description:
|
|
fd fc 1e 66 6a b2 - NATNEG magic
|
|
03 - NATNEG version
|
|
06 - NATNEG record type
|
|
3d f1 00 71 - Session id
|
|
90 - Port type (0x00, 0x80 or 0x90)
|
|
00 - Client index (0x00 - Client,
|
|
0x01 - Host)
|
|
cd - Use game port?
|
|
a0 80 00 00 - Local IP?
|
|
00 90 - Local port?
|
|
"""
|
|
client_id = "%02x" % ord(recv_data[13])
|
|
session_id = utils.get_int(recv_data, 8)
|
|
logger.log(logging.DEBUG,
|
|
"Received connected command from %s:%d...",
|
|
*addr)
|
|
|
|
if session_id in nn.session_list and \
|
|
client_id in nn.session_list[session_id]:
|
|
nn.session_list[session_id][client_id]['connected'] = True
|
|
|
|
|
|
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_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_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_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_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 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_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_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_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))
|
|
|
|
|
|
class GameSpyNatNegUDPServerHandler(SocketServer.BaseRequestHandler):
|
|
"""GameSpy NAT Negotiation handler."""
|
|
|
|
nn_magics = bytearray([0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2])
|
|
nn_commands = {
|
|
'\x00': handle_natneg_init,
|
|
'\x01': handle_natneg_initack,
|
|
'\x02': handle_natneg_erttest,
|
|
'\x03': handle_natneg_ertack,
|
|
'\x04': handle_natneg_stateupdate,
|
|
'\x05': handle_natneg_connect,
|
|
'\x06': handle_natneg_connect_ack,
|
|
'\x07': handle_natneg_connect_ping,
|
|
'\x08': handle_natneg_backup_test,
|
|
'\x09': handle_natneg_backup_ack,
|
|
'\x0A': handle_natneg_address_check,
|
|
'\x0B': handle_natneg_address_reply,
|
|
'\x0C': handle_natneg_natify_request,
|
|
'\x0D': handle_natneg_report,
|
|
'\x0E': handle_natneg_report_ack,
|
|
'\x0F': handle_natneg_preinit,
|
|
'\x10': handle_natneg_preinit_ack
|
|
}
|
|
|
|
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())
|
|
|
|
|
|
class GameSpyNatNegUDPServer(SocketServer.UDPServer):
|
|
"""GameSpy NAT Negotiation server."""
|
|
|
|
def __init__(self,
|
|
server_address=dwc_config.get_ip_port('GameSpyNatNegServer'),
|
|
RequestHandlerClass=GameSpyNatNegUDPServerHandler,
|
|
bind_and_activate=True):
|
|
SocketServer.UDPServer.__init__(self,
|
|
server_address,
|
|
RequestHandlerClass,
|
|
bind_and_activate)
|
|
self.session_list = {}
|
|
self.natneg_preinit_session = {}
|
|
self.secret_key_list = gs_utils.generate_secret_keys("gslist.cfg")
|
|
|
|
self.server_manager = GameSpyServerDatabase(
|
|
address=dwc_config.get_ip_port('GameSpyManager'),
|
|
authkey=""
|
|
)
|
|
self.server_manager.connect()
|
|
|
|
self.write_queue = Queue.Queue()
|
|
threading.Thread(target=self.write_queue_worker).start()
|
|
|
|
def write_queue_send(self, data, address, socket):
|
|
time.sleep(0.05)
|
|
socket.sendto(data, address)
|
|
|
|
def write_queue_worker(self):
|
|
while True:
|
|
data, address, socket = self.write_queue.get()
|
|
threading.Thread(target=self.write_queue_send,
|
|
args=(data, address, socket)).start()
|
|
self.write_queue.task_done()
|
|
|
|
def get_server_info(self, gameid, session_id, client_id):
|
|
"""Get server by public IP."""
|
|
server = None
|
|
ip_str = self.session_list[session_id][client_id]['addr'][0]
|
|
servers = self.server_manager.get_natneg_server(session_id) \
|
|
._getvalue()
|
|
|
|
if servers is None:
|
|
return None
|
|
|
|
for console in [False, True]:
|
|
if server is not None:
|
|
break
|
|
ip = str(utils.get_ip_from_str(ip_str, console))
|
|
server = next((s for s in servers if s['publicip'] == ip), None)
|
|
|
|
return server
|
|
|
|
def get_server_info_alt(self, gameid, session_id, client_id):
|
|
"""Get server by local address."""
|
|
server = None
|
|
ip_str = self.session_list[session_id][client_id]['addr'][0]
|
|
|
|
for console in [False, True]:
|
|
if server is not None:
|
|
break
|
|
ip = str(utils.get_ip_from_str(ip_str, console))
|
|
server = self.server_manager.find_server_by_local_address(
|
|
ip,
|
|
self.session_list[session_id][client_id]['localaddr'],
|
|
self.session_list[session_id][client_id]['gameid']
|
|
)._getvalue()
|
|
|
|
return server
|
|
|
|
def get_server_addr(self, gameid, session_id, client_id):
|
|
"""Get server address."""
|
|
return \
|
|
self.get_server_info(gameid, session_id, client_id) or \
|
|
self.get_server_info_alt(gameid, session_id, client_id)
|
|
|
|
|
|
class GameSpyNatNegServer(object):
|
|
def start(self):
|
|
server = GameSpyNatNegUDPServer()
|
|
logger.log(logging.INFO, "Server is now listening on %s:%d...",
|
|
*server.server_address)
|
|
server.serve_forever()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
natneg_server = GameSpyNatNegServer()
|
|
natneg_server.start()
|