Skip to content

实验 10:用流表手写 VLAN ​

实验 9 里配 VLAN 只用了四条 ovs-vsctl set Port ... tag=N。这一章把同样的效果完全用 OpenFlow 流表实现。

为什么要做这件事:

  • 亲眼看到标签是在哪一步加上去、哪一步摘掉的
  • 理解 tag=100 这行配置背后 OVS 到底生成了什么
  • 学会 push_vlan / pop_vlan —— 这两个动作在 Overlay 和服务链场景里绕不开

起环境 ​

bash
cd labs/learn-ovs/09-vlan-flows
./start.sh

拓扑和实验 9 一样:h1..h4 接在 br0 的 1..4 口。但这次四个口都不设 tag,全靠流表。

start.sh 已经打开了 OpenFlow 1.3:

bash
ovs-vsctl set Bridge br0 protocols=OpenFlow10,OpenFlow13

设计 ​

用两级流表,正好对应 access 口的两个动作:

text
                 主机发来的裸帧
                        │
                        ▼
    ┌───────────────────────────────────────────┐
    │ table 0   入口:按端口打标签                 │
    │   in_port=1 → push_vlan, vid=100          │
    │   in_port=2 → push_vlan, vid=100          │
    │   in_port=3 → push_vlan, vid=200          │
    │   in_port=4 → push_vlan, vid=200          │
    │                    ↓ goto_table:1          │
    └───────────────────────────────────────────┘
                        │  此时帧带着标签
                        ▼
    ┌───────────────────────────────────────────┐
    │ table 1   转发:按 (VLAN, 目的MAC) 决定出口   │
    │   出去之前 pop_vlan 把标签摘掉               │
    └───────────────────────────────────────────┘
                        │
                        ▼
                 主机收到的裸帧

"进来打标签、出去摘标签" —— 这就是 access 口的全部含义。

table 0:入口打标签 ​

bash
N="docker exec clab-vlan-flows-ovs1"
OF="ovs-ofctl -O OpenFlow13"

$N $OF del-flows br0

for pair in "1 100" "2 100" "3 200" "4 200"; do
  set -- $pair
  $N $OF add-flow br0 \
    "table=0,priority=100,in_port=$1,actions=push_vlan:0x8100,mod_vlan_vid:$2,goto_table:1"
done

$N $OF add-flow br0 "table=0,priority=0,actions=drop"

两个动作配合使用:

  • push_vlan:0x8100 —— 插入一个 802.1Q 头。0x8100 就是上一章说的 TPID
  • mod_vlan_vid:100 —— 往刚插入的头里填 VLAN ID

push_vlan 不带 VLAN ID

push_vlan 只负责"插一个空标签",ID 是 0。必须紧跟 mod_vlan_vid 才有意义。

只写 push_vlan 的话,帧会带着 VLAN 0 走,后面按 dl_vlan=100 匹配的规则一条都命中不了。

看一下 OVS 怎么显示这条规则:

bash
$N $OF dump-flows br0 table=0
text
 priority=100,in_port=1 actions=push_vlan:0x8100,set_field:4196->vlan_vid,goto_table:1
 priority=100,in_port=3 actions=push_vlan:0x8100,set_field:4296->vlan_vid,goto_table:1

mod_vlan_vid:100 被规范化成了 set_field:4196->vlan_vid。这个数字值得算一下:

text
4196 = 0x1064 = 0001 0000 0110 0100
                 ↑    └──────────┘
                 │        VLAN ID = 0x064 = 100
                 └─ VLAN present 位(0x1000)

OpenFlow 用最高位表示"这个帧带 VLAN 标签",低 12 位才是 VLAN ID。所以 VLAN 200 是 0x1000 | 200 = 4296。

看到 set_field:4196->vlan_vid 别慌,它就是 VLAN 100。

table 1:转发并摘标签 ​

已知单播:

bash
$N $OF add-flow br0 "table=1,priority=100,dl_vlan=100,dl_dst=<h1MAC>,actions=pop_vlan,output:1"
$N $OF add-flow br0 "table=1,priority=100,dl_vlan=100,dl_dst=<h2MAC>,actions=pop_vlan,output:2"
$N $OF add-flow br0 "table=1,priority=100,dl_vlan=200,dl_dst=<h3MAC>,actions=pop_vlan,output:3"
$N $OF add-flow br0 "table=1,priority=100,dl_vlan=200,dl_dst=<h4MAC>,actions=pop_vlan,output:4"

匹配条件是 dl_vlan 加 dl_dst 两个字段一起 —— 这就是实验 9 里说的"MAC 表的键是 (VLAN, MAC)",在流表里变成了显式的两个匹配字段。

广播和组播,只在本 VLAN 内泛洪:

bash
$N $OF add-flow br0 \
  "table=1,priority=200,dl_vlan=100,dl_dst=01:00:00:00:00:00/01:00:00:00:00:00,\
actions=pop_vlan,output:1,output:2"

$N $OF add-flow br0 \
  "table=1,priority=200,dl_vlan=200,dl_dst=01:00:00:00:00:00/01:00:00:00:00:00,\
actions=pop_vlan,output:3,output:4"

$N $OF add-flow br0 "table=1,priority=0,actions=drop"

这两条就是 VLAN 隔离的全部实现。 VLAN 100 的广播只发给 1、2 口,VLAN 200 的只发给 3、4 口,没有任何一条规则会把包从一个 VLAN 送到另一个。

pop_vlan 和 strip_vlan 是同一个动作

strip_vlan 是 OpenFlow 1.0 的叫法,pop_vlan 是 1.1+ 的叫法。你写哪个都能被接受,但 dump-flows 在 OF1.3 下一律显示成 pop_vlan。

写 strip_vlan 然后 grep strip_vlan 找不到规则,就是这个原因。

输出到入口端口会怎样 ​

广播规则里写了 output:1,output:2。但如果包本来就是从 1 口进来的,会不会被发回给发送者?

bash
$N ovs-appctl ofproto/trace br0 "in_port=1,dl_src=<h1MAC>,dl_dst=ff:ff:ff:ff:ff:ff"
text
    push_vlan:0x8100
    set_field:4196->vlan_vid
    goto_table:1
 1. dl_vlan=100,dl_dst=01:00:00:00:00:00/01:00:00:00:00:00, priority 200
    pop_vlan
    output:1
     >> skipping output to input port
    output:2

Datapath actions: 3

>> skipping output to input port —— OVS 自动跳过了。这是 OpenFlow 的标准语义:output:N 在 N 等于入口端口时不生效。

所以最终 Datapath actions: 3,只发给了 h2。写泛洪规则时可以把所有成员口都列上,不用为每个入口单独写一条。

想真的原路发回,得用特殊端口 IN_PORT:

text
actions=output:IN_PORT

验证 ​

bash
docker exec clab-vlan-flows-h1 ping -c 3 192.168.10.2      # 同 VLAN
docker exec clab-vlan-flows-h3 ping -c 3 192.168.10.4      # 同 VLAN
docker exec clab-vlan-flows-h1 ping -c 2 -W 1 192.168.10.3 # 跨 VLAN
text
h1 → h2:  0% packet loss
h3 → h4:  0% packet loss
h1 → h3:  100% packet loss

和实验 9 的结果一模一样,但这次一个 tag 字段都没用。

再看单播的完整路径:

bash
$N ovs-appctl ofproto/trace br0 "in_port=1,dl_src=<h1MAC>,dl_dst=<h2MAC>"
text
 0. in_port=1, priority 100
    push_vlan:0x8100
    set_field:4196->vlan_vid
    goto_table:1
 1. dl_vlan=100,dl_dst=aa:c1:ab:dd:5a:fb, priority 100
    pop_vlan
    output:2

Datapath actions: 3

标签的一生:table 0 加上,table 1 用它做匹配,出口前摘掉。主机两端都只看到裸帧。

手写 vs 原生 ​

ovs-vsctl set Port tag=100手写流表
配置量一条命令每个口 1 条 + 每个 (VLAN,MAC) 1 条
MAC 学习自动没有,得自己维护
新增主机不用改要加规则
泛洪自动手写,成员变了要改
性能NORMAL 的优化实现规则多了 megaflow 变细
灵活性只能做标准 VLAN任意逻辑

日常配 VLAN 就用 tag。 手写流表的价值在于:

  • 理解机制 —— 你现在知道 tag=100 背后是什么了
  • 非标准需求 —— 比如"按源 IP 而不是按端口决定 VLAN"、"某些流量打标签某些不打",这些 tag 做不到
  • Overlay 的基础 —— 第 4 部分的 VXLAN 用的是同一套"入口打标记、出口摘标记"的思路,只是标记从 VLAN 换成了隧道 VNI

自动检查 ​

bash
./verify.sh
text
  [通过] 没有用 ovs-vsctl 的 tag 字段
  [通过] 存在 push_vlan 规则
  [通过] 存在 pop_vlan 规则
  [通过] 同 VLAN:h1 能 ping 通 h2
  [通过] 同 VLAN:h3 能 ping 通 h4
  [通过] 跨 VLAN:h1 不能 ping 通 h3

自测清单 ​

  1. push_vlan:0x8100 之后为什么必须跟 mod_vlan_vid?
  2. set_field:4196->vlan_vid 里的 4196 怎么算出来的?
  3. pop_vlan 和 strip_vlan 有区别吗?为什么 grep 不到 strip_vlan?
  4. 泛洪规则里 output:1,output:2,包从 1 口进来时会发回去吗?
  5. 手写 VLAN 相比 tag=100,丢掉了什么能力?
  6. 什么场景下必须手写而不能用 tag?

常见问题 ​

加了 push_vlan 但按 dl_vlan 匹配不上 ​

多半是漏了 mod_vlan_vid,帧带的是 VLAN 0。用 trace 看 table 0 之后的字段:

bash
ovs-appctl ofproto/trace br0 "in_port=1,dl_dst=..."

主机收到的包带着 VLAN 标签 ​

出口规则漏了 pop_vlan。主机的普通网卡收到 802.1Q 帧会直接丢弃(除非建了 VLAN 子接口)。

报错 push_vlan not supported ​

需要 OpenFlow 1.1+:

bash
ovs-vsctl set Bridge br0 protocols=OpenFlow10,OpenFlow13
ovs-ofctl -O OpenFlow13 add-flow br0 "..."

想在 trunk 口上不摘标签 ​

trunk 口的出口规则去掉 pop_vlan 就行:

text
table=1,dl_vlan=100,dl_dst=<MAC>,actions=output:<trunk口>

标签原样带出去,这就是 trunk 的行为。

小结 ​

  • access 口的本质是入口 push_vlan + 出口 pop_vlan,中间按 (VLAN, MAC) 转发
  • push_vlan:0x8100 只插空标签,必须跟 mod_vlan_vid 填 ID
  • set_field:4196->vlan_vid 里 0x1000 是 present 位,低 12 位才是 VLAN ID
  • strip_vlan 和 pop_vlan 是同一个动作,OF1.3 下统一显示为 pop_vlan
  • output:N 在 N 等于入口端口时自动跳过,泛洪规则可以把成员口全列上
  • VLAN 隔离在流表里就是"泛洪规则只列本 VLAN 的成员口"
  • 日常用 tag;手写流表用于理解机制、非标准需求,以及为第 4 部分的隧道打基础

下一章:实验 11:链路聚合,把两条物理线绑成一条用。