From 478d33d5bf40ef3faee3c641a8259605be0779e6 Mon Sep 17 00:00:00 2001 From: aligungr Date: Fri, 21 May 2021 19:36:23 +0300 Subject: [PATCH 1/5] L3 RRC/NAS developments (Cherrypicked from ueransimv3.2 https://github.com/aligungr/UERANSIM@2dbbcd5#diff-7eeacbbbe27a313eb85e772d2a22294bb66b38048baf2b9e8a5cb57ba36e8aa1) --- rls.lua | 208 +++++++++++++++++++++++++------------------------------- 1 file changed, 92 insertions(+), 116 deletions(-) diff --git a/rls.lua b/rls.lua index 9b1109d..2f2f856 100644 --- a/rls.lua +++ b/rls.lua @@ -1,138 +1,114 @@ --[[ +-- -- Dissector for Radio Link Simulation Protocol --- (used by UERANSIM ). +-- (UERANSIM project ). -- -- CC0-1.0 2021 - Louis Royer () +-- --]] ---[[ --- ProtoFields ---]] -local rls_protocol = Proto("RLS", "Radio Link Simulation Protocol") -local version_major = ProtoField.uint8("rls.version_major", "RLS Version Major", base.DEC) -local version_minor = ProtoField.uint8("rls.version_minor", "RLS Version Minor", base.DEC) -local version_patch = ProtoField.uint8("rls.version_patch", "RLS Version Patch", base.DEC) +local rlsProtocol = Proto("RLS", "UERANSIM Radio Link Simulation (RLS) Protocol") +local fields = rlsProtocol.fields -local message_type_name = { - [0] = "Reserved", - [1] = "Cell Info Request", - [2] = "Cell Info Response", - [3] = "PDU Delivery" +local msgTypeNames = { + [0] = "[Reserved]", + [1] = "[Reserved]", + [2] = "[Reserved]", + [3] = "[Reserved]", + [4] = "Heartbeat", + [5] = "Heartbeat ACK", + [6] = "PDU Transmission", + [7] = "PDU Transmission ACK", } -local message_type = ProtoField.uint8("rls.message_type", "RLS Message Type", base.DEC, message_type_name) -local sti = ProtoField.uint64("rls.sti", "RLS Temporary Identifier", base.HEX) - --- For cell Info Request -local sim_pos_x = ProtoField.uint32("rls.sim_pos_x", "RLS Position X", base.DEC) -local sim_pos_y = ProtoField.uint32("rls.sim_pos_y", "RLS Position Y", base.DEC) -local sim_pos_z = ProtoField.uint32("rls.sim_pos_z", "RLS Position Z", base.DEC) +local pduTypeNames = { + [0] = "[Reserved]", + [1] = "RRC", + [2] = "Data" +} --- For cell Info Response -local mcc = ProtoField.uint16("rls.mcc", "MCC", base.DEC) -local mnc = ProtoField.uint16("rls.mnc", "MNC", base.DEC) -local long_mnc = ProtoField.bool("rls.long_mnc", "MNC is 3-digit", base.BOOL) -local nci = ProtoField.uint64("rls.nci", "NR Cell Identity", base.HEX) -local tac = ProtoField.uint32("rls.tac", "Tracking Area Code", base.DEC) -local dbm = ProtoField.int32("rls.dbm", "RLS Signal Strength (dBm)", base.DEC) -local gnb_name = ProtoField.string("rls.gnb_name", "gNB Name") -local link_ip = ProtoField.string("rls.link_ip", "gNB Link IP") +local rrcMsgTypeNames = { + [0] = "BCCH-BCH", + [1] = "BCCH-DL-SCH", + [2] = "DL-CCCH", + [3] = "DL-DCCH", + [4] = "PCCH", + [5] = "UL-CCCH", + [6] = "UL-CCCH1", + [7] = "UL-DCCH", +} --- For PDU Delivery -local pdu_type_name = { - [0] = "Reserved", - [1] = "RRC", - [2] = "Data" +local nrRrcDissectors = { + [0] = "nr-rrc.bcch.bch", + [1] = "nr-rrc.bcch.dl.sch", + [2] = "nr-rrc.dl.ccch", + [3] = "nr-rrc.dl.dcch", + [4] = "nr-rrc.pcch", + [5] = "nr-rrc.ul.ccch", + [6] = "nr-rrc.ul.ccch1", + [7] = "nr-rrc.ul.dcch", } -local pdu_type = ProtoField.uint8("rls.pdu_type", "RLS PDU Type", base.DEC, pdu_type_name) +fields.Version = ProtoField.string("rls.version", "Version") +fields.MsgType = ProtoField.uint8("rls.message_type", "Message Type", base.DEC, msgTypeNames) +fields.Sti = ProtoField.uint64("rls.sti", "Sender Node Temporary ID", base.DEC) +fields.PduType = ProtoField.uint8("rls.pdu_type", "PDU Type", base.DEC, pduTypeNames) +fields.PduId = ProtoField.uint32("rls.pdu_id", "PDU ID", base.DEC) +fields.RrcMsgType = ProtoField.uint32("rls.rrc_message_type", "RRC Message Type", base.DEC, rrcMsgTypeNames) +fields.PduLength = ProtoField.uint32("rls.pdu_length", "PDU Length", base.DEC) +fields.PduSessionId = ProtoField.uint32("rls.pdu_session_id", "PDU Session ID", base.DEC) +fields.AcknowledgeItem = ProtoField.uint32("rls.ack_item", "PDU ID") +fields.Dbm = ProtoField.int32("rls.dbm", "RLS Signal Strength (dBm)", base.DEC) +fields.PosX = ProtoField.uint32("rls.pos_x", "RLS Position X", base.DEC) +fields.PosY = ProtoField.uint32("rls.pos_y", "RLS Position Y", base.DEC) +fields.PosZ = ProtoField.uint32("rls.pos_z", "RLS Position Z", base.DEC) -local rrc_channel_name = { - [0] = "BCCH-BCH", - [1] = "BCCH-DL-SCH", - [2] = "DL-CCCH", - [3] = "DL-DCCH", - [4] = "PCCH", - [5] = "UL-CCCH", - [6] = "UL-CCCH1", - [7] = "UL-DCCH", -} +function rlsProtocol.dissector(buffer, pinfo, tree) + if buffer:len() == 0 then return end + if buffer(0, 1):uint() ~= 0x03 then return end -local rrc_channel_dissector = { - [0] = "nr-rrc.bcch.bch", - [1] = "nr-rrc.bcch.dl.sch", - [2] = "nr-rrc.dl.ccch", - [3] = "nr-rrc.dl.dcch", - [4] = "nr-rrc.pcch", - [5] = "nr-rrc.ul.ccch", - [6] = "nr-rrc.ul.ccch1", - [7] = "nr-rrc.ul.dcch", -} + pinfo.cols.protocol = rlsProtocol.name -local rrc_channel = ProtoField.uint32("rls.rrc_channel", "RRC Channel", base.DEC, rrc_channel_name) -local session_id = ProtoField.uint32("rls.session_id", "PDU Session ID", base.DEC) + local versionNumber = buffer(1, 1):uint() .. "." .. buffer(2, 1):uint() .. "." .. buffer(3, 1):uint() + local subtree = tree:add(rlsProtocol, buffer(), "UERANSIM Radio Link Simulation (RLS) protocol") ---[[ --- Dissector definition ---]] -rls_protocol.fields = { - version_major, version_minor, version_patch, message_type, sti, - sim_pos_x, sim_pos_y, sim_pos_z, - mcc, mnc, long_mnc, nci, tac, dbm, gnb_name, link_ip, - pdu_type, rrc_channel, session_id, -} + subtree:add(fields.Version, buffer(1, 3), versionNumber) + subtree:add(fields.MsgType, buffer(4, 1)) + local msgType = buffer(4, 1):uint() -function rls_protocol.dissector(buffer, pinfo, tree) - local length = buffer:len() - if length == 0 then return end - if buffer(0,1):uint() ~= 0x03 then return end + pinfo.cols.info = msgTypeNames[msgType] + subtree:add(fields.Sti, buffer(5, 8)) - pinfo.cols.protocol = rls_protocol.name - local version_number = buffer(1,1):uint().."." - ..buffer(2,1):uint().."." - ..buffer(3,1):uint() - local subtree = tree:add(rls_protocol, buffer(), "RLS Protocol Version "..version_number) - local version = subtree:add(rls_protocol, buffer(2,3), "Version: "..version_number) - version:add(version_major, buffer(1,1)) - version:add(version_minor, buffer(2,1)) - version:add(version_patch, buffer(3,1)) - subtree:add(message_type, buffer(4,1)) - local msg_type = buffer(4,1):uint() - if msg_type <=0 or msg_type > 3 then return end - pinfo.cols.info = message_type_name[msg_type] - subtree:append_text(" - "..message_type_name[msg_type]) - subtree:add(sti, buffer(5,8)) - if msg_type == 1 then -- Cell Info Request - subtree:add(sim_pos_x, buffer(13,4)) - subtree:add(sim_pos_y, buffer(17,4)) - subtree:add(sim_pos_z, buffer(21,4)) - elseif msg_type == 2 then -- Cell Info Response - subtree:add(mcc, buffer(13,2)) - local mnc_tree = subtree:add(rls_protocol, buffer(15,3), "MNC: "..tostring(buffer(15,2):uint())) - mnc_tree:add(mnc, buffer(15,2)) - mnc_tree:add(long_mnc, buffer(17,1)) - subtree:add(nci, buffer(18,8)) - subtree:add(tac, buffer(26,4)) - subtree:add(dbm, buffer(30,4)) - local gnb_name_len = buffer(34,4):uint() - subtree:add(gnb_name, buffer(38,gnb_name_len)) - local link_ip_size = buffer(38+gnb_name_len,4):uint() - subtree:add(link_ip, buffer(42+gnb_name_len,link_ip_size)) - elseif msg_type == 3 then -- PDU Delivery - subtree:add(pdu_type, buffer(13,1)) - local pdu_type_value = buffer(13,1):uint() - local pdu_len = buffer(14,4):uint() - local payload_len = buffer(18+pdu_len,4):uint() - if pdu_type_value == 1 then -- RRC - subtree:add(rrc_channel, buffer(22+pdu_len,payload_len)) - local channel = buffer(22+pdu_len,payload_len):uint() - Dissector.get(rrc_channel_dissector[channel]):call(buffer(18,pdu_len):tvb(), pinfo, tree) - elseif pdu_type_value == 2 then -- DATA - subtree:add(session_id, buffer(22+pdu_len,payload_len)) - Dissector.get("ip"):call(buffer(18,pdu_len):tvb(), pinfo, tree) - end - end + if msgType == 4 then -- Heartbeat + subtree:add(fields.PosX, buffer(13,4)) + subtree:add(fields.PosY, buffer(17,4)) + subtree:add(fields.PosZ, buffer(21,4)) + elseif msgType == 5 then -- Heartbeat ACK + subtree:add(fields.Dbm, buffer(13,4)) + elseif msgType == 6 then -- PDU Transmission + local pduType = buffer(13, 1):uint() + subtree:add(fields.PduType, buffer(13, 1)) + subtree:add(fields.PduId, buffer(14, 4)) + if pduType == 1 then -- RRC PDU + local rrcMsgType = buffer(18, 4):uint() + local pduLength = buffer(22, 4):uint() + subtree:add(fields.RrcMsgType, buffer(18, 4)) + subtree:add(fields.PduLength, buffer(22, 4)) + Dissector.get(nrRrcDissectors[rrcMsgType]):call(buffer(26, pduLength):tvb(), pinfo, tree) + elseif (pduType == 2) then -- Data PDU + subtree:add(fields.PduSessionId, buffer(18, 4)) + local pduLength = buffer(22, 4):uint() + subtree:add(fields.PduLength, buffer(22, 4)) + Dissector.get("ip"):call(buffer(26, pduLength):tvb(), pinfo, tree) + end + elseif msgType == 7 then -- PDU Transmission ACK + local ackCount = buffer(13, 4):uint() + local ackArray = subtree:add(rlsProtocol, buffer(13, 4), "Acknowledge List (" .. ackCount .. ")") + for i = 1,ackCount,1 do + ackArray:add(fields.AcknowledgeItem, buffer(17 + (i - 1) * 4, 4)) + end + end end local udp_port = DissectorTable.get("udp.port") -udp_port:add(4997, rls_protocol) +udp_port:add(4997, rlsProtocol) From a9089eaf547d480907998320459b39b942fcea35 Mon Sep 17 00:00:00 2001 From: Louis Royer <55180044+louisroyer@users.noreply.github.com> Date: Sat, 29 May 2021 11:27:06 +0200 Subject: [PATCH 2/5] Update README.md --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 618e234..be25901 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ [![Build Status](https://travis-ci.org/louisroyer/RLS-wireshark-dissector.svg?branch=master)](https://travis-ci.org/louisroyer/RLS-wireshark-dissector) ## Quick Start -Copy `rls.lua` into `~/.local/lib/wireshark/plugins`. +``` +$ mkdir -p ${XDG_LIB_HOME:-~/.local/lib}/wireshark/plugins +$ git -C ${XDG_LIB_HOME:-~/.local/lib}/wireshark/plugins https://github.com/louisroyer/RLS-wireshark-dissector -NB: you need to use a recent Wireshark version with implementation for `nr-rrc` dissectors. +``` + +NB: you need to use a recent Wireshark version with implementation for `nr-rrc` dissectors (Wireshark version 3.0.0 at least). From 99d5c286e63dbc0e17f97eb7e268b36ab416db23 Mon Sep 17 00:00:00 2001 From: Louis Royer <55180044+louisroyer@users.noreply.github.com> Date: Sat, 29 May 2021 17:04:59 +0200 Subject: [PATCH 3/5] Add backward compatibility --- .luacheckrc | 2 + rls-3-1.lua | 122 ++++++++++++++++++++++++++++++++++++++++ rls-3-2.lua | 118 +++++++++++++++++++++++++++++++++++++++ rls.lua | 156 ++++++++++++++++++---------------------------------- 4 files changed, 294 insertions(+), 104 deletions(-) create mode 100644 rls-3-1.lua create mode 100644 rls-3-2.lua diff --git a/.luacheckrc b/.luacheckrc index 13fc80b..a1249d4 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -7,8 +7,10 @@ stds.wireshark = { read_globals = { "Dissector", "DissectorTable", + "Pref", "Proto", "ProtoField", "base", + "ftypes", } } diff --git a/rls-3-1.lua b/rls-3-1.lua new file mode 100644 index 0000000..605b8de --- /dev/null +++ b/rls-3-1.lua @@ -0,0 +1,122 @@ +--[[ +-- +-- Dissector for Radio Link Simulation Protocol version 3.1.x +-- (UERANSIM project ). +-- +-- CC0-1.0 2021 - Louis Royer () +-- +--]] +require("rls") + +local rlsProtocol31 = Proto("RLS-3.1", "UERANSIM 3.1.x Radio Link Simulation (RLS) Protocol") +local fields = rlsProtocol31.fields + +local msgTypeNames = { + [0] = "[Reserved]", + [1] = "Cell Info Request", + [2] = "Cell Info Response", + [3] = "PDU Delivery", +} + +local pduTypeNames = { + [0] = "[Reserved]", + [1] = "RRC", + [2] = "Data" +} + +local rrcMsgTypeNames = { + [0] = "BCCH-BCH", + [1] = "BCCH-DL-SCH", + [2] = "DL-CCCH", + [3] = "DL-DCCH", + [4] = "PCCH", + [5] = "UL-CCCH", + [6] = "UL-CCCH1", + [7] = "UL-DCCH", +} + +local nrRrcDissectors = { + [0] = "nr-rrc.bcch.bch", + [1] = "nr-rrc.bcch.dl.sch", + [2] = "nr-rrc.dl.ccch", + [3] = "nr-rrc.dl.dcch", + [4] = "nr-rrc.pcch", + [5] = "nr-rrc.ul.ccch", + [6] = "nr-rrc.ul.ccch1", + [7] = "nr-rrc.ul.dcch", +} + +fields.Version = ProtoField.string("rls.version", "Version") +fields.MsgType = ProtoField.uint8("rls.message_type", "Message Type", base.DEC, msgTypeNames) +fields.Sti = ProtoField.uint64("rls.sti", "Sender Node Temporary ID", base.DEC) +fields.PduType = ProtoField.uint8("rls.pdu_type", "PDU Type", base.DEC, pduTypeNames) +fields.RrcMsgType = ProtoField.uint32("rls.rrc_message_type", "RRC Message Type", base.DEC, rrcMsgTypeNames) +fields.PduLength = ProtoField.uint32("rls.pdu_length", "PDU Length", base.DEC) +fields.PduSessionId = ProtoField.uint32("rls.pdu_session_id", "PDU Session ID", base.DEC) +fields.Dbm = ProtoField.int32("rls.dbm", "RLS Signal Strength (dBm)", base.DEC) +fields.PosX = ProtoField.uint32("rls.pos_x", "RLS Position X", base.DEC) +fields.PosY = ProtoField.uint32("rls.pos_y", "RLS Position Y", base.DEC) +fields.PosZ = ProtoField.uint32("rls.pos_z", "RLS Position Z", base.DEC) +fields.Mcc = ProtoField.uint16("rls.mcc", "MCC", base.DEC) +fields.Mnc = ProtoField.uint16("rls.mnc", "MNC", base.DEC) +fields.LongMnc = ProtoField.bool("rls.long_mnc", "MNC is 3-digit", base.BOOL) +fields.Nci = ProtoField.uint64("rls.nci", "NR Cell Identity", base.HEX) +fields.Tac = ProtoField.uint32("rls.tac", "Tracking Area Code", base.DEC) +fields.GnbName = ProtoField.string("rls.gnb_name", "gNB Name") +fields.LinkIp = ProtoField.string("rls.link_ip", "gNB Link IP") + +function rlsProtocol31.dissector(buffer, pinfo, tree) + if buffer:len() == 0 then return false end + if buffer(0, 1):uint() ~= 0x03 then return false end + if buffer(1, 2):uint() > 0x0301 then return false end + + pinfo.cols.protocol = rlsProtocol31.name + + local versionNumber = buffer(1, 1):uint() .. "." .. buffer(2, 1):uint() .. "." .. buffer(3, 1):uint() + local subtree = tree:add(rlsProtocol31, buffer(), "UERANSIM Radio Link Simulation (RLS) protocol") + + subtree:add(fields.Version, buffer(1, 3), versionNumber) + subtree:add(fields.MsgType, buffer(4, 1)) + local msgType = buffer(4, 1):uint() + + pinfo.cols.info = msgTypeNames[msgType] + subtree:add(fields.Sti, buffer(5, 8)) + + if msgType == 1 then -- Cell Info Request + subtree:add(fields.PosX, buffer(13,4)) + subtree:add(fields.PosY, buffer(17,4)) + subtree:add(fields.PosZ, buffer(21,4)) + elseif msgType == 2 then -- Cell Info Response + subtree:add(fields.Mcc, buffer(13,2)) + local mnc_tree = subtree:add(rlsProtocol31, buffer(15,3), "MNC: "..tostring(buffer(15,2):uint())) + mnc_tree:add(fields.Mnc, buffer(15,2)) + mnc_tree:add(fields.LongMnc, buffer(17,1)) + subtree:add(fields.Nci, buffer(18,8)) + subtree:add(fields.Tac, buffer(26,4)) + subtree:add(fields.Dbm, buffer(30,4)) + local gnbNameLength = buffer(34,4):uint() + subtree:add(fields.GnbName, buffer(38,gnbNameLength)) + local linkIpLength = buffer(38+gnbNameLength,4):uint() + subtree:add(fields.LinkIp, buffer(42+gnbNameLength,linkIpLength)) + elseif msgType == 3 then -- PDU Delivery + subtree:add(fields.PduType, buffer(13,1)) + local pduType = buffer(13,1):uint() + local pduLength = buffer(14,4):uint() + local payloadLength = buffer(18+pduLength,4):uint() + if pduType == 1 then -- RRC + local rrcMsgType = buffer(22+pduLength,payloadLength):uint() + subtree:add(fields.RrcMsgType, buffer(22+pduLength,payloadLength)) + subtree:add(fields.PduLength, buffer(14,4)) + Dissector.get(nrRrcDissectors[rrcMsgType]):call(buffer(18,pduLength):tvb(), pinfo, tree) + elseif pduType == 2 then -- DATA + subtree:add(fields.PduSessionId, buffer(22+pduLength,payloadLength)) + subtree:add(fields.PduLength, buffer(14,4)) + Dissector.get("ip"):call(buffer(18,pduLength):tvb(), pinfo, tree) + end + end +end + +local rls = DissectorTable.get("rls") +rls:add(0x0301, rlsProtocol31) +local udp_port = DissectorTable.get("udp.port") +udp_port:add_for_decode_as(rlsProtocol31) diff --git a/rls-3-2.lua b/rls-3-2.lua new file mode 100644 index 0000000..f73a60e --- /dev/null +++ b/rls-3-2.lua @@ -0,0 +1,118 @@ +--[[ +-- +-- Dissector for Radio Link Simulation Protocol version 3.2.x +-- (UERANSIM project ). +-- +-- CC0-1.0 2021 - Louis Royer () +-- +--]] +require("rls") + +local rlsProtocol32 = Proto("RLS-3.2", "UERANSIM 3.2.x Radio Link Simulation (RLS) Protocol") +local fields = rlsProtocol32.fields + +local msgTypeNames = { + [0] = "[Reserved]", + [1] = "[Reserved]", + [2] = "[Reserved]", + [3] = "[Reserved]", + [4] = "Heartbeat", + [5] = "Heartbeat ACK", + [6] = "PDU Transmission", + [7] = "PDU Transmission ACK", +} + +local pduTypeNames = { + [0] = "[Reserved]", + [1] = "RRC", + [2] = "Data" +} + +local rrcMsgTypeNames = { + [0] = "BCCH-BCH", + [1] = "BCCH-DL-SCH", + [2] = "DL-CCCH", + [3] = "DL-DCCH", + [4] = "PCCH", + [5] = "UL-CCCH", + [6] = "UL-CCCH1", + [7] = "UL-DCCH", +} + +local nrRrcDissectors = { + [0] = "nr-rrc.bcch.bch", + [1] = "nr-rrc.bcch.dl.sch", + [2] = "nr-rrc.dl.ccch", + [3] = "nr-rrc.dl.dcch", + [4] = "nr-rrc.pcch", + [5] = "nr-rrc.ul.ccch", + [6] = "nr-rrc.ul.ccch1", + [7] = "nr-rrc.ul.dcch", +} + +fields.Version = ProtoField.string("rls.version", "Version") +fields.MsgType = ProtoField.uint8("rls.message_type", "Message Type", base.DEC, msgTypeNames) +fields.Sti = ProtoField.uint64("rls.sti", "Sender Node Temporary ID", base.DEC) +fields.PduType = ProtoField.uint8("rls.pdu_type", "PDU Type", base.DEC, pduTypeNames) +fields.PduId = ProtoField.uint32("rls.pdu_id", "PDU ID", base.DEC) +fields.RrcMsgType = ProtoField.uint32("rls.rrc_message_type", "RRC Message Type", base.DEC, rrcMsgTypeNames) +fields.PduLength = ProtoField.uint32("rls.pdu_length", "PDU Length", base.DEC) +fields.PduSessionId = ProtoField.uint32("rls.pdu_session_id", "PDU Session ID", base.DEC) +fields.AcknowledgeItem = ProtoField.uint32("rls.ack_item", "PDU ID") +fields.Dbm = ProtoField.int32("rls.dbm", "RLS Signal Strength (dBm)", base.DEC) +fields.PosX = ProtoField.uint32("rls.pos_x", "RLS Position X", base.DEC) +fields.PosY = ProtoField.uint32("rls.pos_y", "RLS Position Y", base.DEC) +fields.PosZ = ProtoField.uint32("rls.pos_z", "RLS Position Z", base.DEC) + +function rlsProtocol32.dissector(buffer, pinfo, tree) + if buffer:len() == 0 then return false end + if buffer(0, 1):uint() ~= 0x03 then return false end + if buffer(1, 2):uint() < 0x0302 then return false end + + pinfo.cols.protocol = rlsProtocol32.name + + local versionNumber = buffer(1, 1):uint() .. "." .. buffer(2, 1):uint() .. "." .. buffer(3, 1):uint() + local subtree = tree:add(rlsProtocol32, buffer(), "UERANSIM Radio Link Simulation (RLS) protocol") + + subtree:add(fields.Version, buffer(1, 3), versionNumber) + subtree:add(fields.MsgType, buffer(4, 1)) + local msgType = buffer(4, 1):uint() + + pinfo.cols.info = msgTypeNames[msgType] + subtree:add(fields.Sti, buffer(5, 8)) + + if msgType == 4 then -- Heartbeat + subtree:add(fields.PosX, buffer(13,4)) + subtree:add(fields.PosY, buffer(17,4)) + subtree:add(fields.PosZ, buffer(21,4)) + elseif msgType == 5 then -- Heartbeat ACK + subtree:add(fields.Dbm, buffer(13,4)) + elseif msgType == 6 then -- PDU Transmission + local pduType = buffer(13, 1):uint() + subtree:add(fields.PduType, buffer(13, 1)) + subtree:add(fields.PduId, buffer(14, 4)) + if pduType == 1 then -- RRC PDU + local rrcMsgType = buffer(18, 4):uint() + local pduLength = buffer(22, 4):uint() + subtree:add(fields.RrcMsgType, buffer(18, 4)) + subtree:add(fields.PduLength, buffer(22, 4)) + Dissector.get(nrRrcDissectors[rrcMsgType]):call(buffer(26, pduLength):tvb(), pinfo, tree) + elseif (pduType == 2) then -- Data PDU + subtree:add(fields.PduSessionId, buffer(18, 4)) + local pduLength = buffer(22, 4):uint() + subtree:add(fields.PduLength, buffer(22, 4)) + Dissector.get("ip"):call(buffer(26, pduLength):tvb(), pinfo, tree) + end + elseif msgType == 7 then -- PDU Transmission ACK + local ackCount = buffer(13, 4):uint() + local ackArray = subtree:add(rlsProtocol32, buffer(13, 4), "Acknowledge List (" .. ackCount .. ")") + for i = 1,ackCount,1 do + ackArray:add(fields.AcknowledgeItem, buffer(17 + (i - 1) * 4, 4)) + end + end +end + +local rls = DissectorTable.get("rls") +rls:add(0x0302, rlsProtocol32) +local udp_port = DissectorTable.get("udp.port") +udp_port:add_for_decode_as(rlsProtocol32) diff --git a/rls.lua b/rls.lua index 2f2f856..0774f2a 100644 --- a/rls.lua +++ b/rls.lua @@ -7,108 +7,56 @@ -- --]] -local rlsProtocol = Proto("RLS", "UERANSIM Radio Link Simulation (RLS) Protocol") -local fields = rlsProtocol.fields - -local msgTypeNames = { - [0] = "[Reserved]", - [1] = "[Reserved]", - [2] = "[Reserved]", - [3] = "[Reserved]", - [4] = "Heartbeat", - [5] = "Heartbeat ACK", - [6] = "PDU Transmission", - [7] = "PDU Transmission ACK", -} - -local pduTypeNames = { - [0] = "[Reserved]", - [1] = "RRC", - [2] = "Data" -} - -local rrcMsgTypeNames = { - [0] = "BCCH-BCH", - [1] = "BCCH-DL-SCH", - [2] = "DL-CCCH", - [3] = "DL-DCCH", - [4] = "PCCH", - [5] = "UL-CCCH", - [6] = "UL-CCCH1", - [7] = "UL-DCCH", -} - -local nrRrcDissectors = { - [0] = "nr-rrc.bcch.bch", - [1] = "nr-rrc.bcch.dl.sch", - [2] = "nr-rrc.dl.ccch", - [3] = "nr-rrc.dl.dcch", - [4] = "nr-rrc.pcch", - [5] = "nr-rrc.ul.ccch", - [6] = "nr-rrc.ul.ccch1", - [7] = "nr-rrc.ul.dcch", -} - -fields.Version = ProtoField.string("rls.version", "Version") -fields.MsgType = ProtoField.uint8("rls.message_type", "Message Type", base.DEC, msgTypeNames) -fields.Sti = ProtoField.uint64("rls.sti", "Sender Node Temporary ID", base.DEC) -fields.PduType = ProtoField.uint8("rls.pdu_type", "PDU Type", base.DEC, pduTypeNames) -fields.PduId = ProtoField.uint32("rls.pdu_id", "PDU ID", base.DEC) -fields.RrcMsgType = ProtoField.uint32("rls.rrc_message_type", "RRC Message Type", base.DEC, rrcMsgTypeNames) -fields.PduLength = ProtoField.uint32("rls.pdu_length", "PDU Length", base.DEC) -fields.PduSessionId = ProtoField.uint32("rls.pdu_session_id", "PDU Session ID", base.DEC) -fields.AcknowledgeItem = ProtoField.uint32("rls.ack_item", "PDU ID") -fields.Dbm = ProtoField.int32("rls.dbm", "RLS Signal Strength (dBm)", base.DEC) -fields.PosX = ProtoField.uint32("rls.pos_x", "RLS Position X", base.DEC) -fields.PosY = ProtoField.uint32("rls.pos_y", "RLS Position Y", base.DEC) -fields.PosZ = ProtoField.uint32("rls.pos_z", "RLS Position Z", base.DEC) - -function rlsProtocol.dissector(buffer, pinfo, tree) - if buffer:len() == 0 then return end - if buffer(0, 1):uint() ~= 0x03 then return end - - pinfo.cols.protocol = rlsProtocol.name - - local versionNumber = buffer(1, 1):uint() .. "." .. buffer(2, 1):uint() .. "." .. buffer(3, 1):uint() - local subtree = tree:add(rlsProtocol, buffer(), "UERANSIM Radio Link Simulation (RLS) protocol") - - subtree:add(fields.Version, buffer(1, 3), versionNumber) - subtree:add(fields.MsgType, buffer(4, 1)) - local msgType = buffer(4, 1):uint() - - pinfo.cols.info = msgTypeNames[msgType] - subtree:add(fields.Sti, buffer(5, 8)) - - if msgType == 4 then -- Heartbeat - subtree:add(fields.PosX, buffer(13,4)) - subtree:add(fields.PosY, buffer(17,4)) - subtree:add(fields.PosZ, buffer(21,4)) - elseif msgType == 5 then -- Heartbeat ACK - subtree:add(fields.Dbm, buffer(13,4)) - elseif msgType == 6 then -- PDU Transmission - local pduType = buffer(13, 1):uint() - subtree:add(fields.PduType, buffer(13, 1)) - subtree:add(fields.PduId, buffer(14, 4)) - if pduType == 1 then -- RRC PDU - local rrcMsgType = buffer(18, 4):uint() - local pduLength = buffer(22, 4):uint() - subtree:add(fields.RrcMsgType, buffer(18, 4)) - subtree:add(fields.PduLength, buffer(22, 4)) - Dissector.get(nrRrcDissectors[rrcMsgType]):call(buffer(26, pduLength):tvb(), pinfo, tree) - elseif (pduType == 2) then -- Data PDU - subtree:add(fields.PduSessionId, buffer(18, 4)) - local pduLength = buffer(22, 4):uint() - subtree:add(fields.PduLength, buffer(22, 4)) - Dissector.get("ip"):call(buffer(26, pduLength):tvb(), pinfo, tree) - end - elseif msgType == 7 then -- PDU Transmission ACK - local ackCount = buffer(13, 4):uint() - local ackArray = subtree:add(rlsProtocol, buffer(13, 4), "Acknowledge List (" .. ackCount .. ")") - for i = 1,ackCount,1 do - ackArray:add(fields.AcknowledgeItem, buffer(17 + (i - 1) * 4, 4)) - end - end +-- This file must only be loaded once, but each version requires it. +if package.loaded['rls'] == nil then + -- Update the following when adding new versions + local latestVersion = 0x0302 + local oldestVersion = 0x0301 + + local rlsProtocol = Proto("RLS", "UERANSIM Radio Link Simulation (RLS) Protocol") + + -- Create a DissectorTable to register dissector for each version of RLS + DissectorTable.new("rls", "RLS version", ftypes.UINT32, base.HEX, rlsProtocol) + + -- Preferences + rlsProtocol.prefs.udp_port = Pref.uint("RLS UDP port", 4997, "UDP port for RLS") + + -- Add version field + local fields = rlsProtocol.fields + fields.Version = ProtoField.string("rls.version", "Version") + + function rlsProtocol.dissector(buffer, pinfo, tree) + -- Generic check + if buffer:len() == 0 then return end + if buffer(0, 1):uint() ~= 0x03 then return end + + -- Add default informations (will be overriden by sub-dissectors) + pinfo.cols.protocol = rlsProtocol.name + pinfo.cols.info = "Unsupported version - Cannot decode" + + local version = buffer(1,2):uint() + local subprotocol = DissectorTable.get("rls"):get_dissector(version) + if subprotocol == nil then + if version > latestVersion then + -- fallback to latest version + version = latestVersion + elseif version < oldestVersion then + -- fallback to oldest version + version = oldestVersion + end + subprotocol = DissectorTable.get("rls"):get_dissector(version) + if subprotocol == nil then + local versionNumber = buffer(1, 1):uint() .. "." .. buffer(2, 1):uint() .. "." .. buffer(3, 1):uint() + local subtree = tree:add(rlsProtocol, buffer(), "UERANSIM Radio Link Simulation (RLS) protocol") + subtree:add(fields.Version, buffer(1, 3), versionNumber) + return 4 + end + end + subprotocol:call(buffer():tvb(), pinfo, tree) + end + + -- Export protocol + local udp_port = DissectorTable.get("udp.port") + udp_port:add(rlsProtocol.prefs.udp_port, rlsProtocol) + udp_port:add_for_decode_as(rlsProtocol) end - -local udp_port = DissectorTable.get("udp.port") -udp_port:add(4997, rlsProtocol) From a75d8b15df4963d7542bb9bcfa187134abcef7e0 Mon Sep 17 00:00:00 2001 From: Louis Royer <55180044+louisroyer@users.noreply.github.com> Date: Sat, 29 May 2021 21:23:00 +0200 Subject: [PATCH 4/5] Error handling for Wireshark < 3.0.0 (without NR-RRC support) --- rls-3-1.lua | 14 +++++++++++++- rls-3-2.lua | 14 +++++++++++++- rls.lua | 3 +-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/rls-3-1.lua b/rls-3-1.lua index 605b8de..ead69ff 100644 --- a/rls-3-1.lua +++ b/rls-3-1.lua @@ -107,7 +107,19 @@ function rlsProtocol31.dissector(buffer, pinfo, tree) local rrcMsgType = buffer(22+pduLength,payloadLength):uint() subtree:add(fields.RrcMsgType, buffer(22+pduLength,payloadLength)) subtree:add(fields.PduLength, buffer(14,4)) - Dissector.get(nrRrcDissectors[rrcMsgType]):call(buffer(18,pduLength):tvb(), pinfo, tree) + -- Old versions of Wireshark (< 3.0.0) cannot handle NR-RRC correctly + local dissector + local function get_dissector() + dissector = Dissector.get(nrRrcDissectors[rrcMsgType]) + end + if pcall(get_dissector) then + dissector:call(buffer(18, pduLength):tvb(), pinfo, tree) + else + pinfo.cols.info = msgTypeNames[msgType] + .. " - " .. rrcMsgTypeNames[rrcMsgType] + .. " - Cannot decode" + return false + end elseif pduType == 2 then -- DATA subtree:add(fields.PduSessionId, buffer(22+pduLength,payloadLength)) subtree:add(fields.PduLength, buffer(14,4)) diff --git a/rls-3-2.lua b/rls-3-2.lua index f73a60e..aec8b25 100644 --- a/rls-3-2.lua +++ b/rls-3-2.lua @@ -96,7 +96,19 @@ function rlsProtocol32.dissector(buffer, pinfo, tree) local pduLength = buffer(22, 4):uint() subtree:add(fields.RrcMsgType, buffer(18, 4)) subtree:add(fields.PduLength, buffer(22, 4)) - Dissector.get(nrRrcDissectors[rrcMsgType]):call(buffer(26, pduLength):tvb(), pinfo, tree) + -- Old versions of Wireshark (< 3.0.0) cannot handle NR-RRC correctly + local dissector + local function get_dissector() + dissector = Dissector.get(nrRrcDissectors[rrcMsgType]) + end + if pcall(get_dissector) then + dissector:call(buffer(26, pduLength):tvb(), pinfo, tree) + else + pinfo.cols.info = msgTypeNames[msgType] + .. " - " .. rrcMsgTypeNames[rrcMsgType] + .. " - Cannot decode" + return false + end elseif (pduType == 2) then -- Data PDU subtree:add(fields.PduSessionId, buffer(18, 4)) local pduLength = buffer(22, 4):uint() diff --git a/rls.lua b/rls.lua index 0774f2a..cda89e6 100644 --- a/rls.lua +++ b/rls.lua @@ -30,9 +30,7 @@ if package.loaded['rls'] == nil then if buffer:len() == 0 then return end if buffer(0, 1):uint() ~= 0x03 then return end - -- Add default informations (will be overriden by sub-dissectors) pinfo.cols.protocol = rlsProtocol.name - pinfo.cols.info = "Unsupported version - Cannot decode" local version = buffer(1,2):uint() local subprotocol = DissectorTable.get("rls"):get_dissector(version) @@ -48,6 +46,7 @@ if package.loaded['rls'] == nil then if subprotocol == nil then local versionNumber = buffer(1, 1):uint() .. "." .. buffer(2, 1):uint() .. "." .. buffer(3, 1):uint() local subtree = tree:add(rlsProtocol, buffer(), "UERANSIM Radio Link Simulation (RLS) protocol") + pinfo.cols.info = "Unsupported version - Cannot decode" subtree:add(fields.Version, buffer(1, 3), versionNumber) return 4 end From 752c33c93e531ef65e36544ce404e381b285c531 Mon Sep 17 00:00:00 2001 From: Louis Royer <55180044+louisroyer@users.noreply.github.com> Date: Mon, 31 May 2021 09:32:16 +0200 Subject: [PATCH 5/5] Add pcap for RLS 3.2.0 --- pcap/rls_3-2-0.pcapng | Bin 0 -> 75196 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 pcap/rls_3-2-0.pcapng diff --git a/pcap/rls_3-2-0.pcapng b/pcap/rls_3-2-0.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..74299119f8e4e40c0bfad8ec2b652c632471eccf GIT binary patch literal 75196 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 literal 0 HcmV?d00001