blob: 3f8ff6c2116277c6388272ed17e749b0fec66a33 [file] [log] [blame]
package net.floodlightcontroller.firewall;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.openflow.protocol.OFMessage;
import org.openflow.protocol.OFPacketIn;
import org.openflow.protocol.OFType;
import net.floodlightcontroller.core.FloodlightContext;
import net.floodlightcontroller.core.IOFMessageListener;
import net.floodlightcontroller.core.IOFSwitch;
import net.floodlightcontroller.core.module.FloodlightModuleContext;
import net.floodlightcontroller.core.module.FloodlightModuleException;
import net.floodlightcontroller.core.module.IFloodlightModule;
import net.floodlightcontroller.core.module.IFloodlightService;
import net.floodlightcontroller.core.IFloodlightProviderService;
import net.floodlightcontroller.devicemanager.IDeviceService;
import java.util.ArrayList;
import net.floodlightcontroller.packet.Ethernet;
import net.floodlightcontroller.packet.IPv4;
import net.floodlightcontroller.restserver.IRestApiService;
import net.floodlightcontroller.routing.IRoutingDecision;
import net.floodlightcontroller.routing.RoutingDecision;
import net.floodlightcontroller.storage.IResultSet;
import net.floodlightcontroller.storage.IStorageSourceService;
import net.floodlightcontroller.storage.StorageException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Stateless firewall implemented as a Google Summer of Code project.
* Configuration done through REST API
*
* @author Amer Tahir
* @edited KC Wang
*/
public class Firewall implements IFirewallService, IOFMessageListener,
IFloodlightModule {
// service modules needed
protected IFloodlightProviderService floodlightProvider;
protected IStorageSourceService storageSource;
protected IRestApiService restApi;
protected static Logger logger;
protected List<FirewallRule> rules; // protected by synchronized
protected boolean enabled;
protected int subnet_mask = IPv4.toIPv4Address("255.255.255.0");
// constant strings for storage/parsing
public static final String TABLE_NAME = "controller_firewallrules";
public static final String COLUMN_RULEID = "ruleid";
public static final String COLUMN_DPID = "dpid";
public static final String COLUMN_IN_PORT = "in_port";
public static final String COLUMN_DL_SRC = "dl_src";
public static final String COLUMN_DL_DST = "dl_dst";
public static final String COLUMN_DL_TYPE = "dl_type";
public static final String COLUMN_NW_SRC_PREFIX = "nw_src_prefix";
public static final String COLUMN_NW_SRC_MASKBITS = "nw_src_maskbits";
public static final String COLUMN_NW_DST_PREFIX = "nw_dst_prefix";
public static final String COLUMN_NW_DST_MASKBITS = "nw_dst_maskbits";
public static final String COLUMN_NW_PROTO = "nw_proto";
public static final String COLUMN_TP_SRC = "tp_src";
public static final String COLUMN_TP_DST = "tp_dst";
public static final String COLUMN_WILDCARD_DPID = "wildcard_dpid";
public static final String COLUMN_WILDCARD_IN_PORT = "wildcard_in_port";
public static final String COLUMN_WILDCARD_DL_SRC = "wildcard_dl_src";
public static final String COLUMN_WILDCARD_DL_DST = "wildcard_dl_dst";
public static final String COLUMN_WILDCARD_DL_TYPE = "wildcard_dl_type";
public static final String COLUMN_WILDCARD_NW_SRC = "wildcard_nw_src";
public static final String COLUMN_WILDCARD_NW_DST = "wildcard_nw_dst";
public static final String COLUMN_WILDCARD_NW_PROTO = "wildcard_nw_proto";
public static final String COLUMN_WILDCARD_TP_SRC = "wildcard_tp_src";
public static final String COLUMN_WILDCARD_TP_DST = "wildcard_tp_dst";
public static final String COLUMN_PRIORITY = "priority";
public static final String COLUMN_ACTION = "action";
public static String ColumnNames[] = { COLUMN_RULEID, COLUMN_DPID,
COLUMN_IN_PORT, COLUMN_DL_SRC, COLUMN_DL_DST, COLUMN_DL_TYPE,
COLUMN_NW_SRC_PREFIX, COLUMN_NW_SRC_MASKBITS, COLUMN_NW_DST_PREFIX,
COLUMN_NW_DST_MASKBITS, COLUMN_NW_PROTO, COLUMN_TP_SRC,
COLUMN_TP_DST, COLUMN_WILDCARD_DPID, COLUMN_WILDCARD_IN_PORT,
COLUMN_WILDCARD_DL_SRC, COLUMN_WILDCARD_DL_DST,
COLUMN_WILDCARD_DL_TYPE, COLUMN_WILDCARD_NW_SRC,
COLUMN_WILDCARD_NW_DST, COLUMN_WILDCARD_NW_PROTO, COLUMN_PRIORITY,
COLUMN_ACTION };
@Override
public String getName() {
return "firewall";
}
@Override
public boolean isCallbackOrderingPrereq(OFType type, String name) {
// no prereq
return false;
}
@Override
public boolean isCallbackOrderingPostreq(OFType type, String name) {
return (type.equals(OFType.PACKET_IN) && name.equals("forwarding"));
}
@Override
public Collection<Class<? extends IFloodlightService>> getModuleServices() {
Collection<Class<? extends IFloodlightService>> l = new ArrayList<Class<? extends IFloodlightService>>();
l.add(IFirewallService.class);
return l;
}
@Override
public Map<Class<? extends IFloodlightService>, IFloodlightService> getServiceImpls() {
Map<Class<? extends IFloodlightService>, IFloodlightService> m = new HashMap<Class<? extends IFloodlightService>, IFloodlightService>();
// We are the class that implements the service
m.put(IFirewallService.class, this);
return m;
}
@Override
public Collection<Class<? extends IFloodlightService>> getModuleDependencies() {
Collection<Class<? extends IFloodlightService>> l = new ArrayList<Class<? extends IFloodlightService>>();
l.add(IFloodlightProviderService.class);
l.add(IStorageSourceService.class);
l.add(IRestApiService.class);
return l;
}
/**
* Reads the rules from the storage and creates a sorted arraylist of
* FirewallRule from them.
*
* Similar to getStorageRules(), which only reads contents for REST GET and
* does no parsing, checking, nor putting into FirewallRule objects
*
* @return the sorted arraylist of FirewallRule instances (rules from
* storage)
*/
protected ArrayList<FirewallRule> readRulesFromStorage() {
ArrayList<FirewallRule> l = new ArrayList<FirewallRule>();
try {
Map<String, Object> row;
// (..., null, null) for no predicate, no ordering
IResultSet resultSet = storageSource.executeQuery(TABLE_NAME,
ColumnNames, null, null);
// put retrieved rows into FirewallRules
for (Iterator<IResultSet> it = resultSet.iterator(); it.hasNext();) {
row = it.next().getRow();
// now, parse row
FirewallRule r = new FirewallRule();
if (!row.containsKey(COLUMN_RULEID)
|| !row.containsKey(COLUMN_DPID)) {
logger.error(
"skipping entry with missing required 'ruleid' or 'switchid' entry: {}",
row);
return l;
}
try {
r.ruleid = Integer
.parseInt((String) row.get(COLUMN_RULEID));
r.dpid = Long.parseLong((String) row.get(COLUMN_DPID));
for (String key : row.keySet()) {
if (row.get(key) == null)
continue;
if (key.equals(COLUMN_RULEID)
|| key.equals(COLUMN_DPID)
|| key.equals("id")) {
continue; // already handled
}
else if (key.equals(COLUMN_IN_PORT)) {
r.in_port = Short.parseShort((String) row
.get(COLUMN_IN_PORT));
}
else if (key.equals(COLUMN_DL_SRC)) {
r.dl_src = Long.parseLong((String) row
.get(COLUMN_DL_SRC));
}
else if (key.equals(COLUMN_DL_DST)) {
r.dl_dst = Long.parseLong((String) row
.get(COLUMN_DL_DST));
}
else if (key.equals(COLUMN_DL_TYPE)) {
r.dl_type = Short.parseShort((String) row
.get(COLUMN_DL_TYPE));
}
else if (key.equals(COLUMN_NW_SRC_PREFIX)) {
r.nw_src_prefix = Integer.parseInt((String) row
.get(COLUMN_NW_SRC_PREFIX));
}
else if (key.equals(COLUMN_NW_SRC_MASKBITS)) {
r.nw_src_maskbits = Integer.parseInt((String) row
.get(COLUMN_NW_SRC_MASKBITS));
}
else if (key.equals(COLUMN_NW_DST_PREFIX)) {
r.nw_dst_prefix = Integer.parseInt((String) row
.get(COLUMN_NW_DST_PREFIX));
}
else if (key.equals(COLUMN_NW_DST_MASKBITS)) {
r.nw_dst_maskbits = Integer.parseInt((String) row
.get(COLUMN_NW_DST_MASKBITS));
}
else if (key.equals(COLUMN_NW_PROTO)) {
r.nw_proto = Short.parseShort((String) row
.get(COLUMN_NW_PROTO));
}
else if (key.equals(COLUMN_TP_SRC)) {
r.tp_src = Short.parseShort((String) row
.get(COLUMN_TP_SRC));
}
else if (key.equals(COLUMN_TP_DST)) {
r.tp_dst = Short.parseShort((String) row
.get(COLUMN_TP_DST));
}
else if (key.equals(COLUMN_WILDCARD_DPID)) {
r.wildcard_dpid = Boolean.parseBoolean((String) row
.get(COLUMN_WILDCARD_DPID));
}
else if (key.equals(COLUMN_WILDCARD_IN_PORT)) {
r.wildcard_in_port = Boolean
.parseBoolean((String) row
.get(COLUMN_WILDCARD_IN_PORT));
}
else if (key.equals(COLUMN_WILDCARD_DL_SRC)) {
r.wildcard_dl_src = Boolean
.parseBoolean((String) row
.get(COLUMN_WILDCARD_DL_SRC));
}
else if (key.equals(COLUMN_WILDCARD_DL_DST)) {
r.wildcard_dl_dst = Boolean
.parseBoolean((String) row
.get(COLUMN_WILDCARD_DL_DST));
}
else if (key.equals(COLUMN_WILDCARD_DL_TYPE)) {
r.wildcard_dl_type = Boolean
.parseBoolean((String) row
.get(COLUMN_WILDCARD_DL_TYPE));
}
else if (key.equals(COLUMN_WILDCARD_NW_SRC)) {
r.wildcard_nw_src = Boolean
.parseBoolean((String) row
.get(COLUMN_WILDCARD_NW_SRC));
}
else if (key.equals(COLUMN_WILDCARD_NW_DST)) {
r.wildcard_nw_dst = Boolean
.parseBoolean((String) row
.get(COLUMN_WILDCARD_NW_DST));
}
else if (key.equals(COLUMN_WILDCARD_NW_PROTO)) {
r.wildcard_nw_proto = Boolean
.parseBoolean((String) row
.get(COLUMN_WILDCARD_NW_PROTO));
}
else if (key.equals(COLUMN_PRIORITY)) {
r.priority = Integer.parseInt((String) row
.get(COLUMN_PRIORITY));
}
else if (key.equals(COLUMN_ACTION)) {
int tmp = Integer.parseInt((String) row.get(COLUMN_ACTION));
if (tmp == FirewallRule.FirewallAction.DENY.ordinal())
r.action = FirewallRule.FirewallAction.DENY;
else if (tmp == FirewallRule.FirewallAction.ALLOW.ordinal())
r.action = FirewallRule.FirewallAction.ALLOW;
else {
r.action = null;
logger.error("action not recognized");
}
}
}
} catch (ClassCastException e) {
logger.error(
"skipping rule {} with bad data : "
+ e.getMessage(), r.ruleid);
}
if (r.action != null)
l.add(r);
}
} catch (StorageException e) {
logger.error("failed to access storage: {}", e.getMessage());
// if the table doesn't exist, then wait to populate later via
// setStorageSource()
}
// now, sort the list based on priorities
Collections.sort(l);
return l;
}
@Override
public void init(FloodlightModuleContext context)
throws FloodlightModuleException {
floodlightProvider = context
.getServiceImpl(IFloodlightProviderService.class);
storageSource = context.getServiceImpl(IStorageSourceService.class);
restApi = context.getServiceImpl(IRestApiService.class);
rules = new ArrayList<FirewallRule>();
logger = LoggerFactory.getLogger(Firewall.class);
// start disabled
enabled = false;
}
@Override
public void startUp(FloodlightModuleContext context) {
// register REST interface
restApi.addRestletRoutable(new FirewallWebRoutable());
// always place firewall in pipeline at bootup
floodlightProvider.addOFMessageListener(OFType.PACKET_IN, this);
// storage, create table and read rules
storageSource.createTable(TABLE_NAME, null);
storageSource.setTablePrimaryKeyName(TABLE_NAME, COLUMN_RULEID);
synchronized (rules) {
this.rules = readRulesFromStorage();
}
}
@Override
public Command receive(IOFSwitch sw, OFMessage msg, FloodlightContext cntx) {
if (!this.enabled)
return Command.CONTINUE;
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);
}
break;
default:
break;
}
return Command.CONTINUE;
}
@Override
public void enableFirewall(boolean enabled) {
logger.info("Setting firewall to {}", enabled);
this.enabled = enabled;
}
@Override
public List<FirewallRule> getRules() {
return this.rules;
}
// Only used to serve REST GET
// Similar to readRulesFromStorage(), which actually checks and stores
// record into FirewallRule list
@Override
public List<Map<String, Object>> getStorageRules() {
ArrayList<Map<String, Object>> l = new ArrayList<Map<String, Object>>();
try {
// null1=no predicate, null2=no ordering
IResultSet resultSet = storageSource.executeQuery(TABLE_NAME,
ColumnNames, null, null);
for (Iterator<IResultSet> it = resultSet.iterator(); it.hasNext();) {
l.add(it.next().getRow());
}
} catch (StorageException e) {
logger.error("failed to access storage: {}", e.getMessage());
// if the table doesn't exist, then wait to populate later via
// setStorageSource()
}
return l;
}
@Override
public String getSubnetMask() {
return IPv4.fromIPv4Address(this.subnet_mask);
}
@Override
public void setSubnetMask(String newMask) {
if (newMask.trim().isEmpty())
return;
this.subnet_mask = IPv4.toIPv4Address(newMask.trim());
}
@Override
public synchronized void addRule(FirewallRule rule) {
// generate random ruleid for each newly created rule
// may want to return to caller if useful
// may want to check conflict
rule.ruleid = rule.genID();
int i = 0;
// locate the position of the new rule in the sorted arraylist
for (i = 0; i < this.rules.size(); i++) {
if (this.rules.get(i).priority >= rule.priority)
break;
}
// now, add rule to the list
if (i <= this.rules.size()) {
this.rules.add(i, rule);
} else {
this.rules.add(rule);
}
// add rule to database
Map<String, Object> entry = new HashMap<String, Object>();
entry.put(COLUMN_RULEID, Integer.toString(rule.ruleid));
entry.put(COLUMN_DPID, Long.toString(rule.dpid));
entry.put(COLUMN_IN_PORT, Short.toString(rule.in_port));
entry.put(COLUMN_DL_SRC, Long.toString(rule.dl_src));
entry.put(COLUMN_DL_DST, Long.toString(rule.dl_dst));
entry.put(COLUMN_DL_TYPE, Short.toString(rule.dl_type));
entry.put(COLUMN_NW_SRC_PREFIX, Integer.toString(rule.nw_src_prefix));
entry.put(COLUMN_NW_SRC_MASKBITS, Integer.toString(rule.nw_src_maskbits));
entry.put(COLUMN_NW_DST_PREFIX, Integer.toString(rule.nw_dst_prefix));
entry.put(COLUMN_NW_DST_MASKBITS, Integer.toString(rule.nw_dst_maskbits));
entry.put(COLUMN_NW_PROTO, Short.toString(rule.nw_proto));
entry.put(COLUMN_TP_SRC, Integer.toString(rule.tp_src));
entry.put(COLUMN_TP_DST, Integer.toString(rule.tp_dst));
entry.put(COLUMN_WILDCARD_DPID,
Boolean.toString(rule.wildcard_dpid));
entry.put(COLUMN_WILDCARD_IN_PORT,
Boolean.toString(rule.wildcard_in_port));
entry.put(COLUMN_WILDCARD_DL_SRC,
Boolean.toString(rule.wildcard_dl_src));
entry.put(COLUMN_WILDCARD_DL_DST,
Boolean.toString(rule.wildcard_dl_dst));
entry.put(COLUMN_WILDCARD_DL_TYPE,
Boolean.toString(rule.wildcard_dl_type));
entry.put(COLUMN_WILDCARD_NW_SRC,
Boolean.toString(rule.wildcard_nw_src));
entry.put(COLUMN_WILDCARD_NW_DST,
Boolean.toString(rule.wildcard_nw_dst));
entry.put(COLUMN_WILDCARD_NW_PROTO,
Boolean.toString(rule.wildcard_nw_proto));
entry.put(COLUMN_WILDCARD_TP_SRC,
Boolean.toString(rule.wildcard_tp_src));
entry.put(COLUMN_WILDCARD_TP_DST,
Boolean.toString(rule.wildcard_tp_dst));
entry.put(COLUMN_PRIORITY, Integer.toString(rule.priority));
entry.put(COLUMN_ACTION, Integer.toString(rule.action.ordinal()));
storageSource.insertRow(TABLE_NAME, entry);
}
@Override
public synchronized void deleteRule(int ruleid) {
Iterator<FirewallRule> iter = this.rules.iterator();
while (iter.hasNext()) {
FirewallRule r = iter.next();
if (r.ruleid == ruleid) {
// found the rule, now remove it
iter.remove();
break;
}
}
// delete from database
storageSource.deleteRow(TABLE_NAME, Integer.toString(ruleid));
}
/**
* Iterates over the firewall rules and tries to match them with the
* incoming packet (flow). Uses the FirewallRule class's matchWithFlow
* method to perform matching. It maintains a pair of wildcards (allow and
* deny) which are assigned later to the firewall's decision, where 'allow'
* wildcards are applied if the matched rule turns out to be an ALLOW rule
* and 'deny' wildcards are applied otherwise. Wildcards are applied to
* firewall decision to optimize flows in the switch, ensuring least number
* of flows per firewall rule. So, if a particular field is not "ANY" (i.e.
* not wildcarded) in a higher priority rule, then if a lower priority rule
* matches the packet and wildcards it, it can't be wildcarded in the
* switch's flow entry, because otherwise some packets matching the higher
* priority rule might escape the firewall. The reason for keeping different
* two different wildcards is that if a field is not wildcarded in a higher
* priority allow rule, the same field shouldn't be wildcarded for packets
* matching the lower priority deny rule (non-wildcarded fields in higher
* priority rules override the wildcarding of those fields in lower priority
* rules of the opposite type). So, to ensure that wildcards are
* appropriately set for different types of rules (allow vs. deny), separate
* wildcards are maintained. Iteration is performed on the sorted list of
* rules (sorted in decreasing order of priority).
*
* @param sw
* the switch instance
* @param pi
* the incoming packet data structure
* @param cntx
* the floodlight context
* @return an instance of RuleWildcardsPair that specify rule that matches
* and the wildcards for the firewall decision
*/
protected RuleWildcardsPair matchWithRule(IOFSwitch sw, OFPacketIn pi,
FloodlightContext cntx) {
FirewallRule matched_rule = null;
Ethernet eth = IFloodlightProviderService.bcStore.get(cntx,
IFloodlightProviderService.CONTEXT_PI_PAYLOAD);
WildcardsPair wildcards = new WildcardsPair();
synchronized (rules) {
Iterator<FirewallRule> iter = this.rules.iterator();
FirewallRule rule = null;
// iterate through list to find a matching firewall rule
while (iter.hasNext()) {
// get next rule from list
rule = iter.next();
// check if rule matches
if (rule.matchesFlow(sw.getId(), pi.getInPort(), eth, wildcards) == true) {
matched_rule = rule;
break;
}
}
}
// make a pair of rule and wildcards, then return it
RuleWildcardsPair ret = new RuleWildcardsPair();
ret.rule = matched_rule;
if (matched_rule == null || matched_rule.action == FirewallRule.FirewallAction.DENY) {
ret.wildcards = wildcards.drop;
} else {
ret.wildcards = wildcards.allow;
}
return ret;
}
/**
* Checks whether an IP address is a broadcast address or not (determines
* using subnet mask)
*
* @param IPAddress
* the IP address to check
* @return true if it is a broadcast address, false otherwise
*/
protected boolean IPIsBroadcast(int IPAddress) {
// inverted subnet mask
int inv_subnet_mask = ~this.subnet_mask;
return ((IPAddress & inv_subnet_mask) == inv_subnet_mask);
}
public Command processPacketInMessage(IOFSwitch sw, OFPacketIn pi,
IRoutingDecision decision, FloodlightContext cntx) {
Ethernet eth = IFloodlightProviderService.bcStore.get(cntx,
IFloodlightProviderService.CONTEXT_PI_PAYLOAD);
// Allowing L2 broadcast + ARP broadcast request (also deny malformed
// broadcasts -> L2 broadcast + L3 unicast)
if (eth.isBroadcast() == true) {
boolean allowBroadcast = true;
// the case to determine if we have L2 broadcast + L3 unicast
// don't allow this broadcast packet if such is the case (malformed
// packet)
if (eth.getEtherType() == Ethernet.TYPE_IPv4
&& this.IPIsBroadcast(((IPv4) eth.getPayload())
.getDestinationAddress()) == false) {
allowBroadcast = false;
}
if (allowBroadcast == true) {
if (logger.isTraceEnabled())
logger.trace("Allowing broadcast traffic for PacketIn={}",
pi);
decision = new RoutingDecision(sw.getId(), pi.getInPort()
, IDeviceService.fcStore.
get(cntx, IDeviceService.CONTEXT_SRC_DEVICE),
IRoutingDecision.RoutingAction.MULTICAST);
decision.addToContext(cntx);
} else {
if (logger.isTraceEnabled())
logger.trace(
"Blocking malformed broadcast traffic for PacketIn={}",
pi);
decision = new RoutingDecision(sw.getId(), pi.getInPort()
, IDeviceService.fcStore.
get(cntx, IDeviceService.CONTEXT_SRC_DEVICE),
IRoutingDecision.RoutingAction.DROP);
decision.addToContext(cntx);
}
return Command.CONTINUE;
}
/*
* ARP response (unicast) can be let through without filtering through
* rules by uncommenting the code below
*/
/*
* else if (eth.getEtherType() == Ethernet.TYPE_ARP) {
* logger.info("allowing ARP traffic"); decision = new
* FirewallDecision(IRoutingDecision.RoutingAction.FORWARD_OR_FLOOD);
* decision.addToContext(cntx); return Command.CONTINUE; }
*/
// check if we have a matching rule for this packet/flow
// and no decision is taken yet
if (decision == null) {
RuleWildcardsPair match_ret = this.matchWithRule(sw, pi, cntx);
FirewallRule rule = match_ret.rule;
if (rule == null || rule.action == FirewallRule.FirewallAction.DENY) {
decision = new RoutingDecision(sw.getId(), pi.getInPort()
, IDeviceService.fcStore.
get(cntx, IDeviceService.CONTEXT_SRC_DEVICE),
IRoutingDecision.RoutingAction.DROP);
decision.setWildcards(match_ret.wildcards);
decision.addToContext(cntx);
if (logger.isTraceEnabled()) {
if (rule == null)
logger.trace(
"No firewall rule found for PacketIn={}, blocking flow",
pi);
else if (rule.action == FirewallRule.FirewallAction.DENY) {
logger.trace("Deny rule={} match for PacketIn={}",
rule, pi);
}
}
} else {
decision = new RoutingDecision(sw.getId(), pi.getInPort()
, IDeviceService.fcStore.
get(cntx, IDeviceService.CONTEXT_SRC_DEVICE),
IRoutingDecision.RoutingAction.FORWARD_OR_FLOOD);
decision.setWildcards(match_ret.wildcards);
decision.addToContext(cntx);
if (logger.isTraceEnabled())
logger.trace("Allow rule={} match for PacketIn={}", rule,
pi);
}
}
return Command.CONTINUE;
}
@Override
public boolean isEnabled() {
return enabled;
}
}