Linux 系统中基于 ovs 或者传统 bridge 实现端口镜像

许多商用交换机允许将流量从一个或多个端口复制到一个指定端口(通常由用户选择),以便进行监控和分析。某些模型提供了选择是仅复制传入流量还是传出流量(当然,或两者兼而有之)的选项。

这方面的典型用例是 IDS/IPS 等流量分析系统,但它也可用于故障排除

此功能有很多名称,其中包括"SPAN"、"端口镜像"、"端口监控"、"监控模式"、"漫游"等等。尽管实际的设置过程因供应商而异(甚至因型号而异),但它们最终的作用是相同的。

但是,标记(即 VLAN)数据包的镜像方式可能存在差异 ;在某些情况下,VLAN 标记会从镜像副本中剥离

由于 Linux 至少实现了两种类型的桥接(现在主要用于创建虚拟网络连接虚拟机),因此人们可能想知道端口镜像是否可行。答案是肯定的,尽管程序可能有点棘手。因此,让我们看看如何使用两种流行的桥接实现(Openvswitch 和内核内桥接)在 Linux 下设置端口镜像,最后再加上另一个麻烦。

Openvswitch

让我们从 Openvswitch 开始,这是一个新的、多平台的桥实现。

极其简化,openvswitch 使用内核模块来管理数据路径(即帧的实际转发),并将其他所有内容保存在用户空间中。守护程序 (ovs-vswitchd) 管理交换机操作(但是可以管理多个网桥,因此只需要运行一个守护程序),另一个守护程序 (ovs-ovsdb) 管理数据库,其中包含构成 ovs-vswitchd 管理的所有网桥配置的各种表。

这两个功能中的每一个都由相应的协议驱动:OpenFlow 用于管理流和数据路径(非强制性),以及 OVSDB 用于管理交换机本身(添加/删除端口、接口、网桥等,以及一般的删除和配置)。

事实上,openvswitch 的基本安装运行一个本地 OVSDB 守护进程,并且所有各种 ovs-vsctl 管理命令(包括下面显示的命令)通过 UNIX 套接字连接到这个本地 OVSDB 实例,要求它执行任务。

因此,我们有我们的桥 ovsbr0,其中三个 VM 分别连接到 vnet0、vnet1 和 vnet2(当然,如果我们有真正的物理接口,则所有内容都保持完全有效和适用)。

yaml 复制代码
# ovs-vsctl show
...
    Bridge "ovsbr0"
        Port "ovsbr0"
            Interface "ovsbr0"
                type: internal
        Port "vnet2"
            Interface "vnet2"
        Port "vnet1"
            Interface "vnet1"
        Port "vnet0"
            Interface "vnet0"
...
# ovs-vsctl list bridge ovsbr0
_uuid               : 0141452d-efc1-47f8-a3b4-24f0c2bc1c36
controller          : []
datapath_id         : "00002e454101f847"
datapath_type       : ""
external_ids        : {}
fail_mode           : []
flood_vlans         : []
flow_tables         : {}
ipfix               : []
mirrors             : [8a547c29-a171-4412-b7ed-b2a1b88815de]
name                : "ovsbr0"
netflow             : []
other_config        : {}
ports               : [1d1da575-73ac-4bac-8e81-1042da415103, a8333e72-cb12-4777-bf55-e339ff41ece1, ccd87251-f61f-47ff-84f3-9e8864e6c2d8, f66298f8-02e8-48cc-a2c8-92181bea2c56]
protocols           : []
sflow               : []
status              : {}
stp_enable          : false

需要注意的一点(除了命令名称之外)是,在 openvswitch 中,绝对所有可以引用的东西都有一个 UUID(这是设计使然)。

在本例中,我们看到交换机有三个端口(加上默认创建的"内部"端口),其 UUID 如 ports 字段(这是一个值列表)所示。

(反过来,每个端口可能并且通常由一个或多个接口组成,这些接口也是对象,并且有自己的 UUID,但这在这里无关紧要)。

为了获得我们端口的实际 UUID,我们可以使用以下命令:

shell 复制代码
# for p in vnet{0..2}; do echo "$p: $(ovs-vsctl get port "$p" _uuid)"; done
vnet0: f66298f8-02e8-48cc-a2c8-92181bea2c56
vnet1: ccd87251-f61f-47ff-84f3-9e8864e6c2d8
vnet2: a8333e72-cb12-4777-bf55-e339ff41ece1

要使用 openvswitch 进行流量镜像,首先要做的是创建一个 mirror 资源到桥上。

ruby 复制代码
# ovs-vsctl -- --id=@m create mirror name=mymirror -- add bridge ovsbr0 mirrors @m cd94ea72-bb7f-4a26-816f-983a085a4bfd

语法可能看起来有点晦涩,但并不复杂(并且在 ovs-vsctl 手册页中有很好的解释)。我们同时运行两个命令,每个命令都由 -- 引入。第一个命令创建一个名为 mymirror 的镜像,并且由于 --id=@m 部分,将其 UUID 保存在 "variable" @m 中,该 UUID 仍可用于以后的命令。我们确实在第二个命令中使用了它,它将新创建的镜像 mymirror 与桥接器 ovsbr0 相关联。

如前所述,所有内容都有一个 UUID,镜像也不例外:新镜像的 UUID 作为 (successful) 命令的结果输出。让我们检查一下:

yaml 复制代码
# ovs-vsctl list bridge ovsbr0
_uuid               : 0141452d-efc1-47f8-a3b4-24f0c2bc1c36
controller          : []
datapath_id         : "00002e454101f847"
datapath_type       : ""
external_ids        : {}
fail_mode           : []
flood_vlans         : []
flow_tables         : {}
ipfix               : []
mirrors             : [cd94ea72-bb7f-4a26-816f-983a085a4bfd]
name                : "ovsbr0"
netflow             : []
other_config        : {}
ports               : [1d1da575-73ac-4bac-8e81-1042da415103, a8333e72-cb12-4777-bf55-e339ff41ece1, ccd87251-f61f-47ff-84f3-9e8864e6c2d8, f66298f8-02e8-48cc-a2c8-92181bea2c56]
protocols           : []
sflow               : []
status              : {}
stp_enable          : false

所以一切都和以前一样,但现在我们的桥有 mirrors(因为它是一个列表,如它在方括号中所示,可以有多个)。

现在我们已经创建了 mirrors 并进入了网桥,我们应该配置它的源端口和目标端口。我们想要镜像所有进出端口 vnet0 的流量,并且我们希望将其发送到桥接端口 vnet2(可能我们有一个流量监控应用程序)。

我们必须小心这里的术语。mirrors 有一组 "source" 和 "destination" 端口,但这些端口仅指端口,即我们要镜像其流量的端口。

如果源端口集(在 openvswitch 术语中为 select_src_port )中包含端口,则其传出流量将被镜像;

如果它包含在目标端口集 (select_dst_port ) 中,则其传入 流量将被镜像。因此,如果我们想要镜像 vnet0 的传入和传出流量,则必须将其包含在两组中:

yaml 复制代码
# f66298f8-02e8-48cc-a2c8-92181bea2c56 is the UUID of vnet0
# ovs-vsctl set mirror mymirror select_src_port=f66298f8-02e8-48cc-a2c8-92181bea2c56 select_dst_port=f66298f8-02e8-48cc-a2c8-92181bea2c56
# ovs-vsctl list mirror mymirror
_uuid               : cd94ea72-bb7f-4a26-816f-983a085a4bfd
external_ids        : {}
name                : mymirror
output_port         : []
output_vlan         : []
select_all          : false
select_dst_port     : [f66298f8-02e8-48cc-a2c8-92181bea2c56]
select_src_port     : [f66298f8-02e8-48cc-a2c8-92181bea2c56]
select_vlan         : []
statistics          : {}

多亏了前面介绍的 --id=@name 功能,我们本可以做同样的事情,而不必指定 vnet0 的实际 UUID:

kotlin 复制代码
# ovs-vsctl -- --id=@vnet0 get port vnet0 -- set mirror mymirror select_src_port=@vnet0 select_dst_port=@vnet0

一般来说,这个语法更清晰、更容易,因此我们将在剩下的步骤中使用它。

如果我们想在两个方向上同时镜像 vnet0 和 vnet1,我们可以这样做:

swift 复制代码
# ovs-vsctl \
  -- --id=@vnet0 get port vnet0 \
  -- --id=@vnet1 get port vnet1 \
  -- set mirror mymirror 'select_src_port=[@vnet0,@vnet1]' 'select_dst_port=[@vnet0,@vnet1]'

所以诀窍是用我们感兴趣的端口的 UUID 填充 select_src_portselect_dst_port

到目前为止,我们已经告诉 openvswitch 我们想要镜像哪些端口。

但我们还没有说我们想要将这个镜像流量发送到哪个端口

这就是 output_port 属性的用途,该属性是将接收镜像流量的端口的 UUID。 在我们的例子中,我们知道这个端口是 vnet2,所以下面介绍了我们如何添加它:

yaml 复制代码
# ovs-vsctl -- --id=@vnet2 get port vnet2 -- set mirror mymirror output-port=@vnet2
# ovs-vsctl list mirror mymirror
_uuid               : cd94ea72-bb7f-4a26-816f-983a085a4bfd
external_ids        : {}
name                : mymirror
output_port         : a8333e72-cb12-4777-bf55-e339ff41ece1
output_vlan         : []
select_all          : false
select_dst_port     : [f66298f8-02e8-48cc-a2c8-92181bea2c56]
select_src_port     : [f66298f8-02e8-48cc-a2c8-92181bea2c56]
select_vlan         : []
statistics          : {}

因此,如果我们现在转到连接到 vnet2 的 VM,我们将看到来自 vnet0 的镜像流量。试试看。

现在我们已经看到了分步过程, 我们也可以在一个命令中完成上述所有操作也就不足为奇了(为清楚起见,重新格式化):

ini 复制代码
# ovs-vsctl \
  -- --id=@m create mirror name=mymirror \
  -- add bridge ovsbr0 mirrors @m \
  -- --id=@vnet0 get port vnet0 \
  -- set mirror mymirror select_src_port=@vnet0 select_dst_port=@vnet0 \
  -- --id=@vnet2 get port vnet2 \
  -- set mirror mymirror output-port=@vnet2
cd94ea72-bb7f-4a26-816f-983a085a4bfd

所有通过网桥的流量镜像到给定端口 的一种快速而肮脏的方法是使用镜像的 select_all 属性:

yaml 复制代码
# ovs-vsctl -- --id=@vnet2 get port vnet2 -- set mirror mymirror select_all=true output-port=@vnet2
# ovs-vsctl list mirror mymirror
_uuid               : cd94ea72-bb7f-4a26-816f-983a085a4bfd
external_ids        : {}
name                : mymirror
output_port         : a8333e72-cb12-4777-bf55-e339ff41ece1
output_vlan         : []
select_all          : true
select_dst_port     : []
select_src_port     : []
select_vlan         : []
statistics          : {tx_bytes=216769, tx_packets=1400}

Openvswitch 镜像会保留 VLAN 标记,因此接收流量时不会受到影响

要删除特定镜像,可以使用以下命令:

ruby 复制代码
# ovs-vsctl -- --id=@m get mirror mymirror -- remove bridge ovsbr0 mirrors @m

要从网桥中删除所有现有镜像:

bash 复制代码
# ovs-vsctl clear bridge ovsbr0 mirrors

传统桥接

在 Openvswitch 出现之前,Linux 几乎一直有(当然现在仍然如此)内核内桥接

这是 Linux 内核中一个更简单但功能强大的桥接实现,它提供了类似于 STP 的基本功能,但仅此而已。特别是,没有本机端口镜像功能

但不要担心:Linux 有一个强大的工具,除了许多其他功能外,它还可以镜像流量。我们谈论的是交通控制子系统(简称 tc ),它可以做各种神奇的事情。

由于它是一个通用框架,因此其功能(包括镜像)不仅限于桥接器;这意味着我们可以镜像任何接口的流量并将其发送到任何其他接口,无论它们是 phisical、virtual、是否是 bridge 的一部分等。

事实上,在此示例中,我们将镜像接口 bond0 的传入/传出流量,并将其复制到虚拟接口 dummy0(对于测试非常有用)。根据需要替换为 vnetx/vifx.y/任何内容。它的工作原理是一样的。

首先,我们做一个非常简短和简化的回顾,因为 tc 非常类似于一种黑色艺术。

Linux 中的每个接口都有一个所谓的排队规则qdisc),它基本上定义了用于将数据包从接口发送出去的标准。这适用于传出数据包;

也可以为传入流量设置 QDidic,尽管它的有用性有些有限(但它肯定用于镜像)。

这些 qdisc 通常称为"根 qdisc"(用于传出流量)和"入口 qdisc"(用于传入流量)。

所以这个想法是:为了镜像接口的流量,我们配置相关的 qdisc(根和/或入口)以在执行任何其他操作之前镜像数据包。

为此,我们需要将分类器(tc 中的过滤器)附加到相关的 qdisc。简而言之,过滤器会尝试根据某些条件匹配数据包,如果匹配成功,则对数据包执行某些操作。

让我们从代码开始,以镜像接口的传入流量,这更简单。首先要做的是为接口建立一个入口 qdisc,因为默认情况下没有:

bash 复制代码
# tc qdisc add dev bond0 ingress

这将为 bond0 创建一个入口 qdisc,并为其提供 ffff: 标识符(对于任何接口,它总是 ffff:,所以没有意外):

yaml 复制代码
# tc qdisc show dev bond0
qdisc ingress ffff: parent ffff:fff1 ----------------

现在,如前所述,我们为其附加一个过滤器。此过滤器仅匹配所有数据包,并将它们镜像到 dummy0。过滤器附加到 qdisc,因此它必须具有对父级的引用。以下是创建筛选器的语法:

python 复制代码
# tc filter add dev bond0 parent ffff: \
    protocol all \
    u32 match u8 0 0 \
    action mirred egress mirror dev dummy0

语法很晦涩难懂(在这种情况下,并不是真的可以立即理解),但基本上有 3 个部分。让我们来分析一下。第一部分是链接到接口 bond0 的父 qdisc 的过滤器创建:

sql 复制代码
tc filter add dev bond0 parent ffff:

然后是匹配规则;首先,我们说应该在任何协议上尝试匹配,因为我们想要所有流量:

css 复制代码
protocol all

这还不是实际过滤器的一部分;它只是 tc 需要知道它应该尝试将实际匹配规则应用于哪些数据包的语法的一部分(好吧,它实际上是一个过滤器,但不是 tc 意义上的)。

然后我们给出实际的过滤规则:

rust 复制代码
u32 match u8 0 0

这是用于告诉 u32 过滤器的语法,在它看到的数据包(即所有数据包)中,所有数据包都应该匹配。"u32" 通知解析器接下来是 u32 匹配,实际匹配发生在 "u8 0 0" 部分,简单来说,如果数据包的第一个字节 (u8) 与 0 进行 AND 运算,则返回 true。一些按位运算的基本知识告诉我们,任何 X 的 X AND 0 == 0,因此匹配始终为 true。

最后,该命令的第三部分指定要对匹配的数据包(同样,所有数据包)执行的操作:

action mirred egress mirror dev dummy0

这里我们使用 mirred 动作,它基本上有两种操作模式:

mirror(这就是我们在这里想要的)到,镜像数据包,

以及 redirect,到,重定向它。两者都使用 "dev" 参数中指定的设备执行其工作。

至于 "egress" 部分,这是截至撰写本文时唯一支持的模式。

如果我们想镜像多个设备,我们所要做的就是指定多个操作:

erlang 复制代码
action mirred egress mirror dev dummy0 \
action mirred egress mirror dev dummy1 ...

因此,如果您已经做到了这里,您会很高兴地知道将这些规则应用于传出流量几乎相同,只是稍微复杂一些。

问题是,与 ingress 情况不同,接口通常确实有一个 egress (outouting) qdisc,

但我们不能直接将过滤器附加到它,因为它是一个无类的 qdisc("classless"只是意味着它不能有"子"类和过滤器)。

所以首先要做的是添加一个有类的出口 qdisc;

完成此操作后,过滤器的附加方式与 Ingress QFiard 相同。

顺便说一句,在无线接口中发现的 mq qdisc 尽管声称是一流的,但似乎不支持直接过滤器连接。

如果我们添加一个有类的 qdisc,我们应该决定使用哪一个,因为它们有几个。最常见的是 PRIO、CBQ 和 HTB

其中,最简单的是 PRIO,这就是我们将在示例中使用的内容。

因此,事不宜迟,让我们将有类出口 qdisc 添加到我们的接口中:

bash 复制代码
# tc qdisc add dev bond0 handle 1: root prio

我们选择给它 handle 1: ;

我们也可以用 100: or 42: ,只要我们在安装过滤器时使用相同的数字就没关系。

一旦我们有一个有分类的 qdisc,我们终于可以将过滤器附加到它上面,就像我们对入口 qdisc 所做的那样:

python 复制代码
# tc filter add dev bond0 parent 1: \
    protocol all \
    u32 match u8 0 0 \
    action mirred egress mirror dev dummy0

现在,让我们启动 dummy0 并检查:

perl 复制代码
# ip link set dummy0 up
# tcpdump -e -v -n -i dummy0
tcpdump: WARNING: dummy0: no IPv4 address assigned
tcpdump: listening on dummy0, link-type EN10MB (Ethernet), capture size 65535 bytes
18:56:41.237966 00:13:72:af:11:23 > 00:16:3e:fd:aa:67, ethertype IPv4 (0x0800), length 153: (tos 0x0, ttl 64, id 57195, offset 0, flags [DF], proto TCP (6), length 139)
    192.168.1.3.17569 > 192.168.1.232.514: Flags [P.], cksum 0x84b9 (correct), seq 3603440679:3603440766, ack 1213686729, win 229, options [nop,nop,TS val 1217617195 ecr 69837571], length 87
18:56:41.238131 00:16:3e:fd:aa:67 > 00:13:72:af:11:23, ethertype IPv4 (0x0800), length 66: (tos 0x0, ttl 64, id 51990, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.232.514 > 192.168.1.3.17569: Flags [.], cksum 0x9889 (correct), ack 87, win 1307, options [nop,nop,TS val 69844202 ecr 1217617195], length 0
...
18:57:06.687832 00:26:b9:72:16:99 > ff:ff:ff:ff:ff:ff, ethertype 802.1Q (0x8100), length 64: vlan 14, p 0, ethertype ARP, Ethernet (len 6), IPv4 (len 4), Reply 10.7.1.1 is-at 00:26:b9:72:16:99, length 46

从上面可以看出,VLAN 标记被复制。

所以总结一下,这里是如何启用从 bond0dummy0 的双向镜像;

sql 复制代码
sif=bond0
dif=dummy0

# ingress
tc qdisc add dev "$sif" ingress
tc filter add dev "$sif" parent ffff: \
          protocol all \
          u32 match u8 0 0 \
          action mirred egress mirror dev "$dif"

# egress
tc qdisc add dev "$sif" handle 1: root prio
tc filter add dev "$sif" parent 1: \
          protocol all \
          u32 match u8 0 0 \
          action mirred egress mirror dev "$dif"

当然,要镜像多个源接口的流量,应该对每个源接口重复上述操作(全部或仅一半,取决于我们是希望两个方向的流量还是只希望一个方向的流量)。

要删除镜像,只需从所有相关的源接口中删除根 qdisc 和入口 qdisc(默认的根 qdisc 将自动恢复):

css 复制代码
tc qdisc del dev bond0 ingress
tc qdisc del dev bond0 root

Daemonlogger

那么,为了这个目的,让我们看看另一种在 Linux 下镜像流量的方法。

有一个很好的工具叫做 daemonlogger,根据它的描述,它 "能够将数据包记录到文件或镜像到另一个接口",这听起来就像我们正在寻找的。Debian 在其标准存储库中拥有它。

快速阅读手册页后,我们可以按如下方式使用它:

vbnet 复制代码
# daemonlogger -i bond0 -o dummy0
[-] Interface set to bond0
[-] Log filename set to "daemonlogger.pcap"
[-] Tap output interface set to dummy0[-] Pidfile configured to "daemonlogger.pid"
[-] Pidpath configured to "/var/run"
[-] Rollover size set to 18446744071562067968 bytes
[-] Rollover time configured for 0 seconds
[-] Pruning behavior set to oldest IN DIRECTORY

-*> DaemonLogger <*-
Version 1.2.1
By Martin Roesch
(C) Copyright 2006-2007 Sourcefire Inc., All rights reserved

sniffing on interface bond0

此时,dummy0 上的 tcpdump 为我们提供了 bond0 的所有流量。诚然,它没有 Openvswitch 和 tc 那么复杂,但绝对要"快速和肮脏"得多。还值得一提的是,它支持 BPF 过滤器,就像 tcpdump 一样,因此可以在镜像之前过滤掉流量。

尽管如此,还是要提醒一句;README 文件末尾显示:

csharp 复制代码
This code is largely untested and probably completely shoddy.

参考: backreference.org/2014/06/17/...

相关推荐
Quantum&Coder23 分钟前
Swift语言的软件工程
开发语言·后端·golang
吴代庄1 小时前
复盘成长——2024年终总结
后端
CyberScriptor1 小时前
CSS语言的语法糖
开发语言·后端·golang
WeeJot嵌入式7 小时前
【C语言】标准IO
c语言·后端
hnmpf8 小时前
flask_sqlalchemy relationship 子表排序
后端·python·flask
Quantum&Coder8 小时前
Swift语言的数据库编程
开发语言·后端·golang
Q_27437851098 小时前
springboot高校电子图书馆的大数据平台规划与设计
大数据·spring boot·后端
aiee8 小时前
GO通过SMTP协议发送邮件
开发语言·后端·golang
JINGWHALE19 小时前
设计模式 行为型 备忘录模式(Memento Pattern)与 常见技术框架应用 解析
前端·人工智能·后端·设计模式·性能优化·系统架构·备忘录模式
大雄野比9 小时前
了解 ASP.NET Core 中的中间件
后端·中间件·asp.net