This is part 5 in the series of building an OpenFlow switch on the Raspberry Pi.
On in part 4 we set-up Ryu to be an L2 switch and it applied flow rules to the LINC switch. The traffic source which triggered the rules was port eth0 which effectively is the control port since this connects the Raspberry Pi to my network and ultimately to the Ryu controller. The flows were therefore applied as a result of noise and chatter on the local LAN.
For our first very simple experiment we need to have a more controlled environment so let's modify the LINC config so that eth0 is solely to connect the switch to the controller.
To shutdown LINC
(linc@raspberrypi)2> init:stop().
Let's edit the LINC config file
sudo vi /home/pi/LINC-Switch/rel/linc/releases/1.0/sys.config
{ports,
[
%% - regular hardware interface
{port, 1, [{interface, "eth1"}]},
{port, 2, [{interface, "eth2"}]},
%% {port, 4, [{interface, "eth0"}]}
{port, 3, [{interface, "wlan0"}]}
%% - hardware interface with explicit type
Comment out port 4 (I moved it as the comma on the last entry for port 3 causes the config file to fail).
Restart the switch
pi@raspberrypi ~/LINC-Switch $ sudo rel/linc/bin/linc console
So I now have 3 ports for traffic on the Pi. I need a traffic source so I connected a laptop via a cable to eth1. There is no DHCP so no IP addresses on the laptop will need be assigned so you will need to change the IP address on the laptop to be a static IP address. I set mine to 192.168.1.130/24 with a default gateway of 192.168.1.1.
So the controller spots the laptop
> installing new source mac received from port 1
If we now look at the flow tables on the switch we can see what's happening in more detail and understand.
Let's view LINCs flow table:
(linc@raspberrypi)1> ets:tab2list(linc:lookup(0, flow_table_0)).
[{flow_entry,{0,#Ref<0.0.0.374>},
0,
{ofp_match,[]},
<<0,0,0,0,0,0,0,0>>,
[],
{1370,616692,527826},
{infinity,0,0},
{infinity,0,0},
[{ofp_instruction_write_actions,4,
[{ofp_action_output,16,controller,65535}]}]},
{flow_entry,{123,#Ref<0.0.0.381>},
123,
{ofp_match,[{ofp_field,openflow_basic,in_port,false,
<<0,0,0,1>>,
undefined},
{ofp_field,openflow_basic,eth_src,false,
<<32,207,48,0,192,96>>,
undefined}]},
<<0,0,0,0,0,0,0,0>>,
[],
{1370,616842,124920},
{infinity,0,0},
{infinity,0,0},
[{ofp_instruction_goto_table,6,1}]}]
The line with
{ofp_field,openflow_basic,eth_src,false,
<<32,207,48,0,192,96>>,
undefined}]},
This is the laptop's MAC address in decimal notation 20:CF:30:00:C0:60
(linc@raspberrypi)1> ets:tab2list(linc:lookup(0, linc_ports)).
[{linc_port,1,<0.164.0>},
{linc_port,2,<0.161.0>},
{linc_port,3,<0.157.0>}]
OK. LINC isn't the most user friendly if you are a network engineer. There are plans to improve this and adopt a more familiar user interface like Cisco IOS.
Right. Let's stop ryu and install a really simple controller configuration to show how things work.
ryu is written in python. I have to admit that it's taking me a while to get used to python syntax having used c, php and other languages that use {} structures for function declarations. Python uses just space or tabs to identify what's a function ! Seems crazy to me but that's how it's done.
## Simple ryu layer 2 hub
## All packets arriving at the OpenFlow switch are passed to the controller
## The controller simply floods all incoming messages out of all ports on the switch
## You would never do this in reality!
## No flows are installed on the switch to remember how to handle packets
from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
class L2Switch(app_manager.RyuApp):
def __init__(self, *args, **kwargs):
super(L2Switch, self).__init__(*args, **kwargs)
## set_ev_cls decorator does all the work. Incoming packets referred to EventOFPPacketIn
@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
## packet_in_handler defines rules which are processed when a packet arrives
def packet_in_handler(self, ev):## All below is part of the packet_in_handler function
## These are datastructures for the incoming message
## ev.msg represents a packet_in
msg = ev.msg
## msg.dp reepresents the datapath for the switch
dp = msg.datapath
## dp.ofproto represents the protocol to the switch which was negotiated
ofp = dp.ofproto
ofp_parser = dp.ofproto_parser
## OFPActionOutput(arg) is which port the message should be sent out of
## OFPP_FLOOD refers to all ports or a flood
actions = [ofp_parser.OFPActionOutput(ofp.OFPP_FLOOD)]
## Build the packet to send using OFPPacketOut
out = ofp_parser.OFPPacketOut(
datapath=dp, buffer_id=msg.buffer_id, in_port=msg.in_port,
actions=actions)
## Send the built packet
dp.send_msg(out)
You would never actually use OpenFlow like this. Here's what it does.
A packet arrives at the switch. The switch checks what rules (flows) have been defined for the arriving packet. The controller hasnt actually installed any so it then refers the packet to the ryu controller.
ryu now dissects the packet passed over the OpenFlow protocol and the above programme tells ryu how to process packets.
The function packet_in_handler is called.
The key line here is
actions = [ofp_parser.OFPActionOutput(ofp.OFPP_FLOOD)]
What this is actually saying is to send the arriving packet to all interfaces. We are building a hub which is exactly what it does - it floods arriving packets to all ports. The final line commits this.
Now we would never do this in reality since it is a massive overhead. The switch will copy every single packet to the controller asking what to do with it. The switch never learns anything!
Now given in a real network the controller may be remote from the switch, you can see this would introduce massive latency and massive traffic duplication !
The point of this is really to show the logic of how OpenFlow works.
We can run ryu with more verbose logging to see more about what it is doing
ryu-manager --verbose l2hub.py
Here's what it comes back with:
loading app l2hub.py
loading app ryu.controller.ofp_handler
instantiating app l2hub.py
instantiating app ryu.controller.ofp_handler
BRICK ofp_event
PROVIDES EventOFPPacketIn TO {'L2Switch': ['main']}
CONSUMES EventOFPEchoRequest
CONSUMES EventOFPErrorMsg
CONSUMES EventOFPHello
CONSUMES EventOFPSwitchFeatures
BRICK L2Switch
CONSUMES EventOFPPacketIn
connected socket:<socket fileno=4 sock=192.168.1.4:6633 peer=192.168.1.15:45743> address:('192.168.1.15', 45743)
hello ev <ryu.controller.ofp_event.EventOFPHello object at 0xf7e510>
move onto config mode
switch features ev version: 0x4 msg_type 0x6 xid 0x70f534ec
move onto main mode
EVENT ofp_event->L2Switch EventOFPPacketIn
Ignore the reference to L2Switch - this is a hub. L2Switch is from the class declaration at the beginning - I copied this example.
You can see ryu is initialising, then it connects to the Raspberry Pi OpenFlow switch running at 192.168.15
It negotiates to use the OF1.3 protocol (0x04)
The Raspberry Pi will report it has also connected to the controller.
16:07:49.296 [info] Connected to controller 192.168.1.4:6633/0 using OFP v4
Now on the laptop connected to the Raspberry Pi, if I set ping running to ping some address, this will be forwarded to the controller. In the controller window you'll see each ping packet event showing in the verbose log
EVENT ofp_event->L2Switch EventOFPPacketIn
In the next post I'll evolve our simple hub to at least not broadcast out of every port.