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