生成树协议(英语:Spanning Tree Protocol,STP),是一种工作在OSI网络模型中的第二层(数据链路层)的通信协议,基本应用是防止交换机冗余链路产生的环路.用于确保以太网中无环路的逻辑拓扑结构.从而避免了广播风暴,大量占用交换机的资源。
在SDN中,如果Mininet建立的拓扑中存在交换机环路,则如果利用普通的Ryu Learning Switch APP进行ryu-manager部署,会出现ping、pingall不通的问题,其原因在于环路中出现了广播风暴。
为了在Mininet中使用带有环路的拓扑,需要让交换机开启STP协议,本文介绍开启STP协议的两种方法。
在Ryu的simple_switch_13模块上进行改进,则可以启用STP协议。
改动的地方主要有三:
1、需要import stplib以支持STP协议,如下:
from ryu.lib import stplib
2、需要在交换机类(Class SimpleSwitch13)中指定STP协议参考类,如下添加:
_CONTEXTS = {'stplib': stplib.Stp}
3、需要修改PacketInHandler(处理packet_in事件的函数)的执行时机,即,将该函数之前的@set_ev_cls中的ofp_event.EventOFPPacketIn改成stplib.EventPacketIn,如下:
@set_ev_cls(stplib.EventPacketIn, MAIN_DISPATCHER)
def _packet_in_handler(self, ev):
......
这样做的原因是,stplib的EventPacketIn也封装了EventOFPPacketIn,且stplib的EventPacketIn能够处理BDPU封包(实现STP学习的关键,通过传送该包发现环路,决定哪些端口需要Forward,哪些需要Block)。
如果是ofp_event类中的EventOFPPacketIn,则无法处理BDPU包。
附录中展示了基于控制器启动STP协议的代码,包含Ryu的L2、L3、L4层转发。
我们也可以直接使用未启用STP协议的控制器,连接在Mininet中启用STP协议的OpenvSwitch。
OpenvSwitch启用STP协议的命令为:
sudo ovs-vsctl set bridge s1 stp_enable=true
注意:
1、s1为交换机(网桥)的名称;
2、true的首字母必须小写。
如果是在Mininet命令行中,则该命令为:
sh sudo ovs-vsctl set bridge s1 stp_enable=true
当然,也可以将该命令封装在Mininet python脚本中,如下:
s1.cmd('ovs-vsctl set bridge s1 stp_enable=true')
上述命令启用了OVS的STP协议功能,无需针对STP协议定制的Ryu控制器应用,就能实现STP协议,避免环路。
上述的两种方法都需要等待一段时间,等待STP协议学习了拓扑结构,决定了哪些端口Forward或Block之后,才能联通,否则是ping不通的。
from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3
from ryu.lib import dpid as dpid_lib
from ryu.lib import stplib
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet
from ryu.app import simple_switch_13
class SimpleSwitch13(simple_switch_13.SimpleSwitch13):
OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
_CONTEXTS = {'stplib': stplib.Stp}
def __init__(self, *args, **kwargs):
super(SimpleSwitch13, self).__init__(*args, **kwargs)
self.mac_to_port = {}
self.stp = kwargs['stplib']
# Sample of stplib config.
# please refer to stplib.Stp.set_config() for details.
config = {dpid_lib.str_to_dpid('0000000000000001'):
{'bridge': {'priority': 0x8000}},
dpid_lib.str_to_dpid('0000000000000002'):
{'bridge': {'priority': 0x9000}},
dpid_lib.str_to_dpid('0000000000000003'):
{'bridge': {'priority': 0xa000}}}
self.stp.set_config(config)
def delete_flow(self, datapath):
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
for dst in self.mac_to_port[datapath.id].keys():
match = parser.OFPMatch(eth_dst=dst)
mod = parser.OFPFlowMod(
datapath, command=ofproto.OFPFC_DELETE,
out_port=ofproto.OFPP_ANY, out_group=ofproto.OFPG_ANY,
priority=1, match=match)
datapath.send_msg(mod)
@set_ev_cls(stplib.EventPacketIn, MAIN_DISPATCHER)
def _packet_in_handler(self, ev):
msg = ev.msg
datapath = msg.datapath
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
in_port = msg.match['in_port']
pkt = packet.Packet(msg.data)
eth = pkt.get_protocols(ethernet.ethernet)[0]
dst = eth.dst
src = eth.src
dpid = datapath.id
self.mac_to_port.setdefault(dpid, {})
self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port)
# learn a mac address to avoid FLOOD next time.
self.mac_to_port[dpid][src] = in_port
if dst in self.mac_to_port[dpid]:
out_port = self.mac_to_port[dpid][dst]
else:
out_port = ofproto.OFPP_FLOOD
actions = [parser.OFPActionOutput(out_port)]
# install a flow to avoid packet_in next time
if out_port != ofproto.OFPP_FLOOD:
match = parser.OFPMatch(in_port=in_port, eth_dst=dst)
self.add_flow(datapath, 1, match, actions)
data = None
if msg.buffer_id == ofproto.OFP_NO_BUFFER:
data = msg.data
out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
in_port=in_port, actions=actions, data=data)
datapath.send_msg(out)
@set_ev_cls(stplib.EventTopologyChange, MAIN_DISPATCHER)
def _topology_change_handler(self, ev):
dp = ev.dp
dpid_str = dpid_lib.dpid_to_str(dp.id)
msg = 'Receive topology change event. Flush MAC table.'
self.logger.debug("[dpid=%s] %s", dpid_str, msg)
if dp.id in self.mac_to_port:
self.delete_flow(dp)
del self.mac_to_port[dp.id]
@set_ev_cls(stplib.EventPortStateChange, MAIN_DISPATCHER)
def _port_state_change_handler(self, ev):
dpid_str = dpid_lib.dpid_to_str(ev.dp.id)
of_state = {stplib.PORT_STATE_DISABLE: 'DISABLE',
stplib.PORT_STATE_BLOCK: 'BLOCK',
stplib.PORT_STATE_LISTEN: 'LISTEN',
stplib.PORT_STATE_LEARN: 'LEARN',
stplib.PORT_STATE_FORWARD: 'FORWARD'}
self.logger.debug("[dpid=%s][port=%d] state=%s",
dpid_str, ev.port_no, of_state[ev.port_state])
# -*- coding:utf-8 -*-
'''
basic_switch STP version
'''
from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet
from ryu.lib.packet import ether_types
from ryu.lib.packet import ipv4
# STP settings.
# See /usr/local/lib/python2.7/dist-packages/ryu/lib/stplib.py
# Line 198: In function [def _set_logger], Disable code annotation to enable STP INFO.
# Line 301: Add [port.port_no == 4294967294] to ignore controller port.
from ryu.lib import stplib
class SimpleSwitch13(app_manager.RyuApp):
OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
# Enable STP
_CONTEXTS = {'stplib': stplib.Stp}
def __init__(self, *args, **kwargs):
super(SimpleSwitch13, self).__init__(*args, **kwargs)
self.mac_to_port = {}
self.stp = kwargs['stplib']
@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
def switch_features_handler(self, ev):
datapath = ev.msg.datapath
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
# install table-miss flow entry
#
# We specify NO BUFFER to max_len of the output action due to
# OVS bug. At this moment, if we specify a lesser number, e.g.,
# 128, OVS will send Packet-In with invalid buffer_id and
# truncated packet data. In that case, we cannot output packets
# correctly. The bug has been fixed in OVS v2.1.0.
match = parser.OFPMatch()
actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
ofproto.OFPCML_NO_BUFFER)]
self.add_flow(datapath, 0, match, actions)
def add_flow(self, datapath, priority, match, actions, buffer_id=None):
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
actions)]
if buffer_id:
mod = parser.OFPFlowMod(datapath=datapath, buffer_id=buffer_id,
priority=priority, match=match,
instructions=inst)
else:
mod = parser.OFPFlowMod(datapath=datapath, priority=priority,
match=match, instructions=inst)
datapath.send_msg(mod)
@set_ev_cls(stplib.EventPacketIn, MAIN_DISPATCHER)
def _packet_in_handler(self, ev):
# If you hit this you might want to increase
# the "miss_send_length" of your switch
msg = ev.msg
datapath = msg.datapath
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
in_port = msg.match['in_port']
pkt = packet.Packet(msg.data)
eth = pkt.get_protocols(ethernet.ethernet)[0]
if eth.ethertype == ether_types.ETH_TYPE_LLDP:
# ignore lldp packet
return
dst = eth.dst
src = eth.src
dpid = datapath.id
self.mac_to_port.setdefault(dpid, {})
# self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port)
# learn a mac address to avoid FLOOD next time.
self.mac_to_port[dpid][src] = in_port
if dst in self.mac_to_port[dpid]:
out_port = self.mac_to_port[dpid][dst]
else:
out_port = ofproto.OFPP_FLOOD
actions = [parser.OFPActionOutput(out_port)]
# install a flow to avoid packet_in next time
if out_port != ofproto.OFPP_FLOOD:
# check IP Protocol and create a match for IP
if eth.ethertype == ether_types.ETH_TYPE_IP:
ip = pkt.get_protocol(ipv4.ipv4)
srcip = ip.src
dstip = ip.dst
match = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_IP,
ipv4_src=srcip,
ipv4_dst=dstip
)
# verify if we have a valid buffer_id, if yes avoid to send both
# flow_mod & packet_out
if msg.buffer_id != ofproto.OFP_NO_BUFFER:
self.add_flow(datapath, 1, match, actions, msg.buffer_id)
return
else:
self.add_flow(datapath, 1, match, actions)
data = None
if msg.buffer_id == ofproto.OFP_NO_BUFFER:
data = msg.data
out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
in_port=in_port, actions=actions, data=data)
datapath.send_msg(out)
# Add flow-entry and install it to datapath.
def add_flow(self, datapath, priority, match, actions, proto_type, buffer_id=None):
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
actions)]
idle_timeout = self.idle_timeout if proto_type else 0
# Construct flow_mod and send to datapath,
# If TCP/UDP/ICMP, set timeout, else no timeout.
if buffer_id:
mod = parser.OFPFlowMod(datapath=datapath, buffer_id=buffer_id,
priority=priority, match=match,
instructions=inst, idle_timeout=idle_timeout)
else:
mod = parser.OFPFlowMod(datapath=datapath, priority=priority,
match=match, instructions=inst, idle_timeout=idle_timeout)
datapath.send_msg(mod)
def delete_flow(self, datapath):
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
for dst in self.mac_to_port[datapath.id].keys():
match = parser.OFPMatch(eth_dst=dst)
mod = parser.OFPFlowMod(
datapath, command=ofproto.OFPFC_DELETE,
out_port=ofproto.OFPP_ANY, out_group=ofproto.OFPG_ANY,
priority=1, match=match)
datapath.send_msg(mod)
@set_ev_cls(stplib.EventTopologyChange, MAIN_DISPATCHER)
def _topology_change_handler(self, ev):
dp = ev.dp
dpid_str = dpid_lib.dpid_to_str(dp.id)
msg = 'Receive topology change event. Flush MAC table.'
# self.logger.debug("[dpid=%s] %s", dpid_str, msg)
if dp.id in self.mac_to_port:
self.delete_flow(dp)
del self.mac_to_port[dp.id]
@set_ev_cls(stplib.EventPortStateChange, MAIN_DISPATCHER)
def _port_state_change_handler(self, ev):
dpid_str = dpid_lib.dpid_to_str(ev.dp.id)
of_state = {stplib.PORT_STATE_DISABLE: 'DISABLE',
stplib.PORT_STATE_BLOCK: 'BLOCK',
stplib.PORT_STATE_LISTEN: 'LISTEN',
stplib.PORT_STATE_LEARN: 'LEARN',
stplib.PORT_STATE_FORWARD: 'FORWARD'}
'''
self.logger.debug("[dpid=%s][port=%d] state=%s",
dpid_str, ev.port_no, of_state[ev.port_state])
'''
# -*- coding:utf-8 -*-
'''
basic_switch STP version
'''
from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet
from ryu.lib.packet import ether_types
from ryu.lib import dpid as dpid_lib
from ryu.lib.packet import in_proto
from ryu.lib.packet import ipv4
from ryu.lib.packet import icmp
from ryu.lib.packet import tcp
from ryu.lib.packet import udp
# STP settings.
# See /usr/local/lib/python2.7/dist-packages/ryu/lib/stplib.py
# Line 198: In function [def _set_logger], Disable code annotation to enable STP INFO.
# Line 301: Add [port.port_no == 4294967294] to ignore controller port.
from ryu.lib import stplib
class SimpleSwitch13(app_manager.RyuApp):
# Use OpenFlow version 1.3
OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
# Enable STP
_CONTEXTS = {'stplib': stplib.Stp}
def __init__(self, *args, **kwargs):
super(SimpleSwitch13, self).__init__(*args, **kwargs)
self.mac_to_port = {}
self.stp = kwargs['stplib']
self.idle_timeout = 10
@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
def switch_features_handler(self, ev):
datapath = ev.msg.datapath
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
# Install table-miss flow entry
match = parser.OFPMatch()
actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
ofproto.OFPCML_NO_BUFFER)]
self.add_flow(datapath, 0, match, actions, 0)
@set_ev_cls(stplib.EventPacketIn, MAIN_DISPATCHER)
def _packet_in_handler(self, ev):
'''
IF a packet_in arrives, handle it to install new rules.
IF packet is TCP/UDP/ICMP, set [is_ip] = 1 else 0,
Avoiding STP rules to get a timeout.
'''
is_ip = 0
msg = ev.msg
datapath = msg.datapath
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
in_port = msg.match['in_port']
pkt = packet.Packet(msg.data)
eth = pkt.get_protocols(ethernet.ethernet)[0]
if eth.ethertype == ether_types.ETH_TYPE_LLDP:
# ignore lldp packet
return
dst = eth.dst
src = eth.src
dpid = datapath.id
self.mac_to_port.setdefault(dpid, {})
# self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port)
# Learn a mac address to avoid FLOOD next time.
self.mac_to_port[dpid][src] = in_port
if dst in self.mac_to_port[dpid]:
out_port = self.mac_to_port[dpid][dst]
else:
out_port = ofproto.OFPP_FLOOD
actions = [parser.OFPActionOutput(out_port)]
# Install a flow to avoid packet_in next time
if out_port != ofproto.OFPP_FLOOD:
# Check IP Protocol and create a match for IP
if eth.ethertype == ether_types.ETH_TYPE_IP:
ip = pkt.get_protocol(ipv4.ipv4)
srcip = ip.src
dstip = ip.dst
protocol = ip.proto
# If ICMP Protocol
if protocol == in_proto.IPPROTO_ICMP:
match = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_IP, ipv4_src=srcip, ipv4_dst=dstip, ip_proto=protocol)
is_ip = 1
# If TCP Protocol
elif protocol == in_proto.IPPROTO_TCP:
t = pkt.get_protocol(tcp.tcp)
match = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_IP, ipv4_src=srcip, ipv4_dst=dstip, ip_proto=protocol, tcp_src=t.src_port, tcp_dst=t.dst_port,)
is_ip = 1
# If UDP Protocol
elif protocol == in_proto.IPPROTO_UDP:
u = pkt.get_protocol(udp.udp)
match = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_IP, ipv4_src=srcip, ipv4_dst=dstip, ip_proto=protocol, udp_src=u.src_port, udp_dst=u.dst_port,)
is_ip = 1
# Verify if there is a valid buffer_id, if yes, avoid to send both
# Flow_mod & packet_out
if msg.buffer_id != ofproto.OFP_NO_BUFFER:
self.add_flow(datapath, 1, match, actions, is_ip, msg.buffer_id)
return
else:
self.add_flow(datapath, 1, match, actions, is_ip)
data = None
if msg.buffer_id == ofproto.OFP_NO_BUFFER:
data = msg.data
out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
in_port=in_port, actions=actions, data=data)
datapath.send_msg(out)
# Add flow-entry and install it to datapath.
def add_flow(self, datapath, priority, match, actions, proto_type, buffer_id=None):
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
actions)]
idle_timeout = self.idle_timeout if proto_type else 0
# Construct flow_mod and send to datapath,
# If TCP/UDP/ICMP, set timeout, else no timeout.
if buffer_id:
mod = parser.OFPFlowMod(datapath=datapath, buffer_id=buffer_id,
priority=priority, match=match,
instructions=inst, idle_timeout=idle_timeout)
else:
mod = parser.OFPFlowMod(datapath=datapath, priority=priority,
match=match, instructions=inst, idle_timeout=idle_timeout)
datapath.send_msg(mod)
def delete_flow(self, datapath):
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
for dst in self.mac_to_port[datapath.id].keys():
match = parser.OFPMatch(eth_dst=dst)
mod = parser.OFPFlowMod(
datapath, command=ofproto.OFPFC_DELETE,
out_port=ofproto.OFPP_ANY, out_group=ofproto.OFPG_ANY,
priority=1, match=match)
datapath.send_msg(mod)
@set_ev_cls(stplib.EventTopologyChange, MAIN_DISPATCHER)
def _topology_change_handler(self, ev):
dp = ev.dp
dpid_str = dpid_lib.dpid_to_str(dp.id)
msg = 'Receive topology change event. Flush MAC table.'
# self.logger.debug("[dpid=%s] %s", dpid_str, msg)
if dp.id in self.mac_to_port:
self.delete_flow(dp)
del self.mac_to_port[dp.id]
@set_ev_cls(stplib.EventPortStateChange, MAIN_DISPATCHER)
def _port_state_change_handler(self, ev):
dpid_str = dpid_lib.dpid_to_str(ev.dp.id)
of_state = {stplib.PORT_STATE_DISABLE: 'DISABLE',
stplib.PORT_STATE_BLOCK: 'BLOCK',
stplib.PORT_STATE_LISTEN: 'LISTEN',
stplib.PORT_STATE_LEARN: 'LEARN',
stplib.PORT_STATE_FORWARD: 'FORWARD'}
'''
self.logger.debug("[dpid=%s][port=%d] state=%s",
dpid_str, ev.port_no, of_state[ev.port_state])
'''
#!/usr/bin/python
from mininet.net import Mininet
from mininet.node import Controller, RemoteController, OVSController
from mininet.node import CPULimitedHost, Host, Node
from mininet.node import OVSKernelSwitch, UserSwitch
from mininet.node import IVSSwitch
from mininet.cli import CLI
from mininet.log import setLogLevel, info
from mininet.link import TCLink, Intf
from subprocess import call
def myNetwork():
net = Mininet( topo=None,
build=False,
ipBase='10.0.0.0/8')
info( '*** Adding controller\n' )
c0=net.addController(name='c0',
controller=RemoteController,
ip='127.0.0.1',
protocol='tcp',
port=6633)
info( '*** Add switches\n')
s2 = net.addSwitch('s2', cls=OVSKernelSwitch, dpid='0000000000000002')
s1 = net.addSwitch('s1', cls=OVSKernelSwitch, dpid='0000000000000001')
s3 = net.addSwitch('s3', cls=OVSKernelSwitch, dpid='0000000000000003')
info( '*** Add hosts\n')
h6 = net.addHost('h6', cls=Host, ip='10.0.0.6', defaultRoute=None)
h4 = net.addHost('h4', cls=Host, ip='10.0.0.4', defaultRoute=None)
h3 = net.addHost('h3', cls=Host, ip='10.0.0.3', defaultRoute=None)
h1 = net.addHost('h1', cls=Host, ip='10.0.0.1', defaultRoute=None)
h2 = net.addHost('h2', cls=Host, ip='10.0.0.2', defaultRoute=None)
h5 = net.addHost('h5', cls=Host, ip='10.0.0.5', defaultRoute=None)
info( '*** Add links\n')
net.addLink(h1, s1)
net.addLink(h2, s1)
net.addLink(h3, s2)
net.addLink(s2, h4)
net.addLink(s1, s3)
net.addLink(s1, s2)
net.addLink(s2, s3)
net.addLink(s3, h6)
net.addLink(s3, h5)
info( '*** Starting network\n')
net.build()
info( '*** Starting controllers\n')
for controller in net.controllers:
controller.start()
info( '*** Starting switches\n')
net.get('s2').start([c0])
net.get('s1').start([c0])
net.get('s3').start([c0])
info( '*** Post configure switches and hosts\n')
s2.cmd('ovs-vsctl set bridge s2 stp_enable=true')
s1.cmd('ovs-vsctl set bridge s1 stp_enable=true')
s3.cmd('ovs-vsctl set bridge s3 stp_enable=true')
CLI(net)
net.stop()
if __name__ == '__main__':
setLogLevel( 'info' )
myNetwork()