Logo

Search

    Overview

    Labs: OVN ACL Processing — A Hands-On Walkthrough

    July 10, 2026
    61 min read

    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.4

    All 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_*)

    StageNumber
    ls_in_pre_acl5
    ls_in_pre_lb6
    ls_in_pre_stateful7
    ls_in_acl_hint8
    ls_in_acl_eval9
    ls_in_acl_sample10
    ls_in_acl_action11
    ls_in_qos12
    ls_in_lb15
    ls_in_acl_after_lb_eval20
    ls_in_acl_after_lb_sample21
    ls_in_acl_after_lb_action22
    ls_in_stateful24

    Egress Pipeline Stages (ls_out_*)

    StageNumber
    ls_out_pre_acl2
    ls_out_pre_lb3
    ls_out_pre_stateful4
    ls_out_acl_hint5
    ls_out_acl_eval6
    ls_out_acl_sample7
    ls_out_acl_action8
    ls_out_stateful12

    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_vip

    CRITICAL: 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 directionPipelineStages
    from-lportIngressls_in_* (5-11)
    to-lportEgressls_out_* (2-8)

    Source: northd.c:7575bool ingress = !strcmp(acl->direction, "from-lport") ? true : false;

    CRITICAL: from-lport means traffic from the logical port, which is the ingress pipeline (traffic arriving at the switch from the VM). to-lport means traffic to the logical port, which is the egress pipeline (traffic leaving the switch toward the VM).

    Command Reference

    Terminal window
    # Add ACL
    ovn-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 ACLs
    ovn-nbctl acl-list <switch>
    # Remove ACL
    ovn-nbctl acl-del <switch> from-lport <priority> <match>
    # List logical flows for a switch
    ovn-sbctl lflow-list <switch>
    # List logical flows filtered by stage
    ovn-sbctl lflow-list <switch> | grep -A 100 "ls_in_acl_eval" | head -60
    ovn-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 options
    ovn-nbctl set NB_Global . options:default_acl_drop=true
    ovn-nbctl remove NB_Global . options default_acl_drop --if-exists

    Lab 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

    Terminal window
    # Create switches
    ovn-nbctl ls-add sw0
    ovn-nbctl ls-add sw1
    # Create router
    ovn-nbctl lr-add R1

    1.2 Create VM Ports

    Terminal window
    # sw0 ports
    ovn-nbctl lsp-add sw0 vm1
    ovn-nbctl lsp-set-addresses vm1 "00:00:00:00:00:01 10.0.0.1"
    ovn-nbctl lsp-add sw0 vm2
    ovn-nbctl lsp-set-addresses vm2 "00:00:00:00:00:02 10.0.0.2"
    # sw1 ports
    ovn-nbctl lsp-add sw1 vm3
    ovn-nbctl lsp-set-addresses vm3 "00:00:00:00:00:03 10.0.0.3"
    ovn-nbctl lsp-add sw1 vm4
    ovn-nbctl lsp-set-addresses vm4 "00:00:00:00:00:04 10.0.0.4"
    Terminal window
    # Router gateway ports
    ovn-nbctl lrp-add R1 sw0-p 00:00:00:00:ff:01 10.0.0.254/24
    ovn-nbctl lrp-add R1 sw1-p 00:00:00:00:ff:02 10.0.0.254/24
    # Connect switches to router
    ovn-nbctl lsp-add sw0 sw0-r
    ovn-nbctl lsp-set-type sw0-r router
    ovn-nbctl lsp-set-addresses sw0-r 00:00:00:00:ff:01
    ovn-nbctl lsp-set-options sw0-r router-port=sw0-p
    ovn-nbctl lsp-add sw1 sw1-r
    ovn-nbctl lsp-set-type sw1-r router
    ovn-nbctl lsp-set-addresses sw1-r 00:00:00:00:ff:02
    ovn-nbctl lsp-set-options sw1-r router-port=sw1-p

    1.4 Verify Topology

    Terminal window
    # Verify switches exist
    ovn-nbctl ls-list
    # Expected:
    # sw0
    # sw1
    # Verify router
    ovn-nbctl lr-list
    # Expected:
    # R1
    # Verify ports on sw0
    ovn-nbctl lsp-list sw0
    # Expected:
    # vm1
    # vm2
    # sw0-r
    # Verify ports on sw1
    ovn-nbctl lsp-list sw1
    # Expected:
    # vm3
    # vm4
    # sw1-r

    1.5 Inspect Logical Flows (Before ACLs)

    Terminal window
    # Show all logical flows for sw0 (will be verbose)
    ovn-sbctl lflow-list sw0 | head -80
    # Filter for ACL stages specifically
    ovn-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)

    Terminal window
    ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow

    2.2 Verify the ACL Appears in Ingress Pipeline

    Terminal window
    # List ACLs
    ovn-nbctl acl-list sw0
    # Expected:
    # from-lport 1001 (ip4.src == 10.0.0.1) allow
    # Check logical flows — filter for ACL eval stages
    ovn-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)

    Terminal window
    ovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" allow

    2.4 Verify the ACL Appears in Egress Pipeline

    Terminal window
    # List ACLs
    ovn-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 eval
    ovn-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

    Terminal window
    # Trace a packet from vm1 (ingress on sw0) — should match the from-lport ACL
    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'

    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 continues

    The 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 DirectionMeansPipelineStage Name
    from-lportTraffic from the port (arriving at switch from VM)Ingressls_in_acl_eval (stage 9)
    to-lportTraffic to the port (leaving switch toward VM)Egressls_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

    Terminal window
    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.


    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

    Terminal window
    ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-related

    3.2 Verify has_stateful_acl is True

    Terminal window
    ovn-nbctl acl-list sw0
    # Expected:
    # from-lport 1001 (ip4.src == 10.0.0.1) allow-related

    The 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

    Terminal window
    # 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 65532
    ovn-sbctl lflow-list sw0 | grep -B 1 -A 5 "65532"

    3.4 Trace a SYN Packet (New Connection)

    Terminal window
    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:

    1. ls_in_pre_acl (stage 5): ct_next — sends traffic through conntrack
    2. ls_in_pre_stateful (stage 7): ct_next — continues through conntrack
    3. ls_in_acl_hint (stage 8): Match at priority 7ct.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
    4. 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
    5. 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
    6. ls_in_stateful (stage 24): Match at priority 100reg0[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-related without explicit labels

    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):

    Terminal window
    # 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:

    1. ls_out_pre_acl (stage 2): ct_next → conntrack
    2. ls_out_pre_stateful (stage 4): ct_next → continues
    3. ls_out_acl_hint (stage 5): Priority 1 — ct.est && ct_mark.blocked == 0 → BLOCK=1
    4. ls_out_acl_eval (stage 6): Match at priority 65532ct.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
    5. 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

    Terminal window
    # 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:

    1. 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
    2. 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 (plain allow-related without 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 requires ct.rpl == 1 (reply direction)
    3. ls_in_acl_action (stage 11): Priority 0 next; — no verdict register was set by the per-ACL flow
    4. ls_in_stateful (stage 24): No ct_commit flow matchesreg0[1] is not set for established packets
      • The priority 0 default next; applies

    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:

    Terminal window
    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 UUID
    ACL_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=42

    Now 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

    Terminal window
    ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"

    Key Concepts

    ConceptValue
    has_stateful_acltrue (because action is allow-related)
    SYN hint (priority 7)ALLOW_NEW=1, DROP=1, COMMIT=1
    SYN-ACK handlingUniversal flow at 65532 in egress pipeline (reply direction, ct.rpl==1)
    Established data handlingPer-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 SYNVariant 2 (no label, no NF) at priority 100
    ct_commit for establishedNot 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

    Terminal window
    ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow

    4.2 Verify has_stateful_acl is False

    Terminal window
    ovn-nbctl acl-list sw0
    # Expected:
    # from-lport 1001 (ip4.src == 10.0.0.1) allow

    Since 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

    Terminal window
    # 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

    Terminal window
    # 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

    Terminal window
    # 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.1
    action: 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

    Terminal window
    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:

    1. ls_in_pre_acl (stage 5): Priority 0 next; — no ct_next flow because has_stateful is false (plain allow does not set has_stateful_acl, and there are no LB VIPs). Only the default next; flow exists.
    2. ls_in_pre_stateful (stage 7): Priority 0 next; — no ct_next flow because there is no conntrack commitment to execute.
    3. ls_in_acl_hint (stage 8): Priority 0 next; — no hint generation (no has_stateful).
    4. ls_in_acl_eval (stage 9): Priority 2001 ip4.src == 10.0.0.1next;
    5. ls_in_acl_action (stage 11): Priority 0 next; — no verdict set by per-ACL flow, default applies
    6. 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

    Propertyallowallow-related
    has_stateful_aclfalsetrue
    ACL_HINT flows (priorities 1-7)NoneGenerated (priorities 1-7)
    ACL_HINT priority 0 flownext; (pass-through)next; (pass-through)
    ct_commit in STATEFULNoneYes (Variant 1 or 2)
    Connection trackingConntrack traversed but not committedConntrack committed
    Reply handlingUniversal flows still applyUniversal flows handle replies

    Cleanup

    Terminal window
    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)

    Terminal window
    ovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" drop

    5.2 Trace a SYN Through Egress

    Terminal window
    # SYN from vm1 to vm2, arriving at sw0 ingress, then egress toward 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 trace walkthrough (ingress):

    1. ls_in_pre_acl (stage 5): ct_next
    2. ls_in_acl_hint (stage 8): Priority 0 next; — no stateful ACL on ingress, no hint flows
    3. ls_in_acl_eval (stage 9): No matching ACL in ingress → priority 0 next; applies
    4. ls_in_acl_action (stage 11): Priority 0 next;

    Expected trace walkthrough (egress):

    1. ls_out_pre_acl (stage 2): ct_next
    2. ls_out_acl_hint (stage 5): Priority 0 next; — no hint flows generated (no has_stateful on this switch)
    3. ls_out_acl_eval (stage 6): Priority 2001 — ip4.dst == 10.0.0.2drop
    4. ls_out_acl_action (stage 8): The packet is dropped

    The SYN is dropped. No connection is established.

    Remove the old ACL and set up a scenario with both:

    Terminal window
    ovn-nbctl acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"
    # Ingress: allow-related for vm1
    ovn-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" drop

    Important: 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)

    Terminal window
    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.1reg8[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=0 because the packet’s CT state was set by ct_next() in ingress PRE_STATEFUL, before ct_commit in ingress STATEFUL created the CT entry. The ct_commit writes 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=0 and priority 4 would match instead.
    • 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
    • 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:

    Terminal window
    # 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):

    PriorityMatchHints SetMeaning
    7ct.new && !ct.estALLOW_NEW=1, DROP=1, COMMIT=1New connection, may be allowed or dropped
    6!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1ALLOW_NEW=1, DROP=1, COMMIT=1Established, previously blocked
    5!ct.trkALLOW=1, DROP=1Not tracked
    4!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 0ALLOW=1, BLOCK=1Established, not blocked
    3!ct.estDROP=1Not established
    2ct.est && ct_mark.blocked == 1DROP=1Established but blocked
    1ct.est && ct_mark.blocked == 0BLOCK=1Established, 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:

    Terminal window
    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_id for 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:

    Terminal window
    # Established data: 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'

    Expected trace (egress):

    1. 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)
    2. 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
    3. ls_out_acl_action (stage 8): No verdict register is set. Priority 0 default next; applies (or implicit drop if default_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:

    1. First SYN is committed (ingress) then dropped (egress) by Flow 1 (DROP hint, reg0[9] == 1)
    2. 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 with ct_mark.blocked = 1
    3. Subsequent request-direction packets: ACL_HINT priority 6 sets DROP=1 → Flow 1 drops them
    4. Subsequent reply-direction packets: Universal flow at 65532 drops them (ct.est && ct.rpl && ct_mark.blocked == 1)
    5. 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

    Terminal window
    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

    Terminal window
    ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-stateless

    6.2 Verify the Flow Appears in PRE_ACL

    Terminal window
    # 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.1
    action: 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

    Terminal window
    # 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.1
    action: [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

    Terminal window
    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

    Terminal window
    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:

    1. ls_in_pre_acl (stage 5): Priority 2001 — ip4.src == 10.0.0.1reg0[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)
    2. ls_in_acl_hint (stage 8): Priority 0 next;
    3. 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
    4. ls_in_acl_action (stage 11): Priority 0 next;
    5. 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:

    Terminal window
    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

    Trace the same packet and compare:

    Propertyallow-statelessallow
    Flow locationls_in_pre_acl (stage 5) + ls_in_acl_eval (stage 9)ls_in_acl_eval (stage 9) only
    Priorityacl_priority + 1000acl_priority + 1000
    ConntrackBypassed (sets reg0[16]=1 in pre_acl)Traversed (but not committed)
    reg0[16] (STATELESS)Set to 1Not set
    ACL_HINT flowsNot generated (no has_stateful from stateless ACL)Not generated (plain allow doesn’t set has_stateful_acl)
    ct_commitNoneNone
    Per-ACL flow in ACL_EVALYes, but no hint matchingYes, no hint matching (plain allow)

    6.7 Challenge: Stateless + Stateful ACL on Same Packet

    Terminal window
    # Remove old ACL
    ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"
    # Add a stateless ACL at priority 1001
    ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-stateless
    # Add a stateful ACL at priority 1002
    ovn-nbctl acl-add sw0 from-lport 1002 "ip4.dst == 10.0.0.2" allow-related

    Trace a packet matching both:

    Terminal window
    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

    Terminal window
    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

    Terminal window
    # Tier 0 ACLs (default tier = 0)
    ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow
    ovn-nbctl acl-add sw0 from-lport 1002 "ip4.src == 10.0.0.2" drop
    # Tier 1 ACLs
    ovn-nbctl --type=ls --tier=1 acl-add sw0 from-lport 1001 "ip4.dst == 10.0.0.3" allow

    Note: The --tier flag 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. Run ovn-nbctl --help | grep tier to check if your version supports it.

    7.2 Verify Tier Match Prefix

    Terminal window
    # Show ACL_EVAL flows for ingress
    ovn-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:

    Terminal window
    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] == 0
    action: reg8[30..31] = 1; next(pipeline=ingress, table=9);

    And possibly another at priority 500 for tier 1:

    match: reg8[30..31] == 1
    action: 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

    Terminal window
    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:

    1. ls_in_acl_eval (stage 9): Priority 2001 — reg8[30..31] == 0 && ip4.src == 10.0.0.1next; (tier 0, allowed)
    2. ls_in_acl_action (stage 11): Priority 500 — reg8[30..31] == 0reg8[30..31] = 1; next(pipeline=ingress, table=9); (advance to tier 1, loop back to ACL_EVAL)
    3. ls_in_acl_eval (stage 9): Priority 2001 — reg8[30..31] == 1 && ip4.dst == 10.0.0.3next; (tier 1, allowed)
    4. ls_in_acl_action (stage 11): Priority 500 — reg8[30..31] == 1reg8[30..31] = 2; next(pipeline=ingress, table=9); (advance to tier 2, loop back to ACL_EVAL)
    5. ls_in_acl_eval (stage 9): No tier 2 ACLs → priority 0 next; (pass-through)
    6. 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)

    Terminal window
    # Add ACLs at tier 0, 1, 2, 3
    ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow
    ovn-nbctl --type=ls --tier=1 acl-add sw0 from-lport 1001 "ip4.dst == 10.0.0.2" allow
    ovn-nbctl --type=ls --tier=2 acl-add sw0 from-lport 1001 "ip4.proto == 6" allow
    ovn-nbctl --type=ls --tier=3 acl-add sw0 from-lport 1001 "tcp.dst == 80" drop

    Trace 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

    Terminal window
    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

    Terminal window
    ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-related

    8.2 List All Universal Flows at Priority 65532

    Terminal window
    ovn-sbctl lflow-list sw0 | grep -B 1 -A 3 "65532"

    Expected universal flows in ls_in_acl_eval at priority 65532:

    #MatchActionPurpose
    1ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)reg8[17] = 1; next;Set DROP verdict for invalid connections and blocked replies
    2ct.est && !ct.rel && ct.rpl && ct_mark.blocked == 0reg0[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 == 0reg0[17]=1; reg8[21]=ct_label.nf; reg8[16]=1; ct_commit_nat; next;Allow related (non-new) connections with NAT
    4ct.est && ct_mark.allow_established == 1reg8[21]=ct_label.nf; reg8[16]=1; next;Allow established with allow_established mark
    5IPv6 CT-omit protocolsreg8[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:

    Terminal window
    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:

    Terminal window
    match: ct.est && !ct.rel && ct.rpl && ct_mark.blocked == 0
    action (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:

    Terminal window
    match: !ct.est && ct.rel && !ct.new && ct_mark.blocked == 0
    action: 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:

    Terminal window
    match: ct.est && ct_mark.allow_established == 1
    action: 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:

    Terminal window
    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)

    Terminal window
    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:

    MatchActionPurpose
    ip && ct.est && ct_mark.blocked == 1reg8[16] = 1; next;Directly allow blocked established connections (sets ALLOW verdict)
    !ct.estreg0[0]=0; reg0[1]=1; next;Pass non-established connections (set COMMIT hint)

    The priority 1 flows ensure that:

    1. 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)
    2. Non-established connections get the COMMIT hint

    8.5 Priority 34000 Flows (Service Monitor + DNS Bypass)

    Terminal window
    ovn-nbctl set NB_Global . options:default_acl_drop=true
    ovn-sbctl lflow-list sw0 | grep -B 1 -A 3 "34000"
    ovn-nbctl remove NB_Global . options default_acl_drop --if-exists

    Expected flows at priority 34000:

    MatchActionPurpose
    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
    Terminal window
    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

    Terminal window
    # Try to add an ACL at priority 65532
    ovn-nbctl acl-add sw0 from-lport 65532 "ip4.src == 10.0.0.1" allow

    What 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:

    1. It can bypass the safety net provided by universal flows
    2. It may cause unexpected behavior for established connections
    3. 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

    Terminal window
    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

    Terminal window
    # Create port group with ports from both switches
    ovn-nbctl pg-add pg_web vm1 vm3

    9.2 Verify Port Group Members

    Terminal window
    # 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 vm3

    9.3 Add an ACL to the Port Group

    Terminal window
    ovn-nbctl acl-add pg_web from-lport 1001 "ip4.src == 10.0.0.1" allow

    9.4 Verify ACL Appears on Both Switches

    Terminal window
    # Check sw0 ACLs
    ovn-nbctl acl-list sw0
    # Expected:
    # from-lport 1001 (ip4.src == 10.0.0.1) allow
    # Check sw1 ACLs
    ovn-nbctl acl-list sw1
    # Expected:
    # from-lport 1001 (ip4.src == 10.0.0.1) allow

    The ACL is propagated to both sw0 (which has vm1) and sw1 (which has vm3).

    9.5 Verify Logical Flows on Both Switches

    Terminal window
    # Check sw0 ingress ACL eval
    ovn-sbctl lflow-list sw0 | grep -B 1 -A 5 "ls_in_acl_eval"
    # Check sw1 ingress ACL eval
    ovn-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 from vm1 (ip4.src == 10.0.0.1)
    • On sw1: The ACL matches traffic from vm3 (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

    Terminal window
    # 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
    # Verify
    ovn-nbctl show | grep -A 5 port_group
    # Expected:
    # vm3
    # Check sw0 ACLs
    ovn-nbctl acl-list sw0
    # Expected:
    # (empty — no ACLs, since vm1 is no longer in pg_web and sw0 has no other ACLs)
    # Check sw1 ACLs
    ovn-nbctl acl-list sw1
    # Expected:
    # from-lport 1001 (ip4.src == 10.0.0.1) allow

    When 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

    Terminal window
    # 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 vm3
    ovn-nbctl acl-list sw0
    # Expected:
    # from-lport 1001 (ip4.src == 10.0.0.1) allow

    Cleanup

    Terminal window
    ovn-nbctl acl-del pg_web from-lport 1001 "ip4.src == 10.0.0.1"
    ovn-nbctl pg-del pg_web

    Challenge

    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

    Terminal window
    # Ingress: allow-related for vm1
    ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-related
    # Egress: drop for vm2
    ovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" drop

    10.2 Trace the First SYN (vm1 → vm2)

    Terminal window
    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:

    1. ls_in_pre_acl (5): ct_next → conntrack
    2. ls_in_pre_stateful (7): ct_next → continues
    3. ls_in_acl_hint (8): Priority 7 — ct.new && !ct.est → ALLOW_NEW=1, DROP=1, COMMIT=1
    4. ls_in_acl_eval (9): Priority 2001 — ip4.src == 10.0.0.1reg8[16] = 1; next; (ALLOW verdict)
    5. ls_in_acl_action (11): Priority 1000 — reg8[16] == 1 → ALLOW → next;
    6. ls_in_stateful (24): Priority 100 — reg0[1]==1 && reg0[13]==0ct_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_stateful is per-switch (not per-pipeline). Since there is an allow-related ACL on sw0, has_stateful is 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=0 because the packet’s CT state was set by ct_next() in ingress PRE_STATEFUL, before ct_commit in ingress STATEFUL created the CT entry.
    1. 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)
    2. 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

    Terminal window
    # Show the BLOCK hint flow for the drop ACL in egress
    ovn-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)

    Terminal window
    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):

    1. 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)
    2. 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
    3. ls_out_acl_action (8): No verdict register set. Priority 0 default next; (or implicit drop if default_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

    Terminal window
    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

    Terminal window
    ovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" reject

    11.2 Trace a SYN

    Terminal window
    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:

    1. ls_in_pre_acl (5): ct_next
    2. ls_in_acl_hint (8): Priority 0 next; (no stateful ACL on ingress)
    3. ls_in_acl_eval (9): No matching ACL → priority 0 next;
    4. ls_in_acl_action (11): Priority 0 next;

    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.2reg0[0]=0; reg8[18]=1; next;

    • The reject action sets reg8[18] (REGBIT_ACL_REJECT) = 1
    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

    Terminal window
    # Remove reject ACL
    ovn-nbctl acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"
    # Add drop ACL
    ovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" drop
    # Trace the same 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'

    Comparison:

    Propertydropreject
    ACL_EVAL matchip4.dst == 10.0.0.2reg8[17]=1; next; (DROP verdict)ip4.dst == 10.0.0.2reg0[0]=0; reg8[18]=1; next; (REJECT verdict)
    ACL_ACTIONPriority 1000 reg8[17]==1 → implicit dropPriority 1000 reg8[18]==1 → ICMP/TCP RST
    Packet sent to sourceNoYes (ICMP unreachable or TCP RST)
    COPP meterNoYes (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

    Terminal window
    # Trace a UDP 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 == 17 && udp.dst == 53'

    For UDP packets, the reject action generates an ICMP destination unreachable (port unreachable) message instead of a TCP RST.

    Cleanup

    Terminal window
    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

    Terminal window
    ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow

    Now enable logging:

    Terminal window
    # Find the ACL UUID
    ACL_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=warning
    ovn-nbctl set acl $ACL_UUID log=true severity=warning name=acl_log_1

    12.2 Verify Logging is Enabled

    Terminal window
    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_1

    12.3 Show the log() Action in Generated Flow

    Terminal window
    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.1
    action: 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.

    Terminal window
    # Show meter groups
    ovn-sbctl meter-group-list

    The meter group ensures that:

    1. Log messages are rate-limited to prevent overwhelming the logging infrastructure
    2. Multiple ACLs sharing the same meter group share the rate limit
    3. Different ACLs can have different rate limits by using different meter groups

    12.5 Challenge: Priority 65533 Flows in Opposite Pipeline

    Terminal window
    # 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 logging
    ACL_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

    Terminal window
    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)

    Terminal window
    # Verify default
    ovn-nbctl get NB_Global . options:acl_ct_translation
    # Expected: "" (empty, meaning false)
    # Add an ACL with a TCP match
    ovn-nbctl acl-add sw0 from-lport 1001 "tcp.dst == 80" allow
    # Show the generated flow
    ovn-sbctl lflow-list sw0 | grep -B 1 -A 5 "ls_in_acl_eval"

    Expected output: The match uses standard header fields:

    match: tcp.dst == 80
    action: reg0[0]=0; next;

    13.2 Enable acl_ct_translation

    Terminal window
    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 flow
    ovn-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 == 80
    action: reg0[0]=0; next;

    13.3 Symbol Table Difference

    With acl_ct_translation=false:

    • tcp.dst → matches TCP destination port in the packet header
    • udp.src → matches UDP source port in the packet header
    • icmp4.type → matches ICMP type in the packet header

    With acl_ct_translation=true:

    • tcp.dstct_tp_dst (matches in conntrack zone)
    • udp.srcct_tp_src (matches in conntrack zone)
    • icmp4.typect_nw_proto or 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

    Terminal window
    # With acl_ct_translation=false (default)
    ovn-nbctl remove NB_Global . options acl_ct_translation --if-exists
    ovn-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 == 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.2 && ip4.frag == 1 && tcp.dst == 80'
    # Second fragment (no TCP header): ip4.frag == 1 && ip4.nxt == 6
    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'

    With acl_ct_translation=false:

    • First fragment (has TCP header): tcp.dst == 80 matches → allowed
    • Second fragment (no TCP header): tcp.dst == 80 does NOT match → may be dropped or allowed by other rules

    With acl_ct_translation=true:

    • First fragment: ct_tp_dst == 80 matches (conntrack reassembles) → allowed
    • Second fragment: ct_tp_dst == 80 matches (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

    Terminal window
    ovn-nbctl acl-del sw0 from-lport 1001 "tcp.dst == 80"
    ovn-nbctl remove NB_Global . options acl_ct_translation --if-exists

    Challenge

    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

    Terminal window
    # Clean slate
    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"
    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)

    Terminal window
    # Add drop ACL in egress
    ovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" drop
    # Trace 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:

    StagePipelineAction
    ls_in_pre_acl (5)Ingressct_next → conntrack
    ls_in_acl_hint (8)IngressPriority 0 → next;
    ls_in_acl_eval (9)IngressNo matching ACL → priority 0 next;
    ls_in_acl_action (11)IngressPriority 0 → next;
    ls_out_pre_acl (2)Egressct_next → conntrack
    ls_out_acl_hint (5)EgressPriority 0 → next;
    ls_out_acl_eval (6)EgressPriority 2001 — ip4.dst == 10.0.0.2 → reg8[17]=1; next; (DROP verdict)
    ls_out_acl_action (8)EgressPriority 1000 — reg8[17]==1 → DROP
    ResultPacket dropped

    14.2 Full Trace: SYN-ACK Reply

    Terminal window
    # 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;).

    StagePipelineAction
    ls_in_pre_acl (5)Ingressct_next → conntrack
    ls_in_acl_hint (8)IngressPriority 0 → next;
    ls_in_acl_eval (9)IngressNo matching ACL → priority 0 next; (universal flow at 65532 does NOT match — ct.est is false)
    ls_in_acl_action (11)IngressPriority 0 → next;
    ResultPacket delivered to vm1 (reply is not subject to egress drop ACL for vm2)

    14.3 Full Trace: Established Data Packet

    Terminal window
    # Established data: 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'

    Expected trace:

    StagePipelineAction
    ls_in_pre_acl (5)Ingressct_next → conntrack
    ls_in_acl_hint (8)IngressPriority 0 → next;
    ls_in_acl_eval (9)IngressNo matching ACL → priority 0 next;
    ls_out_pre_acl (2)Egressct_next → conntrack
    ls_out_acl_hint (5)EgressPriority 0 → next;
    ls_out_acl_eval (6)EgressPriority 2001 — ip4.dst == 10.0.0.2 → reg8[17]=1; next; (DROP verdict)
    ls_out_acl_action (8)EgressPriority 1000 — reg8[17]==1 → DROP
    ResultPacket dropped
    Terminal window
    # Remove old ACL
    ovn-nbctl acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"
    # Add allow-related ACL in ingress
    ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-related
    # Trace 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:

    StagePipelineAction
    ls_in_pre_acl (5)Ingressct_next → conntrack
    ls_in_pre_stateful (7)Ingressct_next → continues
    ls_in_acl_hint (8)IngressPriority 7 — ct.new && !ct.est → ALLOW_NEW=1, DROP=1, COMMIT=1
    ls_in_acl_eval (9)IngressPriority 2001 — ip4.src == 10.0.0.1 → reg8[16]=1; next; (ALLOW verdict)
    ls_in_acl_action (11)IngressPriority 1000 — reg8[16]==1 → ALLOW → next;
    ls_in_stateful (24)IngressPriority 100 — reg0[1]==1 && reg0[13]==0 → ct_commit
    ResultPacket delivered, connection committed
    Terminal window
    # Remove old ACL
    ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"
    # Add allow-related ACL with label
    ovn-nbctl acl-add sw0 from-lport 1001 "ip4.src == 10.0.0.1" allow-related
    ACL_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 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:

    StagePipelineAction
    ls_in_pre_acl (5)Ingressct_next → conntrack
    ls_in_pre_stateful (7)Ingressct_next → continues
    ls_in_acl_hint (8)IngressPriority 7 — ct.new && !ct.est → ALLOW_NEW=1, DROP=1, COMMIT=1
    ls_in_acl_eval (9)IngressPriority 2001 — ip4.src == 10.0.0.1 → reg8[16]=1; next; (ALLOW verdict)
    ls_in_acl_action (11)IngressPriority 1000 — reg8[16]==1 → ALLOW → next;
    ls_in_stateful (24)IngressPriority 100 — reg0[1]==1 && reg0[13]==1 → ct_commit with label
    ResultPacket 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

    Terminal window
    # Remove old ACL
    ovn-nbctl acl-del sw0 from-lport 1001 "ip4.src == 10.0.0.1"
    # Add reject ACL in egress
    ovn-nbctl acl-add sw0 to-lport 1001 "ip4.dst == 10.0.0.2" reject
    # Trace TCP 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:

    StagePipelineAction
    ls_in_pre_acl (5)Ingressct_next → conntrack
    ls_in_acl_hint (8)IngressPriority 0 → next;
    ls_in_acl_eval (9)IngressNo matching ACL → priority 0 next;
    ls_in_acl_action (11)IngressPriority 0 → next;
    ls_out_pre_acl (2)Egressct_next → conntrack
    ls_out_acl_hint (5)EgressPriority 0 → next;
    ls_out_acl_eval (6)EgressPriority 2001 — ip4.dst == 10.0.0.2 → reg0[0]=0; reg8[18]=1; next;
    ls_out_acl_action (8)EgressPriority 1000 — reg8[18]==1 → TCP RST with COPP meter
    ResultTCP RST sent, packet dropped

    14.7 Challenge: Trace a Fragmented Packet

    Terminal window
    # Clean up
    ovn-nbctl acl-del sw0 to-lport 1001 "ip4.dst == 10.0.0.2"
    # Add allow-related ACL
    ovn-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 == 80 matches → allowed, committed
    • Second fragment: tcp.dst == 80 does 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 == 80 matches (conntrack reassembles) → allowed, committed

    Cleanup

    Terminal window
    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

    VariantPriorityMatchWith LabelWith NFUsage
    1100reg0[1]==1 && reg0[13]==1YesNoallow-related with label
    2100reg0[1]==1 && reg0[13]==0NoNoPlain allow-related
    3110reg0[1]==1 && reg0[13]==0 && reg8[21]==1NoYesallow-related with NF
    4110reg0[1]==1 && reg0[13]==1 && reg8[21]==1YesYesallow-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

    FieldPurpose
    ct_mark.blockedSet 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_establishedSet to 1 for connections that should be allowed in established state. Used by OVN internally for service flows.
    ct_label.acl_idStores the ACL identifier (label) for observability. Used by sFlow/IPFIX to identify which ACL matched the packet.
    ct_label.obs_point_idStores 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):

    PriorityMatchreg0[7] ALLOW_NEWreg0[1] COMMITreg0[8] ALLOWreg0[9] DROPreg0[10] BLOCKMeaning
    7ct.new && !ct.est111New connection
    6!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1111Established, blocked
    5!ct.trk11Not tracked
    4!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 011Established, not blocked
    3!ct.est1Not established
    2ct.est && ct_mark.blocked == 11Established but blocked
    1ct.est && ct_mark.blocked == 01Established, set BLOCK hint

    Appendix D: Universal Flows Priority Reference

    PriorityStagePurpose
    65533ACL_EVAL (opposite pipeline)Log-related flows
    65532ACL_EVAL (both pipelines)Safety net: invalid/block/allow established
    34000ACL_EVALService monitor MAC bypass + DNS bypass
    1ACL_EVALRe-allow blocked established + pass non-established
    0ACL_EVALDefault pass-through

    Appendix E: NB_Global Options Reference

    OptionDefaultDescription
    default_acl_dropfalseWhen true, priority 0 in ACL_ACTION is implicit drop instead of next;
    acl_ct_translationfalseWhen true, ACL matches use CT fields (ct_tp_dst) instead of header fields (tcp.dst) for fragment matching
    use_ct_inv_matchtrueWhen true, universal flows match ct.inv for invalid connections

    Setting options:

    Terminal window
    ovn-nbctl set NB_Global . options:default_acl_drop=true
    ovn-nbctl set NB_Global . options:acl_ct_translation=true
    ovn-nbctl set NB_Global . options:use_ct_inv_match=true

    Removing options:

    Terminal window
    ovn-nbctl remove NB_Global . options default_acl_drop --if-exists
    ovn-nbctl remove NB_Global . options acl_ct_translation --if-exists
    ovn-nbctl remove NB_Global . options use_ct_inv_match --if-exists

    CRITICAL: Always use --if-exists when removing NB_Global options to avoid errors if the option is not set.