源码解读ODL的MAC地址学习(一)

2021/5/1 14:25:19

本文主要是介绍源码解读ODL的MAC地址学习(一),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1 简介

我们知道同一子网中主机之间互相传送信息需要用到MAC地址,而我们第一次发送信息的时候只有IP地址而没有MAC地址,所以我们就要进行MAC地址自学习。

交换机中的MAC地址自学习是指在交换机中有一个MAC地址与交换机每个接口的对应表,每当有数据包经过交换机转发的时候,如果它的表中没有这个MAC地址的对应关系就会往所有端口转发数据包,当目标机从某个端口返回信息的时候它就知道了这个MAC地址对应的哪个端口,于是会把这个对应关系加入表中,这个过程就是交换机的MAC地址自学习。

2 ODL中MAC地址原理

前文已经介绍了MAC地址自学的一般过程,在ODL中,我们监控ARP数据包来学习MAC地址,将ARP数据包通过Packet-In消息发给OpenFlow控制器,那么就可以在不过度加重OpenFlow控制器负载的情况下,实现MAC地址学习。

ODL MAC地址学习的源码主要集中在l2switch和OpenFlowPlugin模块:

图片

ODL MAC地址学习的源码主要集中在l2switch和OpenFlowPlugin模块:

红框标注的内容就是l2switch项目中和MAC地址学习有关的,未标注的部分是链路和节点发现;OpenFlow项目中涉及到MAC地址学习的不多,只有一个定义Packet-In和Packet-Out的YANG文件:packet-processing.yang。下面我们会从ARP处理流程出发,具体分析源码,由于篇幅有限,源码中不是很重要的部分没有贴出来,要看完整的源码请下面网址下载:

https://github.com/opendaylight/l2switch/tree/stable/beryllium

https://github.com/opendaylight/openflowplugin/tree/stable/beryllium

同时重要代码的具体分析直接在注释里面。

2.1 ARP处理流程

我们设想一种场景,如图所示:

图片


PC A准备向IPv4地址为10.0.0.2的目标地址发送数据包。此时,假设PC A的ARP表中不存在10.0.0.2这一表项,那么PC A不知道10.0.0.2这一IPv4地址对应的以太网地址,所以准备使用ARP解决该问题。

PC A通过L2广播发送ARP请求,PC B接收该请求后发送ARP响应。PC A接收到ARP响应后将PC B的以太网地址存储到ARP表中。这样,PC A就可以向10.0.0.2这一IPv4地址发送数据包。

以上就是ARP的处理流程,下面我们对流程中的细节进行相应的阐述。

2.2 ARP请求(Packet-In)

首先,PC A发送ARP请求,由OpenFlow交换机接收到该请求,OpenFlow交换机接收到PC A发来的ARP请求之后,检索流表,此时的流表是通过Proactive模式加载的流表,其中的流表项的匹配字段是入端口,行动是上报控制器,这个流表项的具体实现代码存在于l2switch项目中arphandler目录下的ProactiveFloodFlowWriter.java:

Match match = new MatchBuilder()    //匹配端口

          .setInPort(nc.getId())

          .build();

设置匹配字段为入端口。

if(outerSaNodeConnector == null) {

  outputActions.add(new ActionBuilder()

.setOrder(0)

.setKey(new ActionKey(0))

.setAction(new OutputActionCaseBuilder()

.setOutputAction(new OutputActionBuilder()

.setMaxLength(0xffff)

.setOutputNodeConnector(new Uri(OutputPortValues.CONTROLLER.toString()))

.build())

.build())

.build());

}

设置OutPut的action为controller,即上报给控制器。

流表项匹配成功之后,OpenFlow交换机执行通过Packet-In消息向OpenFlow控制器转发ARP请求帧的行动。此时ODL控制器就要做两个处理,一是接受Packet-In消息,而是对Packet-In消息进行解码。

首先看一下Packet-In消息的接收。Packet-In消息的定义YANG文件存在于OpenFlowPlugin项目中的packet-processing.yang:

notification packet-received {

        description "Delivery of incoming packet wrapped in openflow structure.";

     leaf connection-cookie {

            type connection-cookie;

        }

        leaf flow-cookie {

            type flow-type:flow-cookie;

        }

        leaf table-id {

         type table-type:table-id;

        }

        leaf packet-in-reason {

     type identityref {

     base packet-in-reason;

     }

    }

        container match {

         uses match-type:match;

        }

        uses raw-packet;

}

将收到的Packet-In的数据包定义成notification,那么编译之后就会出现PacketProcessingListener.java接口类,为了订阅这个消息来对收到的数据包进行处理,在代码中就要实现这个接口类(implements PacketProcessingListener)。

下面我们来分析对Packet-In数据包的解码,代码的实现逻辑是:先将packet-received数据包解码成ethernet-packet数据包,再将ethernet-packet数据包解码成相应的arp-packet、ipv4-packet和ipv6-packet数据包。具体的实现代码存在于l2switch项目中的packethandler这个目录下。首先我们分析YANG文件:

首先是packet.yang:

grouping packet-chain-grp {

    list packet-chain {

      choice packet {

         case raw-packet {

           uses raw-packet-fields;

         }

      }

    }

  }

这个YANG文件定义了基本数据包,便于具体数据包的YANG文件扩展,该YANG文件中比较重要的就是这个grouping,其他YANG文件扩展的就是packet-chain。同时这个YANG文件还有一点需要注意:这个YANG文件的文件名是packet.yang,但是它的module名是base-packet,所以import的时候用的是base-packet,而不是packet。

下面我们分析ethernet-packet.yang,至于arp-packet.yang,ipv4-packet.yang和ipv6-packet.yang文件和这个文件差不多,就不多做分析了。

ethernet-packet.yang:

import base-packet {

  prefix bpacket;

  revision-date 2014-05-28;

}

……

notification ethernet-packet-received {

  uses bpacket:packet-chain-grp {

    augment "packet-chain/packet" {

      case ethernet-packet {

        uses ethernet-packet-fields;

      }

    }

  }

  uses bpacket:packet-payload;

}

该YANG文件中最重要的就是这个notification,从上述代码中可以发现它扩展了packet.yang中packet-chain中的packet,添加了一个情况:ethernet-packet。

分析完了YANG文件,下面我们来看下具体的解码java代码,前面提到过,解码分为两个步骤,先将packet-received数据包解码成ethernet-packet数据包,再将ethernet-packet数据包解码成相应的arp-packet、ipv4-packet和ipv6-packet数据包。下面我们分析三个java文件:

AbstarctPacketDecoder.java:

public abstract class AbstractPacketDecoder

    implements  NotificationProviderService.NotificationInterestListener , AutoCloseable {  //NotificationInterestListener是一个监听者可能感兴趣的notification

 //ConsumedPacketNotification作为解码的notification

 //ProducedPacketNotification作为解码后的notification被发出去


 /**

   * Keeps track of listeners registered for the notification that a decoder produces.

   * @param aClass

   */ 

  @Override

  public synchronized void onNotificationSubscribtion(Class

    if (aClass !=null && aClass.equals(producedPacketNotificationType)) {

      if(listenerRegistration == null) {

        NotificationListener notificationListener = getConsumedNotificationListener();

        listenerRegistration = notificationProviderService.registerNotificationListener(notificationListener);//注册监听器

      }

    }

  }


 /**

   * Every extended decoder should call this method on a receipt of a input packet notification.

   * This method would make sure it decodes only when necessary and publishes corresponding event

   * on successful decoding.

   */

  public void decodeAndPublish(final ConsumedPacketNotification consumedPacketNotification) {

    decodeAndPublishExecutor.submit(new Runnable() {

        @Override

        public void run() {

            ProducedPacketNotification packetNotification=null;

            if(consumedPacketNotification!= null && canDecode(consumedPacketNotification)) {

              packetNotification = decode(consumedPacketNotification);  //将ConsumedPacketNotification解码,解码后的类型是ProducedPacketNotification

            }

            if(packetNotification != null) {

              notificationProviderService.publish(packetNotification); ////将ConsumedPacketNotification解码后的再发出去 

            }

        }

    });

  }

public abstract ProducedPacketNotification decode(ConsumedPacketNotification consumedPacketNotification);  //定义一个抽象的解码函数,便于其他java类覆写

}

这个文件定义了一个抽象java类,用于实现注册监听器、将ConsumedPacketNotification消息解码成ProducedPacketNotification类型的消息,并把这个消息发出去。这个java类中并没有实现具体的解码过程,只定义了一个抽象的decode()方法,便于其他的java类覆写。

EthernetDecoder.java:

//实现PacketProcessingListener接口,为了监听Packet-ProcessingYANG中的PacketReceived这个notification

public class EthernetDecoder extends AbstractPacketDecoder

  ......


  public EthernetDecoder(NotificationProviderService notificationProviderService) {

    super(EthernetPacketReceived.class, notificationProviderService);

  }

  //重写AbstarctPacketDecoder类中的decodeAndPublish()函数

  //即将RawPacket解码成EthernetPacket类型的消息并发出去

  @Override

  public void onPacketReceived(PacketReceived packetReceived) {

    decodeAndPublish(packetReceived);  //重写AbstarctPacketDecoder类中的decodeAndPublish()函数,即将RawPacket解码成EthernetPacket类型的消息

  }


  //将PacketReceived到的RawPacket解码成EthernetPacket

  @Override

  public EthernetPacketReceived decode(PacketReceived packetReceived) {

    byte[] data = packetReceived.getPayload();

    EthernetPacketReceivedBuilder builder = new EthernetPacketReceivedBuilder();


    //创建 RawPacket实例

    RawPacketBuilder rpb = new RawPacketBuilder()

        .setIngress(packetReceived.getIngress())

        .setConnectionCookie(packetReceived.getConnectionCookie())

        .setFlowCookie(packetReceived.getFlowCookie())

        .setTableId(packetReceived.getTableId())

        .setPacketInReason(packetReceived.getPacketInReason())

        .setPayloadOffset(0)

        .setPayloadLength(data.length);

    if(packetReceived.getMatch() != null ){

        rpb.setMatch(new MatchBuilder(packetReceived.getMatch()).build());

    }

    RawPacket rp = rpb.build();

    ArrayList

    packetChain.add(new PacketChainBuilder()

      .setPacket(rp)

      .build());

    //将RawPacket解码成EthernetPacket

    try {

      EthernetPacketBuilder epBuilder = new EthernetPacketBuilder();//定义一个EthernetPacket实例


      epBuilder.setDestinationMac(new MacAddress(HexEncode.bytesToHexStringFormat(BitBufferHelper.getBits(data, 0, 48))));//得到目的MAC地址

      epBuilder.setSourceMac(new MacAddress(HexEncode.bytesToHexStringFormat(BitBufferHelper.getBits(data, 48, 48))));//得到源MAC地址


      // 将可选字段802.1Q反序列化

      Integer nextField = BitBufferHelper.getInt(BitBufferHelper.getBits(data, 96, 16));

      int extraHeaderBits = 0;

      ArrayList

      while(nextField.equals(ETHERTYPE_8021Q) || nextField.equals(ETHERTYPE_QINQ)) {

        Header8021qBuilder hBuilder = new Header8021qBuilder();

        hBuilder.setTPID(Header8021qType.forValue(nextField));


        // Read 2 more bytes for priority (3bits), drop eligible (1bit), vlan-id (12bits)

        byte[] vlanBytes = BitBufferHelper.getBits(data, 112 + extraHeaderBits, 16);


        // Remove the sign & right-shift to get the priority code

        hBuilder.setPriorityCode((short) ((vlanBytes[0] & 0xff) >> 5));


        // Remove the sign & remove priority code bits & right-shift to get drop-eligible bit

        hBuilder.setDropEligible(1 == (((vlanBytes[0] & 0xff) & 0x10) >> 4));


        // Remove priority code & drop-eligible bits, to get the VLAN-id

        vlanBytes[0] = (byte) (vlanBytes[0] & 0x0F);

        hBuilder.setVlan(new VlanId(BitBufferHelper.getInt(vlanBytes)));


        // Add 802.1Q header to the growing collection

        headerList.add(hBuilder.build());


        // Reset value of "nextField" to correspond to following 2 bytes for next 802.1Q header or EtherType/Length

        nextField = BitBufferHelper.getInt(BitBufferHelper.getBits(data, 128 + extraHeaderBits, 16));


        // 802.1Q header means payload starts at a later position

        extraHeaderBits += 32;

      }

      // Set 802.1Q headers

      if(!headerList.isEmpty()) {

        epBuilder.setHeader8021q(headerList);

      }


      // Deserialize the EtherType or Length field

      if(nextField >= ETHERTYPE_MIN) {

        epBuilder.setEthertype(KnownEtherType.forValue(nextField));

      } else if(nextField <= LENGTH_MAX) {

        epBuilder.setEthernetLength(nextField);

      } else {

        _logger.debug("Undefined header, value is not valid EtherType or length.  Value is " + nextField);

      }


      // Determine start & end of payload

      int payloadStart = ( 112 + extraHeaderBits) / NetUtils.NumBitsInAByte;

      int payloadEnd = data.length - 4;

      epBuilder.setPayloadOffset(payloadStart);

      epBuilder.setPayloadLength(payloadEnd-payloadStart);


      // Deserialize the CRC

      epBuilder.setCrc(BitBufferHelper.getLong(BitBufferHelper.getBits(data, (data.length - 4) * NetUtils.NumBitsInAByte, 32)));


      // Set EthernetPacket field

      packetChain.add(new PacketChainBuilder()

        .setPacket(epBuilder.build())

        .build());


      // Set Payload field

      builder.setPayload(data);

    } catch(BufferException be) {

      _logger.info("Exception during decoding raw packet to ethernet.");

    }

    builder.setPacketChain(packetChain);

    return builder.build();

  }

  .......

}

这个类监听PacketReceived消息,当收到数据包时,将此时的数据包作为RawPacket并解码成EthernetPacket数据包,最后将EthernetPacketReceived消息发出去,这里面的PacketReceived数据包就是Packet-In数据包,这个的定义YANG存在与OpenFlowPlugin项目中,我们在前面已经分析过了,这里就不多做赘述。

ArpDecoder.java

//实现EthernetPacketListener接口,为了监听ethernet-packet YANG中的ethernet-packet-received这个notification

public class ArpDecoder extends AbstractPacketDecoder

    implements EthernetPacketListener {    

   ......

  ////重写AbstarctPacketDecoder类中的decodeAndPublish()函数

  //即将EthernetPacket解码成ArpPacket类型的消息并发出去

  @Override

  public ArpPacketReceived decode(EthernetPacketReceived ethernetPacketReceived) {

    ArpPacketReceivedBuilder arpReceivedBuilder = new ArpPacketReceivedBuilder();


    // 得到packet-chain中的最后一个数据包,这就是以太网数据包

    List

    EthernetPacket ethernetPacket = (EthernetPacket)packetChainList.get(packetChainList.size()-1).getPacket();

    int bitOffset = ethernetPacket.getPayloadOffset() * NetUtils.NumBitsInAByte;

    byte[] data = ethernetPacketReceived.getPayload();  //有效载荷


    ArpPacketBuilder builder = new ArpPacketBuilder();  //创建ArpPacket实例

    try {

      // Decode the hardware-type (HTYPE) and protocol-type (PTYPE) fields

      builder.setHardwareType(KnownHardwareType.forValue(BitBufferHelper.getInt(BitBufferHelper.getBits(data, bitOffset + 0, 16))));  //硬件类型

      builder.setProtocolType(KnownEtherType.forValue(BitBufferHelper.getInt(BitBufferHelper.getBits(data, bitOffset+16, 16))));   //协议类型


      // Decode the hardware-length and protocol-length fields

      builder.setHardwareLength(BitBufferHelper.getShort(BitBufferHelper.getBits(data, bitOffset+32, 8)));  //硬件地址长度

      builder.setProtocolLength(BitBufferHelper.getShort(BitBufferHelper.getBits(data, bitOffset+40, 8)));   //协议地址长度


      // Decode the operation field

      builder.setOperation(KnownOperation.forValue(BitBufferHelper.getInt(BitBufferHelper.getBits(data, bitOffset+48, 16))));  //ar-op


      // Decode the address fields

      //硬件地址

      int indexSrcProtAdd = 64 + 8 * builder.getHardwareLength();

      int indexDstHardAdd = indexSrcProtAdd + 8 * builder.getProtocolLength();

      int indexDstProtAdd = indexDstHardAdd + 8 * builder.getHardwareLength();

      if(builder.getHardwareType().equals(KnownHardwareType.Ethernet)) {

        builder.setSourceHardwareAddress(HexEncode.bytesToHexStringFormat(BitBufferHelper.getBits(data, bitOffset + 64, 8 * builder.getHardwareLength())));

        builder.setDestinationHardwareAddress(HexEncode.bytesToHexStringFormat(BitBufferHelper.getBits(data, bitOffset + indexDstHardAdd, 8 * builder.getHardwareLength())));

      } else {

        _logger.debug("Unknown HardwareType -- sourceHardwareAddress and destinationHardwareAddress are not decoded");

      }

      //IP地址

      if(builder.getProtocolType().equals(KnownEtherType.Ipv4) || builder.getProtocolType().equals(KnownEtherType.Ipv6)) {

        builder.setSourceProtocolAddress(InetAddress.getByAddress(BitBufferHelper.getBits(data, bitOffset + indexSrcProtAdd, 8 * builder.getProtocolLength())).getHostAddress());

        builder.setDestinationProtocolAddress(InetAddress.getByAddress(BitBufferHelper.getBits(data, bitOffset + indexDstProtAdd, 8 * builder.getProtocolLength())).getHostAddress());

      } else {

        _logger.debug("Unknown ProtocolType -- sourceProtocolAddress and destinationProtocolAddress are not decoded");

      }

    } catch(BufferException | UnknownHostException e) {

      _logger.debug("Exception while decoding APR packet", e.getMessage());

    }


    //build arp

    packetChainList.add(new PacketChainBuilder()

      .setPacket(builder.build())

      .build());

    arpReceivedBuilder.setPacketChain(packetChainList);


    // carry forward the original payload.

    arpReceivedBuilder.setPayload(ethernetPacketReceived.getPayload());


    return arpReceivedBuilder.build();  

  }

  ......

}

由于ARP报文存在与以太网的报文的帧头部,所以为了得到ARP报文,就要订阅EthernetPacketReceived消息,所以ArpDecoder这个类实现了EthernetPacketListener接口。ARP报文的格式如下图所示:

图片

具体的解码过程就是根据的ARP报文格式,具体的代码解释在注释中,这里就不多做赘述了。

EthernetPacket数据包也可以解码成Ipv4Packet和Ipv6Pcket数据包,相应的实现代码在Ipv4Decoder.java和Ipv6Decoder.java中,代码逻辑类似,这里因为篇幅限制就不多做解释,感兴趣的可以私信我。

以上就是收到ARP请求,并将相应的数据包解码成ArpPacket、Ipv4Packet和Ipv6Pcket数据包。

2.3 ARP请求(Packet-Out)

为了将来自PC A的ARP请求广播至OpenFlow网络内,ODL控制器将Packet-Out消息发送至OpenFlow交换机,接受到Packet-Out消息的OpenFlow交换机,将ARP请求广播至端口2和端口3。具体的实现代码在l2switch项目中的arphandler目录下。

 

首先我们来分析ArpPacketHandler.java,这个java类是这个部分的核心。




这篇关于源码解读ODL的MAC地址学习(一)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程