On Mon, Jul 08, 2024 at 01:24:14PM GMT, Dumitru Ceara wrote:
> From: Adrian Moreno <amore...@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):
>   s1=$(ovn-nbctl create sample collector="$c1 $c2" metadata=4301)
>   s2=$(ovn-nbctl create sample collector="$c1 $c2" metadata=4302)
>   # Create an ingress ACL to allow IP traffic:
>   ovn-nbctl --sample-new=$s1 --sample-est=$s2 acl-add ls \
>             from-lport 1 "ip" allow-related
>
> The config above will generate IPFIX samples with:
> - 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
> - 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
>

We assume the dp_key is 0 in this case, right? Maybe worth mentioning
it.

> Reported-at: https://issues.redhat.com/browse/FDP-305
> Signed-off-by: Adrian Moreno <amore...@redhat.com>
> Co-authored-by: Dumitru Ceara <dce...@redhat.com>
> Signed-off-by: Dumitru Ceara <dce...@redhat.com>
> ---
>  NEWS                                   |   3 +
>  include/ovn/logical-fields.h           |   2 +
>  lib/logical-fields.c                   |   6 +
>  northd/northd.c                        | 519 +++++++++++++++++++++++--
>  northd/ovn-northd.8.xml                |  26 ++
>  ovn-nb.ovsschema                       |  44 ++-
>  ovn-nb.xml                             |  56 +++
>  tests/atlocal.in                       |   6 +
>  tests/ovn-macros.at                    |   4 +
>  tests/ovn-nbctl.at                     |  20 +
>  tests/ovn-northd.at                    | 240 ++++++++++--
>  tests/ovn.at                           |   3 +
>  tests/system-common-macros.at          |  11 +
>  tests/system-ovn.at                    | 149 +++++++
>  utilities/containers/fedora/Dockerfile |   1 +
>  utilities/containers/ubuntu/Dockerfile |   1 +
>  utilities/ovn-nbctl.8.xml              |   8 +-
>  utilities/ovn-nbctl.c                  |  43 +-
>  18 files changed, 1079 insertions(+), 63 deletions(-)

I'm not done reviewing. I need to spend some time understanding how
locial flows are built. Not very familiar with this. Plus I want to run
some tests. However, here are some initial thoughts.

>
> diff --git a/NEWS b/NEWS
> index fcf182bc02..7899c623f2 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -41,6 +41,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.name configured to be "drop-sampling").
> +  - 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.

nit: Is the double white space between "tables." and "Sampling" intentional?

>
>  OVN v24.03.0 - 01 Mar 2024
>  --------------------------
> diff --git a/include/ovn/logical-fields.h b/include/ovn/logical-fields.h
> index ce79b501cf..8bf7564dbd 100644
> --- a/include/ovn/logical-fields.h
> +++ b/include/ovn/logical-fields.h
> @@ -197,11 +197,13 @@ const struct ovn_field *ovn_field_from_name(const char 
> *name);
>  #define OVN_CT_NATTED_BIT  1
>  #define OVN_CT_LB_SKIP_SNAT_BIT 2
>  #define OVN_CT_LB_FORCE_SNAT_BIT 3
> +#define OVN_CT_SAMPLE_ID_SET_BIT 4
>
>  #define OVN_CT_BLOCKED 1
>  #define OVN_CT_NATTED  2
>  #define OVN_CT_LB_SKIP_SNAT 4
>  #define OVN_CT_LB_FORCE_SNAT 8
> +#define OVN_CT_SAMPLE_ID_SET 16
>
>  #define OVN_CT_ECMP_ETH_1ST_BIT 32
>  #define OVN_CT_ECMP_ETH_END_BIT 79
> diff --git a/lib/logical-fields.c b/lib/logical-fields.c
> index 4acf8a677e..edd0b74ab1 100644
> --- a/lib/logical-fields.c
> +++ b/lib/logical-fields.c
> @@ -175,6 +175,12 @@ 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_domain_id", NULL,
> +                                    "ct_label[64..95]", WR_CT_COMMIT);


I have a question here.

obs_domain_id is u8 from northd's pov. Then ovn-controller expands it to
        (sample->obs_domain_id << 24) | (ep->dp_key & 0xFFFFFF);

AFAICS, what we are storing in the ct_label is the 8bits that correspond
to the sampling_app_id. Also, we match on those 8bits in
build_acl_sample_label_match().

My question is: why use 32bits of ct_label to store/match 8bits? Can't
se safe 24 bits here?

> +    expr_symtab_add_subfield_scoped(symtab, "ct_label.obs_unused", NULL,
> +                                    "ct_label[0..63]", WR_CT_COMMIT);
>
>      expr_symtab_add_field(symtab, "ct_state", MFF_CT_STATE, NULL, false);
>
> diff --git a/northd/northd.c b/northd/northd.c
> index 901b9e9cd1..3adc8c776a 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:
> @@ -6461,6 +6466,409 @@ 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. */

nit: double white space between "ID." and "Otherwise"?

> +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=%hd,"
> +                               "obs_domain=%"PRIu32","
> +                               "obs_point=%"PRIu32");",
> +                               (uint16_t) sample->collectors[i]->probability,
> +                               (uint32_t) sample->collectors[i]->set_id,
> +                               domain_id, point_id);
> +    }
> +}
> +
> +/* 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 || sample_est) {

nit: I guess the check in side this else if() is redundant given the
check at the top of the function.

> +        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,
> +                             uint8_t sample_domain_id)
> +{
> +    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;
> +    }
> +
> +    /* 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_domain_id == %"PRIu32" && "
> +                         "ct_label.obs_point_id == %"PRIu32" && "
> +                         "ct_label.obs_unused == 0",
> +                  domain_id, 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,
> +                                     const char *reg_verdict,
> +                                     const char *verdict_actions,
> +                                     const char *copp_meter,
> +                                     uint8_t sample_domain_id,
> +                                     struct lflow_ref *lflow_ref)
> +{
> +    if (!acl->sample_new) {
> +        return;
> +    }
> +
> +    ds_clear(actions);
> +    ds_clear(match);
> +
> +    ds_put_format(match, "ip && %s == 1 && ", reg_verdict);
> +    build_acl_sample_register_match(match, acl, acl->sample_new);
> +
> +    build_acl_sample_action(actions, acl, acl->sample_new, sample_domain_id);
> +    ds_put_format(actions, " %s", verdict_actions);
> +
> +    ovn_lflow_metered(lflows, od, stage, 1100,
> +                      ds_cstr(match), ds_cstr(actions),
> +                      copp_meter, 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,
> +                                    const char *reg_verdict,
> +                                    const char *verdict_actions,
> +                                    const char *copp_meter,
> +                                    uint8_t sample_domain_id,
> +                                    struct lflow_ref *lflow_ref)
> +{
> +    if (!acl->sample_new) {
> +        return;
> +    }
> +
> +    ds_clear(actions);
> +    ds_clear(match);
> +
> +    ds_put_format(match, "ip && ct.new && %s == 1 && ", reg_verdict);
> +    build_acl_sample_register_match(match, acl, acl->sample_new);
> +
> +    build_acl_sample_action(actions, acl, acl->sample_new, sample_domain_id);
> +    ds_put_format(actions, " %s", verdict_actions);
> +
> +    ovn_lflow_metered(lflows, od, stage, 1100,
> +                      ds_cstr(match), ds_cstr(actions),
> +                      copp_meter, 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,
> +                                         const char *reg_verdict,
> +                                         const char *verdict_actions,
> +                                         const char *copp_meter,
> +                                         uint8_t sample_domain_id,
> +                                         struct lflow_ref *lflow_ref)
> +{
> +    ds_clear(actions);
> +    ds_clear(match);
> +
> +    ds_put_format(match, "ip && ct.trk && "
> +                         "(ct.est || ct.rel) && "
> +                         "!ct.rpl && %s == 1 && ",
> +                  reg_verdict);
> +    build_acl_sample_label_match(match, acl, acl->sample_est,
> +                                 sample_domain_id);
> +
> +    build_acl_sample_action(actions, acl, acl->sample_est, sample_domain_id);
> +    ds_put_format(actions, " %s", verdict_actions);
> +
> +    ovn_lflow_metered(lflows, od, stage, 1200,
> +                      ds_cstr(match), ds_cstr(actions),
> +                      copp_meter, 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,
> +                                        const char *reg_verdict,
> +                                        const char *verdict_actions,
> +                                        const char *copp_meter,
> +                                        uint8_t sample_domain_id,
> +                                        struct lflow_ref *lflow_ref)
> +{
> +    ds_clear(actions);
> +    ds_clear(match);
> +
> +    ds_put_format(match, "ip && ct.trk && "
> +                         "(ct.est || ct.rel) && "
> +                         "ct.rpl && %s == 1 && ",
> +                  reg_verdict);
> +    build_acl_sample_label_match(match, acl, acl->sample_est,
> +                                 sample_domain_id);
> +
> +    build_acl_sample_action(actions, acl, acl->sample_est, sample_domain_id);
> +    ds_put_format(actions, " %s", verdict_actions);
> +
> +    ovn_lflow_metered(lflows, od, rpl_stage, 1200,
> +                      ds_cstr(match), ds_cstr(actions),
> +                      copp_meter, lflow_ref);
> +}
> +
> +/* This builds logical flows that sample and forward traffic

nit: s/sample/samples/ s/forward/forwards/

> + * 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,
> +                                    const char *reg_verdict,
> +                                    const char *verdict_actions,
> +                                    const char *copp_meter,
> +                                    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, reg_verdict, 
> verdict_actions,
> +                                             copp_meter, 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_ACTION
> +                                ? S_SWITCH_IN_ACL_ACTION
> +                                : S_SWITCH_OUT_ACL_ACTION);
> +    build_acl_sample_est_rpl_stateful_flows(od, lflows, rpl_stage,
> +                                            match, actions,
> +                                            acl, reg_verdict, 
> verdict_actions,
> +                                            copp_meter, 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,
> +                       const struct shash *meter_groups,
> +                       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;

nit: I see other instances adding a space after the ":", but not all of
them.

> +    enum ovn_stage stage;
> +
> +    if (ingress && smap_get_bool(&acl->options, "apply-after-lb", false)) {
> +        stage = S_SWITCH_IN_ACL_AFTER_LB_ACTION;
> +    } else if (ingress) {
> +        stage = S_SWITCH_IN_ACL_ACTION;
> +    } else {
> +        stage = S_SWITCH_OUT_ACL_ACTION;
> +    }
> +
> +    struct ds verdict_actions = DS_EMPTY_INITIALIZER;
> +    ds_put_cstr(&verdict_actions, REGBIT_ACL_VERDICT_ALLOW " = 0; "
> +                                  REGBIT_ACL_VERDICT_DROP " = 0; "
> +                                  REGBIT_ACL_VERDICT_REJECT " = 0; ");
> +    if (ls_stateful_rec->max_acl_tier) {
> +        ds_put_cstr(&verdict_actions, REG_ACL_TIER " = 0; ");
> +    }
> +
> +    /* Follow the same verdict determination logic as in
> +     * build_acl_action_lflows().
> +     */
> +    const char *reg_verdict = REGBIT_ACL_VERDICT_ALLOW;
> +    const char *copp_meter = NULL;
> +    if (!strcmp(acl->action, "drop")) {
> +        reg_verdict = REGBIT_ACL_VERDICT_DROP;
> +        ds_put_cstr(&verdict_actions, debug_implicit_drop_action());

In the case drop-sampling is not enabled, this adds a comment:
/* drop */. Is it OK to have it in the middle of the lflow?

> +    } else if (!strcmp(acl->action, "reject")) {
> +        reg_verdict = REGBIT_ACL_VERDICT_REJECT;
> +        copp_meter = copp_meter_get(COPP_REJECT, od->nbs->copp, 
> meter_groups);
> +        build_acl_reject_action(&verdict_actions, ingress);
> +    } else if (!strcmp(acl->action, "allow")
> +               || !strcmp(acl->action, "allow-related")) {
> +        reg_verdict = REGBIT_ACL_VERDICT_ALLOW;
> +        ds_put_cstr(&verdict_actions, "next;");
> +    } else {
> +        /* XXX: ACLs with action "pass" do not support sampling. */

Maybe worth documenting this.

> +        goto done;
> +    }
> +
> +    uint8_t sample_new_domain_id =
> +        sampling_app_get_id(sampling_apps, SAMPLING_APP_ACL_NEW_TRAFFIC);
> +    uint8_t sample_est_domain_id =
> +        sampling_app_get_id(sampling_apps, SAMPLING_APP_ACL_EST_TRAFFIC);
> +
> +    if (!stateful_match) {
> +        build_acl_sample_new_stateless_flows(od, lflows, stage, match, 
> actions,
> +                                             acl, reg_verdict,
> +                                             ds_cstr_ro(&verdict_actions),
> +                                             copp_meter, 
> sample_new_domain_id,
> +                                             lflow_ref);
> +    } else {
> +        build_acl_sample_new_stateful_flows(od, lflows, stage, match, 
> actions,
> +                                            acl, reg_verdict,
> +                                            ds_cstr_ro(&verdict_actions),
> +                                            copp_meter, sample_new_domain_id,
> +                                            lflow_ref);
> +        build_acl_sample_est_stateful_flows(od, lflows, stage, match, 
> actions,
> +                                            acl, reg_verdict,
> +                                            ds_cstr_ro(&verdict_actions),
> +                                            copp_meter, sample_est_domain_id,
> +                                            lflow_ref);
> +    }
> +
> +done:
> +    ds_destroy(&verdict_actions);
> +}
> +
>  static void
>  consider_acl(struct lflow_table *lflows, const struct ovn_datapath *od,
>               const struct nbrec_acl *acl, bool has_stateful,
> @@ -6508,6 +6916,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,
> @@ -6542,10 +6954,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),
> @@ -6565,9 +6977,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),
> @@ -6585,6 +6999,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),
> @@ -6605,6 +7022,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),
> @@ -6685,6 +7105,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,
> @@ -6731,14 +7165,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,12 +7205,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",
> @@ -6842,6 +7263,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
> @@ -7031,6 +7453,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,
> +                               meter_groups, &match, &actions,
> +                               sampling_apps, lflow_ref);
>      }
>
>      const struct ls_port_group *ls_pg =
> @@ -7047,6 +7472,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,
> +                                       meter_groups, &match, &actions,
> +                                       sampling_apps, lflow_ref);
>              }
>          }
>      }
> @@ -7691,8 +8119,11 @@ build_lb_rules(struct lflow_table *lflows, struct 
> ovn_lb_datapaths *lb_dps,
>
>  static void
>  build_stateful(struct ovn_datapath *od, struct lflow_table *lflows,
> +               const struct sampling_app_table *sampling_apps,
>                 struct lflow_ref *lflow_ref)
>  {
> +    uint8_t sample_domain_est =
> +        sampling_app_get_id(sampling_apps, SAMPLING_APP_ACL_EST_TRAFFIC);
>      struct ds actions = DS_EMPTY_INITIALIZER;
>
>      /* Ingress LB, Ingress and Egress stateful Table (Priority 0): Packets 
> are
> @@ -7709,8 +8140,13 @@ 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_format(&actions,
> +                 "ct_commit { "
> +                    "ct_mark.blocked = 0; "
> +                    "ct_label.obs_domain_id = %"PRIu8 "; "
> +                    "ct_label.obs_point_id = " REG_OBS_POINT_ID_EST "; "
> +                  "}; next;",
> +                  sample_domain_est);
>      ovn_lflow_add(lflows, od, S_SWITCH_IN_STATEFUL, 100,
>                    REGBIT_CONNTRACK_COMMIT" == 1 && "
>                    REGBIT_ACL_LABEL" == 1",
> @@ -8718,6 +9154,7 @@ build_lswitch_lflows_pre_acl_and_acl(
>      struct ovn_datapath *od,
>      struct lflow_table *lflows,
>      const struct shash *meter_groups,
> +    const struct sampling_app_table *sampling_apps,
>      struct lflow_ref *lflow_ref)
>  {
>      ovs_assert(od->nbs);
> @@ -8725,7 +9162,7 @@ build_lswitch_lflows_pre_acl_and_acl(
>      build_pre_lb(od, meter_groups, lflows, lflow_ref);
>      build_pre_stateful(od, lflows, lflow_ref);
>      build_qos(od, lflows, lflow_ref);
> -    build_stateful(od, lflows, lflow_ref);
> +    build_stateful(od, lflows, sampling_apps, lflow_ref);
>      build_vtep_hairpin(od, lflows, lflow_ref);
>  }
>
> @@ -15755,6 +16192,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,
> @@ -15764,7 +16202,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);
>  }
>
> @@ -15788,6 +16226,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
> @@ -15802,7 +16241,8 @@ build_lswitch_and_lrouter_iterate_by_ls(struct 
> ovn_datapath *od,
>  {
>      ovs_assert(od->nbs);
>      build_lswitch_lflows_pre_acl_and_acl(od, lsi->lflows,
> -                                         lsi->meter_groups, NULL);
> +                                         lsi->meter_groups,
> +                                         lsi->sampling_apps, NULL);
>
>      build_fwd_group_lflows(od, lsi->lflows, NULL);
>      build_lswitch_lflows_admission_control(od, lsi->lflows, NULL);
> @@ -16079,6 +16519,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);
>                  }
>              }
> @@ -16152,7 +16593,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);
> @@ -16186,6 +16628,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);
>
> @@ -16226,6 +16669,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,
>          };
> @@ -16298,6 +16742,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());
> @@ -16387,7 +16832,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;
> @@ -16811,6 +17257,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/ovn-northd.8.xml b/northd/ovn-northd.8.xml
> index b06b09ac5f..ca97a9b703 100644
> --- a/northd/ovn-northd.8.xml
> +++ b/northd/ovn-northd.8.xml
> @@ -899,6 +899,32 @@
>          next(pipeline=egress,table=5);}</code> action for SCTP associations.
>        </li>
>
> +      <li>
> +        For each ACL with sample_new configured a priority 1100 flow is
> +        installed that additionally matches on the saved observation_point_id
> +        value.  This flow also generates a <code>sample()</code> action along
> +        with the usual action processing for the type of ACL.
> +      </li>
> +
> +      <li>
> +        For each ACL with sample_est configured a priority 1200 flow is
> +        installed that additionally matches on the saved observation_point_id
> +        value for established traffic in the original direction.  This flow
> +        also generates a <code>sample()</code> action along with the usual
> +        action processing for the type of ACL.
> +      </li>
> +
> +      <li>
> +        For each ACL with sample_est configured a priority 1200 flow is
> +        installed that additionally matches on the saved observation_point_id
> +        value for established traffic in the reply direction.  This flow also
> +        generates a <code>sample()</code> action along with the usual action
> +        processing for the type of ACL.  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>
> +
>        <li>
>          If any ACLs have tiers configured on them, then three priority 500
>          flows are installed. If the current tier counter is 0, 1, or 2, then
> diff --git a/ovn-nb.ovsschema b/ovn-nb.ovsschema
> index e131a4c083..977d041d84 100644
> --- a/ovn-nb.ovsschema
> +++ b/ovn-nb.ovsschema
> @@ -1,7 +1,7 @@
>  {
>      "name": "OVN_Northbound",
>      "version": "7.4.0",
> -    "cksum": "1498303893 36355",
> +    "cksum": "83748782 38439",
>      "tables": {
>          "NB_Global": {
>              "columns": {
> @@ -30,6 +30,40 @@
>                  "ipsec": {"type": "boolean"}},
>              "maxRows": 1,
>              "isRoot": true},
> +        "Sample_Collector": {
> +            "columns": {
> +                "name": {"type": "string"},
> +                "probability": {"type": {"key": {
> +                    "type": "integer",
> +                    "minInteger": 0,
> +                    "maxInteger": 65535}}},
> +                "set_id": {"type": {"key": {
> +                    "type": "integer",
> +                    "minInteger": 0,
> +                    "maxInteger": 4294967295}}},
> +                "external_ids": {"type": {"key": "string", "value": "string",
> +                                          "min": 0, "max": "unlimited"}}
> +            },
> +            "indexes": [["name"]],
> +            "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}},
> +                "external_ids": {"type": {"key": "string", "value": "string",
> +                                          "min": 0, "max": "unlimited"}}
> +            },
> +            "indexes": [["metadata"]],
> +            "isRoot": true
> +        },
>          "Copp": {
>              "columns": {
>                  "name": {"type": "string"},
> @@ -275,6 +309,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 f2a8b5c076..4e010272a4 100644
> --- a/ovn-nb.xml
> +++ b/ovn-nb.xml
> @@ -490,6 +490,48 @@
>
>    </table>
>
> +  <table name="Sample_Collector" title="Sample_Collector">
> +    <column name="name">
> +      Sample collector name.
> +    </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 32-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>
> +    <column name="external_ids">
> +      See <em>External IDs</em> at the beginning of this document.
> +    </column>
> +  </table>
>    <table name="Copp" title="Control plane protection">
>      <p>
>        This table is used to define control plane protection policies, i.e.,
> @@ -2500,6 +2542,20 @@ 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.
> +      </p>
> +    </column>

Should we mention that "sample_new" is also used for stateless ACLs?
Maybe it's just me but I don't find it obvious which of them (new or est)
should semantically apply in that case.

> +
> +    <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>
> +    </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-macros.at b/tests/ovn-macros.at
> index 47ada5c70e..863d85daf4 100644
> --- a/tests/ovn-macros.at
> +++ b/tests/ovn-macros.at
> @@ -1049,6 +1049,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])])
> diff --git a/tests/ovn-nbctl.at b/tests/ovn-nbctl.at
> index 31de309215..0905a021eb 100644
> --- a/tests/ovn-nbctl.at
> +++ b/tests/ovn-nbctl.at
> @@ -2802,6 +2802,26 @@ check_row_count nb:ACL 0
>
>  dnl ---------------------------------------------------------------------
>
> +OVN_NBCTL_TEST([acl_sampling], [ACL sampling operations], [
> +check ovn-nbctl ls-add ls
> +sample1=$(ovn-nbctl create sample metadata=4301)
> +sample2=$(ovn-nbctl create sample metadata=4302)
> +check_row_count nb:Sample 2
> +
> +check ovn-nbctl --sample-new=$sample1 acl-add ls from-lport 1 1 allow-related
> +check_column "$sample1" nb:ACL sample_new priority=1
> +
> +check ovn-nbctl --sample-est=$sample2 acl-add ls from-lport 2 1 allow-related
> +check_column "" nb:ACL sample_new priority=2
> +check_column "$sample2" nb:ACL sample_est priority=2
> +
> +check ovn-nbctl --sample-new=$sample1 --sample-est=$sample2 acl-add ls 
> from-lport 3 1 allow-related
> +check_column "$sample1" nb:ACL sample_new priority=3
> +check_column "$sample2" nb:ACL sample_est priority=3
> +])
> +
> +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 3fb883225a..a6843ce998 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -4604,7 +4604,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_domain_id = 0; ct_label.obs_point_id = reg9; }; next;)
>  ])
>
>      AT_CHECK_UNQUOTED([grep "ls_out_pre_lb" sw0flows | ovn_strip_lflows], 
> [0], [dnl
> @@ -4628,7 +4628,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_domain_id = 0; ct_label.obs_point_id = reg9; }; next;)
>  ])
>  }
>
> @@ -4670,7 +4670,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_domain_id = 0; ct_label.obs_point_id = reg9; }; next;)
>  ])
>
>  AT_CHECK([grep "ls_out_pre_lb" sw0flows | ovn_strip_lflows], [0], [dnl
> @@ -4691,7 +4691,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_domain_id = 0; ct_label.obs_point_id = reg9; }; next;)
>  ])
>
>  # LB with event=false and reject=false
> @@ -4720,23 +4720,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_domain_id = 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_domain_id = 0; ct_label.obs_point_id = reg9; }; next;)
>  ])
>
>  # Add new ACL without label
> @@ -4747,27 +4747,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_domain_id = 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_domain_id = 0; ct_label.obs_point_id = reg9; }; next;)
>  ])
>
>  # Delete new ACL with label
> @@ -4784,7 +4784,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_domain_id = 0; ct_label.obs_point_id = reg9; }; next;)
>  ])
>
>  AT_CHECK([grep -w "ls_out_acl_eval" sw0flows | grep 2002 | 
> ovn_strip_lflows], [0], [dnl
> @@ -4794,7 +4794,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_domain_id = 0; ct_label.obs_point_id = reg9; }; next;)
>  ])
>  AT_CLEANUP
>  ])
> @@ -4822,7 +4822,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_domain_id = 0; 
> ct_label.obs_point_id = reg9; };
>  ])
>
>  AS_BOX([from-lport --apply-after-lb allow-related ACL])
> @@ -4830,7 +4830,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_domain_id = 0; 
> ct_label.obs_point_id = reg9; };
>  ])
>
>  AS_BOX([to-lport allow-related ACL])
> @@ -4838,7 +4838,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_domain_id = 0; 
> ct_label.obs_point_id = reg9; };
>  ])
>
>  AT_CLEANUP
> @@ -7658,7 +7658,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_domain_id = 0; ct_label.obs_point_id = reg9; }; next;)
>  ])
>
>  AS_BOX([Remove and add the ACLs back with the apply-after-lb option])
> @@ -7713,7 +7713,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_domain_id = 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])
> @@ -7768,7 +7768,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_domain_id = 0; ct_label.obs_point_id = reg9; }; next;)
>  ])
>
>  AT_CLEANUP
> @@ -12491,6 +12491,198 @@ 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 name=test-collector1 
> probability=65535 set_id=1)
> +collector2=$(ovn-nbctl create Sample_Collector name=test-collector2 
> probability=65535 set_id=2)
> +check_row_count nb:Sample_Collector 2
> +
> +ovn-nbctl create Sampling_App name="acl-new-traffic-sampling" id="42"
> +ovn-nbctl create Sampling_App name="acl-est-traffic-sampling" id="43"
> +check_row_count nb:Sampling_App 2
> +
> +sample1=$(ovn-nbctl create Sample collector="$collector1 $collector2" 
> metadata=4301)
> +sample2=$(ovn-nbctl create Sample collector="$collector1 $collector2" 
> metadata=4302)
> +check_row_count nb:Sample 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"
> +
> +AS_BOX([from-lport ACL sampling (new, est)])
> +check ovn-nbctl acl-del ls
> +check ovn-nbctl --wait=sb --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_action -e ls_in_acl_eval 
> -e ls_out_acl_action | ovn_strip_lflows | ovn_strip_collector_set | grep -e 
> reg3 -e reg9 -e sample], [0], [dnl
> +  table=??(ls_in_acl_action   ), priority=1100 , match=(ip && ct.new && 
> reg8[[16]] == 1 && 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);
>  reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
> +  table=??(ls_in_acl_action   ), priority=1200 , match=(ip && ct.trk && 
> (ct.est || ct.rel) && !ct.rpl && reg8[[16]] == 1 && ct_label.obs_domain_id == 
> 43 && 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);
>  reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
> +  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_out_acl_action  ), priority=1200 , match=(ip && ct.trk && 
> (ct.est || ct.rel) && ct.rpl && reg8[[16]] == 1 && ct_label.obs_domain_id == 
> 43 && 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);
>  reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
> +])
> +
> +dnl Trace new connections.
> +flow="$base_flow"
> +AT_CHECK_UNQUOTED([ovn_trace --ct new ls "$flow" | grep -e sample -e commit 
> -e reg9 | sort], [0], [dnl
> +    ct_commit { ct_mark.blocked = 0; ct_label.obs_domain_id = 43; 
> ct_label.obs_point_id = reg9; };
> +    reg9 = 4302;
> +    sample(probability=65535,collector_set=1,obs_domain=42,obs_point=4301);
> +    sample(probability=65535,collector_set=2,obs_domain=42,obs_point=4301);
> +])
> +
> +dnl Trace estasblished connections.
> +flow="$base_flow && ct_label.obs_domain_id == 43 && ct_label.obs_point_id == 
> 4302"
> +AT_CHECK_UNQUOTED([ovn_trace --ct est ls "$flow" | grep -e sample -e commit 
> -e reg9 | sort], [0], [dnl
> +    reg9 = 4302;
> +    sample(probability=65535,collector_set=1,obs_domain=43,obs_point=4302);
> +    sample(probability=65535,collector_set=2,obs_domain=43,obs_point=4302);
> +])
> +
> +AS_BOX([from-lport ACL sampling (new)])
> +check ovn-nbctl acl-del ls
> +check ovn-nbctl --wait=sb --sample-new=$sample1 acl-add ls from-lport 1 "1" 
> allow-related
> +AT_CHECK([ovn-sbctl lflow-list | grep -e ls_in_acl_action -e ls_in_acl_eval 
> -e ls_out_acl_action | ovn_strip_lflows | ovn_strip_collector_set | grep -e 
> reg3 -e reg9 -e sample], [0], [dnl
> +  table=??(ls_in_acl_action   ), priority=1100 , match=(ip && ct.new && 
> reg8[[16]] == 1 && 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);
>  reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
> +  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;)
> +])
> +
> +dnl Trace new connections.
> +flow="$base_flow"
> +AT_CHECK_UNQUOTED([ovn_trace --ct new ls "$flow" | grep -e sample -e commit 
> -e reg9 | sort], [0], [dnl
> +    ct_commit { ct_mark.blocked = 0; ct_label.obs_domain_id = 43; 
> ct_label.obs_point_id = reg9; };
> +    reg9 = 0;
> +    sample(probability=65535,collector_set=1,obs_domain=42,obs_point=4301);
> +    sample(probability=65535,collector_set=2,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_domain_id == 43 && ct_label.obs_point_id == 
> 0"
> +AT_CHECK_UNQUOTED([ovn_trace --ct est ls "$flow" | grep -e sample -e commit 
> -e reg9 | sort], [0], [dnl
> +    reg9 = 0;
> +])
> +
> +AS_BOX([from-lport-after-lb ACL sampling (new, est)])
> +check ovn-nbctl acl-del ls
> +check ovn-nbctl --wait=sb --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_action -e 
> ls_in_acl_after_lb_eval -e ls_out_acl_action | ovn_strip_lflows | 
> ovn_strip_collector_set | grep -e reg3 -e reg9 -e sample], [0], [dnl
> +  table=??(ls_in_acl_after_lb_action), priority=1100 , match=(ip && ct.new 
> && reg8[[16]] == 1 && 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);
>  reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
> +  table=??(ls_in_acl_after_lb_action), priority=1200 , match=(ip && ct.trk 
> && (ct.est || ct.rel) && !ct.rpl && reg8[[16]] == 1 && ct_label.obs_domain_id 
> == 43 && 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);
>  reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
> +  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_out_acl_action  ), priority=1200 , match=(ip && ct.trk && 
> (ct.est || ct.rel) && ct.rpl && reg8[[16]] == 1 && ct_label.obs_domain_id == 
> 43 && 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);
>  reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
> +])
> +
> +dnl Trace new connections.
> +flow="$base_flow"
> +AT_CHECK_UNQUOTED([ovn_trace --ct new ls "$flow" | grep -e sample -e commit 
> -e reg9 | sort], [0], [dnl
> +    ct_commit { ct_mark.blocked = 0; ct_label.obs_domain_id = 43; 
> ct_label.obs_point_id = reg9; };
> +    reg9 = 4302;
> +    sample(probability=65535,collector_set=1,obs_domain=42,obs_point=4301);
> +    sample(probability=65535,collector_set=2,obs_domain=42,obs_point=4301);
> +])
> +
> +dnl Trace estasblished connections.
> +flow="$base_flow && ct_label.obs_domain_id == 43 && ct_label.obs_point_id == 
> 4302"
> +AT_CHECK_UNQUOTED([ovn_trace --ct est ls "$flow" | grep -e sample -e commit 
> -e reg9 | sort], [0], [dnl
> +    reg9 = 4302;
> +    sample(probability=65535,collector_set=1,obs_domain=43,obs_point=4302);
> +    sample(probability=65535,collector_set=2,obs_domain=43,obs_point=4302);
> +])
> +
> +AS_BOX([from-lport-after-lb ACL sampling (new)])
> +check ovn-nbctl acl-del ls
> +check ovn-nbctl --wait=sb --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_action -e 
> ls_in_acl_after_lb_eval -e ls_out_acl_action | ovn_strip_lflows | 
> ovn_strip_collector_set | grep -e reg3 -e reg9 -e sample], [0], [dnl
> +  table=??(ls_in_acl_after_lb_action), priority=1100 , match=(ip && ct.new 
> && reg8[[16]] == 1 && 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);
>  reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
> +  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;)
> +])
> +
> +dnl Trace new connections.
> +flow="$base_flow"
> +AT_CHECK_UNQUOTED([ovn_trace --ct new ls "$flow" | grep -e sample -e commit 
> -e reg9 | sort], [0], [dnl
> +    ct_commit { ct_mark.blocked = 0; ct_label.obs_domain_id = 43; 
> ct_label.obs_point_id = reg9; };
> +    reg9 = 0;
> +    sample(probability=65535,collector_set=1,obs_domain=42,obs_point=4301);
> +    sample(probability=65535,collector_set=2,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_domain_id == 43 && ct_label.obs_point_id == 
> 0"
> +AT_CHECK_UNQUOTED([ovn_trace --ct est ls "$flow" | grep -e sample -e commit 
> -e reg9 | sort], [0], [dnl
> +    reg9 = 0;
> +])
> +
> +AS_BOX([to-lport ACL sampling (new, est)])
> +check ovn-nbctl acl-del ls
> +check ovn-nbctl --wait=sb --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_action -e 
> ls_out_acl_eval -e ls_in_acl_action | ovn_strip_lflows | 
> ovn_strip_collector_set | grep -e reg3 -e reg9 -e sample], [0], [dnl
> +  table=??(ls_in_acl_action   ), priority=1200 , match=(ip && ct.trk && 
> (ct.est || ct.rel) && ct.rpl && reg8[[16]] == 1 && ct_label.obs_domain_id == 
> 43 && 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);
>  reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
> +  table=??(ls_out_acl_action  ), priority=1100 , match=(ip && ct.new && 
> reg8[[16]] == 1 && 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);
>  reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; next;)
> +  table=??(ls_out_acl_action  ), priority=1200 , match=(ip && ct.trk && 
> (ct.est || ct.rel) && !ct.rpl && reg8[[16]] == 1 && ct_label.obs_domain_id == 
> 43 && 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);
>  reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; 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;)
> +])
> +
> +dnl Trace new connections.
> +flow="$base_flow"
> +AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new ls "$flow" | grep -e sample 
> -e commit -e reg9 | sort], [0], [dnl
> +    ct_commit { ct_mark.blocked = 0; ct_label.obs_domain_id = 43; 
> ct_label.obs_point_id = reg9; };
> +    ct_commit { ct_mark.blocked = 0; };
> +    reg9 = 4302;
> +    sample(probability=65535,collector_set=1,obs_domain=42,obs_point=4301);
> +    sample(probability=65535,collector_set=2,obs_domain=42,obs_point=4301);
> +])
> +
> +dnl Trace estasblished connections.
> +flow="$base_flow && ct_label.obs_domain_id == 43 && ct_label.obs_point_id == 
> 4302"
> +AT_CHECK_UNQUOTED([ovn_trace --ct est --ct est ls "$flow" | grep -e sample 
> -e commit -e reg9 | sort], [0], [dnl
> +    reg9 = 4302;
> +    sample(probability=65535,collector_set=1,obs_domain=43,obs_point=4302);
> +    sample(probability=65535,collector_set=2,obs_domain=43,obs_point=4302);
> +])
> +
> +AS_BOX([to-lport ACL sampling (new)])
> +check ovn-nbctl acl-del ls
> +check ovn-nbctl --wait=sb --sample-new=$sample1 acl-add ls to-lport 1 "1" 
> allow-related
> +AT_CHECK([ovn-sbctl lflow-list | grep -e ls_out_acl_action -e 
> ls_out_acl_eval -e ls_in_acl_action | ovn_strip_lflows | 
> ovn_strip_collector_set | grep -e reg3 -e reg9 -e sample], [0], [dnl
> +  table=??(ls_out_acl_action  ), priority=1100 , match=(ip && ct.new && 
> reg8[[16]] == 1 && 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);
>  reg8[[16]] = 0; reg8[[17]] = 0; reg8[[18]] = 0; 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;)
> +])
> +
> +dnl Trace new connections.
> +flow="$base_flow"
> +AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new ls "$flow" | grep -e sample 
> -e commit -e reg9 | sort], [0], [dnl
> +    ct_commit { ct_mark.blocked = 0; ct_label.obs_domain_id = 43; 
> ct_label.obs_point_id = reg9; };
> +    ct_commit { ct_mark.blocked = 0; };
> +    reg9 = 0;
> +    sample(probability=65535,collector_set=1,obs_domain=42,obs_point=4301);
> +    sample(probability=65535,collector_set=2,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_domain_id == 43 && ct_label.obs_point_id == 
> 0"
> +AT_CHECK_UNQUOTED([ovn_trace --ct est --ct est ls "$flow" | grep -e sample 
> -e commit -e reg9 | sort], [0], [dnl
> +    reg9 = 0;
> +])
> +
> +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 877d0dfdba..189a5e083a 100644
> --- a/tests/ovn.at
> +++ b/tests/ovn.at
> @@ -329,6 +329,9 @@ 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_domain_id = ct_label[64..95]
> +ct_label.obs_point_id = ct_label[96..127]
> +ct_label.obs_unused = ct_label[0..63]
>  ct_mark = NXM_NX_CT_MARK
>  ct_mark.blocked = ct_mark[0]
>  ct_mark.ecmp_reply_port = ct_mark[16..31]
> 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 ddb3d14e92..14f12524c0 100644
> --- a/tests/system-ovn.at
> +++ b/tests/system-ovn.at
> @@ -13022,3 +13022,152 @@ 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_TCPDUMP = no])
> +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 3 UDP load balancers (ports 1000, 2000, 3000)
> +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:2000 42.42.42.3:2000 udp           \
> +    -- lb-add lb3 43.43.43.43:3000 42.42.42.3:3000 udp           \
> +    -- ls-lb-add ls lb1                                          \
> +    -- ls-lb-add ls lb2                                          \
> +    -- ls-lb-add ls lb3
> +
> +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 name=test-collector1 
> probability=65535 set_id=1)
> +collector2=$(ovn-nbctl create Sample_Collector name=test-collector2 
> probability=65535 set_id=2)
> +check_row_count nb:Sample_Collector 2
> +
> +ovn-nbctl create Sampling_App name="acl-new-traffic-sampling" id="42"
> +ovn-nbctl create Sampling_App name="acl-est-traffic-sampling" id="43"
> +check_row_count nb:Sampling_App 2
> +
> +sample1=$(ovn-nbctl create Sample collector="$collector1 $collector2" 
> metadata=1001)
> +sample2=$(ovn-nbctl create Sample collector="$collector1 $collector2" 
> metadata=1002)
> +sample3=$(ovn-nbctl create Sample collector="$collector1 $collector2" 
> metadata=2001)
> +sample4=$(ovn-nbctl create Sample collector="$collector1 $collector2" 
> metadata=2002)
> +sample5=$(ovn-nbctl create Sample collector="$collector1 $collector2" 
> metadata=3001)
> +sample6=$(ovn-nbctl create Sample collector="$collector1 $collector2" 
> metadata=3002)
> +check_row_count nb:Sample 6
> +
> +dnl Create ACLs that match the 3 types of traffic in all 3 possible stages:
> +dnl from-lport, from-lport-after-lb, to-lport
> +check ovn-nbctl --sample-new=$sample1 --sample-est=$sample2 acl-add ls 
> from-lport 1 "inport == \"vm1\" && udp.dst == 1000" allow-related
> +check ovn-nbctl --apply-after-lb --sample-new=$sample3 --sample-est=$sample4 
> acl-add ls from-lport 1 "inport == \"vm1\" && udp.dst == 2000" allow-related
> +check ovn-nbctl --sample-new=$sample5 --sample-est=$sample6 acl-add ls 
> to-lport 1 "outport == \"vm2\" && udp.dst == 3000" allow-related
> +
> +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=1 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 2000], [nc-vm2-2000.pid])
> +NETNS_DAEMONIZE([vm2], [nc -e /bin/cat -k -u -v -l 3000], [nc-vm2-3000.pid])
> +
> +dnl Send traffic to the UDP LB1 (hits the from-lport ACL).
> +NS_CHECK_EXEC([vm1], [echo a | nc --send-only -u 43.43.43.43 1000])
> +
> +dnl Send traffic to the UDP LB1 (hits the from-lport after-lb ACL).
> +NS_CHECK_EXEC([vm1], [echo a | nc --send-only -u 43.43.43.43 2000])
> +
> +dnl Send traffic to the UDP LB1 (hits the from-lport ACL).
> +NS_CHECK_EXEC([vm1], [echo a | nc --send-only -u 43.43.43.43 3000])
> +
> +dnl Wait until OVS sampled all expected packets (2 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=9'])
> +
> +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([nfdump -r nfcapd.* -o json | grep observationPointID | awk 
> '{$1=$1;print}' | sort], [0], [dnl
> +"observationPointID" : 1001,
> +"observationPointID" : 1002,
> +"observationPointID" : 1002,
> +"observationPointID" : 2001,
> +"observationPointID" : 2002,
> +"observationPointID" : 2002,
> +"observationPointID" : 3001,
> +"observationPointID" : 3002,
> +"observationPointID" : 3002,
> +])
> +
> +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 c37cc010ce..816f26cf4b 100644
> --- a/utilities/ovn-nbctl.c
> +++ b/utilities/ovn-nbctl.c
> @@ -2308,6 +2308,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
> @@ -2321,6 +2326,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);
>  }
>
> @@ -2372,6 +2379,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);
>      }
> @@ -2388,6 +2397,38 @@ nbctl_acl_add(struct ctl_context *ctx)
>      if (meter) {
>          nbrec_acl_set_meter(acl, meter);
>      }
> +    if (sample_new) {
> +        const struct nbrec_sample *sample_elem = NULL;
> +        struct uuid sample_uuid;
> +
> +        if (uuid_from_string(&sample_uuid, sample_new)) {
> +            sample_elem = nbrec_sample_get_for_uuid(ctx->idl, &sample_uuid);
> +            if (!sample_elem) {
> +                ctl_error(ctx, "sample record not found");
> +                return;
> +            }
> +            nbrec_acl_set_sample_new(acl, sample_elem);
> +        } else {
> +            ctl_error(ctx, "a valid uuid must be provided");
> +            return;
> +        }
> +    }
> +    if (sample_est) {
> +        const struct nbrec_sample *sample_elem = NULL;
> +        struct uuid sample_uuid;
> +
> +        if (uuid_from_string(&sample_uuid, sample_est)) {
> +            sample_elem = nbrec_sample_get_for_uuid(ctx->idl, &sample_uuid);
> +            if (!sample_elem) {
> +                ctl_error(ctx, "sample record not found");
> +                return;
> +            }
> +            nbrec_acl_set_sample_est(acl, sample_elem);
> +        } else {
> +            ctl_error(ctx, "a valid uuid must be provided");
> +            return;
> +        }
> +    }
>
>      /* Set the ACL label */
>      const char *label = shash_find_data(&ctx->options, "--label");
> @@ -7915,7 +7956,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}",
> --
> 2.44.0
>

_______________________________________________
dev mailing list
d...@openvswitch.org
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to