主题
实验 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就是上一章说的 TPIDmod_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=0text
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:1mod_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 # 跨 VLANtext
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.shtext
[通过] 没有用 ovs-vsctl 的 tag 字段
[通过] 存在 push_vlan 规则
[通过] 存在 pop_vlan 规则
[通过] 同 VLAN:h1 能 ping 通 h2
[通过] 同 VLAN:h3 能 ping 通 h4
[通过] 跨 VLAN:h1 不能 ping 通 h3自测清单
push_vlan:0x8100之后为什么必须跟mod_vlan_vid?set_field:4196->vlan_vid里的 4196 怎么算出来的?pop_vlan和strip_vlan有区别吗?为什么 grep 不到strip_vlan?- 泛洪规则里
output:1,output:2,包从 1 口进来时会发回去吗? - 手写 VLAN 相比
tag=100,丢掉了什么能力? - 什么场景下必须手写而不能用
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填 IDset_field:4196->vlan_vid里0x1000是 present 位,低 12 位才是 VLAN IDstrip_vlan和pop_vlan是同一个动作,OF1.3 下统一显示为pop_vlanoutput:N在 N 等于入口端口时自动跳过,泛洪规则可以把成员口全列上- VLAN 隔离在流表里就是"泛洪规则只列本 VLAN 的成员口"
- 日常用
tag;手写流表用于理解机制、非标准需求,以及为第 4 部分的隧道打基础
下一章:实验 11:链路聚合,把两条物理线绑成一条用。