Openstack中伪造源地址攻击的防御

背景:Openstack在H版之前的时候,租户可以租用虚拟机发动DoS攻击,很多攻击的方式是伪造源地址。还有别的攻击,如ARP攻击等等,都是通过伪造源MAC或IP地址实现的。本文简单介绍一下openstack中抵御虚假源地址的攻击。

这种攻击的特点是攻击者从虚拟化系统内部发起,攻击目标可能是公网主机,或者是内网的主机。对于前者的防御比较容易,只需要在物理网络边界就可以部署安全设施进行检测和清洗;但是后者的攻击流量大部分是存在于虚拟网络和虚拟网络设备中,物理的安全设备是无法观测或理解物理网络中的流量,无法进行有效的防护,所以对于始于内部的DoS攻击,应该在虚拟化环境中将检测并抵御。
在Openstack的架构下,预计一种检测DoS攻击的方式是依靠检测模块Ceilometer,等它成熟之后,就可及时通知管理员或系统,通过Ceilometer与Neutron协作堵住这种攻击。
不过现在Ceilometer还在初级阶段,所以还是需要依靠网络虚拟化模块Neutron进行防护,好在IaaS系统知道VM的真实IP和MAC,所以可以直接在L2防火墙上对数据包进行过滤,H版已经引入了这部分特性。

下面以一台vm为例:
# neutron port-list|grep 100.0.0.16
| 368d35e1-4db7-405f-af26-1f2b6f224bd8 | | fa:16:3e:34:c8:f8 | {“subnet_id”: “57b30eb5-e9ee-489f-85ea-77bcaa6249e5”, “ip_address”: “100.0.0.16”} |

2013.2.1引入了基于IPTABLES的过滤方案
iptables -L -nvx 可以看到:
Chain neutron-openvswi-s368d35e1-4 (1 references)
pkts bytes target prot opt in out source destination
212 29180 RETURN all — * * 100.0.0.16 0.0.0.0/0 MAC FA:16:3E:34:C8:F8
0 0 DROP all — * * 0.0.0.0/0 0.0.0.0/0
此处允许真实IP和mac的数据包经过,对其他数据包抛弃

2013.2.2 引入了基于EBTABLES的过滤方案
ebtables -t nat -L 可以发现下面项
Bridge chain: libvirt-I-tap368d35e1-4d, entries: 6, policy: ACCEPT
-j I-tap368d35e1-4d-mac
-p IPv4 -j I-tap368d35e1-4d-ipv4-ip
-p IPv4 -j I-tap368d35e1-4d-ipv4
-p ARP -j I-tap368d35e1-4d-arp-mac
-p ARP -j I-tap368d35e1-4d-arp-ip
Bridge chain: I-tap368d35e1-4d-mac, entries: 2, policy: ACCEPT
-s fa:16:3e:34:c8:f8 -j RETURN
-j DROP
Bridge chain: I-tap368d35e1-4d-ipv4-ip, entries: 3, policy: ACCEPT
-p IPv4 –ip-src 0.0.0.0 –ip-proto udp -j RETURN
-p IPv4 –ip-src 100.0.0.16 -j RETURN
-j DROP
Bridge chain: I-tap368d35e1-4d-arp-mac, entries: 2, policy: ACCEPT
-p ARP –arp-mac-src fa:16:3e:34:c8:f8 -j RETURN
-j DROP
Bridge chain: I-tap368d35e1-4d-arp-ip, entries: 2, policy: ACCEPT
-p ARP –arp-ip-src 100.0.0.16 -j RETURN
-j DROP
可见确实将VM的mac和ip进行了过滤,防止其他ip和mac的数据包经过

虽然这种防护可以抵御伪造源地址的攻击,但是无法抵御使用真实源地址的DoS攻击,遇到这种情况,还是采用ceilometer或是SDN的流检测的方式较好。

p.s.1 为什么我会知道这些Openstack版本的防护特性,我会告诉你这些特性让我不能测试DDoS攻击吗…
p.s.2 除了IPTABLES和EBTABLES之外,还有没有其他方法?当然有,设想一下这个场景,VM启动之后,neutron在建立port之后向SDN网络控制器发送通知,网络控制器向OVS发送流指令,允许IP、MAC绑定数据流通过VM所在的交换机端口,然后禁止其他数据流从该端口通过。那为什么不这么做呢,原因1:需要SDN控制器,原因2:数据流先经过Linux bridge,所以不如直接在Linux bridge上应用IPTABLES和EBTABLES。
p.s.4 在SDN+Openstack环境下,租户发起的DoS的第一个受害者将是网络控制器,所以Floodlight在流量控制中做了一些工作,可参见这篇文章

Floodlight的流量控制

floodlight在bigswitch退出opendaylight后算是活过来了,新的版本在负载均衡中加入了一些机制,可以参见net.floodlight.loadbalancer

不过我关心的是FL对流量的控制,特别是判断packet_in速率的机制,这个在前几个月的Floodlight是没有的。

现在FL的处理机制是在收到packet_in数据包后,首先判断是否超过阈值,在inputThrottled方法中,计算每1000个数据包所花的时间,得出速率。如果速率超过packetInRateThresholdHigh阈值,则进入监控限制流量模式(enablePacketInThrottle)

在该模式下,依次检查packet_in数据包的src mac(频率为每50个packet_in一次)和in port(频率为每100个packet_in一次),如果近期(1s内)有该类数据包则将其禁止。

这种方法的初衷应该是防止controller的负载过高,例如禁止mac是为了防止host发送过多包,禁止in port是为了禁止sw转发过多包。但后者有可能出现在恶意vm发动DDoS攻击(一个in port,但无数src mac),客观上却使控制器免受DDoS攻击,自然也让域内的VM被DDoS。

贴一下主干代码:

 
/**
     * Determine if this message should be dropped.
     *
     * We compute the current rate by taking a timestamp every 100 messages.
     * Could change to a more complex scheme if more accuracy is needed.
     *
     * Enable throttling if the rate goes above packetInRateThresholdHigh
     * Disable throttling when the rate drops below packetInRateThresholdLow
     *
     * While throttling is enabled, we do the following:
     *  - Remove duplicate packetIn's mapped to the same OFMatch
     *  - After filtering, if packetIn rate per host (mac) is above
     *    packetInRatePerMacThreshold, push a flow mod to block mac on port
     *  - After filtering, if packetIn rate per port is above
     *    packetInRatePerPortThreshold, push a flow mod to block port
     *  - Allow blocking flow mods have a hard timeout and expires automatically
     *
     * TODO: keep a history of all events related in input throttling
     *
     * @param ofm
     * @return
     */
    @Override
    public boolean inputThrottled(OFMessage ofm) {
        if (ofm.getType() != OFType.PACKET_IN) {
            return false;
        }
        ctrSwitchPktin.updateCounterNoFlush();
        // Compute current packet in rate
        messageCount++;
        if (messageCount % 1000 == 0) {
            long now = System.currentTimeMillis();
            if (now != lastMessageTime) {
                currentRate = (int) (1000000 / (now - lastMessageTime));
                lastMessageTime = now;
            } else {
                currentRate = Integer.MAX_VALUE;
            }
        }
        if (!packetInThrottleEnabled) {
            if (currentRate <= packetInRateThresholdHigh) {
                return false; // most common case
            }
            enablePacketInThrottle();
        } else if (currentRate < packetInRateThresholdLow) {
            disablePacketInThrottle();
            return false;
        }

        // Now we are in the slow path where we need to do filtering
        // First filter based on OFMatch
        OFPacketIn pin = (OFPacketIn)ofm;
        OFMatch match = new OFMatch();
        match.loadFromPacket(pin.getPacketData(), pin.getInPort());
        if (ofMatchCache.update(match)) {
           ctrSwitchPktinDrops.updateCounterNoFlush();
            return true;
        }

        // We have packet in with a distinct flow, check per mac rate
        messageCountUniqueOFMatch++;
        if ((messageCountUniqueOFMatch % packetInRatePerMacThreshold) == 1) {
            checkPerSourceMacRate(pin);
        }

        // Check per port rate
        if ((messageCountUniqueOFMatch % packetInRatePerPortThreshold) == 1) {
            checkPerPortRate(pin);
        }
        return false;
    }

让我们计算一下这种机制的效率,由于阈值packetInRateThresholdHigh设置的是1000,那么now-lastMessageTime应为1000ms=1s;考虑到controller每1000个packet_in进行判断,也就是说入包的极限是每秒1000个packet_in数据包,即1000条新流。这个在小范围内是没问题的,但Nick McKeown等人在经典文章“OpenFlow: Enabling Innovation in Campus Networks”中提到Preliminary results suggested that an Ethane controller based on a low-cost desktop PC could process over 10,000 new flows per second — enough for a large college campus. 显然每秒1k条流的处理规模还是太小,那么单单增加阈值也不能解决问题:因为controller并不知道那些流是正常哪些是异常的,当前场景是小规模的还是大规模的,特别是在一些突然场景下。所以简单的流量控制在一定程度上是有效的,但专门的安全设备也是必需的。

Floodlight路由机制解析

路由部分是floodlight最核心的机制,这两天仔细读了一下floodlight这部分的代码,总算有了大体上的了解,与各位分享。

本文中的floodlight(FL)与控制器/网络控制器(NC, nework controller ) 等术语等同,交换机(SW)默认为openflow-enabled switch,不再赘述。

首先谈一下SDN控制器的路由原理:当交换机收到一个不能被当前流表各条流匹配的数据包时,会把这个数据包以openflow的格式(PACKET_IN)发送给控制器。控制器经过路由决策后,同样以openflow的格式(PACKET_OUT)的方式将该数据包的下一跳信息回给该交换机。

如果学过最短路径计算的同学一定知道,两点之间的最短路径计算首先要知道任意两点之间是否相连,如果两者间有link相连还需知道其长度。所以总体而言,FL路由包括两部分:拓扑更新和路由计算,前者是定时事先完成的,形成一个全局的拓扑结构,后者是收到数据包后运行时完成的,根据拓扑生成路由策略。

首先看拓扑更新。SDN环境中拓扑是集中控制的,故FL需要了解全局的拓扑环境,所以FL在链路更新时自动更新拓扑。

    1 链路更新

当交换机加入SDN环境后,控制器通过LLDP协议定时地获得该交换机与其他设备连接的link信息,然后添加或更新这些link(LinkDiscoveryManager.addOrUpdateLink()),最后将这些link更新的事件添加到updates队列中(LinkDiscoveryManager.handleLldp())。

    2 拓扑更新

计算路由的关键在于路径更新和路由计算,该过程为:
2.1 启动 首先拓扑管理启动(TopologyManager.startUp())时新起一个线程UpdateTopologyWorker,间隔500ms重复运行 。
2.2 更新拓扑 如果之前在第一步中有一些链路、交换机等更新,那么LDUpdates队列中会有元素,那么依次取出这些链路更新的元素(TopologyManager.updateTopology()/TopologyManager.applyUpdates()),判断其类型(如链路更新/删除、端口增加/删除),进行响应处理。
以链路更新为例,调用TopologyManager.addOrUpdateLink()方法,判断链路类型(多跳、单跳或隧道),再把link添加到相应交换机的端口中,如果是多跳的则再删除可能的单跳link
2.3 计算拓扑路径 每次新建一个实例(TopologyManager.createNewInstance()),初始化过程中计算拓扑路径(TopologyInstance.compute()),具体地:
2.3.1 归类 将现有所有相连的link置于同一个簇中,从而形成多个簇结构(identifyOpenflowDomains())
2.3.2 建簇 遍历所有link(addLinksToOpenflowDomains()),如果link连接的是同一个簇,则将其加入该簇,所以该方法统计了所有簇内的link。FL这么做也是减少路由计算的开销,不然若干个大二层网络中共有n个节点,就需要n平方的存储开销,计算效率也会下降,如果将不相连的节点分开就减少了上述开销。
2.3.3 遍历所有簇 对于每个簇内sw,形成一个BroadcastTree结构(包括两个属性:cost记录到其他簇内sw的路径长度,links记录其他sw的link),最终将信息保存到destinationRootedTrees中(caculateShortestPathTreeInClusters ()和calculateBroadcastNodePortsInClusters()),核心思想就是使用dijkstra计算簇内任意两点的最短路径,保存到该点的cost和下一跳。
2.4 定义路径计算模式 这样,FL就获得了从一个点出发所有相连的所有路径和点。那么只要给定一个源SW节点和一个目的SW节点,那就能知道这两者间的最短路径长度和。早在2.3开始TopologyInstance在定期新建实例时,就定义了根据拓扑计算路径的方法:

 
pathcache = CacheBuilder.newBuilder().concurrencyLevel(4)
                    .maximumSize(1000L)
                    .build(
                            new CacheLoader() {
                                public Route load(RouteId rid) {
                                    return pathCacheLoader.load(rid);
                                }
                            });

这里的pathcache是一个类似hash表的结构,每当被调用get时执行pathCacheLoader.load(rid),所以这里没有真正计算路由路径,只是一个注册回调。具体运行时在后面3.6中的getRoute方法中被调用:

 
            result = pathcache.get(id);

在TopologyInstance.buildRoute方法实现该路由rid的计算:先确定目的sw,因为2.3.2中以获得源目的sw的最短路径,然后根据nexthoplinks迭代查找该路径上的所有sw,最终形成一个path。
虽然2.4中没有最终计算节点间的路径,但是2.3中使用dijkstra计算了任意两点间的距离,基本上已经完成了90%的路由计算功能,在大二层网络中这也是不小的开销。

    3 路由计算

当完成拓扑计算后,FL在运行时可计算输入的数据包应走的路由。这里还需说明一下,FL的某些模块监听PACKET_IN数据包,Controller收到这个数据包后,会逐个通知这些模块,调用其receive方法:

 
                    for (IOFMessageListener listener : listeners) {
                        pktinProcTime.recordStartTimeComp(listener);
                        cmd = listener.receive(sw, m, bc);
                        pktinProcTime.recordEndTimeComp(listener);

                        if (Command.STOP.equals(cmd)) {
                            break;
                        }
                    }

更详细的模块加载和监听机制可参考我之前写的这篇文章http://blog.marvelplanet.tk/?p=424
那么回到路由计算这部分来,Controller会依次调用以下模块:
3.1 LinkDiscoveryManager链路处理模块,如果数据包是LLDP处理该消息,如步骤1中在此处会有处理;如果是正常的数据包则略过,所以这里忽略
3.2 TopologyManager拓扑管理模块,查看源端口在TopologyInstance是否允许转发
3.3 DeviceManager设备模块,通过源目SW的id和port相连的device找到源设备和目的设备,如找不到则放弃,否则暂存入一个上下文结构context
3.4 Firewall防火墙模块,判断该数据包的源目的主机是否可以通信,如不在规则表中则继续
3.5 LoadBalancer负载均衡模块,如是ARP数据包则查看是否在vipIPToId中,如不在则继续。这里关系不大,忽略。
3.6 ForwardBase路由模块,首先在上下文context查找之前模块是否已有路由策略,如无则检查是否数据包是广播的,如是则Flood。否则调用doForwardFlow,我们主要看这个方法,具体的,从上下文中获取3.3中解析的源目的主机信息,如果不在同一个iland上(我的理解是这两台主机在FL的知识库中没有路径,也许中间有别的传统方法相连,也许根本不相连),则Flood;如果在同一个端口,那就是同一台主机,则放弃;否则调用routingEngine.getRoute获得路由。这个才是最重要的,也是2.4中没有完成的最终部分,这里使用了TopologyInstance的getRoute方法,使用2.4的方法计算出路由路径,然后调用将路由所对应的路径推送给交换机,如果是多跳路径,则将路由策略推送给该路径上的所有交换机(ForwardBase.pushRoute())。

最后给出一个场景吧,假定h1和h2是主机,s1、s2和s3是交换机,拓扑是h1-s1-s2-s3-h2。当h1 ping h2,首先h1发送ARP包,由于该数据包是广播的,那么最终ForwardBase会执行doFlood,所有交换机都会广播这个数据包,最后h2收到s3广播的的ARP请求后,向h1发送ARP响应。h1收到ARP响应后发送ping请求,该包在交换机以PACKET_IN的形式发送给floodlight,此时floodlight知道h1所在的交换机s3,所以调用doForwardFlow,计算出
s1到s3的路径s1->s2,s2->s3,然后将OFFlowMod命令发送给s1、s2和s3。最终数据包通过s1、s2和s3直接被发送到h2,搞定。

p.s.1 Floodlight在计算拓扑calculateShortestPathTreeInClusters时,首先会将所有的路径置为失效,然后在计算所有两点间距离,如果每次(间隔为500ms)存在一条链路变动,那么就需要重做所有计算,在大二层网络中会不会出现性能瓶颈?
p.s.2 Floodlight的计算出路由后,下发的流只包括源目的主机的mac,流的其他字段都是通配符,这么做的逻辑应该是“我只做路由和转发功能,别的不用控制”。这样会给北向的APP造成很大的麻烦,例如在做DDoS的防御时,需要检查每条流的目的端口,如果流的dst_transport_addr是*的话,显然无法检测攻击,甚至连FL自带的Firewall都力不从心。
p.s. 一般而言,交换机的端口与与其他端口相连,有两个对称的link。但TopologyManager中有一类特殊的端口broadcastDomainPorts,这类是广播端口,不是一般的端口,没有对称的link。

Floodlight REST module

1 download jackson, restlet lib

2 create AntiDDoSResource extending ServerResource

public class AntiDDoSResource extends ServerResource {

	@Get("json")
    public Object handleRequest() {
		ISecurityAppProviderService service = 
                (ISecurityAppProviderService)getContext().getAttributes().
                get(ISecurityAppProviderService.class.getCanonicalName());

        String op = (String) getRequestAttributes().get("op");
        String obj = (String) getRequestAttributes().get("obj");

        // REST API check status
        if (op.equalsIgnoreCase("status")) {
            if (service.isEnabled())
                return "{\"result\" : \"ADS enabled\"}";
            else
                return "{\"result\" : \"ADS disabled\"}";
        }

        // REST API enable firewall
        if (op.equalsIgnoreCase("enable")) {
        	service.run();
            return "{\"status\" : \"success\", \"details\" : \"ADS running\"}";
        } 
        
        // REST API disable firewall
        if (op.equalsIgnoreCase("disable")) {
            service.terminate();
            return "{\"status\" : \"success\", \"details\" : \"ADS stopped\"}";
        } 

        // no known options found
        return "{\"status\" : \"failure\", \"details\" : \"invalid operation: "+op+"/"+obj+"\"}";
    }

3 create a ADSWebRoutable class implementing RestletRoutable

public class ADSWebRoutable implements RestletRoutable {
@Override
public Router getRestlet(Context context) {
Router router = new Router(context);
router.attach("/{op}/json", AntiDDoSResource.class);
router.attach("/{op}/{obj}/json", AntiDDoSResource.class);
return router;
}

/**
* Set the base path for the Firewall
*/
@Override
public String basePath() {
return "/app/ads";
}
}

3 bind the ADSWebRoutable with

Floodlight加载和运行模块的原理

这两天简单看了一下Floodlight的模块机制,大概了解了其插件的机制和流 程,编写了一个非常简单的模块,为大家分享一下
Floodlight加载和运行模块的原理
Floodlight的入口是net.floodlightcontroller.core.Main, 在这个类的main函数中,使用FloodlightModuleLoader加载所有的模块,然后再 调用net.floodlightcontroller.restserver.RestApiServer模块的run方法,启动 rest服务器,最后调用net.floodlightcontroller.core.internal.Controller模块的 run方法,启动网络控制器。
 
   public static void main(String[] args) throws FloodlightModuleException {
        ...
        FloodlightModuleLoader fml = new FloodlightModuleLoader();
        IFloodlightModuleContext moduleContext = fml.loadModulesFromConfig(settings.getModuleFile());
        IRestApiService restApi = moduleContext.getServiceImpl(IRestApiService.class);
        restApi.run();
        IFloodlightProviderService controller =
                moduleContext.getServiceImpl(IFloodlightProviderService.class);
        controller.run();
    }
在加载模块的过程中,使用 FloodlightModuleLoader类的findAllModules方法寻找所有在$CLASSPATH路径中的实现了 net.floodlightcontroller.core.module.IFloodlightModule接口的模块,然后用initModules 初始化这些模块,最后用startupModules启动这些模块。
FloodlightModuleLoad.java:303
    initModules(moduleSet);
    startupModules(moduleSet);
以MemoryStorageSource类为例,它继承了 NoSqlStorageSource类,后者又继承了AbstractStorageSource类,而 AbstractStorageSource是实现了IFloodlightModule和 IStorageSourceService两个接口。
IFloodlightModule有一个init方法和一个startUp方法,所以MemoryStorageSource会分别实现这两个方法,并在初始化和启动的时候被调用。



一个模块的简单实例
下面简单介绍如何新建一个模块的过程,以一个IDSController为例,现在 只实现定时打印日志的功能:
1 定义服务com.nsfocus.ids.IIntrusionDetectionService:
 
public interface IIntrusionDetectionService  extends IFloodlightService {
}
2 新建一个实现IFloodlightModule接口的类com.nsfocus.ids.IDSController:
分别实现接口IFloodlightModule的 getModuleServices、getServiceImpls、getModuleDependencies等方法,这三个方法 主要用于初始化模块
系统运行 ServiceLoader.load(IFloodlightModule.class, cl)后会将IDSController加载到支持IFloodlightModule模块的列表中。

3 在配置文件中添加新模块:
在target\bin\floodlightdefault.properties添加:

net.floodlightcontroller.perfmon.PktInProcessingTime,\
net.floodlightcontroller.ui.web.StaticWebRoutable,\
com.nsfocus.ids.IDSController
net.floodlightcontroller.restserver.RestApiServer.port = 8080


这样新模块在加载时就被添加到集合configMods中
此外,要在bin\META-INF\services\net.floodlightcontroller.core.module.ISecurityControllerModule中添加一行
    com.nsfocus.ids.IDSController
代码在运行到FloodlightModuleLoader.findAllModules的ServiceLoader.load方法时,会加载第二个文件中所列的所有模块,如果只在第一个文件中列出来,而没有在第二个文件中模块列出来的话,会抛出SecurityControllerModuleException的异常。所以一定要在两个文件中都添加该模块。

4 在IDSController中添加相应的功能
4.1 添加初始化代码

@Override
public void init(FloodlightModuleContext context) throws FloodlightModuleException {
    threadPool = context.getServiceImpl(IThreadPoolService.class);
    Map<String, String> configOptions = context.getConfigParams(this); 
    try {
        String detectTimeout = configOptions.get("detecttimeout");
        if (detectTimeout != null) {
            DETECT_TASK_INTERVAL = Short.parseShort(detectTimeout);
        }
     } catch (NumberFormatException e) {
        log.warn("Error parsing detecting timeout, " +
            "using default of {} seconds",
            DETECT_TASK_INTERVAL);
     }
     log.debug("NSFOCUS IDS controller initialized");
}

4.2 添加启动代码
@Override
public void startUp(FloodlightModuleContext context) {
    log.debug("NSFOCUS IDS controller started");
    ScheduledExecutorService ses = threadPool.getScheduledExecutor();
    detectTask = new SingletonTask(ses, new Runnable() {
        @Override
        public void run() {
            try {
                detect();
                detectTask.reschedule(DETECT_TASK_INTERVAL, TimeUnit.SECONDS);
            } catch (Exception e) {
                log.error("Exception in IDS detector", e);
            } finally {
            }
        }
     });
    detectTask.reschedule(DETECT_TASK_INTERVAL, TimeUnit.SECONDS);
}
public void detect(){
   log.debug("Detecting...");
}

这样,floodlight在初始化时自动调用IDSController的 init方法,随后在启动时会自动调用IDSController的startUp方法,上面代码使用了定时器,实现定时触发的功能。

下面是部分启动日志:

 


16:02:51.242 [main] DEBUG n.f.core.internal.Controller – OFListeners for PACKET_IN: linkdiscovery,topology,devicemanager,firewall,forwarding,
16:02:51.302 [main] INFO  n.f.core.internal.Controller – Listening for switch connections on 0.0.0.0/0.0.0.0:6633
16:02:53.611 [debugserver-main] INFO  n.f.jython.JythonServer – Starting DebugServer on port 6655
16:02:55.095 [pool-3-thread-15] DEBUG com.nsfocus.ids.IDSController – Detecting…
16:02:59.096 [pool-3-thread-11] DEBUG com.nsfocus.ids.IDSController – Detecting…

P.S. 其他成员变量声明为:

    protected static Logger log = LoggerFactory.getLogger(IDSController.class);
    protected SingletonTask detectTask;
    protected int DETECT_TASK_INTERVAL = 2;
    protected IThreadPoolService threadPool;