diff mbox series

[ovs-dev,v7,5/9] northd: Add ACL Sampling.

Message ID 20240807065126.38132-6-dceara@redhat.com
State Accepted
Headers show
Series Add ACL Sampling using per-flow IPFIX. | expand

Checks

Context Check Description
ovsrobot/apply-robot warning apply and check: warning
ovsrobot/github-robot-_Build_and_Test success github build: passed
ovsrobot/github-robot-_ovn-kubernetes success github build: passed

Commit Message

Dumitru Ceara Aug. 7, 2024, 6:51 a.m. UTC
From: Adrian Moreno <amorenoz@redhat.com>

Introduce a new table called Sample where per-flow IPFIX configuration
can be specified.
Also, reference rows from such table from the ACL table to enable the
configuration of ACL sampling. If enabled, northd will add a sample
action to each ACL related logical flow.

Packets that hit stateful ACLs are sampled in different ways depending
whether they are initiating a new session or are just forwarded on an
existing (already allowed) session.  Two new columns ("sample_new" and
"sample_est") are added to the ACL table to allow for potentially
different sampling rates for the two cases.

Note: If an ACL has both sampling enabled and a label associated to it
then the label value overrides the observation point ID defined in the
sample configuration.  This is a side effect of the implementation
(observation point IDs are stored in conntrack in the same part of the
ct_label where ACL labels are also stored).  The two features
(sampling and ACL labels) serve however similar purposes so it's not
expected that they're both enabled together.

When sampling is enabled on an ACL additional logical flows are created
for that ACL (one for stateless ACLs and 3 for stateful ACLs) in the ACL
action stage of the logical pipeline.  These additional flows match on a
combination of conntrack state values and observation point id values
(either against a logical register or against the stored ct_label state)
in order to determine whether the packets hitting the ACLs must be
sampled or not.  This comes with a slight increase in the number of
logical flows and in the number of OpenFlow rules.  The number of
additional flows _does not_ depend on the original ACL match or action.

New --sample-new and --sample-est optional arguments are added to the
'ovn-nbctl acl-add' command to allow configuring these new types of
sampling for ACLs.

An example workflow of configuring ACL samples is:
  # Create Sampling_App mappings for ACL traffic types:
  ovn-nbctl create Sampling_App name="acl-new-traffic-sampling" \
                                id="42"
  ovn-nbctl create sampling_app name="acl-est-traffic-sampling" \
			        id="43"
  # Create two sample collectors, one that samples all packets (c1)
  # and another one that samples with a probability of 10% (c2):
  c1=$(ovn-nbctl create Sample_Collector name=c1 \
       probability=65535 set_id=1)
  c2=$(ovn-nbctl create Sample_Collector name=c2 \
       probability=6553 set_id=2)
  # Create two sample configurations (for new and for established
  # traffic) and an ingress ACL to allow IP traffic:
  ovn-nbctl \
    -- --id=@s1 create sample collector="$c1 $c2" metadata=4301 \
    -- --id=@s2 create sample collector="$c1 $c2" metadata=4302 \
    -- --sample-new=@s1 --sample-est=@s2 acl-add ls \
            from-lport 1 "ip" allow-related

The config above will generate IPFIX samples with:
- 8 MSB of observation domain id set to 42 (Sampling_App
  "acl-new-traffic-sampling" config) and observation point id
  set to 4301 (Sample s1) for packets that create a new
  connection
- 8 MSB of observation domain id set to 43 (Sampling_app
  "acl-est-traffic-sampling" config) and observation point id
  set to 4302 (Sample s2) for packets that are part of an already
  existing connection

Note: in general, all generated IPFIX sample observation domain IDs are
built by ovn-controller in the following way:
The 8 MSB taken from the sample action's obs_domain_id and the last 24
LSB taken from the Southbound logical datapath tunnel_key (datapath ID).

Acked-by: Mark Michelson <mmichels@redhat.com>
Reported-at: https://issues.redhat.com/browse/FDP-305
Signed-off-by: Adrian Moreno <amorenoz@redhat.com>
Co-authored-by: Ales Musil <amusil@redhat.com>
Signed-off-by: Ales Musil <amusil@redhat.com>
Co-authored-by: Dumitru Ceara <dceara@redhat.com>
Signed-off-by: Dumitru Ceara <dceara@redhat.com>
---
V7:
- Address Nadia's comment:
  - Make Sample_Collector.ID 8 bit long (255 values).
- Added Mark's ack.
V6:
- Address Ilya's comments:
  - make Sample non-root
  - Add unique ID to Sample_Collector to be stored in the session
    conntrack information.  Limit the max number of Sample_Collector
    records to 15 (4 bit).
- Remove the Sample.external_ids: with non-root it doesn't really make
  sense, there's always going to be a direct mapping from a single ACL
  object to the sample.
- Remove HAVE_TCPDUMP from system tests.
- Add fix and test for to-lport ACLs with sampling enabled hit on egress
  towards routers.
- Add test with different collectors (multiple probabilities) that share
  the same set_id.
- Fixed system test checking nfcapd output files (these can auto
  rotate).
V5:
- rebase
- address Ilya's comment:
  - add documentation notes about behavior when mixing ACL labels with
    ACL sampling.
V4:
- added explicit sampling stages
- reduced set_id max supported value
- added support for tiered "pass" ACLs
- improved system test + added tiered ACL system test
- added Ales as co-author for most of the above
- Addressed Mark's comment about better error messages in ovn-nbctl
V3:
- Addressed Ilya's comment:
  - Bumped NB schema version.
V2:
- Addressed Adrian's comments:
  - fixed up observation domain id comment in commit log.
  - store the obs_domain_id in the ct_label as an 8 bit value (add a
    test).
  - removed redundant check in build_acl_sample_label_action().
  - added missing space after ternary ":" operator.
  - documented limitation for sampling ACLs with action "pass".
  - documented sample_new behavior for stateless ACLs.
- Removed unused OVN_CT_SAMPLE_ID_SET_BIT and OVN_CT_SAMPLE_ID_SET.
---
 NEWS                                   |   3 +
 controller/lflow.h                     |  12 +-
 lib/logical-fields.c                   |   4 +
 lib/ovn-util.h                         |   2 +-
 northd/northd.c                        | 463 ++++++++++++++++++++++--
 northd/northd.h                        |  54 +--
 northd/ovn-northd.8.xml                | 133 +++++--
 ovn-nb.ovsschema                       |  47 ++-
 ovn-nb.xml                             |  75 ++++
 tests/atlocal.in                       |   6 +
 tests/ovn-controller.at                | 168 ++++-----
 tests/ovn-macros.at                    |  14 +-
 tests/ovn-nbctl.at                     |  36 ++
 tests/ovn-northd.at                    | 381 ++++++++++++++++++--
 tests/ovn.at                           |  69 ++--
 tests/system-common-macros.at          |  11 +
 tests/system-ovn.at                    | 465 +++++++++++++++++++++++++
 utilities/containers/fedora/Dockerfile |   1 +
 utilities/containers/ubuntu/Dockerfile |   1 +
 utilities/ovn-nbctl.8.xml              |   8 +-
 utilities/ovn-nbctl.c                  |  35 +-
 21 files changed, 1749 insertions(+), 239 deletions(-)
diff mbox series

Patch

diff --git a/NEWS b/NEWS
index 0c91e9608e..676c49e3fa 100644
--- a/NEWS
+++ b/NEWS
@@ -53,6 +53,9 @@  Post v24.03.0
   - The NB_Global.debug_drop_domain_id configured value is now overridden by
     the ID associated with the Sampling_App record created for drop sampling
     (Sampling_App.type configured as "drop").
+    - Add support for ACL sampling through the new Sample_Collector and Sample
+    tables.  Sampling is supported for both traffic that creates new
+    connections and for traffic that is part of an existing connection.
 
 OVN v24.03.0 - 01 Mar 2024
 --------------------------
diff --git a/controller/lflow.h b/controller/lflow.h
index c8a2a3f494..e95a016501 100644
--- a/controller/lflow.h
+++ b/controller/lflow.h
@@ -67,17 +67,17 @@  struct uuid;
 
 /* Start of LOG_PIPELINE_LEN tables. */
 #define OFTABLE_LOG_INGRESS_PIPELINE      8
-#define OFTABLE_OUTPUT_LARGE_PKT_DETECT  37
-#define OFTABLE_OUTPUT_LARGE_PKT_PROCESS 38
-#define OFTABLE_REMOTE_OUTPUT            39
-#define OFTABLE_LOCAL_OUTPUT             40
-#define OFTABLE_CHECK_LOOPBACK           41
+#define OFTABLE_OUTPUT_LARGE_PKT_DETECT  40
+#define OFTABLE_OUTPUT_LARGE_PKT_PROCESS 41
+#define OFTABLE_REMOTE_OUTPUT            42
+#define OFTABLE_LOCAL_OUTPUT             43
+#define OFTABLE_CHECK_LOOPBACK           44
 
 /* Start of the OUTPUT section of the pipeline. */
 #define OFTABLE_OUTPUT_INIT OFTABLE_OUTPUT_LARGE_PKT_DETECT
 
 /* Start of LOG_PIPELINE_LEN tables. */
-#define OFTABLE_LOG_EGRESS_PIPELINE      42
+#define OFTABLE_LOG_EGRESS_PIPELINE      45
 #define OFTABLE_SAVE_INPORT              64
 #define OFTABLE_LOG_TO_PHY               65
 #define OFTABLE_MAC_BINDING              66
diff --git a/lib/logical-fields.c b/lib/logical-fields.c
index 4acf8a677e..0c187e1c84 100644
--- a/lib/logical-fields.c
+++ b/lib/logical-fields.c
@@ -175,6 +175,10 @@  ovn_init_symtab(struct shash *symtab)
                                     WR_CT_COMMIT);
     expr_symtab_add_subfield_scoped(symtab, "ct_label.label", NULL,
                                     "ct_label[96..127]", WR_CT_COMMIT);
+    expr_symtab_add_subfield_scoped(symtab, "ct_label.obs_point_id", NULL,
+                                    "ct_label[96..127]", WR_CT_COMMIT);
+    expr_symtab_add_subfield_scoped(symtab, "ct_label.obs_unused", NULL,
+                                    "ct_label[0..95]", WR_CT_COMMIT);
 
     expr_symtab_add_field(symtab, "ct_state", MFF_CT_STATE, NULL, false);
 
diff --git a/lib/ovn-util.h b/lib/ovn-util.h
index ae971ce5ab..7b98b9b9a1 100644
--- a/lib/ovn-util.h
+++ b/lib/ovn-util.h
@@ -308,7 +308,7 @@  BUILD_ASSERT_DECL(
 #define SCTP_ABORT_CHUNK_FLAG_T (1 << 0)
 
 /* The number of tables for the ingress and egress pipelines. */
-#define LOG_PIPELINE_LEN 29
+#define LOG_PIPELINE_LEN 30
 
 static inline uint32_t
 hash_add_in6_addr(uint32_t hash, const struct in6_addr *addr)
diff --git a/northd/northd.c b/northd/northd.c
index 8b4ef1403a..13f9faba31 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -50,6 +50,7 @@ 
 #include "en-lr-nat.h"
 #include "en-lr-stateful.h"
 #include "en-ls-stateful.h"
+#include "en-sampling-app.h"
 #include "lib/ovn-parallel-hmap.h"
 #include "ovn/actions.h"
 #include "ovn/features.h"
@@ -184,8 +185,10 @@  static bool vxlan_mode;
 
 #define REG_ORIG_TP_DPORT_ROUTER   "reg9[16..31]"
 
-/* Register used for setting a label for ACLs in a Logical Switch. */
-#define REG_LABEL "reg3"
+/* Registers used for pasing observability information for switches:
+ * domain and point ID. */
+#define REG_OBS_POINT_ID_NEW "reg3"
+#define REG_OBS_POINT_ID_EST "reg9"
 
 /* Register used for temporarily store ECMP eth.src to avoid masked ct_label
  * access. It doesn't really occupy registers because the content of the
@@ -209,13 +212,13 @@  static bool vxlan_mode;
  * |    |     REGBIT_{HAIRPIN/HAIRPIN_REPLY}           |   |                                   |
  * |    | REGBIT_ACL_HINT_{ALLOW_NEW/ALLOW/DROP/BLOCK} |   |                                   |
  * |    |     REGBIT_ACL_{LABEL/STATELESS}             | X |                                   |
- * +----+----------------------------------------------+ X |                                   |
- * | R5 |                   UNUSED                     | X |       LB_L2_AFF_BACKEND_IP6       |
- * | R1 |         ORIG_DIP_IPV4 (>= IN_PRE_STATEFUL)   | R |                                   |
- * +----+----------------------------------------------+ E |                                   |
+ * +----+----------------------------------------------+ X |       LB_L2_AFF_BACKEND_IP6       |
+ * | R1 |         ORIG_DIP_IPV4 (>= IN_PRE_STATEFUL)   | R |        (>= IN_LB_AFF_CHECK &&     |
+ * +----+----------------------------------------------+ E |         <= IN_LB_AFF_LEARN)       |
  * | R2 |         ORIG_TP_DPORT (>= IN_PRE_STATEFUL)   | G |                                   |
  * +----+----------------------------------------------+ 0 |                                   |
- * | R3 |                  ACL LABEL                   |   |                                   |
+ * | R3 |             OBS_POINT_ID_NEW                 |   |                                   |
+ * |    |       (>= ACL_EVAL* && <= ACL_ACTION*)       |   |                                   |
  * +----+----------------------------------------------+---+-----------------------------------+
  * | R4 |            REG_LB_AFF_BACKEND_IP4            |   |                                   |
  * +----+----------------------------------------------+ X |                                   |
@@ -225,9 +228,11 @@  static bool vxlan_mode;
  * +----+----------------------------------------------+ G |                                   |
  * | R7 |                   UNUSED                     | 1 |                                   |
  * +----+----------------------------------------------+---+-----------------------------------+
- * | R8 |              LB_AFF_MATCH_PORT               |
+ * |    |              LB_AFF_MATCH_PORT               |
+ * |    |  (>= IN_LB_AFF_CHECK && <= IN_LB_AFF_LEARN)  |
  * +----+----------------------------------------------+
- * | R9 |                   UNUSED                     |
+ * | R9 |              OBS_POINT_ID_EST                |
+ * |    |       (>= ACL_EVAL* && <= ACL_ACTION*)       |
  * +----+----------------------------------------------+
  *
  * Logical Router pipeline:
@@ -6482,6 +6487,355 @@  build_acl_log(struct ds *actions, const struct nbrec_acl *acl,
     ds_put_cstr(actions, "); ");
 }
 
+/* This builds an ACL specific sample action.
+ * If the ACL has a label configured the label itself is used as sample
+ * observation point ID.  Otherwise the configured 'sample->metadata'
+ * is passed as observation point ID. */
+static void
+build_acl_sample_action(struct ds *actions, const struct nbrec_acl *acl,
+                        const struct nbrec_sample *sample,
+                        uint8_t sample_domain_id)
+{
+    if (!sample || sample_domain_id == SAMPLING_APP_ID_NONE) {
+        return;
+    }
+
+    uint32_t domain_id = 0;
+    uint32_t point_id = 0;
+
+    if (acl->label) {
+        domain_id = 0;
+        point_id = acl->label;
+    } else if (sample) {
+        domain_id = sample_domain_id;
+        point_id = sample->metadata;
+    }
+
+    for (size_t i = 0; i < sample->n_collectors; i++) {
+        ds_put_format(actions, "sample(probability=%"PRIu16","
+                               "collector_set=%"PRIu8","
+                               "obs_domain=%"PRIu32","
+                               "obs_point=%"PRIu32");",
+                               (uint16_t) sample->collectors[i]->probability,
+                               (uint8_t) sample->collectors[i]->set_id,
+                               domain_id, point_id);
+    }
+    ds_put_cstr(actions, " next;");
+}
+
+/* This builds an ACL logical flow specific action that stores the observation
+ * point IDs to be used for samples generated for traffic that hits the ACL.
+ * Two observation point IDs are stored in registers, the one for traffic
+ * that creates new connections and the one for traffic that's part of an
+ * existing connection.
+ */
+static void
+build_acl_sample_label_action(struct ds *actions, const struct nbrec_acl *acl,
+                              const struct nbrec_sample *sample_new,
+                              const struct nbrec_sample *sample_est)
+{
+    if (!acl->label && !sample_new && !sample_est) {
+        return;
+    }
+
+    uint32_t point_id_new = 0;
+    uint32_t point_id_est = 0;
+
+    if (acl->label) {
+        point_id_new = acl->label;
+        point_id_est = acl->label;
+    } else {
+        if (sample_new) {
+            point_id_new = sample_new->metadata;
+        }
+        if (sample_est) {
+            point_id_est = sample_est->metadata;
+        }
+    }
+
+    ds_put_format(actions, REGBIT_ACL_LABEL" = 1; "
+                           REG_OBS_POINT_ID_NEW " = %"PRIu32"; "
+                           REG_OBS_POINT_ID_EST " = %"PRIu32"; ",
+                  point_id_new, point_id_est);
+}
+
+/* This builds an ACL logical flow specific match that selects traffic
+ * with an associated observation point ID register equal to that of the
+ * ACL label (if configured) or sample->metadata.
+ */
+static void
+build_acl_sample_register_match(struct ds *match, const struct nbrec_acl *acl,
+                                const struct nbrec_sample *sample)
+{
+    uint32_t point_id = 0;
+
+    if (acl->label) {
+        point_id = acl->label;
+    } else if (sample) {
+        point_id = sample->metadata;
+    }
+
+    ds_put_format(match, REG_OBS_POINT_ID_NEW " == %"PRIu32, point_id);
+}
+
+/* This builds an ACL logical flow specific match that selects conntracked
+ * traffic whose associated ct_label.obs_point ID is equal to that of the
+ * ACL label (if configured) or sample->metadata.  The match also ensures
+ * that the observation domain ID stored in the ct_label is also equal to
+ * 'sample_domain_id'.
+ */
+static void
+build_acl_sample_label_match(struct ds *match, const struct nbrec_acl *acl,
+                             const struct nbrec_sample *sample)
+{
+    uint32_t point_id = 0;
+
+    if (acl->label) {
+        point_id = acl->label;
+    } else if (sample) {
+        point_id = sample->metadata;
+    }
+
+    /* Match on the complete ct_label to avoid masked access to it in the
+     * datapath.  Some NICs do not support HW offloading when masked-access
+     * of ct_label is used in the datapath. */
+    ds_put_format(match, "ct_label.obs_point_id == %"PRIu32" && "
+                         "ct_label.obs_unused == 0", point_id);
+}
+
+/* This builds a logical flow that samples and forwards/drops traffic
+ * that hit a stateless ACL ("pass" or "allow-stateless") that has sampling
+ * enabled.
+ */
+static void
+build_acl_sample_new_stateless_flows(const struct ovn_datapath *od,
+                                     struct lflow_table *lflows,
+                                     enum ovn_stage stage,
+                                     struct ds *match, struct ds *actions,
+                                     const struct nbrec_acl *acl,
+                                     uint8_t sample_domain_id,
+                                     struct lflow_ref *lflow_ref)
+{
+    if (!acl->sample_new) {
+        return;
+    }
+
+    ds_clear(actions);
+    ds_clear(match);
+
+    ds_put_cstr(match, "ip && ");
+    build_acl_sample_register_match(match, acl, acl->sample_new);
+
+    build_acl_sample_action(actions, acl, acl->sample_new, sample_domain_id);
+
+    ovn_lflow_add(lflows, od, stage, 1100, ds_cstr(match),
+                  ds_cstr(actions), lflow_ref);
+}
+
+/* This builds a logical flow that samples and forwards/drops traffic
+ * that created a new conntrack entry and hit a stateful ACL that has sampling
+ * enabled.
+ */
+static void
+build_acl_sample_new_stateful_flows(const struct ovn_datapath *od,
+                                    struct lflow_table *lflows,
+                                    enum ovn_stage stage,
+                                    struct ds *match, struct ds *actions,
+                                    const struct nbrec_acl *acl,
+                                    uint8_t sample_domain_id,
+                                    struct lflow_ref *lflow_ref)
+{
+    if (!acl->sample_new) {
+        return;
+    }
+
+    ds_clear(actions);
+    ds_clear(match);
+
+    /* Match on new connections.  However, for to-lport ACLs, due to
+     * skip_port_from_conntrack() conntrack state might be cleared, so
+     * take that into account too. */
+    ds_put_format(match, "ip && %s && ",
+                  stage != S_SWITCH_OUT_ACL_SAMPLE
+                  ? "ct.new" : "(ct.new || !ct.trk)");
+    build_acl_sample_register_match(match, acl, acl->sample_new);
+
+    build_acl_sample_action(actions, acl, acl->sample_new, sample_domain_id);
+
+    ovn_lflow_add(lflows, od, stage, 1100, ds_cstr(match),
+                  ds_cstr(actions), lflow_ref);
+}
+
+/* This builds a logical flow that samples and forwards traffic
+ * that is part of an existing connection (in the original direction) created
+ * by traffic allowed by a stateful ACL that has sampling enabled.
+ */
+static void
+build_acl_sample_est_orig_stateful_flows(const struct ovn_datapath *od,
+                                         struct lflow_table *lflows,
+                                         enum ovn_stage stage,
+                                         struct ds *match, struct ds *actions,
+                                         const struct nbrec_acl *acl,
+                                         uint8_t sample_domain_id,
+                                         struct lflow_ref *lflow_ref)
+{
+    ds_clear(actions);
+    ds_clear(match);
+
+    ds_put_cstr(match, "ip && ct.trk && "
+                       "(ct.est || ct.rel) && "
+                       "!ct.rpl && ");
+    build_acl_sample_label_match(match, acl, acl->sample_est);
+
+    build_acl_sample_action(actions, acl, acl->sample_est, sample_domain_id);
+
+    ovn_lflow_add(lflows, od, stage, 1200, ds_cstr(match),
+                  ds_cstr(actions), lflow_ref);
+}
+
+/* This builds a logical flow that samples and forwards traffic
+ * that is part of an existing connection (in the reply direction) created
+ * by traffic allowed by a stateful ACL that has sampling enabled.
+ *
+ * NOTE: unlike for traffic in the original direction, this logical flow must
+ * be installed in the "opposite" pipeline.  That is, for "from-lport" ACLs
+ * the conntrack entry is created in the ingress logical port zone and will be
+ * hit by reply traffic in the egress pipeline (before being sent out that
+ * logical port).
+ */
+static void
+build_acl_sample_est_rpl_stateful_flows(const struct ovn_datapath *od,
+                                        struct lflow_table *lflows,
+                                        enum ovn_stage rpl_stage,
+                                        struct ds *match, struct ds *actions,
+                                        const struct nbrec_acl *acl,
+                                        uint8_t sample_domain_id,
+                                        struct lflow_ref *lflow_ref)
+{
+    ds_clear(actions);
+    ds_clear(match);
+
+    ds_put_cstr(match, "ip && ct.trk && "
+                        "(ct.est || ct.rel) && "
+                        "ct.rpl && ");
+    build_acl_sample_label_match(match, acl, acl->sample_est);
+
+    build_acl_sample_action(actions, acl, acl->sample_est, sample_domain_id);
+
+    ovn_lflow_add(lflows, od, rpl_stage, 1200, ds_cstr(match),
+                  ds_cstr(actions), lflow_ref);
+}
+
+/* This builds logical flows that sample and forward traffic
+ * that is part of an existing connection (both in the original and in the
+ * reply direction) created by traffic allowed by a stateful ACL that has
+ * sampling enabled.
+ */
+static void
+build_acl_sample_est_stateful_flows(const struct ovn_datapath *od,
+                                    struct lflow_table *lflows,
+                                    enum ovn_stage stage,
+                                    struct ds *match, struct ds *actions,
+                                    const struct nbrec_acl *acl,
+                                    uint8_t sample_domain_id,
+                                    struct lflow_ref *lflow_ref)
+{
+    if (!acl->sample_est) {
+        return;
+    }
+    build_acl_sample_est_orig_stateful_flows(od, lflows, stage, match, actions,
+                                             acl, sample_domain_id, lflow_ref);
+
+    /* Install flows in the "opposite" pipeline direction to handle reply
+     * traffic on established connections. */
+    enum ovn_stage rpl_stage = (stage == S_SWITCH_OUT_ACL_SAMPLE
+                                ? S_SWITCH_IN_ACL_SAMPLE
+                                : S_SWITCH_OUT_ACL_SAMPLE);
+    build_acl_sample_est_rpl_stateful_flows(od, lflows, rpl_stage,
+                                            match, actions,
+                                            acl, sample_domain_id, lflow_ref);
+}
+
+static void build_acl_reject_action(struct ds *actions, bool is_ingress);
+
+/* This builds all ACL sampling related logical flows:
+ * - for packets creating new connections
+ * - for packets that are part of an existing connection
+ */
+static void
+build_acl_sample_flows(const struct ls_stateful_record *ls_stateful_rec,
+                       const struct ovn_datapath *od,
+                       struct lflow_table *lflows,
+                       const struct nbrec_acl *acl,
+                       struct ds *match, struct ds *actions,
+                       const struct sampling_app_table *sampling_apps,
+                       struct lflow_ref *lflow_ref)
+{
+    bool should_sample_established =
+        ls_stateful_rec->has_stateful_acl
+        && acl->sample_est
+        && !strcmp(acl->action, "allow-related");
+
+    bool stateful_match =
+        ls_stateful_rec->has_stateful_acl
+        && strcmp(acl->action, "allow-stateless");
+
+    /* Only sample if:
+     * - sampling is enabled for traffic creating new connections
+     * OR
+     * - sampling is enabled for traffic on established sessions and the
+     *   switch has stateful ACLs.
+     */
+    if (!acl->sample_new && !should_sample_established) {
+        return;
+    }
+
+    bool ingress = !strcmp(acl->direction, "from-lport") ? true : false;
+    enum ovn_stage stage;
+
+    if (ingress && smap_get_bool(&acl->options, "apply-after-lb", false)) {
+        stage = S_SWITCH_IN_ACL_AFTER_LB_SAMPLE;
+    } else if (ingress) {
+        stage = S_SWITCH_IN_ACL_SAMPLE;
+    } else {
+        stage = S_SWITCH_OUT_ACL_SAMPLE;
+    }
+
+    uint8_t sample_new_domain_id = sampling_app_get_id(sampling_apps,
+                                                       SAMPLING_APP_ACL_NEW);
+    uint8_t sample_est_domain_id = sampling_app_get_id(sampling_apps,
+                                                       SAMPLING_APP_ACL_EST);
+
+    if (!stateful_match) {
+        build_acl_sample_new_stateless_flows(od, lflows, stage, match, actions,
+                                             acl, sample_new_domain_id,
+                                             lflow_ref);
+    } else {
+        build_acl_sample_new_stateful_flows(od, lflows, stage, match, actions,
+                                            acl, sample_new_domain_id,
+                                            lflow_ref);
+        build_acl_sample_est_stateful_flows(od, lflows, stage, match, actions,
+                                            acl, sample_est_domain_id,
+                                            lflow_ref);
+    }
+}
+
+/* This builds all default ACL sampling related logical flows. */
+static void
+build_acl_sample_default_flows(const struct ovn_datapath *od,
+                               struct lflow_table *lflows,
+                               struct lflow_ref *lflow_ref)
+{
+    /* Rules at priority 1 is added below to pass the packet into next table
+     * if there isn't any match. */
+    ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL_SAMPLE, 0, "1", "next;",
+                  lflow_ref);
+    ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL_SAMPLE, 0, "1", "next;",
+                  lflow_ref);
+    ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL_AFTER_LB_SAMPLE, 0, "1",
+                  "next;", lflow_ref);
+}
+
 static void
 consider_acl(struct lflow_table *lflows, const struct ovn_datapath *od,
              const struct nbrec_acl *acl, bool has_stateful,
@@ -6529,6 +6883,10 @@  consider_acl(struct lflow_table *lflows, const struct ovn_datapath *od,
     if (!has_stateful
         || !strcmp(acl->action, "pass")
         || !strcmp(acl->action, "allow-stateless")) {
+
+        /* For stateless ACLs just sample "new" packets. */
+        build_acl_sample_label_action(actions, acl, acl->sample_new, NULL);
+
         ds_put_cstr(actions, "next;");
         ds_put_format(match, "(%s)", acl->match);
         ovn_lflow_add_with_hint(lflows, od, stage, priority,
@@ -6563,10 +6921,10 @@  consider_acl(struct lflow_table *lflows, const struct ovn_datapath *od,
 
         ds_truncate(actions, log_verdict_len);
         ds_put_cstr(actions, REGBIT_CONNTRACK_COMMIT" = 1; ");
-        if (acl->label) {
-            ds_put_format(actions, REGBIT_ACL_LABEL" = 1; "
-                          REG_LABEL" = %"PRId64"; ", acl->label);
-        }
+
+        /* For stateful ACLs sample "new" and "established" packets. */
+        build_acl_sample_label_action(actions, acl, acl->sample_new,
+                                      acl->sample_est);
         ds_put_cstr(actions, "next;");
         ovn_lflow_add_with_hint(lflows, od, stage, priority,
                                 ds_cstr(match), ds_cstr(actions),
@@ -6586,9 +6944,11 @@  consider_acl(struct lflow_table *lflows, const struct ovn_datapath *od,
                       acl->match);
         if (acl->label) {
             ds_put_cstr(actions, REGBIT_CONNTRACK_COMMIT" = 1; ");
-            ds_put_format(actions, REGBIT_ACL_LABEL" = 1; "
-                          REG_LABEL" = %"PRId64"; ", acl->label);
         }
+
+        /* For stateful ACLs sample "new" and "established" packets. */
+        build_acl_sample_label_action(actions, acl, acl->sample_new,
+                                      acl->sample_est);
         ds_put_cstr(actions, "next;");
         ovn_lflow_add_with_hint(lflows, od, stage, priority,
                                 ds_cstr(match), ds_cstr(actions),
@@ -6606,6 +6966,9 @@  consider_acl(struct lflow_table *lflows, const struct ovn_datapath *od,
         ds_put_format(match, " && (%s)", acl->match);
 
         ds_truncate(actions, log_verdict_len);
+
+        /* For drop ACLs just sample all packets as "new" packets. */
+        build_acl_sample_label_action(actions, acl, acl->sample_new, NULL);
         ds_put_cstr(actions, "next;");
         ovn_lflow_add_with_hint(lflows, od, stage, priority,
                                 ds_cstr(match), ds_cstr(actions),
@@ -6626,6 +6989,9 @@  consider_acl(struct lflow_table *lflows, const struct ovn_datapath *od,
         ds_put_format(match, " && (%s)", acl->match);
 
         ds_truncate(actions, log_verdict_len);
+
+        /* For drop ACLs just sample all packets as "new" packets. */
+        build_acl_sample_label_action(actions, acl, acl->sample_new, NULL);
         ds_put_cstr(actions, "ct_commit { ct_mark.blocked = 1; }; next;");
         ovn_lflow_add_with_hint(lflows, od, stage, priority,
                                 ds_cstr(match), ds_cstr(actions),
@@ -6706,6 +7072,20 @@  ovn_update_ipv6_options(struct hmap *lr_ports)
 
 #define IPV6_CT_OMIT_MATCH "nd || nd_ra || nd_rs || mldv1 || mldv2"
 
+static void
+build_acl_reject_action(struct ds *actions, bool is_ingress)
+{
+    ds_put_format(
+        actions, "reg0 = 0; "
+        "reject { "
+          "/* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ "
+          "outport <-> inport; next(pipeline=%s,table=%d); "
+        "};",
+        is_ingress ? "egress" : "ingress",
+        is_ingress ? ovn_stage_get_table(S_SWITCH_OUT_QOS)
+            : ovn_stage_get_table(S_SWITCH_IN_L2_LKUP));
+}
+
 static void
 build_acl_action_lflows(const struct ls_stateful_record *ls_stateful_rec,
                         const struct ovn_datapath *od,
@@ -6722,6 +7102,12 @@  build_acl_action_lflows(const struct ls_stateful_record *ls_stateful_rec,
         S_SWITCH_OUT_ACL_ACTION,
     };
 
+    enum ovn_stage eval_stages[] = {
+        S_SWITCH_IN_ACL_EVAL,
+        S_SWITCH_IN_ACL_AFTER_LB_EVAL,
+        S_SWITCH_OUT_ACL_EVAL,
+    };
+
     ds_clear(actions);
     ds_put_cstr(actions, REGBIT_ACL_VERDICT_ALLOW " = 0; "
                         REGBIT_ACL_VERDICT_DROP " = 0; "
@@ -6752,14 +7138,7 @@  build_acl_action_lflows(const struct ls_stateful_record *ls_stateful_rec,
         bool ingress = ovn_stage_get_pipeline(stage) == P_IN;
 
         ds_truncate(actions, verdict_len);
-        ds_put_format(
-            actions, "reg0 = 0; "
-            "reject { "
-            "/* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ "
-            "outport <-> inport; next(pipeline=%s,table=%d); };",
-            ingress ? "egress" : "ingress",
-            ingress ? ovn_stage_get_table(S_SWITCH_OUT_QOS)
-                : ovn_stage_get_table(S_SWITCH_IN_L2_LKUP));
+        build_acl_reject_action(actions, ingress);
 
         ovn_lflow_metered(lflows, od, stage, 1000,
                           REGBIT_ACL_VERDICT_REJECT " == 1", ds_cstr(actions),
@@ -6778,7 +7157,7 @@  build_acl_action_lflows(const struct ls_stateful_record *ls_stateful_rec,
             ds_put_format(&tier_actions, REG_ACL_TIER " = %"PRIuSIZE"; "
                           "next(pipeline=%s,table=%d);",
                           j + 1, ingress ? "ingress" : "egress",
-                          ovn_stage_get_table(stage) - 1);
+                          ovn_stage_get_table(eval_stages[i]));
             ovn_lflow_add(lflows, od, stage, 500, ds_cstr(match),
                          ds_cstr(&tier_actions), lflow_ref);
         }
@@ -6799,12 +7178,6 @@  build_acl_log_related_flows(const struct ovn_datapath *od,
      * the ACL, then we need to ensure that the related and reply
      * traffic is logged, so we install a slightly higher-priority
      * flow that matches the ACL, allows the traffic, and logs it.
-     *
-     * Note: Matching the ct_label.label may prevent OVS flow HW
-     * offloading to work for some NICs because masked-access of
-     * ct_label is not supported on those NICs due to HW
-     * limitations. In such case the user may choose to avoid using the
-     * "log-related" option.
      */
     bool ingress = !strcmp(acl->direction, "from-lport") ? true :false;
     bool log_related = smap_get_bool(&acl->options, "log-related",
@@ -6863,6 +7236,7 @@  build_acls(const struct ls_stateful_record *ls_stateful_rec,
            struct lflow_table *lflows,
            const struct ls_port_group_table *ls_port_groups,
            const struct shash *meter_groups,
+           const struct sampling_app_table *sampling_apps,
            struct lflow_ref *lflow_ref)
 {
     const char *default_acl_action = default_acl_drop
@@ -7043,6 +7417,8 @@  build_acls(const struct ls_stateful_record *ls_stateful_rec,
                   REGBIT_ACL_VERDICT_ALLOW " = 1; next;",
                   lflow_ref);
 
+    build_acl_sample_default_flows(od, lflows, lflow_ref);
+
     /* Ingress or Egress ACL Table (Various priorities). */
     for (size_t i = 0; i < od->nbs->n_acls; i++) {
         struct nbrec_acl *acl = od->nbs->acls[i];
@@ -7052,6 +7428,8 @@  build_acls(const struct ls_stateful_record *ls_stateful_rec,
         consider_acl(lflows, od, acl, has_stateful,
                      meter_groups, ls_stateful_rec->max_acl_tier,
                      &match, &actions, lflow_ref);
+        build_acl_sample_flows(ls_stateful_rec, od, lflows, acl,
+                               &match, &actions, sampling_apps, lflow_ref);
     }
 
     const struct ls_port_group *ls_pg =
@@ -7068,6 +7446,9 @@  build_acls(const struct ls_stateful_record *ls_stateful_rec,
                 consider_acl(lflows, od, acl, has_stateful,
                              meter_groups, ls_stateful_rec->max_acl_tier,
                              &match, &actions, lflow_ref);
+                build_acl_sample_flows(ls_stateful_rec, od, lflows, acl,
+                                       &match, &actions, sampling_apps,
+                                       lflow_ref);
             }
         }
     }
@@ -7727,8 +8108,11 @@  build_stateful(struct ovn_datapath *od, struct lflow_table *lflows,
      * We always set ct_mark.blocked to 0 here as
      * any packet that makes it this far is part of a connection we
      * want to allow to continue. */
-    ds_put_cstr(&actions, "ct_commit { ct_mark.blocked = 0; "
-                          "ct_label.label = " REG_LABEL "; }; next;");
+    ds_put_cstr(&actions,
+                 "ct_commit { "
+                    "ct_mark.blocked = 0; "
+                    "ct_label.obs_point_id = " REG_OBS_POINT_ID_EST "; "
+                  "}; next;");
     ovn_lflow_add(lflows, od, S_SWITCH_IN_STATEFUL, 100,
                   REGBIT_CONNTRACK_COMMIT" == 1 && "
                   REGBIT_ACL_LABEL" == 1",
@@ -15776,6 +16160,7 @@  build_ls_stateful_flows(const struct ls_stateful_record *ls_stateful_rec,
                         const struct ovn_datapath *od,
                         const struct ls_port_group_table *ls_pgs,
                         const struct shash *meter_groups,
+                        const struct sampling_app_table *sampling_apps,
                         struct lflow_table *lflows)
 {
     build_ls_stateful_rec_pre_acls(ls_stateful_rec, od, ls_pgs, lflows,
@@ -15785,7 +16170,7 @@  build_ls_stateful_flows(const struct ls_stateful_record *ls_stateful_rec,
     build_acl_hints(ls_stateful_rec, od, lflows,
                     ls_stateful_rec->lflow_ref);
     build_acls(ls_stateful_rec, od, lflows, ls_pgs, meter_groups,
-               ls_stateful_rec->lflow_ref);
+               sampling_apps, ls_stateful_rec->lflow_ref);
     build_lb_hairpin(ls_stateful_rec, od, lflows, ls_stateful_rec->lflow_ref);
 }
 
@@ -15809,6 +16194,7 @@  struct lswitch_flow_build_info {
     struct ds actions;
     size_t thread_lflow_counter;
     const char *svc_monitor_mac;
+    const struct sampling_app_table *sampling_apps;
 };
 
 /* Helper function to combine all lflow generation which is iterated by
@@ -16100,6 +16486,7 @@  build_lflows_thread(void *arg)
                     build_ls_stateful_flows(ls_stateful_rec, od,
                                             lsi->ls_port_groups,
                                             lsi->meter_groups,
+                                            lsi->sampling_apps,
                                             lsi->lflows);
                 }
             }
@@ -16173,7 +16560,8 @@  build_lswitch_and_lrouter_flows(
     const struct hmap *svc_monitor_map,
     const struct hmap *bfd_connections,
     const struct chassis_features *features,
-    const char *svc_monitor_mac)
+    const char *svc_monitor_mac,
+    const struct sampling_app_table *sampling_apps)
 {
 
     char *svc_check_match = xasprintf("eth.dst == %s", svc_monitor_mac);
@@ -16207,6 +16595,7 @@  build_lswitch_and_lrouter_flows(
             lsiv[index].svc_check_match = svc_check_match;
             lsiv[index].thread_lflow_counter = 0;
             lsiv[index].svc_monitor_mac = svc_monitor_mac;
+            lsiv[index].sampling_apps = sampling_apps;
             ds_init(&lsiv[index].match);
             ds_init(&lsiv[index].actions);
 
@@ -16247,6 +16636,7 @@  build_lswitch_and_lrouter_flows(
             .features = features,
             .svc_check_match = svc_check_match,
             .svc_monitor_mac = svc_monitor_mac,
+            .sampling_apps = sampling_apps,
             .match = DS_EMPTY_INITIALIZER,
             .actions = DS_EMPTY_INITIALIZER,
         };
@@ -16319,6 +16709,7 @@  build_lswitch_and_lrouter_flows(
                                    &od->nbs->header_.uuid));
             build_ls_stateful_flows(ls_stateful_rec, od, lsi.ls_port_groups,
                                     lsi.meter_groups,
+                                    lsi.sampling_apps,
                                     lsi.lflows);
         }
         stopwatch_stop(LFLOWS_LS_STATEFUL_STOPWATCH_NAME, time_msec());
@@ -16408,7 +16799,8 @@  void build_lflows(struct ovsdb_idl_txn *ovnsb_txn,
                                     input_data->svc_monitor_map,
                                     input_data->bfd_connections,
                                     input_data->features,
-                                    input_data->svc_monitor_mac);
+                                    input_data->svc_monitor_mac,
+                                    input_data->sampling_apps);
 
     if (parallelization_state == STATE_INIT_HASH_SIZES) {
         parallelization_state = STATE_USE_PARALLELIZATION;
@@ -16832,6 +17224,7 @@  lflow_handle_ls_stateful_changes(struct ovsdb_idl_txn *ovnsb_txn,
         build_ls_stateful_flows(ls_stateful_rec, od,
                                 lflow_input->ls_port_groups,
                                 lflow_input->meter_groups,
+                                lflow_input->sampling_apps,
                                 lflows);
 
         /* Sync the new flows to SB. */
diff --git a/northd/northd.h b/northd/northd.h
index e50aa6731a..b628911510 100644
--- a/northd/northd.h
+++ b/northd/northd.h
@@ -397,27 +397,30 @@  enum ovn_stage {
     PIPELINE_STAGE(SWITCH, IN,  PRE_STATEFUL,   6, "ls_in_pre_stateful")  \
     PIPELINE_STAGE(SWITCH, IN,  ACL_HINT,       7, "ls_in_acl_hint")      \
     PIPELINE_STAGE(SWITCH, IN,  ACL_EVAL,       8, "ls_in_acl_eval")      \
-    PIPELINE_STAGE(SWITCH, IN,  ACL_ACTION,     9, "ls_in_acl_action")    \
-    PIPELINE_STAGE(SWITCH, IN,  QOS,           10, "ls_in_qos")    \
-    PIPELINE_STAGE(SWITCH, IN,  LB_AFF_CHECK,  11, "ls_in_lb_aff_check")  \
-    PIPELINE_STAGE(SWITCH, IN,  LB,            12, "ls_in_lb")            \
-    PIPELINE_STAGE(SWITCH, IN,  LB_AFF_LEARN,  13, "ls_in_lb_aff_learn")  \
-    PIPELINE_STAGE(SWITCH, IN,  PRE_HAIRPIN,   14, "ls_in_pre_hairpin")   \
-    PIPELINE_STAGE(SWITCH, IN,  NAT_HAIRPIN,   15, "ls_in_nat_hairpin")   \
-    PIPELINE_STAGE(SWITCH, IN,  HAIRPIN,       16, "ls_in_hairpin")       \
-    PIPELINE_STAGE(SWITCH, IN,  ACL_AFTER_LB_EVAL,  17, \
-                   "ls_in_acl_after_lb_eval")  \
-    PIPELINE_STAGE(SWITCH, IN,  ACL_AFTER_LB_ACTION,  18, \
+    PIPELINE_STAGE(SWITCH, IN,  ACL_SAMPLE,     9, "ls_in_acl_sample")    \
+    PIPELINE_STAGE(SWITCH, IN,  ACL_ACTION,    10, "ls_in_acl_action")    \
+    PIPELINE_STAGE(SWITCH, IN,  QOS,           11, "ls_in_qos")    \
+    PIPELINE_STAGE(SWITCH, IN,  LB_AFF_CHECK,  12, "ls_in_lb_aff_check")  \
+    PIPELINE_STAGE(SWITCH, IN,  LB,            13, "ls_in_lb")            \
+    PIPELINE_STAGE(SWITCH, IN,  LB_AFF_LEARN,  14, "ls_in_lb_aff_learn")  \
+    PIPELINE_STAGE(SWITCH, IN,  PRE_HAIRPIN,   15, "ls_in_pre_hairpin")   \
+    PIPELINE_STAGE(SWITCH, IN,  NAT_HAIRPIN,   16, "ls_in_nat_hairpin")   \
+    PIPELINE_STAGE(SWITCH, IN,  HAIRPIN,       17, "ls_in_hairpin")       \
+    PIPELINE_STAGE(SWITCH, IN,  ACL_AFTER_LB_EVAL,  18, \
+                   "ls_in_acl_after_lb_eval")                             \
+     PIPELINE_STAGE(SWITCH, IN,  ACL_AFTER_LB_SAMPLE,  19, \
+                   "ls_in_acl_after_lb_sample")  \
+    PIPELINE_STAGE(SWITCH, IN,  ACL_AFTER_LB_ACTION,  20, \
                    "ls_in_acl_after_lb_action")  \
-    PIPELINE_STAGE(SWITCH, IN,  STATEFUL,      19, "ls_in_stateful")      \
-    PIPELINE_STAGE(SWITCH, IN,  ARP_ND_RSP,    20, "ls_in_arp_rsp")       \
-    PIPELINE_STAGE(SWITCH, IN,  DHCP_OPTIONS,  21, "ls_in_dhcp_options")  \
-    PIPELINE_STAGE(SWITCH, IN,  DHCP_RESPONSE, 22, "ls_in_dhcp_response") \
-    PIPELINE_STAGE(SWITCH, IN,  DNS_LOOKUP,    23, "ls_in_dns_lookup")    \
-    PIPELINE_STAGE(SWITCH, IN,  DNS_RESPONSE,  24, "ls_in_dns_response")  \
-    PIPELINE_STAGE(SWITCH, IN,  EXTERNAL_PORT, 25, "ls_in_external_port") \
-    PIPELINE_STAGE(SWITCH, IN,  L2_LKUP,       26, "ls_in_l2_lkup")       \
-    PIPELINE_STAGE(SWITCH, IN,  L2_UNKNOWN,    27, "ls_in_l2_unknown")    \
+    PIPELINE_STAGE(SWITCH, IN,  STATEFUL,      21, "ls_in_stateful")      \
+    PIPELINE_STAGE(SWITCH, IN,  ARP_ND_RSP,    22, "ls_in_arp_rsp")       \
+    PIPELINE_STAGE(SWITCH, IN,  DHCP_OPTIONS,  23, "ls_in_dhcp_options")  \
+    PIPELINE_STAGE(SWITCH, IN,  DHCP_RESPONSE, 24, "ls_in_dhcp_response") \
+    PIPELINE_STAGE(SWITCH, IN,  DNS_LOOKUP,    25, "ls_in_dns_lookup")    \
+    PIPELINE_STAGE(SWITCH, IN,  DNS_RESPONSE,  26, "ls_in_dns_response")  \
+    PIPELINE_STAGE(SWITCH, IN,  EXTERNAL_PORT, 27, "ls_in_external_port") \
+    PIPELINE_STAGE(SWITCH, IN,  L2_LKUP,       28, "ls_in_l2_lkup")       \
+    PIPELINE_STAGE(SWITCH, IN,  L2_UNKNOWN,    29, "ls_in_l2_unknown")    \
                                                                           \
     /* Logical switch egress stages. */                                   \
     PIPELINE_STAGE(SWITCH, OUT, PRE_ACL,      0, "ls_out_pre_acl")        \
@@ -425,11 +428,12 @@  enum ovn_stage {
     PIPELINE_STAGE(SWITCH, OUT, PRE_STATEFUL, 2, "ls_out_pre_stateful")   \
     PIPELINE_STAGE(SWITCH, OUT, ACL_HINT,     3, "ls_out_acl_hint")       \
     PIPELINE_STAGE(SWITCH, OUT, ACL_EVAL,     4, "ls_out_acl_eval")       \
-    PIPELINE_STAGE(SWITCH, OUT, ACL_ACTION,   5, "ls_out_acl_action")     \
-    PIPELINE_STAGE(SWITCH, OUT, QOS,          6, "ls_out_qos")       \
-    PIPELINE_STAGE(SWITCH, OUT, STATEFUL,     7, "ls_out_stateful")       \
-    PIPELINE_STAGE(SWITCH, OUT, CHECK_PORT_SEC,  8, "ls_out_check_port_sec") \
-    PIPELINE_STAGE(SWITCH, OUT, APPLY_PORT_SEC, 9, "ls_out_apply_port_sec") \
+    PIPELINE_STAGE(SWITCH, OUT, ACL_SAMPLE,   5, "ls_out_acl_sample")     \
+    PIPELINE_STAGE(SWITCH, OUT, ACL_ACTION,   6, "ls_out_acl_action")     \
+    PIPELINE_STAGE(SWITCH, OUT, QOS,          7, "ls_out_qos")       \
+    PIPELINE_STAGE(SWITCH, OUT, STATEFUL,     8, "ls_out_stateful")       \
+    PIPELINE_STAGE(SWITCH, OUT, CHECK_PORT_SEC,  9, "ls_out_check_port_sec") \
+    PIPELINE_STAGE(SWITCH, OUT, APPLY_PORT_SEC, 10, "ls_out_apply_port_sec") \
                                                                       \
     /* Logical router ingress stages. */                              \
     PIPELINE_STAGE(ROUTER, IN,  ADMISSION,       0, "lr_in_admission")    \
diff --git a/northd/ovn-northd.8.xml b/northd/ovn-northd.8.xml
index ba85e4bfd7..3abd5f75bb 100644
--- a/northd/ovn-northd.8.xml
+++ b/northd/ovn-northd.8.xml
@@ -867,7 +867,47 @@ 
       </li>
     </ul>
 
-    <h3>Ingress Table 9: <code>from-lport</code> ACL action</h3>
+    <h3>Ingress Table 9: <code>from-lport</code> ACL sampling</h3>
+
+    <p>
+      Logical flows in this table sample traffic matched by
+      <code>from-lport</code> ACLs with sampling enabled.
+    </p>
+
+    <ul>
+      <li>
+        If no ACLs have sampling enabled, then a priority 0 flow is installed
+        that matches everything and advances to the next table.
+      </li>
+
+      <li>
+        For each ACL with sample_new configured a priority 1100 flow is
+        installed that matches on the saved observation_point_id value.
+        This flow generates a <code>sample()</code> action and then advances
+        the packet to the next table.
+      </li>
+
+      <li>
+        For each ACL with sample_est configured a priority 1200 flow is
+        installed that matches on the saved observation_point_id value
+        for established traffic in the original direction.  This flow
+        generates a <code>sample()</code> action and then advances
+        the packet to the next table.
+      </li>
+
+      <li>
+        For each ACL with sample_est configured a priority 1200 flow is
+        installed that matches on the saved observation_point_id
+        value for established traffic in the reply direction.  This flow
+        generates a <code>sample()</code> action and then advances
+        the packet to the next table.  Note: this flow is installed in the
+        opposite pipeline (in the ingress pipeline for ACLs applied in the
+        egress direction and in the egress pipeline for ACLs applied in the
+        ingress direction).
+      </li>
+    </ul>
+
+    <h3>Ingress Table 10: <code>from-lport</code> ACL action</h3>
 
     <p>
       Logical flows in this table decide how to proceed based on the values of
@@ -907,7 +947,7 @@ 
       </li>
     </ul>
 
-    <h3>Ingress Table 10: <code>from-lport</code> QoS</h3>
+    <h3>Ingress Table 11: <code>from-lport</code> QoS</h3>
 
     <p>
       Logical flows in this table closely reproduce those in the
@@ -930,7 +970,7 @@ 
       </li>
     </ul>
 
-    <h3>Ingress Table 11: Load balancing affinity check</h3>
+    <h3>Ingress Table 12: Load balancing affinity check</h3>
 
     <p>
       Load balancing affinity check table contains the following
@@ -958,7 +998,7 @@ 
       </li>
     </ul>
 
-    <h3>Ingress Table 12: LB</h3>
+    <h3>Ingress Table 13: LB</h3>
 
     <ul>
       <li>
@@ -1038,7 +1078,7 @@ 
       </li>
     </ul>
 
-    <h3>Ingress Table 13: Load balancing affinity learn</h3>
+    <h3>Ingress Table 14: Load balancing affinity learn</h3>
 
     <p>
       Load balancing affinity learn table contains the following
@@ -1069,7 +1109,7 @@ 
       </li>
     </ul>
 
-    <h3>Ingress Table 14: Pre-Hairpin</h3>
+    <h3>Ingress Table 15: Pre-Hairpin</h3>
     <ul>
       <li>
         If the logical switch has load balancer(s) configured, then a
@@ -1087,7 +1127,7 @@ 
       </li>
     </ul>
 
-    <h3>Ingress Table 15: Nat-Hairpin</h3>
+    <h3>Ingress Table 16: Nat-Hairpin</h3>
     <ul>
       <li>
          If the logical switch has load balancer(s) configured, then a
@@ -1122,7 +1162,7 @@ 
       </li>
     </ul>
 
-    <h3>Ingress Table 16: Hairpin</h3>
+    <h3>Ingress Table 17: Hairpin</h3>
     <ul>
       <li>
         <p>
@@ -1160,7 +1200,7 @@ 
       </li>
     </ul>
 
-    <h3>Ingress table 17: <code>from-lport</code> ACL evaluation after LB</h3>
+    <h3>Ingress table 18: <code>from-lport</code> ACL evaluation after LB</h3>
 
     <p>
       Logical flows in this table closely reproduce those in the
@@ -1245,7 +1285,47 @@ 
       </li>
     </ul>
 
-    <h3>Ingress Table 18: <code>from-lport</code> ACL action after LB</h3>
+    <h3>Ingress Table 19: <code>from-lport</code> ACL sampling after LB</h3>
+
+    <p>
+      Logical flows in this table sample traffic matched by
+      <code>from-lport</code> ACLs (evaluation after LB) with sampling enabled.
+    </p>
+
+    <ul>
+      <li>
+        If no ACLs have sampling enabled, then a priority 0 flow is installed
+        that matches everything and advances to the next table.
+      </li>
+
+      <li>
+        For each ACL with sample_new configured a priority 1100 flow is
+        installed that matches on the saved observation_point_id value.
+        This flow generates a <code>sample()</code> action and then advances
+        the packet to the next table.
+      </li>
+
+      <li>
+        For each ACL with sample_est configured a priority 1200 flow is
+        installed that matches on the saved observation_point_id value
+        for established traffic in the original direction.  This flow
+        generates a <code>sample()</code> action and then advances
+        the packet to the next table.
+      </li>
+
+      <li>
+        For each ACL with sample_est configured a priority 1200 flow is
+        installed that matches on the saved observation_point_id
+        value for established traffic in the reply direction.  This flow
+        generates a <code>sample()</code> action and then advances
+        the packet to the next table.  Note: this flow is installed in the
+        opposite pipeline (in the ingress pipeline for ACLs applied in the
+        egress direction and in the egress pipeline for ACLs applied in the
+        ingress direction).
+      </li>
+    </ul>
+
+    <h3>Ingress Table 20: <code>from-lport</code> ACL action after LB</h3>
 
     <p>
       Logical flows in this table decide how to proceed based on the values of
@@ -1285,7 +1365,7 @@ 
       </li>
     </ul>
 
-    <h3>Ingress Table 19: Stateful</h3>
+    <h3>Ingress Table 21: Stateful</h3>
 
     <ul>
       <li>
@@ -1308,7 +1388,7 @@ 
       </li>
     </ul>
 
-    <h3>Ingress Table 20: ARP/ND responder</h3>
+    <h3>Ingress Table 22: ARP/ND responder</h3>
 
     <p>
       This table implements ARP/ND responder in a logical switch for known
@@ -1643,7 +1723,7 @@  output;
       </li>
     </ul>
 
-    <h3>Ingress Table 21: DHCP option processing</h3>
+    <h3>Ingress Table 23: DHCP option processing</h3>
 
     <p>
       This table adds the DHCPv4 options to a DHCPv4 packet from the
@@ -1704,7 +1784,7 @@  next;
       </li>
     </ul>
 
-    <h3>Ingress Table 22: DHCP responses</h3>
+    <h3>Ingress Table 24: DHCP responses</h3>
 
     <p>
       This table implements DHCP responder for the DHCP replies generated by
@@ -1785,7 +1865,7 @@  output;
       </li>
     </ul>
 
-    <h3>Ingress Table 23 DNS Lookup</h3>
+    <h3>Ingress Table 25 DNS Lookup</h3>
 
     <p>
       This table looks up and resolves the DNS names to the corresponding
@@ -1814,7 +1894,7 @@  reg0[4] = dns_lookup(); next;
       </li>
     </ul>
 
-    <h3>Ingress Table 24 DNS Responses</h3>
+    <h3>Ingress Table 26 DNS Responses</h3>
 
     <p>
       This table implements DNS responder for the DNS replies generated by
@@ -1849,7 +1929,7 @@  output;
       </li>
     </ul>
 
-    <h3>Ingress table 25 External ports</h3>
+    <h3>Ingress table 27 External ports</h3>
 
     <p>
       Traffic from the <code>external</code> logical ports enter the ingress
@@ -1892,7 +1972,7 @@  output;
       </li>
     </ul>
 
-    <h3>Ingress Table 26 Destination Lookup</h3>
+    <h3>Ingress Table 28 Destination Lookup</h3>
 
     <p>
       This table implements switching behavior.  It contains these logical
@@ -2090,7 +2170,7 @@  output;
       </li>
     </ul>
 
-    <h3>Ingress Table 27 Destination unknown</h3>
+    <h3>Ingress Table 29 Destination unknown</h3>
 
     <p>
       This table handles the packets whose destination was not found or
@@ -2298,26 +2378,31 @@  output;
       </li>
     </ul>
 
-    <h3>Egress Table 5: <code>to-lport</code> ACL action</h3>
+    <h3>Egress Table 5: <code>to-lport</code> ACL sampling</h3>
+    <p>
+      This is similar to ingress table <code>ACL sampling</code>.
+    </p>
+
+    <h3>Egress Table 6: <code>to-lport</code> ACL action</h3>
     <p>
       This is similar to ingress table <code>ACL action</code>.
     </p>
 
-    <h3>Egress Table 6: <code>to-lport</code> QoS</h3>
+    <h3>Egress Table 7: <code>to-lport</code> QoS</h3>
 
     <p>
       This is similar to ingress table <code>QoS</code> except
       they apply to <code>to-lport</code> QoS rules.
     </p>
 
-    <h3>Egress Table 7: Stateful</h3>
+    <h3>Egress Table 8: Stateful</h3>
 
     <p>
       This is similar to ingress table <code>Stateful</code> except that
       there are no rules added for load balancing new connections.
     </p>
 
-    <h3>Egress Table 8: Egress Port Security - check</h3>
+    <h3>Egress Table 9: Egress Port Security - check</h3>
 
     <p>
       This is similar to the port security logic in table
@@ -2346,7 +2431,7 @@  output;
       </li>
     </ul>
 
-    <h3>Egress Table 9: Egress Port Security - Apply</h3>
+    <h3>Egress Table 10: Egress Port Security - Apply</h3>
 
     <p>
       This is similar to the ingress port security logic in ingress table
diff --git a/ovn-nb.ovsschema b/ovn-nb.ovsschema
index a6a377f20b..b4a395c567 100644
--- a/ovn-nb.ovsschema
+++ b/ovn-nb.ovsschema
@@ -1,7 +1,7 @@ 
 {
     "name": "OVN_Northbound",
-    "version": "7.5.0",
-    "cksum": "1137408189 36223",
+    "version": "7.6.0",
+    "cksum": "2171465655 38284",
     "tables": {
         "NB_Global": {
             "columns": {
@@ -30,6 +30,41 @@ 
                 "ipsec": {"type": "boolean"}},
             "maxRows": 1,
             "isRoot": true},
+        "Sample_Collector": {
+            "columns": {
+                "id": {"type": {"key": {
+                    "type": "integer",
+                    "minInteger": 1,
+                    "maxInteger": 255}}},
+                "name": {"type": "string"},
+                "probability": {"type": {"key": {
+                    "type": "integer",
+                    "minInteger": 0,
+                    "maxInteger": 65535}}},
+                "set_id": {"type": {"key": {
+                    "type": "integer",
+                    "minInteger": 1,
+                    "maxInteger": 4294967295}}},
+                "external_ids": {"type": {"key": "string", "value": "string",
+                                          "min": 0, "max": "unlimited"}}
+            },
+            "indexes": [["id"]],
+            "isRoot": true
+        },
+        "Sample": {
+            "columns": {
+                "collectors": {"type": {"key": {"type": "uuid",
+                                                "refTable": "Sample_Collector",
+                                                "refType": "strong"},
+                                        "min": 0,
+                                        "max": "unlimited"}},
+                "metadata": {"type": {"key": {"type": "integer",
+                                              "minInteger": 1,
+                                              "maxInteger": 4294967295},
+                                      "min": 1, "max":1}}
+            },
+            "indexes": [["metadata"]]
+        },
         "Copp": {
             "columns": {
                 "name": {"type": "string"},
@@ -275,6 +310,14 @@ 
                 "tier": {"type": {"key": {"type": "integer",
                                           "minInteger": 0,
                                           "maxInteger": 3}}},
+                "sample_new": {"type": {"key": {"type": "uuid",
+                                                "refTable": "Sample",
+                                                "refType": "strong"},
+                                        "min": 0, "max": 1}},
+                "sample_est": {"type": {"key": {"type": "uuid",
+                                                "refTable": "Sample",
+                                                "refType": "strong"},
+                                        "min": 0, "max": 1}},
                 "options": {
                      "type": {"key": "string",
                               "value": "string",
diff --git a/ovn-nb.xml b/ovn-nb.xml
index bc44f67642..181eab0999 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -511,6 +511,48 @@ 
 
   </table>
 
+  <table name="Sample_Collector" title="Sample_Collector">
+    <column name="id">
+      Sample collector unique id used for differentiating collectors that use
+      the same <code>set_id</code> with different <code>probability</code>
+      values.  The supported value range for IDs is <code>1-255</code>.
+    </column>
+    <column name="name">Name of the sample collector.</column>
+    <column name="probability">
+      Sampling probability for this collector.  It must be an integer number
+      between 0 and 65535.  A value of 0 corresponds to no packets being
+      sampled while a value of 65535 corresponds to all packets being sampled.
+    </column>
+    <column name="set_id">
+      The 8-bit integer identifier of the set of of collectors to send
+      packets to. See Flow_Sample_Collector_Set Table in ovs-vswitchd's
+      database schema.
+    </column>
+    <column name="external_ids">
+      See <em>External IDs</em> at the beginning of this document.
+    </column>
+  </table>
+
+  <table name="Sample" title="Sample">
+    <p>
+      This table describes a Sampling configuration. Entries in other tables
+      might be associated with Sample entries to indicate how the sample
+      should be generated.
+
+      For an example, see <ref table="ACL"/>.
+    </p>
+    <column name="collectors">
+      A list of references to <ref table="Sample_Collector"/> records to be
+      used when generating samples (e.g., IPFIX).  A sample can be sent to
+      multiple collectors simultaneously.
+    </column>
+    <column name="metadata">
+      Will be used as Observation Point ID in every sample.  The Observation
+      Domain ID will be generated by ovn-northd and includes the logical
+      datapath key as the least significant 24 bits and the sampling
+      application type (e.g., drop debugging) as the 8 most significant bits.
+    </column>
+  </table>
   <table name="Copp" title="Control plane protection">
     <p>
       This table is used to define control plane protection policies, i.e.,
@@ -2342,6 +2384,12 @@  or
         created only for allowed connections so the label is valid only
         for allow and allow-related actions.
       </p>
+
+      <p>
+        Note: if an ACL has both sampling enabled and a label associated to it
+        then the label value overrides the observation point ID defined in the
+        <code>sample_new</code> or <code>sample_est</code> configuration.
+      </p>
     </column>
     <column name="priority">
       <p>
@@ -2551,6 +2599,33 @@  or
       </column>
     </group>
 
+    <column name="sample_new">
+      <p>
+        The entry in the <ref table="Sample"/> table to use for sampling for
+        new sessions matched by this ACL.  In case the ACL is stateless
+        this is used for sampling all traffic matched by the ACL.
+      </p>
+
+      <p>
+        Note: if an ACL has both sampling enabled and a label associated to it
+        then the label value overrides the observation point ID defined in the
+        <code>sample_new</code> configuration.
+      </p>
+    </column>
+
+    <column name="sample_est">
+      <p>
+        The entry in the <ref table="Sample"/> table to use for sampling for
+        established/related sessions matched by this ACL.
+      </p>
+
+      <p>
+        Note: if an ACL has both sampling enabled and a label associated to it
+        then the label value overrides the observation point ID defined in the
+        <code>sample_est</code> configuration.
+      </p>
+    </column>
+
     <group title="Common Columns">
       <column name="options">
         This column provides general key/value settings. The supported
diff --git a/tests/atlocal.in b/tests/atlocal.in
index 32d1c374ea..29e1bb2982 100644
--- a/tests/atlocal.in
+++ b/tests/atlocal.in
@@ -196,6 +196,12 @@  find_command bfdd-beacon
 # Set HAVE_ARPING
 find_command arping
 
+# Set HAVE_NFCAPD
+find_command nfcapd
+
+# Set HAVE_NFDUMP
+find_command nfdump
+
 # Turn off proxies.
 unset http_proxy
 unset https_proxy
diff --git a/tests/ovn-controller.at b/tests/ovn-controller.at
index 74bff9035a..50da0de19c 100644
--- a/tests/ovn-controller.at
+++ b/tests/ovn-controller.at
@@ -944,7 +944,7 @@  ovn-appctl -t ovn-controller vlog/set file:dbg
 
 # Get the OF table numbers
 acl_eval=$(ovn-debug lflow-stage-to-oftable ls_out_acl_eval)
-acl_action=$(ovn-debug lflow-stage-to-oftable ls_out_acl_action)
+acl_sample=$(ovn-debug lflow-stage-to-oftable ls_out_acl_sample)
 
 dp_key=$(printf "%x" $(fetch_column datapath tunnel_key external_ids:name=ls1))
 port_key=$(printf "%x" $(fetch_column port_binding tunnel_key logical_port=ls1-lp1))
@@ -965,9 +965,9 @@  for i in $(seq 10); do
     if test "$i" = 3; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     fi
     AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval | grep -c "priority=1100"], [0], [$i
@@ -987,7 +987,7 @@  for i in $(seq 10); do
     if test "$i" = 9; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}'], [0], [dnl
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     fi
     if test "$i" = 10; then
@@ -1013,12 +1013,12 @@  for i in $(seq 10); do
     if test "$i" = 3; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.1.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.1.2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.1.3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.1.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.1.2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.1.3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     fi
     AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval | grep -c "priority=1100"], [0], [$(($i * 2))
@@ -1121,7 +1121,7 @@  ovn-appctl -t ovn-controller vlog/set file:dbg
 
 # Get the OF table numbers
 acl_eval=$(ovn-debug lflow-stage-to-oftable ls_out_acl_eval)
-acl_action=$(ovn-debug lflow-stage-to-oftable ls_out_acl_action)
+acl_sample=$(ovn-debug lflow-stage-to-oftable ls_out_acl_sample)
 
 dp_key=$(printf "%x" $(fetch_column datapath tunnel_key external_ids:name=ls1))
 port_key=$(printf "%x" $(fetch_column port_binding tunnel_key logical_port=ls1-lp1))
@@ -1142,9 +1142,9 @@  for i in $(seq 10); do
     if test "$i" = 1; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,tp_dst=111 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,tp_dst=222 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,tp_dst=333 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,tp_dst=111 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,tp_dst=222 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,tp_dst=333 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     else
         # (1 conj_id flow + 3 tp_dst flows) = 4 extra flows
@@ -1157,7 +1157,7 @@  priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,tp_dst=33
             grep -v reply | awk '{print $7, $8}' | \
             sed -r 's/conjunction.*,/conjunction,/' | \
             sed -r 's/conj_id=.*,metadata/conj_id=,metadata/' | sort], [0], [dnl
-priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1 actions=conjunction,1/2)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.2 actions=conjunction,1/2)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.3 actions=conjunction,1/2)
@@ -1184,9 +1184,9 @@  for i in $(seq 10); do
         # no conjunction left
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10,tp_dst=111 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10,tp_dst=222 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10,tp_dst=333 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10,tp_dst=111 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10,tp_dst=222 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10,tp_dst=333 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     else
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval | grep -c "priority=1100"], [0], [$((14 - $i))
@@ -1209,7 +1209,7 @@  for i in $(seq 10); do
             grep -v reply | awk '{print $7, $8}' | \
             sed -r 's/conjunction.*,/conjunction,/' | \
             sed -r 's/conj_id=.*,metadata/conj_id=,metadata/' | sort], [0], [dnl
-priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1 actions=conjunction,1/2)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.2 actions=conjunction,1/2)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.3 actions=conjunction,1/2)
@@ -1319,7 +1319,7 @@  ovn-appctl -t ovn-controller vlog/set file:dbg
 
 # Get the OF table numbers
 acl_eval=$(ovn-debug lflow-stage-to-oftable ls_out_acl_eval)
-acl_action=$(ovn-debug lflow-stage-to-oftable ls_out_acl_action)
+acl_sample=$(ovn-debug lflow-stage-to-oftable ls_out_acl_sample)
 
 dp_key=$(printf "%x" $(fetch_column datapath tunnel_key external_ids:name=ls1))
 port_key=$(printf "%x" $(fetch_column port_binding tunnel_key logical_port=ls1-lp1))
@@ -1343,7 +1343,7 @@  for i in $(seq 10); do
     if test "$i" = 1; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,nw_dst=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,nw_dst=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     else
         # (1 conj_id + nw_src * i + nw_dst * i) = 1 + i*2 flows
@@ -1356,7 +1356,7 @@  priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,nw_dst=10.
             grep -v reply | awk '{print $7, $8}' | \
             sed -r 's/conjunction.*,/conjunction,/' | \
             sed -r 's/conj_id=.*,metadata/conj_id=,metadata/' | sort], [0], [dnl
-priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.6 actions=conjunction,1/2)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.7 actions=conjunction,1/2)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.8 actions=conjunction,1/2)
@@ -1385,7 +1385,7 @@  for i in $(seq 10); do
         # no conjunction left
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10,nw_dst=10.0.0.15 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10,nw_dst=10.0.0.15 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     else
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval | grep -c "priority=1100"], [0], [$((21 - $i*2))
@@ -1411,9 +1411,9 @@  for i in $(seq 2 10); do
     if test "$i" = 3; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,nw_dst=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.2,nw_dst=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.3,nw_dst=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,nw_dst=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.2,nw_dst=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.3,nw_dst=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     fi
     AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval | grep -c "priority=1100"], [0], [$i
@@ -1437,8 +1437,8 @@  for i in $(seq 10); do
     if test "$i" = 9; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}'], [0], [dnl
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10,nw_dst=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10,nw_dst=10.0.0.7 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10,nw_dst=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10,nw_dst=10.0.0.7 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     elif test "$i" = 10; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval | grep "priority=1100"], [1], [ignore])
@@ -1478,7 +1478,7 @@  ovn-appctl -t ovn-controller vlog/set file:dbg
 
 # Get the OF table numbers
 acl_eval=$(ovn-debug lflow-stage-to-oftable ls_out_acl_eval)
-acl_action=$(ovn-debug lflow-stage-to-oftable ls_out_acl_action)
+acl_sample=$(ovn-debug lflow-stage-to-oftable ls_out_acl_sample)
 
 dp_key=$(printf "%x" $(fetch_column datapath tunnel_key external_ids:name=ls1))
 port_key=$(printf "%x" $(fetch_column port_binding tunnel_key logical_port=ls1-lp1))
@@ -1504,8 +1504,8 @@  for i in $(seq 10); do
     if test "$i" = 1; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     else
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval | grep -c "priority=1100"], [0], [$(($i*2))
@@ -1517,12 +1517,12 @@  priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1 actions=lo
             grep -v reply | awk '{print $7, $8}' | \
             sed -r 's/conjunction.*,/conjunction,/' | \
             sed -r 's/conj_id=.*,/conj_id=,/' | sort], [0], [dnl
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.7 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.8 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.7 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.8 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     fi
 done
@@ -1578,7 +1578,7 @@  ovn-appctl -t ovn-controller vlog/set file:dbg
 
 # Get the OF table numbers
 acl_eval=$(ovn-debug lflow-stage-to-oftable ls_out_acl_eval)
-acl_action=$(ovn-debug lflow-stage-to-oftable ls_out_acl_action)
+acl_sample=$(ovn-debug lflow-stage-to-oftable ls_out_acl_sample)
 
 dp_key=$(printf "%x" $(fetch_column datapath tunnel_key external_ids:name=ls1))
 port_key=$(printf "%x" $(fetch_column port_binding tunnel_key logical_port=ls1-lp1))
@@ -1604,8 +1604,8 @@  for i in $(seq 10); do
     if test "$i" = 1; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     elif test "$i" -lt 6; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval | grep -c "priority=1100"], [0], [$(($i*2))
@@ -1620,12 +1620,12 @@  priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.6 actions=lo
             grep -v reply | awk '{print $7, $8}' | \
             sed -r 's/conjunction.*,/conjunction,/' | \
             sed -r 's/conj_id=.*,/conj_id=,/' | sort], [0], [dnl
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.7 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.8 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.6 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.7 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.8 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     fi
 done
@@ -1687,7 +1687,7 @@  ovn-appctl -t ovn-controller vlog/set file:dbg
 
 # Get the OF table numbers
 acl_eval=$(ovn-debug lflow-stage-to-oftable ls_out_acl_eval)
-acl_action=$(ovn-debug lflow-stage-to-oftable ls_out_acl_action)
+acl_sample=$(ovn-debug lflow-stage-to-oftable ls_out_acl_sample)
 
 dp_key=$(printf "%x" $(fetch_column datapath tunnel_key external_ids:name=ls1))
 port_key=$(printf "%x" $(fetch_column port_binding tunnel_key logical_port=ls1-lp1))
@@ -1708,7 +1708,7 @@  for i in $(seq 10); do
     if test "$i" = 1; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,nw_dst=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,nw_dst=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     else
         # (1 conj_id + nw_src * i + nw_dst * i) = 1 + i*2 flows
@@ -1721,7 +1721,7 @@  priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.1,nw_dst=10.
             grep -v reply | awk '{print $7, $8}' | \
             sed -r 's/conjunction.*,/conjunction,/' | \
             sed -r 's/conj_id=.*,metadata/conj_id=,metadata/' | sort], [0], [dnl
-priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.1 actions=conjunction,1/2)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.2 actions=conjunction,1/2)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.3 actions=conjunction,1/2)
@@ -1748,7 +1748,7 @@  for i in $(seq 10); do
         # no conjunction left
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10,nw_dst=10.0.0.10 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.10,nw_dst=10.0.0.10 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     else
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval | grep -c "priority=1100"], [0], [$((21 - $i*2))
@@ -1771,7 +1771,7 @@  for i in $(seq 10); do
             grep -v reply | awk '{print $7, $8}' | \
             sed -r 's/conjunction.*,/conjunction,/' | \
             sed -r 's/conj_id=.*,metadata/conj_id=,metadata/' | sort], [0], [dnl
-priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.1 actions=conjunction,1/2)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.2 actions=conjunction,1/2)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.3 actions=conjunction,1/2)
@@ -1811,7 +1811,7 @@  AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key
     grep -v reply | awk '{print $7, $8}' | \
     sed -r 's/conjunction.*,/conjunction,/' | \
     sed -r 's/conj_id=.*,metadata/conj_id=,metadata/' | sort], [0], [dnl
-priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.1 actions=conjunction,1/2)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.2 actions=conjunction,1/2)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.3 actions=conjunction,1/2)
@@ -1835,7 +1835,7 @@  AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key
     grep -v reply | awk '{print $7, $8}' | \
     sed -r 's/conjunction.*,/conjunction,/' | \
     sed -r 's/conj_id=.*,metadata/conj_id=,metadata/' | sort], [0], [dnl
-priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.1 actions=conjunction,1/2)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.2 actions=conjunction,1/2)
 priority=1100,ip,reg15=0x$port_key,metadata=0x$dp_key,nw_dst=10.0.0.3 actions=conjunction,1/2)
@@ -1874,7 +1874,7 @@  ovn-appctl -t ovn-controller vlog/set file:dbg
 
 # Get the OF table numbers
 acl_eval=$(ovn-debug lflow-stage-to-oftable ls_out_acl_eval)
-acl_action=$(ovn-debug lflow-stage-to-oftable ls_out_acl_action)
+acl_sample=$(ovn-debug lflow-stage-to-oftable ls_out_acl_sample)
 
 dp_key=$(printf "%x" $(fetch_column datapath tunnel_key external_ids:name=ls1))
 port_key=$(printf "%x" $(fetch_column port_binding tunnel_key logical_port=ls1-lp1))
@@ -1897,8 +1897,8 @@  AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key
     grep -v reply | awk '{print $7, $8}' | \
     sed -r 's/conjunction.[[0-9]]*,/conjunction,/g' | \
     sed -r 's/conj_id=.*,metadata/conj_id=,metadata/' | sort], [0], [dnl
-priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.11 actions=conjunction,1/2)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.12 actions=conjunction,1/2)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.13 actions=conjunction,1/2)
@@ -1922,8 +1922,8 @@  AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key
     grep -v reply | awk '{print $7, $8}' | \
     sed -r 's/conjunction.[[0-9]]*,/conjunction,/g' | \
     sed -r 's/conj_id=.*,metadata/conj_id=,metadata/' | sort], [0], [dnl
-priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.11 actions=conjunction,1/2)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.12 actions=conjunction,1/2)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.13 actions=conjunction,1/2)
@@ -1953,8 +1953,8 @@  AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key
     grep -v reply | awk '{print $7, $8}' | \
     sed -r 's/conjunction.[[0-9]]*,/conjunction,/g' | \
     sed -r 's/conj_id=.*,metadata/conj_id=,metadata/' | sort], [0], [dnl
-priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,conj_id=,metadata=0x$dp_key actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.11 actions=conjunction,1/2)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.12 actions=conjunction,1/2)
 priority=1100,tcp,reg15=0x$port_key,metadata=0x$dp_key,nw_src=10.0.0.13 actions=conjunction,1/2)
@@ -1999,7 +1999,7 @@  ovn-appctl -t ovn-controller vlog/set file:dbg
 
 # Get the OF table numbers
 acl_eval=$(ovn-debug lflow-stage-to-oftable ls_out_acl_eval)
-acl_action=$(ovn-debug lflow-stage-to-oftable ls_out_acl_action)
+acl_sample=$(ovn-debug lflow-stage-to-oftable ls_out_acl_sample)
 
 dp_key=$(printf "%x" $(fetch_column datapath tunnel_key external_ids:name=ls1))
 port_key=$(printf "%x" $(fetch_column port_binding tunnel_key logical_port=ls1-lp1))
@@ -2020,9 +2020,9 @@  for i in $(seq 5); do
     if test "$i" = 3; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,reg15=0x$port_key,metadata=0x$dp_key,dl_src=aa:aa:aa:aa:aa:01 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,reg15=0x$port_key,metadata=0x$dp_key,dl_src=aa:aa:aa:aa:aa:02 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,reg15=0x$port_key,metadata=0x$dp_key,dl_src=aa:aa:aa:aa:aa:03 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,reg15=0x$port_key,metadata=0x$dp_key,dl_src=aa:aa:aa:aa:aa:01 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,reg15=0x$port_key,metadata=0x$dp_key,dl_src=aa:aa:aa:aa:aa:02 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,reg15=0x$port_key,metadata=0x$dp_key,dl_src=aa:aa:aa:aa:aa:03 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     fi
     AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval | grep -c "priority=1100"], [0], [$i
@@ -2043,7 +2043,7 @@  for i in $(seq 5); do
     if test "$i" = 4; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}'], [0], [dnl
-priority=1100,reg15=0x$port_key,metadata=0x$dp_key,dl_src=aa:aa:aa:aa:aa:05 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,reg15=0x$port_key,metadata=0x$dp_key,dl_src=aa:aa:aa:aa:aa:05 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     fi
     if test "$i" = 5; then
@@ -2084,7 +2084,7 @@  ovn-appctl -t ovn-controller vlog/set file:dbg
 
 # Get the OF table numbers
 acl_eval=$(ovn-debug lflow-stage-to-oftable ls_out_acl_eval)
-acl_action=$(ovn-debug lflow-stage-to-oftable ls_out_acl_action)
+acl_sample=$(ovn-debug lflow-stage-to-oftable ls_out_acl_sample)
 
 dp_key=$(printf "%x" $(fetch_column datapath tunnel_key external_ids:name=ls1))
 port_key=$(printf "%x" $(fetch_column port_binding tunnel_key logical_port=ls1-lp1))
@@ -2105,9 +2105,9 @@  for i in $(seq 5); do
     if test "$i" = 3; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,ipv6,reg15=0x$port_key,metadata=0x$dp_key,ipv6_src=ff::1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ipv6,reg15=0x$port_key,metadata=0x$dp_key,ipv6_src=ff::2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ipv6,reg15=0x$port_key,metadata=0x$dp_key,ipv6_src=ff::3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ipv6,reg15=0x$port_key,metadata=0x$dp_key,ipv6_src=ff::1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ipv6,reg15=0x$port_key,metadata=0x$dp_key,ipv6_src=ff::2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ipv6,reg15=0x$port_key,metadata=0x$dp_key,ipv6_src=ff::3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     fi
     AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval | grep -c "priority=1100"], [0], [$i
@@ -2127,7 +2127,7 @@  for i in $(seq 5); do
     if test "$i" = 4; then
         AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | \
             grep -v reply | awk '{print $7, $8}'], [0], [dnl
-priority=1100,ipv6,reg15=0x$port_key,metadata=0x$dp_key,ipv6_src=ff::5 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ipv6,reg15=0x$port_key,metadata=0x$dp_key,ipv6_src=ff::5 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
     fi
     if test "$i" = 5; then
@@ -2167,7 +2167,7 @@  ovn-appctl -t ovn-controller vlog/set file:dbg
 
 # Get the OF table numbers
 acl_eval=$(ovn-debug lflow-stage-to-oftable ls_out_acl_eval)
-acl_action=$(ovn-debug lflow-stage-to-oftable ls_out_acl_action)
+acl_sample=$(ovn-debug lflow-stage-to-oftable ls_out_acl_sample)
 
 ovn-nbctl create address_set name=as1 addresses=8.8.8.8
 check ovn-nbctl acl-add ls1 to-lport 100 'outport == "ls1-lp1" && ip4.src == $as1' drop
@@ -2939,7 +2939,7 @@  ovn-appctl -t ovn-controller vlog/set file:dbg
 
 # Get the OF table numbers
 acl_eval=$(ovn-debug lflow-stage-to-oftable ls_out_acl_eval)
-acl_action=$(ovn-debug lflow-stage-to-oftable ls_out_acl_action)
+acl_sample=$(ovn-debug lflow-stage-to-oftable ls_out_acl_sample)
 
 dp_key=$(printf "%x" $(fetch_column datapath tunnel_key external_ids:name=ls1))
 port_key=$(printf "%x" $(fetch_column port_binding tunnel_key logical_port=ls1-lp1))
@@ -2950,7 +2950,7 @@  check ovn-nbctl add address_set as1 addresses 10.0.0.0/24
 check ovn-nbctl --wait=hv sync
 
 AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.0/24 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.0/24 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
 
 check ovn-nbctl add address_set as1 addresses 10.0.0.1
@@ -2960,22 +2960,22 @@  check ovn-nbctl add address_set as1 addresses 10.0.0.4
 check ovn-nbctl --wait=hv sync
 
 AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.0/24 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.4 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.0/24 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.4 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
 
 
 check ovn-appctl inc-engine/recompute
 
 AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval,reg15=0x$port_key | grep -v reply | awk '{print $7, $8}' | sort], [0], [dnl
-priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.0/24 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
-priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.4 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_action)
+priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.0/24 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.2 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.3 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
+priority=1100,ip,reg15=0x1,metadata=0x1,nw_src=10.0.0.4 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$acl_sample)
 ])
 
 OVN_CLEANUP([hv1])
diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at
index a7a59a9124..eca642a67a 100644
--- a/tests/ovn-macros.at
+++ b/tests/ovn-macros.at
@@ -1129,6 +1129,10 @@  ovn_strip_lflows() {
      sed 's/table=[[0-9]]\{1,2\}\s\?/table=??/g' | sort
 }
 
+ovn_strip_collector_set() {
+    sed 's/collector_set=[[0-9]]*,\?/collector_set=??,/g'
+}
+
 OVS_END_SHELL_HELPERS
 
 m4_define([OVN_POPULATE_ARP], [AT_CHECK(ovn_populate_arp__, [0], [ignore])])
@@ -1189,11 +1193,11 @@  m4_define([OVN_CHECK_SCAPY_EDNS_CLIENT_SUBNET_SUPPORT],
 
 m4_define([OFTABLE_PHY_TO_LOG], [0])
 m4_define([OFTABLE_LOG_INGRESS_PIPELINE], [8])
-m4_define([OFTABLE_OUTPUT_LARGE_PKT_DETECT], [37])
-m4_define([OFTABLE_OUTPUT_LARGE_PKT_PROCESS], [38])
-m4_define([OFTABLE_REMOTE_OUTPUT], [39])
-m4_define([OFTABLE_LOCAL_OUTPUT], [40])
-m4_define([OFTABLE_LOG_EGRESS_PIPELINE], [42])
+m4_define([OFTABLE_OUTPUT_LARGE_PKT_DETECT], [40])
+m4_define([OFTABLE_OUTPUT_LARGE_PKT_PROCESS], [41])
+m4_define([OFTABLE_REMOTE_OUTPUT], [42])
+m4_define([OFTABLE_LOCAL_OUTPUT], [43])
+m4_define([OFTABLE_LOG_EGRESS_PIPELINE], [45])
 m4_define([OFTABLE_SAVE_INPORT], [64])
 m4_define([OFTABLE_LOG_TO_PHY], [65])
 m4_define([OFTABLE_MAC_BINDING], [66])
diff --git a/tests/ovn-nbctl.at b/tests/ovn-nbctl.at
index 797ee0b45e..2efa13b938 100644
--- a/tests/ovn-nbctl.at
+++ b/tests/ovn-nbctl.at
@@ -2803,6 +2803,42 @@  check_row_count nb:ACL 0
 
 dnl ---------------------------------------------------------------------
 
+OVN_NBCTL_TEST([acl_sampling], [ACL sampling operations], [
+check ovn-nbctl ls-add ls
+ovn-nbctl \
+  --id=@sample1 create Sample metadata=4301 -- \
+  --sample-new=@sample1 acl-add ls from-lport 1 1 allow-related
+sample1=$(fetch_column nb:Sample _uuid metadata=4301)
+check_column "$sample1" nb:ACL sample_new priority=1
+
+ovn-nbctl \
+  --id=@sample2 create Sample metadata=4302 -- \
+  --sample-est=@sample2 acl-add ls from-lport 2 1 allow-related
+sample2=$(fetch_column nb:Sample _uuid metadata=4302)
+check_column "$sample2" nb:ACL sample_est priority=2
+
+ovn-nbctl \
+  --id=@sample3 create Sample metadata=4303 -- \
+  --id=@sample4 create Sample metadata=4304 -- \
+  --sample-new=@sample3 --sample-est=@sample4 acl-add ls from-lport 3 1 allow-related
+sample3=$(fetch_column nb:Sample _uuid metadata=4303)
+sample4=$(fetch_column nb:Sample _uuid metadata=4304)
+check_column "$sample3" nb:ACL sample_new priority=3
+check_column "$sample4" nb:ACL sample_est priority=3
+
+dnl Check invalid sample_new and sample_est values.
+AT_CHECK([ovn-nbctl --sample-new=foo acl-add ls from-lport 4 1 allow-related], [1], [], [dnl
+ovn-nbctl: invalid --sample-new: "foo" is not a valid UUID
+])
+
+AT_CHECK([ovn-nbctl --sample-est=bar acl-add ls from-lport 4 1 allow-related], [1], [], [dnl
+ovn-nbctl: invalid --sample-est: "bar" is not a valid UUID
+])
+
+])
+
+dnl ---------------------------------------------------------------------
+
 AT_SETUP([ovn-nbctl - daemon retry connection])
 OVN_NBCTL_TEST_START daemon
 pid=$(cat ovsdb-server.pid)
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index ebf02ef10a..6cc372b8a4 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -4609,7 +4609,7 @@  check_stateful_flows() {
     AT_CHECK([grep "ls_in_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_stateful     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; }; next;)
-  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; }; next;)
+  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; }; next;)
 ])
 
     AT_CHECK_UNQUOTED([grep "ls_out_pre_lb" sw0flows | ovn_strip_lflows], [0], [dnl
@@ -4633,7 +4633,7 @@  check_stateful_flows() {
     AT_CHECK([grep "ls_out_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_out_stateful    ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; }; next;)
-  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; }; next;)
+  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; }; next;)
 ])
 }
 
@@ -4676,7 +4676,7 @@  AT_CHECK([grep "ls_in_lb " sw0flows | ovn_strip_lflows], [0], [dnl
 AT_CHECK([grep "ls_in_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_stateful     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; }; next;)
-  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; }; next;)
+  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; }; next;)
 ])
 
 AT_CHECK([grep "ls_out_pre_lb" sw0flows | ovn_strip_lflows], [0], [dnl
@@ -4697,7 +4697,7 @@  AT_CHECK([grep "ls_out_pre_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
 AT_CHECK([grep "ls_out_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_out_stateful    ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; }; next;)
-  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; }; next;)
+  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; }; next;)
 ])
 
 # LB with event=false and reject=false
@@ -4726,23 +4726,23 @@  ovn-sbctl dump-flows sw0 > sw0flows
 AT_CAPTURE_FILE([sw0flows])
 
 AT_CHECK([grep -w "ls_in_acl_eval" sw0flows | grep 2002 | ovn_strip_lflows], [0], [dnl
-  table=??(ls_in_acl_eval     ), priority=2002 , match=(reg0[[7]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; next;)
-  table=??(ls_in_acl_eval     ), priority=2002 , match=(reg0[[8]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; next;)
+  table=??(ls_in_acl_eval     ), priority=2002 , match=(reg0[[7]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; reg9 = 1234; next;)
+  table=??(ls_in_acl_eval     ), priority=2002 , match=(reg0[[8]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; reg9 = 1234; next;)
 ])
 AT_CHECK([grep "ls_in_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_stateful     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; }; next;)
-  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; }; next;)
+  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; }; next;)
 ])
 
 AT_CHECK([grep -w "ls_out_acl_eval" sw0flows | grep 2002 | ovn_strip_lflows], [0], [dnl
-  table=??(ls_out_acl_eval    ), priority=2002 , match=(reg0[[7]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; next;)
-  table=??(ls_out_acl_eval    ), priority=2002 , match=(reg0[[8]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; next;)
+  table=??(ls_out_acl_eval    ), priority=2002 , match=(reg0[[7]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; reg9 = 1234; next;)
+  table=??(ls_out_acl_eval    ), priority=2002 , match=(reg0[[8]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; reg9 = 1234; next;)
 ])
 AT_CHECK([grep "ls_out_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_out_stateful    ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; }; next;)
-  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; }; next;)
+  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; }; next;)
 ])
 
 # Add new ACL without label
@@ -4753,27 +4753,27 @@  ovn-sbctl dump-flows sw0 > sw0flows
 AT_CAPTURE_FILE([sw0flows])
 
 AT_CHECK([grep -w "ls_in_acl_eval" sw0flows | grep 2002 | ovn_strip_lflows], [0], [dnl
-  table=??(ls_in_acl_eval     ), priority=2002 , match=(reg0[[7]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; next;)
+  table=??(ls_in_acl_eval     ), priority=2002 , match=(reg0[[7]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; reg9 = 1234; next;)
   table=??(ls_in_acl_eval     ), priority=2002 , match=(reg0[[7]] == 1 && (udp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; next;)
-  table=??(ls_in_acl_eval     ), priority=2002 , match=(reg0[[8]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; next;)
+  table=??(ls_in_acl_eval     ), priority=2002 , match=(reg0[[8]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; reg9 = 1234; next;)
   table=??(ls_in_acl_eval     ), priority=2002 , match=(reg0[[8]] == 1 && (udp)), action=(reg8[[16]] = 1; next;)
 ])
 AT_CHECK([grep "ls_in_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_stateful     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; }; next;)
-  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; }; next;)
+  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; }; next;)
 ])
 
 AT_CHECK([grep -w "ls_out_acl_eval" sw0flows | grep 2002 | ovn_strip_lflows], [0], [dnl
-  table=??(ls_out_acl_eval    ), priority=2002 , match=(reg0[[7]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; next;)
+  table=??(ls_out_acl_eval    ), priority=2002 , match=(reg0[[7]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; reg9 = 1234; next;)
   table=??(ls_out_acl_eval    ), priority=2002 , match=(reg0[[7]] == 1 && (udp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; next;)
-  table=??(ls_out_acl_eval    ), priority=2002 , match=(reg0[[8]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; next;)
+  table=??(ls_out_acl_eval    ), priority=2002 , match=(reg0[[8]] == 1 && (tcp)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 1234; reg9 = 1234; next;)
   table=??(ls_out_acl_eval    ), priority=2002 , match=(reg0[[8]] == 1 && (udp)), action=(reg8[[16]] = 1; next;)
 ])
 AT_CHECK([grep "ls_out_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_out_stateful    ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; }; next;)
-  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; }; next;)
+  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; }; next;)
 ])
 
 # Delete new ACL with label
@@ -4790,7 +4790,7 @@  AT_CHECK([grep -w "ls_in_acl_eval" sw0flows | grep 2002 | ovn_strip_lflows], [0]
 AT_CHECK([grep "ls_in_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_stateful     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; }; next;)
-  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; }; next;)
+  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; }; next;)
 ])
 
 AT_CHECK([grep -w "ls_out_acl_eval" sw0flows | grep 2002 | ovn_strip_lflows], [0], [dnl
@@ -4800,7 +4800,7 @@  AT_CHECK([grep -w "ls_out_acl_eval" sw0flows | grep 2002 | ovn_strip_lflows], [0
 AT_CHECK([grep "ls_out_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_out_stateful    ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; }; next;)
-  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; }; next;)
+  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; }; next;)
 ])
 AT_CLEANUP
 ])
@@ -4828,7 +4828,7 @@  check ovn-nbctl --wait=sb -- acl-del ls -- --label=1234 acl-add ls from-lport 1
 
 dnl Check that the label is committed to conntrack in the ingress pipeline
 AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --ct new ls "$flow" | grep -e ls_in_stateful -A 2 | grep commit], [0], [dnl
-    ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; };
+    ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; };
 ])
 
 AS_BOX([from-lport --apply-after-lb allow-related ACL])
@@ -4836,7 +4836,7 @@  check ovn-nbctl --wait=sb -- acl-del ls -- --apply-after-lb --label=1234 acl-add
 
 dnl Check that the label is committed to conntrack in the ingress pipeline
 AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --ct new ls "$flow" | grep -e ls_in_stateful -A 2 | grep commit], [0], [dnl
-    ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; };
+    ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; };
 ])
 
 AS_BOX([to-lport allow-related ACL])
@@ -4844,7 +4844,7 @@  check ovn-nbctl --wait=sb -- acl-del ls -- --label=1234 acl-add ls to-lport 1 ip
 
 dnl Check that the label is committed to conntrack in the ingress pipeline
 AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --ct new ls "$flow" | grep -e ls_out_stateful -A 2 | grep commit], [0], [dnl
-    ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; };
+    ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; };
 ])
 
 AT_CLEANUP
@@ -7680,7 +7680,7 @@  AT_CHECK([grep -e "ls_in_lb " lsflows | ovn_strip_lflows], [0], [dnl
 AT_CHECK([grep -e "ls_in_stateful" lsflows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_stateful     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; }; next;)
-  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; }; next;)
+  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; }; next;)
 ])
 
 AS_BOX([Remove and add the ACLs back with the apply-after-lb option])
@@ -7735,7 +7735,7 @@  AT_CHECK([grep -e "ls_in_lb " lsflows | ovn_strip_lflows], [0], [dnl
 AT_CHECK([grep -e "ls_in_stateful" lsflows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_stateful     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; }; next;)
-  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; }; next;)
+  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; }; next;)
 ])
 
 AS_BOX([Remove and add the ACLs back with a few ACLs with apply-after-lb option])
@@ -7790,7 +7790,7 @@  AT_CHECK([grep -e "ls_in_lb " lsflows | ovn_strip_lflows], [0], [dnl
 AT_CHECK([grep -e "ls_in_stateful" lsflows | ovn_strip_lflows], [0], [dnl
   table=??(ls_in_stateful     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; }; next;)
-  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.label = reg3; }; next;)
+  table=??(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1 && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; }; next;)
 ])
 
 AT_CLEANUP
@@ -8069,15 +8069,18 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_action), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=65535, match=(1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=65535, match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.dst == $svc_monitor_mac), action=(next;)
   table=??(ls_out_acl_action  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_acl_eval    ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_eval    ), priority=65535, match=(1), action=(next;)
   table=??(ls_out_acl_hint    ), priority=65535, match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == $svc_monitor_mac), action=(next;)
 ])
@@ -8094,15 +8097,18 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_action), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=65535, match=(1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=65535, match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.dst == $svc_monitor_mac), action=(next;)
   table=??(ls_out_acl_action  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_acl_eval    ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_eval    ), priority=65535, match=(1), action=(next;)
   table=??(ls_out_acl_hint    ), priority=65535, match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == $svc_monitor_mac), action=(next;)
 ])
@@ -8119,15 +8125,18 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_action), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=65535, match=(1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=65535, match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.dst == $svc_monitor_mac), action=(next;)
   table=??(ls_out_acl_action  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_acl_eval    ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_eval    ), priority=65535, match=(1), action=(next;)
   table=??(ls_out_acl_hint    ), priority=65535, match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == $svc_monitor_mac), action=(next;)
 ])
@@ -8154,11 +8163,13 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_action), priority=1000 , match=(reg8[[18]] == 1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=egress,table=??); };)
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=1001 , match=((ip4 && tcp)), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.dst == $svc_monitor_mac), action=(next;)
   table=??(ls_out_acl_action  ), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
@@ -8169,6 +8180,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_out_acl_eval    ), priority=34000, match=(eth.src == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_eval    ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=0    , match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == $svc_monitor_mac), action=(next;)
 ])
@@ -8191,11 +8203,13 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_action), priority=1000 , match=(reg8[[18]] == 1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=egress,table=??); };)
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=1001 , match=((ip4 && tcp)), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.dst == $svc_monitor_mac), action=(next;)
   table=??(ls_out_acl_action  ), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
@@ -8206,6 +8220,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_out_acl_eval    ), priority=34000, match=(eth.src == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_eval    ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=0    , match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == $svc_monitor_mac), action=(next;)
 ])
@@ -8228,11 +8243,13 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_action), priority=1000 , match=(reg8[[18]] == 1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=egress,table=??); };)
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=1001 , match=((ip4 && tcp)), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.dst == $svc_monitor_mac), action=(next;)
   table=??(ls_out_acl_action  ), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; /* drop */)
@@ -8243,6 +8260,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_out_acl_eval    ), priority=34000, match=(eth.src == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_eval    ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=0    , match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == $svc_monitor_mac), action=(next;)
 ])
@@ -8266,6 +8284,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(reg0[[17]] == 1), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=1    , match=(ip && !ct.est), action=(next;)
   table=??(ls_in_acl_eval     ), priority=1    , match=(ip && ct.est && ct_mark.blocked == 1), action=(reg0[[1]] = 1; reg8[[16]] = 1; next;)
@@ -8284,6 +8303,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_hint     ), priority=5    , match=(!ct.trk), action=(reg0[[8]] = 1; reg0[[9]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=6    , match=(!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=7    , match=(ct.new && !ct.est), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=100  , match=(ip), action=(reg0[[0]] = 1; next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(((ip4 && icmp4.type == 3 && icmp4.code == 4) || (ip6 && icmp6.type == 2 && icmp6.code == 0)) && flags.tunnel_rx == 1), action=(next;)
@@ -8310,6 +8330,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_out_acl_hint    ), priority=5    , match=(!ct.trk), action=(reg0[[8]] = 1; reg0[[9]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=6    , match=(!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=7    , match=(ct.new && !ct.est), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=100  , match=(ip), action=(reg0[[0]] = 1; next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.mcast), action=(next;)
@@ -8340,10 +8361,12 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=1001 , match=((ip4 && tcp)), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.dst == $svc_monitor_mac), action=(next;)
   table=??(ls_out_acl_action  ), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
@@ -8354,6 +8377,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_out_acl_eval    ), priority=34000, match=(eth.src == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_eval    ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=0    , match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == $svc_monitor_mac), action=(next;)
 ])
@@ -8377,10 +8401,12 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=1001 , match=((ip4 && tcp)), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.dst == $svc_monitor_mac), action=(next;)
   table=??(ls_out_acl_action  ), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
@@ -8391,6 +8417,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_out_acl_eval    ), priority=34000, match=(eth.src == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_eval    ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=0    , match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == $svc_monitor_mac), action=(next;)
 ])
@@ -8414,10 +8441,12 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=1001 , match=((ip4 && tcp)), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.dst == $svc_monitor_mac), action=(next;)
   table=??(ls_out_acl_action  ), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; /* drop */)
@@ -8428,6 +8457,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_out_acl_eval    ), priority=34000, match=(eth.src == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_eval    ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=0    , match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == $svc_monitor_mac), action=(next;)
 ])
@@ -8453,6 +8483,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_eval), priority=1001 , match=(reg0[[8]] == 1 && (ip4 && tcp)), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(reg0[[17]] == 1), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=1    , match=(ip && !ct.est), action=(next;)
   table=??(ls_in_acl_eval     ), priority=1    , match=(ip && ct.est && ct_mark.blocked == 1), action=(reg0[[1]] = 1; reg8[[16]] = 1; next;)
@@ -8469,6 +8500,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_hint     ), priority=5    , match=(!ct.trk), action=(reg0[[8]] = 1; reg0[[9]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=6    , match=(!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=7    , match=(ct.new && !ct.est), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=100  , match=(ip), action=(reg0[[0]] = 1; next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(((ip4 && icmp4.type == 3 && icmp4.code == 4) || (ip6 && icmp6.type == 2 && icmp6.code == 0)) && flags.tunnel_rx == 1), action=(next;)
@@ -8495,6 +8527,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_out_acl_hint    ), priority=5    , match=(!ct.trk), action=(reg0[[8]] = 1; reg0[[9]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=6    , match=(!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=7    , match=(ct.new && !ct.est), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=100  , match=(ip), action=(reg0[[0]] = 1; next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.mcast), action=(next;)
@@ -8524,10 +8557,12 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_action), priority=1000 , match=(reg8[[18]] == 1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=egress,table=??); };)
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.dst == $svc_monitor_mac), action=(next;)
   table=??(ls_out_acl_action  ), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
@@ -8539,6 +8574,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_out_acl_eval    ), priority=34000, match=(eth.src == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_eval    ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=0    , match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == $svc_monitor_mac), action=(next;)
 ])
@@ -8561,10 +8597,12 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_action), priority=1000 , match=(reg8[[18]] == 1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=egress,table=??); };)
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.dst == $svc_monitor_mac), action=(next;)
   table=??(ls_out_acl_action  ), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
@@ -8576,6 +8614,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_out_acl_eval    ), priority=34000, match=(eth.src == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_eval    ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=0    , match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == $svc_monitor_mac), action=(next;)
 ])
@@ -8598,10 +8637,12 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_action), priority=1000 , match=(reg8[[18]] == 1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=egress,table=??); };)
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_eval     ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.dst == $svc_monitor_mac), action=(next;)
   table=??(ls_out_acl_action  ), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; /* drop */)
@@ -8613,6 +8654,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_out_acl_eval    ), priority=34000, match=(eth.src == $svc_monitor_mac), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_eval    ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=0    , match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == $svc_monitor_mac), action=(next;)
 ])
@@ -8636,6 +8678,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(reg8[[16]] = 1; next;)
   table=??(ls_in_acl_after_lb_eval), priority=65532, match=(reg0[[17]] == 1), action=(reg8[[16]] = 1; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=1    , match=(ip && !ct.est), action=(next;)
   table=??(ls_in_acl_eval     ), priority=1    , match=(ip && ct.est && ct_mark.blocked == 1), action=(reg0[[1]] = 1; reg8[[16]] = 1; next;)
@@ -8652,6 +8695,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_in_acl_hint     ), priority=5    , match=(!ct.trk), action=(reg0[[8]] = 1; reg0[[9]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=6    , match=(!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=7    , match=(ct.new && !ct.est), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_pre_acl      ), priority=100  , match=(ip), action=(reg0[[0]] = 1; next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(((ip4 && icmp4.type == 3 && icmp4.code == 4) || (ip6 && icmp6.type == 2 && icmp6.code == 0)) && flags.tunnel_rx == 1), action=(next;)
@@ -8680,6 +8724,7 @@  AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | ovn_strip_lflows], [0], [
   table=??(ls_out_acl_hint    ), priority=5    , match=(!ct.trk), action=(reg0[[8]] = 1; reg0[[9]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=6    , match=(!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
   table=??(ls_out_acl_hint    ), priority=7    , match=(ct.new && !ct.est), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=100  , match=(ip), action=(reg0[[0]] = 1; next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.mcast), action=(next;)
@@ -9925,8 +9970,10 @@  AT_CHECK([ovn-sbctl dump-flows | grep "ls_in_acl" | grep "match=(1)"  | ovn_stri
   table=??(ls_in_acl_action   ), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
   table=??(ls_in_acl_after_lb_action), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
 ])
 
 check ovn-nbctl --wait=sb acl-del S1
@@ -9940,8 +9987,10 @@  AT_CHECK([ovn-sbctl dump-flows | grep "ls_in_acl" | grep "match=(1)"  | ovn_stri
   table=??(ls_in_acl_action   ), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
   table=??(ls_in_acl_after_lb_action), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
 ])
 
 check ovn-nbctl --wait=sb acl-del S1
@@ -9955,8 +10004,10 @@  AT_CHECK([ovn-sbctl dump-flows | grep "ls_in_acl" | grep "match=(1)"  | ovn_stri
   table=??(ls_in_acl_action   ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_action), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
 ])
 
 
@@ -9968,8 +10019,10 @@  AT_CHECK([ovn-sbctl dump-flows | grep "ls_in_acl" | grep "match=(1)"  | ovn_stri
   table=??(ls_in_acl_action   ), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
   table=??(ls_in_acl_after_lb_action), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
 ])
 
 check ovn-nbctl --wait=sb acl-del S1
@@ -9982,8 +10035,10 @@  AT_CHECK([ovn-sbctl dump-flows | grep "ls_in_acl" | grep "match=(1)"  | ovn_stri
   table=??(ls_in_acl_action   ), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
   table=??(ls_in_acl_after_lb_action), priority=0    , match=(1), action=(reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
   table=??(ls_in_acl_after_lb_eval), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_eval     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
 ])
 
 AT_CLEANUP
@@ -12521,6 +12576,284 @@  AT_CHECK([ovn-sbctl lflow-list | grep ls_in_l2_unknown.*sample | ovn_strip_lflow
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([ACL Sampling])
+AT_KEYWORDS([acl])
+
+ovn_start
+
+collector1=$(ovn-nbctl create Sample_Collector id=1 name=c1 probability=65535 set_id=100)
+collector2=$(ovn-nbctl create Sample_Collector id=2 name=c2 probability=65535 set_id=200)
+check_row_count nb:Sample_Collector 2
+
+ovn-nbctl create Sampling_App type="acl-new" id="42"
+ovn-nbctl create Sampling_App type="acl-est" id="43"
+check_row_count nb:Sampling_App 2
+
+check ovn-nbctl                               \
+  -- ls-add ls                                \
+  -- lsp-add ls lsp1                          \
+  -- lsp-set-addresses lsp1 00:00:00:00:00:01 \
+  -- lsp-add ls lsp2                          \
+  -- lsp-set-addresses lsp2 00:00:00:00:00:02
+check ovn-nbctl --wait=sb sync
+
+base_flow="inport == \"lsp1\" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.src == 42.42.42.1 && ip4.dst == 42.42.42.2"
+m4_define([TRACE_FILTER], [grep -e sample -e commit -e reg9 | grep -v _sample | sort])
+
+AS_BOX([from-lport ACL sampling (new, est)])
+check ovn-nbctl acl-del ls
+ovn-nbctl --wait=sb \
+  --id=@sample1 create Sample collector="$collector1 $collector2" metadata=4301 -- \
+  --id=@sample2 create Sample collector="$collector1 $collector2" metadata=4302 -- \
+  --sample-new=@sample1 --sample-est=@sample2 acl-add ls from-lport 1 "1" allow-related
+AT_CHECK([ovn-sbctl lflow-list | grep -e ls_in_acl_sample -e ls_in_acl_eval -e ls_out_acl_sample | ovn_strip_lflows | ovn_strip_collector_set | grep -e reg3 -e reg9 -e sample], [0], [dnl
+  table=??(ls_in_acl_eval     ), priority=1001 , match=(reg0[[7]] == 1 && (1)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 4301; reg9 = 4302; next;)
+  table=??(ls_in_acl_eval     ), priority=1001 , match=(reg0[[8]] == 1 && (1)), action=(reg8[[16]] = 1; reg0[[13]] = 1; reg3 = 4301; reg9 = 4302; next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=1100 , match=(ip && ct.new && reg3 == 4301), action=(sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4301);sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4301); next;)
+  table=??(ls_in_acl_sample   ), priority=1200 , match=(ip && ct.trk && (ct.est || ct.rel) && !ct.rpl && ct_label.obs_point_id == 4302 && ct_label.obs_unused == 0), action=(sample(probability=65535,collector_set=??,obs_domain=43,obs_point=4302);sample(probability=65535,collector_set=??,obs_domain=43,obs_point=4302); next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=1200 , match=(ip && ct.trk && (ct.est || ct.rel) && ct.rpl && ct_label.obs_point_id == 4302 && ct_label.obs_unused == 0), action=(sample(probability=65535,collector_set=??,obs_domain=43,obs_point=4302);sample(probability=65535,collector_set=??,obs_domain=43,obs_point=4302); next;)
+])
+
+dnl Trace new connections.
+flow="$base_flow"
+AT_CHECK_UNQUOTED([ovn_trace --ct new ls "$flow" | TRACE_FILTER], [0], [dnl
+    ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; };
+    reg9 = 4302;
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4301);
+    sample(probability=65535,collector_set=200,obs_domain=42,obs_point=4301);
+])
+
+dnl Trace estasblished connections.
+flow="$base_flow && ct_label.obs_point_id == 4302"
+AT_CHECK_UNQUOTED([ovn_trace --ct est ls "$flow" | TRACE_FILTER], [0], [dnl
+    reg9 = 4302;
+    sample(probability=65535,collector_set=100,obs_domain=43,obs_point=4302);
+    sample(probability=65535,collector_set=200,obs_domain=43,obs_point=4302);
+])
+
+AS_BOX([from-lport ACL sampling (new)])
+check ovn-nbctl acl-del ls
+ovn-nbctl --wait=sb \
+  --id=@sample1 create Sample collector="$collector1 $collector2" metadata=4301 -- \
+  --sample-new=@sample1 acl-add ls from-lport 1 "1" allow-related
+AT_CHECK([ovn-sbctl lflow-list | grep -e ls_in_acl_sample -e ls_in_acl_eval -e ls_out_acl_sample | ovn_strip_lflows | ovn_strip_collector_set | grep -e reg3 -e reg9 -e sample], [0], [dnl
+  table=??(ls_in_acl_eval     ), priority=1001 , match=(reg0[[7]] == 1 && (1)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 4301; reg9 = 0; next;)
+  table=??(ls_in_acl_eval     ), priority=1001 , match=(reg0[[8]] == 1 && (1)), action=(reg8[[16]] = 1; reg0[[13]] = 1; reg3 = 4301; reg9 = 0; next;)
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=1100 , match=(ip && ct.new && reg3 == 4301), action=(sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4301);sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4301); next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
+])
+
+dnl Trace new connections.
+flow="$base_flow"
+AT_CHECK_UNQUOTED([ovn_trace --ct new ls "$flow" | TRACE_FILTER], [0], [dnl
+    ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; };
+    reg9 = 0;
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4301);
+    sample(probability=65535,collector_set=200,obs_domain=42,obs_point=4301);
+])
+
+dnl Trace established connections (no point id was committed in the label in
+dnl the original direction).
+flow="$base_flow && ct_label.obs_point_id == 0"
+AT_CHECK_UNQUOTED([ovn_trace --ct est ls "$flow" | TRACE_FILTER], [0], [dnl
+    reg9 = 0;
+])
+
+AS_BOX([from-lport-after-lb ACL sampling (new, est)])
+check ovn-nbctl acl-del ls
+ovn-nbctl --wait=sb \
+  --id=@sample1 create Sample collector="$collector1 $collector2" metadata=4301 -- \
+  --id=@sample2 create Sample collector="$collector1 $collector2" metadata=4302 -- \
+  --apply-after-lb --sample-new=@sample1 --sample-est=@sample2 acl-add ls from-lport 1 "1" allow-related
+AT_CHECK([ovn-sbctl lflow-list | grep -e ls_in_acl_after_lb_sample -e ls_in_acl_after_lb_eval -e ls_out_acl_sample | ovn_strip_lflows | ovn_strip_collector_set | grep -e reg3 -e reg9 -e sample], [0], [dnl
+  table=??(ls_in_acl_after_lb_eval), priority=1001 , match=(reg0[[7]] == 1 && (1)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 4301; reg9 = 4302; next;)
+  table=??(ls_in_acl_after_lb_eval), priority=1001 , match=(reg0[[8]] == 1 && (1)), action=(reg8[[16]] = 1; reg0[[13]] = 1; reg3 = 4301; reg9 = 4302; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_after_lb_sample), priority=1100 , match=(ip && ct.new && reg3 == 4301), action=(sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4301);sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4301); next;)
+  table=??(ls_in_acl_after_lb_sample), priority=1200 , match=(ip && ct.trk && (ct.est || ct.rel) && !ct.rpl && ct_label.obs_point_id == 4302 && ct_label.obs_unused == 0), action=(sample(probability=65535,collector_set=??,obs_domain=43,obs_point=4302);sample(probability=65535,collector_set=??,obs_domain=43,obs_point=4302); next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=1200 , match=(ip && ct.trk && (ct.est || ct.rel) && ct.rpl && ct_label.obs_point_id == 4302 && ct_label.obs_unused == 0), action=(sample(probability=65535,collector_set=??,obs_domain=43,obs_point=4302);sample(probability=65535,collector_set=??,obs_domain=43,obs_point=4302); next;)
+])
+
+dnl Trace new connections.
+flow="$base_flow"
+AT_CHECK_UNQUOTED([ovn_trace --ct new ls "$flow" | TRACE_FILTER], [0], [dnl
+    ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; };
+    reg9 = 4302;
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4301);
+    sample(probability=65535,collector_set=200,obs_domain=42,obs_point=4301);
+])
+
+dnl Trace estasblished connections.
+flow="$base_flow && ct_label.obs_point_id == 4302"
+AT_CHECK_UNQUOTED([ovn_trace --ct est ls "$flow" | TRACE_FILTER], [0], [dnl
+    reg9 = 4302;
+    sample(probability=65535,collector_set=100,obs_domain=43,obs_point=4302);
+    sample(probability=65535,collector_set=200,obs_domain=43,obs_point=4302);
+])
+
+AS_BOX([from-lport-after-lb ACL sampling (new)])
+check ovn-nbctl acl-del ls
+ovn-nbctl --wait=sb \
+  --id=@sample1 create Sample collector="$collector1 $collector2" metadata=4301 -- \
+  --apply-after-lb --sample-new=@sample1 acl-add ls from-lport 1 "1" allow-related
+AT_CHECK([ovn-sbctl lflow-list | grep -e ls_in_acl_after_lb_sample -e ls_in_acl_after_lb_eval -e ls_out_acl_sample | ovn_strip_lflows | ovn_strip_collector_set | grep -e reg3 -e reg9 -e sample], [0], [dnl
+  table=??(ls_in_acl_after_lb_eval), priority=1001 , match=(reg0[[7]] == 1 && (1)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 4301; reg9 = 0; next;)
+  table=??(ls_in_acl_after_lb_eval), priority=1001 , match=(reg0[[8]] == 1 && (1)), action=(reg8[[16]] = 1; reg0[[13]] = 1; reg3 = 4301; reg9 = 0; next;)
+  table=??(ls_in_acl_after_lb_sample), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_after_lb_sample), priority=1100 , match=(ip && ct.new && reg3 == 4301), action=(sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4301);sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4301); next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
+])
+
+dnl Trace new connections.
+flow="$base_flow"
+AT_CHECK_UNQUOTED([ovn_trace --ct new ls "$flow" | TRACE_FILTER], [0], [dnl
+    ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; };
+    reg9 = 0;
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4301);
+    sample(probability=65535,collector_set=200,obs_domain=42,obs_point=4301);
+])
+
+dnl Trace established connections (no point id was committed in the label in
+dnl the original direction).
+flow="$base_flow && ct_label.obs_point_id == 0"
+AT_CHECK_UNQUOTED([ovn_trace --ct est ls "$flow" | TRACE_FILTER], [0], [dnl
+    reg9 = 0;
+])
+
+AS_BOX([to-lport ACL sampling (new, est)])
+check ovn-nbctl acl-del ls
+ovn-nbctl --wait=sb \
+  --id=@sample1 create Sample collector="$collector1 $collector2" metadata=4301 -- \
+  --id=@sample2 create Sample collector="$collector1 $collector2" metadata=4302 -- \
+  --sample-new=@sample1 --sample-est=@sample2 acl-add ls to-lport 1 "1" allow-related
+AT_CHECK([ovn-sbctl lflow-list | grep -e ls_out_acl_sample -e ls_out_acl_eval -e ls_in_acl_sample | ovn_strip_lflows | ovn_strip_collector_set | grep -e reg3 -e reg9 -e sample], [0], [dnl
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_sample   ), priority=1200 , match=(ip && ct.trk && (ct.est || ct.rel) && ct.rpl && ct_label.obs_point_id == 4302 && ct_label.obs_unused == 0), action=(sample(probability=65535,collector_set=??,obs_domain=43,obs_point=4302);sample(probability=65535,collector_set=??,obs_domain=43,obs_point=4302); next;)
+  table=??(ls_out_acl_eval    ), priority=1001 , match=(reg0[[7]] == 1 && (1)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 4301; reg9 = 4302; next;)
+  table=??(ls_out_acl_eval    ), priority=1001 , match=(reg0[[8]] == 1 && (1)), action=(reg8[[16]] = 1; reg0[[13]] = 1; reg3 = 4301; reg9 = 4302; next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=1100 , match=(ip && (ct.new || !ct.trk) && reg3 == 4301), action=(sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4301);sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4301); next;)
+  table=??(ls_out_acl_sample  ), priority=1200 , match=(ip && ct.trk && (ct.est || ct.rel) && !ct.rpl && ct_label.obs_point_id == 4302 && ct_label.obs_unused == 0), action=(sample(probability=65535,collector_set=??,obs_domain=43,obs_point=4302);sample(probability=65535,collector_set=??,obs_domain=43,obs_point=4302); next;)
+])
+
+dnl Trace new connections.
+flow="$base_flow"
+AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new ls "$flow" | TRACE_FILTER], [0], [dnl
+    ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; };
+    ct_commit { ct_mark.blocked = 0; };
+    reg9 = 4302;
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4301);
+    sample(probability=65535,collector_set=200,obs_domain=42,obs_point=4301);
+])
+
+dnl Trace estasblished connections.
+flow="$base_flow && ct_label.obs_point_id == 4302"
+AT_CHECK_UNQUOTED([ovn_trace --ct est --ct est ls "$flow" | TRACE_FILTER], [0], [dnl
+    reg9 = 4302;
+    sample(probability=65535,collector_set=100,obs_domain=43,obs_point=4302);
+    sample(probability=65535,collector_set=200,obs_domain=43,obs_point=4302);
+])
+
+AS_BOX([to-lport ACL sampling (new)])
+check ovn-nbctl acl-del ls
+ovn-nbctl --wait=sb \
+  --id=@sample1 create Sample collector="$collector1 $collector2" metadata=4301 -- \
+  --sample-new=@sample1 acl-add ls to-lport 1 "1" allow-related
+AT_CHECK([ovn-sbctl lflow-list | grep -e ls_out_acl_sample -e ls_out_acl_eval -e ls_in_acl_sample | ovn_strip_lflows | ovn_strip_collector_set | grep -e reg3 -e reg9 -e sample], [0], [dnl
+  table=??(ls_in_acl_sample   ), priority=0    , match=(1), action=(next;)
+  table=??(ls_out_acl_eval    ), priority=1001 , match=(reg0[[7]] == 1 && (1)), action=(reg8[[16]] = 1; reg0[[1]] = 1; reg0[[13]] = 1; reg3 = 4301; reg9 = 0; next;)
+  table=??(ls_out_acl_eval    ), priority=1001 , match=(reg0[[8]] == 1 && (1)), action=(reg8[[16]] = 1; reg0[[13]] = 1; reg3 = 4301; reg9 = 0; next;)
+  table=??(ls_out_acl_sample  ), priority=0    , match=(1), action=(next;)
+  table=??(ls_out_acl_sample  ), priority=1100 , match=(ip && (ct.new || !ct.trk) && reg3 == 4301), action=(sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4301);sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4301); next;)
+])
+
+dnl Trace new connections.
+flow="$base_flow"
+AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new ls "$flow" | TRACE_FILTER], [0], [dnl
+    ct_commit { ct_mark.blocked = 0; ct_label.obs_point_id = reg9; };
+    ct_commit { ct_mark.blocked = 0; };
+    reg9 = 0;
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4301);
+    sample(probability=65535,collector_set=200,obs_domain=42,obs_point=4301);
+])
+
+dnl Trace established connections (no point id was committed in the label in
+dnl the original direction).
+flow="$base_flow && ct_label.obs_point_id == 0"
+AT_CHECK_UNQUOTED([ovn_trace --ct est --ct est ls "$flow" | TRACE_FILTER], [0], [dnl
+    reg9 = 0;
+])
+
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([ACL Sampling - same collector set id, multiple probabilities])
+AT_KEYWORDS([acl])
+
+ovn_start
+
+collector1=$(ovn-nbctl create Sample_Collector id=1 name=c1 probability=10000 set_id=100)
+collector2=$(ovn-nbctl create Sample_Collector id=2 name=c2 probability=20000 set_id=100)
+check_row_count nb:Sample_Collector 2
+
+ovn-nbctl create Sampling_App type="acl-new" id="42"
+ovn-nbctl create Sampling_App type="acl-est" id="43"
+check_row_count nb:Sampling_App 2
+
+check ovn-nbctl                               \
+  -- ls-add ls                                \
+  -- lsp-add ls lsp1                          \
+  -- lsp-set-addresses lsp1 00:00:00:00:00:01 \
+  -- lsp-add ls lsp2                          \
+  -- lsp-set-addresses lsp2 00:00:00:00:00:02
+
+ovn-nbctl --wait=sb \
+  --id=@sample1 create Sample collector="$collector1" metadata=4301 -- \
+  --id=@sample2 create Sample collector="$collector2" metadata=4302 -- \
+  --sample-new=@sample1 --sample-est=@sample2 acl-add ls from-lport 1 "1" allow-related
+ovn-nbctl --wait=sb \
+  --id=@sample1 create Sample collector="$collector1" metadata=4303 -- \
+  --id=@sample2 create Sample collector="$collector2" metadata=4304 -- \
+  --apply-after-lb --sample-new=@sample1 --sample-est=@sample2 acl-add ls from-lport 1 "1" allow-related
+ovn-nbctl --wait=sb \
+  --id=@sample1 create Sample collector="$collector1" metadata=4305 -- \
+  --id=@sample2 create Sample collector="$collector2" metadata=4306 -- \
+  --sample-new=@sample1 --sample-est=@sample2 acl-add ls to-lport 1 "1" allow-related
+
+check_row_count nb:ACL 3
+check_row_count nb:Sample 6
+check ovn-nbctl --wait=sb sync
+
+AT_CHECK([ovn-sbctl lflow-list | grep probability | ovn_strip_lflows], [0], [dnl
+  table=??(ls_in_acl_after_lb_sample), priority=1100 , match=(ip && ct.new && reg3 == 4303), dnl
+action=(sample(probability=10000,collector_set=100,obs_domain=42,obs_point=4303); next;)
+  table=??(ls_in_acl_after_lb_sample), priority=1200 , match=(ip && ct.trk && (ct.est || ct.rel) && !ct.rpl && ct_label.obs_point_id == 4304 && ct_label.obs_unused == 0), dnl
+action=(sample(probability=20000,collector_set=100,obs_domain=43,obs_point=4304); next;)
+  table=??(ls_in_acl_sample   ), priority=1100 , match=(ip && ct.new && reg3 == 4301), dnl
+action=(sample(probability=10000,collector_set=100,obs_domain=42,obs_point=4301); next;)
+  table=??(ls_in_acl_sample   ), priority=1200 , match=(ip && ct.trk && (ct.est || ct.rel) && !ct.rpl && ct_label.obs_point_id == 4302 && ct_label.obs_unused == 0), dnl
+action=(sample(probability=20000,collector_set=100,obs_domain=43,obs_point=4302); next;)
+  table=??(ls_in_acl_sample   ), priority=1200 , match=(ip && ct.trk && (ct.est || ct.rel) && ct.rpl && ct_label.obs_point_id == 4306 && ct_label.obs_unused == 0), dnl
+action=(sample(probability=20000,collector_set=100,obs_domain=43,obs_point=4306); next;)
+  table=??(ls_out_acl_sample  ), priority=1100 , match=(ip && (ct.new || !ct.trk) && reg3 == 4305), dnl
+action=(sample(probability=10000,collector_set=100,obs_domain=42,obs_point=4305); next;)
+  table=??(ls_out_acl_sample  ), priority=1200 , match=(ip && ct.trk && (ct.est || ct.rel) && !ct.rpl && ct_label.obs_point_id == 4306 && ct_label.obs_unused == 0), dnl
+action=(sample(probability=20000,collector_set=100,obs_domain=43,obs_point=4306); next;)
+  table=??(ls_out_acl_sample  ), priority=1200 , match=(ip && ct.trk && (ct.est || ct.rel) && ct.rpl && ct_label.obs_point_id == 4302 && ct_label.obs_unused == 0), dnl
+action=(sample(probability=20000,collector_set=100,obs_domain=43,obs_point=4302); next;)
+  table=??(ls_out_acl_sample  ), priority=1200 , match=(ip && ct.trk && (ct.est || ct.rel) && ct.rpl && ct_label.obs_point_id == 4304 && ct_label.obs_unused == 0), dnl
+action=(sample(probability=20000,collector_set=100,obs_domain=43,obs_point=4304); next;)
+])
+
+AT_CLEANUP
+])
+
 OVN_FOR_EACH_NORTHD_NO_HV([
 AT_SETUP([NAT with match])
 ovn_start
diff --git a/tests/ovn.at b/tests/ovn.at
index cee361188a..0f401ab96a 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -329,6 +329,8 @@  ct.trk = ct_state[5]
 ct_label = NXM_NX_CT_LABEL
 ct_label.ecmp_reply_eth = ct_label[32..79]
 ct_label.label = ct_label[96..127]
+ct_label.obs_point_id = ct_label[96..127]
+ct_label.obs_unused = ct_label[0..95]
 ct_mark = NXM_NX_CT_MARK
 ct_mark.blocked = ct_mark[0]
 ct_mark.ecmp_reply_port = ct_mark[16..31]
@@ -1355,6 +1357,11 @@  ct_commit(ct_label=18446744073709551615);
 ct_commit(ct_label=18446744073709551616);
     Syntax error at `(' expecting `;'.
 
+# Observation domain and point id.
+ct_commit { ct_label.obs_point_id = reg2; };
+    encodes as ct(commit,zone=NXM_NX_REG13[[0..15]],exec(move:NXM_NX_XXREG0[[32..63]]->NXM_NX_CT_LABEL[[96..127]]))
+    has prereqs ip
+
 ct_mark = 12345
     Field ct_mark is not modifiable.
 ct_mark.blocked = 1/1
@@ -13405,7 +13412,7 @@  tpa=$(ip_to_hex 10 0 0 100)
 send_garp 1 000000000001 ffffffffffff $spa $tpa
 
 dnl traffic from localport should not be sent to localnet
-AT_CHECK([tcpdump -r hv1/br-phys_n1-tx.pcap arp[[24:4]]=0x0a000064 | wc -l],[0],[dnl
+AT_CHECK([tcpdump -vnne -r hv1/br-phys_n1-tx.pcap arp[[24:4]]=0x0a000064 | wc -l],[0],[dnl
 0
 ],[ignore])
 
@@ -18565,7 +18572,7 @@  AT_CHECK([cat 2.packets], [0], [expout])
 
 # There should be total of 9 flows present with conjunction action and 2 flows
 # with conj match. Eg.
-# table=ls_out_acl_eval, priority=2001,conj_id=2,metadata=0x1 actions=resubmit(,ls_out_acl_action)
+# table=ls_out_acl_eval, priority=2001,conj_id=2,metadata=0x1 actions=resubmit(,ls_out_acl_sample)
 # table=ls_out_acl_eval, priority=2001,conj_id=3,metadata=0x1 actions=drop
 # priority=2001,ip,metadata=0x1,nw_dst=10.0.0.6 actions=conjunction(2,2/2)
 # priority=2001,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(2,2/2)
@@ -18856,7 +18863,7 @@  check ovn-nbctl --wait=hv sync
 
 # Get the OF table numbers
 acl_eval=$(ovn-debug lflow-stage-to-oftable ls_out_acl_eval)
-acl_action=$(ovn-debug lflow-stage-to-oftable ls_out_acl_action)
+acl_sample=$(ovn-debug lflow-stage-to-oftable ls_out_acl_sample)
 
 ovn-sbctl dump-flows > sbflows
 AT_CAPTURE_FILE([sbflows])
@@ -18924,11 +18931,11 @@  AT_CHECK_UNQUOTED([as hv1 ovs-ofctl dump-flows br-int table=$acl_eval | ofctl_st
     grep "priority=1003" | \
     sed 's/conjunction([[^)]]*)/conjunction()/g' | \
     sed 's/conj_id=[[0-9]]*,/conj_id=xxx,/g' | sort], [0], [dnl
- table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
- table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
+ table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
+ table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction()
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction()
- table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
+ table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.2 actions=conjunction()
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.42 actions=conjunction()
 ])
@@ -18969,11 +18976,11 @@  AT_CHECK_UNQUOTED([as hv1 ovs-ofctl dump-flows br-int table=$acl_eval | ofctl_st
     grep "priority=1003" | \
     sed 's/conjunction([[^)]]*)/conjunction()/g' | \
     sed 's/conj_id=[[0-9]]*,/conj_id=xxx,/g' | sort], [0], [dnl
- table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
- table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
+ table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
+ table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction()
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction()
- table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
+ table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.2 actions=conjunction()
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.42 actions=conjunction()
 ])
@@ -18987,8 +18994,8 @@  AT_CHECK_UNQUOTED([as hv1 ovs-ofctl dump-flows br-int table=$acl_eval | ofctl_st
     grep "priority=1003" | \
     sed 's/conjunction([[^)]]*)/conjunction()/g' | \
     sed 's/conj_id=[[0-9]]*,/conj_id=xxx,/g' | sort], [0], [dnl
- table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
- table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
+ table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
+ table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction()
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction()
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=conjunction(),conjunction()
@@ -19027,11 +19034,11 @@  AT_CHECK_UNQUOTED([as hv1 ovs-ofctl dump-flows br-int table=$acl_eval | ofctl_st
    grep "priority=1003" | \
    sed 's/conjunction([[^)]]*)/conjunction()/g' | \
    sed 's/conj_id=[[0-9]]*,/conj_id=xxx,/g' | sort], [0], [dnl
- table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
- table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
+ table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
+ table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction()
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction()
- table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
+ table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.2 actions=conjunction()
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.42 actions=conjunction()
 ])
@@ -19048,16 +19055,16 @@  AT_CHECK_UNQUOTED([as hv1 ovs-ofctl dump-flows br-int table=$acl_eval | ofctl_st
    grep "priority=1003" | \
    sed 's/conjunction([[^)]]*)/conjunction()/g' | \
    sed 's/conj_id=[[0-9]]*,/conj_id=xxx,/g' | sort], [0], [dnl
- table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
- table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
- table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
+ table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
+ table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
+ table=$acl_eval, priority=1003,conj_id=xxx,ip,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction(),conjunction()
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction(),conjunction()
- table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
+ table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.2 actions=conjunction(),conjunction()
  table=$acl_eval, priority=1003,ip,metadata=0x1,nw_src=10.0.0.42 actions=conjunction()
- table=$acl_eval, priority=1003,udp,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
- table=$acl_eval, priority=1003,udp6,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
+ table=$acl_eval, priority=1003,udp,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
+ table=$acl_eval, priority=1003,udp6,metadata=0x1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
 ])
 
 OVN_CLEANUP([hv1])
@@ -22081,7 +22088,7 @@  check_virtual_offlows_present() {
     lr0_public_dp_key=$(printf "%x" $(fetch_column Port_Binding tunnel_key logical_port=lr0-public))
 
     AT_CHECK_UNQUOTED([as $hv ovs-ofctl dump-flows br-int table=$acl_eval,ip | ofctl_strip_all | grep "priority=2000"], [0], [dnl
- table=$acl_eval, priority=2000,ip,metadata=0x$sw0_dp_key actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$(ovn-debug lflow-stage-to-oftable ls_out_acl_action))
+ table=$acl_eval, priority=2000,ip,metadata=0x$sw0_dp_key actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$(ovn-debug lflow-stage-to-oftable ls_out_acl_sample))
 ])
 
     AT_CHECK_UNQUOTED([as $hv ovs-ofctl dump-flows br-int table=$ip_input | ofctl_strip_all | \
@@ -32529,7 +32536,7 @@  ovs-ofctl dump-flows br-int table=$acl_eval | grep "reg14=0x${rtr_port_key},meta
 # 42.42.42.42 coming from the router port.
 AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int table=$acl_eval | ofctl_strip_all | \
     grep "reg14=0x${rtr_port_key},metadata=0x${dp_key},nw_dst=42.42.42.42"], [0], [dnl
- table=$acl_eval, priority=1001,ip,reg14=0x${rtr_port_key},metadata=0x${dp_key},nw_dst=42.42.42.42 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$(ovn-debug lflow-stage-to-oftable ls_in_acl_action))
+ table=$acl_eval, priority=1001,ip,reg14=0x${rtr_port_key},metadata=0x${dp_key},nw_dst=42.42.42.42 actions=load:0x1->OXM_OF_PKT_REG4[[49]],resubmit(,$(ovn-debug lflow-stage-to-oftable ls_in_acl_sample))
 ])
 
 OVN_CLEANUP([hv1])
@@ -34386,8 +34393,8 @@  check ovn-nbctl set nb_global . options:use_common_zone="true"
 check ovn-nbctl --wait=hv sync
 # Use constants so that if tables or registers change, this test can
 # be updated easily.
-DNAT_TABLE=16
-SNAT_TABLE=45
+DNAT_TABLE=$(ovn-debug lflow-stage-to-oftable lr_in_dnat)
+SNAT_TABLE=$(ovn-debug lflow-stage-to-oftable lr_out_snat)
 DNAT_ZONE_REG="NXM_NX_REG11[[0..15]]"
 SNAT_ZONE_REG="NXM_NX_REG12[[0..15]]"
 
@@ -35528,7 +35535,7 @@  ovn-nbctl --wait=hv sync
 
 # Get the OF table numbers
 acl_eval=$(ovn-debug lflow-stage-to-oftable ls_in_acl_eval)
-acl_action=$(ovn-debug lflow-stage-to-oftable ls_in_acl_action)
+acl_sample=$(ovn-debug lflow-stage-to-oftable ls_in_acl_sample)
 
 dnl Ensure the ACL is not translated to OpenFlow.
 as hv1
@@ -35543,14 +35550,14 @@  lsp2=0x$(fetch_column Port_Binding tunnel_key logical_port=lsp2)
 dnl Ensure the ACL is translated to OpenFlows expanding pg1.
 as hv1
 AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int | grep '42\.42\.42\.42' | ofctl_strip_all], [0], [dnl
- table=$acl_eval, priority=1001,ip,reg14=$lsp1,metadata=0x1,nw_src=42.42.42.42 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
- table=$acl_eval, priority=1001,ip,reg14=$lsp2,metadata=0x1,nw_src=42.42.42.42 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
+ table=$acl_eval, priority=1001,ip,reg14=$lsp1,metadata=0x1,nw_src=42.42.42.42 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
+ table=$acl_eval, priority=1001,ip,reg14=$lsp2,metadata=0x1,nw_src=42.42.42.42 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
 ])
 
 dnl Remove a port from pg1 and expect OpenFlows to be correctly updated.
 check ovn-nbctl --wait=hv pg-set-ports pg1 lsp2
 AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int | grep '42\.42\.42\.42' | ofctl_strip_all], [0], [dnl
- table=$acl_eval, priority=1001,ip,reg14=$lsp2,metadata=0x1,nw_src=42.42.42.42 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
+ table=$acl_eval, priority=1001,ip,reg14=$lsp2,metadata=0x1,nw_src=42.42.42.42 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
 ])
 
 dnl Change the Chassis_Template_Var mapping to use the address set.
@@ -35559,14 +35566,14 @@  check ovn-nbctl --wait=hv set Chassis_Template_Var hv1 variables:CONDITION='ip4.
 dnl Ensure the ACL is translated to OpenFlows expanding as1.
 as hv1
 AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int | grep '42\.42\.42\.42' | ofctl_strip_all], [0], [dnl
- table=$acl_eval, priority=1001,ip,metadata=0x1,nw_src=42.42.42.42,nw_dst=1.1.1.1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
- table=$acl_eval, priority=1001,ip,metadata=0x1,nw_src=42.42.42.42,nw_dst=1.1.1.2 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
+ table=$acl_eval, priority=1001,ip,metadata=0x1,nw_src=42.42.42.42,nw_dst=1.1.1.1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
+ table=$acl_eval, priority=1001,ip,metadata=0x1,nw_src=42.42.42.42,nw_dst=1.1.1.2 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
 ])
 
 dnl Remove an IP from AS1 and expect OpenFlows to be correctly updated.
 check ovn-nbctl --wait=hv set address_set as1 addresses=\"1.1.1.1\"
 AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int | grep '42\.42\.42\.42' | ofctl_strip_all], [0], [dnl
- table=$acl_eval, priority=1001,ip,metadata=0x1,nw_src=42.42.42.42,nw_dst=1.1.1.1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_action)
+ table=$acl_eval, priority=1001,ip,metadata=0x1,nw_src=42.42.42.42,nw_dst=1.1.1.1 actions=load:0x1->OXM_OF_PKT_REG4[[48]],resubmit(,$acl_sample)
 ])
 
 dnl Remove the mapping and expect OpenFlows to be removed.
diff --git a/tests/system-common-macros.at b/tests/system-common-macros.at
index 691c271a3a..c595561734 100644
--- a/tests/system-common-macros.at
+++ b/tests/system-common-macros.at
@@ -237,6 +237,17 @@  m4_define([STRIP_MONITOR_CSUM], [grep "csum:" | sed 's/csum:.*/csum: <skip>/'])
 m4_define([FORMAT_CT],
     [[grep -F "dst=$1," | sed -e 's/port=[0-9]*/port=<cleared>/g' -e 's/id=[0-9]*/id=<cleared>/g' -e 's/state=[0-9_A-Z]*/state=<cleared>/g' | sort | uniq]])
 
+# DAEMONIZE([command], [pidfile])
+#
+# Run 'command' as a background process and record its pid to 'pidfile' to
+# allow cleanup on exit.
+#
+m4_define([DAEMONIZE],
+   [$1 & echo $! > $2
+     echo "kill \`cat $2\`" >> cleanup
+   ]
+)
+
 # NETNS_DAEMONIZE([namespace], [command], [pidfile])
 #
 # Run 'command' as a background process within 'namespace' and record its pid
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index 7770d58dc3..ef9652f02a 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -13050,3 +13050,468 @@  OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
 /connection dropped.*/d"])
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn -- ACL Sampling])
+AT_SKIP_IF([test $HAVE_NFCAPD = no])
+AT_SKIP_IF([test $HAVE_NFDUMP = no])
+AT_KEYWORDS([ACL])
+
+CHECK_CONNTRACK()
+CHECK_CONNTRACK_NAT()
+ovn_start
+OVS_TRAFFIC_VSWITCHD_START()
+ADD_BR([br-int])
+
+dnl Set external-ids in br-int needed for ovn-controller
+check ovs-vsctl \
+        -- set Open_vSwitch . external-ids:system-id=hv1 \
+        -- set Open_vSwitch . external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+dnl Start ovn-controller
+start_daemon ovn-controller
+
+dnl Logical network:
+dnl 1 logical switch connetected to one logical router
+dnl 6 UDP load balancers (ports 1000, 1010, 2000, 2010, 3000, 3010)
+dnl 2 VIFs
+
+check ovn-nbctl                                                  \
+    -- lr-add rtr                                                \
+    -- lrp-add rtr rtr-ls 00:00:00:00:01:00 42.42.42.1/24        \
+    -- ls-add ls                                                 \
+    -- lsp-add ls ls-rtr                                         \
+    -- lsp-set-addresses ls-rtr 00:00:00:00:01:00                \
+    -- lsp-set-type ls-rtr router                                \
+    -- lsp-set-options ls-rtr router-port=rtr-ls                 \
+    -- lsp-add ls vm1 -- lsp-set-addresses vm1 00:00:00:00:00:01 \
+    -- lsp-add ls vm2 -- lsp-set-addresses vm2 00:00:00:00:00:02 \
+    -- lb-add lb1 43.43.43.43:1000 42.42.42.3:1000 udp           \
+    -- lb-add lb2 43.43.43.43:1010 42.42.42.3:1010 udp           \
+    -- lb-add lb3 43.43.43.43:2000 42.42.42.3:2000 udp           \
+    -- lb-add lb4 43.43.43.43:2010 42.42.42.3:2010 udp           \
+    -- lb-add lb5 43.43.43.43:3000 42.42.42.3:3000 udp           \
+    -- lb-add lb6 43.43.43.43:3010 42.42.42.3:3010 udp           \
+    -- ls-lb-add ls lb1                                          \
+    -- ls-lb-add ls lb2                                          \
+    -- ls-lb-add ls lb3                                          \
+    -- ls-lb-add ls lb4                                          \
+    -- ls-lb-add ls lb5                                          \
+    -- ls-lb-add ls lb6
+
+ADD_NAMESPACES(vm1)
+ADD_VETH(vm1, vm1, br-int, "42.42.42.2/24", "00:00:00:00:00:01", "42.42.42.1")
+
+ADD_NAMESPACES(vm2)
+ADD_VETH(vm2, vm2, br-int, "42.42.42.3/24", "00:00:00:00:00:02", "42.42.42.1")
+
+collector1=$(ovn-nbctl create Sample_Collector id=1 name=c1 probability=65535 set_id=100)
+collector2=$(ovn-nbctl create Sample_Collector id=2 name=c2 probability=65535 set_id=200)
+check_row_count nb:Sample_Collector 2
+
+ovn-nbctl create Sampling_App type="acl-new" id="42"
+ovn-nbctl create Sampling_App type="acl-est" id="43"
+check_row_count nb:Sampling_App 2
+
+dnl Create ACLs that match the 3 types of traffic in all 3 possible stages:
+dnl from-lport, from-lport-after-lb, to-lport.
+ovn-nbctl                                                                         \
+    -- --id=@sample_in_1c_new create Sample collector="$collector1" metadata=1001 \
+    -- --id=@sample_in_1c_est create Sample collector="$collector1" metadata=1002 \
+    -- --sample-new=@sample_in_1c_new --sample-est=@sample_in_1c_est              \
+       acl-add ls from-lport 1 "inport == \"vm1\" && udp.dst == 1000"             \
+       allow-related
+ovn-nbctl                                                                                     \
+    -- --id=@sample_in_2c_new create Sample collector="$collector1 $collector2" metadata=1011 \
+    -- --id=@sample_in_2c_est create Sample collector="$collector1 $collector2" metadata=1012 \
+    -- --sample-new=@sample_in_2c_new --sample-est=@sample_in_2c_est                          \
+       acl-add ls from-lport 1 "inport == \"vm1\" && udp.dst == 1010"                         \
+       allow-related
+
+ovn-nbctl                                                                            \
+    -- --id=@sample_in_lb_1c_new create Sample collector="$collector1" metadata=2001 \
+    -- --id=@sample_in_lb_1c_est create Sample collector="$collector1" metadata=2002 \
+    -- --apply-after-lb --sample-new=@sample_in_lb_1c_new                            \
+       --sample-est=@sample_in_lb_1c_est                                             \
+       acl-add ls from-lport 1 "inport == \"vm1\" && udp.dst == 2000"                \
+       allow-related
+ovn-nbctl                                                                                        \
+    -- --id=@sample_in_lb_2c_new create Sample collector="$collector1 $collector2" metadata=2011 \
+    -- --id=@sample_in_lb_2c_est create Sample collector="$collector1 $collector2" metadata=2012 \
+    -- --apply-after-lb --sample-new=@sample_in_lb_2c_new --sample-est=@sample_in_lb_2c_est      \
+       acl-add ls from-lport 1 "inport == \"vm1\" && udp.dst == 2010"                            \
+       allow-related
+
+ovn-nbctl                                                                          \
+    -- --id=@sample_out_1c_new create Sample collector="$collector1" metadata=3001 \
+    -- --id=@sample_out_1c_est create Sample collector="$collector1" metadata=3002 \
+    -- --sample-new=@sample_out_1c_new --sample-est=@sample_out_1c_est             \
+       acl-add ls to-lport 1 "outport == \"vm2\" && udp.dst == 3000"               \
+       allow-related
+ovn-nbctl                                                                                      \
+    -- --id=@sample_out_2c_new create Sample collector="$collector1 $collector2" metadata=3011 \
+    -- --id=@sample_out_2c_est create Sample collector="$collector1 $collector2" metadata=3012 \
+    -- --sample-new=@sample_out_2c_new --sample-est=@sample_out_2c_est                         \
+       acl-add ls to-lport 1 "outport == \"vm2\" && udp.dst == 3010"                           \
+       allow-related
+
+check_row_count nb:ACL 6
+check_row_count nb:Sample 12
+
+dnl Wait for ovn-controller to catch up.
+wait_for_ports_up
+check ovn-nbctl --wait=hv sync
+
+dnl Start an IPFIX collector.
+DAEMONIZE([nfcapd -B 1024000 -w . -p 4242 2> collector.err], [collector.pid])
+
+dnl Wait for the collector to be up.
+OVS_WAIT_UNTIL([grep -q 'Startup nfcapd.' collector.err])
+
+dnl Configure the OVS flow sample collector.
+ovs-vsctl --id=@br get Bridge br-int \
+    -- --id=@ipfix create IPFIX targets=\"127.0.0.1:4242\" template_interval=1 \
+    -- --id=@cs create Flow_Sample_Collector_Set id=100 bridge=@br ipfix=@ipfix
+
+dnl And wait for it to be up and running.
+OVS_WAIT_UNTIL([ovs-ofctl dump-ipfix-flow br-int | grep -q '1 ids'])
+
+dnl Start UDP echo server on vm2.
+NETNS_DAEMONIZE([vm2], [nc -e /bin/cat -k -u -v -l 1000], [nc-vm2-1000.pid])
+NETNS_DAEMONIZE([vm2], [nc -e /bin/cat -k -u -v -l 1010], [nc-vm2-1010.pid])
+NETNS_DAEMONIZE([vm2], [nc -e /bin/cat -k -u -v -l 2000], [nc-vm2-2000.pid])
+NETNS_DAEMONIZE([vm2], [nc -e /bin/cat -k -u -v -l 2010], [nc-vm2-2010.pid])
+NETNS_DAEMONIZE([vm2], [nc -e /bin/cat -k -u -v -l 3000], [nc-vm2-3000.pid])
+NETNS_DAEMONIZE([vm2], [nc -e /bin/cat -k -u -v -l 3010], [nc-vm2-3010.pid])
+
+dnl Send traffic (2 packets) to the UDP LB1 (hits the from-lport ACL).
+NS_CHECK_EXEC([vm1], [(echo a; sleep 1; echo a) | nc --send-only -u 43.43.43.43 1000])
+NS_CHECK_EXEC([vm1], [(echo a; sleep 1; echo a) | nc --send-only -u 43.43.43.43 1010])
+
+dnl Send traffic (2 packets) to the UDP LB1 (hits the from-lport after-lb ACL).
+NS_CHECK_EXEC([vm1], [(echo a; sleep 1; echo a) | nc --send-only -u 43.43.43.43 2000])
+NS_CHECK_EXEC([vm1], [(echo a; sleep 1; echo a) | nc --send-only -u 43.43.43.43 2010])
+
+dnl Send traffic (2 packets) to the UDP LB1 (hits the from-lport ACL).
+NS_CHECK_EXEC([vm1], [(echo a; sleep 1; echo a) | nc --send-only -u 43.43.43.43 3000])
+NS_CHECK_EXEC([vm1], [(echo a; sleep 1; echo a) | nc --send-only -u 43.43.43.43 3010])
+
+dnl Wait until OVS sampled all expected packets (4 data packets + 1 ICMP
+dnl port unreachable error on each session).
+OVS_WAIT_UNTIL([ovs-ofctl dump-ipfix-flow br-int | grep -q 'sampled pkts=30'])
+
+dnl Check the IPFIX samples.
+kill $(cat collector.pid)
+OVS_WAIT_WHILE([kill -0 $(cat collector.pid) 2>/dev/null])
+
+dnl Can't match on observation domain ID due to the followig fix not being
+dnl available in any released version of nfdump:
+dnl https://github.com/phaag/nfdump/issues/544
+dnl
+dnl Only match on the point ID.
+dnl
+dnl Expect for each ACL:
+dnl - one sample for new packets
+dnl - four samples for established packets (3 data + one icmp error)
+AT_CHECK([for f in $(ls -1 nfcapd.*); do nfdump -o json -r $f; done | grep observationPointID | awk '{$1=$1;print}' | sort], [0], [dnl
+"observationPointID" : 1001,
+"observationPointID" : 1002,
+"observationPointID" : 1002,
+"observationPointID" : 1002,
+"observationPointID" : 1002,
+"observationPointID" : 1011,
+"observationPointID" : 1012,
+"observationPointID" : 1012,
+"observationPointID" : 1012,
+"observationPointID" : 1012,
+"observationPointID" : 2001,
+"observationPointID" : 2002,
+"observationPointID" : 2002,
+"observationPointID" : 2002,
+"observationPointID" : 2002,
+"observationPointID" : 2011,
+"observationPointID" : 2012,
+"observationPointID" : 2012,
+"observationPointID" : 2012,
+"observationPointID" : 2012,
+"observationPointID" : 3001,
+"observationPointID" : 3002,
+"observationPointID" : 3002,
+"observationPointID" : 3002,
+"observationPointID" : 3002,
+"observationPointID" : 3011,
+"observationPointID" : 3012,
+"observationPointID" : 3012,
+"observationPointID" : 3012,
+"observationPointID" : 3012,
+])
+
+OVS_APP_EXIT_AND_WAIT([ovn-controller])
+
+as ovn-sb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as ovn-nb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as northd
+OVS_APP_EXIT_AND_WAIT([ovn-northd])
+
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+/connection dropped.*/d"])
+
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn -- Tiered ACL Sampling])
+AT_SKIP_IF([test $HAVE_NFCAPD = no])
+AT_SKIP_IF([test $HAVE_NFDUMP = no])
+AT_KEYWORDS([ACL])
+
+CHECK_CONNTRACK()
+CHECK_CONNTRACK_NAT()
+ovn_start
+OVS_TRAFFIC_VSWITCHD_START()
+ADD_BR([br-int])
+
+dnl Set external-ids in br-int needed for ovn-controller
+check ovs-vsctl \
+        -- set Open_vSwitch . external-ids:system-id=hv1 \
+        -- set Open_vSwitch . external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+dnl Start ovn-controller
+start_daemon ovn-controller
+
+dnl Logical network:
+dnl 1 logical switch
+dnl 2 VIFs
+
+check ovn-nbctl                                                  \
+    -- ls-add ls                                                 \
+    -- lsp-add ls vm1 -- lsp-set-addresses vm1 00:00:00:00:00:01 \
+    -- lsp-add ls vm2 -- lsp-set-addresses vm2 00:00:00:00:00:02
+ADD_NAMESPACES(vm1)
+ADD_VETH(vm1, vm1, br-int, "42.42.42.2/24", "00:00:00:00:00:01", "42.42.42.1")
+
+ADD_NAMESPACES(vm2)
+ADD_VETH(vm2, vm2, br-int, "42.42.42.3/24", "00:00:00:00:00:02", "42.42.42.1")
+
+collector1=$(ovn-nbctl create Sample_Collector id=1 name=c1 probability=65535 set_id=100)
+check_row_count nb:Sample_Collector 1
+
+ovn-nbctl create Sampling_App type="acl-new" id="42"
+ovn-nbctl create Sampling_App type="acl-est" id="43"
+check_row_count nb:Sampling_App 2
+
+dnl Create two tiers of ACLs.
+ovn-nbctl                                                                     \
+    -- --id=@sample_1_new create Sample collector="$collector1" metadata=1001 \
+    -- --id=@sample_1_est create Sample collector="$collector1" metadata=1002 \
+    -- --tier=0 --sample-new=@sample_1_new --sample-est=@sample_1_est         \
+       acl-add ls from-lport 1 "inport == \"vm1\" && udp.dst == 1000"         \
+       pass
+
+ovn-nbctl                                                                     \
+    -- --id=@sample_2_new create Sample collector="$collector1" metadata=2001 \
+    -- --id=@sample_2_est create Sample collector="$collector1" metadata=2002 \
+    -- --tier=1 --sample-new=@sample_2_new --sample-est=@sample_2_est         \
+       acl-add ls from-lport 1 "inport == \"vm1\" && udp.dst == 1000" \
+       allow-related
+
+check_row_count nb:ACL 2
+check_row_count nb:Sample 4
+
+dnl Wait for ovn-controller to catch up.
+wait_for_ports_up
+check ovn-nbctl --wait=hv sync
+
+dnl Start an IPFIX collector.
+DAEMONIZE([nfcapd -B 1024000 -w . -p 4242 2> collector.err], [collector.pid])
+
+dnl Wait for the collector to be up.
+OVS_WAIT_UNTIL([grep -q 'Startup nfcapd.' collector.err])
+
+dnl Configure the OVS flow sample collector.
+ovs-vsctl --id=@br get Bridge br-int \
+    -- --id=@ipfix create IPFIX targets=\"127.0.0.1:4242\" template_interval=1 \
+    -- --id=@cs create Flow_Sample_Collector_Set id=100 bridge=@br ipfix=@ipfix
+
+dnl And wait for it to be up and running.
+OVS_WAIT_UNTIL([ovs-ofctl dump-ipfix-flow br-int | grep -q '1 ids'])
+
+dnl Start UDP echo server on vm2.
+NETNS_DAEMONIZE([vm2], [nc -e /bin/cat -k -u -v -l 1000], [nc-vm2-1000.pid])
+
+dnl Send traffic to the UDP server (hits both ACL tiers).
+NS_CHECK_EXEC([vm1], [echo a | nc --send-only -u 42.42.42.3 1000])
+
+dnl Wait until OVS sampled all expected packets:
+dnl - first packet sampled by both tiers
+dnl - reply packet sampled by last tier (established session)
+dnl - related ICMP port unreachable error sampled by last tier (established session)
+OVS_WAIT_UNTIL([ovs-ofctl dump-ipfix-flow br-int | grep -q 'sampled pkts=4'])
+
+dnl Check the IPFIX samples.
+kill $(cat collector.pid)
+OVS_WAIT_WHILE([kill -0 $(cat collector.pid) 2>/dev/null])
+
+dnl Can't match on observation domain ID due to the followig fix not being
+dnl available in any released version of nfdump:
+dnl https://github.com/phaag/nfdump/issues/544
+dnl
+dnl Only match on the point ID.
+AT_CHECK([for f in $(ls -1 nfcapd.*); do nfdump -o json -r $f; done | grep observationPointID | awk '{$1=$1;print}' | sort], [0], [dnl
+"observationPointID" : 1001,
+"observationPointID" : 2001,
+"observationPointID" : 2002,
+"observationPointID" : 2002,
+])
+
+OVS_APP_EXIT_AND_WAIT([ovn-controller])
+
+as ovn-sb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as ovn-nb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as northd
+OVS_APP_EXIT_AND_WAIT([ovn-northd])
+
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+/connection dropped.*/d"])
+
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn -- ACL Sampling - Stateful ACL - to-lport router port])
+AT_SKIP_IF([test $HAVE_NFCAPD = no])
+AT_SKIP_IF([test $HAVE_NFDUMP = no])
+AT_KEYWORDS([ACL])
+
+CHECK_CONNTRACK()
+CHECK_CONNTRACK_NAT()
+ovn_start
+OVS_TRAFFIC_VSWITCHD_START()
+ADD_BR([br-int])
+
+dnl Set external-ids in br-int needed for ovn-controller
+check ovs-vsctl \
+        -- set Open_vSwitch . external-ids:system-id=hv1 \
+        -- set Open_vSwitch . external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+dnl Start ovn-controller
+start_daemon ovn-controller
+
+dnl Logical network:
+dnl 1 router
+dnl 1 logical switch
+dnl 1 VIF
+
+check ovn-nbctl                                      \
+  -- lr-add lr                                       \
+  -- lrp-add lr lrp1 00:00:00:00:01:00 42.42.42.1/24 \
+  -- ls-add ls                                       \
+  -- lsp-add ls vm1                                  \
+  -- lsp-set-addresses vm1 00:00:00:00:00:01         \
+  -- lsp-add ls ls-lr                                \
+  -- lsp-set-type ls-lr router                       \
+  -- lsp-set-options ls-lr router-port=lrp1          \
+  -- lsp-set-addresses ls-lr router
+check ovn-nbctl --wait=sb sync
+
+ADD_NAMESPACES(vm1)
+ADD_VETH(vm1, vm1, br-int, "42.42.42.2/24", "00:00:00:00:00:01", "42.42.42.1")
+
+collector1=$(ovn-nbctl create Sample_Collector id=1 name=c1 probability=65535 set_id=100)
+collector2=$(ovn-nbctl create Sample_Collector id=2 name=c2 probability=65535 set_id=200)
+check_row_count nb:Sample_Collector 2
+
+ovn-nbctl create Sampling_App type="acl-new" id="42"
+check_row_count nb:Sampling_App 1
+
+dnl Create three ACLs:
+dnl - one from-lport, stateful, allowing all traffic
+dnl - one to-lport, dropping all traffic to 1.1.1.1 (single collector)
+dnl - one to-lport, dropping all traffic to 1.1.1.2 (two collectors)
+ovn-nbctl                                                                          \
+  -- --id=@sample1 create Sample collector="$collector1" metadata=1001             \
+  -- --id=@sample2 create Sample collector="$collector1" metadata=1002             \
+  -- --id=@sample3 create Sample collector="$collector1 $collector2" metadata=1003 \
+  -- --sample-new=@sample1 acl-add ls from-lport 1 "1" allow-related               \
+  -- --sample-new=@sample2 acl-add ls to-lport 1 "ip4.dst == 1.1.1.1" drop         \
+  -- --sample-new=@sample3 acl-add ls to-lport 1 "ip4.dst == 1.1.1.2" drop
+
+check_row_count nb:ACL 3
+check_row_count nb:Sample 3
+
+dnl Wait for ovn-controller to catch up.
+wait_for_ports_up
+check ovn-nbctl --wait=hv sync
+
+dnl Start an IPFIX collector.
+DAEMONIZE([nfcapd -B 1024000 -w . -p 4242 2> collector.err], [collector.pid])
+
+dnl Wait for the collector to be up.
+OVS_WAIT_UNTIL([grep -q 'Startup nfcapd.' collector.err])
+
+dnl Configure the OVS flow sample collector.
+ovs-vsctl --id=@br get Bridge br-int \
+    -- --id=@ipfix create IPFIX targets=\"127.0.0.1:4242\" template_interval=1 \
+    -- --id=@cs create Flow_Sample_Collector_Set id=100 bridge=@br ipfix=@ipfix
+
+dnl And wait for it to be up and running.
+OVS_WAIT_UNTIL([ovs-ofctl dump-ipfix-flow br-int | grep -q '1 ids'])
+
+NS_CHECK_EXEC([vm1], [ping -c 1 1.1.1.1], [1], [ignore], [ignore])
+NS_CHECK_EXEC([vm1], [ping -c 1 1.1.1.2], [1], [ignore], [ignore])
+
+dnl Wait until OVS sampled the two ICMP packet on two ACLs.
+OVS_WAIT_UNTIL([ovs-ofctl dump-ipfix-flow br-int | grep -q 'sampled pkts=4'])
+
+dnl Check the IPFIX samples.
+kill $(cat collector.pid)
+OVS_WAIT_WHILE([kill -0 $(cat collector.pid) 2>/dev/null])
+
+dnl Can't match on observation domain ID due to the followig fix not being
+dnl available in any released version of nfdump:
+dnl https://github.com/phaag/nfdump/issues/544
+dnl
+dnl Only match on the point ID.
+AT_CHECK([for f in $(ls -1 nfcapd.*); do nfdump -o json -r $f; done | grep observationPointID | awk '{$1=$1;print}' | sort], [0], [dnl
+"observationPointID" : 1001,
+"observationPointID" : 1001,
+"observationPointID" : 1002,
+"observationPointID" : 1003,
+])
+
+OVS_APP_EXIT_AND_WAIT([ovn-controller])
+
+as ovn-sb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as ovn-nb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as northd
+OVS_APP_EXIT_AND_WAIT([ovn-northd])
+
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+/connection dropped.*/d"])
+
+AT_CLEANUP
+])
diff --git a/utilities/containers/fedora/Dockerfile b/utilities/containers/fedora/Dockerfile
index 078180cff3..4dce1e32b4 100755
--- a/utilities/containers/fedora/Dockerfile
+++ b/utilities/containers/fedora/Dockerfile
@@ -27,6 +27,7 @@  RUN dnf -y update \
         libcap-ng-devel \
         libtool \
         net-tools \
+        nfdump \
         ninja-build \
         nmap-ncat \
         numactl-devel \
diff --git a/utilities/containers/ubuntu/Dockerfile b/utilities/containers/ubuntu/Dockerfile
index 7cf0751225..073afa8764 100755
--- a/utilities/containers/ubuntu/Dockerfile
+++ b/utilities/containers/ubuntu/Dockerfile
@@ -33,6 +33,7 @@  RUN apt update -y \
         llvm-dev \
         ncat \
         net-tools \
+        nfdump \
         ninja-build \
         python3-dev \
         python3-pip \
diff --git a/utilities/ovn-nbctl.8.xml b/utilities/ovn-nbctl.8.xml
index e2657ca02c..e1e5b681e1 100644
--- a/utilities/ovn-nbctl.8.xml
+++ b/utilities/ovn-nbctl.8.xml
@@ -399,7 +399,7 @@ 
       must be either <code>switch</code> or <code>port-group</code>.
     </p>
     <dl>
-        <dt>[<code>--type=</code>{<code>switch</code> | <code>port-group</code>}] [<code>--log</code>] [<code>--meter=</code><var>meter</var>] [<code>--severity=</code><var>severity</var>] [<code>--name=</code><var>name</var>] [<code>--label=</code><var>label</var>] [<code>--may-exist</code>] [<code>--apply-after-lb</code>] [<code>--tier</code>] <code>acl-add</code> <var>entity</var> <var>direction</var> <var>priority</var> <var>match</var> <var>verdict</var></dt>
+        <dt>[<code>--type=</code>{<code>switch</code> | <code>port-group</code>}] [<code>--log</code>] [<code>--meter=</code><var>meter</var>] [<code>--severity=</code><var>severity</var>] [<code>--name=</code><var>name</var>] [<code>--label=</code><var>label</var>] [<code>--sample-new=</code><var>sample</var>] [<code>--sample-est=</code><var>sample</var>] [<code>--may-exist</code>] [<code>--apply-after-lb</code>] [<code>--tier</code>] <code>acl-add</code> <var>entity</var> <var>direction</var> <var>priority</var> <var>match</var> <var>verdict</var></dt>
       <dd>
         <p>
           Adds the specified ACL to <var>entity</var>.  <var>direction</var>
@@ -424,6 +424,12 @@ 
           names a meter configured by <code>meter-add</code>.
         </p>
 
+        <p>
+          The <code>--sample-new</code> (and optionally
+          <code>--sample-est</code>) enable ACL sampling. A valid uuid of a
+          row of the <ref table="Sample"/> table must be provided.
+        </p>
+
         <p>
           The <code>--apply-after-lb</code> option sets
           <code>apply-after-lb=true</code> in the <code>options</code> column
diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c
index 679d3f2d93..d45be75c78 100644
--- a/utilities/ovn-nbctl.c
+++ b/utilities/ovn-nbctl.c
@@ -2318,6 +2318,11 @@  nbctl_pre_acl(struct ctl_context *ctx)
     ovsdb_idl_add_column(ctx->idl, &nbrec_acl_col_match);
     ovsdb_idl_add_column(ctx->idl, &nbrec_acl_col_options);
     ovsdb_idl_add_column(ctx->idl, &nbrec_acl_col_tier);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_acl_col_sample_new);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_acl_col_sample_est);
+
+    ovsdb_idl_add_table(ctx->idl, &nbrec_table_sample_collector);
+    ovsdb_idl_add_table(ctx->idl, &nbrec_table_sample);
 }
 
 static void
@@ -2331,6 +2336,8 @@  nbctl_pre_acl_list(struct ctl_context *ctx)
     ovsdb_idl_add_column(ctx->idl, &nbrec_acl_col_severity);
     ovsdb_idl_add_column(ctx->idl, &nbrec_acl_col_meter);
     ovsdb_idl_add_column(ctx->idl, &nbrec_acl_col_label);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_acl_col_sample_new);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_acl_col_sample_est);
     ovsdb_idl_add_column(ctx->idl, &nbrec_acl_col_options);
 }
 
@@ -2382,6 +2389,8 @@  nbctl_acl_add(struct ctl_context *ctx)
     const char *severity = shash_find_data(&ctx->options, "--severity");
     const char *name = shash_find_data(&ctx->options, "--name");
     const char *meter = shash_find_data(&ctx->options, "--meter");
+    const char *sample_new = shash_find_data(&ctx->options, "--sample-new");
+    const char *sample_est = shash_find_data(&ctx->options, "--sample-est");
     if (log || severity || name || meter) {
         nbrec_acl_set_log(acl, true);
     }
@@ -2399,6 +2408,30 @@  nbctl_acl_add(struct ctl_context *ctx)
         nbrec_acl_set_meter(acl, meter);
     }
 
+    if (sample_new) {
+        char *sample_setting = xasprintf("sample_new=%s", sample_new);
+        error = ctl_set_column("ACL", &acl->header_, sample_setting,
+                               ctx->symtab);
+        free(sample_setting);
+        if (error) {
+            ctl_error(ctx, "invalid --sample-new: %s", error);
+            free(error);
+            return;
+        }
+    }
+
+    if (sample_est) {
+        char *sample_setting = xasprintf("sample_est=%s", sample_est);
+        error = ctl_set_column("ACL", &acl->header_, sample_setting,
+                               ctx->symtab);
+        free(sample_setting);
+        if (error) {
+            ctl_error(ctx, "invalid --sample-est: %s", error);
+            free(error);
+            return;
+        }
+    }
+
     /* Set the ACL label */
     const char *label = shash_find_data(&ctx->options, "--label");
     if (label) {
@@ -7925,7 +7958,7 @@  static const struct ctl_command_syntax nbctl_commands[] = {
     { "acl-add", 5, 6, "{SWITCH | PORTGROUP} DIRECTION PRIORITY MATCH ACTION",
       nbctl_pre_acl, nbctl_acl_add, NULL,
       "--log,--may-exist,--type=,--name=,--severity=,--meter=,--label=,"
-      "--apply-after-lb,--tier=", RW },
+      "--apply-after-lb,--tier=,--sample-new=,--sample-est=", RW },
     { "acl-del", 1, 4, "{SWITCH | PORTGROUP} [DIRECTION [PRIORITY MATCH]]",
       nbctl_pre_acl, nbctl_acl_del, NULL, "--type=,--tier=", RW },
     { "acl-list", 1, 1, "{SWITCH | PORTGROUP}",