nwfilter(network filter)是libvirt中的网络数据包过滤子系统,用于过滤进出虚拟机的网络流量,仅支持qemu kvm虚拟机。本文主要记录官网文档中理解稍有困难的部分,其他更多详细信息参考官网文档 libvirt: Network Filters

本文操作系统环境为 CentOS Linux release 7.5.1804,libvirt使用版本为libvirt-3.9.0-14。

概念

nwfilter以XML格式书写。一个nwfilter可以包含流量过滤规则,也可以引用其他nwfilter,两者可以同时存在。nwfilter以被虚拟机网卡接口引用的方式生效。其实现依赖ebtables、iptables和ip6tables工具。

以libvirt自带的clean-traffic为例说明nwfilter中的几个概念。

virsh nwfilter-dumpxml clean-traffic

clean-traffic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<filter name='clean-traffic' chain='root'>
<uuid>a2c07285-f55f-4f9a-8e3d-5bccd51052a6</uuid>
<filterref filter='no-mac-spoofing'/>
<filterref filter='no-ip-spoofing'/>
<rule action='accept' direction='out' priority='-650'>
<mac protocolid='ipv4'/>
</rule>
<filterref filter='allow-incoming-ipv4'/>
<filterref filter='no-arp-spoofing'/>
<rule action='accept' direction='inout' priority='-500'>
<mac protocolid='arp'/>
</rule>
<filterref filter='no-other-l2-traffic'/>
<filterref filter='qemu-announce-self'/>
</filter>
  • filter
    最外层元素filter是固定的,属性有name和chain。name是唯一的,nwfilter间不能冲突。chain是一个链的概念,有几种选择可以枚举,后面具体介绍。
    • uuid
      唯一的,可以由libvirt自动生成,不需要关注。
    • filterref
      该元素代表了对另一个nwfilter的引用。其中只有一个属性filter,值为引用的另一个nwfilter的name。被引用的nwfilter中rule所属的chain不做变动,与引用者的chain无关。
    • rule
      真正的过滤规则所在,有匹配规则、动作和优先级三种成分构成。action表示对数据包的动作,direction表示数据包需要匹配的方向,priority表示优先级(后面具体介绍)。
      • mac
        rule中的内部嵌套元素,该元素表示了rule中匹配的数据包协议类型,有几种类型可以枚举。其支持的属性与数据包类型有关,表示数据包需要匹配的规则。

虚拟机配置中interface元素中嵌套插入filterref而生效该nwfilter,虚拟机如果变更引用的nwfilter需要关机重启,已经引用的nwfilter内容变更可以修改nwfilter而不需要重启虚拟机。

虚拟机interface配置
1
2
3
4
5
6
7
<interface type='network'>
<mac address='52:54:00:43:53:32'/>
<source network='s-engine-net'/>
<model type='virtio'/>
<filterref filter='s-engine-nwfilter'/>
<address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>
</interface>

chain

nwfilter中过滤规则(rule)是以链(chain)的方式组织起来的。chain有几种值可以选择:

  • root
  • mac
  • stp
  • vlan
  • arp、rarp
  • ipv4
  • ipv6

上面的chain名字中,除root外其他的都可以作为一个名字的前缀(比如ipv4-a和ipv4-b)。rule需要挂载到一个chain上,既可以是root,也可以是其他chain。其他chain是挂载到root上的。所有数据包都先经过root(这里只说会先经过root,但不一定会流经root链中的rule,因为是否流经root上的某一个rule取决于该rule前面是否有更高优先级的其他rule或chain以及它们对数据包做何种裁决),其他chain的名字前缀表示了哪种协议类型的数据包会经过该chain,因此一个rule应该包含在哪个chain中需要考虑流经的数据包是否有意义(比如在arp的chain中过滤ipv4数据包是没有意义的)。

可以看到chain中没有四层的TCP或UDP等协议,这与chain的实现有关。nwfilter中chain的部分对应了ebtables对三层协议的匹配能力,因此chain只包含了三层协议的匹配。这里面的mac是二层协议,其实就是ebtables收到的所有数据包,不匹配三层协议就是它了,它与root所过滤的数据包是一致的,但是它是挂载到root上的。

chain在实现上依赖ebtables的nat表,因为nat表中包含了PREROUTING和POSTROUTING链,这是内核的两个hook点,可以过滤从虚拟机发出和进入虚拟机的流量。

实现

nwfilter中的chain与ebtables中的chain(链)是直接对应的,chain的实现参考生成的ebtables规则会比较好理解,以一个配置例子说明:

s-engine-nwfilter
1
2
3
4
5
6
7
8
9
10
11
<filter name='s-engine-nwfilter' chain='root'>
<uuid>702687d4-e516-4eaa-9f40-1c81e0d3c38d</uuid>
<filterref filter='filter-test1'/>
<filterref filter='filter-test2'/>
<rule action='drop' direction='out' priority='-400'>
<ip protocol='udp' dstportstart='10086'/>
</rule>
<rule action='drop' direction='in' priority='-800'>
<ip protocol='udp' dstportstart='10010'/>
</rule>
</filter>
filter-test1
1
2
3
4
5
6
<filter name='filter-test1' chain='ipv4-abc' priority='-700'>
<uuid>35b50319-b597-4f12-acad-0fbaf3ab4303</uuid>
<rule action='drop' direction='out' priority='500'>
<ip protocol='udp' dstportstart='9999'/>
</rule>
</filter>
filter-test2
1
2
3
4
5
6
<filter name='filter-test2' chain='mac-def' priority='-600'>
<uuid>1b432e1a-e1c8-49d6-be02-fff48a3b2928</uuid>
<rule action='drop' direction='out' priority='500'>
<ip protocol='udp' dstportstart='9996' dstportend='9999'/>
</rule>
</filter>

运行命令ebtables -t nat -L可以看到ebtables中生成的规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Bridge table: nat

Bridge chain: PREROUTING, entries: 1, policy: ACCEPT
-i vnet1 -j libvirt-I-vnet1

Bridge chain: OUTPUT, entries: 0, policy: ACCEPT

Bridge chain: POSTROUTING, entries: 1, policy: ACCEPT
-o vnet1 -j libvirt-O-vnet1

Bridge chain: libvirt-I-vnet1, entries: 3, policy: ACCEPT
-p IPv4 -j I-vnet1-ipv4-abc
-j I-vnet1-mac-def
-p IPv4 --ip-proto udp --ip-dport 10086 -j DROP

Bridge chain: libvirt-O-vnet1, entries: 1, policy: ACCEPT
-p IPv4 --ip-proto udp --ip-dport 10010 -j DROP

Bridge chain: I-vnet1-ipv4-abc, entries: 1, policy: ACCEPT
-p IPv4 --ip-proto udp --ip-dport 9999 -j DROP

Bridge chain: I-vnet1-mac-def, entries: 1, policy: ACCEPT
-p IPv4 --ip-proto udp --ip-dport 9996:9999 -j DROP

nwfilter解释如下:

  • s-engine-nwfilter
    配置了名为root的chain,这个chain最先接收所有的数据包,因此不需要优先级。同时引用filter-test1和filter-test2(引用者和被引用者的chain没有任何关系,纯粹是单纯的并列关系)。
  • filter-test1
    配置了名为ipv4-abc的chain,从名字前缀可以看到这个chain接收三层协议为IPV4的数据包,优先级-700(chain中优先级范围为[-1000, 1000],数字越小越优先)。
  • filter-test2
    配置名为mac-def的chain,从名字前缀可以看到这个chain接收二层数据包,优先级-600。

对比ebtables中的规则:

  • PREROUTING链
    将vnet1(虚拟机网卡在host上对应该端口)上进入网桥的数据包(也就是虚拟机发出的数据包)跳转到用户自定义链libvirt-I-vnet1中。
    • libvirt-I-vnet1链
      这个链其实就对应名为root的chain,但它只是该chain的一半,负责虚拟机向外发出的数据包。包含了直接挂载到root上的direction为out的rule,比如这里的将IPV4协议数据包中上层协议为UDP且目的端口为10086的数据包drop掉。还挂载的其他chain(如果这个chain中包含了direction为out的rule)。比如IPV4数据包跳转用户自定义链I-vnet1-ipv4-abc,比如所有数据包跳转用户自定义链I-vnet1-mac-def。
      • I-vnet1-ipv4-abc链
        对应名为ipv4-abc的chain的一半。挂载了一条rule,将IPV4协议数据包中上层协议为UDP且目的端口9999的数据包drop掉。
      • I-vnet1-mac-def链
        对应名为mac-def的chain的一般。挂载了一条rule,将IPV4协议数据包中上层协议为UDP且目的端口从9996到9999的数据包drop掉。
  • POSTROUTING链
    将要发往vnet1的数据包(也就是虚拟机将要接收的数据包)跳转到用户自定义链libvirt-O-vnet1中。
    • libvirt-O-vnet1链
      同libvirt-I-vnet1一样也都对应名为root的chain,区别在于这个链负责发往虚拟机的数据包。由于该方向只有一条rule,而且是直接挂载到root上的,因此该链上只有一条规则,没有其他用户自定义链的跳转。

priority

实现一节中说明了chain和其中的rule如何对应到ebtables中,由于chain和rule都有priority属性,因此实际使用时还要注意priority(优先级)的配置。

  • rule靠优先级与同一chain上的其他rule做排序。
  • 非root的chain靠优先级与其他chain做排序。
  • root上的rule和chain是并列关系,靠优先级做混合排序。

比如ebtables中的这一段,由于libvirt-I-vnet1对应了名为root的chain,可以看到对应名为ipv4-abc和mac-def的两个chain和root上该方向上的rule通过优先级做了混合排序。

1
2
3
4
Bridge chain: libvirt-I-vnet1, entries: 3, policy: ACCEPT
-p IPv4 -j I-vnet1-ipv4-abc
-j I-vnet1-mac-def
-p IPv4 --ip-proto udp --ip-dport 10086 -j DROP

chain在不配置priority时,libvirt会赋予默认优先级,参考官网文档

priority关系

rule

一个rule代表了一个过滤规则,一个rule节点可以包含几个属性以及内部嵌套的协议节点。

1
2
3
<rule action='drop' direction='in' priority='-800'>
<ip protocol='udp' dstportstart='10010'/>
</rule>

属性

  • action
    必选项。可以选择以下几种:
    • drop
      直接丢弃数据包。
    • reject
      丢弃数据包并生成一个ICMP消息。
    • accept
      接收该数据包并跳过其他所有chain和rule。
    • return
      跳过当前chain上的其他过滤器,并返回到root上继续检查后续chain或rule。如果当前位于root,则接收该数据包。
    • continue
      不产生影响,可以用于计数。
  • direction
    必选项。可以选择以下几种:
    • in
      进入虚拟机的数据包。
    • out
      虚拟机发出的数据包。
    • inout
      双向数据包。
  • priority
    rule的优先级。
  • statematch
    默认是true。对于四层协议可以对放行的数据包所对应的连接做状态保持,也就是放行该连接的后续数据包,该功能依赖iptables或ip6tables。这避免ebtables单纯判定一方端口的情况,对于防御攻击效果更好。

协议节点

rule内部嵌套的协议节点代表了该rule需要匹配何种协议。三层协议依赖ebtables,四层协议依赖iptables和ip6tables。每种协议节点有不同的过滤属性。具体信息参考官网文档

需要说明的是由于nwfilter在三层协议数据包处理上依赖ebtables,四层协议数据包处理上依赖iptables(或ip6tables),而这两种工具在内核中使用的是不同的hook点。也就是说同一个数据包,可能在ebtables中被accept了,但是在iptables中可能做出不同的裁决(比如drop),这里对该情况做明确说明。

如果有一组复杂的nwfilter,某一个rule中配置了ip协议节点的过滤规则并且做了accept裁决,另一个rule中配置了tcp协议节点的过滤规则并且做了drop裁决,当一个数据包同时匹配了这两个rule时,将会首先经过ebtables并accept通过,然后经过iptables并被drop丢弃。三层节点与四层节点过滤器的先后关系与rule的priority完全无关,四层节点之间比较时priority才有意义。

除了明显的四层协议名节点,all这个节点名也是生效到iptables中的。

虽然三层ip协议节点可以通过属性protocol配置过滤简单的四层协议头信息,但如果使用中存在四层协议节点,那么推荐在三层协议节点中对出入的ip数据包都做accept处理,但是优先级靠后(比如libvirt提供的clean-traffic),这样可以在靠前的位置放置各种防伪造策略,然后将所有四层相关的过滤规则都使用四层协议节点配置,这样既避免了逻辑上的混淆,同时也利用了四层协议节点默认的状态保持功能。比如以下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<filter name='s-engine-filter' chain='root'>
<uuid>4f446dac-30a6-444e-8118-9a15c959fac5</uuid>
<filterref filter='clean-traffic'/>
<rule action='return' direction='in' priority='500'>
<all srcipfrom='172.16.128.0' srcipto='172.16.255.254'/>
</rule>
<rule action='return' direction='out' priority='500'>
<tcp dstipaddr='172.16.0.1' dstportstart='1821'/>
</rule>
<rule action='return' direction='out' priority='500'>
<udp srcipaddr='0.0.0.0' dstipaddr='255.255.255.255' srcportstart='68' dstportstart='67'/>
</rule>
<rule action='return' direction='in' priority='500'>
<udp srcipaddr='172.16.0.1' srcportstart='67' dstportstart='68'/>
</rule>
<rule action='drop' direction='inout' priority='1000'>
<all/>
</rule>
</filter>
  • 引用clean-traffic,确保虚拟机不会发出各种伪造数据包,只让干净合法的流量出入。
  • 其余都使用四层协议节点配置,生效到iptables。
    • 对来源在172.16.128.0到172.16.255.254范围内的入包做放行,并保持连接状态放行相应的出包。
    • 对目的地址172.16.0.1,目的端口1821的UDP数据出包做放行,并保持连接状态。
    • 后面两条是对DHCP协议数据包做放行,其中172.16.0.1是DHCP服务器地址。
    • 丢弃其他所有的四层数据包(这个prirority优先级最低)。

action

关于action有一点需要特别说明。ebtables的用户自定义链是有policy的(iptables用户自定义链没有policy),而且libvirt使用的是默认policy,也就是accept。这就是说如果数据包被跳转到一个chain,而该chain中的rule并没有对数据包做出裁决,最后将会生效该chain的默认policy,也就是accept,这阻止了数据包继续被其他chain或rule检查。如果不注意这一点有可能导致实际效果与预期不符。

实现一节中的例子来说,由于名为ipv4-abc的chain比名为mac-def的chain优先级配置数字更小,因此IPV4数据包会先被跳转到ipv4-abc链中,如果虚拟机发出一个目的端口是9996的UDP数据包,该链并没有drop该数据包,最后该链的policy会accept该数据包,而mac-def中的rule将不会如预期drop该数据包。

使用

nwfilter的使用比较简单,分为命令行和API两种方式。

命令行

命令virsh help filter可以查看nwfilter相关的命令。

1
2
3
4
5
6
Network Filter (help keyword 'filter'):
nwfilter-define define or update a network filter from an XML file
nwfilter-dumpxml network filter information in XML
nwfilter-edit edit XML configuration for a network filter
nwfilter-list list network filters
nwfilter-undefine undefine a network filter

API

API的使用直接参考官网文档就可以了。

API列表 - Module libvirt-nwfilter from libvirt
更多API - Reference Manual for libvirt
XML Format

PS:使用API对已经存在的nwfilter做修改时,需要先拿到旧的uuid,在XML文本中设置该uuid,再调用virNWFilterDefineXML函数。