This will be a two-week lab with two parts. First you will establish shortest-path connectivity between hosts across a network topology with multiple equal-cost paths. Next, you will add some packet inspection and modification functionality to behave like a basic firewall.
For this lab, we'll be using Mininet to simulate small datacenter network architectures like the ones we've been reading about in class. Because Mininet requires a very specific software configuration, we'll be running it inside of a VM to make things easier.
Due to storage space constraints, you'll need to store your VM images on /local rather than your home directory. Storing things on /local has two major implications that you need to account for:
To get started, you'll need to import a copy of the starter VM image:
After you've completed these steps once, you should see the your VM in the list of VMs available to start. Go ahead and turn it on.
While you could work from within the new VM window that just came up, doing so is a huge pain if you enjoy nice things like graphical interfaces. Instead, it's much easier to connect to the VM via ssh with X11 forwarding turned on. The VM is already configured such that you can connect to it by sshing to your local machine on port 22222. (Port 22222 on your machine gets forwarded to port 22 on the VM):
ssh -Y localhost -p 22222 -l mininet (The password is: mininet)
Note that you will need two terminals connected to the VM: one to run Mininet, and another to run the Pox OpenFlow controller. You can ssh in from two different terminals, but I recommend trying out the screen utility.
Once you're connected to the VM over ssh, you'll need to start Pox, the OpenFlow controller that will tell your switches what to do, and Mininet, the network simulator. It shouldn't matter which one you start first.
When you invoke Mininet, you need to choose a topology. I've provided two example networks: a basic three level, singly-root tree and a k=4 Al-Fares fat tree like the one in figure 3. To start Mininet:
cd ~/lab2 For the simple tree topology: sudo mn --controller=remote --custom basictree.py --topo basictree For the k=4 fat tree topology: sudo mn --controller=remote --custom k4fat.py --topo k4fat
To start Pox:
cd ~/pox ./pox.py openflow.discovery lab2 --hosts=[host file] --firewall=[firewall file] Where [hosts file] is the file containing host information for the Mininet topology you chose (basichosts or k4hosts). The --firewall argument is optional. If present, it will define a set of policy rules for your switches to enforce (part 2).
Once both Mininet and Pox are running, you can issue commands to the virtual hosts. Within Mininet, you can run commands at each host by invoking a terminal, which will pop-up in a new window:
mininet> xterm [host name] For example: mininet> xterm host_A1
As a shortcut, you can also issue some commands directly on the Mininet command line. For instance, if you want one host to "ping" another (send it a small message to test connectivity), you can issue:
mininet> host_A1 ping host_A2
For your solution, you'll be editing the controller code: lab2.py. This should be the only file you need to submit, unless you choose to divide your code into multiple modules/files (which is fine with me, but not required).
There are three main things going on in lab2.py:
As an input, your controller will receive a "hosts" file that contains connectivity information for each host in the network. You'll use the information it provides to respond to queries about a host's address information (ARP) and determine which destination switch to forward a packet to to reach a target host. This file is a list of the following information, one line per host:
HostName IPAddress MACAddress SwitchDPID SwitchPort For example: host_A_A1 10.1.1.1 00:00:00:01:11:11 00-00-00-01-00-01-00-00 1
This entry says that the host named "host_A_A1" has IP address 10.1.1.1, MAC address 00:00:00:01:11:11 and that it's connected to port #1 of the switch whose DPID is 00-00-00-01-00-01-00-00. (A DPID is just a unique identifier for a switch - DataPath IDentifier.)
When something of interest happens in your virtual network (e.g., a new switch comes online, or a packet is received and a switch isn't sure what to do with it), your controller will receive an event notification. For network events (switch arrival/departure), the Lab2 Pox component will receive a call to the _handle_LinkEvent method with information about which link(s) are now (un)available. You'll use this information to maintain a switch connectivity graph so that you can route packets to their destinations.
The NetworkX graph library will take care of most of the heavy lifting for you, including shortest path calculations. Note that for each edge in your graph, you will need to include information about the switch port numbers so that when you'll know which port to send it out of when you go to forward a packet.
You should respond to both link up events (add edge to graph) and link down events (remove edge from graph) to allow routing of new flows to continue, even if a link fails. The starter code provides the details of the event notification format.
When a switch receives a packet for which it has no matching flow entry, it will forward that packet to your controller, allowing you to decide its fate. When this occurs, one of your Lab2Switch instances will receive a call to the _handle_PacketIn method with the details of the packet's headers. You can then issue OpenFlow commands to the switch to tell it what to do in response, such as forwarding the packet, installing a flow entry into the switch's table, etc. If no path exists to route the packet, print a message saying so, including some information about the packet.
The event notification also includes other potentially useful information. For example, event.port is the port number of the switch that received the packet. You'll likely find this useful when responding to ARP queries, since those just need to be sent out the port they came in from. This is a simple and effective way of identifying that port number.
You should expect to handle two types of packets, ARP and IPv4. Any other type of packet may be ignored. The way you'll handle them is very different:
The Pox documentation describes the details for issuing OpenFlow commands. As a brief summary, to send a packet:
Create an of.ofp_packet_out() object:
openflow_command = of.ofp_packet_out()
Set the data (here, packet is an Ethernet frame object):
openflow_command.data = packet.pack()
Set one or more OpenFlow actions. In this case, just the "output action": of.ofp_action_output. Fill in the port= with the number of the port you'd like the switch to send it out:
openflow_command.actions.append(of.ofp_action_output(port=...))
After you've set up the openflow command object, you can send it to the switch via a connection to the switch. If you're executing within the context of a Lab2Switch's _handle_PacketIn handler, which is likely, the connection is accessible as self.connection:
self.connection.send(openflow_command)
Installing a flow table entry is very similar, only you create an of.ofp_flow_mod rather than of.ofp_packet_out. You must also set the .match attribute to describe which packets should match this rule. It has the following attributes (and others too):
# Note: This match field is required to be able to match on IP fields in the # network-layer headers. If you get "Fields ignored due to unspecified # prerequisites" warnings, it's because you didn't set this: openflow_flowmod.match.dl_type = pkt.ethernet.IP_TYPE openflow_flowmod.match.nw_src (Source IP address) openflow_flowmod.match.nw_dst (Destination IP address) openflow_flowmod.match.nw_proto (Layer 4 protocol - ICMP is 1, TCP is 6, UDP is 17) For TCP/UDP: openflow_flowmod.match.tp_src (Source port number) openflow_flowmod.match.tp_dst (Destination port number)
When adding flow entries to your switch tables, you should specify as many matching attributes as possible. That is, for TCP and UDP, you should use all five of the attributes above. For ICMP (ping messages), just use the first three, since the meaning of port numbers for ICMP isn't defined.
Note: You also need to set the actions to take on the flowmod message and send it to the switch, just as you do for packet_out commands.
After we're able to route packets, we'll add some basic firewall features to enforce a rudimentary security policy. Like the hosts file you're given at controller, start-up, a static set of firewall rules will be passed in under the name firewall. The rules will be of the form:
SwitchDPID action match [arguments] The switchDPID tells you the identifier of the switch that should enforce this rule. action is one of: BLOCK, REDIRECT, or MARK match is one or more packet header values that must be present to be considered a match. Options here are srcip, dstip, protocol, and tos. [arguments] depends on the action: For BLOCK, there will be no arguments. For REDIRECT, the arguments will be: TO [ip address] For MARK, the arguments will be: TOS [integer identifier] (TOS stands for "type of service" the header field we'll use for marking.)
When a switch receives an IP packet, you should subject the packet to that switch's rules prior to forwarding it.
A few rule examples:
At the specified switch, block all traffic coming from host 10.1.1.1: 00-00-02-00-00-00-00-00 BLOCK srcip=10.1.1.1 At the specified switch, block all traffic coming from host 10.1.2.2 if the packet's TOS is marked with 32 and it's destined for 10.1.3.1: 00-00-03-00-00-00-00-00 BLOCK srcip=10.1.2.2 dstip=10.1.3.1 tos=32 At the specified switch, redirect traffic destined for 10.1.2.1 and instead send it to 10.1.2.2: 00-00-01-00-00-00-00-00 REDIRECT dstip=10.1.2.1 TO 10.1.2.2 At the specified switch, mark the TOS field of all packets sent by 10.1.1.1 with value 40: 00-00-04-00-00-00-00-00 MARK srcip=10.1.1.1 TOS 40
You may assume that multiple rules for the same switch will not overlap or conflict with one another. For simplicity, our firewall only reads and modifies IP packets.
Note that the number of fields in the match portion of the rule varies (there will always be at least one). In order to be considered a match, all of the defined fields must be equal to the specified values.
Just like simple routing, we want to install flow table entries for matching flows so that the switches handle most of the packets without help from the controller. This means that for matching packets, you'll need to set your list of OpenFlow output actions differently. Note that actions is a list and that you can set multiple actions. If you set multiple actions, they will be executed in the order that you specify them. If one of your actions is to send the packet out of a switch port, that needs to be last!
The list of actions you can take are documented in the OpenFlow actions section of the Pox documentation. You should keep the following things in mind as you implement the three actions:
Note: You'll need to copy lab2.py from the VM to [your home directory]/cs91/labs/02/ before running handin.
Once you are satisfied with your solution, you can hand it in using the handin91 command from a terminal on any of the CS lab machines.
Only one of you or your partner should run handin91 to submit your joint solutions. If you accidentally both run it, send me email right away letting me know which of the two solutions I should keep and which I should discard.
You may run handin91 as many times as you like, and only the most recent submission will be recorded. This is useful if you realize, after handing in some programs, that you'd like to make a few more changes to them.