https://zhuanlan.zhihu.com/p/30935141
还记得我2013年考下CCIE RS后,在国外一个技术论坛偶然读到了一篇介绍SDN的文章,作者把SDN写得神乎其神,中心思想就是:完全靠网络工程师手动配置和手动排错,效率低下的传统网络迟早有”寿终正寝“的一天,而取而代之的就是能够带来”革命性改变“的SDN。的确,IT技术日新月异,当年CCIE RS v1 v2考试大纲里的那些古董级别的Apple Talk, FDDI, Token Ring, X.25, ATM等等,现在还有几个人有兴趣去花时间理解它们?由此自己开始关注Software Defined Network (软件定义网络)。
本篇文章是我2014年自学Mininet时的一些心得和笔记,温故知新,如今回味起来依然能学到不少东西:
1. SDN和传统网络最大的区别在于:SDN具有灵活的软件编程能力,让网络的自动化管理和控制能力获得空前的提升,能够有效地解决当前网络系统所面临的资源规模扩展受限、组网灵活性差的问题。
2. 传统网络设备的Control Plane和Data Plane在SDN中被完全拆开,互不干涉。
3. SDN的转发机制不再是Destination-based,而是Flow-based。
4. SDN的Control logic由SDN Controller(类似于现在的IOS之类的命令行操作系统,但运行方式不同)掌控。
5. Openflow, Opendaylight, OpenContrail等都是SDN的一部分。
6. 。。。。。。。。。。。
理论太多,不想赘述,因为学再多理论也比不过亲自动手尝试,还好最近找到了一款叫做Mininet的东西,这对所有SDN的初学者是一个福音。作为一个轻量级网络研究平台,Mininet已经出现很多年了,有志于研究SDN(openflow)的都知道它的来历和用途,关于Mininet的背景就不多做介绍了。下面是自己使用Mininet时做的一些笔记:
安装Mininet的步骤:
1. 下载Mininet(版本2.1.0)的镜像文件 (https://github.com/mininet/mininet/wiki/Mininet-VM-Images),这是一个基于Ubuntu的虚拟机文件。
2. 用VMware或者Virtual Box打开
Mininet使用笔记:
1. 学习Mininet之前,最好将Mininet官方的Walkthrough过一遍(http://mininet.org/walkthrough/)
2. sudo mn命令将创建一个最简单的拓扑,包括一个SDN Controller (c0),一个交换机 (s1),两台主机 (h1和h2)
3. Mininet几个比较重要的选项和参数总结如下:
--topo= 这个是Mininet创建的虚拟网络的拓扑,有4种类型:
minimal – 即上面提到的sudo mn命令,包括一个SDN Controller (c0),一个交换机 (s1),两台主机 (h1和h2)
single,X – 一个交换机,下面直连X个主机(自已定义)
linear,X – 创建X个环状链路的交换机,每个交换机下面直连一个主机
tree,X – 树状型拓扑,有X个fanout
--switch= 创建不同类型的交换机
ovsk – Mininet默认自带的Open vSwitch,已经预装在VM里面
user – 比ovsk慢很多,不推荐使用
--controller= 即SDN Controller,三个参数
ovsc – Mininet默认自带的OVS Controller,已经预装在VM里面
nox – 顾名思义,NOX controller
remote – 不创建Controller,尝试连接外部Controller
--mac 创建自定义的MAC地址
来做个实验具体说明,首先创建一个交换机,3个主机,无Controller的拓扑。
命令: Sudo mn –topo=single,3 –mac –controller=remote
在SDN中,交换机是没有Control Plane的,也就是说它仅是一个纯粹的转发设备, 并且这种”无脑型“的Openflow交换机只有在收到SDN controller的指示后,才能做出转发决定。遇到未知traffic时,Openflow交换机只会做一件事:就是把它们转发给SDN controller,自己什么也不管。这大大降低了习惯在传统网络的交换机中做各种2层排错的网工们的工作量。
既然Controller是SDN网络的大脑,那么创建一个没有controller的SDN拓扑还能玩吗?当然可以,这里要用到dptcl这个工具,dptcl的作用是可以跳过controller,直接通过TCP 6634这个端口来控制和查看openflow交换机的flow table(记住SDN网络的转发机制是flow-based,不是destination-based),不过dptcl和SDN controller是完全不同的两种东西,不能划等号,这点切记。
DPTCL的命令格式:
dptcl [show/dump-flows/add-flow] tcp:127.0.0.1:6634
最终实验拓扑如下:
具体配置和验证命令:
1. 开启Wireshark,让它在后台运行
mininet@mininet-vm:~$ sudo wireshark
2. 创建一个交换机(ovsk类型),3个主机,无Controller的SDN网络
mininet@mininet-vm:~$ sudo mn --topo=single,3 --mac --switch=ovsk --controller=remote
*** Creating network
*** Adding controller
Unable to contact the remote controller at 127.0.0.1:6633 //无controller的拓扑
*** Adding hosts:
h1 h2 h3
*** Adding switches:
s1
*** Adding links:
(h1, s1) (h2, s1) (h3, s1)
*** Configuring hosts
h1 h2 h3
*** Starting controller
*** Starting 1 switches
s1*** Starting CLI:
mininet>
3. 查看网络节点
mininet> nodes
available nodes are:
c0 h1 h2 h3 s1
mininet>
4. 查看物理拓扑
mininet> net
h1 h1-eth0:s1-eth1
h2 h2-eth0:s1-eth2
h3 h3-eth0:s1-eth3
s1 lo: s1-eth1:h1-eth0 s1-eth2:h2-eth0 s1-eth3:h3-eth0
c0
mininet>
5. 查看各个节点的信息
mininet> dump
mininet>
验证SDN交换机工作原理
Scenario #1 (SDN交换机flow table为空)
1. 首先在三个主机(h1,h2,h3)上开启Xterm (Windows用户需要安装Xming,并在putty里开启X-forwarding)
mininet> xterm h1 h2 h3
2. 用dpctl查看交换机当前的flow table信息 (由于还没有手动添加flow entry,该flow table为空)
mininet> dpctl dump-flows
*** s1 --------------------------------------------
NXST_FLOW reply (xid=0x4):
3. 在h1上ping h2,在h2上用tcpdump抓包,查看结果
结论: Ping失败,h2上没有收到任何ICMP echo request packet.
原因: 拓扑里没有SDN controller,我们也没有用dptcl给openflow交换机添加任何flow entry, 所以交换机不会做转发决定,并直接丢弃h1到h2的ping包。
Scenario #2 (为SDN交换机添加flow entry)
1. 用dpctl给SDN交换机添加双向的flow entry, 因为ping包除了echo request还有echo reply
mininet@mininet-vm:~$ dpctl add-flow tcp:127.0.0.1:6634 in_port=1,actions=output:2
mininet@mininet-vm:~$ dpctl add-flow tcp:127.0.0.1:6634 in_port=2,actions=output:1
2. 查看SDN交换机的flow table,两条flow entry添加成功。
mininet> dpctl dump-flows
*** s1 --------------------------------------------
NXST_FLOW reply (xid=0x4):
cookie=0x0, duration=27,213s, table=0, n_packets=5, n_bytes=378, idle_timeout=60, idle_age=7, in_port=1 actions=output:2
cookie=0x0, duration=27,213s, table=0, n_packets=5, n_bytes=378, idle_timeout=60, idle_age=7, in_port=2 actions=output:1
3. 在h1上ping h2,在h2和h3上用tcpdump抓包,查看结果
结论:h1成功ping到h2,并且h3没收到任何ping包。
其他一些常用dpctl命令及功能 (拓扑同上)
1. 关闭或开启openflow交换机的端口(等于对一个端口shutdown / no shutdown)
dpctl mod-port [port num] up/down
2. 关闭交换机的端口1(下接h1),
mininet> dpctl mod-port 1 down
关闭端口1后再来h1 ping h2,结果当然fail
*** s1 ------------------------------------------------------------------------
mininet> h1 ping h2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
--- 10.0.0.2 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2000ms
3. 开启交换机的端口1
mininet> dpctl mod-port 1 up
ping成功
*** s1 ------------------------------------------------------------------------
mininet> h1 ping -c 2 h2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=8.37 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=1.02 ms
--- 10.0.0.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 1.026/4.698/8.370/3.672 ms
4. 查看端口的统计信息,包括Tx,Rx counters, bytes以及Error counters等等
mininet> dpctl dump-ports
*** s1 ------------------------------------------------------------------------
OFPST_PORT reply (xid=0x2): 4 ports
port 3: rx pkts=7, bytes=558, drop=0, errs=0, frame=0, over=0, crc=0
tx pkts=0, bytes=0, drop=0, errs=0, coll=0
port 1: rx pkts=32, bytes=2076, drop=0, errs=0, frame=0, over=0, crc=0
tx pkts=14, bytes=1092, drop=0, errs=0, coll=0
port 2: rx pkts=33, bytes=2154, drop=0, errs=0, frame=0, over=0, crc=0
tx pkts=26, bytes=1596, drop=0, errs=0, coll=0
port LOCAL: rx pkts=0, bytes=0, drop=0, errs=0, frame=0, over=0, crc=0
tx pkts=0, bytes=0, drop=0, errs=0, coll=0
5. 查看端口的一层和二层信息
mininet> dpctl show
*** s1 ------------------------------------------------------------------------
OFPT_FEATURES_REPLY (xid=0x2): dpid:0000000000000001
n_tables:254, n_buffers:256
capabilities: FLOW_STATS TABLE_STATS PORT_STATS QUEUE_STATS ARP_MATCH_IP
actions:
OUTPUT SET_VLAN_VID SET_VLAN_PCP STRIP_VLAN SET_DL_SRC SET_DL_DST
SET_NW_SRC SET_NW_DST SET_NW_TOS SET_TP_SRC SET_TP_DST ENQUEUE
1(s1-eth1): addr:66:9a:a3:e0:64:8f
config: 0
state: 0
current: 10GB-FD COPPER
speed: 10000 Mbps now, 0 Mbps max
2(s1-eth2): addr:26:bb:36:e0:99:4e
config: 0
state: 0
current: 10GB-FD COPPER
speed: 10000 Mbps now, 0 Mbps max
3(s1-eth3): addr:76:c9:ca:0e:92:4b
config: 0
state: 0
current: 10GB-FD COPPER
speed: 10000 Mbps now, 0 Mbps max
LOCAL(s1): addr:fe:3f:bf:f6:26:42
config: 0
state: 0
speed: 0 Mbps now, 0 Mbps max
OFPT_GET_CONFIG_REPLY (xid=0x4): frags=normal miss_send_len=0
开篇时曾提到:默认情况下,SDN交换机的flow table为空,在没有controller的情况下,可以使用dpctl来查询和管理交换机的flow table,之前的实验里我用dpctl给交换机加了两个flow,让h1可以ping通h2。两条命令如下:
dpctl add-flow in_port=1,actions=output:2
dpctl add-flow in_port=2,actions=output:1
第一条命令的意思是:用dpctl对交换机添加flow,让交换机从s1-eth1这个端口接收到的所有traffic都从s1-eth2这个端口发出去。
第二条命令的意思是:用dpctl对交换机添加flow,让交换机从s1-eth2这个端口接收到的所有traffic都从s1-eth1这个端口发出去。(这里重点强调“所有traffic",原因后面解释)
添加这两条flow后,h1能够ping通h2.
mininet> h1 ping h2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=3.80 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.915 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=1.50 ms
^C
--- 10.0.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2005ms
rtt min/avg/max/mdev = 0.915/2.073/3.805/1.248 ms
除了这种匹配所有traffic的方法外,dpctl还允许自定义更详细的traffic类型,比如ARP,IPv4, IPv6, MPLS等等,用dpctl的命令来匹配这些traffic不难,关键是要弄懂交换机是怎样识别它收到的traffci是属于哪种类型的,从而参照自己的flowtable然后对该traffic进行转发。要弄懂这点,就必须了解EtherType(以太网类型字段)这个东西,首先来回顾一下二层帧(frame)的结构:
如图,我已经把EtherType标记出来了,它就在Source MAC字段的后面。
根据IEEE 802.3定义,EtherType字段长度为2Byte,它的作用是用来指明应用于Payload这个字段里的是什么协议,它的起始值是0x0800,指代的是IPv4这个协议,常见的EtherType数值和所对应的协议如下:
0x0800 = IPv4
0x0806 = ARP
0x86DD = IPv6
0x8847 = MPLS (unicast)
0x8848 = MPLS (multicast)
在dpctl里面,我们使用dl_type来指代EtherType,下面来做个试验,具体说明一下怎么在dpctl里面根据EtherType来自定义traffic的协议类型。
首先把之前添加的两条匹配所有traffic的flow拿掉,这里用dpctl del-flows这个命令,删除后用dpctl dump-flows验证,确保交换机flow table为空。
mininet> dpctl del-flows
*** s1 ------------------------------------------------------------------------
mininet> dpctl dump-flows
*** s1 ------------------------------------------------------------------------
NXST_FLOW reply (xid=0x4):
然后用dpctl给交换机添加两条traffic类型为IPv4的flow,命令如下:
dpctl add-flow dl_type=0x0800,nw_dst=10.0.0.2,actions=output:2
dpctl add-flow dl_type=0x0800,nw_dst=10.0.0.1,actions=output:1
第一条命令的意思是:用dpctl对交换机添加flow,让交换机把所有EtherType为0x0800(IPv4)并且destiation IP为10.0.0.2的traffic从s1-eth2这个端口发出去。
第二条命令的意思是:用dpctl对交换机添加flow,让交换机把所有EtherType为0x0800(IPv4)并且destiation IP为10.0.0.1的traffic从s1-eth1这个端口发出去。
添加完后验证一下交换机的flow table:
mininet> dpctl dump-flows
*** s1 ------------------------------------------------------------------------
NXST_FLOW reply (xid=0x4):
cookie=0x0, duration=477.255s, table=0, n_packets=0, n_bytes=0, idle_age=477, ip,nw_dst=10.0.0.2 actions=output:2
cookie=0x0, duration=469.45s, table=0, n_packets=0, n_bytes=0, idle_age=469, ip,nw_dst=10.0.0.1 actions=output:1
然后再次尝试h1是否能ping通h2
mininet> h1 ping -c 3 h2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
--- 10.0.0.2 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2013ms
结论:Ping失败!
原因:众所周知,处在同一网段下的host,它们之间的交流是L2 forwarding,是要靠ARP来解析MAC地址的,之前我们只匹配了0x0800 (IPv4) 这个协议,并没有匹配到0x0806(ARP),这样当交换机收到h1的ARP包后,因为没有controller,flow table里面也没有相应的flow告诉它如何转发这个ARP包,交换机只能将它丢弃,从而导致h1 ping h2失败。
添加ARP的dpctl命令如下:
dpctl add-flow dl_type=0x0806,actions=NORMAL
这条命令的意思是:用dpctl对交换机添加flow,让交换机以NORMAL形式(即广播)将所有ARP包从各个端口广播出去。
老规矩,添加完flow后,马上验证flow table
mininet> dpctl dump-flows
*** s1 ------------------------------------------------------------------------
NXST_FLOW reply (xid=0x4):
cookie=0x0, duration=1288.291s, table=0, n_packets=22, n_bytes=2156, idle_age=703, ip,nw_dst=10.0.0.2 actions=output:2
cookie=0x0, duration=1280.486s, table=0, n_packets=8, n_bytes=784, idle_age=727, ip,nw_dst=10.0.0.1 actions=output:1
cookie=0x0, duration=8.772s, table=0, n_packets=0, n_bytes=0, idle_age=8, arp actions=NORMAL
再次尝试h1是否能ping通h2
mininet> h1 ping -c 3 h2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=6.34 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.991 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=1.08 ms
--- 10.0.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2006ms
rtt min/avg/max/mdev = 0.991/2.807/6.345/2.502 ms
结论:IP和ARP的flow都添加完毕后,h1 ping h2成功!
用WIRESHARK抓包,靠实战理解openflow交换机和Controller之间的工作原理。
1. 首先创建一个最简单的拓扑,1个Controller, 1个交换机,2台HOST
mininet@mininet-vm:~$ sudo mn
*** Creating network
*** Adding controller
*** Adding hosts:
h1 h2
*** Adding switches:
s1
*** Adding links:
(h1, s1) (h2, s1)
*** Configuring hosts
h1 h2
*** Starting controller
*** Starting 1 switches
s1
*** Starting CLI:
mininet>
2. 重开一个putty窗口(记住enable X forwarding),ssh进入VM后开启wireshark
mininet@mininet-vm:~$ sudo wireshark &
3. Wireshark启动后,Interface List选lo0 (127.0.0.1),然后点Start,如下图
4.回到第一个putty窗口 (mininet>)下,用pingall命令来让h1(10.0.0.1)和h2(10.0.0.2)互相ping对方,因为这次的实验拓扑已经有了一台controller,所以无需再用dpctl来手动对交换机添加flow,h1已经可以直接ping通h2了。
mininet> pingall
*** Ping: testing ping reachability
h1 -> h2
h2 -> h1
*** Results: 0% dropped (2/2 received)
5. 回到Wireshark,这个时候应该capture到了很多TCP包,先不管它们,在Filter里输入of进行过滤,目的是为了抓到OFP (即Openflow Protocol)的包,如下图:
6. 点开第一个包,即Echo Request (SM)(8B)这个包,如下图:
从这个包里可以看出的信息是:
a.这个Echo Request是openflow交换机(127.0.0.1:60497) 发给Controller (127.0.0.1:6633)的,之间已经提到过,dpctl是靠TCP 6634这个端口来控制交换机的,而Controller则是用TCP 6633这个端口来控制交换机,而为了监控交换机和Controller之间的connectivity,交换机会不间断地向Controller发出Echo Request (注意这个不是ICMP的Echo Request),Controller收到Echo Request包后会向交换机返回一个Echo Reply包。
b. 点开最下面的Openflow Protocol(就不截图了),可以看到OFP的一些基本信息,比如version为0x01,Type为Echo Request (SM) (2),Length为8等等等。
7. 看完交换机和Echo Request和Echo Reply包后,接下来来看刚才用pingall命令,让h1 (10.0.0.1)和(10.0.0.2)互ping后出现的包。
如上图,在我抓的包里面:
a. 把序号为436,437,439,440的包分为一组,这一组是10.0.0.1 ping 10.0.0.2的Ping Echo Request以及Ping Echo Reply的包。
b. 把序号为441,442,443,444的包分为一组,这一组是10.0.0.2 ping 10.0.0.1的Ping Echo Request以及Ping Echo Reply的包。
8. 点开436这个包,如下图:
这里你会感到奇怪,为什么刚才在第7步里看到的该包,Source为10.0.0.1,Destination为10.0.0.2,为什么点开该包后,里面的内容却是Source127.0.0.1:60497(交换机), Destination 127.0.0.1:6633(Controller)?要回答这个问题,就需要回到第7步去看436这个包的Protocol,你会发现它已不再是我们在传统网络里理解的那个单独的ICMP包了,在SDN里,它已经变成了OFP+ICMP包。对这个OFP+ICMP包应该这样理解:当10.0.0.1 ping 10.0.0.2的时候,“无脑”的交换机(127.0.0.1:60497) 因为不知道怎么转发这个ICMP包,它唯一能做的就是用OFP包将这个ICMP包封装,将它转发给Controller (127.0.0.1:6633) 做决定,而这个由交换机重新封装后的ICMP包就叫做OFP+ICMP包。
9. 点开437这个包, 如下图:
这个包是接436这个包的,它是由Controller收到交换机发给它的OFP+ICMP后,Controller再返回给一个OFP包给交换机,所以它的Source127.0.0.1:6634,Destination127.0.0.1:60497。这里需要重点关注的是该包OpenFlow Protocol里面的内容。如图,依次点开OpenFlow Protocol>Output Actions(s)>Action,这里可以看到Output port:2 ,顾名思义,它的意思就是说:Controller现在告诉交换机,“关于这个10.0.0.1 ping 10.0.0.2的ICMP包,你把它从s1-eth2这个端口发出去”,这样h1 (10.0.0.1)的Ping echo request包就能到达h2 (10.0.0.2)了,因为h2就是直连在s1-eth2这个端口下的。
10. 后面的438-444就无需多解释了,有网络基础的都懂,它们和436还有437这两个包其实都是一个原理,只是source和destination不同。
要把SDN学精,学会写代码是必不可少的。整个Mininet的架构基本是用Python 2.0写出来的(注意不是3.0,前后两者差别很大),而自己的Python水平大概还停留在入门阶段。任重道远,下面是自己总结的Mininet中常见的一些Class, Methods, Functions还有Variables:
Topo: 用来创建拓扑,Mininet API中最基本的Class
addSwitch(): 在拓扑中创建一个switch,并返回switch name。
addHost(): 在拓扑中创建一个host,并返回host name。
addLink(): 在拓扑中创建一个双向的link,并返回link key,默认情况下link都是双向的(bidirectional)。
Mininet: 用来创建和管理一个拓扑的Main Class。
start(): 启动网络
pingAll():所有host相互ping对方,用来测试网络连接性
stop(): 关闭网络
net.hosts: 表示拓扑内所有的host
dumpNodeConnections(): 显示指定节点(Node)的connection
setLogLevel( 'info' | 'debug' | 'output' ): 设定Mininet默认的ouput level,一般用info。
举个简单的例子,用python写一个单交换机,下接N个host的拓扑的代码:
from mininet.topo import Topo
from mininet.net import Mininet
from mininet.util import dumpNodeConnections
from mininet.log import setLogLevel
class SingleSwitchTopo(Topo):
def __init__(self, n=2, **opts):
Topo.__init__(self, **opts) # 初始化拓扑以及默认的option
switch = self.addSwitch('s1') # 添加一个名为s1的交换机
for h in range(n):
host = self.addHost('h%s' % (h + 1)) #添加主机
self.addLink(host, switch) #添加双向连接
def simpleTest():
topo = SingleSwitchTopo(n=4)
net = Mininet(topo) #用Main Class来创建拓扑
net.start() #启动网络
print "Dumping host connections"
dumpNodeConnections(net.hosts) #显示拓扑内所有节点(host)的connection信息
print "Testing network connectivity"
net.pingAll() #所有host相互ping对方,用来测试网络连接性
net.stop()
if __name__ == '__main__':
setLogLevel('info') # 设置 Mininet 默认输出级别为info
simpleTest()
验证:
# python test-single.py
*** Creating network
*** Adding controller
*** Adding hosts:h1 h2 h3 h4
*** Adding switches:
s1
*** Adding links:
(h1, s1) (h2, s1) (h3, s1) (h4, s1)
*** Configuring hosts
h1 h2 h3 h4
*** Starting controller
*** Starting 1 switches
s1
Dumping host connections
h1 h1-eth0:s1-eth1
h2 h2-eth0:s1-eth2
h3 h3-eth0:s1-eth3
h4 h4-eth0:s1-eth4
Testing network connectivity
*** Ping: testing ping reachability
h1 -> h2 h3 h4
h2 -> h1 h3 h4
h3 -> h1 h2 h4
h4 -> h1 h2 h3
*** Results: 0% dropped (12/12 received)