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类是这个部分的核心。