Logo

Search

    Overview

    OVN ACL Processing: How a Simple `ovn-nbctl acl-add` Becomes OpenFlow

    July 9, 2026
    30 min read

    If you’ve ever looked at OVN ACLs and wondered what actually happens after you run ovn-nbctl acl-add — like, the full journey from that one command to the OpenFlow rules sitting in your datapath — this is the deep dive I wish I had when I started.

    I spent a lot of time tracing this path through the OVN source code, and it’s one of those things that looks simple on the surface (“it’s just an ACL, right?”) but gets surprisingly complex once you see every pipeline stage it touches. So here it is, all laid out.

    Table of Contents

    1. High-Level Intent: The ovn-nbctl Command
    2. NB Database: ACL Record Storage
    3. Northd: Scanning ACLs and Tracking State
    4. Pipeline Stage Architecture
    5. Pre-ACL Stage: Conntrack Setup and Stateless Filters
    6. ACL Hints Stage: Pre-computing CT State-Based Categories
    7. ACL Evaluation Stage: Per-ACL Flow Generation
    8. ACL Action Stage: Verdict Enforcement and Tier Transitions
    9. Universal Flows: The High-Priority Safety Net
    10. Stateful Stage: Conntrack Commitment
    11. ACL Sampling and Logging
    12. Controller: Translating Logical Flows to OpenFlow
    13. End-to-End Packet Walk

    1. High-Level Intent: The ovn-nbctl Command

    Everything starts with a single command:

    ovn-nbctl acl-add <switch> to-lport 1001 "ip4.src == 10.0.0.1" drop

    That writes a row into the NB database’s ACL table. Each ACL record has a bunch of fields that control how it behaves:

    ColumnTypeDescription
    priorityinteger (0-32767)Higher values evaluated first
    directionfrom-lport or to-lportIngress (from-lport) or egress (to-lport)
    matchstringLogical expression (e.g., "ip4.src == 10.0.0.1")
    actionenumallow, allow-related, allow-stateless, drop, reject, pass
    tierinteger (0-3)ACL evaluation tier (default 0)
    logbooleanEnable logging
    severityenumalert, warning, notice, info, debug
    meterstringRate-limiting meter for logging
    labelinteger (0-4294967295)Observation point ID for sampling
    sample_newUUID refSample rule for new connections
    sample_estUUID refSample rule for established connections
    network_function_groupUUID refNetwork function chain group
    optionsmappersist-established, apply-after-lb, log-related

    You can also attach ACLs to port groups (via Port_Group.acls), which then apply to all logical switches containing ports that belong to that port group. Handy for writing a policy once and having it show up everywhere.


    2. NB Database: ACL Record Storage

    The NB Logical_Switch table references ACLs like this:

    "acls": {"type": {"key": {"type": "uuid",
    "refTable": "ACL",
    "refType": "strong"},
    "min": 0,
    "max": "unlimited"}}

    And the NB_Global table has a few options that affect ACL behavior globally:

    OptionDefaultEffect
    default_acl_dropfalseWhen true, switches with ACLs drop traffic by default at priority 0 in ACL_ACTION stage (instead of next;)
    acl_ct_translationfalseWhen true, ACL match expressions use conntrack fields (ct_tp_src/dst, ct_proto) instead of header fields (tcp/udp src/dst), enabling fragment matching
    use_ct_inv_matchtrueWhether to match ct.inv when dropping invalid connections

    These come up later. Just file them away for now.


    3. Northd: Scanning ACLs and Tracking State

    3.1 The en-ls-stateful Engine

    Before any flows get generated, northd scans all the ACLs on each logical switch and populates a per-switch tracking structure. This happens in ovn/northd/en-ls-stateful.c.

    The key data structure is struct ls_stateful_record:

    struct ls_stateful_record {
    struct hmap_node key_node;
    struct uuid nbs_uuid;
    size_t ls_index;
    bool has_stateful_acl; /* Set if ANY ACL uses "allow-related" */
    bool has_lb_vip; /* Set if any LB VIP is configured */
    bool has_acls; /* Set if any ACL exists at all */
    struct acl_tier max_acl_tier; /* Max tier per direction */
    struct uuidset related_acls; /* UUIDs of all ACLs on this switch */
    struct lflow_ref *lflow_ref;
    };

    The scanning logic in ls_stateful_record_set_acls_() goes through every ACL and:

    1. Sets has_acls = true if any ACL exists.
    2. Calls update_ls_max_acl_tier() to track the maximum tier value for each direction (ingress pre-LB, ingress post-LB, egress).
    3. Inserts the ACL UUID into related_acls.
    4. Sets has_stateful_acl = true only if the ACL’s action is "allow-related".

    Here’s something that tripped me up: has_stateful_acl is specifically tied to allow-related. But the variable that actually decides whether conntrack is needed is has_stateful, which is computed in build_acls():

    bool has_stateful = (ls_stateful_rec->has_stateful_acl
    || ls_stateful_rec->has_lb_vip);

    So a switch with only LB VIPs (load balancer virtual IPs) and no allow-related ACLs still forces traffic through conntrack, even though has_stateful_acl is false. The LB VIPs need conntrack for connection tracking and NAT, so the has_stateful aggregate flag makes sure the right flows are generated. Both flags are tracked independently, and only their union drives the conntrack-dependent flow generation.

    3.2 Tier Tracking

    Each ACL has a tier field (0-3). The function update_ls_max_acl_tier() tracks the maximum tier per direction:

    static void
    update_ls_max_acl_tier(struct ls_stateful_record *ls_stateful_rec,
    const struct nbrec_acl *acl)
    {
    if (!acl->tier) return;
    uint64_t *tier;
    if (!strcmp(acl->direction, "from-lport")) {
    if (smap_get_bool(&acl->options, "apply-after-lb", false)) {
    tier = &ls_stateful_rec->max_acl_tier.ingress_post_lb;
    } else {
    tier = &ls_stateful_rec->max_acl_tier.ingress_pre_lb;
    }
    } else {
    tier = &ls_stateful_rec->max_acl_tier.egress;
    }
    *tier = MAX(*tier, acl->tier);
    }

    Tiers are how OVN separates ACL evaluation into independent groups. An ACL at tier 0 can allow or drop a packet, and if it allows, the packet moves to tier 1 for the next set of ACLs. More on this later.


    4. Pipeline Stage Architecture

    OVN logical switches have both ingress and egress pipelines, each split into multiple stages. ACL processing spans several stages in each direction. Here’s the full ACL-relevant pipeline:

    Ingress Pipeline (traffic arriving at the switch)

    Stage 5: PRE_ACL -- Conntrack defrag, skip ports, stateless filters
    Stage 6: PRE_LB -- Load balancing setup
    Stage 7: PRE_STATEFUL -- ct_next, ct_lb_mark
    Stage 8: ACL_HINT -- Pre-compute hint bits from CT state
    Stage 9: ACL_EVAL -- Evaluate per-ACL rules, set verdict registers
    Stage 10: ACL_SAMPLE -- Traffic sampling for observation
    Stage 11: ACL_ACTION -- Execute allow/drop/reject verdict
    Stage 12: QOS -- Quality of service rules
    ...
    Stage 15: LB -- Load balancing
    ...
    Stage 20: ACL_AFTER_LB_EVAL -- Post-LB ACL evaluation (apply-after-lb)
    Stage 21: ACL_AFTER_LB_SAMPLE -- Post-LB sampling
    Stage 22: ACL_AFTER_LB_ACTION -- Post-LB verdict resolution
    ...
    Stage 24: STATEFUL -- Conntrack commitment (ct_commit)

    Egress Pipeline (traffic leaving the switch)

    Stage 2: PRE_ACL -- Conntrack defrag, skip ports, stateless filters
    Stage 3: PRE_LB -- Load balancing setup
    Stage 4: PRE_STATEFUL -- ct_next, ct_lb_mark
    Stage 5: ACL_HINT -- Pre-compute hint bits from CT state
    Stage 6: ACL_EVAL -- Evaluate per-ACL rules, set verdict registers
    Stage 7: ACL_SAMPLE -- Traffic sampling for observation
    Stage 8: ACL_ACTION -- Execute allow/drop/reject verdict
    ...
    Stage 12: STATEFUL -- Conntrack commitment (ct_commit)

    The ingress pipeline has more stages because it includes QOS (stage 12), LB (stage 15), and the after-LB ACL stages (20-22). The egress pipeline is simpler: after ACL_ACTION (stage 8), the next ACL-relevant stage is STATEFUL (stage 12). No separate QOS stage, no after-LB stages.

    The pipeline diagram for both directions:

    INGRESS EGRESS
    +-------------+ +-------------+
    | PRE_ACL | | PRE_ACL |
    | (stage 5) | | (stage 2) |
    +------+------+ +------+------+
    | |
    v v
    +------+------+ +------+------+
    | PRE_LB | | PRE_LB |
    | (stage 6) | | (stage 3) |
    +------+------+ +------+------+
    | |
    v v
    +------+------+ +------+------+
    | PRE_STATEFUL| | PRE_STATEFUL|
    | (stage 7) | | (stage 4) |
    +------+------+ +------+------+
    | |
    v v
    +------+------+ +------+------+
    | ACL_HINT | | ACL_HINT |
    | (stage 8) | | (stage 5) |
    +------+------+ +------+------+
    | |
    v v
    +------+------+ +------+------+
    | ACL_EVAL | | ACL_EVAL |
    | (stage 9) | | (stage 6) |
    +------+------+ +------+------+
    | |
    v v
    +------+------+ +------+------+
    | ACL_SAMPLE | | ACL_SAMPLE |
    | (stage 10) | | (stage 7) |
    +------+------+ +------+------+
    | |
    v v
    +------+------+ +------+------+
    | ACL_ACTION | | ACL_ACTION |
    | (stage 11) | | (stage 8) |
    +------+------+ +------+------+
    | |
    v v
    +------+------+ |
    | QOS (12) | |
    +------+------+
    |
    v
    +------+------+ +------+------+
    | LB (15) | | STATEFUL |
    +------+------+ | (stage 12) |
    | +------+------+
    v ^
    +------+------+ |
    | ACL_AFTER_LB| |
    | _EVAL (20) | |
    +------+------+ |
    | |
    v |
    +------+------+ |
    | ACL_AFTER_LB| |
    | _SAMPLE(21) | |
    +------+------+ |
    | |
    v |
    +------+------+ |
    | ACL_AFTER_LB| |
    | _ACTION(22) |---conntrack commit-------->+
    +------+------+
    |
    v
    +------+------+
    | STATEFUL |
    | (stage 24) |
    +------+------+

    Register Layout for ACL Processing

    A bunch of registers carry state between pipeline stages:

    reg0 (R0) - ACL Hint Bits and CT Control:
    reg0[0] REGBIT_CONNTRACK_DEFRAG
    reg0[1] REGBIT_CONNTRACK_COMMIT
    reg0[2] REGBIT_CONNTRACK_NAT
    reg0[7] REGBIT_ACL_HINT_ALLOW_NEW
    reg0[8] REGBIT_ACL_HINT_ALLOW
    reg0[9] REGBIT_ACL_HINT_DROP
    reg0[10] REGBIT_ACL_HINT_BLOCK
    reg0[13] REGBIT_ACL_LABEL
    reg0[16] REGBIT_ACL_STATELESS
    reg0[17] REGBIT_ACL_HINT_ALLOW_REL
    reg0[20] REGBIT_ACL_PERSIST_ID
    reg0[21] REGBIT_ACL_HINT_ALLOW_PERSISTED
    reg2 (R2) - Persistent ACL ID:
    reg2[16..31] REG_ACL_ID
    reg8 (R8) - ACL Verdict and Tier:
    reg8[0..7] REG_OBS_COLLECTOR_ID_NEW
    reg8[8..15] REG_OBS_COLLECTOR_ID_EST
    reg8[16] REGBIT_ACL_VERDICT_ALLOW
    reg8[17] REGBIT_ACL_VERDICT_DROP
    reg8[18] REGBIT_ACL_VERDICT_REJECT
    reg8[19..20] REGBIT_ACL_OBS_STAGE
    reg8[30..31] REG_ACL_TIER
    reg3 (R3) - Observation Point ID (new):
    reg3 REG_OBS_POINT_ID_NEW
    reg9 (R9) - Observation Point ID (established):
    reg9 REG_OBS_POINT_ID_EST

    Don’t try to memorize these. Just know they exist — each stage reads and writes bits in these registers to pass information forward.


    5. Pre-ACL Stage: Conntrack Setup and Stateless Filters

    The Pre-ACL stage is basically the warm-up lap. It prepares traffic for the real ACL processing that comes later. If has_stateful is true (meaning has_stateful_acl || has_lb_vip), this stage sends IP traffic to conntrack for defragmentation and connection tracking.

    5.1 Default Flows (build_pre_acls)

    At priority 0, all traffic is allowed through. Priority 110 bypasses conntrack for service monitor MAC:

    INGRESS PRE_ACL (stage 5):
    Priority 0: match="1" action="next;"
    Priority 110: match="eth.dst == $svc_monitor_mac" action="next;"
    EGRESS PRE_ACL (stage 2):
    Priority 0: match="1" action="next;"
    Priority 110: match="eth.src == $svc_monitor_mac" action="next;"

    5.2 Stateful Pre-ACL Flows (build_ls_stateful_rec_pre_acls)

    When has_stateful_acl is true (or has_lb_vip), extra flows get generated. These basically decide what goes to conntrack and what doesn’t.

    At priority 110, various traffic types are excluded from conntrack:

    INGRESS PRE_ACL (stage 5):
    Priority 110: match="ip && inport == <router_port>" action="next;"
    (skips router ports unless enable_router_port_acl is set)
    Priority 110: match="ip && inport == <switch_port>" action="next;"
    (skips local switch ports)
    Priority 110: match="ip && inport == <localnet_port>" action="next;"
    (skips localnet ports)
    Priority 110: match="nd || nd_rs || nd_ra || mldv1 || mldv2 || (udp && udp.src == 546 && udp.dst == 547)"
    action="next;"
    (ND, ICMPv6, MLD bypass conntrack)
    Priority 110: match="<tunnel ICMP too-big>" action="next;"
    (tunnel ICMP packet-too-big bypass)
    Priority 110: match="eth.mcast" action="next;"
    (multicast bypass -- only when acl_ct_translation is disabled)
    EGRESS PRE_ACL (stage 2):
    (mirror of ingress: same skip flows for router, switch, localnet ports,
    ND/ICMP/MLD, and multicast)

    At priority 100, the remaining IP traffic gets sent to conntrack:

    INGRESS PRE_ACL (stage 5):
    Priority 100: match="ip" action="REGBIT_CONNTRACK_DEFRAG = 1; next;"
    EGRESS PRE_ACL (stage 2):
    Priority 100: match="ip" action="REGBIT_CONNTRACK_DEFRAG = 1; next;"

    5.3 Stateless Filters (build_stateless_filters)

    ACLs with action "allow-stateless" are handled entirely here in the Pre-ACL stage. They set REGBIT_ACL_STATELESS = 1 and skip conntrack completely:

    INGRESS PRE_ACL (stage 5):
    Priority <acl_priority + 1000>: match="(<acl_match>)"
    action="REGBIT_ACL_STATELESS = 1; next;"
    EGRESS PRE_ACL (stage 2):
    Priority <acl_priority + 1000>: match="(<acl_match>)"
    action="REGBIT_ACL_STATELESS = 1; next;"

    The priority offset of 1000 (OVN_ACL_PRI_OFFSET) means stateless filter flows live in the range 1000-33767 — above the infrastructure flows (0-110) but below the universal flows at UINT16_MAX - 3 (65532).


    6. ACL Hints Stage: Pre-computing CT State-Based Categories

    This is one of my favorite parts of the ACL pipeline. The ACL_HINT stage generates flows that pre-compute which categories of ACL action a packet might match, based on connection tracking state bits. These hints reduce the number of OpenFlow rules needed in ACL_EVAL by narrowing down what each packet could hit.

    The hint stage only generates flows when has_stateful_acl or has_lb_vip is true. If neither is present, the stage just passes traffic through.

    6.1 Hint Flow Summary

    These flows are installed identically in both S_SWITCH_IN_ACL_HINT (stage 8) and S_SWITCH_OUT_ACL_HINT (stage 5):

    PriorityMatchHints SetMeaning
    7ct.new && !ct.estALLOW_NEW=1, DROP=1, COMMIT=1New connections can hit allow or drop ACLs. COMMIT is set because new traffic must be tracked.
    6!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1ALLOW_NEW=1, DROP=1, COMMIT=1Established request-direction traffic that was previously blocked. May hit re-allow or drop ACLs (e.g., after an ACL policy is re-added).
    5!ct.trkALLOW=1, DROP=1Untracked traffic (not yet sent through conntrack). Can be allowed or dropped.
    4!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 0ALLOW=1, BLOCK=1Established request-direction traffic that was previously allowed. Can hit allow ACLs (stays allowed) or drop ACLs (must be blocked).
    3!ct.estDROP=1Not-established traffic. Catch-all for all non-established traffic where only drop ACLs are relevant.
    2ct.est && ct_mark.blocked == 1DROP=1Established connections that are already blocked. Only drop ACLs apply.
    1ct.est && ct_mark.blocked == 0BLOCK=1Established unblocked connections. If they hit a drop ACL, they need to be blocked.

    6.2 Why Priority 3 Covers All Non-Established Traffic

    The priority 3 flow with match !ct.est is worth a closer look. Its match is satisfied by any packet that is not in the ct.est (established) state. That includes:

    • New connections (ct.new): Already covered by priority 7 with more specific hints (ALLOW_NEW + DROP + COMMIT).
    • Untracked packets (!ct.trk): Already covered by priority 5 with ALLOW + DROP hints.

    So priority 3 acts as a catch-all fallback for traffic that somehow didn’t match the more specific flows at priorities 7 or 5 but is still not established. Setting only the DROP hint here means such traffic defaults to the drop-hint path in ACL_EVAL. For non-established traffic, only drop ACLs are relevant because there’s no existing connection state to match allow ACLs against.

    6.3 How Hints Reduce OpenFlow Rules

    Without hints, every per-ACL flow in ACL_EVAL would need to match against ct.new, ct.est, ct.rpl, ct_mark.blocked, etc. With hints, a per-ACL flow only needs to match on the hint bit(s) relevant to that ACL type. For example, a drop ACL only needs to match REGBIT_ACL_HINT_DROP == 1 (or REGBIT_ACL_HINT_BLOCK == 1), avoiding the need to spell out the full CT state expression in every ACL flow.

    It’s basically an optimization layer that saves you from generating hundreds of redundant flows.


    7. ACL Evaluation Stage: Per-ACL Flow Generation

    This is where the actual work happens. The ACL_EVAL stage (ls_in_acl_eval / ls_out_acl_eval) evaluates each ACL rule. The central function is consider_acl(), which generates logical flows for each ACL.

    7.1 Stage Selection

    Where the flow lands depends on the ACL’s direction and options:

    if (ingress && apply-after-lb) {
    stage = S_SWITCH_IN_ACL_AFTER_LB_EVAL; /* stage 20 */
    } else if (ingress) {
    stage = S_SWITCH_IN_ACL_EVAL; /* stage 9 */
    } else {
    stage = S_SWITCH_OUT_ACL_EVAL; /* stage 6 */
    }

    7.2 Priority Calculation

    The NB ACL priority (0-32767) gets offset by 1000:

    logical_flow_priority = acl->priority + OVN_ACL_PRI_OFFSET (1000)

    So a priority 1001 ACL becomes logical flow priority 2001.

    7.3 Tier Matching

    When multiple tiers are in use, each per-ACL flow is prefixed with a tier match:

    reg8[30..31] == <tier> && (<acl_match>)

    This ensures only ACLs belonging to the current evaluation tier get considered.

    For stateful switches (has_stateful is true), consider_acl() generates two flows for each allow or allow-related ACL:

    Flow 1 — New connections (ALLOW_NEW hint):

    Priority <acl_pri + 1000>:
    match: reg8[30..31] == <tier> && reg0[7] == 1 && (<acl_match>)
    action: [log] [persist_id] [sample_label] [nfg] next;

    The reg0[7] == 1 match restricts this to new connections (or established-but-blocked connections that may be re-allowed). For persist-established ACLs, the action includes reg2[16..31] = <acl_id>; reg0[20] = 1; to store the persistent ACL ID.

    Flow 2 — Established connections (ALLOW hint):

    Priority <acl_pri + 1000>:
    match: reg8[30..31] == <tier> && reg0[8] == 1 && (<acl_match>)
    action: [log] [conditional_commit] [sample_label] [nfg] next;

    Here’s a critical detail about the conditional commit: REGBIT_CONNTRACK_COMMIT (reg0[1]) is only set to 1 when the ACL has a label, sample_est, or network_function_group:

    if (acl->label || acl->sample_est || acl->network_function_group) {
    ds_put_cstr(actions, REGBIT_CONNTRACK_COMMIT" = 1; ");
    }

    For a plain allow-related ACL without label, sample_est, or network_function_group, no commit happens for established flows. They simply pass through without being re-committed to conntrack. The initial commitment already happened when the connection was new (Flow 1). The COMMIT bit is only needed for established flows when there’s extra metadata that needs to be stored in the conntrack entry.

    7.5 Drop / Reject ACLs

    For drop or reject ACLs, two flows are generated:

    Flow 1 — New/untracked traffic (DROP hint):

    Priority <acl_pri + 1000>:
    match: reg8[30..31] == <tier> && reg0[9] == 1 && (<acl_match>)
    action: [log] [sample] next;

    The action sets the verdict register and proceeds to ACL_ACTION stage for enforcement.

    Flow 2 — Established, previously allowed traffic (BLOCK hint):

    Priority <acl_pri + 1000>:
    match: reg8[30..31] == <tier> && reg0[10] == 1 && (<acl_match>)
    action: [log] [sample] ct_commit { ct_mark.blocked = 1; ct_label.obs_point_id = <obs_pid>; }; next;

    This flow re-commits the connection with ct_mark.blocked = 1 to signal that this previously-allowed connection is now blocked. Subsequent packets on this connection will match the priority 1 universal flow (ip && ct.est && ct_mark.blocked == 1) and be re-allowed, preventing an asymmetric drop where the SYN was allowed but return traffic was blocked.

    7.6 Stateless / Pass ACLs

    For pass, allow-stateless, or switches without stateful ACLs, only a single flow is generated:

    Priority <acl_pri + 1000>:
    match: (<acl_match>)
    action: [sample_label] next;

    No hint matching, no CT interaction. Simple and clean.


    8. ACL Action Stage: Verdict Enforcement and Tier Transitions

    The ACL_ACTION stage is where the verdict set in ACL_EVAL gets enforced, and where tier transitions happen.

    8.1 Default Flows

    At the start of each ACTION stage, the verdict registers get cleared:

    action: reg8[16] = 0; reg8[17] = 0; reg8[18] = 0;

    This is prepended to all flows in this stage.

    8.2 Verdict Enforcement

    PriorityMatchAction
    1000reg8[16] == 1 (ALLOW)Clear verdict bits, next;
    1000reg8[17] == 1 (DROP)Clear verdict bits, implicit drop
    1000reg8[18] == 1 (REJECT)Clear verdict bits, reject action (ICMP unreachable or TCP RST) with COPP meter
    500reg8[30..31] == <tier>reg8[30..31] = <tier+1>; next(pipeline, eval_stage); (tier advancement)
    01default_acl_action (either next; or implicit drop, depending on default_acl_drop option)

    8.3 Tier Transitions

    When no verdict is reached at the current tier, the packet loops back to the ACL_EVAL stage with an incremented tier value:

    Priority 500:
    match: reg8[30..31] == <current_tier>
    action: reg8[30..31] = <current_tier + 1>; next(pipeline=<ingress|egress>, table=<acl_eval_table>);

    This loop continues until either a verdict is reached or the tier counter exceeds the maximum tier, at which point the priority 0 default flow applies.

    8.4 The Default Action and default_acl_drop

    The NB_Global.options["default_acl_drop"] setting controls what happens at priority 0:

    • When default_acl_drop = false (default): the priority 0 action is next; — traffic without an ACL match proceeds to the next pipeline stage.
    • When default_acl_drop = true: the priority 0 action is an implicit drop — traffic without any ACL match is dropped.

    9. Universal Flows: The High-Priority Safety Net

    At priority UINT16_MAX - 3 (65532), build_acls() generates “universal” flows that apply to all traffic regardless of per-ACL matches. These handle edge cases that must take precedence over user-defined ACLs.

    All universal flows at priority 65532 are generated on both ingress and egress ACL_EVAL stages when has_stateful is true.

    9.1 Drop Invalid and Blocked Reply Traffic

    INGRESS ACL_EVAL (stage 9) and EGRESS ACL_EVAL (stage 6):
    Priority 65532:
    match: ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)
    action: reg8[17] = 1; next;

    Invalid conntrack entries (ct.inv) and reply-direction traffic on blocked connections are dropped. The ct.inv match is conditional on use_ct_inv_match and lb_with_stateless_mode.

    9.2 Allow Established Reply Traffic

    INGRESS ACL_EVAL (stage 9):
    Priority 65532:
    match: ct.est && !ct.rel && ct.rpl && ct_mark.blocked == 0
    action: reg0[9] = 0; reg0[10] = 0; reg0[17] = 1;
    reg8[21] = ct_label.nf; reg8[16] = 1; next;
    EGRESS ACL_EVAL (stage 6):
    Priority 65532:
    match: ct.est && !ct.rel && ct.rpl && ct_mark.blocked == 0
    action: reg8[21] = ct_label.nf; reg8[16] = 1; next;

    Reply-direction traffic on established unblocked connections is always allowed. The ingress version additionally clears the DROP and BLOCK hint bits and sets the ALLOW_REL hint, while also propagating the network function enabled bit from ct_label.nf.

    INGRESS ACL_EVAL (stage 9) and EGRESS ACL_EVAL (stage 6):
    Priority 65532:
    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;

    Related traffic (ICMP errors, FTP data connections, etc.) that is not new and not established is allowed with NAT commitment.

    9.4 Allow Persistent Established Traffic

    INGRESS ACL_EVAL (stage 9) and EGRESS ACL_EVAL (stage 6):
    Priority 65532:
    match: ct.est && ct_mark.allow_established == 1
    action: reg8[21] = ct_label.nf; reg8[16] = 1; next;

    Traffic matching the persist-established flag in ct_mark is allowed without further ACL evaluation.

    9.5 Allow IPv6 CT-Omit Protocols

    INGRESS ACL_EVAL (stage 9), EGRESS ACL_EVAL (stage 6),
    and INGRESS ACL_AFTER_LB_EVAL (stage 20):
    Priority 65532:
    match: <IPV6_CT_OMIT_MATCH>
    action: reg8[16] = 1; next;

    Certain IPv6 protocols always bypass conntrack. No questions asked.

    9.6 After-LB Universal Flows

    The after-LB stages have their own universal flows too:

    INGRESS ACL_AFTER_LB_EVAL (stage 20):
    Priority 65532:
    match: reg0[17] == 1 (ALLOW_REL hint from ingress ACL_EVAL)
    action: reg8[16] = 1; next;
    Priority 65532:
    match: reg0[21] == 1 (ALLOW_PERSISTED hint from ingress ACL_EVAL)
    action: reg8[16] = 1; next;

    9.7 Service Monitor and DNS Bypass

    At priority 34000 (above the universal 65532 flows):

    INGRESS ACL_EVAL (stage 9):
    Priority 34000:
    match: eth.dst == $svc_monitor_mac
    action: reg8[16] = 1; next;
    EGRESS ACL_EVAL (stage 6):
    Priority 34000:
    match: eth.src == $svc_monitor_mac
    action: reg8[16] = 1; next;
    EGRESS ACL_EVAL (stage 6) -- when DNS records exist:
    Priority 34000:
    match: flags.from_ctrl && udp.src == 53
    action: reg8[16] = 1; ct_commit; next; (if has_stateful)
    action: reg8[16] = 1; next; (if !has_stateful)

    9.8 Default Flows in ACL_EVAL

    INGRESS and EGRESS ACL_EVAL (stages 9 and 6):
    Priority 0: match="1" action="next;"
    INGRESS ACL_AFTER_LB_EVAL (stage 20):
    Priority 0: match="1" action="next;"

    For ACLs with log, label, and log-related option enabled, slightly higher-priority flows get installed at UINT16_MAX - 2 (65533) to intercept reply and related traffic for logging:

    OPPOSITE pipeline ACL_EVAL (ingress ACLs get egress eval flows, and vice versa):
    Priority 65533:
    match: ct.est && !ct.rel && ct.rpl && ct_mark.blocked == 0
    && ct_label.label == <acl_label>
    action: [log] reg8[21] = ct_label.nf; reg8[16] = 1; next;
    Priority 65533:
    match: !ct.est && ct.rel && !ct.new && ct_mark.blocked == 0
    && ct_label.label == <acl_label>
    action: [log] reg8[21] = ct_label.nf; reg8[16] = 1; next;

    Note that these flows are installed on the opposite pipeline from where the ACL itself is defined. An ingress ACL’s log-related flows are installed in the egress ACL_EVAL stage (and vice versa), because reply/related traffic flows in the opposite direction.


    10. Stateful Stage: Conntrack Commitment

    The STATEFUL stage (ls_in_stateful / ls_out_stateful) does the actual conntrack commitment when REGBIT_CONNTRACK_COMMIT (reg0[1]) is set. This stage generates flows on both ingress and egress pipelines.

    10.1 Default Flow

    INGRESS STATEFUL (stage 24) and EGRESS STATEFUL (stage 12):
    Priority 0: match="1" action="next;"

    10.2 Four Variants of ct_commit

    The stage generates four variants of ct_commit flows, each on both ingress (stage 24) and egress (stage 12):

    Variant 1 — With label, no NF (Priority 100):

    INGRESS STATEFUL (stage 24) and EGRESS STATEFUL (stage 12):
    Priority 100:
    match: reg0[1] == 1 && reg0[13] == 1
    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;

    Used when the ACL has a label (observation point ID) or sample_est, but no network function group.

    Variant 2 — Without label, no NF (Priority 100):

    INGRESS STATEFUL (stage 24) and EGRESS STATEFUL (stage 12):
    Priority 100:
    match: reg0[1] == 1 && reg0[13] == 0
    action: 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;

    Used for plain allow-related ACLs without label or NF group.

    Variant 3 — Without label, with NF (Priority 110):

    INGRESS STATEFUL (stage 24) and EGRESS STATEFUL (stage 12):
    Priority 110:
    match: reg0[1] == 1 && reg0[13] == 0 && reg8[21] == 1
    action: ct_commit {
    ct_mark.blocked = 0;
    ct_mark.allow_established = reg0[20];
    ct_label.acl_id = reg2[16..31];
    ct_label.nf = 1;
    ct_label.nf_id = reg0[22..29];
    }; next;

    Variant 4 — With label AND NF (Priority 110):

    INGRESS STATEFUL (stage 24) and EGRESS STATEFUL (stage 12):
    Priority 110:
    match: reg0[1] == 1 && reg0[13] == 1 && reg8[21] == 1
    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 = 1;
    ct_label.nf_id = reg0[22..29];
    }; next;

    10.3 What Each ct_commit Field Stores

    FieldSourcePurpose
    ct_mark.blockedAlways 0New connections start unblocked
    ct_mark.allow_establishedreg0[20] (REGBIT_ACL_PERSIST_ID)Flag for persist-established ACLs
    ct_mark.obs_stagereg8[19..20]Observation stage for sampling
    ct_mark.obs_collector_idreg8[8..15]IPFIX collector ID
    ct_label.obs_point_idreg9Observation point ID for sampling
    ct_label.acl_idreg2[16..31]Persistent ACL ID for flush
    ct_label.nf0 or 1Network function chain flag
    ct_label.nf_id0 or reg0[22..29]Network function group ID

    10.4 When ct_commit Actually Happens

    For a packet to reach the STATEFUL stage and trigger ct_commit, it must have REGBIT_CONNTRACK_COMMIT (reg0[1]) set to 1. This bit is set in these cases:

    1. ACL_HINT stage, priority 7: New connections (ct.new && !ct.est) — always set to ensure new traffic is tracked.
    2. ACL_HINT stage, priority 6: Established-but-blocked request-direction traffic — set to allow re-commitment.
    3. ACL_EVAL stage, per-ACL Flow 2: Only when the ACL has a label, sample_est, or network_function_group. For a plain allow-related ACL without these, established flows pass through ACL_EVAL without setting the COMMIT bit and reach STATEFUL’s priority 0 default next; flow, skipping ct_commit.

    This last point is important: plain allow-related ACLs don’t re-commit established flows. The connection was already committed during the SYN. No need to do it again for every data packet.


    11. ACL Sampling and Logging

    11.1 ACL Logging (build_acl_log)

    When an ACL has log = true, the action includes a log() call:

    log(name="<acl_name>", severity=<severity>, meter="<meter>",
    direction=<direction>);

    The severity defaults to "info" if not specified. The direction is always from-lport for ingress or to-lport for egress.

    When a fair meter is used, alloc_acl_log_unique_meter_name() generates a unique name by appending a double underscore and the ACL’s UUID:

    return xasprintf("%s__" UUID_FMT,
    acl->meter, UUID_ARGS(&acl->header_.uuid));

    This produces names like <meter>__<uuid>, making sure each ACL using the same fair meter gets its own rate-limited counter.

    11.2 ACL Sampling (build_acl_sample_*)

    Traffic sampling for IPFIX/OVSFIX observation uses a multi-stage approach:

    1. ACL_EVAL stage: build_acl_sample_label_action() stores observation point IDs and collector IDs in registers (reg3, reg9, reg8[0..15], reg8[19..20]).
    2. ACL_SAMPLE stage: Sampling flows at priority 1100 (for new) and 1200 (for established) match on the register values and emit sample() actions.

    The sampling domain IDs are mapped by en-sampling-app:

    SAMPLING_APP_DROP_DEBUG -> "drop"
    SAMPLING_APP_ACL_NEW -> "acl-new"
    SAMPLING_APP_ACL_EST -> "acl-est"

    12. Controller: Translating Logical Flows to OpenFlow

    12.1 The Translation Process

    ovn-controller reads logical flows from the SB database and translates them to OpenFlow rules installed in the OVS datapath. The translation happens in ovn/controller/lflow.c.

    For each logical flow, the controller:

    1. Parses the match expression using the appropriate symbol table.
    2. Converts the OVN match to an OVS OpenFlow match.
    3. Converts the OVN action to OpenFlow instructions (apply-actions, ct actions, etc.).
    4. Installs the OpenFlow rule via ofctrl_add_flow().

    12.2 ACL CT Translation Symbol Table

    When the acl_ct_translation global option is enabled, ACL logical flows are tagged with "acl_ct_translation"="true" in the SB database. The controller detects this tag and uses an alternative symbol table (acl_ct_symtab) that maps L4 port fields to their conntrack equivalents:

    Standard SymbolCT-Translated Match
    tcpct.trk && !ct.inv && ct_proto == 6
    tcp.srcMFF_CT_TP_SRC (via tcp predicate)
    tcp.dstMFF_CT_TP_DST (via tcp predicate)
    udpct.trk && !ct.inv && ct_proto == 17
    udp.srcMFF_CT_TP_SRC (via udp predicate)
    udp.dstMFF_CT_TP_DST (via udp predicate)
    sctpct.trk && !ct.inv && ct_proto == 132
    sctp.srcMFF_CT_TP_SRC (via sctp predicate)
    sctp.dstMFF_CT_TP_DST (via sctp predicate)

    The key difference: the standard table uses header fields (MFF_TCP_SRC, MFF_TCP_DST) which are only available on the first fragment, while the CT translation table uses conntrack fields (MFF_CT_TP_SRC, MFF_CT_TP_DST) which are available for all fragments via the conntrack entry. This is what makes fragment matching possible.

    The ct_label.acl_id subfield is defined as:

    expr_symtab_add_subfield_scoped(symtab, "ct_label.acl_id", NULL,
    "ct_label[80..95]", WR_CT_COMMIT);

    This maps ct_label.acl_id to bits 80-95 of the 128-bit conntrack label.

    12.3 ACL ID Conntrack Flush (acl-ids)

    When an ACL with persist-established gets deleted from the NB database:

    1. northd (en-acl-ids) deletes the corresponding SB_ACL_ID record.
    2. ovn-controller (acl-ids.c) detects the deletion and enters the SB_DELETED state.
    3. The controller sends a conntrack flush request to OVS matching all entries with the deleted ACL’s ID in ct_label[80..95].

    The flush mask is constructed as:

    ovs_u128 mask = {
    .u64.hi = 0xffff0000,
    };

    To understand why this corresponds to ct_label[80..95]: the 128-bit label is split into u64.hi (bits 64-127) and u64.lo (bits 0-63). The value 0xffff0000 is a 32-bit mask at bit positions 16-31 within the u64.hi word. Since u64.hi represents bits 64-127, positions 16-31 within it map to bits 64+16=80 through 64+31=95 of the full 128-bit label. Consistent with the field definition ct_label[80..95].

    The ACL ID value is placed in the same position:

    ovs_u128 ct_id = {
    .u64.hi = acl_id->id << 16,
    };

    The ID (16-bit value) is shifted left by 16 bits within u64.hi, placing it in bits 16-31 of the hi word (bits 80-95 of the label), matching the flush mask.

    The flush operation uses ofp_ct_match_encode() to send a datapath-level conntrack flush request. If the flush fails, it retries up to 3 times (MAX_FLUSHES). After 3 failures, the entry is abandoned with a warning log.

    12.4 ACL Log Packet-In

    When a packet triggers an ACL log action, OVN generates a packet-in to ovn-controller with the ACTION_OPCODE_LOG opcode. The handle_acl_log() function in ovn/lib/acl-log.c formats and logs the verdict, severity, direction, and flow headers via VLOG_INFO.


    13. End-to-End Packet Walk

    This is the part where everything comes together. I’ll trace individual packets through the entire pipeline for a few common scenarios.

    13.1 Scenario: SYN Packet with Drop ACL

    Setup: Logical switch with ACL priority 1001, to-lport, ip4.src == 10.0.0.1, drop.

    A TCP SYN packet from 10.0.0.1 arrives at the switch.

    Ingress pipeline:

    1. PRE_ACL (stage 5): Match ip at priority 100. Action: REGBIT_CONNTRACK_DEFRAG = 1; next;. Packet is sent to conntrack for defrag and tracking.

    2. PRE_LB (stage 6): Pass through (no LB flows).

    3. PRE_STATEFUL (stage 7): Match triggers ct_next(); to execute the conntrack action. After this, the packet has CT state: ct.new=1, ct.est=0, ct.rpl=0, ct.trk=1, ct_mark.blocked=0.

    4. ACL_HINT (stage 8): Priority 7 matches ct.new && !ct.est. Sets ALLOW_NEW=1, DROP=1, COMMIT=1. Sets REGBIT_CONNTRACK_COMMIT = 1.

    5. ACL_EVAL (stage 9):

      • Priority 65532 flows: The drop ACL’s match ip4.src == 10.0.0.1 does not match the universal flow patterns (which check ct.inv, ct.est, etc.). No match.
      • Priority 1: ip && !ct.est matches (the packet is not established). Action: next; (bare pass-through).
      • Priority 2001 (the ACL): reg8[30..31] == 0 && reg0[9] == 1 && (ip4.src == 10.0.0.1). All conditions match. Action: reg8[17] = 1; next;. Verdict: DROP.
    6. ACL_SAMPLE (stage 10): Pass through (no sampling configured).

    7. ACL_ACTION (stage 11): Priority 1000 matches reg8[17] == 1 (DROP). Action: clear verdict bits, implicit drop.

    The SYN is dropped.

    Now consider the return SYN-ACK:

    The SYN-ACK arrives from the destination (not 10.0.0.1, so it does not match the drop ACL) in the egress pipeline.

    Egress pipeline:

    1. PRE_ACL (stage 2): Match ip at priority 100. REGBIT_CONNTRACK_DEFRAG = 1; next;.

    2. PRE_STATEFUL (stage 4): ct_next();. CT state for the SYN-ACK: ct.new=0, ct.est=1, ct.rpl=1, ct.trk=1, ct_mark.blocked=0.

    3. ACL_HINT (stage 5): Priority 4 matches !ct.new && ct.est && !ct.rpl && ct_mark.blocked == 0. But the SYN-ACK is reply direction (ct.rpl=1), so this does NOT match. Priority 1 matches ct.est && ct_mark.blocked == 0. Sets BLOCK=1.

    4. ACL_EVAL (stage 6):

      • Priority 65532: ct.est && !ct.rel && ct.rpl && ct_mark.blocked == 0. This matches the SYN-ACK. Action: reg8[21] = ct_label.nf; reg8[16] = 1; next;. Verdict: ALLOW.
      • The SYN-ACK is allowed by the universal reply flow without ever reaching the egress drop ACL.
    5. ACL_ACTION (stage 8): Priority 1000 matches reg8[16] == 1 (ALLOW). Action: next;.

    The SYN-ACK is allowed. This is important: even though the SYN was dropped by a stateful ACL, the reply traffic is always permitted by the universal flows. The connection tracking state makes sure that return traffic for any connection (including dropped ones) is allowed, preventing asymmetric routing issues.

    Setup: Logical switch with ACL priority 1001, from-lport, ip4.dst == 10.0.0.2, allow-related.

    A TCP SYN packet to 10.0.0.2 originates from a VM on this switch.

    Ingress pipeline:

    1. PRE_ACL (stage 5): REGBIT_CONNTRACK_DEFRAG = 1; next;.

    2. PRE_STATEFUL (stage 7): ct_next();. CT state: ct.new=1, ct.est=0, ct.rpl=0, ct.trk=1.

    3. ACL_HINT (stage 8): Priority 7 matches ct.new && !ct.est. Sets ALLOW_NEW=1, DROP=1, COMMIT=1.

    4. ACL_EVAL (stage 9):

      • Priority 2001 (the ACL): reg8[30..31] == 0 && reg0[7] == 1 && (ip4.dst == 10.0.0.2). Matches. Action: reg8[16] = 1; next;. Verdict: ALLOW.
    5. ACL_ACTION (stage 11): Priority 1000 matches reg8[16] == 1. Action: next;.

    6. QOS (stage 12): Pass through.

    7. LB (stage 15): Pass through.

    8. STATEFUL (stage 24): Priority 100 matches reg0[1] == 1 && reg0[13] == 0 (COMMIT set by hint stage, no label). Action: 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;.

    The SYN is allowed and committed to conntrack.

    Return SYN-ACK (egress):

    1. PRE_ACL (stage 2): REGBIT_CONNTRACK_DEFRAG = 1; next;.

    2. PRE_STATEFUL (stage 4): ct_next();. CT state: ct.new=0, ct.est=1, ct.rpl=1, ct_mark.blocked=0.

    3. ACL_HINT (stage 5): Priority 1 matches ct.est && ct_mark.blocked == 0. Sets BLOCK=1.

    4. ACL_EVAL (stage 6):

      • Priority 65532: ct.est && !ct.rel && ct.rpl && ct_mark.blocked == 0. Matches. Action: reg8[21] = ct_label.nf; reg8[16] = 1; next;. Verdict: ALLOW.
    5. ACL_ACTION (stage 8): ALLOW -> next;.

    The SYN-ACK is allowed via the universal reply flow. No per-ACL flow is needed for the return traffic.

    Setup: Same as 13.2. A subsequent TCP data packet on the now-established connection.

    Ingress pipeline:

    1. PRE_ACL through PRE_STATEFUL: Same as before. CT state: ct.new=0, ct.est=1, ct.rpl=0, ct_mark.blocked=0.

    2. ACL_HINT (stage 8): Priority 4 matches !ct.new && ct.est && !ct.rpl && ct_mark.blocked == 0. Sets ALLOW=1, BLOCK=1. Note: COMMIT is NOT set at this priority.

    3. ACL_EVAL (stage 9):

      • Priority 2001 (the ACL, Flow 2): reg8[30..31] == 0 && reg0[8] == 1 && (ip4.dst == 10.0.0.2). Matches. Since this is a plain allow-related ACL without label, sample_est, or network_function_group, REGBIT_CONNTRACK_COMMIT is NOT set. Action: [log_verdict] next;.
    4. ACL_ACTION (stage 11): ALLOW -> next;.

    5. STATEFUL (stage 24): The packet reaches STATEFUL with reg0[1] == 0 (COMMIT not set). It matches the priority 0 default flow: match="1", action="next;". No ct_commit happens.

    This is correct behavior: the connection was already committed during the SYN. Established flows on a plain allow-related ACL (without label/sample/nfg) pass through without re-committing. The COMMIT bit is only set when additional metadata needs to be stored in the conntrack entry.

    Setup: ACL priority 1001, from-lport, ip4.dst == 10.0.0.2, allow-related, label=42.

    The established data packet reaches ACL_EVAL with ALLOW hint. Flow 2 of the ACL matches. Because acl->label is set, the action includes REGBIT_CONNTRACK_COMMIT = 1. The packet reaches STATEFUL with reg0[1] == 1, and the variant 1 flow (priority 100, label=1) commits with observation metadata.

    13.5 Scenario: Drop ACL Blocking an Established Connection

    Setup: ACL priority 1001, to-lport, tcp.dst == 80, drop.

    A previously-allowed connection on port 80 now has a drop ACL applied. An established data packet arrives.

    Ingress pipeline:

    1. ACL_HINT (stage 8): Priority 4 matches !ct.new && ct.est && !ct.rpl && ct_mark.blocked == 0. Sets ALLOW=1, BLOCK=1.

    2. ACL_EVAL (stage 9):

      • Priority 2001 (the ACL, Flow 2): reg8[30..31] == 0 && reg0[10] == 1 && (tcp.dst == 80). The BLOCK hint is set and the ACL match hits. Action: ct_commit { ct_mark.blocked = 1; ct_label.obs_point_id = <obs_pid>; }; next;.
    3. ACL_ACTION (stage 11): No verdict register is set by the drop ACL’s Flow 2 action (it sets ct_mark.blocked via ct_commit but does not set any verdict register). The priority 0 default flow applies: next; or implicit drop depending on default_acl_drop.

    Subsequent packets on this connection will have ct_mark.blocked == 1 after the first blocked packet. They’ll match the priority 1 universal flow: ip && ct.est && ct_mark.blocked == 1 -> reg8[16] = 1; next; (ALLOW). This prevents a black hole where the SYN was allowed but subsequent data is dropped, which would leave the connection in a half-open state from the server’s perspective.


    Appendix A: Complete Flow Priority Map

    PriorityStageDescription
    0PRE_ACLDefault allow
    100PRE_ACLSend IP to CT for defrag
    110PRE_ACLSkip ND/ICMP/MLD/monitor/multicast from CT
    0ACL_HINTDefault pass-through (no ACLs/LB VIPs)
    1-7ACL_HINTCT state-based hint computation
    0ACL_EVALDefault next; or implicit drop
    1ACL_EVALRe-allow blocked established; pass non-established
    34000ACL_EVALService monitor and DNS bypass
    1000+ACL_EVALPer-ACL rules (user priority + 1000)
    65532ACL_EVALUniversal: drop invalid/blocked; allow reply/related/persisted
    65533ACL_EVALLog-related flows for ACLs with label+log+log-related
    65535ACL_EVALMax priority (no-ACL switch: bypass)
    500ACL_ACTIONTier advancement
    1000ACL_ACTIONVerdict enforcement (allow/drop/reject)
    0ACL_ACTIONDefault action (next; or drop)
    100STATEFULct_commit with label or without label
    110STATEFULct_commit with NF enabled

    Appendix B: Summary of ACL Action Types

    ActionConntrackNew Flow MatchEstablished Flow MatchNotes
    allowYes (commit)ALLOW_NEW==1 && (match)ALLOW==1 && (match)Same as allow-related
    allow-relatedYes (commit)ALLOW_NEW==1 && (match)ALLOW==1 && (match)Reply/related always allowed at priority 65532
    allow-statelessNo(match)N/AHandled in PRE_ACL stage
    dropBlock if establishedDROP==1 && (match)BLOCK==1 && (match) + ct_commit{blocked=1}Re-blocks established connections
    rejectBlock if establishedDROP==1 && (match)BLOCK==1 && (match) + ct_commit{blocked=1}Sends ICMP unreachable or TCP RST
    passNo(match)N/ASkips to next tier