86890704fd
todo: add documentation & wireshark dissector
469 lines
13 KiB
Python
Executable file
469 lines
13 KiB
Python
Executable file
# This file is part of Scapy
|
|
# See http://www.secdev.org/projects/scapy for more information
|
|
# Copyright (C) 2019 Freie Universitaet Berlin
|
|
# This program is published under GPLv2 license
|
|
#
|
|
# Specification:
|
|
# http://www.mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf
|
|
|
|
# scapy.contrib.description = MQTT for Sensor Networks (MQTT-SN)
|
|
# scapy.contrib.status = loads
|
|
|
|
from scapy.packet import Packet, bind_layers, bind_bottom_up
|
|
from scapy.fields import BitField, BitEnumField, ByteField, ByteEnumField, \
|
|
ConditionalField, FieldLenField, ShortField, StrFixedLenField, \
|
|
StrLenField, XByteEnumField
|
|
from scapy.layers.inet import UDP
|
|
from scapy.error import Scapy_Exception
|
|
from scapy.compat import chb, orb
|
|
from scapy.volatile import RandNum
|
|
import struct
|
|
|
|
|
|
# Constants
|
|
ADVERTISE = 0x00
|
|
SEARCHGW = 0x01
|
|
GWINFO = 0x02
|
|
CONNECT = 0x04
|
|
CONNACK = 0x05
|
|
WILLTOPICREQ = 0x06
|
|
WILLTOPIC = 0x07
|
|
WILLMSGREQ = 0x08
|
|
WILLMSG = 0x09
|
|
REGISTER = 0x0a
|
|
REGACK = 0x0b
|
|
PUBLISH = 0x0c
|
|
PUBACK = 0x0d
|
|
PUBCOMP = 0x0e
|
|
PUBREC = 0x0f
|
|
PUBREL = 0x10
|
|
SUBSCRIBE = 0x12
|
|
SUBACK = 0x13
|
|
UNSUBSCRIBE = 0x14
|
|
UNSUBACK = 0x15
|
|
PINGREQ = 0x16
|
|
PINGRESP = 0x17
|
|
DISCONNECT = 0x18
|
|
WILLTOPICUPD = 0x1a
|
|
WILLTOPICRESP = 0x1b
|
|
WILLMSGUPD = 0x1c
|
|
WILLMSGRESP = 0x1d
|
|
ENCAPS_MSG = 0xfe
|
|
|
|
QOS_0 = 0b00
|
|
QOS_1 = 0b01
|
|
QOS_2 = 0b10
|
|
QOS_NEG1 = 0b11
|
|
|
|
TID_NORMAL = 0b00
|
|
TID_PREDEF = 0b01
|
|
TID_SHORT = 0b10
|
|
TID_RESVD = 0b11
|
|
|
|
|
|
ACCEPTED = 0x00
|
|
REJ_CONJ = 0x01
|
|
REJ_TID = 0x02
|
|
REJ_NOTSUP = 0x03
|
|
|
|
|
|
# Custom fields
|
|
class VariableFieldLenField(FieldLenField):
|
|
"""
|
|
MQTT-SN length field either has 1 byte for values [0x02, 0xff] or 3 bytes
|
|
for values [0x0100, 0xffff]. If the first byte is 0x01 the length value
|
|
comes in network byte-order in the next 2 bytes. MQTT-SN packets are at
|
|
least 2 bytes long (length field + type field).
|
|
"""
|
|
def addfield(self, pkt, s, val):
|
|
val = self.i2m(pkt, val)
|
|
if (val < 2) or (val > 0xffff):
|
|
raise Scapy_Exception("%s: invalid length field value" %
|
|
self.__class__.__name__)
|
|
elif val > 0xff:
|
|
return s + b"\x01" + struct.pack("!H", val)
|
|
else:
|
|
return s + chb(val)
|
|
|
|
def getfield(self, pkt, s):
|
|
if orb(s[0]) == 0x01:
|
|
if len(s) < 3:
|
|
raise Scapy_Exception("%s: malformed length field" %
|
|
self.__class__.__name__)
|
|
return s[3:], (orb(s[1]) << 8) | orb(s[2])
|
|
else:
|
|
return s[1:], orb(s[0])
|
|
|
|
def randval(self):
|
|
return RandVariableFieldLen()
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(VariableFieldLenField, self).__init__(*args, **kwargs)
|
|
|
|
|
|
class RandVariableFieldLen(RandNum):
|
|
def __init__(self):
|
|
super(RandVariableFieldLen, self).__init__(0, 0xffff)
|
|
|
|
|
|
# Layers
|
|
PACKET_TYPE = {
|
|
ADVERTISE: "ADVERTISE",
|
|
SEARCHGW: "SEARCHGW",
|
|
GWINFO: "GWINFO",
|
|
CONNECT: "CONNECT",
|
|
CONNACK: "CONNACK",
|
|
WILLTOPICREQ: "WILLTOPICREQ",
|
|
WILLTOPIC: "WILLTOPIC",
|
|
WILLMSGREQ: "WILLMSGREQ",
|
|
WILLMSG: "WILLMSG",
|
|
REGISTER: "REGISTER",
|
|
REGACK: "REGACK",
|
|
PUBLISH: "PUBLISH",
|
|
PUBACK: "PUBACK",
|
|
PUBCOMP: "PUBCOMP",
|
|
PUBREC: "PUBREC",
|
|
PUBREL: "PUBREL",
|
|
SUBSCRIBE: "SUBSCRIBE",
|
|
SUBACK: "SUBACK",
|
|
UNSUBSCRIBE: "UNSUBSCRIBE",
|
|
UNSUBACK: "UNSUBACK",
|
|
PINGREQ: "PINGREQ",
|
|
PINGRESP: "PINGRESP",
|
|
DISCONNECT: "DISCONNECT",
|
|
WILLTOPICUPD: "WILLTOPICUPD",
|
|
WILLTOPICRESP: "WILLTOPICRESP",
|
|
WILLMSGUPD: "WILLMSGUPD",
|
|
WILLMSGRESP: "WILLMSGRESP",
|
|
ENCAPS_MSG: "Encapsulated message",
|
|
}
|
|
|
|
|
|
QOS_LEVELS = {
|
|
QOS_0: 'Fire and Forget',
|
|
QOS_1: 'Acknowledged deliver',
|
|
QOS_2: 'Assured Delivery',
|
|
QOS_NEG1: 'No Connection required',
|
|
}
|
|
|
|
|
|
TOPIC_ID_TYPES = {
|
|
TID_NORMAL: 'Normal ID',
|
|
TID_PREDEF: 'Pre-defined ID',
|
|
TID_SHORT: 'Short Topic Name',
|
|
TID_RESVD: 'Reserved',
|
|
}
|
|
|
|
|
|
RETURN_CODES = {
|
|
ACCEPTED: "Accepted",
|
|
REJ_CONJ: "Rejected: congestion",
|
|
REJ_TID: "Rejected: invalid topic ID",
|
|
REJ_NOTSUP: "Rejected: not supported",
|
|
}
|
|
|
|
|
|
FLAG_FIELDS = [
|
|
BitField("dup", 0, 1),
|
|
BitEnumField("qos", QOS_0, 2, QOS_LEVELS),
|
|
BitField("retain", 0, 1),
|
|
BitField("will", 0, 1),
|
|
BitField("cleansess", 0, 1),
|
|
BitEnumField("tid_type", TID_NORMAL, 2, TOPIC_ID_TYPES),
|
|
]
|
|
|
|
|
|
def _mqttsn_length_from(size_until):
|
|
def fun(pkt):
|
|
if (hasattr(pkt.underlayer, "len")):
|
|
if pkt.underlayer.len > 0xff:
|
|
return pkt.underlayer.len - size_until - 4
|
|
elif (pkt.underlayer.len > 1) and (pkt.underlayer.len < 0xffff):
|
|
return pkt.underlayer.len - size_until - 2
|
|
# assume string to be of length 0
|
|
return len(pkt.payload) - size_until + 1
|
|
return fun
|
|
|
|
|
|
def _mqttsn_len_adjust(pkt, x):
|
|
res = x + len(pkt.payload)
|
|
if (pkt.type == DISCONNECT) and \
|
|
(getattr(pkt.payload, "duration", None) is None):
|
|
res -= 2 # duration is optional with DISCONNECT
|
|
elif (pkt.type == ENCAPS_MSG) and \
|
|
(getattr(pkt.payload, "w_node_id", None) is not None):
|
|
res = x + len(pkt.payload.w_node_id) + 1
|
|
if res > 0xff:
|
|
res += 2
|
|
return res
|
|
|
|
|
|
class MQTTSN(Packet):
|
|
name = "MQTT-SN header"
|
|
fields_desc = [
|
|
# Since the size of the len field depends on the next layer, we
|
|
# need to "cheat" with the length_of parameter and use adjust
|
|
# parameter to calculate the value.
|
|
VariableFieldLenField("len", None, length_of="len",
|
|
adjust=_mqttsn_len_adjust),
|
|
XByteEnumField("type", 0, PACKET_TYPE),
|
|
]
|
|
|
|
|
|
class MQTTSNAdvertise(Packet):
|
|
name = "MQTT-SN advertise gateway"
|
|
fields_desc = [
|
|
ByteField("gw_id", 0),
|
|
ShortField("duration", 0),
|
|
]
|
|
|
|
|
|
class MQTTSNSearchGW(Packet):
|
|
name = "MQTT-SN search gateway"
|
|
fields_desc = [
|
|
ByteField("radius", 0),
|
|
]
|
|
|
|
|
|
class MQTTSNGwInfo(Packet):
|
|
name = "MQTT-SN gateway info"
|
|
fields_desc = [
|
|
ByteField("gw_id", 0),
|
|
StrLenField("gw_addr", "", length_from=_mqttsn_length_from(1)),
|
|
]
|
|
|
|
|
|
class MQTTSNConnect(Packet):
|
|
name = "MQTT-SN connect command"
|
|
fields_desc = FLAG_FIELDS + [
|
|
ByteField("prot_id", 1),
|
|
ShortField("duration", 0),
|
|
StrLenField("client_id", "", length_from=_mqttsn_length_from(4)),
|
|
]
|
|
|
|
|
|
class MQTTSNConnack(Packet):
|
|
name = "MQTT-SN connect ACK"
|
|
fields_desc = [
|
|
ByteEnumField("return_code", ACCEPTED, RETURN_CODES),
|
|
]
|
|
|
|
|
|
class MQTTSNWillTopicReq(Packet):
|
|
name = "MQTT-SN will topic request"
|
|
|
|
|
|
class MQTTSNWillTopic(Packet):
|
|
name = "MQTT-SN will topic"
|
|
fields_desc = FLAG_FIELDS + [
|
|
StrLenField("will_topic", "", length_from=_mqttsn_length_from(1)),
|
|
]
|
|
|
|
|
|
class MQTTSNWillMsgReq(Packet):
|
|
name = "MQTT-SN will message request"
|
|
|
|
|
|
class MQTTSNWillMsg(Packet):
|
|
name = "MQTT-SN will message"
|
|
fields_desc = [
|
|
StrLenField("will_msg", "", length_from=_mqttsn_length_from(0))
|
|
]
|
|
|
|
|
|
class MQTTSNRegister(Packet):
|
|
name = "MQTT-SN register"
|
|
fields_desc = [
|
|
ShortField("tid", 0),
|
|
ShortField("mid", 0),
|
|
StrLenField("topic_name", "", length_from=_mqttsn_length_from(4)),
|
|
]
|
|
|
|
|
|
class MQTTSNRegack(Packet):
|
|
name = "MQTT-SN register ACK"
|
|
fields_desc = [
|
|
ShortField("tid", 0),
|
|
ShortField("mid", 0),
|
|
ByteEnumField("return_code", ACCEPTED, RETURN_CODES),
|
|
]
|
|
|
|
|
|
class MQTTSNPublish(Packet):
|
|
name = "MQTT-SN publish message"
|
|
fields_desc = FLAG_FIELDS + [
|
|
ShortField("tid", 0),
|
|
ShortField("mid", 0),
|
|
StrLenField("data", "", length_from=_mqttsn_length_from(5)),
|
|
]
|
|
|
|
|
|
class MQTTSNPuback(Packet):
|
|
name = "MQTT-SN publish ACK"
|
|
fields_desc = [
|
|
ShortField("tid", 0),
|
|
ShortField("mid", 0),
|
|
ByteEnumField("return_code", ACCEPTED, RETURN_CODES),
|
|
]
|
|
|
|
|
|
class MQTTSNPubcomp(Packet):
|
|
name = "MQTT-SN publish complete"
|
|
fields_desc = [
|
|
ShortField("mid", 0),
|
|
]
|
|
|
|
|
|
class MQTTSNPubrec(Packet):
|
|
name = "MQTT-SN publish received"
|
|
fields_desc = [
|
|
ShortField("mid", 0),
|
|
]
|
|
|
|
|
|
class MQTTSNPubrel(Packet):
|
|
name = "MQTT-SN publish release"
|
|
fields_desc = [
|
|
ShortField("mid", 0),
|
|
]
|
|
|
|
|
|
class MQTTSNSubscribe(Packet):
|
|
name = "MQTT-SN subscribe request"
|
|
fields_desc = FLAG_FIELDS + [
|
|
ShortField("mid", 0),
|
|
ConditionalField(ShortField("tid", None),
|
|
lambda pkt: pkt.tid_type == 0b01),
|
|
ConditionalField(StrFixedLenField("short_topic", None, length=2),
|
|
lambda pkt: pkt.tid_type == 0b10),
|
|
ConditionalField(StrLenField("topic_name", None,
|
|
length_from=_mqttsn_length_from(3)),
|
|
lambda pkt: pkt.tid_type not in [0b01, 0b10]),
|
|
]
|
|
|
|
|
|
class MQTTSNSuback(Packet):
|
|
name = "MQTT-SN subscribe ACK"
|
|
fields_desc = FLAG_FIELDS + [
|
|
ShortField("tid", 0),
|
|
ShortField("mid", 0),
|
|
ByteEnumField("return_code", ACCEPTED, RETURN_CODES),
|
|
]
|
|
|
|
|
|
class MQTTSNUnsubscribe(Packet):
|
|
name = "MQTT-SN unsubscribe request"
|
|
fields_desc = FLAG_FIELDS + [
|
|
ShortField("mid", 0),
|
|
ConditionalField(ShortField("tid", None),
|
|
lambda pkt: pkt.tid_type == 0b01),
|
|
ConditionalField(StrFixedLenField("short_topic", None, length=2),
|
|
lambda pkt: pkt.tid_type == 0b10),
|
|
ConditionalField(StrLenField("topic_name", None,
|
|
length_from=_mqttsn_length_from(3)),
|
|
lambda pkt: pkt.tid_type not in [0b01, 0b10]),
|
|
]
|
|
|
|
|
|
class MQTTSNUnsuback(Packet):
|
|
name = "MQTT-SN unsubscribe ACK"
|
|
fields_desc = [
|
|
ShortField("mid", 0),
|
|
]
|
|
|
|
|
|
class MQTTSNPingReq(Packet):
|
|
name = "MQTT-SN ping request"
|
|
fields_desc = [
|
|
StrLenField("client_id", "", length_from=_mqttsn_length_from(0)),
|
|
]
|
|
|
|
|
|
class MQTTSNPingResp(Packet):
|
|
name = "MQTT-SN ping response"
|
|
|
|
|
|
class MQTTSNDisconnect(Packet):
|
|
name = "MQTT-SN disconnect request"
|
|
fields_desc = [
|
|
ConditionalField(
|
|
ShortField("duration", None),
|
|
lambda pkt: hasattr(pkt.underlayer, "len") and
|
|
((pkt.underlayer.len is None) or (pkt.underlayer.len > 2))
|
|
),
|
|
]
|
|
|
|
|
|
class MQTTSNWillTopicUpd(Packet):
|
|
name = "MQTT-SN will topic update"
|
|
fields_desc = FLAG_FIELDS + [
|
|
StrLenField("will_topic", "", length_from=_mqttsn_length_from(1)),
|
|
]
|
|
|
|
|
|
class MQTTSNWillTopicResp(Packet):
|
|
name = "MQTT-SN will topic response"
|
|
fields_desc = [
|
|
ByteEnumField("return_code", ACCEPTED, RETURN_CODES),
|
|
]
|
|
|
|
|
|
class MQTTSNWillMsgUpd(Packet):
|
|
name = "MQTT-SN will message update"
|
|
fields_desc = [
|
|
StrLenField("will_msg", "", length_from=_mqttsn_length_from(0))
|
|
]
|
|
|
|
|
|
class MQTTSNWillMsgResp(Packet):
|
|
name = "MQTT-SN will message response"
|
|
fields_desc = [
|
|
ByteEnumField("return_code", ACCEPTED, RETURN_CODES),
|
|
]
|
|
|
|
|
|
class MQTTSNEncaps(Packet):
|
|
name = "MQTT-SN encapsulated message"
|
|
fields_desc = [
|
|
BitField("resvd", 0, 6),
|
|
BitField("radius", 0, 2),
|
|
StrLenField(
|
|
"w_node_id", "",
|
|
length_from=_mqttsn_length_from(1)
|
|
),
|
|
]
|
|
|
|
|
|
# Layer bindings
|
|
bind_bottom_up(UDP, MQTTSN, sport=1883)
|
|
bind_bottom_up(UDP, MQTTSN, dport=1883)
|
|
bind_layers(UDP, MQTTSN, dport=1883, sport=1883)
|
|
bind_layers(MQTTSN, MQTTSNAdvertise, type=ADVERTISE)
|
|
bind_layers(MQTTSN, MQTTSNSearchGW, type=SEARCHGW)
|
|
bind_layers(MQTTSN, MQTTSNGwInfo, type=GWINFO)
|
|
bind_layers(MQTTSN, MQTTSNConnect, type=CONNECT)
|
|
bind_layers(MQTTSN, MQTTSNConnack, type=CONNACK)
|
|
bind_layers(MQTTSN, MQTTSNWillTopicReq, type=WILLTOPICREQ)
|
|
bind_layers(MQTTSN, MQTTSNWillTopic, type=WILLTOPIC)
|
|
bind_layers(MQTTSN, MQTTSNWillMsgReq, type=WILLMSGREQ)
|
|
bind_layers(MQTTSN, MQTTSNWillMsg, type=WILLMSG)
|
|
bind_layers(MQTTSN, MQTTSNRegister, type=REGISTER)
|
|
bind_layers(MQTTSN, MQTTSNRegack, type=REGACK)
|
|
bind_layers(MQTTSN, MQTTSNPublish, type=PUBLISH)
|
|
bind_layers(MQTTSN, MQTTSNPuback, type=PUBACK)
|
|
bind_layers(MQTTSN, MQTTSNPubcomp, type=PUBCOMP)
|
|
bind_layers(MQTTSN, MQTTSNPubrec, type=PUBREC)
|
|
bind_layers(MQTTSN, MQTTSNPubrel, type=PUBREL)
|
|
bind_layers(MQTTSN, MQTTSNSubscribe, type=SUBSCRIBE)
|
|
bind_layers(MQTTSN, MQTTSNSuback, type=SUBACK)
|
|
bind_layers(MQTTSN, MQTTSNUnsubscribe, type=UNSUBSCRIBE)
|
|
bind_layers(MQTTSN, MQTTSNUnsuback, type=UNSUBACK)
|
|
bind_layers(MQTTSN, MQTTSNPingReq, type=PINGREQ)
|
|
bind_layers(MQTTSN, MQTTSNPingResp, type=PINGRESP)
|
|
bind_layers(MQTTSN, MQTTSNDisconnect, type=DISCONNECT)
|
|
bind_layers(MQTTSN, MQTTSNWillTopicUpd, type=WILLTOPICUPD)
|
|
bind_layers(MQTTSN, MQTTSNWillTopicResp, type=WILLTOPICRESP)
|
|
bind_layers(MQTTSN, MQTTSNWillMsgUpd, type=WILLMSGUPD)
|
|
bind_layers(MQTTSN, MQTTSNWillMsgResp, type=WILLMSGRESP)
|
|
bind_layers(MQTTSN, MQTTSNEncaps, type=ENCAPS_MSG)
|
|
bind_layers(MQTTSNEncaps, MQTTSN)
|