| package net.floodlightcontroller.firewall; |
| |
| import static org.easymock.EasyMock.expect; |
| import static org.easymock.EasyMock.replay; |
| import static org.easymock.EasyMock.verify; |
| |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import net.floodlightcontroller.core.FloodlightContext; |
| import net.floodlightcontroller.core.IFloodlightProviderService; |
| import net.floodlightcontroller.core.IOFSwitch; |
| import net.floodlightcontroller.core.module.FloodlightModuleContext; |
| import net.floodlightcontroller.core.module.FloodlightModuleException; |
| import net.floodlightcontroller.core.test.MockFloodlightProvider; |
| import net.floodlightcontroller.packet.ARP; |
| import net.floodlightcontroller.packet.Data; |
| import net.floodlightcontroller.packet.Ethernet; |
| import net.floodlightcontroller.packet.IPacket; |
| import net.floodlightcontroller.packet.IPv4; |
| import net.floodlightcontroller.packet.TCP; |
| import net.floodlightcontroller.packet.UDP; |
| import net.floodlightcontroller.restserver.IRestApiService; |
| import net.floodlightcontroller.restserver.RestApiServer; |
| import net.floodlightcontroller.routing.IRoutingDecision; |
| import net.floodlightcontroller.storage.IStorageSourceService; |
| import net.floodlightcontroller.storage.memory.MemoryStorageSource; |
| import net.floodlightcontroller.test.FloodlightTestCase; |
| import net.floodlightcontroller.util.MACAddress; |
| |
| import org.easymock.EasyMock; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.openflow.protocol.OFPacketIn; |
| import org.openflow.protocol.OFPacketIn.OFPacketInReason; |
| import org.openflow.protocol.OFType; |
| import org.openflow.util.HexString; |
| |
| /** |
| * Unit test for stateless firewall implemented as a Google Summer of Code project. |
| * |
| * @author Amer Tahir |
| */ |
| public class FirewallTest extends FloodlightTestCase { |
| protected MockFloodlightProvider mockFloodlightProvider; |
| protected FloodlightContext cntx; |
| protected OFPacketIn packetIn; |
| protected IOFSwitch sw; |
| protected IPacket tcpPacket; |
| protected IPacket broadcastARPPacket; |
| protected IPacket ARPReplyPacket; |
| protected IPacket broadcastIPPacket; |
| protected IPacket tcpPacketReply; |
| protected IPacket broadcastMalformedPacket; |
| private Firewall firewall; |
| public static String TestSwitch1DPID = "00:00:00:00:00:00:00:01"; |
| |
| @Before |
| public void setUp() throws Exception { |
| super.setUp(); |
| cntx = new FloodlightContext(); |
| mockFloodlightProvider = getMockFloodlightProvider(); |
| firewall = new Firewall(); |
| IStorageSourceService storageService = new MemoryStorageSource(); |
| RestApiServer restApi = new RestApiServer(); |
| |
| // Mock switches |
| long dpid = HexString.toLong(TestSwitch1DPID); |
| sw = EasyMock.createNiceMock(IOFSwitch.class); |
| expect(sw.getId()).andReturn(dpid).anyTimes(); |
| expect(sw.getStringId()).andReturn(TestSwitch1DPID).anyTimes(); |
| replay(sw); |
| // Load the switch map |
| Map<Long, IOFSwitch> switches = new HashMap<Long, IOFSwitch>(); |
| switches.put(dpid, sw); |
| mockFloodlightProvider.setSwitches(switches); |
| |
| FloodlightModuleContext fmc = new FloodlightModuleContext(); |
| fmc.addService(IFloodlightProviderService.class, |
| mockFloodlightProvider); |
| fmc.addService(IFirewallService.class, firewall); |
| fmc.addService(IStorageSourceService.class, storageService); |
| fmc.addService(IRestApiService.class, restApi); |
| |
| try { |
| restApi.init(fmc); |
| } catch (FloodlightModuleException e) { |
| e.printStackTrace(); |
| } |
| |
| firewall.init(fmc); |
| firewall.startUp(fmc); |
| |
| // Build our test packet |
| this.tcpPacket = new Ethernet() |
| .setDestinationMACAddress("00:11:22:33:44:55") |
| .setSourceMACAddress("00:44:33:22:11:00") |
| .setVlanID((short) 42) |
| .setEtherType(Ethernet.TYPE_IPv4) |
| .setPayload( |
| new IPv4() |
| .setTtl((byte) 128) |
| .setSourceAddress("192.168.1.1") |
| .setDestinationAddress("192.168.1.2") |
| .setPayload(new TCP() |
| .setSourcePort((short) 81) |
| .setDestinationPort((short) 80) |
| .setPayload(new Data(new byte[] {0x01})))); |
| |
| // Build a broadcast ARP packet |
| this.broadcastARPPacket = new Ethernet() |
| .setDestinationMACAddress("FF:FF:FF:FF:FF:FF") |
| .setSourceMACAddress("00:44:33:22:11:00") |
| .setVlanID((short) 42) |
| .setEtherType(Ethernet.TYPE_ARP) |
| .setPayload( |
| new ARP() |
| .setHardwareType(ARP.HW_TYPE_ETHERNET) |
| .setProtocolType(ARP.PROTO_TYPE_IP) |
| .setOpCode(ARP.OP_REQUEST) |
| .setHardwareAddressLength((byte)6) |
| .setProtocolAddressLength((byte)4) |
| .setSenderHardwareAddress(Ethernet.toMACAddress("00:44:33:22:11:00")) |
| .setSenderProtocolAddress(IPv4.toIPv4Address("192.168.1.1")) |
| .setTargetHardwareAddress(Ethernet.toMACAddress("00:00:00:00:00:00")) |
| .setTargetProtocolAddress(IPv4.toIPv4Address("192.168.1.2")) |
| .setPayload(new Data(new byte[] {0x01}))); |
| |
| // Build a ARP packet |
| this.ARPReplyPacket = new Ethernet() |
| .setDestinationMACAddress("00:44:33:22:11:00") |
| .setSourceMACAddress("00:11:22:33:44:55") |
| .setVlanID((short) 42) |
| .setEtherType(Ethernet.TYPE_ARP) |
| .setPayload( |
| new ARP() |
| .setHardwareType(ARP.HW_TYPE_ETHERNET) |
| .setProtocolType(ARP.PROTO_TYPE_IP) |
| .setOpCode(ARP.OP_REQUEST) |
| .setHardwareAddressLength((byte)6) |
| .setProtocolAddressLength((byte)4) |
| .setSenderHardwareAddress(Ethernet.toMACAddress("00:11:22:33:44:55")) |
| .setSenderProtocolAddress(IPv4.toIPv4Address("192.168.1.2")) |
| .setTargetHardwareAddress(Ethernet.toMACAddress("00:44:33:22:11:00")) |
| .setTargetProtocolAddress(IPv4.toIPv4Address("192.168.1.1")) |
| .setPayload(new Data(new byte[] {0x01}))); |
| |
| // Build a broadcast IP packet |
| this.broadcastIPPacket = new Ethernet() |
| .setDestinationMACAddress("FF:FF:FF:FF:FF:FF") |
| .setSourceMACAddress("00:44:33:22:11:00") |
| .setVlanID((short) 42) |
| .setEtherType(Ethernet.TYPE_IPv4) |
| .setPayload( |
| new IPv4() |
| .setTtl((byte) 128) |
| .setSourceAddress("192.168.1.1") |
| .setDestinationAddress("192.168.1.255") |
| .setPayload(new UDP() |
| .setSourcePort((short) 5000) |
| .setDestinationPort((short) 5001) |
| .setPayload(new Data(new byte[] {0x01})))); |
| |
| // Build a malformed broadcast packet |
| this.broadcastMalformedPacket = new Ethernet() |
| .setDestinationMACAddress("FF:FF:FF:FF:FF:FF") |
| .setSourceMACAddress("00:44:33:22:11:00") |
| .setVlanID((short) 42) |
| .setEtherType(Ethernet.TYPE_IPv4) |
| .setPayload( |
| new IPv4() |
| .setTtl((byte) 128) |
| .setSourceAddress("192.168.1.1") |
| .setDestinationAddress("192.168.1.2") |
| .setPayload(new UDP() |
| .setSourcePort((short) 5000) |
| .setDestinationPort((short) 5001) |
| .setPayload(new Data(new byte[] {0x01})))); |
| |
| this.tcpPacketReply = new Ethernet() |
| .setDestinationMACAddress("00:44:33:22:11:00") |
| .setSourceMACAddress("00:11:22:33:44:55") |
| .setVlanID((short) 42) |
| .setEtherType(Ethernet.TYPE_IPv4) |
| .setPayload( |
| new IPv4() |
| .setTtl((byte) 128) |
| .setSourceAddress("192.168.1.2") |
| .setDestinationAddress("192.168.1.1") |
| .setPayload(new TCP() |
| .setSourcePort((short) 80) |
| .setDestinationPort((short) 81) |
| .setPayload(new Data(new byte[] {0x02})))); |
| } |
| |
| protected void setPacketIn(IPacket packet) { |
| byte[] serializedPacket = packet.serialize(); |
| // Build the PacketIn |
| this.packetIn = ((OFPacketIn) mockFloodlightProvider.getOFMessageFactory().getMessage(OFType.PACKET_IN)) |
| .setBufferId(-1) |
| .setInPort((short) 1) |
| .setPacketData(serializedPacket) |
| .setReason(OFPacketInReason.NO_MATCH) |
| .setTotalLength((short) serializedPacket.length); |
| |
| // Add the packet to the context store |
| IFloodlightProviderService.bcStore. |
| put(cntx, |
| IFloodlightProviderService.CONTEXT_PI_PAYLOAD, |
| (Ethernet)packet); |
| } |
| |
| @Test |
| public void testNoRules() throws Exception { |
| // enable firewall first |
| firewall.enableFirewall(true); |
| // simulate a packet-in event |
| this.setPacketIn(tcpPacket); |
| firewall.receive(sw, this.packetIn, cntx); |
| verify(sw); |
| |
| assertEquals(0, firewall.rules.size()); |
| |
| IRoutingDecision decision = IRoutingDecision.rtStore.get(cntx, IRoutingDecision.CONTEXT_DECISION); |
| // no rules to match, so firewall should deny |
| assertEquals(decision.getRoutingAction(), IRoutingDecision.RoutingAction.DROP); |
| } |
| |
| @Test |
| public void testReadRulesFromStorage() throws Exception { |
| // add 2 rules first |
| FirewallRule rule = new FirewallRule(); |
| rule.in_port = 2; |
| rule.dl_src = MACAddress.valueOf("00:00:00:00:00:01").toLong(); |
| rule.dl_dst = MACAddress.valueOf("00:00:00:00:00:02").toLong(); |
| rule.priority = 1; |
| rule.action = FirewallRule.FirewallAction.DENY; |
| firewall.addRule(rule); |
| rule = new FirewallRule(); |
| rule.in_port = 3; |
| rule.dl_src = MACAddress.valueOf("00:00:00:00:00:02").toLong(); |
| rule.dl_dst = MACAddress.valueOf("00:00:00:00:00:01").toLong(); |
| rule.nw_proto = IPv4.PROTOCOL_TCP; |
| rule.wildcard_nw_proto = false; |
| rule.tp_dst = 80; |
| rule.priority = 2; |
| rule.action = FirewallRule.FirewallAction.ALLOW; |
| firewall.addRule(rule); |
| |
| List<FirewallRule> rules = firewall.readRulesFromStorage(); |
| // verify rule 1 |
| FirewallRule r = rules.get(0); |
| assertEquals(r.in_port, 2); |
| assertEquals(r.priority, 1); |
| assertEquals(r.dl_src, MACAddress.valueOf("00:00:00:00:00:01").toLong()); |
| assertEquals(r.dl_dst, MACAddress.valueOf("00:00:00:00:00:02").toLong()); |
| assertEquals(r.action, FirewallRule.FirewallAction.DENY); |
| // verify rule 2 |
| r = rules.get(1); |
| assertEquals(r.in_port, 3); |
| assertEquals(r.priority, 2); |
| assertEquals(r.dl_src, MACAddress.valueOf("00:00:00:00:00:02").toLong()); |
| assertEquals(r.dl_dst, MACAddress.valueOf("00:00:00:00:00:01").toLong()); |
| assertEquals(r.nw_proto, IPv4.PROTOCOL_TCP); |
| assertEquals(r.tp_dst, 80); |
| assertEquals(r.wildcard_nw_proto, false); |
| assertEquals(r.action, FirewallRule.FirewallAction.ALLOW); |
| } |
| |
| @Test |
| public void testRuleInsertionIntoStorage() throws Exception { |
| // add TCP rule |
| FirewallRule rule = new FirewallRule(); |
| rule.nw_proto = IPv4.PROTOCOL_TCP; |
| rule.wildcard_nw_proto = false; |
| rule.priority = 1; |
| firewall.addRule(rule); |
| |
| List<Map<String, Object>> rulesFromStorage = firewall.getStorageRules(); |
| assertEquals(1, rulesFromStorage.size()); |
| assertEquals(Integer.parseInt((String)rulesFromStorage.get(0).get("ruleid")), rule.ruleid); |
| } |
| |
| @Test |
| public void testRuleDeletion() throws Exception { |
| // add TCP rule |
| FirewallRule rule = new FirewallRule(); |
| rule.nw_proto = IPv4.PROTOCOL_TCP; |
| rule.wildcard_nw_proto = false; |
| rule.priority = 1; |
| firewall.addRule(rule); |
| int rid = rule.ruleid; |
| |
| List<Map<String, Object>> rulesFromStorage = firewall.getStorageRules(); |
| assertEquals(1, rulesFromStorage.size()); |
| assertEquals(Integer.parseInt((String)rulesFromStorage.get(0).get("ruleid")), rid); |
| |
| // delete rule |
| firewall.deleteRule(rid); |
| rulesFromStorage = firewall.getStorageRules(); |
| assertEquals(0, rulesFromStorage.size()); |
| } |
| |
| @Test |
| public void testFirewallDisabled() throws Exception { |
| // firewall isn't enabled by default |
| // so, it shouldn't make any decision |
| |
| // add TCP rule |
| FirewallRule rule = new FirewallRule(); |
| rule.nw_proto = IPv4.PROTOCOL_TCP; |
| rule.wildcard_nw_proto = false; |
| rule.priority = 1; |
| firewall.addRule(rule); |
| |
| this.setPacketIn(tcpPacket); |
| firewall.receive(sw, this.packetIn, cntx); |
| verify(sw); |
| |
| assertEquals(1, firewall.rules.size()); |
| |
| IRoutingDecision decision = IRoutingDecision.rtStore.get(cntx, IRoutingDecision.CONTEXT_DECISION); |
| assertNull(decision); |
| } |
| |
| @Test |
| public void testSimpleAllowRule() throws Exception { |
| // enable firewall first |
| firewall.enableFirewall(true); |
| |
| // add TCP rule |
| FirewallRule rule = new FirewallRule(); |
| rule.dl_type = Ethernet.TYPE_IPv4; |
| rule.wildcard_dl_type = false; |
| rule.nw_proto = IPv4.PROTOCOL_TCP; |
| rule.wildcard_nw_proto = false; |
| // source is IP 192.168.1.2 |
| rule.nw_src_prefix = IPv4.toIPv4Address("192.168.1.2"); |
| rule.wildcard_nw_src = false; |
| // dest is network 192.168.1.0/24 |
| rule.nw_dst_prefix = IPv4.toIPv4Address("192.168.1.0"); |
| rule.nw_dst_maskbits = 24; |
| rule.wildcard_nw_dst = false; |
| rule.priority = 1; |
| firewall.addRule(rule); |
| |
| // simulate a packet-in events |
| |
| this.setPacketIn(tcpPacketReply); |
| firewall.receive(sw, this.packetIn, cntx); |
| verify(sw); |
| |
| IRoutingDecision decision = IRoutingDecision.rtStore.get(cntx, IRoutingDecision.CONTEXT_DECISION); |
| assertEquals(decision.getRoutingAction(), IRoutingDecision.RoutingAction.FORWARD_OR_FLOOD); |
| |
| // clear decision |
| IRoutingDecision.rtStore.remove(cntx, IRoutingDecision.CONTEXT_DECISION); |
| |
| this.setPacketIn(tcpPacket); |
| firewall.receive(sw, this.packetIn, cntx); |
| verify(sw); |
| |
| decision = IRoutingDecision.rtStore.get(cntx, IRoutingDecision.CONTEXT_DECISION); |
| assertEquals(decision.getRoutingAction(), IRoutingDecision.RoutingAction.DROP); |
| } |
| |
| @Test |
| public void testOverlappingRules() throws Exception { |
| firewall.enableFirewall(true); |
| |
| // add TCP port 80 (destination only) allow rule |
| FirewallRule rule = new FirewallRule(); |
| rule.dl_type = Ethernet.TYPE_IPv4; |
| rule.wildcard_dl_type = false; |
| rule.nw_proto = IPv4.PROTOCOL_TCP; |
| rule.wildcard_nw_proto = false; |
| rule.tp_dst = 80; |
| rule.priority = 1; |
| firewall.addRule(rule); |
| |
| // add block all rule |
| rule = new FirewallRule(); |
| rule.action = FirewallRule.FirewallAction.DENY; |
| rule.priority = 2; |
| firewall.addRule(rule); |
| |
| assertEquals(2, firewall.rules.size()); |
| |
| // packet destined to TCP port 80 - should be allowed |
| |
| this.setPacketIn(tcpPacket); |
| firewall.receive(sw, this.packetIn, cntx); |
| verify(sw); |
| |
| IRoutingDecision decision = IRoutingDecision.rtStore.get(cntx, IRoutingDecision.CONTEXT_DECISION); |
| assertEquals(decision.getRoutingAction(), IRoutingDecision.RoutingAction.FORWARD_OR_FLOOD); |
| |
| // clear decision |
| IRoutingDecision.rtStore.remove(cntx, IRoutingDecision.CONTEXT_DECISION); |
| |
| // packet destined for port 81 - should be denied |
| |
| this.setPacketIn(tcpPacketReply); |
| firewall.receive(sw, this.packetIn, cntx); |
| verify(sw); |
| |
| decision = IRoutingDecision.rtStore.get(cntx, IRoutingDecision.CONTEXT_DECISION); |
| assertEquals(decision.getRoutingAction(), IRoutingDecision.RoutingAction.DROP); |
| } |
| |
| @Test |
| public void testARP() throws Exception { |
| // enable firewall first |
| firewall.enableFirewall(true); |
| |
| // no rules inserted so all traffic other than broadcast and ARP-request-broadcast should be blocked |
| |
| // simulate an ARP broadcast packet-in event |
| |
| this.setPacketIn(broadcastARPPacket); |
| firewall.receive(sw, this.packetIn, cntx); |
| verify(sw); |
| |
| // broadcast-ARP traffic should be allowed |
| IRoutingDecision decision = IRoutingDecision.rtStore.get(cntx, IRoutingDecision.CONTEXT_DECISION); |
| assertEquals(IRoutingDecision.RoutingAction.MULTICAST, decision.getRoutingAction()); |
| |
| // clear decision |
| IRoutingDecision.rtStore.remove(cntx, IRoutingDecision.CONTEXT_DECISION); |
| |
| // simulate an ARP reply packet-in event |
| |
| this.setPacketIn(ARPReplyPacket); |
| firewall.receive(sw, this.packetIn, cntx); |
| verify(sw); |
| |
| // ARP reply traffic should be denied |
| decision = IRoutingDecision.rtStore.get(cntx, IRoutingDecision.CONTEXT_DECISION); |
| assertEquals(decision.getRoutingAction(), IRoutingDecision.RoutingAction.DROP); |
| } |
| |
| @Test |
| public void testIPBroadcast() throws Exception { |
| // enable firewall first |
| firewall.enableFirewall(true); |
| |
| // set subnet mask for IP broadcast |
| firewall.setSubnetMask("255.255.255.0"); |
| |
| // no rules inserted so all traffic other than broadcast and ARP-request-broadcast should be blocked |
| |
| // simulate a packet-in event |
| |
| this.setPacketIn(broadcastIPPacket); |
| firewall.receive(sw, this.packetIn, cntx); |
| verify(sw); |
| |
| // broadcast traffic should be allowed |
| IRoutingDecision decision = IRoutingDecision.rtStore.get(cntx, IRoutingDecision.CONTEXT_DECISION); |
| assertEquals(IRoutingDecision.RoutingAction.MULTICAST, decision.getRoutingAction()); |
| } |
| |
| @Test |
| public void testMalformedIPBroadcast() throws Exception { |
| // enable firewall first |
| firewall.enableFirewall(true); |
| |
| // no rules inserted so all traffic other than broadcast and ARP-request-broadcast should be blocked |
| |
| // simulate a packet-in event |
| |
| this.setPacketIn(broadcastMalformedPacket); |
| firewall.receive(sw, this.packetIn, cntx); |
| verify(sw); |
| |
| // malformed broadcast traffic should NOT be allowed |
| IRoutingDecision decision = IRoutingDecision.rtStore.get(cntx, IRoutingDecision.CONTEXT_DECISION); |
| assertEquals(decision.getRoutingAction(), IRoutingDecision.RoutingAction.DROP); |
| } |
| |
| @Test |
| public void testLayer2Rule() throws Exception { |
| // enable firewall first |
| firewall.enableFirewall(true); |
| |
| // add L2 rule |
| FirewallRule rule = new FirewallRule(); |
| rule.dl_src = MACAddress.valueOf("00:44:33:22:11:00").toLong(); |
| rule.wildcard_dl_src = false; |
| rule.dl_dst = MACAddress.valueOf("00:11:22:33:44:55").toLong(); |
| rule.wildcard_dl_dst = false; |
| rule.priority = 1; |
| firewall.addRule(rule); |
| |
| // add TCP deny all rule |
| rule = new FirewallRule(); |
| rule.nw_proto = IPv4.PROTOCOL_TCP; |
| rule.wildcard_nw_proto = false; |
| rule.priority = 2; |
| rule.action = FirewallRule.FirewallAction.DENY; |
| firewall.addRule(rule); |
| |
| // simulate a packet-in event |
| |
| this.setPacketIn(tcpPacket); |
| firewall.receive(sw, this.packetIn, cntx); |
| verify(sw); |
| |
| IRoutingDecision decision = IRoutingDecision.rtStore.get(cntx, IRoutingDecision.CONTEXT_DECISION); |
| assertEquals(decision.getRoutingAction(), IRoutingDecision.RoutingAction.FORWARD_OR_FLOOD); |
| } |
| } |