diff mbox

[ovs-dev,v6,1/1] ovn: Apply ACL changes to existing connections.

Message ID 1467317645-9483-2-git-send-email-russell@ovn.org
State Accepted
Headers show

Commit Message

Russell Bryant June 30, 2016, 8:14 p.m. UTC
Prior to this commit, once a connection had been committed to the
connection tracker, the connection would continue to be allowed, even
if the policy defined in the ACL table changed.  This patch changes
the implementation so that existing connections are affected by policy
changes.

The implementation is based on the suggested approach in this mailing
list thread:

    http://openvswitch.org/pipermail/dev/2016-February/065716.html

Instead of always allowing packets associated with an established
connection, we now put all packets in the request direction through
the flows generated based on OVN ACLs.  If a packet associated with an
established connection hits a "drop" ACL, that means we have
encountered a policy change and should drop packets associated with
this connection from now on.  We handle this by setting "ct_label" on
the associated connection tracking entry.

These changes also account for re-allowing a known connection after
ct_label had been set on it. This can happen if you delete an ACL and
then re-create it while connection state is still known.

The proposal on the mailing list also discussed the idea that
ovn-controller could periodically sweep the connection tracker and
delete entries with ct_label set.  That is not implemented in this
patch.  Instead, we rely on connections dying since we're dropping
its packets and then allowing the connection tracking entry to
eventually time out.  More proactively clearing them out could be a
future enhancement.

As a realistic example of how this works, consider this security policy
from an OpenStack+OVN development environment.

    +---------+-----------------------+
    | name    | security_group_rules  |
    +---------+-----------------------+
    | default | egress, IPv4          |
    |         | egress, IPv6          |
    |         | ingress, IPv4, 22/tcp |
    |         | ingress, IPv4, icmp   |
    +---------+-----------------------+

The OpenStack Neutron plugin creates ACLs that drop traffic by default
and higher priority ACLs for each type of traffic that is allowed.  In
this case, the ACLs for a port using the "default" security group are:

  from-lport  1002 (inport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip4) allow-related
  from-lport  1002 (inport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip6) allow-related
  from-lport  1001 (inport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip) drop
    to-lport  1002 (outport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip4 && icmp4) allow-related
    to-lport  1002 (outport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip4 && tcp && tcp.dst == 22) allow-related
    to-lport  1001 (outport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip) drop

which results in the following logical flows:

  table=1(   ls_in_pre_acl), priority=  100, match=(ip), action=(ct_next;)
  table=1(   ls_in_pre_acl), priority=    0, match=(1), action=(next;)
  table=2(       ls_in_acl), priority=65535, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label[0] == 0), action=(next;)
  table=2(       ls_in_acl), priority=65535, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label[0] == 0), action=(next;)
  table=2(       ls_in_acl), priority=65535, match=(ct.inv || (ct.est && ct.rpl && ct_label[0] == 1)), action=(drop;)
  table=2(       ls_in_acl), priority= 2002, match=(!ct.new && ct.est && !ct.rpl && ct_label[0] == 0 && (inport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip4)), action=(next;)
  table=2(       ls_in_acl), priority= 2002, match=(!ct.new && ct.est && !ct.rpl && ct_label[0] == 0 && (inport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip6)), action=(next;)
  table=2(       ls_in_acl), priority= 2002, match=(((ct.new && !ct.est) || (!ct.new && ct.est && !ct.rpl && ct_label[0] == 1)) && (inport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip4)), action=(ct_commit(ct_label=0/1); next;)
  table=2(       ls_in_acl), priority= 2002, match=(((ct.new && !ct.est) || (!ct.new && ct.est && !ct.rpl && ct_label[0] == 1)) && (inport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip6)), action=(ct_commit(ct_label=0/1); next;)
  table=2(       ls_in_acl), priority= 2001, match=((!ct.est || (ct.est && ct_label[0] == 1)) && (inport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip)), action=(drop;)
  table=2(       ls_in_acl), priority= 2001, match=(ct.est && ct_label[0] == 0 && (inport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip)), action=(ct_commit(ct_label=1/1);)
  table=2(       ls_in_acl), priority=    1, match=(ip && (!ct.est || (ct.est && ct_label[0] == 1))), action=(ct_commit(ct_label=0/1); next;)
  table=2(       ls_in_acl), priority=    0, match=(1), action=(next;)

  table=0(  ls_out_pre_acl), priority=  100, match=(ip), action=(ct_next;)
  table=0(  ls_out_pre_acl), priority=    0, match=(1), action=(next;)
  table=1(      ls_out_acl), priority=65535, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label[0] == 0), action=(next;)
  table=1(      ls_out_acl), priority=65535, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label[0] == 0), action=(next;)
  table=1(      ls_out_acl), priority=65535, match=(ct.inv || (ct.est && ct.rpl && ct_label[0] == 1)), action=(drop;)
  table=1(      ls_out_acl), priority= 2002, match=(!ct.new && ct.est && !ct.rpl && ct_label[0] == 0 && (outport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip4 && icmp4)), action=(next;)
  table=1(      ls_out_acl), priority= 2002, match=(!ct.new && ct.est && !ct.rpl && ct_label[0] == 0 && (outport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip4 && tcp && tcp.dst == 22)), action=(next;)
  table=1(      ls_out_acl), priority= 2002, match=(((ct.new && !ct.est) || (!ct.new && ct.est && !ct.rpl && ct_label[0] == 1)) && (outport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip4 && icmp4)), action=(ct_commit(ct_label=0/1); next;)
  table=1(      ls_out_acl), priority= 2002, match=(((ct.new && !ct.est) || (!ct.new && ct.est && !ct.rpl && ct_label[0] == 1)) && (outport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip4 && tcp && tcp.dst == 22)), action=(ct_commit(ct_label=0/1); next;)
  table=1(      ls_out_acl), priority= 2001, match=((!ct.est || (ct.est && ct_label[0] == 1)) && (outport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip)), action=(drop;)
  table=1(      ls_out_acl), priority= 2001, match=(ct.est && ct_label[0] == 0 && (outport == "0a7409c8-d179-4915-9eb2-f53426ae16dd" && ip)), action=(ct_commit(ct_label=1/1);)
  table=1(      ls_out_acl), priority=    1, match=(ip && (!ct.est || (ct.est && ct_label[0] == 1))), action=(ct_commit(ct_label=0/1); next;)
  table=1(      ls_out_acl), priority=    0, match=(1), action=(next;)

One way I tested this by leaving ping running, ensuring that it was
blocked when the rule for ICMP was deleted, and then re-allowed when
the rule allowing ICMP was restored.  In this case, the ICMP
connection is still known by the connection tracker, but the flows
ensure that ct_label gets reset back to 0.

Reported-by: Xiao Li Xu <xiaolixu@cn.ibm.com>
Reported-at: https://bugs.launchpad.net/networking-ovn/+bug/1536080
Suggested-by: Justin Pettit <jpettit@ovn.org>
Signed-off-by: Russell Bryant <russell@ovn.org>
Acked-by: Han Zhou <zhouhan@gmail.com>
Acked-by: Ben Pfaff <blp@ovn.org>
---
 ovn/northd/ovn-northd.8.xml |  58 +++++++++++---
 ovn/northd/ovn-northd.c     | 182 +++++++++++++++++++++++++++++++++-----------
 2 files changed, 185 insertions(+), 55 deletions(-)

Comments

Justin Pettit July 7, 2016, 5:13 p.m. UTC | #1
> On Jun 30, 2016, at 10:14 PM, Russell Bryant <russell@ovn.org> wrote:
> 
> diff --git a/ovn/northd/ovn-northd.8.xml b/ovn/northd/ovn-northd.8.xml
> index 260cc14..d2bddcb 100644
> --- a/ovn/northd/ovn-northd.8.xml
> +++ b/ovn/northd/ovn-northd.8.xml
> 

> 
> @@ -282,20 +299,37 @@
>       </li>
> 
>       <li>
> -        A priority-65535 flow that allows any traffic that has been
> -        committed to the connection tracker (i.e., established flows).
> +        A priority-65535 flow that allows any traffic in the reply
> +        direction for a connection that has been committed to the
> +        connection tracker (i.e., established flows), as long as
> +        the committed flow does not have <code>ct_label[0]=1</code> set.

There are a couple of places where "<code>ct_label[0]=1</code> set", but I think it would be clearer to just drop "=1".  Especially if you take my later suggestion to use a symbolic name for the field.

> @@ -1444,38 +1472,106 @@ build_acls(struct ovn_datapath *od, struct hmap *lflows, struct hmap *ports)
> ...
> +                ds_put_format(&match, "((ct.new && !ct.est)"
> +                                      " || (!ct.new && ct.est && !ct.rpl "
> +                                           "&& ct_label[0] == 1)) "

It might be nice to use a symbolic name, similar to "eth.mcast".  That way it's meaning is clearer, and if we ever need to change the label, it only needs to be done in one place.

> +            if (has_stateful) {
> +                /* The implementation of "drop" differs if stateful ACLs are in
> +                 * use for this datapath.  In that case, the actions differ
> +                 * depending on whether the connection was previously committed
> +                 * to the connection tracker with ct_commit.
> +                 *
> +                 * If the packet is not part of an established connection, then
> +                 * we can simply drop it. */

Minor nit: I think it might be clearer to break the two paragraphs into two comments because the first paragraph applies to the entire code block.

> +                ds_put_format(&match,
> +                              "(!ct.est || (ct.est && ct_label[0] == 1)) "
> +                              "&& (%s)",
> +                              acl->match);
> +                ovn_lflow_add(lflows, od, stage, acl->priority +
> +                        OVN_ACL_PRI_OFFSET, ds_cstr(&match), "drop;");
> +
> +                /* For an existing connection without ct_label set, we've
> +                 * encountered a policy change. ACLs previously allowed
> +                 * this connection and we committed the connection tracking
> +                 * entry.  Current policy says that we should drop this
> +                 * connection.  First, we set bit 0 of ct_label to indicate
> +                 * that this connection is set for deletion.  By not
> +                 * specifying "next;", we implicitly drop the packet after
> +                 * updating conntrack state. */
> +
> +                ds_clear(&match);

I think you can drop the blank line after the comment.

Thanks for implementing this.  I apologize for taking so long to review it.  As penance, I'd be happy to rebase the code if you'd like.

Acked-by: Justin Pettit <jpettit@ovn.org>

--Justin
Russell Bryant July 7, 2016, 6:11 p.m. UTC | #2
On Thu, Jul 7, 2016 at 12:13 PM, Justin Pettit <jpettit@ovn.org> wrote:

>
> > On Jun 30, 2016, at 10:14 PM, Russell Bryant <russell@ovn.org> wrote:
> >
> > diff --git a/ovn/northd/ovn-northd.8.xml b/ovn/northd/ovn-northd.8.xml
> > index 260cc14..d2bddcb 100644
> > --- a/ovn/northd/ovn-northd.8.xml
> > +++ b/ovn/northd/ovn-northd.8.xml
> >
>
> >
> > @@ -282,20 +299,37 @@
> >       </li>
> >
> >       <li>
> > -        A priority-65535 flow that allows any traffic that has been
> > -        committed to the connection tracker (i.e., established flows).
> > +        A priority-65535 flow that allows any traffic in the reply
> > +        direction for a connection that has been committed to the
> > +        connection tracker (i.e., established flows), as long as
> > +        the committed flow does not have <code>ct_label[0]=1</code> set.
>
> There are a couple of places where "<code>ct_label[0]=1</code> set", but I
> think it would be clearer to just drop "=1".  Especially if you take my
> later suggestion to use a symbolic name for the field.
>
> > @@ -1444,38 +1472,106 @@ build_acls(struct ovn_datapath *od, struct hmap
> *lflows, struct hmap *ports)
> > ...
> > +                ds_put_format(&match, "((ct.new && !ct.est)"
> > +                                      " || (!ct.new && ct.est &&
> !ct.rpl "
> > +                                           "&& ct_label[0] == 1)) "
>
> It might be nice to use a symbolic name, similar to "eth.mcast".  That way
> it's meaning is clearer, and if we ever need to change the label, it only
> needs to be done in one place.
>
> > +            if (has_stateful) {
> > +                /* The implementation of "drop" differs if stateful
> ACLs are in
> > +                 * use for this datapath.  In that case, the actions
> differ
> > +                 * depending on whether the connection was previously
> committed
> > +                 * to the connection tracker with ct_commit.
> > +                 *
> > +                 * If the packet is not part of an established
> connection, then
> > +                 * we can simply drop it. */
>
> Minor nit: I think it might be clearer to break the two paragraphs into
> two comments because the first paragraph applies to the entire code block.
>
> > +                ds_put_format(&match,
> > +                              "(!ct.est || (ct.est && ct_label[0] ==
> 1)) "
> > +                              "&& (%s)",
> > +                              acl->match);
> > +                ovn_lflow_add(lflows, od, stage, acl->priority +
> > +                        OVN_ACL_PRI_OFFSET, ds_cstr(&match), "drop;");
> > +
> > +                /* For an existing connection without ct_label set,
> we've
> > +                 * encountered a policy change. ACLs previously allowed
> > +                 * this connection and we committed the connection
> tracking
> > +                 * entry.  Current policy says that we should drop this
> > +                 * connection.  First, we set bit 0 of ct_label to
> indicate
> > +                 * that this connection is set for deletion.  By not
> > +                 * specifying "next;", we implicitly drop the packet
> after
> > +                 * updating conntrack state. */
> > +
> > +                ds_clear(&match);
>
> I think you can drop the blank line after the comment.
>
> Thanks for implementing this.  I apologize for taking so long to review
> it.  As penance, I'd be happy to rebase the code if you'd like.
>
> Acked-by: Justin Pettit <jpettit@ovn.org>
>

Thanks for the review!

As for the rebase, I certainly would not be upset if someone rebased it for
me.  :-)  Guru offered as well, since the conflict came from his LB
series.  However, both of you are doing other useful and important work, so
I by no means expect anyone to do it.  I'll get it at some point in the
next couple weeks otherwise.
Russell Bryant July 20, 2016, 9:26 p.m. UTC | #3
On Thu, Jul 7, 2016 at 1:13 PM, Justin Pettit <jpettit@ovn.org> wrote:

>
> > On Jun 30, 2016, at 10:14 PM, Russell Bryant <russell@ovn.org> wrote:
> >
> > diff --git a/ovn/northd/ovn-northd.8.xml b/ovn/northd/ovn-northd.8.xml
> > index 260cc14..d2bddcb 100644
> > --- a/ovn/northd/ovn-northd.8.xml
> > +++ b/ovn/northd/ovn-northd.8.xml
> >
>
> >
> > @@ -282,20 +299,37 @@
> >       </li>
> >
> >       <li>
> > -        A priority-65535 flow that allows any traffic that has been
> > -        committed to the connection tracker (i.e., established flows).
> > +        A priority-65535 flow that allows any traffic in the reply
> > +        direction for a connection that has been committed to the
> > +        connection tracker (i.e., established flows), as long as
> > +        the committed flow does not have <code>ct_label[0]=1</code> set.
>
> There are a couple of places where "<code>ct_label[0]=1</code> set", but I
> think it would be clearer to just drop "=1".  Especially if you take my
> later suggestion to use a symbolic name for the field.
>

I dropped the "=1" part.


>
> > @@ -1444,38 +1472,106 @@ build_acls(struct ovn_datapath *od, struct hmap
> *lflows, struct hmap *ports)
> > ...
> > +                ds_put_format(&match, "((ct.new && !ct.est)"
> > +                                      " || (!ct.new && ct.est &&
> !ct.rpl "
> > +                                           "&& ct_label[0] == 1)) "
>
> It might be nice to use a symbolic name, similar to "eth.mcast".  That way
> it's meaning is clearer, and if we ever need to change the label, it only
> needs to be done in one place.


Yes, that's a very nice suggestion.  I decided to address this in a
follow-up patch, though.  I hope you don't mind.  I'm including it in
ovn/TODO in the meantime.


>
> > +            if (has_stateful) {
> > +                /* The implementation of "drop" differs if stateful
> ACLs are in
> > +                 * use for this datapath.  In that case, the actions
> differ
> > +                 * depending on whether the connection was previously
> committed
> > +                 * to the connection tracker with ct_commit.
> > +                 *
> > +                 * If the packet is not part of an established
> connection, then
> > +                 * we can simply drop it. */
>
> Minor nit: I think it might be clearer to break the two paragraphs into
> two comments because the first paragraph applies to the entire code block.


Done.


>
> > +                ds_put_format(&match,
> > +                              "(!ct.est || (ct.est && ct_label[0] ==
> 1)) "
> > +                              "&& (%s)",
> > +                              acl->match);
> > +                ovn_lflow_add(lflows, od, stage, acl->priority +
> > +                        OVN_ACL_PRI_OFFSET, ds_cstr(&match), "drop;");
> > +
> > +                /* For an existing connection without ct_label set,
> we've
> > +                 * encountered a policy change. ACLs previously allowed
> > +                 * this connection and we committed the connection
> tracking
> > +                 * entry.  Current policy says that we should drop this
> > +                 * connection.  First, we set bit 0 of ct_label to
> indicate
> > +                 * that this connection is set for deletion.  By not
> > +                 * specifying "next;", we implicitly drop the packet
> after
> > +                 * updating conntrack state. */
> > +
> > +                ds_clear(&match);
>
> I think you can drop the blank line after the comment.
>

Done.


> Thanks for implementing this.  I apologize for taking so long to review
> it.  As penance, I'd be happy to rebase the code if you'd like.
>
> Acked-by: Justin Pettit <jpettit@ovn.org>
>

After some help rebasing and re-testing from Babu Shanmugam, I went through
this again, addressed comments, and applied this to master.
Justin Pettit July 20, 2016, 11:13 p.m. UTC | #4
> On Jul 20, 2016, at 2:26 PM, Russell Bryant <russell@ovn.org> wrote:
> 
> After some help rebasing and re-testing from Babu Shanmugam, I went through this again, addressed comments, and applied this to master.

Great!  Glad to see it finally merged.  Thanks for being patient with me.

--Justin
diff mbox

Patch

diff --git a/ovn/northd/ovn-northd.8.xml b/ovn/northd/ovn-northd.8.xml
index 260cc14..d2bddcb 100644
--- a/ovn/northd/ovn-northd.8.xml
+++ b/ovn/northd/ovn-northd.8.xml
@@ -247,7 +247,8 @@ 
       in table 4.  It contains a priority-0 flow that simply moves
       traffic to table 4.  If stateful ACLs are used in the logical
       datapath, a priority-100 flow is added that sends IP packets to
-      the connection tracker before advancing to table 4.
+      the connection tracker with the <code>ct_next;</code> action
+      before advancing to table 4.
     </p>
 
     <h3>Ingress table 4: <code>from-lport</code> ACLs</h3>
@@ -255,15 +256,31 @@ 
     <p>
       Logical flows in this table closely reproduce those in the
       <code>ACL</code> table in the <code>OVN_Northbound</code> database
-      for the <code>from-lport</code> direction.  <code>allow</code>
-      ACLs translate into logical flows with the <code>next;</code>
-      action, <code>allow-related</code> ACLs translate into logical
-      flows with the <code>ct_commit; next;</code> actions, other ACLs
-      translate to <code>drop;</code>.  The <code>priority</code> values
-      from the <code>ACL</code> table have a limited range and have 1000
-      added to them to leave room for OVN default flows at both higher
-      and lower priorities.
+      for the <code>from-lport</code> direction. The <code>priority</code>
+      values from the <code>ACL</code> table have a limited range and have
+      1000 added to them to leave room for OVN default flows at both
+      higher and lower priorities.
     </p>
+    <ul>
+      <li>
+        <code>allow</code> ACLs translate into logical flows with
+        the <code>next;</code> action.  If there are any stateful ACLs
+        on this datapath, then <code>allow</code> ACLs translate to
+        <code>ct_commit; next;</code>.
+      </li>
+      <li>
+        <code>allow-related</code> ACLs translate into logical
+        flows with the <code>ct_commit(ct_label=0/1); next;</code> actions
+        for new connections and <code>next;</code> for existing connections.
+      </li>
+      <li>
+        Other ACLs translate to <code>drop;</code> for new or untracked
+        connections and <code>ct_commit(ct_label=1/1);</code> for known
+        connections.  Setting <code>ct_label</code> marks a connection
+        as one that was previously allowed, but should no longer be
+        allowed due to a policy change.
+      </li>
+    </ul>
 
     <p>
       Ingress table 4 also contains a priority 0 flow with action
@@ -282,20 +299,37 @@ 
       </li>
 
       <li>
-        A priority-65535 flow that allows any traffic that has been
-        committed to the connection tracker (i.e., established flows).
+        A priority-65535 flow that allows any traffic in the reply
+        direction for a connection that has been committed to the
+        connection tracker (i.e., established flows), as long as
+        the committed flow does not have <code>ct_label[0]=1</code> set.
+        We only handle traffic in the reply direction here because
+        we want all packets going in the request direction to still
+        go through the flows that implement the currently defined
+        policy based on ACLs.  If a connection is no longer allowed by
+        policy, <code>ct_label[0]</code> will get set and packets in the
+        reply direction will no longer be allowed, either.
       </li>
 
       <li>
         A priority-65535 flow that allows any traffic that is considered
         related to a committed flow in the connection tracker (e.g., an
-        ICMP Port Unreachable from a non-listening UDP port).
+        ICMP Port Unreachable from a non-listening UDP port), as long
+        as the committed flow does not have <code>ct_label[0]=1</code> set.
       </li>
 
       <li>
         A priority-65535 flow that drops all traffic marked by the
         connection tracker as invalid.
       </li>
+
+      <li>
+        A priority-65535 flow that drops all trafic in the reply direction
+        with <code>ct_label[0]=1</code> set meaning that the connection
+        should no longer be allowed due to a policy change.  Packets
+        in the request direction are skipped here to let a newly created
+        ACL re-allow this connection.
+      </li>
     </ul>
 
     <h3>Ingress Table 5: ARP responder</h3>
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index c2cf15e..7604447 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -1393,48 +1393,76 @@  build_acls(struct ovn_datapath *od, struct hmap *lflows, struct hmap *ports)
          * commit IP flows.  This is because, while the initiater's
          * direction may not have any stateful rules, the server's may
          * and then its return traffic would not have an associated
-         * conntrack entry and would return "+invalid". */
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, 1, "ip",
-                      "ct_commit; next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, 1, "ip",
-                      "ct_commit; next;");
+         * conntrack entry and would return "+invalid".
+         *
+         * We use "ct_commit" for a connection that is not already known
+         * by the connection tracker.  Once a connection is committed,
+         * subsequent packets will hit the flow at priority 0 that just
+         * uses "next;"
+         *
+         * We also check for established connections that have ct_label[0]
+         * set on them.  That's a connection that was disallowed, but is
+         * now allowed by policy again since it hit this default-allow flow.
+         * We need to set ct_label[0]=0 to let the connection continue.
+         * Subsequent packets will hit the flow at priority 0 that just
+         * uses "next;". */
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, 1,
+                      "ip && (!ct.est || (ct.est && ct_label[0] == 1))",
+                      "ct_commit(ct_label=0/1); next;");
+        ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, 1,
+                      "ip && (!ct.est || (ct.est && ct_label[0] == 1))",
+                      "ct_commit(ct_label=0/1); next;");
 
         /* Ingress and Egress ACL Table (Priority 65535).
          *
-         * Always drop traffic that's in an invalid state.  This is
-         * enforced at a higher priority than ACLs can be defined. */
+         * Always drop traffic that's in an invalid state.  Also drop
+         * reply direction packets for connections that have been marked
+         * for deletion (bit 0 of ct_label is set).
+         *
+         * This is enforced at a higher priority than ACLs can be defined. */
         ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX,
-                      "ct.inv", "drop;");
+                      "ct.inv || (ct.est && ct.rpl && ct_label[0] == 1)",
+                      "drop;");
         ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX,
-                      "ct.inv", "drop;");
+                      "ct.inv || (ct.est && ct.rpl && ct_label[0] == 1)",
+                      "drop;");
 
         /* Ingress and Egress ACL Table (Priority 65535).
          *
-         * Always allow traffic that is established to a committed
-         * conntrack entry.  This is enforced at a higher priority than
-         * ACLs can be defined. */
+         * Allow reply traffic that is part of an established
+         * conntrack entry that has not been marked for deletion
+         * (bit 0 of ct_label).  We only match traffic in the
+         * reply direction because we want traffic in the request
+         * direction to hit the currently defined policy from ACLs.
+         *
+         * This is enforced at a higher priority than ACLs can be defined. */
         ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX,
-                      "ct.est && !ct.rel && !ct.new && !ct.inv",
+                      "ct.est && !ct.rel && !ct.new && !ct.inv "
+                      "&& ct.rpl && ct_label[0] == 0",
                       "next;");
         ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX,
-                      "ct.est && !ct.rel && !ct.new && !ct.inv",
+                      "ct.est && !ct.rel && !ct.new && !ct.inv "
+                      "&& ct.rpl && ct_label[0] == 0",
                       "next;");
 
         /* Ingress and Egress ACL Table (Priority 65535).
          *
-         * Always allow traffic that is related to an existing conntrack
-         * entry.  This is enforced at a higher priority than ACLs can
-         * be defined.
+         * Allow traffic that is related to an existing conntrack entry that
+         * has not been marked for deletion (bit 0 of ct_label).
+         *
+         * This is enforced at a higher priority than ACLs can be defined.
          *
          * NOTE: This does not support related data sessions (eg,
          * a dynamically negotiated FTP data channel), but will allow
          * related traffic such as an ICMP Port Unreachable through
          * that's generated from a non-listening UDP port.  */
         ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX,
-                      "!ct.est && ct.rel && !ct.new && !ct.inv",
+                      "!ct.est && ct.rel && !ct.new && !ct.inv "
+                      "&& ct_label[0] == 0",
                       "next;");
         ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX,
-                      "!ct.est && ct.rel && !ct.new && !ct.inv",
+                      "!ct.est && ct.rel && !ct.new && !ct.inv "
+                      "&& ct_label[0] == 0",
                       "next;");
     }
 
@@ -1444,38 +1472,106 @@  build_acls(struct ovn_datapath *od, struct hmap *lflows, struct hmap *ports)
         bool ingress = !strcmp(acl->direction, "from-lport") ? true :false;
         enum ovn_stage stage = ingress ? S_SWITCH_IN_ACL : S_SWITCH_OUT_ACL;
 
-        if (!strcmp(acl->action, "allow")) {
+        if (!strcmp(acl->action, "allow")
+            || !strcmp(acl->action, "allow-related")) {
             /* If there are any stateful flows, we must even commit "allow"
              * actions.  This is because, while the initiater's
              * direction may not have any stateful rules, the server's
              * may and then its return traffic would not have an
              * associated conntrack entry and would return "+invalid". */
-            const char *actions = has_stateful ? "ct_commit; next;" : "next;";
-            ovn_lflow_add(lflows, od, stage,
-                          acl->priority + OVN_ACL_PRI_OFFSET,
-                          acl->match, actions);
-        } else if (!strcmp(acl->action, "allow-related")) {
+            if (!has_stateful) {
+                ovn_lflow_add(lflows, od, stage,
+                              acl->priority + OVN_ACL_PRI_OFFSET,
+                              acl->match, "next;");
+            } else {
+                struct ds match = DS_EMPTY_INITIALIZER;
+
+                /* Commit the connection tracking entry if it's a new
+                 * connection that matches this ACL.  After this commit,
+                 * the reply traffic is allowed by a flow we create at
+                 * priority 65535, defined earlier.
+                 *
+                 * It's also possible that a known connection was marked for
+                 * deletion after a policy was deleted, but the policy was
+                 * re-added while that connection is still known.  We catch
+                 * that case here and un-set ct_label[0] to indicate that the
+                 * connection should be allowed to resume.
+                 */
+                ds_put_format(&match, "((ct.new && !ct.est)"
+                                      " || (!ct.new && ct.est && !ct.rpl "
+                                           "&& ct_label[0] == 1)) "
+                                      "&& (%s)", acl->match);
+                ovn_lflow_add(lflows, od, stage,
+                              acl->priority + OVN_ACL_PRI_OFFSET,
+                              ds_cstr(&match), "ct_commit(ct_label=0/1); next;");
+
+                /* Match on traffic in the request direction for an established
+                 * connection tracking entry that has not been marked for
+                 * deletion.  There is no need to commit here, so we can just
+                 * proceed to the next table. We use this to ensure that this
+                 * connection is still allowed by the currently defined
+                 * policy. */
+                ds_clear(&match);
+                ds_put_format(&match,
+                              "!ct.new && ct.est && !ct.rpl"
+                              " && ct_label[0] == 0 && (%s)",
+                              acl->match);
+                ovn_lflow_add(lflows, od, stage,
+                              acl->priority + OVN_ACL_PRI_OFFSET,
+                              ds_cstr(&match), "next;");
+
+                ds_destroy(&match);
+            }
+        } else if (!strcmp(acl->action, "drop")
+                   || !strcmp(acl->action, "reject")) {
             struct ds match = DS_EMPTY_INITIALIZER;
 
-            /* Commit the connection tracking entry, which allows all
-             * other traffic related to this entry to flow due to the
-             * 65535 priority flow defined earlier. */
-            ds_put_format(&match, "ct.new && (%s)", acl->match);
-            ovn_lflow_add(lflows, od, stage,
-                          acl->priority + OVN_ACL_PRI_OFFSET,
-                          ds_cstr(&match), "ct_commit; next;");
+            /* XXX Need to support "reject", treat it as "drop;" for now. */
+            if (!strcmp(acl->action, "reject")) {
+                VLOG_INFO("reject is not a supported action");
+            }
 
-            ds_destroy(&match);
-        } else if (!strcmp(acl->action, "drop")) {
-            ovn_lflow_add(lflows, od, stage,
-                          acl->priority + OVN_ACL_PRI_OFFSET,
-                          acl->match, "drop;");
-        } else if (!strcmp(acl->action, "reject")) {
-            /* xxx Need to support "reject". */
-            VLOG_INFO("reject is not a supported action");
-            ovn_lflow_add(lflows, od, stage,
-                          acl->priority + OVN_ACL_PRI_OFFSET,
-                          acl->match, "drop;");
+            if (has_stateful) {
+                /* The implementation of "drop" differs if stateful ACLs are in
+                 * use for this datapath.  In that case, the actions differ
+                 * depending on whether the connection was previously committed
+                 * to the connection tracker with ct_commit.
+                 *
+                 * If the packet is not part of an established connection, then
+                 * we can simply drop it. */
+                ds_put_format(&match,
+                              "(!ct.est || (ct.est && ct_label[0] == 1)) "
+                              "&& (%s)",
+                              acl->match);
+                ovn_lflow_add(lflows, od, stage, acl->priority +
+                        OVN_ACL_PRI_OFFSET, ds_cstr(&match), "drop;");
+
+                /* For an existing connection without ct_label set, we've
+                 * encountered a policy change. ACLs previously allowed
+                 * this connection and we committed the connection tracking
+                 * entry.  Current policy says that we should drop this
+                 * connection.  First, we set bit 0 of ct_label to indicate
+                 * that this connection is set for deletion.  By not
+                 * specifying "next;", we implicitly drop the packet after
+                 * updating conntrack state. */
+
+                ds_clear(&match);
+                ds_put_format(&match,
+                              "ct.est && ct_label[0] == 0 && (%s)",
+                              acl->match);
+                ovn_lflow_add(lflows, od, stage,
+                              acl->priority + OVN_ACL_PRI_OFFSET,
+                              ds_cstr(&match), "ct_commit(ct_label=1/1);");
+
+                ds_destroy(&match);
+            } else {
+                /* There are no stateful ACLs in use on this datapath,
+                 * so a "drop" ACL is simply the "drop" logical flow action
+                 * in all cases. */
+                ovn_lflow_add(lflows, od, stage,
+                              acl->priority + OVN_ACL_PRI_OFFSET,
+                              acl->match, "drop;");
+            }
         }
     }
 }