Skip to content

Instantly share code, notes, and snippets.

@flarn2006
Last active June 15, 2021 05:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save flarn2006/2611b84101cdc48f954528479d7ddda0 to your computer and use it in GitHub Desktop.
Save flarn2006/2611b84101cdc48f954528479d7ddda0 to your computer and use it in GitHub Desktop.
Wireshark dissector for Sonic Robo Blast 2 netplay
local srb2_proto = Proto('SRB2', 'Sonic Robo Blast 2')
srb2_proto.prefs.validate_checksum = Pref.bool('Validate checksums', false, 'Whether to validate the packet checksum')
local function add_fields(...)
for i,v in ipairs({...}) do
table.insert(srb2_proto.fields, v)
end
end
-- Version 2.2.9
local packet_types = {
[0] = 'PT_NOTHING',
[1] = 'PT_SERVERCFG',
[2] = 'PT_CLIENTCMD',
[3] = 'PT_CLIENTMIS',
[4] = 'PT_CLIENT2CMD',
[5] = 'PT_CLIENT2MIS',
[6] = 'PT_NODEKEEPALIVE',
[7] = 'PT_NODEKEEPALIVEMIS',
[8] = 'PT_SERVERTICS',
[9] = 'PT_SERVERREFUSE',
[10] = 'PT_SERVERSHUTDOWN',
[11] = 'PT_CLIENTQUIT',
[12] = 'PT_ASKINFO',
[13] = 'PT_SERVERINFO',
[14] = 'PT_PLAYERINFO',
[15] = 'PT_REQUESTFILE',
[16] = 'PT_ASKINFOVIAMS (obsolete)',
[17] = 'PT_WILLRESENDGAMESTATE',
[18] = 'PT_CANRECEIVEGAMESTATE',
[19] = 'PT_RECEIVEDGAMESTATE',
[20] = 'PT_SENDINGLUAFILE',
[21] = 'PT_ASKLUAFILE',
[22] = 'PT_HASLUAFILE',
[23] = 'PT_FILEFRAGMENT',
[24] = 'PT_FILEACK',
[25] = 'PT_FILERECEIVED',
[26] = 'PT_TEXTCMD',
[27] = 'PT_TEXTCMD2',
[28] = 'PT_CLIENTJOIN',
[29] = 'PT_NODETIMEOUT',
[30] = 'PT_LOGIN',
[31] = 'PT_PING'
}
local packet_dissectors = {}
local function NetbufferChecksum(buf)
local c = 0x1234567
for i=4,buf:len()-1 do
c = c + buf(i,1):uint() * (i-3)
end
return c
end
local f_checksum = ProtoField.uint32('srb2.checksum', 'Checksum', base.HEX)
local f_valid = ProtoField.bool('srb2.valid', 'Checksum Status')
local f_ack = ProtoField.uint8('srb2.ack', 'ACK')
local f_ackret = ProtoField.uint8('srb2.ackret', 'ACK Return')
local f_ptype = ProtoField.uint8('srb2.ptype', 'Packet Type', base.DEC, packet_types)
local f_reserved = ProtoField.uint8('srb2.reserved', 'Reserved')
local f_datalen = ProtoField.uint32('srb2.datalen', 'Data Length')
local f_data = ProtoField.bytes('srb2.data', 'Data', base.NONE)
add_fields(f_checksum, f_valid, f_ack, f_ackret, f_ptype, f_reserved, f_datalen, f_data)
-- PT_SERVERCFG
local game_states = {
[0] = 'GS_NULL',
[1] = 'GS_LEVEL',
[2] = 'GS_INTERMISSION',
[3] = 'GS_CONTINUING',
[4] = 'GS_TITLESCREEN',
[5] = 'GS_TIMEATTACK',
[6] = 'GS_CREDITS',
[7] = 'GS_EVALUATION',
[8] = 'GS_GAMEEND',
[9] = 'GS_INTRO',
[10] = 'GS_ENDING',
[11] = 'GS_CUTSCENE',
[12] = 'GS_DEDICATEDSERVER',
[13] = 'GS_WAITINGPLAYERS'
}
local game_types = {
[0] = 'GT_COOP',
[1] = 'GT_COMPETITION',
[2] = 'GT_RACE',
[3] = 'GT_MATCH',
[4] = 'GT_TEAMMATCH',
[5] = 'GT_TAG',
[6] = 'GT_HIDEANDSEEK',
[7] = 'GT_CTF'
}
-- PT_SERVERCFG
local f_version = ProtoField.uint8('srb2.data.version', 'Version')
local f_subversion = ProtoField.uint8('srb2.data.subversion', 'Build')
local f_serverplayer = ProtoField.uint8('srb2.data.serverplayer', 'Server Player')
local f_totalslotnum = ProtoField.uint8('srb2.data.totalslotnum', 'Total Slot Num')
local f_gametic = ProtoField.uint32('srb2.data.gametic', 'Game Tic')
local f_clientnode = ProtoField.uint8('srb2.data.clientnode', 'Client Node')
local f_gamestate = ProtoField.uint8('srb2.data.gamestate', 'Game State', base.DEC, game_states)
local f_gametype = ProtoField.uint8('srb2.data.gametype', 'Game Type', base.DEC, game_types)
local f_modifiedgame = ProtoField.uint8('srb2.data.modifiedgame', 'Modified Game')
local f_server_context = ProtoField.string('srb2.data.server_context', 'Server Context')
add_fields(f_version, f_subversion, f_serverplayer, f_totalslotnum, f_gametic, f_clientnode)
add_fields(f_gamestate, f_gametype, f_modifiedgame, f_server_context)
-- PT_SERVERREFUSE
local f_refuse_reason = ProtoField.string('srb2.data.refuse_reason', 'Refusal Reason')
add_fields(f_refuse_reason)
-- PT_REQUESTFILE
local f_fileid = ProtoField.uint8('srb2.data.fileid', 'File ID')
local f_filename = ProtoField.string('srb2.data.filename', 'Filename')
local f__255 = ProtoField.uint8('srb2.data._255', '_255')
add_fields(f_fileid, f_filename, f__255)
-- PT_FILEFRAGMENT
-- f_fileid
local f_filesize = ProtoField.uint32('srb2.data.filesize', 'File Size')
local f_iteration = ProtoField.uint8('srb2.data.iteration', 'Iteration')
local f_position = ProtoField.uint32('srb2.data.position', 'Position', base.HEX)
local f_size = ProtoField.uint16('srb2.data.fragmentsize', 'Fragment Size')
local f_fragment = ProtoField.bytes('srb2.data.fragment', 'Fragment', base.NONE)
add_fields(f_filesize, f_iteration, f_position, f_size, f_fragment)
-- PT_FILEACK
-- f_fileid
-- f_iteration
local f_numsegments = ProtoField.uint8('srb2.data.numsegments', '# of Segments')
add_fields(f_numsegments)
-- PT_FILERECEIVED
-- f_fileid
-- PT_CLIENTJOIN
-- f__255
local f_packetversion = ProtoField.uint8('srb2.data.packetversion', 'Packet Version')
local f_application = ProtoField.string('srb2.data.application', 'Application')
-- f_version
-- f_subversion
local f_localplayers = ProtoField.uint8('srb2.data.localplayers', 'Local Players')
local f_mode = ProtoField.uint8('srb2.data.mode', 'Mode')
local f_p1name = ProtoField.string('srb2.data.p1name', 'Player 1 Name')
local f_p2name = ProtoField.string('srb2.data.p2name', 'Player 2 Name')
add_fields(f_packetversion, f_application, f_localplayers, f_mode, f_p1name, f_p2name)
function srb2_proto.dissector(buffer, pinfo, tree)
pinfo.cols.protocol = 'SRB2'
local ptype = buffer(6,1):uint()
local ptype_str = (packet_types[ptype] or 'Unknown')..' ('..ptype..')'
pinfo.cols.info = ptype_str
local cksum_expected = NetbufferChecksum(buffer)
local cksum_given = buffer(0,4):le_uint()
local cksum_valid = (cksum_given == cksum_expected)
local cksum_str = 'Checksum Status: '
if srb2_proto.prefs.validate_checksum then
if cksum_valid then
cksum_str = cksum_str..'OK'
else
cksum_str = cksum_str..string.format('should be 0x%X!', cksum_expected)
end
else
cksum_str = cksum_str..'Unverified'
end
local datalen = buffer:len() - 8
local subtree = tree:add(srb2_proto, buffer(), 'Sonic Robo Blast 2 netgame protocol')
subtree:add_le(f_checksum, buffer(0,4))
subtree:add(f_valid, cksum_valid):set_text(cksum_str):set_generated(true)
subtree:add(f_ack, buffer(4,1))
subtree:add(f_ackret, buffer(5,1))
subtree:add(f_ptype, buffer(6,1))
subtree:add(f_reserved, buffer(7,1))
subtree:add(f_datalen, datalen):set_generated(true)
local data_tree = subtree:add(f_data, buffer(8))
local stage2 = packet_dissectors[packet_types[ptype]]
if stage2 then
stage2(buffer(8), datalen, pinfo, data_tree)
end
end
local MAXAPPLICATION = 16
local MAXPLAYERNAME = 21
function packet_dissectors.PT_SERVERCFG(range, datalen, pinfo, tree)
tree:add(f_version, range(0,1))
tree:add(f_subversion, range(1,1))
tree:add(f_serverplayer, range(2,1))
tree:add(f_totalslotnum, range(3,1))
tree:add_le(f_gametic, range(4,4))
tree:add(f_clientnode, range(8,1))
tree:add(f_gamestate, range(9,1))
tree:add(f_gametype, range(10,1))
tree:add(f_modifiedgame, range(11,1))
tree:add(f_server_context, range(12,8))
end
function packet_dissectors.PT_SERVERREFUSE(range, datalen, pinfo, tree)
tree:add(f_refuse_reason, range)
end
function packet_dissectors.PT_REQUESTFILE(range, datalen, pinfo, tree)
tree:add(f_fileid, range(0,1))
tree:add(f_filename, range(1, datalen-2))
tree:add(f__255, range(datalen-1, 1))
end
function packet_dissectors.PT_FILEFRAGMENT(range, datalen, pinfo, tree)
tree:add(f_fileid, range(0,1))
tree:add_le(f_filesize, range(1,4))
tree:add(f_iteration, range(5,1))
tree:add_le(f_position, range(6,4))
tree:add_le(f_size, range(10,2))
tree:add(f_fragment, range(12))
end
function packet_dissectors.PT_FILEACK(range, datalen, pinfo, tree)
tree:add(f_fileid, range(0,1))
tree:add(f_iteration, range(1,1))
tree:add(f_numsegments, range(2,1))
local numsegments = (datalen - 3) / 8
for i=0,numsegments-1 do
local segstart = 3 + 8*i
local start = range(segstart, 4):le_uint()
local acks = range(segstart+4, 4):le_uint()
local str = string.format('Segment %u: start=0x%X, acks=', i, start)
for j=1,32 do
if acks % 2 == 1 then
str = str..'1'
else
str = str..'0'
end
acks = math.floor(acks / 2)
end
tree:add(range(segstart, 8), str)
end
end
function packet_dissectors.PT_FILERECEIVED(range, datalen, pinfo, tree)
tree:add(f_fileid, range(0,1))
end
function packet_dissectors.PT_CLIENTJOIN(range, datalen, pinfo, tree)
tree:add(f__255, range(0,1))
tree:add(f_packetversion, range(1,1))
tree:add(f_application, range(2, MAXAPPLICATION))
tree:add(f_version, range(2+MAXAPPLICATION, 1))
tree:add(f_subversion, range(3+MAXAPPLICATION, 1))
tree:add(f_localplayers, range(4+MAXAPPLICATION, 1))
tree:add(f_mode, range(5+MAXAPPLICATION, 1))
tree:add(f_p1name, range(6+MAXAPPLICATION, MAXPLAYERNAME))
tree:add(f_p2name, range(6+MAXAPPLICATION+MAXPLAYERNAME, MAXPLAYERNAME))
end
DissectorTable.get('udp.port'):add(5029, srb2_proto)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment