diff mbox

[ovs-dev,PATCHv2] ovn: Add support for ACL logging.

Message ID 1501282118-88264-1-git-send-email-jpettit@ovn.org
State Accepted
Headers show

Commit Message

Justin Pettit July 28, 2017, 10:48 p.m. UTC
Signed-off-by: Justin Pettit <jpettit@ovn.org>
Acked-by: Han Zhou <zhouhan@gmail.com>
---
v1->v2: Incorporate Ben and Han's feedback.
        Improve output of "ovn-nbctl acl-list".
---
 NEWS                                |   1 +
 include/ovn/actions.h               |  67 ++++++++++++--------
 ovn/controller/ovn-controller.8.xml |   9 +++
 ovn/controller/pinctrl.c            |   5 ++
 ovn/lib/acl-log.c                   | 105 ++++++++++++++++++++++++++++++++
 ovn/lib/acl-log.h                   |  54 +++++++++++++++++
 ovn/lib/actions.c                   | 118 ++++++++++++++++++++++++++++++++++++
 ovn/lib/automake.mk                 |   2 +
 ovn/northd/ovn-northd.c             |  77 ++++++++++++++++++++---
 ovn/ovn-nb.ovsschema                |  15 ++++-
 ovn/ovn-nb.xml                      |  42 ++++++++++---
 ovn/ovn-sb.xml                      |  41 +++++++++++++
 ovn/utilities/ovn-nbctl.8.xml       |  34 +++++++----
 ovn/utilities/ovn-nbctl.c           |  44 +++++++++++---
 ovn/utilities/ovn-trace.c           |  19 ++++++
 tests/ovn-nbctl.at                  |   8 +--
 tests/ovn.at                        | 105 ++++++++++++++++++++++++++++++++
 17 files changed, 680 insertions(+), 66 deletions(-)
 create mode 100644 ovn/lib/acl-log.c
 create mode 100644 ovn/lib/acl-log.h
diff mbox

Patch

diff --git a/NEWS b/NEWS
index b2deac57d6bd..facea0228d3a 100644
--- a/NEWS
+++ b/NEWS
@@ -48,6 +48,7 @@  Post-v2.7.0
      * Multiple chassis may now be specified for L3 gateways.  When more than
        one chassis is specified, OVN will manage high availability for that
        gateway.
+     * Add support for ACL logging.
    - Tracing with ofproto/trace now traces through recirculation.
    - OVSDB:
      * New support for role-based access control (see ovsdb-server(1)).
diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index 9e4a5c5ab1e8..b88effee7437 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -48,30 +48,31 @@  struct simap;
  *    "ovnact".  The structure must have a fixed length, that is, it may not
  *    end with a flexible array member.
  */
-#define OVNACTS                                     \
-    OVNACT(OUTPUT,        ovnact_null)              \
-    OVNACT(NEXT,          ovnact_next)              \
-    OVNACT(LOAD,          ovnact_load)              \
-    OVNACT(MOVE,          ovnact_move)              \
-    OVNACT(EXCHANGE,      ovnact_move)              \
-    OVNACT(DEC_TTL,       ovnact_null)              \
-    OVNACT(CT_NEXT,       ovnact_ct_next)           \
-    OVNACT(CT_COMMIT,     ovnact_ct_commit)         \
-    OVNACT(CT_DNAT,       ovnact_ct_nat)            \
-    OVNACT(CT_SNAT,       ovnact_ct_nat)            \
-    OVNACT(CT_LB,         ovnact_ct_lb)             \
-    OVNACT(CT_CLEAR,      ovnact_null)              \
-    OVNACT(CLONE,         ovnact_nest)              \
-    OVNACT(ARP,           ovnact_nest)              \
-    OVNACT(ND_NA,         ovnact_nest)              \
-    OVNACT(GET_ARP,       ovnact_get_mac_bind)      \
-    OVNACT(PUT_ARP,       ovnact_put_mac_bind)      \
-    OVNACT(GET_ND,        ovnact_get_mac_bind)      \
-    OVNACT(PUT_ND,        ovnact_put_mac_bind)      \
-    OVNACT(PUT_DHCPV4_OPTS, ovnact_put_dhcp_opts)   \
-    OVNACT(PUT_DHCPV6_OPTS, ovnact_put_dhcp_opts)   \
-    OVNACT(SET_QUEUE,       ovnact_set_queue)       \
-    OVNACT(DNS_LOOKUP,      ovnact_dns_lookup)
+#define OVNACTS                                       \
+    OVNACT(OUTPUT,            ovnact_null)            \
+    OVNACT(NEXT,              ovnact_next)            \
+    OVNACT(LOAD,              ovnact_load)            \
+    OVNACT(MOVE,              ovnact_move)            \
+    OVNACT(EXCHANGE,          ovnact_move)            \
+    OVNACT(DEC_TTL,           ovnact_null)            \
+    OVNACT(CT_NEXT,           ovnact_ct_next)         \
+    OVNACT(CT_COMMIT,         ovnact_ct_commit)       \
+    OVNACT(CT_DNAT,           ovnact_ct_nat)          \
+    OVNACT(CT_SNAT,           ovnact_ct_nat)          \
+    OVNACT(CT_LB,             ovnact_ct_lb)           \
+    OVNACT(CT_CLEAR,          ovnact_null)            \
+    OVNACT(CLONE,             ovnact_nest)            \
+    OVNACT(ARP,               ovnact_nest)            \
+    OVNACT(ND_NA,             ovnact_nest)            \
+    OVNACT(GET_ARP,           ovnact_get_mac_bind)    \
+    OVNACT(PUT_ARP,           ovnact_put_mac_bind)    \
+    OVNACT(GET_ND,            ovnact_get_mac_bind)    \
+    OVNACT(PUT_ND,            ovnact_put_mac_bind)    \
+    OVNACT(PUT_DHCPV4_OPTS,   ovnact_put_dhcp_opts)   \
+    OVNACT(PUT_DHCPV6_OPTS,   ovnact_put_dhcp_opts)   \
+    OVNACT(SET_QUEUE,         ovnact_set_queue)       \
+    OVNACT(DNS_LOOKUP,        ovnact_dns_lookup)      \
+    OVNACT(LOG,               ovnact_log)
 
 /* enum ovnact_type, with a member OVNACT_<ENUM> for each action. */
 enum OVS_PACKED_ENUM ovnact_type {
@@ -265,6 +266,14 @@  struct ovnact_dns_lookup {
     struct expr_field dst;      /* 1-bit destination field. */
 };
 
+/* OVNACT_LOG. */
+struct ovnact_log {
+    struct ovnact ovnact;
+    uint8_t verdict;            /* One of LOG_VERDICT_*. */
+    uint8_t severity;           /* One of LOG_SEVERITY_*. */
+    char *name;
+};
+
 /* Internal use by the helpers below. */
 void ovnact_init(struct ovnact *, enum ovnact_type, size_t len);
 void *ovnact_put(struct ofpbuf *, enum ovnact_type, size_t len);
@@ -400,6 +409,16 @@  enum action_opcode {
      *
      */
     ACTION_OPCODE_DNS_LOOKUP,
+
+    /* "log(arguments)".
+     *
+     * Arguments are as follows:
+     *   - An 8-bit verdict.
+     *   - An 8-bit severity.
+     *   - An 16-bit string length for the name.
+     *   - A variable length string containing the name.
+     */
+    ACTION_OPCODE_LOG,
 };
 
 /* Header. */
diff --git a/ovn/controller/ovn-controller.8.xml b/ovn/controller/ovn-controller.8.xml
index d1fcd8a7b3c4..5641abc7c6f3 100644
--- a/ovn/controller/ovn-controller.8.xml
+++ b/ovn/controller/ovn-controller.8.xml
@@ -20,6 +20,15 @@ 
       machine-local and do not run over a physical network.
     </p>
 
+    <h1>ACL Logging</h1>
+    <p>
+      ACL log messages are logged through <code>ovn-controller</code>'s
+      logging mechanism.  ACL log entries have the module
+      <code>acl_log</code> at log level <code>info</code>.  Configuring
+      logging is described below in the <code>Logging Options</code>
+      section.
+    </p>
+
     <h1>Options</h1>
 
     <h2>Daemon Options</h2>
diff --git a/ovn/controller/pinctrl.c b/ovn/controller/pinctrl.c
index b7bcee65f909..3d43631555de 100644
--- a/ovn/controller/pinctrl.c
+++ b/ovn/controller/pinctrl.c
@@ -39,6 +39,7 @@ 
 #include "ovn-controller.h"
 #include "ovn/actions.h"
 #include "ovn/lex.h"
+#include "ovn/lib/acl-log.h"
 #include "ovn/lib/logical-fields.h"
 #include "ovn/lib/ovn-dhcp.h"
 #include "ovn/lib/ovn-util.h"
@@ -981,6 +982,10 @@  process_packet_in(const struct ofp_header *msg, struct controller_ctx *ctx)
         pinctrl_handle_dns_lookup(&packet, &pin, &userdata, &continuation, ctx);
         break;
 
+    case ACTION_OPCODE_LOG:
+        handle_acl_log(&headers, &userdata);
+        break;
+
     default:
         VLOG_WARN_RL(&rl, "unrecognized packet-in opcode %"PRIu32,
                      ntohl(ah->opcode));
diff --git a/ovn/lib/acl-log.c b/ovn/lib/acl-log.c
new file mode 100644
index 000000000000..0f5e43aff720
--- /dev/null
+++ b/ovn/lib/acl-log.c
@@ -0,0 +1,105 @@ 
+/*
+ * Copyright (c) 2017 Nicira, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+#include "ovn/lib/acl-log.h"
+#include <string.h>
+#include "flow.h"
+#include "openvswitch/json.h"
+#include "openvswitch/ofpbuf.h"
+#include "openvswitch/vlog.h"
+
+
+VLOG_DEFINE_THIS_MODULE(acl_log);
+
+const char *
+log_verdict_to_string(uint8_t verdict)
+{
+    if (verdict == LOG_VERDICT_ALLOW) {
+        return "allow";
+    } else if (verdict == LOG_VERDICT_DROP) {
+        return "drop";
+    } else if (verdict == LOG_VERDICT_REJECT) {
+        return "reject";
+    } else {
+        return "<unknown>";
+    }
+}
+
+const char *
+log_severity_to_string(uint8_t severity)
+{
+    if (severity == LOG_SEVERITY_ALERT) {
+        return "alert";
+    } else if (severity == LOG_SEVERITY_WARNING) {
+        return "warning";
+    } else if (severity == LOG_SEVERITY_NOTICE) {
+        return "notice";
+    } else if (severity == LOG_SEVERITY_INFO) {
+        return "info";
+    } else if (severity == LOG_SEVERITY_DEBUG) {
+        return "debug";
+    } else {
+        return "<unknown>";
+    }
+}
+
+uint8_t
+log_severity_from_string(const char *name)
+{
+    if (!strcmp(name, "alert")) {
+        return LOG_SEVERITY_ALERT;
+    } else if (!strcmp(name, "warning")) {
+        return LOG_SEVERITY_WARNING;
+    } else if (!strcmp(name, "notice")) {
+        return LOG_SEVERITY_NOTICE;
+    } else if (!strcmp(name, "info")) {
+        return LOG_SEVERITY_INFO;
+    } else if (!strcmp(name, "debug")) {
+        return LOG_SEVERITY_DEBUG;
+    } else {
+        return UINT8_MAX;
+    }
+}
+
+void
+handle_acl_log(const struct flow *headers, struct ofpbuf *userdata)
+{
+    if(!VLOG_IS_INFO_ENABLED()) {
+        return;
+    }
+
+    struct log_pin_header *lph = ofpbuf_try_pull(userdata, sizeof *lph);
+    if (!lph) {
+        VLOG_WARN("log data missing");
+        return;
+    }
+
+    size_t name_len = userdata->size;
+    char *name = name_len ? xmemdup0(userdata->data, name_len) : NULL;
+
+    struct ds ds = DS_EMPTY_INITIALIZER;
+    ds_put_cstr(&ds, "name=");
+    json_string_escape(name_len ? name : "<unnamed>", &ds);
+    ds_put_format(&ds, ", verdict=%s, severity=%s: ",
+                  log_verdict_to_string(lph->verdict),
+                  log_severity_to_string(lph->severity));
+    flow_format(&ds, headers, NULL);
+
+    VLOG_INFO("%s", ds_cstr(&ds));
+    ds_destroy(&ds);
+    free(name);
+}
diff --git a/ovn/lib/acl-log.h b/ovn/lib/acl-log.h
new file mode 100644
index 000000000000..55dc75b7f446
--- /dev/null
+++ b/ovn/lib/acl-log.h
@@ -0,0 +1,54 @@ 
+/*
+ * Copyright (c) 2017 Nicira, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ACL_LOG_H
+#define ACL_LOG_H 1
+
+#include <stdint.h>
+#include "openvswitch/types.h"
+
+struct ofpbuf;
+struct flow;
+
+struct log_pin_header {
+    uint8_t verdict;            /* One of LOG_VERDICT_*. */
+    uint8_t severity;           /* One of LOG_SEVERITY*. */
+    /* Followed by an optional string containing the rule's name. */
+};
+
+enum log_verdict {
+    LOG_VERDICT_ALLOW,
+    LOG_VERDICT_DROP,
+    LOG_VERDICT_REJECT,
+    LOG_VERDICT_UNKNOWN = UINT8_MAX
+};
+
+const char *log_verdict_to_string(uint8_t verdict);
+
+
+/* Severity levels.  Based on RFC5424 levels. */
+#define LOG_SEVERITY_ALERT    1
+#define LOG_SEVERITY_WARNING  4
+#define LOG_SEVERITY_NOTICE   5
+#define LOG_SEVERITY_INFO     6
+#define LOG_SEVERITY_DEBUG    7
+
+const char *log_severity_to_string(uint8_t severity);
+uint8_t log_severity_from_string(const char *name);
+
+void handle_acl_log(const struct flow *headers, struct ofpbuf *userdata);
+
+#endif /* ovn/lib/acl-log.h */
diff --git a/ovn/lib/actions.c b/ovn/lib/actions.c
index 937f94d80bbb..f010b05a1f48 100644
--- a/ovn/lib/actions.c
+++ b/ovn/lib/actions.c
@@ -33,6 +33,7 @@ 
 #include "ovn/actions.h"
 #include "ovn/expr.h"
 #include "ovn/lex.h"
+#include "ovn/lib/acl-log.h"
 #include "packets.h"
 #include "openvswitch/shash.h"
 #include "simap.h"
@@ -1759,6 +1760,121 @@  ovnact_dns_lookup_free(struct ovnact_dns_lookup *dl OVS_UNUSED)
 {
 }
 
+static void
+parse_log_arg(struct action_context *ctx, struct ovnact_log *log)
+{
+    if (lexer_match_id(ctx->lexer, "verdict")) {
+        if (!lexer_force_match(ctx->lexer, LEX_T_EQUALS)) {
+            return;
+        }
+        if (lexer_match_id(ctx->lexer, "drop")) {
+            log->verdict = LOG_VERDICT_DROP;
+        } else if (lexer_match_id(ctx->lexer, "reject")) {
+            log->verdict = LOG_VERDICT_REJECT;
+        } else if (lexer_match_id(ctx->lexer, "allow")) {
+            log->verdict = LOG_VERDICT_ALLOW;
+        } else {
+            lexer_syntax_error(ctx->lexer, "unknown acl verdict");
+        }
+    } else if (lexer_match_id(ctx->lexer, "name")) {
+        if (!lexer_force_match(ctx->lexer, LEX_T_EQUALS)) {
+            return;
+        }
+        /* If multiple names are given, use the most recent. */
+        if (log->name) {
+            free(log->name);
+        }
+        if (ctx->lexer->token.type == LEX_T_STRING) {
+            /* Arbitrarily limit the name length to 64 bytes, since
+             * these will be encoded in datapath actions. */
+            if (strlen(ctx->lexer->token.s) >= 64) {
+                lexer_syntax_error(ctx->lexer, "name must be shorter "
+                                               "than 64 characters");
+                return;
+            }
+            log->name = xstrdup(ctx->lexer->token.s);
+        } else {
+            lexer_syntax_error(ctx->lexer, "expecting string");
+            return;
+        }
+        lexer_get(ctx->lexer);
+    } else if (lexer_match_id(ctx->lexer, "severity")) {
+        if (!lexer_force_match(ctx->lexer, LEX_T_EQUALS)) {
+            return;
+        }
+        if (ctx->lexer->token.type == LEX_T_ID) {
+            uint8_t severity = log_severity_from_string(ctx->lexer->token.s);
+            if (severity != UINT8_MAX) {
+                log->severity = severity;
+                lexer_get(ctx->lexer);
+                return;
+            }
+        }
+        lexer_syntax_error(ctx->lexer, "expecting severity");
+    } else {
+        lexer_syntax_error(ctx->lexer, NULL);
+    }
+}
+
+static void
+parse_LOG(struct action_context *ctx)
+{
+    struct ovnact_log *log = ovnact_put_LOG(ctx->ovnacts);
+
+    /* Provide default values. */
+    log->severity = LOG_SEVERITY_INFO;
+    log->verdict = LOG_VERDICT_UNKNOWN;
+
+    if (lexer_match(ctx->lexer, LEX_T_LPAREN)) {
+        while (!lexer_match(ctx->lexer, LEX_T_RPAREN)) {
+            parse_log_arg(ctx, log);
+            if (ctx->lexer->error) {
+                return;
+            }
+            lexer_match(ctx->lexer, LEX_T_COMMA);
+        }
+    }
+}
+
+static void
+format_LOG(const struct ovnact_log *log, struct ds *s)
+{
+    ds_put_cstr(s, "log(");
+
+    if (log->name) {
+        ds_put_format(s, "name=\"%s\", ", log->name);
+    }
+
+    ds_put_format(s, "verdict=%s, ", log_verdict_to_string(log->verdict));
+    ds_put_format(s, "severity=%s);", log_severity_to_string(log->severity));
+}
+
+static void
+encode_LOG(const struct ovnact_log *log,
+           const struct ovnact_encode_params *ep OVS_UNUSED,
+           struct ofpbuf *ofpacts)
+{
+    size_t oc_offset = encode_start_controller_op(ACTION_OPCODE_LOG, false,
+                                                  ofpacts);
+
+    struct log_pin_header *lph = ofpbuf_put_uninit(ofpacts, sizeof *lph);
+    lph->verdict = log->verdict;
+    lph->severity = log->severity;
+
+    if (log->name) {
+        int name_len = strlen(log->name);
+        ofpbuf_put(ofpacts, log->name, name_len);
+    }
+
+    encode_finish_controller_op(oc_offset, ofpacts);
+}
+
+static void
+ovnact_log_free(struct ovnact_log *log)
+{
+    free(log->name);
+}
+
 /* Parses an assignment or exchange or put_dhcp_opts action. */
 static void
 parse_set_action(struct action_context *ctx)
@@ -1838,6 +1954,8 @@  parse_action(struct action_context *ctx)
         parse_put_mac_bind(ctx, 128, ovnact_put_PUT_ND(ctx->ovnacts));
     } else if (lexer_match_id(ctx->lexer, "set_queue")) {
         parse_SET_QUEUE(ctx);
+    } else if (lexer_match_id(ctx->lexer, "log")) {
+        parse_LOG(ctx);
     } else {
         lexer_syntax_error(ctx->lexer, "expecting action");
     }
diff --git a/ovn/lib/automake.mk b/ovn/lib/automake.mk
index 9ad8b6af7564..d9ed050f3476 100644
--- a/ovn/lib/automake.mk
+++ b/ovn/lib/automake.mk
@@ -4,6 +4,8 @@  ovn_lib_libovn_la_LDFLAGS = \
         -Wl,--version-script=$(top_builddir)/ovn/lib/libovn.sym \
         $(AM_LDFLAGS)
 ovn_lib_libovn_la_SOURCES = \
+	ovn/lib/acl-log.c \
+	ovn/lib/acl-log.h \
 	ovn/lib/actions.c \
 	ovn/lib/chassis-index.c \
 	ovn/lib/chassis-index.h \
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index 5f59d4bb7ae8..df002b9bf3a4 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -3020,6 +3020,40 @@  build_pre_stateful(struct ovn_datapath *od, struct hmap *lflows)
 }
 
 static void
+build_acl_log(struct ds *actions, const struct nbrec_acl *acl)
+{
+    if (!acl->log) {
+        return;
+    }
+
+    ds_put_cstr(actions, "log(");
+
+    if (acl->name) {
+        ds_put_format(actions, "name=\"%s\", ", acl->name);
+    }
+
+    /* If a severity level isn't specified, default to "info". */
+    if (acl->severity) {
+        ds_put_format(actions, "severity=%s, ", acl->severity);
+    } else {
+        ds_put_format(actions, "severity=info, ");
+    }
+
+    if (!strcmp(acl->action, "drop")) {
+        ds_put_cstr(actions, "verdict=drop, ");
+    } else if (!strcmp(acl->action, "reject")) {
+        ds_put_cstr(actions, "verdict=reject, ");
+    } else if (!strcmp(acl->action, "allow")
+        || !strcmp(acl->action, "allow-related")) {
+        ds_put_cstr(actions, "verdict=allow, ");
+    }
+
+    ds_chomp(actions, ' ');
+    ds_chomp(actions, ',');
+    ds_put_cstr(actions, "); ");
+}
+
+static void
 build_acls(struct ovn_datapath *od, struct hmap *lflows)
 {
     bool has_stateful = has_stateful_acl(od);
@@ -3133,11 +3167,17 @@  build_acls(struct ovn_datapath *od, struct hmap *lflows)
              * may and then its return traffic would not have an
              * associated conntrack entry and would return "+invalid". */
             if (!has_stateful) {
+                struct ds actions = DS_EMPTY_INITIALIZER;
+                build_acl_log(&actions, acl);
+                ds_put_cstr(&actions, "next;");
                 ovn_lflow_add_with_hint(lflows, od, stage,
                                         acl->priority + OVN_ACL_PRI_OFFSET,
-                                        acl->match, "next;", stage_hint);
+                                        acl->match, ds_cstr(&actions),
+                                        stage_hint);
+                ds_destroy(&actions);
             } else {
                 struct ds match = DS_EMPTY_INITIALIZER;
+                struct ds actions = DS_EMPTY_INITIALIZER;
 
                 /* Commit the connection tracking entry if it's a new
                  * connection that matches this ACL.  After this commit,
@@ -3155,10 +3195,13 @@  build_acls(struct ovn_datapath *od, struct hmap *lflows)
                                       " || (!ct.new && ct.est && !ct.rpl "
                                            "&& ct_label.blocked == 1)) "
                                       "&& (%s)", acl->match);
+                ds_put_cstr(&actions, REGBIT_CONNTRACK_COMMIT" = 1; ");
+                build_acl_log(&actions, acl);
+                ds_put_cstr(&actions, "next;");
                 ovn_lflow_add_with_hint(lflows, od, stage,
                                         acl->priority + OVN_ACL_PRI_OFFSET,
                                         ds_cstr(&match),
-                                        REGBIT_CONNTRACK_COMMIT" = 1; next;",
+                                        ds_cstr(&actions),
                                         stage_hint);
 
                 /* Match on traffic in the request direction for an established
@@ -3168,20 +3211,26 @@  build_acls(struct ovn_datapath *od, struct hmap *lflows)
                  * connection is still allowed by the currently defined
                  * policy. */
                 ds_clear(&match);
+                ds_clear(&actions);
                 ds_put_format(&match,
                               "!ct.new && ct.est && !ct.rpl"
                               " && ct_label.blocked == 0 && (%s)",
                               acl->match);
+
+                build_acl_log(&actions, acl);
+                ds_put_cstr(&actions, "next;");
                 ovn_lflow_add_with_hint(lflows, od, stage,
                                         acl->priority + OVN_ACL_PRI_OFFSET,
-                                        ds_cstr(&match), "next;",
+                                        ds_cstr(&match), ds_cstr(&actions),
                                         stage_hint);
 
                 ds_destroy(&match);
+                ds_destroy(&actions);
             }
         } else if (!strcmp(acl->action, "drop")
                    || !strcmp(acl->action, "reject")) {
             struct ds match = DS_EMPTY_INITIALIZER;
+            struct ds actions = DS_EMPTY_INITIALIZER;
 
             /* XXX Need to support "reject", treat it as "drop;" for now. */
             if (!strcmp(acl->action, "reject")) {
@@ -3199,9 +3248,12 @@  build_acls(struct ovn_datapath *od, struct hmap *lflows)
                               "(!ct.est || (ct.est && ct_label.blocked == 1)) "
                               "&& (%s)",
                               acl->match);
+                ds_clear(&actions);
+                build_acl_log(&actions, acl);
+                ds_put_cstr(&actions, "/* drop */");
                 ovn_lflow_add_with_hint(lflows, od, stage,
                                         acl->priority + OVN_ACL_PRI_OFFSET,
-                                        ds_cstr(&match), "drop;",
+                                        ds_cstr(&match), ds_cstr(&actions),
                                         stage_hint);
 
                 /* For an existing connection without ct_label set, we've
@@ -3215,25 +3267,32 @@  build_acls(struct ovn_datapath *od, struct hmap *lflows)
                  * ct_commit() to the "stateful" stage, but since we're
                  * dropping the packet, we go ahead and do it here. */
                 ds_clear(&match);
+                ds_clear(&actions);
                 ds_put_format(&match,
                               "ct.est && ct_label.blocked == 0 && (%s)",
                               acl->match);
+                ds_put_cstr(&actions, "ct_commit(ct_label=1/1); ");
+                build_acl_log(&actions, acl);
+                ds_put_cstr(&actions, "/* drop */");
                 ovn_lflow_add_with_hint(lflows, od, stage,
                                         acl->priority + OVN_ACL_PRI_OFFSET,
-                                        ds_cstr(&match),
-                                        "ct_commit(ct_label=1/1);",
+                                        ds_cstr(&match), ds_cstr(&actions),
                                         stage_hint);
 
-                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. */
+                ds_clear(&actions);
+                build_acl_log(&actions, acl);
+                ds_put_cstr(&actions, "/* drop */");
                 ovn_lflow_add_with_hint(lflows, od, stage,
                                         acl->priority + OVN_ACL_PRI_OFFSET,
-                                        acl->match, "drop;", stage_hint);
-                ds_destroy(&match);
+                                        acl->match, ds_cstr(&actions),
+                                        stage_hint);
             }
+            ds_destroy(&match);
+            ds_destroy(&actions);
         }
         free(stage_hint);
     }
diff --git a/ovn/ovn-nb.ovsschema b/ovn/ovn-nb.ovsschema
index d85a3fe98e44..a077bfb8107a 100644
--- a/ovn/ovn-nb.ovsschema
+++ b/ovn/ovn-nb.ovsschema
@@ -1,7 +1,7 @@ 
 {
     "name": "OVN_Northbound",
-    "version": "5.7.0",
-    "cksum": "3754583060 16164",
+    "version": "5.8.0",
+    "cksum": "2812300190 16766",
     "tables": {
         "NB_Global": {
             "columns": {
@@ -116,7 +116,7 @@ 
             "isRoot": true},
         "Load_Balancer": {
             "columns": {
-		"name": {"type": "string"},
+                "name": {"type": "string"},
                 "vips": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}},
@@ -130,6 +130,9 @@ 
             "isRoot": true},
         "ACL": {
             "columns": {
+                "name": {"type": {"key": {"type": "string",
+                                          "maxLength": 63},
+                                          "min": 0, "max": 1}},
                 "priority": {"type": {"key": {"type": "integer",
                                               "minInteger": 0,
                                               "maxInteger": 32767}}},
@@ -139,6 +142,12 @@ 
                 "action": {"type": {"key": {"type": "string",
                                             "enum": ["set", ["allow", "allow-related", "drop", "reject"]]}}},
                 "log": {"type": "boolean"},
+                "severity": {"type": {"key": {"type": "string",
+                                              "enum": ["set",
+                                                       ["alert", "warning",
+                                                        "notice", "info",
+                                                        "debug"]]},
+                                      "min": 0, "max": 1}},
                 "external_ids": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}}},
diff --git a/ovn/ovn-nb.xml b/ovn/ovn-nb.xml
index 61522f140daa..31303a846a51 100644
--- a/ovn/ovn-nb.xml
+++ b/ovn/ovn-nb.xml
@@ -1035,17 +1035,43 @@ 
       </ul>
     </column>
 
-    <column name="log">
+    <group title="Logging">
       <p>
-        If set to <code>true</code>, packets that match the ACL will trigger a
-        log message on the transport node or nodes that perform ACL processing.
-        Logging may be combined with any <ref column="action"/>.
+        These columns control whether and how OVN logs packets that match an
+        ACL.
       </p>
 
-      <p>
-        Logging is not yet implemented.
-      </p>
-    </column>
+      <column name="log">
+        <p>
+          If set to <code>true</code>, packets that match the ACL will trigger
+          a log message on the transport node or nodes that perform ACL
+          processing.  Logging may be combined with any <ref column="action"/>.
+        </p>
+
+        <p>
+          If set to <code>false</code>, the remaining columns in this group
+          have no significance.
+        </p>
+      </column>
+
+      <column name="name">
+        <p>
+          This name, if it is provided, is included in log records.  It
+          provides the administrator and the cloud management system a way to
+          associate a log record with a particular ACL.
+        </p>
+      </column>
+
+      <column name="severity">
+        <p>
+          The severity of the ACL.  The severity levels match those of syslog,
+          in decreasing level of severity: <code>alert</code>,
+          <code>warning</code>, <code>notice</code>, <code>info</code>, or
+          <code>debug</code>.  When the column is empty, the default is
+          <code>info</code>.
+        </p>
+      </column>
+    </group>
 
     <group title="Common Columns">
       <column name="external_ids">
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index c1731d284531..b254931412bb 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -1515,6 +1515,47 @@ 
         </dd>
       </dl>
 
+      <dl>
+        <dt>
+          <code>log(<var>key</var>=<var>value</var>, </code>...<code>);</code>
+        </dt>
+
+        <dd>
+          <p>
+            Causes <code>ovn-controller</code> to log the packet on the chassis
+            that processes it.  Packet logging currently uses the same logging
+            mechanism as other Open vSwitch and OVN messages, which means that
+            whether and where log messages appear depends on the local logging
+            configuration that can be configured with <code>ovs-appctl</code>,
+            etc.
+          </p>
+          <p>
+            The <code>log</code> action takes zero or more of the following
+            key-value pair arguments that control what is logged:
+          </p>
+          <dl>
+            <dt><code>name=</code><var>string</var></dt>
+            <dd>
+              An optional name for the ACL.  The <var>string</var> is
+              currently limited to 64 bytes.
+            </dd>
+            <dt><code>severity=</code><var>level</var></dt>
+            <dd>
+              Indicates the severity of the event.  The <var>level</var> is one
+              of following (from more to less serious): <code>alert</code>,
+              <code>warning</code>, <code>notice</code>, <code>info</code>, or
+              <code>debug</code>.  If a severity is not provided, the default
+              is <code>info</code>.
+            </dd>
+            <dt><code>verdict=</code><var>value</var></dt>
+            <dd>
+              The verdict for packets matching the flow.  The value must be one
+              of <code>allow</code>, <code>deny</code>, or <code>reject</code>.
+            </dd>
+          </dl>
+        </dd>
+      </dl>
+
       <p>
         The following actions will likely be useful later, but they have not
         been thought out carefully.
diff --git a/ovn/utilities/ovn-nbctl.8.xml b/ovn/utilities/ovn-nbctl.8.xml
index f4fc6cfa6898..d26677db9ec1 100644
--- a/ovn/utilities/ovn-nbctl.8.xml
+++ b/ovn/utilities/ovn-nbctl.8.xml
@@ -76,17 +76,29 @@ 
 
     <h1>Logical Switch ACL Commands</h1>
     <dl>
-      <dt>[<code>--log</code>] [<code>--may-exist</code>] <code>acl-add</code> <var>switch</var> <var>direction</var> <var>priority</var> <var>match</var> <var>action</var></dt>
-      <dd>
-        Adds the specified ACL to <var>switch</var>.
-        <var>direction</var> must be either <code>from-lport</code> or
-        <code>to-lport</code>.  <var>priority</var> must be between
-        <code>0</code> and <code>32767</code>, inclusive.  If
-        <code>--log</code> is specified, packet logging is enabled for the
-        ACL.  A full description of the fields are in <code>ovn-nb</code>(5).
-        If <code>--may-exist</code> is specified, adding a duplicated ACL
-        succeeds but the ACL is not really created. Without <code>--may-exist</code>,
-        adding a duplicated ACL results in error.
+      <dt>[<code>--log</code>] [<code>--severity=</code><var>severity</var>] [<code>--name=</code><var>name</var>] [<code>--may-exist</code>] <code>acl-add</code> <var>switch</var> <var>direction</var> <var>priority</var> <var>match</var> <var>verdict</var></dt>
+      <dd>
+        <p>
+          Adds the specified ACL to <var>switch</var>.
+          <var>direction</var> must be either <code>from-lport</code> or
+          <code>to-lport</code>.  <var>priority</var> must be between
+          <code>0</code> and <code>32767</code>, inclusive.  A full
+          description of the fields are in <code>ovn-nb</code>(5).  If
+          <code>--may-exist</code> is specified, adding a duplicated ACL
+          succeeds but the ACL is not really created. Without
+          <code>--may-exist</code>, adding a duplicated ACL results in
+          error.
+        </p>
+
+        <p>
+          The <code>--log</code> option enables packet logging for the ACL.
+          The options <code>--severity</code> and <code>--name</code> specify a
+          severity and name, respectively, for log entries (and also enable
+          logging).  The severity must be one of <code>alert</code>,
+          <code>warning</code>, <code>notice</code>, <code>info</code>, or
+          <code>debug</code>.  If a severity is not specified, the default is
+          <code>info</code>.
+        </p>
       </dd>
 
       <dt><code>acl-del</code> <var>switch</var> [<var>direction</var> [<var>priority</var> <var>match</var>]]</dt>
diff --git a/ovn/utilities/ovn-nbctl.c b/ovn/utilities/ovn-nbctl.c
index ce692acb3fb8..3cbb5f0f6280 100644
--- a/ovn/utilities/ovn-nbctl.c
+++ b/ovn/utilities/ovn-nbctl.c
@@ -24,6 +24,7 @@ 
 #include "dirs.h"
 #include "fatal-signal.h"
 #include "openvswitch/json.h"
+#include "ovn/lib/acl-log.h"
 #include "ovn/lib/ovn-nb-idl.h"
 #include "ovn/lib/ovn-util.h"
 #include "packets.h"
@@ -332,7 +333,8 @@  Logical switch commands:\n\
   ls-list                   print the names of all logical switches\n\
 \n\
 ACL commands:\n\
-  acl-add SWITCH DIRECTION PRIORITY MATCH ACTION [log]\n\
+  [--log] [--severity=SEVERITY] [--name=NAME] [--may-exist]\n\
+  acl-add SWITCH DIRECTION PRIORITY MATCH ACTION\n\
                             add an ACL to SWITCH\n\
   acl-del SWITCH [DIRECTION [PRIORITY MATCH]]\n\
                             remove ACLs from SWITCH\n\
@@ -1311,9 +1313,21 @@  nbctl_acl_list(struct ctl_context *ctx)
 
     for (i = 0; i < ls->n_acls; i++) {
         const struct nbrec_acl *acl = acls[i];
-        ds_put_format(&ctx->output, "%10s %5"PRId64" (%s) %s%s\n",
-                      acl->direction, acl->priority,
-                      acl->match, acl->action, acl->log ? " log" : "");
+        ds_put_format(&ctx->output, "%10s %5"PRId64" (%s) %s",
+                      acl->direction, acl->priority, acl->match,
+                      acl->action);
+        if (acl->log) {
+            ds_put_cstr(&ctx->output, " log(");
+            if (acl->name) {
+                ds_put_format(&ctx->output, "name=%s,", acl->name);
+            }
+            if (acl->severity) {
+                ds_put_format(&ctx->output, "severity=%s", acl->severity);
+            }
+            ds_chomp(&ctx->output, ',');
+            ds_put_cstr(&ctx->output, ")");
+        }
+        ds_put_cstr(&ctx->output, "\n");
     }
 
     free(acls);
@@ -1369,9 +1383,23 @@  nbctl_acl_add(struct ctl_context *ctx)
     nbrec_acl_set_direction(acl, direction);
     nbrec_acl_set_match(acl, ctx->argv[4]);
     nbrec_acl_set_action(acl, action);
-    if (shash_find(&ctx->options, "--log") != NULL) {
+
+    /* Logging options. */
+    bool log = shash_find(&ctx->options, "--log") != NULL;
+    const char *severity = shash_find_data(&ctx->options, "--severity");
+    const char *name = shash_find_data(&ctx->options, "--name");
+    if (log || severity || name) {
         nbrec_acl_set_log(acl, true);
     }
+    if (severity) {
+        if (log_severity_from_string(severity) == UINT8_MAX) {
+            ctl_fatal("bad severity: %s", severity);
+        }
+        nbrec_acl_set_severity(acl, severity);
+    }
+    if (name) {
+        nbrec_acl_set_name(acl, name);
+    }
 
     /* Check if same acl already exists for the ls */
     for (size_t i = 0; i < ls->n_acls; i++) {
@@ -3292,6 +3320,8 @@  static const struct ctl_table_class tables[NBREC_N_TABLES] = {
 
     [NBREC_TABLE_ADDRESS_SET].row_ids[0]
     = {&nbrec_address_set_col_name, NULL, NULL},
+
+    [NBREC_TABLE_ACL].row_ids[0] = {&nbrec_acl_col_name, NULL, NULL},
 };
 
 static void
@@ -3543,8 +3573,8 @@  static const struct ctl_command_syntax nbctl_commands[] = {
     { "ls-list", 0, 0, "", NULL, nbctl_ls_list, NULL, "", RO },
 
     /* acl commands. */
-    { "acl-add", 5, 5, "SWITCH DIRECTION PRIORITY MATCH ACTION", NULL,
-      nbctl_acl_add, NULL, "--log,--may-exist", RW },
+    { "acl-add", 5, 6, "SWITCH DIRECTION PRIORITY MATCH ACTION", NULL,
+      nbctl_acl_add, NULL, "--log,--may-exist,--name=,--severity=", RW },
     { "acl-del", 1, 4, "SWITCH [DIRECTION [PRIORITY MATCH]]", NULL,
       nbctl_acl_del, NULL, "", RW },
     { "acl-list", 1, 1, "SWITCH", NULL, nbctl_acl_list, NULL, "", RO },
diff --git a/ovn/utilities/ovn-trace.c b/ovn/utilities/ovn-trace.c
index ab56221d7829..0fe05f839cc6 100644
--- a/ovn/utilities/ovn-trace.c
+++ b/ovn/utilities/ovn-trace.c
@@ -34,6 +34,7 @@ 
 #include "ovn/actions.h"
 #include "ovn/expr.h"
 #include "ovn/lex.h"
+#include "ovn/lib/acl-log.h"
 #include "ovn/lib/logical-fields.h"
 #include "ovn/lib/ovn-sb-idl.h"
 #include "ovn/lib/ovn-dhcp.h"
@@ -1682,6 +1683,20 @@  execute_ct_nat(const struct ovnact_ct_nat *ct_nat,
 }
 
 static void
+execute_log(const struct ovnact_log *log, struct flow *uflow,
+            struct ovs_list *super)
+{
+    char *packet_str = flow_to_string(uflow, NULL);
+    ovntrace_node_append(super, OVNTRACE_NODE_TRANSFORMATION,
+                    "LOG: ACL name=%s, verdict=%s, severity=%s, packet=\"%s\"",
+                    log->name ? log->name : "<unnamed>",
+                    log_verdict_to_string(log->verdict),
+                    log_severity_to_string(log->severity),
+                    packet_str);
+    free(packet_str);
+}
+
+static void
 trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
               const struct ovntrace_datapath *dp, struct flow *uflow,
               uint8_t table_id, enum ovnact_pipeline pipeline,
@@ -1816,6 +1831,10 @@  trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
         case OVNACT_DNS_LOOKUP:
             execute_dns_lookup(ovnact_get_DNS_LOOKUP(a), uflow, super);
             break;
+
+        case OVNACT_LOG:
+            execute_log(ovnact_get_LOG(a), uflow, super);
+            break;
         }
 
     }
diff --git a/tests/ovn-nbctl.at b/tests/ovn-nbctl.at
index 29496c5c73e0..354b8df96d01 100644
--- a/tests/ovn-nbctl.at
+++ b/tests/ovn-nbctl.at
@@ -195,7 +195,7 @@  OVN_NBCTL_TEST_START
 
 AT_CHECK([ovn-nbctl ls-add ls0])
 AT_CHECK([ovn-nbctl --log acl-add ls0 from-lport 600 udp drop])
-AT_CHECK([ovn-nbctl --log acl-add ls0 to-lport 500 udp drop])
+AT_CHECK([ovn-nbctl --log --name=test --severity=info acl-add ls0 to-lport 500 udp drop])
 AT_CHECK([ovn-nbctl acl-add ls0 from-lport 400 tcp drop])
 AT_CHECK([ovn-nbctl acl-add ls0 to-lport 300 tcp drop])
 AT_CHECK([ovn-nbctl acl-add ls0 from-lport 200 ip drop])
@@ -206,10 +206,10 @@  AT_CHECK([grep 'already existed' stderr], [0], [ignore])
 AT_CHECK([ovn-nbctl --may-exist acl-add ls0 to-lport 100 ip drop])
 
 AT_CHECK([ovn-nbctl acl-list ls0], [0], [dnl
-from-lport   600 (udp) drop log
+from-lport   600 (udp) drop log()
 from-lport   400 (tcp) drop
 from-lport   200 (ip) drop
-  to-lport   500 (udp) drop log
+  to-lport   500 (udp) drop log(name=test,severity=info)
   to-lport   300 (tcp) drop
   to-lport   100 (ip) drop
 ])
@@ -217,7 +217,7 @@  from-lport   200 (ip) drop
 dnl Delete in one direction.
 AT_CHECK([ovn-nbctl acl-del ls0 to-lport])
 AT_CHECK([ovn-nbctl acl-list ls0], [0], [dnl
-from-lport   600 (udp) drop log
+from-lport   600 (udp) drop log()
 from-lport   400 (tcp) drop
 from-lport   200 (ip) drop
 ])
diff --git a/tests/ovn.at b/tests/ovn.at
index 248aea4b26df..fde4fe9591ea 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -5741,6 +5741,111 @@  OVN_CLEANUP([hv1],[hv2])
 
 AT_CLEANUP
 
+
+AT_SETUP([ovn -- ACL logging])
+AT_KEYWORDS([ovn])
+ovn_start
+
+net_add n1
+
+sim_add hv
+as hv
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+for i in lp1 lp2; do
+    ovs-vsctl -- add-port br-int $i -- \
+        set interface $i external-ids:iface-id=$i \
+        options:tx_pcap=hv/$i-tx.pcap \
+        options:rxq_pcap=hv/$i-rx.pcap
+done
+
+lp1_mac="f0:00:00:00:00:01"
+lp1_ip="192.168.1.2"
+
+lp2_mac="f0:00:00:00:00:02"
+lp2_ip="192.168.1.3"
+
+ovn-nbctl ls-add lsw0
+ovn-nbctl --wait=sb lsp-add lsw0 lp1
+ovn-nbctl --wait=sb lsp-add lsw0 lp2
+ovn-nbctl lsp-set-addresses lp1 $lp1_mac
+ovn-nbctl lsp-set-addresses lp2 $lp2_mac
+ovn-nbctl --wait=sb sync
+
+ovn-nbctl acl-add lsw0 to-lport 1000 'tcp.dst==80' drop
+ovn-nbctl --log --severity=alert --name=drop-flow acl-add lsw0 to-lport 1000 'tcp.dst==81' drop
+
+ovn-nbctl acl-add lsw0 to-lport 1000 'tcp.dst==82' allow
+ovn-nbctl --log --severity=info --name=allow-flow acl-add lsw0 to-lport 1000 'tcp.dst==83' allow
+
+ovn-nbctl acl-add lsw0 to-lport 1000 'tcp.dst==84' allow-related
+ovn-nbctl --log acl-add lsw0 to-lport 1000 'tcp.dst==85' allow-related
+
+ovn-nbctl acl-add lsw0 to-lport 1000 'tcp.dst==86' reject
+ovn-nbctl --log --severity=alert --name=reject-flow acl-add lsw0 to-lport 1000 'tcp.dst==87' reject
+
+ovn-sbctl dump-flows
+
+
+# Send packet that should be dropped without logging.
+packet="inport==\"lp1\" && eth.src==$lp1_mac && eth.dst==$lp2_mac &&
+        ip4 && ip.ttl==64 && ip4.src==$lp1_ip && ip4.dst==$lp2_ip &&
+        tcp && tcp.flags==2 && tcp.src==4360 && tcp.dst==80"
+as hv ovs-appctl -t ovn-controller inject-pkt "$packet"
+
+# Send packet that should be dropped with logging.
+packet="inport==\"lp1\" && eth.src==$lp1_mac && eth.dst==$lp2_mac &&
+        ip4 && ip.ttl==64 && ip4.src==$lp1_ip && ip4.dst==$lp2_ip &&
+        tcp && tcp.flags==2 && tcp.src==4361 && tcp.dst==81"
+as hv ovs-appctl -t ovn-controller inject-pkt "$packet"
+
+# Send packet that should be allowed without logging.
+packet="inport==\"lp1\" && eth.src==$lp1_mac && eth.dst==$lp2_mac &&
+        ip4 && ip.ttl==64 && ip4.src==$lp1_ip && ip4.dst==$lp2_ip &&
+        tcp && tcp.flags==2 && tcp.src==4362 && tcp.dst==82"
+as hv ovs-appctl -t ovn-controller inject-pkt "$packet"
+
+# Send packet that should be allowed with logging.
+packet="inport==\"lp1\" && eth.src==$lp1_mac && eth.dst==$lp2_mac &&
+        ip4 && ip.ttl==64 && ip4.src==$lp1_ip && ip4.dst==$lp2_ip &&
+        tcp && tcp.flags==2 && tcp.src==4363 && tcp.dst==83"
+as hv ovs-appctl -t ovn-controller inject-pkt "$packet"
+
+# Send packet that should allow related flows without logging.
+packet="inport==\"lp1\" && eth.src==$lp1_mac && eth.dst==$lp2_mac &&
+        ip4 && ip.ttl==64 && ip4.src==$lp1_ip && ip4.dst==$lp2_ip &&
+        tcp && tcp.flags==2 && tcp.src==4364 && tcp.dst==84"
+as hv ovs-appctl -t ovn-controller inject-pkt "$packet"
+
+# Send packet that should allow related flows with logging.
+packet="inport==\"lp1\" && eth.src==$lp1_mac && eth.dst==$lp2_mac &&
+        ip4 && ip.ttl==64 && ip4.src==$lp1_ip && ip4.dst==$lp2_ip &&
+        tcp && tcp.flags==2 && tcp.src==4365 && tcp.dst==85"
+as hv ovs-appctl -t ovn-controller inject-pkt "$packet"
+
+# Send packet that should allow related flows with logging.
+packet="inport==\"lp1\" && eth.src==$lp1_mac && eth.dst==$lp2_mac &&
+        ip4 && ip.ttl==64 && ip4.src==$lp1_ip && ip4.dst==$lp2_ip &&
+        tcp && tcp.flags==2 && tcp.src==4366 && tcp.dst==86"
+as hv ovs-appctl -t ovn-controller inject-pkt "$packet"
+
+# Send packet that should allow related flows with logging.
+packet="inport==\"lp1\" && eth.src==$lp1_mac && eth.dst==$lp2_mac &&
+        ip4 && ip.ttl==64 && ip4.src==$lp1_ip && ip4.dst==$lp2_ip &&
+        tcp && tcp.flags==2 && tcp.src==4367 && tcp.dst==87"
+as hv ovs-appctl -t ovn-controller inject-pkt "$packet"
+
+AT_CHECK([grep 'acl_log' hv/ovn-controller.log | sed 's/.*name=/name=/'], [0], [dnl
+name="drop-flow", verdict=drop, severity=alert: tcp,vlan_tci=0x0000,dl_src=f0:00:00:00:00:01,dl_dst=f0:00:00:00:00:02,nw_src=192.168.1.2,nw_dst=192.168.1.3,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=4361,tp_dst=81,tcp_flags=syn
+name="allow-flow", verdict=allow, severity=info: tcp,vlan_tci=0x0000,dl_src=f0:00:00:00:00:01,dl_dst=f0:00:00:00:00:02,nw_src=192.168.1.2,nw_dst=192.168.1.3,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=4363,tp_dst=83,tcp_flags=syn
+name="<unnamed>", verdict=allow, severity=info: tcp,vlan_tci=0x0000,dl_src=f0:00:00:00:00:01,dl_dst=f0:00:00:00:00:02,nw_src=192.168.1.2,nw_dst=192.168.1.3,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=4365,tp_dst=85,tcp_flags=syn
+name="reject-flow", verdict=reject, severity=alert: tcp,vlan_tci=0x0000,dl_src=f0:00:00:00:00:01,dl_dst=f0:00:00:00:00:02,nw_src=192.168.1.2,nw_dst=192.168.1.3,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=4367,tp_dst=87,tcp_flags=syn
+])
+
+OVN_CLEANUP([hv])
+AT_CLEANUP
+
+
 AT_SETUP([ovn -- DSCP marking check])
 AT_KEYWORDS([ovn])
 ovn_start