Ryu:如何在LLDP中添加自定义LLDPDU

时间:2022-05-06
本文章向大家介绍Ryu:如何在LLDP中添加自定义LLDPDU,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

在许多实验场景中,都需要使用链路发现协议(LLDP)来发现链路,从而构建网络拓扑。然而LLDP协议不仅仅可以用来发现拓扑,也可以用于时延检测等业务。LLDP通过添加对应的TLV格式的LLDPDU(LLDP数据单元)来携带对应的信息,从而为上层业务提供信息支撑。为实现LLDP数据单元的拓展,本文将以Ryu控制器为例,介绍如何添加自定义的LLDPDU,从而满足多种业务的需求。

添加自定义LLDPDU其实只需修改ryu/lib/packet/lldp.py即可,但是由于该文件仅定义了LLDP的相关类,如何使用还需要其他文件去调用,所以还需要其他的修改步骤。具体步骤将在文章后续介绍。

修改lldp.py文件

ryu/lib/packet/lldp.py文件是Ryu控制器中关于LLDP协议数据类的描述,其中定义了如LLDPBasicTLV类等重要的报文类。 以添加发送时间戳的TLV为例,我们需要完成TLV类型号的声明,以及TLV类的定义。

在文件开头处有关于LLDP TLV类型的声明,所以首先我们需要添加一个新的类型:LLDP_TLV_SEND_TIME,其类型号为11。

然后设计此类型的LLDPDU格式,其格式仅包含一个长度为8字节的Double类型的时间戳数据。如何完成类的描述,可以参考TTL类,具体代码如下。

TimeStamp类中定义了该LLDPDU的格式,初始化函数以及序列化函数。

修改switches.py

完成LLDPDU的定义之后,还需要在某文件中对其进行初始化构造。如果另外重新编写一个LLDP的构造、发送以及接受解析模块,那么则需要重新写许多代码,所以此处推荐直接修改Ryu/topology/switches.py文件。

switches.py文件中的LLDPPacket类完成了LLDP数据包的初始化和序列化实现。

该类的lldp_packet方法可以构造LLDP数据包,并返回序列化之后的数据。在此函数中,我们需要添加timestamp的TLV。

在lldp_parse方法中,需将获取到的字节流的数据解析为对应的LLDP数据包。由于在发送之前,我们加入了一个timestamp的TLV,所以解析时需要完成这个TLV的解析,并将TimeStamp作为返回值返回。


py
    class LLDPPacket(object):
        # make a LLDP packet for link discovery.
        CHASSIS_ID_PREFIX = 'dpid:'
        CHASSIS_ID_PREFIX_LEN = len(CHASSIS_ID_PREFIX)
        CHASSIS_ID_FMT = CHASSIS_ID_PREFIX + '%s'
        PORT_ID_STR = '!I'      # uint32_t
        PORT_ID_SIZE = 4
        DOMAIN_ID_PREFIX = 'domain_id:'
        DOMAIN_ID_PREFIX_LEN = len(DOMAIN_ID_PREFIX)
        DOMAIN_ID_FMT = DOMAIN_ID_PREFIX + '%s'
        VPORT_ID_STR = '!I'      # uint32_t
        VPORT_ID_SIZE = 4
        class LLDPUnknownFormat(RyuException):
            message = '%(msg)s'
        @staticmethod
        def lldp_packet(dpid, port_no, dl_addr, ttl, timestamp,
                        vport_no=ofproto_v1_0.OFPP_NONE):
            pkt = packet.Packet()
            dst = lldp.LLDP_MAC_NEAREST_BRIDGE
            src = dl_addr
            ethertype = ETH_TYPE_LLDP
            eth_pkt = ethernet.ethernet(dst, src, ethertype)
            pkt.add_protocol(eth_pkt)
            tlv_chassis_id = lldp.ChassisID(
                subtype=lldp.ChassisID.SUB_LOCALLY_ASSIGNED,
                chassis_id=LLDPPacket.CHASSIS_ID_FMT %
                dpid_to_str(dpid))
            tlv_port_id = lldp.PortID(subtype=lldp.PortID.SUB_PORT_COMPONENT,
                                      port_id=struct.pack(
                                          LLDPPacket.PORT_ID_STR,
                                          port_no))
            tlv_ttl = lldp.TTL(ttl=ttl)
            tlv_timestamp = lldp.TimeStamp(timestamp=timestamp)
            tlv_end = lldp.End()
            tlvs = (tlv_chassis_id, tlv_port_id, tlv_ttl, tlv_timestamp, tlv_end)
            lldp_pkt = lldp.lldp(tlvs)
            pkt.add_protocol(lldp_pkt)
            pkt.serialize()
            return pkt.data
        @staticmethod
        def lldp_parse(data):
            pkt = packet.Packet(data)
            i = iter(pkt)
            eth_pkt = i.next()
            assert type(eth_pkt) == ethernet.ethernet
            lldp_pkt = i.next()
            if type(lldp_pkt) != lldp.lldp:
                raise LLDPPacket.LLDPUnknownFormat()
            tlv_chassis_id = lldp_pkt.tlvs[0]
            if tlv_chassis_id.subtype != lldp.ChassisID.SUB_LOCALLY_ASSIGNED:
                raise LLDPPacket.LLDPUnknownFormat(
                    msg='unknown chassis id subtype %d' % tlv_chassis_id.subtype)
            chassis_id = tlv_chassis_id.chassis_id
            if not chassis_id.startswith(LLDPPacket.CHASSIS_ID_PREFIX):
                raise LLDPPacket.LLDPUnknownFormat(
                    msg='unknown chassis id format %s' % chassis_id)
            src_dpid = str_to_dpid(chassis_id[LLDPPacket.CHASSIS_ID_PREFIX_LEN:])
            tlv_port_id = lldp_pkt.tlvs[1]
            if tlv_port_id.subtype != lldp.PortID.SUB_PORT_COMPONENT:
                raise LLDPPacket.LLDPUnknownFormat(
                    msg='unknown port id subtype %d' % tlv_port_id.subtype)
            port_id = tlv_port_id.port_id
            if len(port_id) != LLDPPacket.PORT_ID_SIZE:
                raise LLDPPacket.LLDPUnknownFormat(
                    msg='unknown port id %d' % port_id)
            (src_port_no, ) = struct.unpack(LLDPPacket.PORT_ID_STR, port_id)
            tlv_timestamp = lldp_pkt.tlvs[3]
            timestamp = tlv_timestamp.timestamp
            return src_dpid, src_port_no, timestamp

到此为止,完成了LLDP的构造和解析的定义。但是由于修改了构造函数的参数列表,和解析函数的返回值,所以在构造LLDP数据包和解析LLDP数据包时,均需要做一些改动。示例代码如下:

py
    def _port_added(self, port):
        _time = time.time()
        lldp_data = LLDPPacket.lldp_packet(port.dpid, port.port_no,
                                           port.hw_addr, self.DEFAULT_TTL, _time)
py
        @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
        def packet_in_handler(self, ev):
            if not self.link_discovery:
                return
            msg = ev.msg
            try:
                src_dpid, src_port_no, timestamp = LLDPPacket.lldp_parse(msg.data)
            except LLDPPacket.LLDPUnknownFormat as e:
                # This handler can receive all the packtes which can be
                # not-LLDP packet. Ignore it silently
                return

此处需要提醒读者的是,在Ryu的Switches模块中,被发送的LLDP都是一次构造之后保存起来,发送时直接发送的,所以添加的时间戳会固定在第一次构造时的时间。所以如果希望正确地插入发送时间戳,还需要进行额外的逻辑修改。但是这也许就破坏了Ryu设计的完整性,所以如何操作还需要读者自行斟酌。

然而,像VPort_ID之类的不随时间而改变的TLV,则可以直接使用。添加VPort_ID的步骤和以上的例子同理,其VPort_ID类的示例代码如下所示:

py

    @lldp.set_tlv_type(LLDP_TLV_VPORT_ID)
    class VPortID(LLDPBasicTLV):
        _PACK_STR = '!B'
        _PACK_SIZE = struct.calcsize(_PACK_STR)
        # subtype id(1 octet) + port id length(1 - 255 octet)
        _LEN_MIN = 2
        _LEN_MAX = 256
        # VPort ID subtype
        SUB_INTERFACE_ALIAS = 1     # ifAlias (IETF RFC 2863)
        SUB_PORT_COMPONENT = 2      # entPhysicalAlias (IETF RFC 4133)
        SUB_MAC_ADDRESS = 3         # MAC address (IEEE Std 802)
        SUB_NETWORK_ADDRESS = 4     # networkAddress
        SUB_INTERFACE_NAME = 5      # ifName (IETF RFC 2863)
        SUB_AGENT_CIRCUIT_ID = 6    # agent circuit ID(IETF RFC 3046)
        SUB_LOCALLY_ASSIGNED = 7    # local
        def __init__(self, buf=None, *args, **kwargs):
            super(VPortID, self).__init__(buf, *args, **kwargs)
            if buf:
                (self.subtype, ) = struct.unpack(
                    self._PACK_STR, self.tlv_info[:self._PACK_SIZE])
                self.vport_id = self.tlv_info[self._PACK_SIZE:]
            else:
                self.subtype = kwargs['subtype']
                self.vport_id = kwargs['vport_id']
                self.len = self._PACK_SIZE + len(self.vport_id)
                assert self._len_valid()
                self.typelen = (self.tlv_type << LLDP_TLV_TYPE_SHIFT)|self.len
        def serialize(self):
            return struct.pack('!HB', self.typelen, self.subtype) +self.vport_id

总结

LLDP协议可添加自定义TLV格式的特性,使其可以灵活地被修改,进而应用到不同的业务场景中,十分方便。本文就以Ryu控制器为例,介绍了如何添加自定义LLDPDU的详细流程,希望对读者有一定的帮助。此外,为计算时延,还可以通过switches模块中的PortDatak类的发送时间戳来实现,无需修改LLDP数据包格式。如何在Ryu中完成时延测试的内容将在下一篇文章中详细介绍,敬请关注。