Floodlight 防火墙是如何起作用的
前言
用mininet + floodlight搭建好环境之后,运行flooglight,然后在mininet中建立网络拓扑,建好之后,pingall,所有host之间可以ping通。
然后向控制器floodlight中添加防火墙规则,比如
curl -X POST -d '{"src-ip": "10.0.0.1/32", "dst-ip": "10.0.0.2/32","action":"DENY","priority":"10"}' http://localhost:8080/wm/firewall/rules/json
,然后10.0.0.1与10.0.0.2之间就不能ping通了。
那么,这条规则是如何起作用的呢?
注:为简单起见,文中贴的代码片段是在Floodloght源代码文件基础上删减后的。
首先定位到控制数据包转发的类net.floodlightcontroller.forwarding.Forwarding,从该类的processPacketInMessage方法入手。当交换机收到不知道怎么处理的数据包时(即新来的数据包与交换机里现存的所有flow-entry都不匹配),就以packetIn消息的形式将该数据包传给controller。以下方法就是处理packetIn消息的代码:
public Command processPacketInMessage(IOFSwitch sw, OFPacketIn pi, IRoutingDecision decision, FloodlightContext cntx) {
// If a decision has been made we obey it // otherwise we just forward if (decision != null) {switch(decision.getRoutingAction()) { case NONE: // don't do anything return Command.CONTINUE;case FORWARD: doForwardFlow(sw, pi, cntx, false); return Command.CONTINUE; case MULTICAST: // treat as broadcast doFlood(sw, pi, cntx); return Command.CONTINUE; case DROP: doDropFlow(sw, pi, decision, cntx); return Command.CONTINUE; default: } } else { doForwardFlow(sw, pi, cntx, false); } return Command.CONTINUE; }
上述代码表明,decision决定了一个数据包是被forward还是被drop。特别的,当decision为null时,对该packet进行forward或者flood。
所以接下来看哪里调用了processPacketInMessage方法。找到net.floodlightcontroller.routing.ForwardingBase这个类,该类的receive方法是唯一一个调用了processPacketInMessage方法的函数,以下是代码:
@Override public Command receive(IOFSwitch sw, OFMessage msg, FloodlightContext cntx) { switch (msg.getType()) { case PACKET_IN: IRoutingDecision decision = null; if (cntx != null) decision = IRoutingDecision.rtStore.get(cntx, IRoutingDecision.CONTEXT_DECISION);
return this.processPacketInMessage(sw, (OFPacketIn) msg, decision, cntx); default: break; } return Command.CONTINUE; }
上述代码表明decision的值来源于cntx。cntx是一个对象,创建它的类是FloodlightContext,源代码里的注释表示“This is a context object where floodlight listeners can register and later retrieve context information associated with an event”。调用receive的方法有三个,如下:
net.floodlightcontroller.core.internal.Controller.handleMessage(IOFSwitch, OFMessage, FloodlightContext)
net.floodlightcontroller.core.internal.Controller.handleOutgoingMessage(IOFSwitch, OFMessage, FloodlightContext)
net.floodlightcontroller.core.internal.OFSwitchImpl.deliverStatisticsReply(OFMessage)
而cntx的意思大概也明白了:是一个floodlight运行的上下文环境,cntx从floodlight一开始运行的时候就收集一些信息,floodlight里的监听器相关连的事件,如果事件发生,监听器可以从当前的cntx中检索需要的值。可见这个cntx是一个处于线程池中动态更新的对象。
再往下的跟踪超出我的理解范围了(有兴趣的可以自己去跟以下,会跟到net.floodlightcontroller.core.internal.Controller这个类)。可以这么理解:当你向控制器中添加防火墙规则时候,监听器会检测到新事件的产生(添加防火墙规则时,eclipse控制台的突然大量输出可以说明这一点),并将相关信息注册到cntx。
返回来看看processPacketInMessage方法里调用的doDropFlow和doForwardFlow方法。这两个方法也在net.floodlightcontroller.forwarding.Forwarding这个类里。先看doDropFlow方法,代码如下:
protected void doDropFlow(IOFSwitch sw, OFPacketIn pi, IRoutingDecision decision, FloodlightContext cntx) { // initialize match structure and populate it using the packet OFMatch match = new OFMatch(); match.loadFromPacket(pi.getPacketData(), pi.getInPort()); if (decision.getWildcards() != null) { match.setWildcards(decision.getWildcards()); } // Create flow-mod based on packet-in and src-switch OFFlowMod fm = (OFFlowMod) floodlightProvider.getOFMessageFactory().getMessage(OFType.FLOW_MOD); List<OFAction> actions = new ArrayList<OFAction>(); // Set no action to drop long cookie = AppCookie.makeCookie(FORWARDING_APP_ID, 0); fm.setCookie(cookie) .setHardTimeout((short) 0) .setIdleTimeout((short) 5) .setBufferId(OFPacketOut.BUFFER_ID_NONE) .setMatch(match) .setActions(actions) .setLengthU(OFFlowMod.MINIMUM_LENGTH); // +OFActionOutput.MINIMUM_LENGTH); try { messageDamper.write(sw, fm, cntx); } catch (IOException e) { log.error("Failure writing drop flow mod", e); } }
上述代码里核心的一句是messageDamper.write(sw, fm, cntx);这行代码表示往交换机sw里下发流表策略。sw是将这个packet传送给controller的交换机。而下发的这些流表就能实现防火墙规则的功能,禁止10.0.0.1与10.0.0.2之间通信。留意fm和cntx,这两个变量决定了下发的流表是什么样的。
再看doForwardFlow方法。doForwardFlow代码复杂一些,把前人的分析copy过来:
参考自http://lamoop.com/post/2013-11-28/40060265578
首先声明一个OFMatch对象,并将packet-in中的相关信息加载到该对象中:
OFMatch match = new OFMatch(); match.loadFromPacket(pi.getPacketData(), pi.getInPort());
然后通过packet-in消息获取目标和源设备(idevice),即以下代码:
IDevice dstDevice =IDeviceService.fcStore.get(cntx, IDeviceService.CONTEXT_DST_DEVICE); if (dstDevice != null) { IDevice srcDevice = IDeviceService.fcStore. get(cntx, IDeviceService.CONTEXT_SRC_DEVICE);
再然后定位发生packet-in消息的switch所属于的island的标志
Long srcIsland = topology.getL2DomainId(sw.getId());
接下来,判断目标和源设备是否处于同一个island,是则继续,否则执行doFlood方法(类似广播),代码如下:
// Validate that we have a destination known on the same island // Validate that the source and destination are not on the same switchport boolean on_same_island = false; boolean on_same_if = false; for (SwitchPort dstDap : dstDevice.getAttachmentPoints()) { long dstSwDpid = dstDap.getSwitchDPID(); Long dstIsland = topology.getL2DomainId(dstSwDpid); if ((dstIsland != null) && dstIsland.equals(srcIsland)) { on_same_island = true; if ((sw.getId() == dstSwDpid) && (pi.getInPort() == dstDap.getPort())) { on_same_if = true; } break; } } if (!on_same_island) { // Flood since we don't know the dst device if (log.isTraceEnabled()) { log.trace("No first hop island found for destination " + "device {}, Action = flooding", dstDevice); } doFlood(sw, pi, cntx); return; }
如果在同一个island中,则通过getAttachmentPoints方法获取目标和源设备相连的交换机的端口信息
SwitchPort[] srcDaps = srcDevice.getAttachmentPoints(); Arrays.sort(srcDaps, clusterIdComparator); SwitchPort[] dstDaps = dstDevice.getAttachmentPoints(); Arrays.sort(dstDaps, clusterIdComparator);
获取到的结果是两个排序的端口数组,数组的元素类似:
SwitchPort [switchDPID=7, port=2, errorStatus=null]
再接下来进入一个while循环,循环的终止条件是取完上一步得到的两个数组中的元素,目标是找到属于同一个island的分布在两个数组中的元素。循环内部先通过getL2DomainId方法判断,所选取的与目标和源相连接的交换机是否在同一个island,如果不在,则根据两个island标志的大小选择性的选取srcDaps或dstDaps中的其它元素,继续比较,之所以可以这么做,是因为两个数组是经过排序的,而且key就是各个交换机所属的island标志,代码为while循环最后的if-else语句:
} else if (srcVsDest < 0) { iSrcDaps++; } else { iDstDaps++; }
如果找到两个island相同的元素,会调用routingEngine的getRoute方法获取两个端口之间的路由,这才是我们真正关心的流程。这里并未继续跟踪getRoute是如何获取两个交换机端口之间的最短路径(官网提到获取的路由是最短路径),其获取的结果类似:
[[id=00:00:00:00:00:00:00:07, port=2], [id=00:00:00:00:00:00:00:07, port=3], [id=00:00:00:00:00:00:00:05, port=2], [id=00:00:00:00:00:00:00:05, port=3], [id=00:00:00:00:00:00:00:01, port=2], [id=00:00:00:00:00:00:00:01, port=1], [id=00:00:00:00:00:00:00:02, port=3], [id=00:00:00:00:00:00:00:02, port=1], [id=00:00:00:00:00:00:00:03, port=3], [id=00:00:00:00:00:00:00:03, port=1]]
接下来利用最初生成的OFMatch对象信息定义一个规则:
wildcard_hints = ((Integer) sw.getAttribute(IOFSwitch.PROP_FASTWILDCARDS)) .intValue() & ~OFMatch.OFPFW_IN_PORT & ~OFMatch.OFPFW_DL_VLAN & ~OFMatch.OFPFW_DL_SRC & ~OFMatch.OFPFW_DL_DST & ~OFMatch.OFPFW_NW_SRC_MASK & ~OFMatch.OFPFW_NW_DST_MASK;
最后调用pushRoute方法,下发路由策略,即flow信息。
pushRoute(route, match, wildcard_hints, pi, sw.getId(), cookie, cntx, requestFlowRemovedNotifn, false, OFFlowMod.OFPFC_ADD);
继续追踪pushRoute函数,其在ForwardingBase.java中实现,其实就是循环上面route获取到的最短路径,在每个交换机上添加一条flow,包含inport和outport,当然上面提到的wildcard_hints也会通过setMatch方法设置到没条flow中。
pushRoute函数里也有messageDamper.write(sw, fm, cntx);这行代码。作用与doDropFlow方法里的这行代码一样:往交换机里下发流表策略。doDropFlow里是下发阻止某两个主机通信的流表,而这里下发的流表是为了网络中的其他主机可以通信。