Floodlight 防火墙是如何起作用的

时间:2024-01-18 20:36:38

前言

用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里是下发阻止某两个主机通信的流表,而这里下发的流表是为了网络中的其他主机可以通信。