These labs are designed to accompany the OVN ACL Processing deep dive. Each lab is self-contained and can be run on a local OVN deployment. Work through them sequentially — each builds on concepts from the previous one.
Conventions
Topology
┌──────────┐ │ R1 │ │ (router) │ └──┬───┬───┘ sw0-p │ │ sw1-p ┌───────┘ └───────┐ ┌────┴────┐ ┌────┴────┐ │ sw0 │ │ sw1 │ └─┬───┬───┘ └─┬───┬───┘ vm1│ │vm2 vm3│ │vm4 10.0.0.1 10.0.0.2 10.0.0.3 10.0.0.4All VMs are on the 10.0.0.0/24 network. The router R1 connects sw0 and sw1 via transit ports.
Ingress Pipeline Stages (ls_in_*)
| Stage | Number |
|---|---|
| ls_in_pre_acl | 5 |
| ls_in_pre_lb | 6 |
| ls_in_pre_stateful | 7 |
| ls_in_acl_hint | 8 |
| ls_in_acl_eval | 9 |
| ls_in_acl_sample | 10 |
| ls_in_acl_action | 11 |
| ls_in_qos | 12 |
| ls_in_lb | 15 |
| ls_in_acl_after_lb_eval | 20 |
| ls_in_acl_after_lb_sample | 21 |
| ls_in_acl_after_lb_action | 22 |
| ls_in_stateful | 24 |
Egress Pipeline Stages (ls_out_*)
| Stage | Number |
|---|---|
| ls_out_pre_acl | 2 |
| ls_out_pre_lb | 3 |
| ls_out_pre_stateful | 4 |
| ls_out_acl_hint | 5 |
| ls_out_acl_eval | 6 |
| ls_out_acl_sample | 7 |
| ls_out_acl_action | 8 |
| ls_out_stateful | 12 |
Key Formulas
logical_flow_priority = acl_priority + 1000 (OVN_ACL_PRI_OFFSET)Examples:
- ACL priority 1001 → logical flow priority 2001
- ACL priority 1000 → logical flow priority 2000
- ACL priority 10 → logical flow priority 1010
has_stateful_acl = true IF AND ONLY IF any ACL action == "allow-related" false FOR "allow", "drop", "reject", "allow-stateless"
has_stateful = has_stateful_acl || has_lb_vipCRITICAL: has_stateful is per-switch, not per-pipeline. If an allow-related ACL exists on ingress, has_stateful is true for the entire switch, so both ingress and egress ACL_HINT stages generate hint flows.
Direction Mapping (Verified from OVN Source)
| ACL direction | Pipeline | Stages |
|---|---|---|
from-lport | Ingress | ls_in_* (5-11) |
to-lport | Egress | ls_out_* (2-8) |
Source: northd.c:7575 — bool ingress = !strcmp(acl->direction, "from-lport") ? true : false;
CRITICAL:
from-lportmeans traffic from the logical port, which is the ingress pipeline (traffic arriving at the switch from the VM).to-lportmeans traffic to the logical port, which is the egress pipeline (traffic leaving the switch toward the VM).
Command Reference
# Add ACLovn-nbctl acl-add <switch> from-lport <priority> <match> [allow-related|allow|drop|reject]ovn-nbctl acl-add <switch> to-lport <priority> <match> [allow-related|allow|drop|reject]
# List ACLsovn-nbctl acl-list <switch>
# Remove ACLovn-nbctl acl-del <switch> from-lport <priority> <match>
# List logical flows for a switchovn-sbctl lflow-list <switch>
# List logical flows filtered by stageovn-sbctl lflow-list <switch> | grep -A 100 "ls_in_acl_eval" | head -60ovn-sbctl lflow-list <switch> | grep -A 100 "ls_out_acl_eval" | head -60
# Packet trace (from logical port)ovn-trace <switch> 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'
# Set NB_Global optionsovn-nbctl set NB_Global . options:default_acl_drop=trueovn-nbctl remove NB_Global . options default_acl_drop --if-existsLab 1: Topology and Environment Setup
Objective
Create the basic OVN topology and learn how to inspect logical flows.
Steps
1.1 Create Logical Switches and Router
# Create switchesovn-nbctl ls-add sw0ovn-nbctl ls-add sw1
# Create routerovn-nbctl lr-add R11.2 Create VM Ports
# sw0 portsovn-nbctl lsp-add sw0 vm1ovn-nbctl lsp-set-addresses vm1 "00:00:00:00:00:01 10.0.0.1"
ovn-nbctl lsp-add sw0 vm2ovn-nbctl lsp-set-addresses vm2 "00:00:00:00:00:02 10.0.0.2"
# sw1 portsovn-nbctl lsp-add sw1 vm3ovn-nbctl lsp-set-addresses vm3 "00:00:00:00:00:03 10.0.0.3"
ovn-nbctl lsp-add sw1 vm4ovn-nbctl lsp-set-addresses vm4 "00:00:00:00:00:04 10.0.0.4"1.3 Create Router Ports and Transit Links
# Router gateway portsovn-nbctl lrp-add R1 sw0-p 00:00:00:00:ff:01 10.0.0.254/24ovn-nbctl lrp-add R1 sw1-p 00:00:00:00:ff:02 10.0.0.254/24
# Connect switches to routerovn-nbctl lsp-add sw0 sw0-rovn-nbctl lsp-set-type sw0-r routerovn-nbctl lsp-set-addresses sw0-r 00:00:00:00:ff:01ovn-nbctl lsp-set-options sw0-r router-port=sw0-p
ovn-nbctl lsp-add sw1 sw1-rovn-nbctl lsp-set-type sw1-r routerovn-nbctl lsp-set-addresses sw1-r 00:00:00:00:ff:02ovn-nbctl lsp-set-options sw1-r router-port=sw1-p1.4 Verify Topology
# Verify switches existovn-nbctl ls-list# Expected:# sw0# sw1
# Verify routerovn-nbctl lr-list# Expected:# R1
# Verify ports on sw0ovn-nbctl lsp-list sw0# Expected:# vm1# vm2# sw0-r
# Verify ports on sw1ovn-nbctl lsp-list sw1# Expected:# vm3# vm4# sw1-r1.5 Inspect Logical Flows (Before ACLs)
# Show all logical flows for sw0 (will be verbose)ovn-sbctl lflow-list sw0 | head -80
# Filter for ACL stages specificallyovn-sbctl lflow-list sw0 | grep -E "ls_in_acl|ls_out_acl"
# Show the pre-acl flows (priority 1, ct_next)ovn-sbctl lflow-list sw0 | grep -B 1 -A 5 "ls_in_pre_acl"ovn-sbctl lflow-list sw0 | grep -B 1 -A 5 "ls_out_pre_acl"What to observe: Without any ACLs, the pre_acl stages contain only default flows. There are no acl_eval, acl_hint, or acl_action flows yet.
Challenge
Run ovn-sbctl lflow-list sw0 and count how many total logical flows exist before adding any ACLs. Record the number for comparison after adding ACLs in later labs.
Lab 2: Direction Mapping — from-lport vs to-lport
Objective
Understand the critical difference between from-lport (ingress) and to-lport (egress) ACLs, and verify they appear in the correct pipeline.
Steps
2.1 Add a from-lport ACL (Ingress Pipeline)
ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow2.2 Verify the ACL Appears in Ingress Pipeline
# List ACLsovn-nbctl acl-list sw0# Expected:# from-lport 1001 (ip4.src == 10.0.0.1) allow
# Check logical flows — filter for ACL eval stagesovn-sbctl lflow-list sw0 | grep -A 20 "ls_in_acl_eval"Expected output: You will see a flow in ls_in_acl_eval (ingress stage 9) that matches ip4.src == 10.0.0.1 at priority 2001 (= 1001 + 1000). The action will be next; (for plain allow).
You will NOT see any corresponding flow in ls_out_acl_eval for this ACL.
2.3 Add a to-lport ACL (Egress Pipeline)
ovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" allow2.4 Verify the ACL Appears in Egress Pipeline
# List ACLsovn-nbctl acl-list sw0# Expected:# from-lport 1001 (ip4.src == 10.0.0.1) allow# to-lport 1001 (ip4.dst == 10.0.0.2) allow
# Check logical flows — egress ACL evalovn-sbctl lflow-list sw0 | grep -A 20 "ls_out_acl_eval"Expected output: You will see a flow in ls_out_acl_eval (egress stage 6) that matches ip4.dst == 10.0.0.2 at priority 2001.
You will NOT see any corresponding flow in ls_in_acl_eval for this ACL.
2.5 Verify with a Trace
# Trace a packet from vm1 (ingress on sw0) — should match the from-lport ACLovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'In the trace output, look for:
ls_in_acl_eval (stage 9): ... matching flow at priority 2001 ... ... action: reg8[16] = 1; next; ... ← sets ALLOW verdict, then continuesThe egress pipeline does run (you’ll see ls_out_acl_eval in the trace), but your from-lport ACL only generates a flow in the ingress pipeline. In egress, only the universal flows (ND/MLD at priority 65532, svc_monitor_mac at priority 34000) and the priority 0 pass-through exist. The packet is not affected by your from-lport ACL in egress — see Lab 5 for an example where egress ACLs (to-lport rules) actively affect the packet.
2.6 Direction Mapping Summary
| ACL Direction | Means | Pipeline | Stage Name |
|---|---|---|---|
from-lport | Traffic from the port (arriving at switch from VM) | Ingress | ls_in_acl_eval (stage 9) |
to-lport | Traffic to the port (leaving switch toward VM) | Egress | ls_out_acl_eval (stage 6) |
Why? From the OVN source (northd.c:7575):
bool ingress = !strcmp(acl->direction, "from-lport") ? true : false;from-lport is treated as ingress because the ACL applies to traffic originating from the port — the switch evaluates it as traffic enters (ingresses) the switch from that port.
Cleanup
ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"ovn-nbctl acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"Challenge
Add an ACL with from-lport direction on sw1 (not sw0). Run ovn-sbctl lflow-list sw0 and ovn-sbctl lflow-list sw1. Verify the ACL only appears on sw1’s ingress pipeline, not on sw0.
Lab 3: Allow-Related ACL — Conntrack Commitment
Objective
Trace a full connection through the pipeline with an allow-related ACL. Observe how has_stateful_acl is set to true, and how the different packet phases (SYN, SYN-ACK, established data) are handled differently.
Steps
3.1 Add the allow-related ACL
ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-related3.2 Verify has_stateful_acl is True
ovn-nbctl acl-list sw0# Expected:# from-lport 1001 (ip4.src == 10.0.0.1) allow-relatedThe allow-related action sets has_stateful_acl = true on sw0. This triggers generation of:
- ACL_HINT flows (priorities 1-7) on both ingress and egress
- ct_commit flows in the STATEFUL stage
- Universal flows at priority 65532 and 65533
3.3 Verify Generated Flows
# Show ACL_HINT flows (ingress stage 8)ovn-sbctl lflow-list sw0 | grep -B 1 -A 10 "ls_in_acl_hint"
# Show ACL_EVAL flows (ingress stage 9)ovn-sbctl lflow-list sw0 | grep -B 1 -A 10 "ls_in_acl_eval"
# Show STATEFUL stage flows (ingress stage 24)ovn-sbctl lflow-list sw0 | grep -B 1 -A 10 "ls_in_stateful"
# Show universal flows at priority 65532ovn-sbctl lflow-list sw0 | grep -B 1 -A 5 "65532"3.4 Trace a SYN Packet (New Connection)
ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'Expected trace walkthrough:
- ls_in_pre_acl (stage 5):
ct_next— sends traffic through conntrack - ls_in_pre_stateful (stage 7):
ct_next— continues through conntrack - ls_in_acl_hint (stage 8): Match at priority 7 —
ct.new && !ct.est- Actions: Sets ALLOW_NEW=1, DROP=1, COMMIT=1 (and REGBIT_CONNTRACK_COMMIT = 1)
- Meaning: this packet is new, it may be allowed or dropped, and it needs conntrack commit
- ls_in_acl_eval (stage 9): Match at priority 2001 — the per-ACL flow
reg8[30..31] == 0 && reg0[7] == 1 && (ip4.src == 10.0.0.1)- Action:
reg8[16] = 1; next;— sets ALLOW verdict and continues - The COMMIT hint (reg0[1]) was set by the ACL_HINT stage (priority7), not by this flow
- Action:
- ls_in_acl_action (stage 11): Priority 1000 —
reg8[16] == 1(ALLOW verdict)- Action: Clear verdict bits,
next; - This is the verdict enforcement stage — the ALLOW verdict from ACL_EVAL is enforced here
- Action: Clear verdict bits,
- ls_in_stateful (stage 24): Match at priority 100 —
reg0[1]==1 && reg0[13]==0- Actions:
ct_commit { ct_mark.blocked = 0; ct_mark.allow_established = reg0[20]; ct_label.acl_id = reg2[16..31]; ct_label.nf = 0; ct_label.nf_id = 0; }; next; - This is ct_commit Variant 2 (without label, no NF) — used for plain
allow-relatedwithout explicit labels
- Actions:
3.5 Trace the SYN-ACK Reply
After the SYN is committed, the SYN-ACK reply arrives at sw0 from vm2. The SYN-ACK goes through the egress pipeline on sw0 (toward vm1):
# SYN-ACK: vm2 → vm1 (arrives at sw0 on vm2's port, egress toward vm1)ovn-trace sw0 'inport == "vm2" && eth.src == 00:00:00:00:00:02 && eth.dst == 00:00:00:00:00:01 && ip4.src == 10.0.0.2 && ip4.dst == 10.0.0.1 && ip.proto == 6 && tcp.flags == 18'Expected trace walkthrough (egress pipeline):
The SYN-ACK is a reply packet (ct.est && !ct.new && ct.rpl). It hits the universal flow at priority 65532 in the egress ACL_EVAL stage:
- ls_out_pre_acl (stage 2):
ct_next→ conntrack - ls_out_pre_stateful (stage 4):
ct_next→ continues - ls_out_acl_hint (stage 5): Priority 1 —
ct.est && ct_mark.blocked == 0→ BLOCK=1 - ls_out_acl_eval (stage 6): Match at priority 65532 —
ct.est && !ct.rel && ct.rpl && ct_mark.blocked == 0- Action:
reg8[21] = ct_label.nf; reg8[16] = 1; next;— sets ALLOW verdict - This is the universal ALLOW flow for established replies
- Action:
- ls_out_acl_action (stage 8): Priority 1000 —
reg8[16] == 1→ ALLOW →next;
The packet does NOT need to match the per-ACL flow because the universal flow at 65532 has higher priority than 2001.
Note: The same universal flow also exists in ls_in_acl_eval (ingress stage 9), but for same-switch traffic the SYN-ACK traverses the egress pipeline.
3.6 Trace an Established Data Packet
# Established data: vm1 → vm2 (after handshake complete)ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 16'Expected trace walkthrough:
- ls_in_acl_hint (stage 8): Match at priority 4 —
!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 0- Actions set: ALLOW=1, BLOCK=1
- Note: COMMIT is NOT set at this priority
- Meaning: packet is established and allowed; set BLOCK hint for any matching egress drop ACLs
- ls_in_acl_eval (stage 9): Match at priority 2001 (the ACL, Flow 2) —
reg8[30..31] == 0 && reg0[8] == 1 && (ip4.src == 10.0.0.1)- The ALLOW hint (reg0[8]) is set by ACL_HINT priority 4, so this flow matches
- Action:
next;— no COMMIT bit set (plainallow-relatedwithout label/sample_est/nfg) - Note: The universal flow at 65532 (
ct.est && !ct.rel && ct.rpl && ct_mark.blocked == 0) does NOT match because this is request-direction traffic (ct.rpl == 0), and the universal flow requiresct.rpl == 1(reply direction)
- ls_in_acl_action (stage 11): Priority 0
next;— no verdict register was set by the per-ACL flow - ls_in_stateful (stage 24): No ct_commit flow matches —
reg0[1]is not set for established packets- The priority 0 default
next;applies
- The priority 0 default
Key observation: For request-direction established traffic (ct.rpl == 0), the universal flow at priority 65532 does NOT match (it requires ct.rpl == 1). The per-ACL flow at priority 2001 handles it instead. The universal reply flow only matches reply-direction traffic. For plain allow-related (no label), established packets do NOT trigger ct_commit in the STATEFUL stage. The COMMIT hint (reg0[1]) is set by the ACL_HINT stage during the SYN, not by the ACL_EVAL per-ACL flow.
3.7 Challenge: Add Label to ACL
Remove the old ACL and add one with a label:
ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"
ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-related# Find the ACL UUIDACL_UUID=$(ovn-nbctl --data=bare --no-heading --columns=_uuid find acl direction=from-lport priority=1001 match="ip4.src == 10.0.0.1")# Set the label (label is a direct column on the ACL table, not inside options)ovn-nbctl set acl $ACL_UUID label=42Now trace a SYN again. In ls_in_stateful (stage 24), the match changes:
- Priority 100:
reg0[1]==1 && reg0[13]==1→ this is ct_commit Variant 1 (with label, no NF) - Action:
ct_commit { ct_mark.blocked = 0; ct_mark.allow_established = reg0[20]; ct_mark.obs_stage = reg8[19..20]; ct_mark.obs_collector_id = reg8[8..15]; ct_label.obs_point_id = reg9; ct_label.acl_id = reg2[16..31]; ct_label.nf = 0; ct_label.nf_id = 0; }; next;
The label is stored in ct_label.acl_id and can be used for observability (e.g., sFlow/IPFIX).
Cleanup
ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"Key Concepts
| Concept | Value |
|---|---|
has_stateful_acl | true (because action is allow-related) |
| SYN hint (priority 7) | ALLOW_NEW=1, DROP=1, COMMIT=1 |
| SYN-ACK handling | Universal flow at 65532 in egress pipeline (reply direction, ct.rpl==1) |
| Established data handling | Per-ACL flow at priority 2001 (request direction, ct.rpl==0 — universal flow does NOT match) |
| Established hint (priority 4) | ALLOW=1, BLOCK=1 |
| ct_commit for SYN | Variant 2 (no label, no NF) at priority 100 |
| ct_commit for established | Not triggered (reg0[1] not set by per-ACL flow for plain allow-related) |
Lab 4: Plain Allow ACL — No Stateful Commitment
Objective
Show that a plain allow ACL does NOT set has_stateful_acl to true, generates no ACL_HINT flows (when no LB VIPs exist), and does not trigger any ct_commit.
Steps
4.1 Add a Plain Allow ACL
ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow4.2 Verify has_stateful_acl is False
ovn-nbctl acl-list sw0# Expected:# from-lport 1001 (ip4.src == 10.0.0.1) allowSince the action is allow (NOT allow-related), has_stateful_acl remains false on sw0. If there is also no LB VIP on sw0, then has_stateful is also false.
4.3 Verify No ACL_HINT Flows
# Check for ACL_HINT flows (ingress stage 8)ovn-sbctl lflow-list sw0 | grep -A 20 "ls_in_acl_hint"Expected output: Only the priority 0 pass-through flow at ls_in_acl_hint:
ls_in_acl_hint (stage 8), priority 0: match: 1 action: next;There are no hint-generating flows at priorities 1-7. Without has_stateful, OVN does not generate the ACL_HINT decision flows because there is no conntrack commitment to hint about.
4.4 Verify No ct_commit Flows in STATEFUL
# Check STATEFUL stage (ingress stage 24)ovn-sbctl lflow-list sw0 | grep -B 1 -A 10 "ls_in_stateful"Expected output: Only the priority 0 pass-through flow:
ls_in_stateful (stage 24), priority 0: match: 1 action: next;No ct_commit variant flows are generated. Since there is no allow-related ACL, the connection is never committed to conntrack.
4.5 Verify the ACL_EVAL Flow
# Check ACL_EVAL stage (ingress stage 9)ovn-sbctl lflow-list sw0 | grep -B 1 -A 10 "ls_in_acl_eval"Expected output: A per-ACL flow at priority 2001:
match: ip4.src == 10.0.0.1action: next;The action is simply next; — it allows the packet and moves on. No COMMIT hint (reg0[1] is not set).
4.6 Trace a Packet
ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'Expected trace walkthrough:
- ls_in_pre_acl (stage 5): Priority 0
next;— noct_nextflow becausehas_statefulis false (plainallowdoes not sethas_stateful_acl, and there are no LB VIPs). Only the defaultnext;flow exists. - ls_in_pre_stateful (stage 7): Priority 0
next;— noct_nextflow because there is no conntrack commitment to execute. - ls_in_acl_hint (stage 8): Priority 0
next;— no hint generation (nohas_stateful). - ls_in_acl_eval (stage 9): Priority 2001
ip4.src == 10.0.0.1→next; - ls_in_acl_action (stage 11): Priority 0
next;— no verdict set by per-ACL flow, default applies - ls_in_stateful (stage 24): Priority 0
next;— no ct_commit
No connection state is tracked. Conntrack is not even traversed. Subsequent packets from the same flow will also be allowed, but there is no conntrack entry to distinguish new vs. established.
4.7 Difference Summary
| Property | allow | allow-related |
|---|---|---|
has_stateful_acl | false | true |
| ACL_HINT flows (priorities 1-7) | None | Generated (priorities 1-7) |
| ACL_HINT priority 0 flow | next; (pass-through) | next; (pass-through) |
| ct_commit in STATEFUL | None | Yes (Variant 1 or 2) |
| Connection tracking | Conntrack traversed but not committed | Conntrack committed |
| Reply handling | Universal flows still apply | Universal flows handle replies |
Cleanup
ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"Challenge
Add both a plain allow ACL and an allow-related ACL on the same switch at different priorities. Verify that has_stateful_acl is true because at least one ACL uses allow-related. Show the ACL_HINT flows that are now generated.
Lab 5: Drop ACL — The BLOCK Hint Mechanism
Objective
Understand how drop ACLs interact with the BLOCK hint mechanism. When an allow-related ACL and a drop ACL are on the same switch, established packets that match the drop ACL in one pipeline get marked as blocked, but subsequent packets pass through without being dropped.
Steps
5.1 Add a to-lport Drop ACL (Egress Pipeline)
ovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" drop5.2 Trace a SYN Through Egress
# SYN from vm1 to vm2, arriving at sw0 ingress, then egress toward vm2ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'Expected trace walkthrough (ingress):
- ls_in_pre_acl (stage 5):
ct_next - ls_in_acl_hint (stage 8): Priority 0
next;— no stateful ACL on ingress, no hint flows - ls_in_acl_eval (stage 9): No matching ACL in ingress → priority 0
next;applies - ls_in_acl_action (stage 11): Priority 0
next;
Expected trace walkthrough (egress):
- ls_out_pre_acl (stage 2):
ct_next - ls_out_acl_hint (stage 5): Priority 0
next;— no hint flows generated (nohas_statefulon this switch) - ls_out_acl_eval (stage 6): Priority 2001 —
ip4.dst == 10.0.0.2→drop - ls_out_acl_action (stage 8): The packet is dropped
The SYN is dropped. No connection is established.
5.3 Add allow-related + Drop ACLs
Remove the old ACL and set up a scenario with both:
ovn-nbctl acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"
# Ingress: allow-related for vm1ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-related
# Egress: drop to vm2 (same priority)ovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" dropImportant: Since there is now an allow-related ACL on sw0, has_stateful_acl = true for the entire switch. This means both ingress and egress ACL_HINT stages generate hint flows (priorities 1-7).
5.4 Trace a SYN (vm1 → vm2)
ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'Expected:
- Ingress ACL_HINT (stage 8): Priority 7 —
ct.new && !ct.est→ ALLOW_NEW=1, DROP=1, COMMIT=1 - Ingress ACL_EVAL (stage 9): Priority 2001 —
ip4.src == 10.0.0.1→reg8[16] = 1; next;(ALLOW verdict) - Ingress ACL_ACTION (stage 11): Priority 1000 —
reg8[16] == 1→ ALLOW →next; - Ingress STATEFUL (stage 24): ct_commit Variant 2 at priority 100 — connection committed with
ct_mark.blocked = 0 - Egress ACL_HINT (stage 5): Priority 7 —
ct.new && !ct.est→ ALLOW_NEW=1, DROP=1, COMMIT=1- For this SYN packet, the CT state is still
ct.new=1, ct.est=0because the packet’s CT state was set byct_next()in ingress PRE_STATEFUL, beforect_commitin ingress STATEFUL created the CT entry. Thect_commitwrites the entry but does not retroactively change the packet’s CT state flags. - Note: For subsequent established packets (not this SYN), the CT entry will already exist when they reach egress, so
ct.est=1, ct.new=0and priority 4 would match instead.
- For this SYN packet, the CT state is still
- Egress ACL_EVAL (stage 6): Priority 2001 — Flow 1:
reg8[30..31] == 0 && reg0[9] == 1 && (ip4.dst == 10.0.0.2)→reg8[17] = 1; next;(DROP verdict)- This is Flow 1 of the drop ACL: it matches on
reg0[9] == 1(DROP hint) and sets the DROP verdict register
- This is Flow 1 of the drop ACL: it matches on
- Egress ACL_ACTION (stage 8): Priority 1000 —
reg8[17] == 1→ DROP → implicit drop
The SYN is dropped. The connection was committed in ingress, then dropped in egress. The conntrack entry exists (committed), but the packet never reached vm2.
5.5 Examine the BLOCK Hint Flows
Since has_stateful is true (due to the allow-related ACL) and there is a drop ACL, the BLOCK hint mechanism is activated. Hint flows are generated on both ingress and egress ACL_HINT stages:
# Show ACL_HINT flows in egress (stage 5)ovn-sbctl lflow-list sw0 | grep -B 1 -A 10 "ls_out_acl_hint"Expected BLOCK hint flows (egress, priorities 1-7):
| Priority | Match | Hints Set | Meaning |
|---|---|---|---|
| 7 | ct.new && !ct.est | ALLOW_NEW=1, DROP=1, COMMIT=1 | New connection, may be allowed or dropped |
| 6 | !ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1 | ALLOW_NEW=1, DROP=1, COMMIT=1 | Established, previously blocked |
| 5 | !ct.trk | ALLOW=1, DROP=1 | Not tracked |
| 4 | !ct.new && ct.est && !ct.rpl && ct_mark.blocked == 0 | ALLOW=1, BLOCK=1 | Established, not blocked |
| 3 | !ct.est | DROP=1 | Not established |
| 2 | ct.est && ct_mark.blocked == 1 | DROP=1 | Established but blocked |
| 1 | ct.est && ct_mark.blocked == 0 | BLOCK=1 | Established, set BLOCK hint |
5.6 Examine the Drop ACL Flows in ACL_EVAL
The drop ACL generates two flows in ls_out_acl_eval when has_stateful is true:
ovn-sbctl lflow-list sw0 | grep -B 1 -A 5 "ls_out_acl_eval"Flow 1 (DROP hint) — for new/untracked traffic:
match: reg8[30..31] == 0 && reg0[9] == 1 && (ip4.dst == 10.0.0.2)action: [log] [sample] next;- Matches when DROP hint (reg0[9]) is set
- Sets the DROP verdict register (reg8[17] = 1) and continues to ACL_ACTION for enforcement
- Note: The action is NOT
drop;— it sets the verdict register and continues
Flow 2 (BLOCK hint) — for established, previously-allowed traffic:
match: reg8[30..31] == 0 && reg0[10] == 1 && (ip4.dst == 10.0.0.2)action: [log] [sample] ct_commit { ct_mark.blocked = 1; ct_label.obs_point_id = <obs_pid>; }; next;- Matches when BLOCK hint (reg0[10]) is set
- Commits the connection with
ct_mark.blocked = 1 - Sets
ct_label.obs_point_idfor observability - Does NOT set any verdict register — the packet continues (
next;)
Both flows are at priority 2001 (acl_priority 1001 + offset 1000).
5.7 Trace an Established Packet Hitting the Drop ACL
After the SYN is committed (even though it was dropped in egress), consider what happens when an established data packet arrives from vm1 to vm2:
# Established data: vm1 → vm2ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 16'Expected trace (egress):
- ls_out_acl_hint (stage 5): Priority 4 —
!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 0→ ALLOW=1, BLOCK=1- Since
ct_mark.blocked == 0(first established packet after the dropped SYN), this matches - Sets BLOCK hint (reg0[10]=1)
- Since
- ls_out_acl_eval (stage 6): Priority 2001 (Flow 2, BLOCK hint) —
reg8[30..31] == 0 && reg0[10] == 1 && (ip4.dst == 10.0.0.2)— MATCHES- Action:
ct_commit { ct_mark.blocked = 1; ct_label.obs_point_id = <obs_pid>; }; next; - The connection is marked as blocked in conntrack
- No verdict register is set — the packet continues
- Action:
- ls_out_acl_action (stage 8): No verdict register is set. Priority 0 default
next;applies (or implicit drop ifdefault_acl_drop=true)
The packet is committed as blocked but NOT dropped. The BLOCK hint flow (Flow 2) does not set a DROP verdict. The packet continues through the pipeline.
5.8 Subsequent Established Packets
After ct_mark.blocked is set to 1, the ACL_HINT priorities change. For request-direction blocked traffic (ct.est=1, ct.rpl=0, ct_mark.blocked=1):
- Priority 6 matches:
!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1→ ALLOW_NEW=1, DROP=1, COMMIT=1 - This is the highest-priority match. The DROP hint (reg0[9]) is set.
- ACL_EVAL: Flow 1 (DROP hint,
reg0[9] == 1) matches →reg8[17] = 1; next;→ DROP verdict - ACL_ACTION: Priority 1000 →
reg8[17] == 1→ implicit drop
Request-direction blocked traffic is dropped by Flow 1.
For reply-direction blocked traffic (ct.est=1, ct.rpl=1, ct_mark.blocked=1):
- Priority 2 matches:
ct.est && ct_mark.blocked == 1→ DROP=1 - ACL_EVAL: Priority 65532 universal flow:
ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)→reg8[17] = 1; next;→ DROP verdict - ACL_ACTION: Priority 1000 →
reg8[17] == 1→ implicit drop
Reply-direction blocked traffic is also dropped (by the universal flow at 65532).
The full BLOCK mechanism prevents asymmetric drops through this sequence:
- First SYN is committed (ingress) then dropped (egress) by Flow 1 (DROP hint,
reg0[9] == 1) - For the first established packet, ACL_HINT priority 4 sets BLOCK=1 (
reg0[10]). The DROP ACL’s Flow 2 (BLOCK hint,reg0[10] == 1) matches and commits the connection withct_mark.blocked = 1 - Subsequent request-direction packets: ACL_HINT priority 6 sets DROP=1 → Flow 1 drops them
- Subsequent reply-direction packets: Universal flow at 65532 drops them (
ct.est && ct.rpl && ct_mark.blocked == 1) - The blocked state is visible in conntrack for debugging and monitoring
The asymmetry is the core mechanism: reply-direction blocked traffic is dropped by the universal flow at 65532, while request-direction blocked traffic is dropped by the per-ACL DROP hint flow. Both paths prevent the black hole scenario where the SYN was allowed but subsequent data is perpetually dropped.
Cleanup
ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"ovn-nbctl acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"Challenge
Set up an allow-related ACL on ingress and a drop ACL on egress with a different priority (e.g., ingress at 1001, egress at 2001). Trace packets and observe how the tier matching (reg8[30..31]) changes in the BLOCK hint flow.
Lab 6: Stateless ACL — Bypassing Conntrack
Objective
Understand how allow-stateless ACLs bypass conntrack commitment and appear in the pre_acl stage. A per-ACL flow IS also generated in ACL_EVAL for stateless ACLs, but it uses a simpler form without hint matching.
Steps
6.1 Add a Stateless ACL
ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-stateless6.2 Verify the Flow Appears in PRE_ACL
# Check pre_acl stage (ingress stage 5)ovn-sbctl lflow-list sw0 | grep -B 1 -A 10 "ls_in_pre_acl"Expected output: A flow in ls_in_pre_acl (stage 5) at priority 2001 (= 1001 + 1000):
match: ip4.src == 10.0.0.1action: reg0[16] = 1; next;The action sets REGBIT_ACL_STATELESS (reg0[16]) = 1, indicating this packet bypasses conntrack for ACL purposes.
6.3 Verify the Per-ACL Flow in ACL_EVAL
# Check ACL_EVAL stage (ingress stage 9)ovn-sbctl lflow-list sw0 | grep -B 1 -A 10 "ls_in_acl_eval"Expected output: A per-ACL flow at priority 2001:
match: ip4.src == 10.0.0.1action: [sample_label] next;For allow-stateless and pass ACLs, a single per-ACL flow is generated in ACL_EVAL without hint matching. The match is just the ACL’s match expression — no reg0[7], reg0[8], reg0[9], or reg0[10] prefix. This is different from stateful ACLs (allow, allow-related, drop, reject) which generate two flows with hint matching when has_stateful is true.
6.4 Verify No ct_commit in STATEFUL
ovn-sbctl lflow-list sw0 | grep -B 1 -A 10 "ls_in_stateful"Expected output: Only the priority 0 pass-through flow:
ls_in_stateful (stage 24), priority 0: match: 1 action: next;No ct_commit variant flows are generated for stateless ACLs.
6.5 Trace a Packet
ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'Expected trace walkthrough:
- ls_in_pre_acl (stage 5): Priority 2001 —
ip4.src == 10.0.0.1→reg0[16] = 1; next;- The packet is allowed immediately and
REGBIT_ACL_STATELESS(reg0[16]) is set - The packet does NOT go through
ct_next(conntrack is bypassed for this flow)
- The packet is allowed immediately and
- ls_in_acl_hint (stage 8): Priority 0
next; - ls_in_acl_eval (stage 9): Priority 2001 —
ip4.src == 10.0.0.1→[sample_label] next;- The per-ACL flow is generated for stateless ACLs, but it simply passes through
- ls_in_acl_action (stage 11): Priority 0
next; - ls_in_stateful (stage 24): Priority 0
next;— no ct_commit
The packet bypasses conntrack entirely. No connection state is tracked.
6.6 Compare with Stateful ACL
Remove the stateless ACL and add a stateful one at the same priority:
ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allowTrace the same packet and compare:
| Property | allow-stateless | allow |
|---|---|---|
| Flow location | ls_in_pre_acl (stage 5) + ls_in_acl_eval (stage 9) | ls_in_acl_eval (stage 9) only |
| Priority | acl_priority + 1000 | acl_priority + 1000 |
| Conntrack | Bypassed (sets reg0[16]=1 in pre_acl) | Traversed (but not committed) |
| reg0[16] (STATELESS) | Set to 1 | Not set |
| ACL_HINT flows | Not generated (no has_stateful from stateless ACL) | Not generated (plain allow doesn’t set has_stateful_acl) |
| ct_commit | None | None |
| Per-ACL flow in ACL_EVAL | Yes, but no hint matching | Yes, no hint matching (plain allow) |
6.7 Challenge: Stateless + Stateful ACL on Same Packet
# Remove old ACLovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"
# Add a stateless ACL at priority 1001ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-stateless
# Add a stateful ACL at priority 1002ovn-nbctl acl-add sw0 from-lport 1002 "ip4.dst == 10.0.0.2" allow-relatedTrace a packet matching both:
ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'Expected: The stateless ACL at priority 2001 matches first in ls_in_pre_acl and sets REGBIT_ACL_STATELESS. The packet is allowed and moves on. The stateful ACL at priority 2002 in ls_in_acl_eval may also match, but since the packet is already allowed in pre_acl, the behavior depends on the exact OVN version and whether the stateless bit prevents further ACL evaluation.
Cleanup
ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"ovn-nbctl acl-del sw0 from-lport 1002 "ip4.dst == 10.0.0.2"Lab 7: Tiered ACLs — Multi-Tier Evaluation
Objective
Understand how OVN evaluates ACLs in tiers. Each tier gets its own set of flows, and the tier is advanced between evaluations by looping back to the ACL_EVAL stage.
Steps
7.1 Add ACLs at Tier 0 and Tier 1
# Tier 0 ACLs (default tier = 0)ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allowovn-nbctl acl-add sw0 from-lport 1002 "ip4.src == 10.0.0.2" drop
# Tier 1 ACLsovn-nbctl --type=ls --tier=1 acl-add sw0 from-lport 1001 "ip4.dst == 10.0.0.3" allowNote: The
--tierflag is used to specify the tier for an ACL. If not specified, the default tier is 0. Requires OVN 23.09+. On older versions, tiered ACLs are not supported — all ACLs evaluate at tier 0 only. Runovn-nbctl --help | grep tierto check if your version supports it.
7.2 Verify Tier Match Prefix
# Show ACL_EVAL flows for ingressovn-sbctl lflow-list sw0 | grep -B 1 -A 5 "ls_in_acl_eval"Expected output: Each per-ACL flow includes a tier match prefix:
reg8[30..31] == 0 && ip4.src == 10.0.0.1 → next;reg8[30..31] == 0 && ip4.src == 10.0.0.2 → drop;reg8[30..31] == 1 && ip4.dst == 10.0.0.3 → next;The reg8[30..31] field holds the current tier. Tier 0 flows match reg8[30..31] == 0, tier 1 flows match reg8[30..31] == 1.
7.3 Tier Advancement at ACL_ACTION Stage
Between tiers, the ls_in_acl_action stage (ingress stage 11) advances the tier and loops back to ACL_EVAL:
ovn-sbctl lflow-list sw0 | grep -B 1 -A 10 "ls_in_acl_action"Expected output: A tier advancement flow at priority 500:
match: reg8[30..31] == 0action: reg8[30..31] = 1; next(pipeline=ingress, table=9);And possibly another at priority 500 for tier 1:
match: reg8[30..31] == 1action: reg8[30..31] = 2; next(pipeline=ingress, table=9);CRITICAL: The tier advancement flow does NOT use next; (which goes to the next sequential table). It uses next(pipeline=<ingress|egress>, table=<acl_eval_table>) to loop back to the ACL_EVAL stage with an incremented tier. This is what creates the tier evaluation loop.
7.4 Trace a Packet Through Tier 0 → Tier 1
ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.3 && ip.proto == 6 && tcp.flags == 2'Expected trace walkthrough:
- ls_in_acl_eval (stage 9): Priority 2001 —
reg8[30..31] == 0 && ip4.src == 10.0.0.1→next;(tier 0, allowed) - ls_in_acl_action (stage 11): Priority 500 —
reg8[30..31] == 0→reg8[30..31] = 1; next(pipeline=ingress, table=9);(advance to tier 1, loop back to ACL_EVAL) - ls_in_acl_eval (stage 9): Priority 2001 —
reg8[30..31] == 1 && ip4.dst == 10.0.0.3→next;(tier 1, allowed) - ls_in_acl_action (stage 11): Priority 500 —
reg8[30..31] == 1→reg8[30..31] = 2; next(pipeline=ingress, table=9);(advance to tier 2, loop back to ACL_EVAL) - ls_in_acl_eval (stage 9): No tier 2 ACLs → priority 0
next;(pass-through) - ls_in_acl_action (stage 11): No tier advancement matches (tier 2 > max tier) → priority 0
next;(default action)
The packet is allowed at both tiers and delivered.
7.5 What Happens When Max Tier is Exceeded
When reg8[30..31] exceeds the maximum tier (determined by the number of ACL tiers configured), the tier advancement flow at priority 500 does not match, and the priority 0 default next; applies. This means:
- No more tier-specific ACL evaluation
- The packet proceeds through the pipeline with the default action
7.6 BLOCK Hint Flows Per Tier
Each tier generates its own set of BLOCK hint flows in ACL_HINT. The tier match prefix reg8[30..31] == <tier> ensures that each tier’s hints are evaluated independently.
7.7 Challenge: Full Tier Progression (0-3)
# Add ACLs at tier 0, 1, 2, 3ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allowovn-nbctl --type=ls --tier=1 acl-add sw0 from-lport 1001 "ip4.dst == 10.0.0.2" allowovn-nbctl --type=ls --tier=2 acl-add sw0 from-lport 1001 "ip4.proto == 6" allowovn-nbctl --type=ls --tier=3 acl-add sw0 from-lport 1001 "tcp.dst == 80" dropTrace a TCP packet to port 80 and observe:
- Tier 0: allowed (ip4.src match)
- Tier 1: allowed (ip4.dst match)
- Tier 2: allowed (proto match)
- Tier 3: dropped (tcp.dst == 80 match)
Show how the tier match prefix reg8[30..31] changes at each stage and how the packet loops back to ACL_EVAL after each tier advancement.
Cleanup
ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"ovn-nbctl acl-del sw0 from-lport 1002 "ip4.src == 10.0.0.2"ovn-nbctl acl-del sw0 from-lport 1001 "ip4.dst == 10.0.0.3"ovn-nbctl acl-del sw0 from-lport 1001 "ip4.dst == 10.0.0.2"ovn-nbctl acl-del sw0 from-lport 1001 "ip4.proto == 6"ovn-nbctl acl-del sw0 from-lport 1001 "tcp.dst == 80"Lab 8: Universal Flows — The Safety Net
Objective
List and understand all universal flows at various priorities in the ACL_EVAL stage. Understand that universal flows do NOT directly drop packets — they set verdict registers and continue to ACL_ACTION for enforcement.
Steps
8.1 Add an allow-related ACL to Trigger Universal Flows
ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-related8.2 List All Universal Flows at Priority 65532
ovn-sbctl lflow-list sw0 | grep -B 1 -A 3 "65532"Expected universal flows in ls_in_acl_eval at priority 65532:
| # | Match | Action | Purpose |
|---|---|---|---|
| 1 | ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1) | reg8[17] = 1; next; | Set DROP verdict for invalid connections and blocked replies |
| 2 | ct.est && !ct.rel && ct.rpl && ct_mark.blocked == 0 | reg0[9]=0; reg0[10]=0; reg0[17]=1; reg8[21]=ct_label.nf; reg8[16]=1; next; (ingress) | Allow established replies (clears DROP/BLOCK hints, sets ALLOW_REL, sets ALLOW verdict) |
| 3 | !ct.est && ct.rel && !ct.new && ct_mark.blocked == 0 | reg0[17]=1; reg8[21]=ct_label.nf; reg8[16]=1; ct_commit_nat; next; | Allow related (non-new) connections with NAT |
| 4 | ct.est && ct_mark.allow_established == 1 | reg8[21]=ct_label.nf; reg8[16]=1; next; | Allow established with allow_established mark |
| 5 | IPv6 CT-omit protocols | reg8[16]=1; next; | Allow specific IPv6 protocols without conntrack |
These flows exist in BOTH ingress (ls_in_acl_eval) and egress (ls_out_acl_eval) pipelines.
CRITICAL: Flow 1 does NOT use drop; as its action. It sets the DROP verdict register (reg8[17] = 1) and continues (next;) to the ACL_ACTION stage, where the priority 1000 flow enforces the drop. This is the two-stage enforcement architecture: ACL_EVAL sets the verdict, ACL_ACTION enforces it.
8.3 Explain Each Universal Flow
Flow 1 — Drop Invalid/Blocked:
match: ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)action: reg8[17] = 1; next;Sets the DROP verdict for packets that are either invalid (ct.inv) or are replies to a connection that was marked as blocked. The verdict is enforced at ACL_ACTION (priority 1000).
Flow 2 — Allow Established Replies:
match: ct.est && !ct.rel && ct.rpl && ct_mark.blocked == 0action (ingress): reg0[9]=0; reg0[10]=0; reg0[17]=1; reg8[21]=ct_label.nf; reg8[16]=1; next;action (egress): reg8[21]=ct_label.nf; reg8[16]=1; next;Allows established reply packets. The ingress version clears DROP (reg0[9]) and BLOCK (reg0[10]) hints, sets ALLOW_REL (reg0[17]), and sets the ALLOW verdict (reg8[16]). The egress version is simpler, only setting nf hint and ALLOW verdict.
Flow 3 — Allow Related with NAT:
match: !ct.est && ct.rel && !ct.new && ct_mark.blocked == 0action: reg0[17]=1; reg8[21]=ct_label.nf; reg8[16]=1; ct_commit_nat; next;Allows related (non-new) connections with NAT commitment. Sets ALLOW_REL hint, propagates nf flag, sets ALLOW verdict, and commits NAT. Used for ICMP errors, FTP data connections, and similar protocols.
Flow 4 — Allow Established with Mark:
match: ct.est && ct_mark.allow_established == 1action: reg8[21]=ct_label.nf; reg8[16]=1; next;Allows established connections that have the allow_established mark set (from persist-established ACLs). Propagates nf flag and sets ALLOW verdict.
Flow 5 — IPv6 CT-Omit:
match: (various IPv6 protocol matches)action: reg8[16]=1; next;Allows specific IPv6 protocols (like ICMPv6 neighbor discovery) without conntrack evaluation.
8.4 Priority 1 Flows (Re-Allow Blocked + Pass Non-Established)
ovn-sbctl lflow-list sw0 | grep -B 1 -A 3 "priority.*1[^0-9]" | grep -A 3 "acl"Expected flows at priority 1 in ls_in_acl_eval:
| Match | Action | Purpose |
|---|---|---|
ip && ct.est && ct_mark.blocked == 1 | reg8[16] = 1; next; | Directly allow blocked established connections (sets ALLOW verdict) |
!ct.est | reg0[0]=0; reg0[1]=1; next; | Pass non-established connections (set COMMIT hint) |
The priority 1 flows ensure that:
- Blocked established connections are directly allowed (ALLOW verdict at priority 1 prevents them from being dropped by the per-ACL DROP hint flow at lower priority)
- Non-established connections get the COMMIT hint
8.5 Priority 34000 Flows (Service Monitor + DNS Bypass)
ovn-nbctl set NB_Global . options:default_acl_drop=trueovn-sbctl lflow-list sw0 | grep -B 1 -A 3 "34000"ovn-nbctl remove NB_Global . options default_acl_drop --if-existsExpected flows at priority 34000:
| Match | Action | Purpose |
|---|---|---|
eth.dst == $svc_monitor_mac (ingress) / eth.src == $svc_monitor_mac (egress) | reg8[16] = 1; next; | Service monitor health check bypass — sets ALLOW verdict |
flags.from_ctrl && udp.src == 53 (egress) | reg8[16] = 1; ct_commit; next; (if has_stateful) / reg8[16] = 1; next; (if !has_stateful) | DNS response bypass — allows DNS responses from controller |
These flows ensure that:
- DNS responses from OVN’s internal DNS service are always allowed
- Service monitor health checks bypass ACL drops
8.6 Priority 65533 Flows (Log-Related)
ovn-sbctl lflow-list sw0 | grep -B 1 -A 3 "65533"Expected flows at priority 65533:
These flows appear in the opposite pipeline from where the ACL was defined. They handle log-related actions for ACLs that have log=true.
For a from-lport ACL with logging enabled:
- The ACL evaluation happens in ingress (
ls_in_acl_eval) - The log-related flow appears in egress (
ls_out_acl_eval) at priority 65533
The purpose is to ensure logging happens after the packet has traversed both pipelines.
8.7 Challenge: ACL at Priority 65532 or Higher
# Try to add an ACL at priority 65532ovn-nbctl acl-add sw0 from-lport 65532 "ip4.src == 10.0.0.1" allowWhat happens: The ACL is added successfully, but its logical flow priority would be 65532 + 1000 = 66532, which is HIGHER than the universal flows at 65532. This means the per-ACL flow would be evaluated before the universal flows.
This is generally not recommended because:
- It can bypass the safety net provided by universal flows
- It may cause unexpected behavior for established connections
- The universal flows are designed to handle edge cases that per-ACL flows should not interfere with
Best practice: Keep ACL priorities well below 65532.
Cleanup
ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"Challenge
Add ACLs at priority 65530, 65531, and 65532. Trace a SYN packet and observe how the universal flows at 65532 interact with the per-ACL flows. Show that packets matching the ACL at 65532 are evaluated before the universal flows.
Lab 9: Port Groups — ACL Propagation
Objective
Understand how ACLs applied to port groups propagate to all switches containing ports in the group. Also understand that has_stateful is per-switch, so adding an allow-related ACL to a port group affects all switches containing ports in that group.
Steps
9.1 Create a Port Group
# Create port group with ports from both switchesovn-nbctl pg-add pg_web vm1 vm39.2 Verify Port Group Members
# Verify port group exists (pg-list may not exist on older versions; use ovn-nbctl show)ovn-nbctl show | grep -A 5 port_group# Expected:# port_group pg_web# port vm1# port vm39.3 Add an ACL to the Port Group
ovn-nbctl acl-add pg_web from-lport 1001 "ip4.src == 10.0.0.1" allow9.4 Verify ACL Appears on Both Switches
# Check sw0 ACLsovn-nbctl acl-list sw0# Expected:# from-lport 1001 (ip4.src == 10.0.0.1) allow
# Check sw1 ACLsovn-nbctl acl-list sw1# Expected:# from-lport 1001 (ip4.src == 10.0.0.1) allowThe ACL is propagated to both sw0 (which has vm1) and sw1 (which has vm3).
9.5 Verify Logical Flows on Both Switches
# Check sw0 ingress ACL evalovn-sbctl lflow-list sw0 | grep -B 1 -A 5 "ls_in_acl_eval"
# Check sw1 ingress ACL evalovn-sbctl lflow-list sw1 | grep -B 1 -A 5 "ls_in_acl_eval"Expected: Both switches have the per-ACL flow at priority 2001 matching ip4.src == 10.0.0.1 in their ingress ACL_EVAL stage.
9.6 ACL Evaluated Independently on Each Switch
The ACL is evaluated on each switch independently:
- On
sw0: The ACL matches traffic fromvm1(ip4.src == 10.0.0.1) - On
sw1: The ACL matches traffic fromvm3(ip4.src == 10.0.0.1)
Since vm3 has IP 10.0.0.3, the ACL ip4.src == 10.0.0.1 would NOT match vm3’s traffic on sw1. The ACL is present but does not affect vm3’s traffic.
9.7 Remove a Port from the Group
# Remove vm1 from the port group (pg-del-ports may not exist; use pg-set-ports to replace the member list)ovn-nbctl pg-set-ports pg_web vm3
# Verifyovn-nbctl show | grep -A 5 port_group# Expected:# vm3
# Check sw0 ACLsovn-nbctl acl-list sw0# Expected:# (empty — no ACLs, since vm1 is no longer in pg_web and sw0 has no other ACLs)
# Check sw1 ACLsovn-nbctl acl-list sw1# Expected:# from-lport 1001 (ip4.src == 10.0.0.1) allowWhen vm1 is removed from pg_web, the ACL is removed from sw0 (since no other ports on sw0 are in the group). The ACL remains on sw1 because vm3 is still in the group.
9.8 Re-add the Port and Verify
# Re-add vm1 to the port group (pg-add-ports may not exist; use pg-add with full member list)ovn-nbctl pg-add pg_web vm1 vm3ovn-nbctl acl-list sw0# Expected:# from-lport 1001 (ip4.src == 10.0.0.1) allowCleanup
ovn-nbctl acl-del pg_web from-lport 1001 "ip4.src == 10.0.0.1"ovn-nbctl pg-del pg_webChallenge
Create two port groups: pg_web (vm1, vm3) and pg_db (vm2, vm4). Add an ACL to pg_web allowing TCP port 80 and an ACL to pg_db allowing TCP port 3306. Verify that each ACL only appears on switches with ports in the respective group. Then move vm2 from pg_db to pg_web and verify the ACL propagation changes.
Lab 10: The BLOCK Hint Deep Dive
Objective
Deep dive into the BLOCK hint mechanism. Set up a scenario with allow-related at ingress and drop at egress, then trace established packets to see how ct_mark.blocked is set and how subsequent packets are handled.
Steps
10.1 Setup
# Ingress: allow-related for vm1ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-related
# Egress: drop for vm2ovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" drop10.2 Trace the First SYN (vm1 → vm2)
ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'Trace walkthrough:
Ingress pipeline:
ls_in_pre_acl (5):ct_next→ conntrackls_in_pre_stateful (7):ct_next→ continuesls_in_acl_hint (8): Priority 7 —ct.new && !ct.est→ ALLOW_NEW=1, DROP=1, COMMIT=1ls_in_acl_eval (9): Priority 2001 —ip4.src == 10.0.0.1→reg8[16] = 1; next;(ALLOW verdict)ls_in_acl_action (11): Priority 1000 —reg8[16] == 1→ ALLOW →next;ls_in_stateful (24): Priority 100 —reg0[1]==1 && reg0[13]==0→ct_commit { ct_mark.blocked = 0; ct_mark.allow_established = reg0[20]; ct_label.acl_id = reg2[16..31]; ct_label.nf = 0; ct_label.nf_id = 0; }; next;
Connection is committed to conntrack.
Egress pipeline:
7. ls_out_pre_acl (2): ct_next → conntrack
8. ls_out_pre_stateful (4): ct_next → continues
9. ls_out_acl_hint (5): Priority 7 — ct.new && !ct.est → ALLOW_NEW=1, DROP=1, COMMIT=1
has_statefulis per-switch (not per-pipeline). Since there is anallow-relatedACL onsw0,has_statefulis true for the entire switch, so egress ACL_HINT flows ARE generated.- For this SYN packet, the CT state is still
ct.new=1, ct.est=0because the packet’s CT state was set byct_next()in ingress PRE_STATEFUL, beforect_commitin ingress STATEFUL created the CT entry.
ls_out_acl_eval (6): Priority 2001 — Flow 1:reg8[30..31] == 0 && reg0[9] == 1 && (ip4.dst == 10.0.0.2)→reg8[17] = 1; next;(DROP verdict)ls_out_acl_action (8): Priority 1000 —reg8[17] == 1→ DROP → implicit drop
The SYN is dropped. The connection was committed in ingress, then dropped in egress.
10.3 Examine the BLOCK Hint Flow in ACL_EVAL
# Show the BLOCK hint flow for the drop ACL in egressovn-sbctl lflow-list sw0 | grep -B 1 -A 5 "ls_out_acl_eval"Expected Flow 1 (DROP hint) for the drop ACL:
match: reg8[30..31] == 0 && reg0[9] == 1 && (ip4.dst == 10.0.0.2)action: [log] [sample] next;Expected Flow 2 (BLOCK hint) for the drop ACL:
match: reg8[30..31] == 0 && reg0[10] == 1 && (ip4.dst == 10.0.0.2)action: [log] [sample] ct_commit { ct_mark.blocked = 1; ct_label.obs_point_id = <obs_pid>; }; next;Flow 1 matches DROP hint (reg0[9]=1) for new/untracked traffic and sets the DROP verdict. Flow 2 matches BLOCK hint (reg0[10]=1) for established, previously-allowed traffic and commits as blocked without setting a verdict.
10.4 Trace an Established Packet (vm1 → vm2)
ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 16'Trace walkthrough (egress):
ls_out_acl_hint (5): Priority 4 —!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 0→ ALLOW=1, BLOCK=1- Since
ct_mark.blocked == 0(first established packet), this matches - Sets BLOCK hint (reg0[10]=1)
- Since
ls_out_acl_eval (6): Priority 2001 (Flow 2, BLOCK hint) —reg8[30..31] == 0 && reg0[10] == 1 && (ip4.dst == 10.0.0.2)→ MATCHES- Action:
ct_commit { ct_mark.blocked = 1; ct_label.obs_point_id = <obs_pid>; }; next; - The connection is marked as blocked in conntrack
- No verdict register is set — the packet continues
- Action:
ls_out_acl_action (8): No verdict register set. Priority 0 defaultnext;(or implicit drop ifdefault_acl_drop=true)
The first established packet is committed as blocked but NOT dropped. The BLOCK hint flow (Flow 2) does not set a DROP verdict. The packet continues through the pipeline.
10.5 Subsequent Established Packets
After ct_mark.blocked is set to 1, the ACL_HINT priorities change. For request-direction blocked traffic (ct.est=1, ct.rpl=0, ct_mark.blocked=1):
- Priority 6 matches:
!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1→ ALLOW_NEW=1, DROP=1, COMMIT=1 - ACL_EVAL: Flow 1 (DROP hint,
reg0[9] == 1) matches →reg8[17] = 1; next;→ DROP verdict - ACL_ACTION: Priority 1000 →
reg8[17] == 1→ implicit drop
For reply-direction blocked traffic (ct.est=1, ct.rpl=1, ct_mark.blocked=1):
- Priority 2 matches:
ct.est && ct_mark.blocked == 1→ DROP=1 - ACL_EVAL: Priority 65532 universal flow:
ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)→reg8[17] = 1; next;→ DROP verdict - ACL_ACTION: Priority 1000 →
reg8[17] == 1→ implicit drop
Both request-direction and reply-direction blocked traffic is dropped. The BLOCK hint mechanism prevents asymmetric drops (where the SYN was allowed but return traffic was blocked) by marking the connection as blocked in conntrack, allowing the reply-direction universal flow at 65532 to drop reply traffic consistently.
Cleanup
ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"ovn-nbctl acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"Challenge
Set up the same scenario but with the drop ACL at a different priority (e.g., 2001 instead of 1001). Trace packets and show how the tier match prefix reg8[30..31] changes in the BLOCK hint flow. What happens if the drop ACL is at a higher priority than the allow-related ACL?
Lab 11: Reject Action — ICMP Unreachable / TCP RST
Objective
Understand how the reject action differs from drop. The reject action sends an ICMP unreachable (for UDP/ICMP) or TCP RST (for TCP) before dropping the packet.
Steps
11.1 Add a Reject ACL
ovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" reject11.2 Trace a SYN
ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'Expected trace walkthrough:
Ingress pipeline:
ls_in_pre_acl (5):ct_nextls_in_acl_hint (8): Priority 0next;(no stateful ACL on ingress)ls_in_acl_eval (9): No matching ACL → priority 0next;ls_in_acl_action (11): Priority 0next;
Egress pipeline:
5. ls_out_pre_acl (2): ct_next
6. ls_out_acl_hint (5): Priority 0 next; (no has_stateful on this switch)
7. ls_out_acl_eval (6): Priority 2001 — ip4.dst == 10.0.0.2 → reg0[0]=0; reg8[18]=1; next;
- The reject action sets
reg8[18](REGBIT_ACL_REJECT) = 1
ls_out_acl_action (8): Priority 1000 —reg8[18]==1- For TCP: Generates TCP RST with COPP meter
- For UDP/ICMP: Generates ICMP destination unreachable with COPP meter
The reject action is the same as drop through ACL_EVAL, but at ACL_ACTION stage, reg8[18]==1 triggers ICMP unreachable or TCP RST.
11.3 Compare with Drop ACL
# Remove reject ACLovn-nbctl acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"
# Add drop ACLovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" drop
# Trace the same SYNovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'Comparison:
| Property | drop | reject |
|---|---|---|
| ACL_EVAL match | ip4.dst == 10.0.0.2 → reg8[17]=1; next; (DROP verdict) | ip4.dst == 10.0.0.2 → reg0[0]=0; reg8[18]=1; next; (REJECT verdict) |
| ACL_ACTION | Priority 1000 reg8[17]==1 → implicit drop | Priority 1000 reg8[18]==1 → ICMP/TCP RST |
| Packet sent to source | No | Yes (ICMP unreachable or TCP RST) |
| COPP meter | No | Yes (rate-limited) |
11.4 COPP Meter for Reject Actions
The reject action uses a COPP (Control Plane Policing) meter to rate-limit the ICMP/TCP RST responses. This prevents a flood of reject responses from overwhelming the control plane.
The COPP meter is configured in the OVN southbound database and is applied to all reject-generated packets.
11.5 Challenge: Trace a UDP Packet
# Trace a UDP packetovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 17 && udp.dst == 53'For UDP packets, the reject action generates an ICMP destination unreachable (port unreachable) message instead of a TCP RST.
Cleanup
ovn-nbctl acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"Lab 12: ACL Logging
Objective
Understand how ACL logging works, including the log() action, meter groups, and the priority 65533 flows in the opposite pipeline.
Steps
12.1 Add an ACL with Logging Enabled
ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allowNow enable logging:
# Find the ACL UUIDACL_UUID=$(ovn-nbctl --data=bare --no-heading --columns=_uuid find acl direction=from-lport priority=1001 match="ip4.src == 10.0.0.1")
# Enable logging with severity=warningovn-nbctl set acl $ACL_UUID log=true severity=warning name=acl_log_112.2 Verify Logging is Enabled
ovn-nbctl --columns=direction,priority,match,action,log,severity,name list acl# Expected:# from-lport 1001 (ip4.src == 10.0.0.1) allow true warning acl_log_112.3 Show the log() Action in Generated Flow
ovn-sbctl lflow-list sw0 | grep -B 1 -A 5 "ls_in_acl_eval"Expected output: The per-ACL flow now includes a log() action:
match: ip4.src == 10.0.0.1action: log(name="acl_log_1", severity=warning, meter=<meter>, direction=from-lport); reg0[0]=0; next;The log() action is evaluated at the ACL_EVAL stage, before the packet continues through the pipeline. The parameters include the ACL name, severity level, meter for rate-limiting, and direction (from-lport for ingress, to-lport for egress).
12.4 Meter Groups and Fair Metering
OVN ACL logging uses meter groups to implement fair metering. The meter group determines the rate limit for log messages.
# Show meter groupsovn-sbctl meter-group-listThe meter group ensures that:
- Log messages are rate-limited to prevent overwhelming the logging infrastructure
- Multiple ACLs sharing the same meter group share the rate limit
- Different ACLs can have different rate limits by using different meter groups
12.5 Challenge: Priority 65533 Flows in Opposite Pipeline
# Add a from-lport ACL with logging (ingress pipeline)ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow
# Enable loggingACL_UUID=$(ovn-nbctl --data=bare --no-heading --columns=_uuid find acl direction=from-lport priority=1001 match="ip4.src == 10.0.0.1")ovn-nbctl set acl $ACL_UUID log=true severity=warning name=acl_log_1
# Check for priority 65533 flows in the OPPOSITE pipeline (egress)ovn-sbctl lflow-list sw0 | grep -B 1 -A 5 "65533"Expected: Priority 65533 flows appear in ls_out_acl_eval (egress pipeline), even though the ACL was defined in ingress (from-lport). These flows handle log-related actions in the opposite pipeline.
Why? The log action needs to be evaluated after the packet has traversed both pipelines. By placing the log-related flow in the opposite pipeline, OVN ensures that the packet’s final action (allow/drop) is determined before logging occurs.
Cleanup
ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"Challenge
Add multiple ACLs with logging at different severities (info, warning, error). Trace packets through each ACL and verify that the log() action in the generated flow includes the correct severity. Show how meter groups affect the rate limiting of log messages.
Lab 13: Global Options — acl_ct_translation
Objective
Understand how the acl_ct_translation option changes ACL match expressions from standard header fields to CT (conntrack) fields for fragment matching.
Steps
13.1 Default State (acl_ct_translation = false)
# Verify defaultovn-nbctl get NB_Global . options:acl_ct_translation# Expected: "" (empty, meaning false)
# Add an ACL with a TCP matchovn-nbctl acl-add sw0 from-lport 1001 "tcp.dst == 80" allow
# Show the generated flowovn-sbctl lflow-list sw0 | grep -B 1 -A 5 "ls_in_acl_eval"Expected output: The match uses standard header fields:
match: tcp.dst == 80action: reg0[0]=0; next;13.2 Enable acl_ct_translation
ovn-nbctl set NB_Global . options:acl_ct_translation=true
# Re-add the ACL (or modify it to trigger regeneration)ovn-nbctl acl-del sw0 from-lport 1001 "tcp.dst == 80"ovn-nbctl acl-add sw0 from-lport 1001 "tcp.dst == 80" allow
# Show the generated flowovn-sbctl lflow-list sw0 | grep -B 1 -A 5 "ls_in_acl_eval"Expected output: The match now uses CT fields:
match: ct_tp_dst == 80action: reg0[0]=0; next;13.3 Symbol Table Difference
With acl_ct_translation=false:
tcp.dst→ matches TCP destination port in the packet headerudp.src→ matches UDP source port in the packet headericmp4.type→ matches ICMP type in the packet header
With acl_ct_translation=true:
tcp.dst→ct_tp_dst(matches in conntrack zone)udp.src→ct_tp_src(matches in conntrack zone)icmp4.type→ct_nw_protoor similar CT field
Why? When dealing with fragmented packets, the transport layer headers (TCP/UDP ports) may be in a different fragment than the IP header. Standard header fields cannot match across fragments, but CT fields can because conntrack reassembles fragments.
13.4 Challenge: Fragmented TCP Packets
# With acl_ct_translation=false (default)ovn-nbctl remove NB_Global . options acl_ct_translation --if-existsovn-nbctl acl-del sw0 from-lport 1001 "tcp.dst == 80"ovn-nbctl acl-add sw0 from-lport 1001 "tcp.dst == 80" allow
# Trace a fragmented TCP packet# First fragment (IP header + TCP header): ip4.frag == 1ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip4.frag == 1 && tcp.dst == 80'
# Second fragment (no TCP header): ip4.frag == 1 && ip4.nxt == 6ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip4.frag == 1'With acl_ct_translation=false:
- First fragment (has TCP header):
tcp.dst == 80matches → allowed - Second fragment (no TCP header):
tcp.dst == 80does NOT match → may be dropped or allowed by other rules
With acl_ct_translation=true:
- First fragment:
ct_tp_dst == 80matches (conntrack reassembles) → allowed - Second fragment:
ct_tp_dst == 80matches (conntrack reassembles) → allowed
The key difference: With CT translation, conntrack handles fragment reassembly, so the ACL can match on reassembled packets rather than individual fragments.
Cleanup
ovn-nbctl acl-del sw0 from-lport 1001 "tcp.dst == 80"ovn-nbctl remove NB_Global . options acl_ct_translation --if-existsChallenge
Enable acl_ct_translation and add ACLs for multiple protocols (TCP, UDP, ICMP). Show the CT symbol table for each protocol. Compare with the standard symbol table when acl_ct_translation=false.
Lab 14: End-to-End Packet Trace
Objective
Perform complete packet traces for various scenarios, combining all concepts from previous labs.
Setup
# Clean slateovn-nbctl --if-exists acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"ovn-nbctl --if-exists acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"ovn-nbctl --if-exists acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"ovn-nbctl --if-exists acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"14.1 Full Trace: SYN with Drop ACL (Egress)
# Add drop ACL in egressovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" drop
# Trace SYNovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'Expected trace:
| Stage | Pipeline | Action |
|---|---|---|
| ls_in_pre_acl (5) | Ingress | ct_next → conntrack |
| ls_in_acl_hint (8) | Ingress | Priority 0 → next; |
| ls_in_acl_eval (9) | Ingress | No matching ACL → priority 0 next; |
| ls_in_acl_action (11) | Ingress | Priority 0 → next; |
| ls_out_pre_acl (2) | Egress | ct_next → conntrack |
| ls_out_acl_hint (5) | Egress | Priority 0 → next; |
| ls_out_acl_eval (6) | Egress | Priority 2001 — ip4.dst == 10.0.0.2 → reg8[17]=1; next; (DROP verdict) |
| ls_out_acl_action (8) | Egress | Priority 1000 — reg8[17]==1 → DROP |
| Result | Packet dropped |
14.2 Full Trace: SYN-ACK Reply
# SYN-ACK from vm2 to vm1 (reply to the dropped SYN)ovn-trace sw0 'inport == "vm2" && eth.src == 00:00:00:00:00:02 && eth.dst == 00:00:00:00:00:01 && ip4.src == 10.0.0.2 && ip4.dst == 10.0.0.1 && ip.proto == 6 && tcp.flags == 18'Expected trace:
Since the SYN was dropped (no connection established), ct.est is false for the SYN-ACK. The universal flow at 65532 (ct.est && !ct.rel && ct.rpl && ct_mark.blocked == 0) does NOT match because ct.est is false. The SYN-ACK hits priority 0 (default next;).
| Stage | Pipeline | Action |
|---|---|---|
| ls_in_pre_acl (5) | Ingress | ct_next → conntrack |
| ls_in_acl_hint (8) | Ingress | Priority 0 → next; |
| ls_in_acl_eval (9) | Ingress | No matching ACL → priority 0 next; (universal flow at 65532 does NOT match — ct.est is false) |
| ls_in_acl_action (11) | Ingress | Priority 0 → next; |
| Result | Packet delivered to vm1 (reply is not subject to egress drop ACL for vm2) |
14.3 Full Trace: Established Data Packet
# Established data: vm1 → vm2ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 16'Expected trace:
| Stage | Pipeline | Action |
|---|---|---|
| ls_in_pre_acl (5) | Ingress | ct_next → conntrack |
| ls_in_acl_hint (8) | Ingress | Priority 0 → next; |
| ls_in_acl_eval (9) | Ingress | No matching ACL → priority 0 next; |
| ls_out_pre_acl (2) | Egress | ct_next → conntrack |
| ls_out_acl_hint (5) | Egress | Priority 0 → next; |
| ls_out_acl_eval (6) | Egress | Priority 2001 — ip4.dst == 10.0.0.2 → reg8[17]=1; next; (DROP verdict) |
| ls_out_acl_action (8) | Egress | Priority 1000 — reg8[17]==1 → DROP |
| Result | Packet dropped |
14.4 Full Trace: Allow-Related ACL (Ingress)
# Remove old ACLovn-nbctl acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"
# Add allow-related ACL in ingressovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-related
# Trace SYNovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'Expected trace:
| Stage | Pipeline | Action |
|---|---|---|
| ls_in_pre_acl (5) | Ingress | ct_next → conntrack |
| ls_in_pre_stateful (7) | Ingress | ct_next → continues |
| ls_in_acl_hint (8) | Ingress | Priority 7 — ct.new && !ct.est → ALLOW_NEW=1, DROP=1, COMMIT=1 |
| ls_in_acl_eval (9) | Ingress | Priority 2001 — ip4.src == 10.0.0.1 → reg8[16]=1; next; (ALLOW verdict) |
| ls_in_acl_action (11) | Ingress | Priority 1000 — reg8[16]==1 → ALLOW → next; |
| ls_in_stateful (24) | Ingress | Priority 100 — reg0[1]==1 && reg0[13]==0 → ct_commit |
| Result | Packet delivered, connection committed |
14.5 Full Trace: Allow-Related + Label (ct_commit with Metadata)
# Remove old ACLovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"
# Add allow-related ACL with labelovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-relatedACL_UUID=$(ovn-nbctl --data=bare --no-heading --columns=_uuid find acl direction=from-lport priority=1001 match="ip4.src == 10.0.0.1")ovn-nbctl set acl $ACL_UUID label=42
# Trace SYNovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'Expected trace:
| Stage | Pipeline | Action |
|---|---|---|
| ls_in_pre_acl (5) | Ingress | ct_next → conntrack |
| ls_in_pre_stateful (7) | Ingress | ct_next → continues |
| ls_in_acl_hint (8) | Ingress | Priority 7 — ct.new && !ct.est → ALLOW_NEW=1, DROP=1, COMMIT=1 |
| ls_in_acl_eval (9) | Ingress | Priority 2001 — ip4.src == 10.0.0.1 → reg8[16]=1; next; (ALLOW verdict) |
| ls_in_acl_action (11) | Ingress | Priority 1000 — reg8[16]==1 → ALLOW → next; |
| ls_in_stateful (24) | Ingress | Priority 100 — reg0[1]==1 && reg0[13]==1 → ct_commit with label |
| Result | Packet delivered, ct_label.acl_id = 42 |
The ct_commit variant is now Variant 1 (with label, no NF) because reg0[13]==1 (label present).
14.6 Full Trace: Reject ACL
# Remove old ACLovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"
# Add reject ACL in egressovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" reject
# Trace TCP SYNovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip.proto == 6 && tcp.flags == 2'Expected trace:
| Stage | Pipeline | Action |
|---|---|---|
| ls_in_pre_acl (5) | Ingress | ct_next → conntrack |
| ls_in_acl_hint (8) | Ingress | Priority 0 → next; |
| ls_in_acl_eval (9) | Ingress | No matching ACL → priority 0 next; |
| ls_in_acl_action (11) | Ingress | Priority 0 → next; |
| ls_out_pre_acl (2) | Egress | ct_next → conntrack |
| ls_out_acl_hint (5) | Egress | Priority 0 → next; |
| ls_out_acl_eval (6) | Egress | Priority 2001 — ip4.dst == 10.0.0.2 → reg0[0]=0; reg8[18]=1; next; |
| ls_out_acl_action (8) | Egress | Priority 1000 — reg8[18]==1 → TCP RST with COPP meter |
| Result | TCP RST sent, packet dropped |
14.7 Challenge: Trace a Fragmented Packet
# Clean upovn-nbctl acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"
# Add allow-related ACLovn-nbctl acl-add sw0 from-lport 1001 "tcp.dst == 80" allow-related
# Trace a fragmented TCP packet (first fragment with TCP header)ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip4.frag == 1 && tcp.dst == 80'
# Trace a fragmented TCP packet (second fragment without TCP header)ovn-trace sw0 'inport == "vm1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 10.0.0.1 && ip4.dst == 10.0.0.2 && ip4.frag == 1'Expected behavior:
With acl_ct_translation=false (default):
- First fragment:
tcp.dst == 80matches → allowed, committed - Second fragment:
tcp.dst == 80does NOT match (no TCP header in second fragment) → may be dropped or allowed by universal flows
With acl_ct_translation=true:
- Both fragments:
ct_tp_dst == 80matches (conntrack reassembles) → allowed, committed
Cleanup
ovn-nbctl --if-exists acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"ovn-nbctl --if-exists acl-del sw0 from-lport 1001 "tcp.dst == 80"ovn-nbctl --if-exists acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"Appendix A: ct_commit Variants Reference
| Variant | Priority | Match | With Label | With NF | Usage |
|---|---|---|---|---|---|
| 1 | 100 | reg0[1]==1 && reg0[13]==1 | Yes | No | allow-related with label |
| 2 | 100 | reg0[1]==1 && reg0[13]==0 | No | No | Plain allow-related |
| 3 | 110 | reg0[1]==1 && reg0[13]==0 && reg8[21]==1 | No | Yes | allow-related with NF |
| 4 | 110 | reg0[1]==1 && reg0[13]==1 && reg8[21]==1 | Yes | Yes | allow-related with label AND NF |
Key fields:
reg0[1]: COMMIT hint (set by ACL_HINT when packet needs conntrack commit)reg0[13]: Label present (set when ACL has a label option)reg8[21]: NF hint (set when Network Function processing is needed)
Appendix B: ct_mark and ct_label Fields
| Field | Purpose |
|---|---|
ct_mark.blocked | Set to 1 when a packet is blocked by a drop ACL after being committed by an allow-related ACL. Prevents asymmetric drops. |
ct_mark.allow_established | Set to 1 for connections that should be allowed in established state. Used by OVN internally for service flows. |
ct_label.acl_id | Stores the ACL identifier (label) for observability. Used by sFlow/IPFIX to identify which ACL matched the packet. |
ct_label.obs_point_id | Stores the observation point ID for BLOCK hint flows. Identifies which drop ACL caused the block. |
Appendix C: ACL_HINT Priority Reference
When has_stateful is true (due to allow-related ACL or LB VIP):
| Priority | Match | reg0[7] ALLOW_NEW | reg0[1] COMMIT | reg0[8] ALLOW | reg0[9] DROP | reg0[10] BLOCK | Meaning |
|---|---|---|---|---|---|---|---|
| 7 | ct.new && !ct.est | 1 | 1 | — | 1 | — | New connection |
| 6 | !ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1 | 1 | 1 | — | 1 | — | Established, blocked |
| 5 | !ct.trk | — | — | 1 | 1 | — | Not tracked |
| 4 | !ct.new && ct.est && !ct.rpl && ct_mark.blocked == 0 | — | — | 1 | — | 1 | Established, not blocked |
| 3 | !ct.est | — | — | — | 1 | — | Not established |
| 2 | ct.est && ct_mark.blocked == 1 | — | — | — | 1 | — | Established but blocked |
| 1 | ct.est && ct_mark.blocked == 0 | — | — | — | — | 1 | Established, set BLOCK hint |
Appendix D: Universal Flows Priority Reference
| Priority | Stage | Purpose |
|---|---|---|
| 65533 | ACL_EVAL (opposite pipeline) | Log-related flows |
| 65532 | ACL_EVAL (both pipelines) | Safety net: invalid/block/allow established |
| 34000 | ACL_EVAL | Service monitor MAC bypass + DNS bypass |
| 1 | ACL_EVAL | Re-allow blocked established + pass non-established |
| 0 | ACL_EVAL | Default pass-through |
Appendix E: NB_Global Options Reference
| Option | Default | Description |
|---|---|---|
default_acl_drop | false | When true, priority 0 in ACL_ACTION is implicit drop instead of next; |
acl_ct_translation | false | When true, ACL matches use CT fields (ct_tp_dst) instead of header fields (tcp.dst) for fragment matching |
use_ct_inv_match | true | When true, universal flows match ct.inv for invalid connections |
Setting options:
ovn-nbctl set NB_Global . options:default_acl_drop=trueovn-nbctl set NB_Global . options:acl_ct_translation=trueovn-nbctl set NB_Global . options:use_ct_inv_match=trueRemoving options:
ovn-nbctl remove NB_Global . options default_acl_drop --if-existsovn-nbctl remove NB_Global . options acl_ct_translation --if-existsovn-nbctl remove NB_Global . options use_ct_inv_match --if-existsCRITICAL: Always use
--if-existswhen removing NB_Global options to avoid errors if the option is not set.