设置 OVS 交换机和虚拟接口

# 启动 OVS 服务
sudo service openvswitch-switch start

# 创建一个 OVS 交换机
sudo ovs-vsctl add-br br0

# 创建两个 veth 对
sudo ip link add veth1 type veth peer name ovs-veth1
sudo ip link add veth2 type veth peer name ovs-veth2

# 将一端 veth 接口添加到 OVS 交换机
sudo ovs-vsctl add-port br0 ovs-veth1
sudo ovs-vsctl add-port br0 ovs-veth2

# 启动接口
sudo ip link set ovs-veth1 up
sudo ip link set ovs-veth2 up

# 设置 OVS 控制器
sudo ovs-vsctl set-controller br0 tcp:127.0.0.1:6633

创建网络命名空间并移动接口

# 创建两个网络命名空间
sudo ip netns add ns1
sudo ip netns add ns2

# 将 veth1 移动到 ns1 命名空间
sudo ip link set veth1 netns ns1

# 将 veth2 移动到 ns2 命名空间
sudo ip link set veth2 netns ns2

# 在 ns1 中启动 veth1 接口并分配 IP 地址
sudo ip netns exec ns1 ip addr add 10.0.0.1/24 dev veth1
sudo ip netns exec ns1 ip link set veth1 up

# 在 ns2 中启动 veth2 接口并分配 IP 地址
sudo ip netns exec ns2 ip addr add 10.0.0.2/24 dev veth2
sudo ip netns exec ns2 ip link set veth2 up

运行 Ryu 控制器

ryu-manager simple_switch.py

测试网络连接

# 从 ns1 中 ping ns2 中的 veth2
sudo ip netns exec ns1 ping 10.0.0.2

验证流表
你可以使用 ovs-ofctl 工具查看 OVS 交换机的流表,验证 Ryu 控制器是否成功安装了流表项。

# 查看 OVS 交换机的流表
sudo ovs-ofctl dump-flows br0

应该看到类似以下的输出,表示两个接口之间的连接正常,并且流量通过 OVS 交换机:

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=0.033 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.031 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=0.032 ms

simple_switch.py 它的工作原理更接近于传统的以太网交换机,通过学习和存储 MAC 地址到端口的映射来转发数据包。因此,它的行为更像是一个 MAC 地址表(也称为转发表),而不是路由表。

# simple_switch.py
from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import MAIN_DISPATCHER, CONFIG_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

class SimpleSwitch13(app_manager.RyuApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]

    def __init__(self, *args, **kwargs):
        super(SimpleSwitch13, self).__init__(*args, **kwargs)
        # 用于存储 MAC 地址到端口的映射
        self.mac_to_port = {}

    # 处理交换机特性消息的事件处理器
    @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

        # 安装一个默认的 table-miss 流表项(即匹配不到其他流表项时的默认动作)
        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)

    # 处理 Packet-In 消息的事件处理器
    @set_ev_cls(ofp_event.EventOFPPacketIn, 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, {})

        # 学习源 MAC 地址到端口的映射
        self.mac_to_port[dpid][src] = in_port

        # 如果目的 MAC 地址在我们的映射表中,获取相应的端口
        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)]

        # 如果目的端口不是泛洪,安装一个流表项以避免下次再触发 Packet-In
        if out_port != ofproto.OFPP_FLOOD:
            match = parser.OFPMatch(in_port=in_port, eth_dst=dst)
            # 如果有有效的 buffer_id,避免发送流表项和 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)
        
        # 发送 Packet-Out 消息
        out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
                                  in_port=in_port, actions=actions, data=msg.data)
        datapath.send_msg(out)

simple_switch.py 解释:
@set_ev_cls 是 Ryu 控制器框架中的一个装饰器,用于将特定的事件处理函数与事件类型和状态机状态关联起来

  • EventOFPSwitchFeatures: 交换机首次连接到控制器时,控制器会接收到这个消息。可以在此事件中安装默认的流表项。
 @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

        # 安装一个默认的 table-miss 流表项(即匹配不到其他流表项时的默认动作)
        match = parser.OFPMatch()
        actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER, ofproto.OFPCML_NO_BUFFER)]
        self.add_flow(datapath, 0, match, actions)
  • EventOFPPacketIn: 当交换机接收到一个数据包并将其发送到控制器时,会触发这个事件。可以在此事件中处理数据包并决定转发路径。
   @set_ev_cls(ofp_event.EventOFPPacketIn, 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']

......
  • EventOFPPortStatus: 当交换机端口状态发生变化(如端口启用/禁用)时,会触发这个事件。可以在此事件中更新端口状态信息。
@set_ev_cls(ofp_event.EventOFPPortStatus, MAIN_DISPATCHER)
def port_status_handler(self, ev):
    # 处理端口状态消息

EventOFPFlowRemoved: 当流表项过期或被删除时,会触发这个事件。可以在此事件中进行清理操作或统计流量信息。

@set_ev_cls(ofp_event.EventOFPFlowRemoved, MAIN_DISPATCHER)
def flow_removed_handler(self, ev):
    # 处理流表项移除消息

EventOFPEchoRequest: 用于保持控制器与交换机之间的连接。可以在此事件中回复 Echo Reply。

@set_ev_cls(ofp_event.EventOFPEchoRequest, MAIN_DISPATCHER)
def echo_request_handler(self, ev):
    # 处理 Echo Request 消息

CONFIG_DISPATCHER: 通常在交换机刚连接到控制器时,用于处理交换机特性消息(Switch Features)。

@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
def switch_features_handler(self, ev):
    # 处理交换机特性消息

MAIN_DISPATCHER: 用于处理大多数 OpenFlow 消息,如 Packet-In、Port Status 等。

@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
def packet_in_handler(self, ev):
    # 处理 Packet-In 消息

DEAD_DISPATCHER: 用于处理连接断开或交换机失效的情况。

@set_ev_cls(ofp_event.EventOFPStateChange, DEAD_DISPATCHER)
def state_change_handler(self, ev):
    # 处理交换机状态变化消息

ev的数据结构:

ev 对象的主要属性包括:
msg: 包含了 OpenFlow 消息的详细信息。
datapath: 表示与交换机的连接。
version: OpenFlow 版本。
msg_type: 消息类型。
msg_len: 消息长度。
xid: 事务 ID。
n_buffers: 交换机的缓冲区数量。
n_tables: 交换机的流表数量。
capabilities: 交换机的能力标志。
ports: 交换机的端口信息。

datapath 是 Ryu 控制器中表示与 OpenFlow 交换机连接的对象。它封装了与交换机通信所需的各种信息和方法。datapath 对象在处理 OpenFlow 消息时非常重要,因为它提供了发送消息到交换机的接口。

datapath:主要属性:
id: 交换机的唯一标识符(通常是交换机的 datapath ID)
address: 交换机的 IP 地址和端口号
ofproto: 表示 OpenFlow 协议的常量和枚举值
ofproto_parser: 用于解析和生成 OpenFlow 消息的解析器
msg: 最近接收到的 OpenFlow 消息
ports: 交换机的端口信息

datapath:主要方法:
send_msg: 发送 OpenFlow 消息到交换机 参数:消息对象


匹配条件:

//创建一个空的匹配条件对象。
match = parser.OFPMatch()
//可以通过调用其方法来设置具体的匹配字段
match.set_ipv4_src("10.0.0.1")
match.set_tcp_dst(80)

执行动作:

actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER, ofproto.OFPCML_NO_BUFFER)]
  • parser.OFPActionOutput: 这是一个用于创建输出动作的类。输出动作指定数据包应该被发送到哪个端口。
  • ofproto.OFPP_CONTROLLER: 这是一个常量,表示控制器端口。将数据包发送到控制器端口意味着数据包将被转发到控制器进行处理。
  • ofproto.OFPCML_NO_BUFFER: 这是一个常量,表示不使用缓冲区。数据包将被完整地发送到控制器,而不是只发送数据包的一部分。

优先级:
在 OpenFlow 协议中,流表项的优先级是一个 16 位整数,范围从 0 到 65535(即 0x0000 到 0xFFFF)。优先级用于决定当多个流表项匹配同一个数据包时,应该应用哪个流表项。优先级值越高,流表项的优先级越高,越优先匹配。

def add_flow(self, datapath, priority, match, actions, buffer_id=None):
    pass

OFPInstructionActions 是 OpenFlow 协议中的一个类,用于表示一种指令(Instruction),该指令指定一组动作(Actions)在匹配条件满足时应该被执行。

inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS, actions)]

OFPFlowMod 是 OpenFlow 协议中的一个类,用于创建和发送流表项修改消息(Flow Modification Message)。这种消息用于在交换机中添加、修改或删除流表项。

class OFPFlowMod:
    def __init__(self, datapath, cookie=0, cookie_mask=0, table_id=0, command=OFPFC_ADD,
                 idle_timeout=0, hard_timeout=0, priority=0, buffer_id=OFP_NO_BUFFER,
                 out_port=OFPP_ANY, out_group=OFPG_ANY, flags=0, match=None, instructions=[]):
        # 参数初始化

参数解释

  • datapath:
    类型: Datapath
    说明: 表示连接到交换机的通道。通过这个参数,控制器可以知道消息应该发送到哪个交换机。
  • cookie:
    类型: int
    说明: 用户定义的标识符,用于标识流表项。可以用于统计和管理。
  • cookie_mask:
    类型: int
    说明: 用于掩码匹配的 cookie 值。通常在修改或删除流表项时使用。
  • table_id:
    类型: int
    说明: 流表的 ID,指定流表项应该添加到哪个流表中。默认值为 0。
  • command:
    类型: int
    说明: 指定流表项的操作类型。常见的操作类型包括添加(OFPFC_ADD)、修改(OFPFC_MODIFY)和删除(OFPFC_DELETE)。
  • idle_timeout:
    类型: int
    说明: 流表项的空闲超时时间(秒)。如果流表项在指定时间内没有匹配任何数据包,则会被删除。默认值为 0,表示没有空闲超时。
  • hard_timeout:
    类型: int
    说明: 流表项的硬超时时间(秒)。无论是否有数据包匹配,流表项在指定时间后都会被删除。默认值为 0,表示没有硬超时。
  • priority:
    类型: int
    说明: 流表项的优先级。优先级越高,流表项越优先匹配。范围从 0 到 65535。
  • buffer_id:
    类型: int
    说明: 数据包的缓冲区 ID。如果数据包存储在交换机的缓冲区中,可以使用这个 ID 直接引用数据包。默认值为 OFP_NO_BUFFER,表示没有缓冲区。
  • out_port:
    类型: int
    说明: 指定流表项的有效输出端口。用于删除流表项时,指定需要删除的流表项的输出端口。默认值为 OFPP_ANY,表示任意端口。
  • out_group:
    类型: int
    说明: 指定流表项的有效输出组。用于删除流表项时,指定需要删除的流表项的输出组。默认值为 OFPG_ANY,表示任意组。
  • flags:
    类型: int
    说明: 流表项的标志位。可以设置一些特殊的标志,例如 OFPFF_SEND_FLOW_REM 表示当流表项被删除时发送通知。
  • match:
    类型: OFPMatch
    说明: 流表项的匹配条件。定义哪些数据包应该匹配这个流表项。
  • instructions:
    类型: list
    说明: 流表项的指令列表。定义当数据包匹配流表项时应该执行的动作。

mac_to_port 数据结构:

mac_to_port
    ├── 1 (DPID)
    │   ├── 00:11:22:33:44:55 -> 1 (Port)
    │   └── 66:77:88:99:aa:bb -> 2 (Port)
    └── 2 (DPID)
        ├── aa:bb:cc:dd:ee:ff -> 3 (Port)
        └── 11:22:33:44:55:66 -> 4 (Port)

图示的实际应用

  • 学习阶段:
    当交换机 1 接收到来自端口 1 的数据包,其源 MAC 地址为 00:11:22:33:44:55,交换机会将这个 MAC 地址和端口的映射关系存储到 mac_to_port 中,即 mac_to_port1 = 1。
    类似地,当交换机 2 接收到来自端口 3 的数据包,其源 MAC 地址为 aa:bb:cc:dd:ee:ff,交换机会将这个 MAC 地址和端口的映射关系存储到 mac_to_port 中,即 mac_to_port2 = 3。
  • 转发阶段:
    当交换机 1 接收到一个目的 MAC 地址为 66:77:88:99:aa:bb 的数据包时,会查找 mac_to_port1,找到对应的端口 2,并将数据包转发到端口 2。
    如果交换机 2 接收到一个目的 MAC 地址为 11:22:33:44:55:66 的数据包时,会查找 mac_to_port2,找到对应的端口 4,并将数据包转发到端口 4。

putao
5 声望0 粉丝

推动世界向前发展,改善民生。