主题
有状态 ACL 与执行顺序
这一篇解决一个经典需求,顺带揭开本书里反复出现的一类问题的根源。
需求:内网能出去,外网不能进来
无状态 ACL 做不到这件事。
回包的目的端口是 41247 —— 一个客户端随机挑的临时端口。 你没法预先知道它,也不可能为每个可能的端口写规则。
无状态方案只能退而求其次:放行所有"高位端口"(1024-65535), 或者匹配 TCP 的 ACK 标志位。前者等于门户大开,后者对 UDP 无效。
permit+reflect
VPP 的解法是放行的同时记下这条流,回程自动放行:
bash
set acl-plugin acl permit+reflect src 203.0.113.1/32应用配置
bash
./labctl reset base
./labctl apply base 13
./labctl verify base 13配置分两条:
bash
# 外网口 output:内网出去的包在这里建会话
vppctl set acl-plugin interface host-eth2 output acl <reflect>
# 外网口 input:外网主动进来的包在这里被拦
vppctl set acl-plugin interface host-eth2 input acl <deny>结果:
text
── 1) 内网主动出去 —— reflect 应放行回包 ──────
h1 → 203.0.113.1:41247
✓ 出方向通,回包被 reflect 会话放行计数器讲清了机制
text
acl-plugin-out-ip4-fa new sessions added 1
acl-plugin-out-ip4-fa existing session packets 4
acl-plugin-in-ip4-fa existing session packets 4 ← 注意这一行
acl-plugin-in-ip4-fa restart session timer 1关键在 acl-plugin-in-ip4-fa existing session packets。
回程的包走的是 input 方向,那里挂的是 deny ACL。它没被拦下, 是因为会话查找发生在规则匹配之前 —— 命中已有会话就直接放行, 根本轮不到 deny 规则。
会话表可以直接看:
bash
./labctl vppctl base show acl-plugin sessionstext
Sessions total: add 2 - del 1 = 1
Sessions active: add 2 - deact 1 = 1会话超时可以调:
bash
set acl-plugin session timeout udp idle <秒>
set acl-plugin session timeout tcp idle <秒>
set acl-plugin session timeout tcp transient <秒>和 NAT 的会话老化是同一类机制, 同样要注意"保活间隔必须小于超时"。
ACL 优先于 NAT 的静态映射
实验里配了一条端口转发 203.0.113.1:8080 → h2:9000, 然后让 s1 去连它:
text
── 3) 外网主动进来 —— 即使有 NAT 端口转发也被拦 ──
s1 尝试连 203.0.113.1:8080(这条端口转发是存在的)
TCP local 10.10.0.12:9000 external 203.0.113.1:8080 vrf 0
✓ 被 input 的 deny ACL 拦下从 feature arc 上看得很清楚:
bash
./labctl vppctl base show interface features host-eth2text
ip4-unicast:
ip4-sv-reassembly-feature
acl-plugin-in-ip4-fa ← ACL 先执行
nat-pre-out2in ← NAT 后执行包还没走到 NAT 就被 ACL 丢了。
排障提示
"配了端口转发但外面连不上"这类问题,除了查 NAT, 也要查 show acl-plugin interface 有没有 ACL 拦在前面 —— 以及 show errors 里 acl-plugin-in-ip4-fa ACL deny packets 有没有在涨。
这一篇最重要的一点:出向 ACL 看到的是 NAT 后的地址
出方向规则必须写转换后的地址
实验里做了这个对照。把 reflect 规则从 src 203.0.113.1/32 改成内网地址 src 10.10.0.0/24:
text
把 reflect 规则改成匹配内网地址 10.10.0.0/24 试试
✓ 立刻不通了
acl-plugin-out-ip4-fa ACL deny packets 3原因:包到达外网口的 output 弧时,NAT 已经把源地址 从 10.10.0.11 改成了 203.0.113.1。规则写内网网段自然匹配不上, 落到隐式 deny。
完整的对应关系:
text
┌──────────────┬────────────────────────────────────────┐
│ ACL 挂在哪 │ 规则要写什么地址 │
├──────────────┼────────────────────────────────────────┤
│ 内网口 input │ 内网真实地址(NAT 还没执行) │
│ 外网口 input │ 外网地址(out2in 的 NAT 还没执行) │
│ 外网口 output │ **NAT 转换之后**的地址 │
└──────────────┴────────────────────────────────────────┘这三条都在实验里验证过:
text
内网口 input 写 10.10.0.11 ✓ 有效(第 15 步验证)
外网口 output 写 10.10.0.11 ✗ 无效
外网口 output 写 203.0.113.1 ✓ 有效这是 ACL 和 NAT 一起用时最容易出错的地方
症状是"ACL 规则看起来完全正确,但流量就是被拦"。 show acl-plugin acl 里规则写得明明白白, show acl-plugin interface 里也确实挂上了,就是不通。
判断方法:看 ACL deny packets 是在 -in- 还是 -out- 那个节点上涨。 在 -out- 上涨,八成就是地址写成转换前的了。
第 9 章的 feature 顺序会把这套机制完整讲一遍。
一个不工作的特性:macip ACL
VPP 提供了 macip ACL,用途是把 IP 和 MAC 绑定,防止地址欺骗:
bash
set acl-plugin macip acl permit ip 10.10.0.11/32 mac aa:c1:ab:6d:0a:eb mask 48
set acl-plugin macip interface host-eth1 acl 0在本实验环境(af_packet 三层接口)上它不工作
实测:MAC 和 IP 完全匹配的流量照样被丢弃。
排查过程:
bash
# 1. 确认 VPP 看到的源 MAC 和规则里的一致
vppctl show trace | grep ethernet-input -A1text
IP4: aa:c1:ab:6d:0a:eb -> aa:c1:ab:1d:5f:51 ← 和规则里的 MAC 一模一样bash
# 2. 看计数器
vppctl show errors | grep -i inacltext
18 ip4-inacl input ACL misses errorip4-inacl 是基于 classify 表的老路径,不是 acl-plugin 的快路径。 show acl-plugin macip acl 的输出也印证了这一点:
text
ip4_table_index 5, ip6_table_index 5, l2_table_index 5macip 会构建分类表,而这些表的匹配偏移是按 L2/桥接场景设计的。 在 af_packet 三层接口上,分类表匹配不到,全部落到 miss → 丢弃。
结论:macip ACL 面向二层/桥接部署,不适用于本教程这种三层接口。
三层场景下防源地址欺骗的正解是 uRPF —— 它靠路由表反查, 不需要维护任何 IP/MAC 名单,而且工作正常。见本章最后一篇。
小结
| 解决什么 | 内网能出去、外网不能进来,且不必预知回包端口 |
| 命令 | set acl-plugin acl permit+reflect src ... |
| 挂法 | 出方向建会话,入方向拦截 |
| 机制 | 会话查找先于规则匹配,命中会话直接放行 |
| 计数器 | new sessions added / existing session packets |
| 与 NAT 的关系 | 入向 ACL 先于 NAT;出向 ACL 后于 NAT |
| 最大的坑 | 出向规则必须写转换后的地址 |
| macip | 三层接口上不工作,签名是 ip4-inacl: input ACL misses |
下一篇:ABF —— 用 ACL 驱动转发。