ACL sampling samples drop and reject verdicts only for packets that
create a new connection ("ip && ct.new").  On a logical switch with
stateful ACLs a deny verdict applies to every packet matching the ACL,
regardless of its conntrack state, so two classes of denied traffic were
never attributed to the deny ACL:

- untracked traffic, e.g. traffic entering the logical switch from a
  router port (skip_port_from_conntrack()).  from-lport ACLs only matched
  "ct.new"; the "(ct.new || !ct.trk)" exception introduced by
  d15b12da6 ("northd: Add ACL Sampling.") only covers to-lport ACLs.

- traffic of a session that was allowed by a sampled allow-related ACL
  and is later denied by a new drop/reject ACL.  The packet is dropped,
  but it was sampled by the established-session sampler of the
  allow-related ACL (ct_label.obs_point_id), i.e. attributed to the ACL
  that admitted the session instead of the one that denied the packet.

Add a per-ACL logical flow in the ACL sample stages, at priority 1300
(above the established-session samplers), that samples packets carrying
a drop or reject verdict and the observation point of the ACL, for all
conntrack states.  The flow is only installed for drop/reject ACLs with
sample_new on logical switches with stateful ACLs; stateless switches
keep the existing sampler.  The flow matches on the observation stage
too, so that observation data recorded by a different ACL stage is not
attributed to the verdict.  It relies on the previous commit clearing
the observation data of a "pass" ACL before the next tier, so no tier
match is needed and deny ACLs sharing a Sample share one flow.

sample_est of a drop/reject ACL does not install a sampler.  When such
an ACL also has sample_new, the deny verdict sampler takes precedence
over the established-session samplers; the sample_est observation point
is still stored in the ct_label of the blocked connection.  A
drop/reject ACL with only sample_est keeps its current behavior.

Behavior change: a packet of an established session that is denied by a
sampled drop/reject ACL is now reported with the deny ACL's sample_new
observation point (acl-new domain) instead of the allow-related ACL's
sample_est observation point (acl-est domain).

Add northd unit tests and a system test that keeps a single UDP socket
open while the session is first allowed and then denied, and captures
the IPFIX records of the denied established and untracked traffic.

Fixes: d15b12da6fe6 ("northd: Add ACL Sampling.")
Assisted-by: OpenAI Codex
Assisted-by: Claude Code
Signed-off-by: Chanyeol Yoon <[email protected]>
---
 NEWS                |   5 +
 northd/northd.c     |  44 +++++++++
 ovn-nb.xml          |  19 +++-
 tests/ovn-northd.at | 155 ++++++++++++++++++++++++++++++-
 tests/system-ovn.at | 222 ++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 442 insertions(+), 3 deletions(-)

diff --git a/NEWS b/NEWS
index d41626a90..973fa8281 100644
--- a/NEWS
+++ b/NEWS
@@ -3,6 +3,11 @@ Post v26.09.0
    - ACL sampling: observation data recorded by a sampled "pass" ACL is no
      longer inherited by the ACL that determines the verdict in the next
      tier.
+   - ACL sampling: "drop" and "reject" ACLs on logical switches with stateful
+     ACLs now sample all denied traffic, including untracked traffic and
+     traffic of sessions established before the ACL was added.  Such traffic
+     was previously not sampled or, for established sessions, attributed to
+     the "allow-related" ACL that originally admitted the session.
    - Learn ARP sender hardware address (arp.sha) and IPv6 Neighbor
      Discovery link-layer addresses (nd.tll, nd.sll) into the FDB
      table in addition to eth.src.  This allows VRRP virtual MACs to
diff --git a/northd/northd.c b/northd/northd.c
index ccbf1e759..215da65c7 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -7937,6 +7937,42 @@ acl_use_generic_sample_flows(const struct nbrec_sample 
*sample,
     return sample && sample->n_collectors == 1 && features->sample_with_reg;
 }
 
+/* This builds a logical flow that samples traffic dropped or rejected by a
+ * stateful-switch ACL that has sampling enabled.  Unlike allowed traffic,
+ * a drop/reject verdict applies to every packet that matches the ACL,
+ * regardless of its connection tracking state: untracked packets and
+ * packets of previously established connections are denied too.  Sample
+ * all of them. */
+static void
+build_acl_sample_drop_flows(const struct ovn_datapath *od,
+                            struct lflow_table *lflows,
+                            const struct ovn_stage *stage,
+                            enum acl_observation_stage obs_stage,
+                            struct ds *match, struct ds *actions,
+                            const struct nbrec_acl *acl,
+                            uint8_t sample_domain_id,
+                            struct lflow_ref *lflow_ref)
+{
+    ds_clear(match);
+    ds_clear(actions);
+
+    ds_put_cstr(match, "ip && "REGBIT_ACL_LABEL" == 1 && ("
+                       REGBIT_ACL_VERDICT_DROP" == 1 || "
+                       REGBIT_ACL_VERDICT_REJECT" == 1) && ");
+    build_acl_sample_register_match(match, acl, acl->sample_new);
+    /* Only consider observation data recorded by this ACL stage.  Data
+     * recorded by other stages (e.g., by a from-lport ACL when evaluating
+     * to-lport ACLs) must not be attributed to this verdict. */
+    ds_put_format(match, " && "REGBIT_ACL_OBS_STAGE" == %"PRIu8,
+                  (uint8_t) obs_stage);
+    build_acl_sample_action(actions, acl, acl->sample_new, sample_domain_id);
+
+    /* The current deny verdict takes precedence over the established
+     * connection samplers (priority 1200 and lower). */
+    ovn_lflow_add(lflows, od, stage, 1300, ds_cstr(match), ds_cstr(actions),
+                  lflow_ref, WITH_HINT(&acl->header_));
+}
+
 /* This builds all ACL sampling related logical flows:
  * - for packets creating new connections
  * - for packets that are part of an existing connection
@@ -7990,6 +8026,14 @@ build_acl_sample_flows(const struct ls_stateful_record 
*ls_stateful_rec,
     uint8_t sample_est_domain_id = sampling_app_get_id(sampling_apps,
                                                        SAMPLING_APP_ACL_EST);
 
+    if (ls_stateful_rec->has_stateful_acl && acl->sample_new
+        && (!strcmp(acl->action, "drop") || !strcmp(acl->action, "reject"))) {
+        build_acl_sample_drop_flows(od, lflows, stage, obs_stage, match,
+                                    actions, acl, sample_new_domain_id,
+                                    lflow_ref);
+        return;
+    }
+
     if (acl_use_generic_sample_flows(acl->sample_new, features)) {
         build_acl_sample_generic_new_flows(od, lflows, stage, obs_stage,
                                            match, actions,
diff --git a/ovn-nb.xml b/ovn-nb.xml
index f72fb688b..75ac79a93 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -3199,6 +3199,17 @@ or
         this is used for sampling all traffic matched by the ACL.
       </p>
 
+      <p>
+        For <code>drop</code> and <code>reject</code> ACLs on a logical
+        switch with stateful ACLs this is also used for sampling all traffic
+        matched by the ACL, regardless of its connection tracking state.
+        Untracked traffic (e.g., traffic entering the logical switch from a
+        router port) and traffic that belongs to sessions established before
+        the ACL was added are denied, and therefore sampled, too.  This
+        sampler takes precedence over the established session samplers of
+        <code>allow-related</code> ACLs.
+      </p>
+
       <p>
         Sampling of a <code>pass</code> ACL follows the connection state
         rules of the ACL sample stage, in the tier of the <code>pass</code>
@@ -3218,7 +3229,13 @@ or
     <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.
+        established/related sessions matched by this ACL.  Only
+        <code>allow-related</code> ACLs install samplers for
+        established/related sessions.  For <code>drop</code> and
+        <code>reject</code> ACLs that also have <ref column="sample_new"/>
+        configured, the deny verdict sampler takes precedence; the
+        observation point ID of this entry is still stored in the connection
+        tracking label of the blocked session.
       </p>
 
       <p>
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index ab3e62529..d5362b349 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -15566,6 +15566,153 @@ OVN_CLEANUP_NORTHD
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([ACL Sampling - deny verdicts on stateful switch])
+AT_KEYWORDS([acl sampling])
+
+ovn_start
+
+check ovn-sbctl chassis-add gw1 geneve 127.0.0.1 \
+  -- set chassis gw1 other_config:ovn-sample-with-registers="true"
+
+collector1=$(ovn-nbctl create Sample_Collector id=1 name=c1 probability=65535 
set_id=100)
+check_row_count nb:Sample_Collector 1
+
+check_uuid ovn-nbctl create Sampling_App type="acl-new" id="42"
+check_uuid ovn-nbctl create Sampling_App type="acl-est" id="43"
+check_row_count nb:Sampling_App 2
+
+dnl Traffic entering the switch from the router port is not sent to
+dnl conntrack (skip_port_from_conntrack), so it is always untracked.
+check ovn-nbctl                                      \
+  -- lr-add lr                                       \
+  -- lrp-add lr lrp1 00:00:00:00:01:00 42.42.42.1/24 \
+  -- ls-add ls                                       \
+  -- lsp-add ls vm1                                  \
+  -- lsp-set-addresses vm1 00:00:00:00:00:01         \
+  -- lsp-add ls vm2                                  \
+  -- lsp-set-addresses vm2 00:00:00:00:00:02         \
+  -- lsp-add-router-port ls ls-lr lrp1
+check ovn-nbctl --wait=sb sync
+
+rp_flow="inport == \"ls-lr\" && eth.src == 00:00:00:00:01:00 && eth.dst == 
00:00:00:00:00:01 && ip4.src == 10.0.0.1 && ip4.dst == 42.42.42.2 && tcp"
+vm_flow="inport == \"vm1\" && eth.src == 00:00:00:00:00:01 && eth.dst == 
00:00:00:00:00:02 && ip4.src == 42.42.42.2 && ip4.dst == 42.42.42.3 && tcp"
+m4_define([TRACE_FILTER], [grep -e sample -e commit | grep -v _sample | sort])
+
+dnl Stateful allow-related ACLs for vm1 with sampling enabled, plus
+dnl sampled drop/reject ACLs in every ACL stage.
+check_uuid ovn-nbctl --wait=sb                                                 
\
+  -- --id=@sgn create Sample collector="$collector1" metadata=4301             
\
+  -- --id=@sge create Sample collector="$collector1" metadata=4302             
\
+  -- --sample-new=@sgn --sample-est=@sge                                       
\
+     acl-add ls from-lport 1 'inport == "vm1"' allow-related                   
\
+  -- --id=@sgn2 create Sample collector="$collector1" metadata=4303            
\
+  -- --id=@sge2 create Sample collector="$collector1" metadata=4304            
\
+  -- --sample-new=@sgn2 --sample-est=@sge2                                     
\
+     acl-add ls to-lport 1 'outport == "vm1"' allow-related                    
\
+  -- --id=@d1 create Sample collector="$collector1" metadata=4401              
\
+  -- --sample-new=@d1 acl-add ls from-lport 2 'tcp.dst == 22' drop             
\
+  -- --id=@d2 create Sample collector="$collector1" metadata=4402              
\
+  -- --sample-new=@d2 acl-add ls from-lport 2 'tcp.dst == 23' reject           
\
+  -- --id=@d3 create Sample collector="$collector1" metadata=4403              
\
+  -- --sample-new=@d3 acl-add ls to-lport 2 'tcp.dst == 24' drop               
\
+  -- --id=@d4 create Sample collector="$collector1" metadata=4404              
\
+  -- --apply-after-lb --sample-new=@d4 acl-add ls from-lport 2 'tcp.dst == 25' 
drop
+check_row_count nb:ACL 6
+
+dnl Drop/reject ACLs on a switch with stateful ACLs sample every packet that
+dnl gets the deny verdict, regardless of the connection tracking state.
+AT_CHECK([ovn-sbctl lflow-list ls | grep -e ls_in_acl_sample -e 
ls_in_acl_after_lb_sample -e ls_out_acl_sample | ovn_strip_lflows | 
ovn_strip_collector_set | grep -e 'reg3 == 440'], [0], [dnl
+  table=??(ls_in_acl_after_lb_sample), priority=1300 , match=(ip && reg0[[13]] 
== 1 && (reg8[[17]] == 1 || reg8[[18]] == 1) && reg3 == 4404 && reg8[[19..20]] 
== 1), action=(flags.pkt_sampled = 1; 
sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4404); next;)
+  table=??(ls_in_acl_sample   ), priority=1300 , match=(ip && reg0[[13]] == 1 
&& (reg8[[17]] == 1 || reg8[[18]] == 1) && reg3 == 4401 && reg8[[19..20]] == 
0), action=(flags.pkt_sampled = 1; 
sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4401); next;)
+  table=??(ls_in_acl_sample   ), priority=1300 , match=(ip && reg0[[13]] == 1 
&& (reg8[[17]] == 1 || reg8[[18]] == 1) && reg3 == 4402 && reg8[[19..20]] == 
0), action=(flags.pkt_sampled = 1; 
sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4402); next;)
+  table=??(ls_out_acl_sample  ), priority=1300 , match=(ip && reg0[[13]] == 1 
&& (reg8[[17]] == 1 || reg8[[18]] == 1) && reg3 == 4403 && reg8[[19..20]] == 
2), action=(flags.pkt_sampled = 1; 
sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4403); next;)
+])
+
+AS_BOX([from-lport drop: untracked traffic from the router port])
+AT_CHECK_UNQUOTED([ovn_trace ls "$rp_flow && tcp.dst == 22" | TRACE_FILTER], 
[0], [dnl
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4401);
+])
+
+AS_BOX([from-lport reject: untracked traffic from the router port])
+AT_CHECK_UNQUOTED([ovn_trace ls "$rp_flow && tcp.dst == 23" | TRACE_FILTER], 
[0], [dnl
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4402);
+])
+
+AS_BOX([from-lport drop: new connection])
+AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new ls "$vm_flow && tcp.dst == 22" 
| TRACE_FILTER], [0], [dnl
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4401);
+])
+
+AS_BOX([from-lport drop: established connection previously allowed by the 
allow-related ACL])
+dnl The deny verdict is sampled.  The established-connection sampler of the
+dnl allow-related ACL (obs_domain 43, obs_point 4302) must not be hit.
+flow="$vm_flow && tcp.dst == 22 && ct_label.obs_point_id == 4302 && 
ct_mark.obs_collector_id == 1 && ct_mark.obs_stage == 0"
+AT_CHECK_UNQUOTED([ovn_trace --ct est --ct est ls "$flow" | TRACE_FILTER], 
[0], [dnl
+    ct_commit { ct_mark.blocked = 1; ct_label.obs_point_id = 0; };
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4401);
+])
+
+AS_BOX([from-lport allow-related: established connection is still sampled])
+flow="$vm_flow && tcp.dst == 80 && ct_label.obs_point_id == 4302 && 
ct_mark.obs_collector_id == 1 && ct_mark.obs_stage == 0"
+AT_CHECK_UNQUOTED([ovn_trace --ct est --ct est ls "$flow" | TRACE_FILTER | 
grep -v 'ct_commit {'], [0], [dnl
+    
sample(probability=65535,collector_set=100,obs_domain=43,obs_point=ct_label.obs_point_id);
+])
+
+AS_BOX([to-lport drop: new connection towards vm1])
+AT_CHECK_UNQUOTED([ovn_trace --ct new ls "$rp_flow && tcp.dst == 24" | 
TRACE_FILTER], [0], [dnl
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4403);
+])
+
+AS_BOX([to-lport drop: established connection previously allowed by the 
allow-related ACL])
+flow="$rp_flow && tcp.dst == 24 && ct_label.obs_point_id == 4304 && 
ct_mark.obs_collector_id == 1 && ct_mark.obs_stage == 2"
+AT_CHECK_UNQUOTED([ovn_trace --ct est ls "$flow" | TRACE_FILTER], [0], [dnl
+    ct_commit { ct_mark.blocked = 1; ct_label.obs_point_id = 0; };
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4403);
+])
+
+AS_BOX([from-lport-after-lb drop: new connection])
+dnl The from-lport allow-related ACL samples the new connection (4301, via
+dnl registers) before the from-lport-after-lb drop ACL samples the verdict.
+AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new ls "$vm_flow && tcp.dst == 25" 
| TRACE_FILTER], [0], [dnl
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4404);
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=reg3);
+])
+
+AS_BOX([drop with sample_new and sample_est])
+dnl sample_est is only used by allow-related ACLs; a deny verdict is always
+dnl sampled with sample_new.
+check_uuid ovn-nbctl --wait=sb                                                 
\
+  -- --id=@d5n create Sample collector="$collector1" metadata=4405             
\
+  -- --id=@d5e create Sample collector="$collector1" metadata=4406             
\
+  -- --sample-new=@d5n --sample-est=@d5e acl-add ls from-lport 2 'tcp.dst == 
26' drop
+AT_CHECK([ovn-sbctl lflow-list ls | grep -e ls_in_acl_sample -e 
ls_out_acl_sample | ovn_strip_lflows | ovn_strip_collector_set | grep -e 
440[[56]]], [0], [dnl
+  table=??(ls_in_acl_sample   ), priority=1300 , match=(ip && reg0[[13]] == 1 
&& (reg8[[17]] == 1 || reg8[[18]] == 1) && reg3 == 4405 && reg8[[19..20]] == 
0), action=(flags.pkt_sampled = 1; 
sample(probability=65535,collector_set=??,obs_domain=42,obs_point=4405); next;)
+])
+dnl The sample_est observation point ID is only stored in the conntrack
+dnl label of the blocked connection.
+flow="$vm_flow && tcp.dst == 26 && ct_label.obs_point_id == 4302 && 
ct_mark.obs_collector_id == 1 && ct_mark.obs_stage == 0"
+AT_CHECK_UNQUOTED([ovn_trace --ct est --ct est ls "$flow" | TRACE_FILTER], 
[0], [dnl
+    ct_commit { ct_mark.blocked = 1; ct_label.obs_point_id = 4406; };
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4405);
+])
+
+AS_BOX([switch without stateful ACLs keeps the stateless sampler])
+check ovn-nbctl                                      \
+  -- ls-add ls2                                      \
+  -- lsp-add ls2 vm3                                 \
+  -- lsp-set-addresses vm3 00:00:00:00:00:03
+check_uuid ovn-nbctl --wait=sb                                                 
\
+  -- --id=@d6 create Sample collector="$collector1" metadata=4407              
\
+  -- --sample-new=@d6 acl-add ls2 from-lport 2 'tcp.dst == 22' drop
+AT_CHECK([ovn-sbctl lflow-list ls2 | grep -e ls_in_acl_sample | 
ovn_strip_lflows | ovn_strip_collector_set | grep -e obs_point], [0], [dnl
+  table=??(ls_in_acl_sample   ), priority=900  , match=(ip && reg8[[0..7]] == 
1 && reg8[[19..20]] == 0), action=(flags.pkt_sampled = 1; 
sample(probability=65535,collector_set=??,obs_domain=42,obs_point=reg3); next;)
+])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
 OVN_FOR_EACH_NORTHD_NO_HV([
 AT_SETUP([ACL Sampling - pass ACL observation data does not leak to the next 
tier])
 AT_KEYWORDS([acl sampling])
@@ -15624,11 +15771,15 @@ check ovn-nbctl --wait=sb sync
 
 dnl The "pass" ACL is sampled in its own tier, according to the connection
 dnl state rules of the sample stage (new connections on this stateful
-dnl switch).
+dnl switch).  The sampled drop ACL is sampled for every denied packet.
 AS_BOX([sampled drop after pass, new connection: pass and drop are both 
sampled])
 AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new ls "$vm_flow && tcp.dst == 22" 
| TRACE_FILTER], [0], [dnl
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4501);
     
sample(probability=65535,collector_set=100,obs_domain=42,obs_point=m4_if(REGS, 
[true], [reg3], [4501]));
-    
sample(probability=65535,collector_set=100,obs_domain=42,obs_point=m4_if(REGS, 
[true], [reg3], [4501]));
+])
+AS_BOX([sampled drop after pass, untracked: only the drop is sampled])
+AT_CHECK_UNQUOTED([ovn_trace ls "$rp_flow && tcp.dst == 22" | TRACE_FILTER], 
[0], [dnl
+    sample(probability=65535,collector_set=100,obs_domain=42,obs_point=4501);
 ])
 
 dnl The unsampled drop ACL must not be attributed to the "pass" ACL Sample.
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index 3cb4b858a..03508da88 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -13595,6 +13595,228 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port 
patch-.*/d
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn -- ACL Sampling - Stateful ACL - deny verdicts])
+AT_SKIP_IF([test $HAVE_NFCAPD = no])
+AT_SKIP_IF([test $HAVE_NFDUMP = no])
+AT_KEYWORDS([ACL])
+
+CHECK_CONNTRACK()
+CHECK_CONNTRACK_NAT()
+ovn_start
+OVS_TRAFFIC_VSWITCHD_START()
+ADD_BR([br-int])
+
+dnl Set external-ids in br-int needed for ovn-controller
+check ovs-vsctl \
+        -- set Open_vSwitch . external-ids:system-id=hv1 \
+        -- set Open_vSwitch . 
external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+dnl Start ovn-controller
+start_daemon ovn-controller
+
+dnl Logical network:
+dnl 1 router
+dnl 2 logical switches, 1 VIF each
+dnl
+dnl ls1 (42.42.42.0/24): vm1
+dnl ls2 (43.43.43.0/24): vm2, with stateful ACLs
+
+check ovn-nbctl                                      \
+  -- lr-add lr                                       \
+  -- lrp-add lr lrp1 00:00:00:00:01:00 42.42.42.1/24 \
+  -- lrp-add lr lrp2 00:00:00:00:02:00 43.43.43.1/24 \
+  -- ls-add ls1                                      \
+  -- lsp-add ls1 vm1                                 \
+  -- lsp-set-addresses vm1 00:00:00:00:00:01         \
+  -- lsp-add-router-port ls1 ls1-lr lrp1             \
+  -- ls-add ls2                                      \
+  -- lsp-add ls2 vm2                                 \
+  -- lsp-set-addresses vm2 00:00:00:00:00:02         \
+  -- lsp-add-router-port ls2 ls2-lr lrp2
+check ovn-nbctl --wait=sb sync
+
+ADD_NAMESPACES(vm1)
+ADD_VETH(vm1, vm1, br-int, "42.42.42.2/24", "00:00:00:00:00:01", "42.42.42.1")
+
+ADD_NAMESPACES(vm2)
+ADD_VETH(vm2, vm2, br-int, "43.43.43.2/24", "00:00:00:00:00:02", "43.43.43.1")
+
+collector1=$(ovn-nbctl create Sample_Collector id=1 name=c1 probability=65535 
set_id=4294967295)
+check_row_count nb:Sample_Collector 1
+
+check_uuid ovn-nbctl create Sampling_App type="acl-new" id="42"
+check_uuid ovn-nbctl create Sampling_App type="acl-est" id="43"
+check_row_count nb:Sampling_App 2
+
+dnl A stateful allow-related ACL for vm2 with sampling enabled.
+check_uuid ovn-nbctl                                                        \
+  -- --id=@sample_sg_new create Sample collector="$collector1" metadata=2001 \
+  -- --id=@sample_sg_est create Sample collector="$collector1" metadata=2002 \
+  -- --sample-new=@sample_sg_new --sample-est=@sample_sg_est                 \
+     acl-add ls2 from-lport 1 'inport == "vm2"' allow-related
+
+check_row_count nb:ACL 1
+check_row_count nb:Sample 2
+
+dnl UDP echo server (vm1).  Creates the "ready" file given as second argument
+dnl once the socket is bound.
+AT_DATA([server.py], [dnl
+import os
+import socket
+import sys
+
+port = int(sys.argv[[1]])
+ready = sys.argv[[2]]
+sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+sock.bind(("0.0.0.0", port))
+with open(ready + ".tmp", "w"):
+    pass
+os.replace(ready + ".tmp", ready)
+
+while True:
+    data, addr = sock.recvfrom(65535)
+    sock.sendto(data, addr)
+])
+
+dnl UDP client (vm2) that keeps a single socket, i.e. a single 5-tuple, for
+dnl the whole test:
+dnl  1. send a request and wait for the echo reply (the session is now
+dnl     established), then create the "ready" file;
+dnl  2. wait for the "go" file, send a second request on the same socket and
+dnl     record whether a reply arrived in the "result" file;
+dnl  3. keep the socket open until the "done" file exists so that no ICMP
+dnl     port unreachable is generated for late replies.
+dnl Files are written completely and then renamed into place so that the test
+dnl never reads a partially written file.
+AT_DATA([client.py], [dnl
+import os
+import socket
+import sys
+import time
+
+
+def publish(path, content):
+    with open(path + ".tmp", "w") as f:
+        f.write(content)
+    os.replace(path + ".tmp", path)
+
+
+dst, port, ready, go, result, done = sys.argv[[1:7]]
+port = int(port)
+
+sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+sock.settimeout(10)
+sock.sendto(b"a\n", (dst, port))
+data, addr = sock.recvfrom(65535)
+publish(ready, data.decode())
+
+while not os.path.exists(go):
+    time.sleep(0.2)
+
+sock.settimeout(3)
+sock.sendto(b"b\n", (dst, port))
+try:
+    sock.recvfrom(65535)
+    outcome = "reply"
+except socket.timeout:
+    outcome = "timeout"
+publish(result, outcome)
+
+while not os.path.exists(done):
+    time.sleep(0.2)
+sock.close()
+])
+
+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=4294967295 bridge=@br 
ipfix=@ipfix
+
+check ovn-nbctl --wait=hv sync
+dnl And wait for it to be up and running.
+OVS_WAIT_UNTIL([ovs-ofctl dump-ipfix-flow br-int | grep -q '1 ids'])
+
+NETNS_DAEMONIZE([vm1], [$PYTHON3 ./server.py 1000 server-ready], 
[server-vm1-1000.pid])
+OVS_WAIT_UNTIL([test -f server-ready])
+
+dnl Establish a session from vm2, allowed by the stateful ACL:
+dnl - the request is sampled as a new session (2001)
+dnl - the reply is sampled as an established session (2002)
+NETNS_DAEMONIZE([vm2], [$PYTHON3 ./client.py 42.42.42.2 1000 ready go result 
done], [client-vm2.pid])
+OVS_WAIT_UNTIL([test -f ready])
+AT_CHECK([cat ready], [0], [a
+])
+OVS_WAIT_UNTIL([ovs-ofctl dump-ipfix-flow br-int | grep -q 'sampled pkts=2'])
+
+dnl Now deny the same traffic with sampled drop ACLs:
+dnl - one from-lport ACL matching the session established above
+dnl - one from-lport ACL matching untracked traffic entering ls2 from the
+dnl   router port
+check_uuid ovn-nbctl                                                           
 \
+  -- --id=@sample_deny_vm create Sample collector="$collector1" metadata=3001  
 \
+  -- --sample-new=@sample_deny_vm                                              
 \
+     acl-add ls2 from-lport 2 'inport == "vm2" && udp.dst == 1000' drop        
 \
+  -- --id=@sample_deny_rp create Sample collector="$collector1" metadata=3002  
 \
+  -- --sample-new=@sample_deny_rp                                              
 \
+     acl-add ls2 from-lport 2 'inport == "ls2-lr" && udp.dst == 1000' drop
+check_row_count nb:ACL 3
+check_row_count nb:Sample 4
+check ovn-nbctl --wait=hv sync
+
+dnl The established session is now denied: the second request on the same
+dnl socket gets no reply and is sampled by the drop ACL (3001), not by the
+dnl established session sampler of the allow-related ACL (2002).
+touch go
+OVS_WAIT_UNTIL([test -f result])
+AT_CHECK([cat result], [0], [timeout])
+OVS_WAIT_UNTIL([ovs-ofctl dump-ipfix-flow br-int | grep -q 'sampled pkts=3'])
+
+dnl Untracked traffic from the router port is denied and sampled (3002).
+NS_CHECK_EXEC([vm1], [echo c | ncat --send-only -u 43.43.43.2 1000])
+OVS_WAIT_UNTIL([ovs-ofctl dump-ipfix-flow br-int | grep -q 'sampled pkts=4'])
+
+dnl Check the IPFIX samples.
+kill $(cat collector.pid)
+OVS_WAIT_WHILE([kill -0 $(cat collector.pid) 2>/dev/null])
+touch done
+
+dnl Can't match on observation domain ID due to the followig fix not being
+dnl available in any released version of nfdump:
+dnl https://github.com/phaag/nfdump/issues/544
+dnl
+dnl Only match on the point ID.
+AT_CHECK([for f in $(ls -1 nfcapd.*); do nfdump -o json -r $f; done | grep 
observationPointID | awk '{$1=$1;print}' | sort], [0], [dnl
+"observationPointID" : 2001,
+"observationPointID" : 2002,
+"observationPointID" : 3001,
+"observationPointID" : 3002,
+])
+
+OVN_CLEANUP_CONTROLLER([hv1])
+
+OVN_CLEANUP_NORTHD
+
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+/connection dropped.*/d"])
+
+AT_CLEANUP
+])
+
 OVN_FOR_EACH_NORTHD([
 AT_SETUP([ovn -- ACL Sampling - Stateful ACL - routed traffic])
 AT_SKIP_IF([test $HAVE_NFCAPD = no])
-- 
2.55.0

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to