diff mbox series

[ovs-dev,7/8] ovs-vswitchd: Implement OFPT_TABLE_FEATURES table modification request.

Message ID 20180830200056.15484-7-blp@ovn.org
State Accepted
Headers show
Series [ovs-dev,1/8] vconn: Avoid null dereference on error path. | expand

Commit Message

Ben Pfaff Aug. 30, 2018, 8 p.m. UTC
This allows a controller to change the name of OpenFlow flow tables in the
OVS software switch.

CC: Brad Cowie <brad@cowie.nz>
Signed-off-by: Ben Pfaff <blp@ovn.org>
---
 NEWS                             |   4 +
 include/openvswitch/ofp-errors.h |   3 +
 include/openvswitch/ofp-msgs.h   |   2 +-
 include/openvswitch/ofp-table.h  |  17 ++-
 include/openvswitch/vconn.h      |   2 +
 lib/ofp-print.c                  |   5 +-
 lib/ofp-table.c                  | 177 ++++++++++++++++++++----
 lib/vconn.c                      |  40 ++++++
 ofproto/ofproto-dpif.c           |   1 +
 ofproto/ofproto-provider.h       |  19 +++
 ofproto/ofproto.c                | 284 +++++++++++++++++++++++++++++++++++----
 tests/ofproto.at                 |  90 +++++++++++++
 utilities/ovs-ofctl.8.in         |  11 ++
 utilities/ovs-ofctl.c            | 127 ++++++++++++++---
 14 files changed, 701 insertions(+), 81 deletions(-)

Comments

Justin Pettit Jan. 11, 2019, 9:07 a.m. UTC | #1
> On Aug 30, 2018, at 1:00 PM, Ben Pfaff <blp@ovn.org> wrote:
> 
> diff --git a/lib/ofp-table.c b/lib/ofp-table.c
> index afa9b9103b88..e295441af20a 100644
> --- a/lib/ofp-table.c
> +++ b/lib/ofp-table.c
> @@ -350,11 +350,15 @@ parse_oxms(struct ofpbuf *payload, bool loose,
> /* Converts an OFPMP_TABLE_FEATURES request or reply in 'msg' into an abstract
>  * ofputil_table_features in 'tf'.
>  *
> - * If 'loose' is true, this function ignores properties and values that it does
> - * not understand, as a controller would want to do when interpreting
> - * capabilities provided by a switch.  If 'loose' is false, this function
> - * treats unknown properties and values as an error, as a switch would want to
> - * do when interpreting a configuration request made by a controller.
> + * If 'raw_properties' is nonnull function ignores properties and values that

I think it would be clearer if "the" or "this" was before "function".

> @@ -418,8 +425,11 @@ ofputil_decode_table_features(struct ofpbuf *msg,
> 
>     struct ofpbuf properties = ofpbuf_const_initializer(ofpbuf_pull(msg, len),
>                                                         len);
> -    tf->any_properties = properties.size > 0;
>     ofpbuf_pull(&properties, sizeof *otf);
> +    tf->any_properties = properties.size > 0;

Is changing the order of these operations a bug fix that should be backported?

> @@ -514,7 +524,7 @@ ofputil_decode_table_features(struct ofpbuf *msg,
>      * OpenFlow 1.5 requires all of them if any property is present. */
>     if ((seen & OFPTFPT13_REQUIRED) != OFPTFPT13_REQUIRED
>         && (tf->any_properties || oh->version < OFP15_VERSION)) {
> -        VLOG_WARN_RL(&rl, "table features message missing required property");
> +        VLOG_WARN_RL(&rl, "table features message missing required property %d", seen);

Since 'seen' is a bitmap, it might be easer to read in hex.

> @@ -1293,6 +1314,10 @@ parse_ofp_table_mod(struct ofputil_table_mod *tm, const char *table_id,
>     } else if (!strcmp(setting, "novacancy")) {
>         tm->vacancy = OFPUTIL_TABLE_VACANCY_OFF;
>         *usable_versions = (1 << OFP14_VERSION) | (1u << OFP15_VERSION);
> +    } else if (tm->table_id != OFPTT_ALL && !strncmp(setting, "name:", 5)) {
> +        *namep = setting + 5;
> +        *usable_versions = ((1 << OFP13_VERSION) | (1u << OFP14_VERSION)
> +                            | (1u << OFP15_VERSION));

Is OFP16_VERSION left out on a purpose?

> +/* Returns true if the 1-bits in 'super' are a superset of the 1-bits in 'sub',
> + * false otherwise.  'super' and 'sub' both have 'n_bits' bits. */
> +static bool
> +bitmap_is_superset(const unsigned long int *super,
> +                   const unsigned long int *sub, size_t n_bits)
> +{
> +    size_t n_longs = bitmap_n_longs(n_bits);
> +    for (size_t i = 0; i < n_longs; i++) {
> +        if (!ulong_is_superset(super[i], sub[i])) {
> +            return false;
> +        }
> +    }
> +    return true;
> +}

Do you think it's worth putting this in "bitmap.h"?

> +/* Returns true if the 1-bits in 'super' are a superset of the 1-bits in 'sub',
> + * false otherwise. */
> +static bool
> +mf_bitmap_is_superset(const struct mf_bitmap *super,
> +                      const struct mf_bitmap *sub)
> +{
> +    return bitmap_is_superset(super->bm, sub->bm, MFF_N_IDS);
> +}

Do you think it's worth putting this in "meta-flow.h"?

> diff --git a/ofproto/ofproto.c b/ofproto/ofproto.c
> index c9d73c10c0ae..d65a3fea1559 100644
> --- a/ofproto/ofproto.c
> +++ b/ofproto/ofproto.c
> 
> +static void
> +copy_properties(struct ofputil_table_features *dst,
> +                const struct ofputil_table_features *src)
> +{
> +    dst->any_properties = src->any_properties;
> +    if (src->any_properties) {
> +        dst->nonmiss = src->nonmiss;
> +        dst->miss = src->miss;
> +        dst->match = src->match;
> +        dst->mask = src->mask;
> +        dst->wildcard = src->wildcard;
> +    }
> +
> +}

I think there's an empty newline above.

> +static void
> +handle_table_features_request(struct ofconn *ofconn,
> +                              const struct ovs_list *msgs)
> +{
> ...
> +    /* Update the table features configuration, if requested. */
>     struct ofputil_table_features *features;
>     query_tables(ofproto, &features, NULL);
> +    if (!ovs_list_is_singleton(msgs) || msg->size) {
> +        int retval = handle_table_features_change(ofconn, msgs, features);
> +        if (retval) {
> +            if (retval < 0) {
> +                /* handle_table_features_change() already sent an error. */
> +            } else {
> +                ofconn_send_error(ofconn, request, retval);
> +            }
> +            return;

Does 'features' need to be freed?

> 
> +/* Returns true if oftable_set_name(table, name, level) would be effective,
> + * false otherwise.  */
> +static bool
> +oftable_may_set_name(const struct oftable *table, const char *name, int level)
> +{
> +    return (level >= table->name_level
> +            || !name
> +            || !strncmp(name, table->name,
> +                        strnlen(name, OFP_MAX_TABLE_NAME_LEN)));

I think 'table->name' can be NULL, in which case, I believe the behavior of strncmp is undefined.

The strncmp() check seems to return false if the name is being changed, which seems like different behavior from oftable_may_set_name().

> diff --git a/utilities/ovs-ofctl.8.in b/utilities/ovs-ofctl.8.in
> index 4850f4a25504..968e1ec347af 100644
> --- a/utilities/ovs-ofctl.8.in
> +++ b/utilities/ovs-ofctl.8.in
> @@ -88,6 +88,17 @@ Send to controller.  (This is how an OpenFlow 1.0 switch always
> handles packets that do not match any flow in the last table.)
> .RE
> .IP
> +In OpenFlow 1.3 and later (which must be enabled with the \fB\-O\fR
> +option) and Open vSwitch 2.11 and later only, \fBmod\-table\fR can
> +change the name of a table:
> +.RS
> +.IP \fBname:\fInew-name\fR
> +Changes the name of the table to \fInew-name\fR.  (This will be
> +ineffective if the name is set via the \fBname\fR column in the
> +\fBFlow_Table\fR table in the \fBOpen_vSwitch\fR database as described
> +in \fBovs\-vswitchd.conf.db\fR(5).)

Do you think it's worth mentioning that the name can be cleared by leaving off "new-name"?  Also, it may be worth documenting that the name must be cleared before it can be changed.

> diff --git a/utilities/ovs-ofctl.c b/utilities/ovs-ofctl.c
> index aa9a1291e60a..82b2b42a92cd 100644
> --- a/utilities/ovs-ofctl.c
> +++ b/utilities/ovs-ofctl.c

> @@ -2607,27 +2687,32 @@ ofctl_mod_table(struct ovs_cmdl_context *ctx)
> 
> +    if (name) {
> +        change_table_name(vconn, tm.table_id, name);
> +    } else {
> +        /* For OpenFlow 1.4+, ovs-ofctl mod-table should not affect
> +         * table-config properties that the user didn't ask to change, so it is
> +         * necessary to restore the current configuration of table-config
> +         * parameters using OFPMP14_TABLE_DESC request. */
> +        if ((allowed_versions & (1u << OFP14_VERSION)) ||
> +            (allowed_versions & (1u << OFP15_VERSION))) {

Should OFP16_VERSION be included?

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

--Justin
Ben Pfaff Jan. 16, 2019, 12:07 a.m. UTC | #2
Thanks for the review.  I fixed the obvious stuff and just dropped the
request.

On Fri, Jan 11, 2019 at 01:07:36AM -0800, Justin Pettit wrote:
> 
> > On Aug 30, 2018, at 1:00 PM, Ben Pfaff <blp@ovn.org> wrote:
> > @@ -418,8 +425,11 @@ ofputil_decode_table_features(struct ofpbuf *msg,
> > 
> >     struct ofpbuf properties = ofpbuf_const_initializer(ofpbuf_pull(msg, len),
> >                                                         len);
> > -    tf->any_properties = properties.size > 0;
> >     ofpbuf_pull(&properties, sizeof *otf);
> > +    tf->any_properties = properties.size > 0;
> 
> Is changing the order of these operations a bug fix that should be backported?

I think so.  I broke this out as a separate patch and backported it.

> > @@ -514,7 +524,7 @@ ofputil_decode_table_features(struct ofpbuf *msg,
> >      * OpenFlow 1.5 requires all of them if any property is present. */
> >     if ((seen & OFPTFPT13_REQUIRED) != OFPTFPT13_REQUIRED
> >         && (tf->any_properties || oh->version < OFP15_VERSION)) {
> > -        VLOG_WARN_RL(&rl, "table features message missing required property");
> > +        VLOG_WARN_RL(&rl, "table features message missing required property %d", seen);
> 
> Since 'seen' is a bitmap, it might be easer to read in hex.

I think this doesn't need to be there at all.  It was more of a debug
print during development.  I dropped the change.

> > @@ -1293,6 +1314,10 @@ parse_ofp_table_mod(struct ofputil_table_mod *tm, const char *table_id,
> >     } else if (!strcmp(setting, "novacancy")) {
> >         tm->vacancy = OFPUTIL_TABLE_VACANCY_OFF;
> >         *usable_versions = (1 << OFP14_VERSION) | (1u << OFP15_VERSION);
> > +    } else if (tm->table_id != OFPTT_ALL && !strncmp(setting, "name:", 5)) {
> > +        *namep = setting + 5;
> > +        *usable_versions = ((1 << OFP13_VERSION) | (1u << OFP14_VERSION)
> > +                            | (1u << OFP15_VERSION));
> 
> Is OFP16_VERSION left out on a purpose?

None of the other cases considers OFP16_VERSION, so it's consistent with
those.  It's probably a bug overall though.

OF1.6 is dead and will never be released.  It probably makes sense to
just remove all support.  I'll work on that.

> > +static void
> > +handle_table_features_request(struct ofconn *ofconn,
> > +                              const struct ovs_list *msgs)
> > +{
> > ...
> > +    /* Update the table features configuration, if requested. */
> >     struct ofputil_table_features *features;
> >     query_tables(ofproto, &features, NULL);
> > +    if (!ovs_list_is_singleton(msgs) || msg->size) {
> > +        int retval = handle_table_features_change(ofconn, msgs, features);
> > +        if (retval) {
> > +            if (retval < 0) {
> > +                /* handle_table_features_change() already sent an error. */
> > +            } else {
> > +                ofconn_send_error(ofconn, request, retval);
> > +            }
> > +            return;
> 
> Does 'features' need to be freed?

Good catch, thanks.

I moved the existing free call to just before checking the return value:

@@ -3995,6 +3994,7 @@ handle_table_features_request(struct ofconn *ofconn,
     query_tables(ofproto, &features, NULL);
     if (!ovs_list_is_singleton(msgs) || msg->size) {
         int retval = handle_table_features_change(ofconn, msgs, features);
+        free(features);
         if (retval) {
             if (retval < 0) {
                 /* handle_table_features_change() already sent an error. */
@@ -4005,7 +4005,6 @@ handle_table_features_request(struct ofconn *ofconn,
         }
 
         /* Features may have changed, re-query. */
-        free(features);
         query_tables(ofproto, &features, NULL);
     }
 
> > 
> > +/* Returns true if oftable_set_name(table, name, level) would be effective,
> > + * false otherwise.  */
> > +static bool
> > +oftable_may_set_name(const struct oftable *table, const char *name, int level)
> > +{
> > +    return (level >= table->name_level
> > +            || !name
> > +            || !strncmp(name, table->name,
> > +                        strnlen(name, OFP_MAX_TABLE_NAME_LEN)));
> 
> I think 'table->name' can be NULL, in which case, I believe the behavior of strncmp is undefined.

It appears that if table->name is NULL, then table->name_level is always
0, so strncmp() doesn't get called.

I added || !table->name to the condition just in case.  It would suck to
segfault because of a bug in table names.

> The strncmp() check seems to return false if the name is being changed, which seems like different behavior from oftable_may_set_name().

If this check gets to the point of calling strncmp(), then the name
can't be changed because the level is too low, but we are still willing
to return true if the table's current name is the same as the requested
one, hence the !strncmp() check.

> > diff --git a/utilities/ovs-ofctl.8.in b/utilities/ovs-ofctl.8.in
> > index 4850f4a25504..968e1ec347af 100644
> > --- a/utilities/ovs-ofctl.8.in
> > +++ b/utilities/ovs-ofctl.8.in
> > @@ -88,6 +88,17 @@ Send to controller.  (This is how an OpenFlow 1.0 switch always
> > handles packets that do not match any flow in the last table.)
> > .RE
> > .IP
> > +In OpenFlow 1.3 and later (which must be enabled with the \fB\-O\fR
> > +option) and Open vSwitch 2.11 and later only, \fBmod\-table\fR can
> > +change the name of a table:
> > +.RS
> > +.IP \fBname:\fInew-name\fR
> > +Changes the name of the table to \fInew-name\fR.  (This will be
> > +ineffective if the name is set via the \fBname\fR column in the
> > +\fBFlow_Table\fR table in the \fBOpen_vSwitch\fR database as described
> > +in \fBovs\-vswitchd.conf.db\fR(5).)
> 
> Do you think it's worth mentioning that the name can be cleared by
> leaving off "new-name"?  

OK, done.

> Also, it may be worth documenting that the name must be cleared before
> it can be changed.

It's not necessary to do that, unless it's already set to something
different via OVSDB.  The parenthetical tries to document that.

> > diff --git a/utilities/ovs-ofctl.c b/utilities/ovs-ofctl.c
> > index aa9a1291e60a..82b2b42a92cd 100644
> > --- a/utilities/ovs-ofctl.c
> > +++ b/utilities/ovs-ofctl.c
> 
> > @@ -2607,27 +2687,32 @@ ofctl_mod_table(struct ovs_cmdl_context *ctx)
> > 
> > +    if (name) {
> > +        change_table_name(vconn, tm.table_id, name);
> > +    } else {
> > +        /* For OpenFlow 1.4+, ovs-ofctl mod-table should not affect
> > +         * table-config properties that the user didn't ask to change, so it is
> > +         * necessary to restore the current configuration of table-config
> > +         * parameters using OFPMP14_TABLE_DESC request. */
> > +        if ((allowed_versions & (1u << OFP14_VERSION)) ||
> > +            (allowed_versions & (1u << OFP15_VERSION))) {
> 
> Should OFP16_VERSION be included?

Might as well.

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

Thanks for the review!
diff mbox series

Patch

diff --git a/NEWS b/NEWS
index 33b4d8a2368f..6afeeacd6a5e 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,10 @@  Post-v2.10.0
 ---------------------
    - Linux datapath:
      * Support for the kernel versions 4.16.x and 4.17.x.
+   - OpenFlow:
+     * OFPMP_TABLE_FEATURES_REQUEST can now modify table features.
+   - ovs-ofctl:
+     * "mod-table" command can now change OpenFlow table names.
    - The environment variable OVS_SYSLOG_METHOD, if set, is now used
      as the default syslog method.
    - The environment variable OVS_CTL_TIMEOUT, if set, is now used
diff --git a/include/openvswitch/ofp-errors.h b/include/openvswitch/ofp-errors.h
index 6e8e55ab4f4f..2f00851f1b7a 100644
--- a/include/openvswitch/ofp-errors.h
+++ b/include/openvswitch/ofp-errors.h
@@ -674,6 +674,9 @@  enum ofperr {
     /* OF1.5+(13,10).  Can't handle this many flow tables. */
     OFPERR_OFPTFFC_TOO_MANY,
 
+    /* NX1.3+(44).  Table specified multiple times. */
+    OFPERR_NXTFFC_DUP_TABLE,
+
 
 /* ## ------------------ ## */
 /* ## OFPET_BAD_PROPERTY ## */
diff --git a/include/openvswitch/ofp-msgs.h b/include/openvswitch/ofp-msgs.h
index 66897b1cdeab..067ab3f0b095 100644
--- a/include/openvswitch/ofp-msgs.h
+++ b/include/openvswitch/ofp-msgs.h
@@ -422,7 +422,7 @@  enum ofpraw {
     /* OFPST 1.3+ (11): struct ofp13_meter_features. */
     OFPRAW_OFPST13_METER_FEATURES_REPLY,
 
-    /* OFPST 1.3+ (12): void. */
+    /* OFPST 1.3+ (12): uint8_t[8][]. */
     OFPRAW_OFPST13_TABLE_FEATURES_REQUEST,
 
     /* OFPST 1.3+ (12): struct ofp13_table_features, uint8_t[8][]. */
diff --git a/include/openvswitch/ofp-table.h b/include/openvswitch/ofp-table.h
index 370ec85aec55..8e0a1cbe2e3a 100644
--- a/include/openvswitch/ofp-table.h
+++ b/include/openvswitch/ofp-table.h
@@ -155,7 +155,7 @@  struct ofpbuf *ofputil_encode_table_mod(const struct ofputil_table_mod *,
                                        enum ofputil_protocol);
 void ofputil_table_mod_format(struct ds *, const struct ofputil_table_mod *,
                               const struct ofputil_table_map *);
-char *parse_ofp_table_mod(struct ofputil_table_mod *,
+char *parse_ofp_table_mod(struct ofputil_table_mod *, const char **namep,
                           const char *table_id, const char *flow_miss_handling,
                           const struct ofputil_table_map *,
                           uint32_t *usable_versions)
@@ -271,15 +271,18 @@  struct ofputil_table_features {
     struct mf_bitmap wildcard;  /* Subset of 'match' that may be wildcarded. */
 };
 
-int ofputil_decode_table_features(struct ofpbuf *,
-                                  struct ofputil_table_features *, bool loose);
+int ofputil_decode_table_features(
+    struct ofpbuf *, struct ofputil_table_features *,
+    struct ofpbuf *raw_properties);
 
 struct ofpbuf *ofputil_encode_table_features_request(enum ofp_version);
 
 struct ofpbuf *ofputil_encode_table_desc_request(enum ofp_version);
 
-void ofputil_append_table_features_reply(
-    const struct ofputil_table_features *tf, struct ovs_list *replies);
+void ofputil_append_table_features(
+    const struct ofputil_table_features *tf,
+    const struct ofpbuf *raw_properties,
+    struct ovs_list *msgs);
 
 void ofputil_table_features_format(
     struct ds *, const struct ofputil_table_features *features,
@@ -290,6 +293,10 @@  void ofputil_table_features_format(
 void ofputil_table_features_format_finish(struct ds *,
                                           int first_ditto, int last_ditto);
 
+bool ofputil_table_features_are_superset(
+    const struct ofputil_table_features *super,
+    const struct ofputil_table_features *sub);
+
 /* Abstract table stats.
  *
  * This corresponds to the OpenFlow 1.3 table statistics structure, which only
diff --git a/include/openvswitch/vconn.h b/include/openvswitch/vconn.h
index b30d46af0eb8..a9316b111dae 100644
--- a/include/openvswitch/vconn.h
+++ b/include/openvswitch/vconn.h
@@ -58,6 +58,8 @@  int vconn_transact(struct vconn *, struct ofpbuf *, struct ofpbuf **);
 int vconn_transact_noreply(struct vconn *, struct ofpbuf *, struct ofpbuf **);
 int vconn_transact_multiple_noreply(struct vconn *, struct ovs_list *requests,
                                     struct ofpbuf **replyp);
+int vconn_transact_multipart(struct vconn *, struct ovs_list *request,
+                             struct ovs_list *replies);
 
 int vconn_dump_flows(struct vconn *, const struct ofputil_flow_stats_request *,
                      enum ofputil_protocol,
diff --git a/lib/ofp-print.c b/lib/ofp-print.c
index 9d4141e3b747..9d435336c2f2 100644
--- a/lib/ofp-print.c
+++ b/lib/ofp-print.c
@@ -234,9 +234,8 @@  ofp_print_table_features_reply(struct ds *s, const struct ofp_header *oh)
     int first_ditto = -1, last_ditto = -1;
     for (int i = 0; ; i++) {
         struct ofputil_table_features tf;
-        int retval;
-
-        retval = ofputil_decode_table_features(&b, &tf, true);
+        struct ofpbuf raw_properties;
+        int retval = ofputil_decode_table_features(&b, &tf, &raw_properties);
         if (retval) {
             ofputil_table_features_format_finish(s, first_ditto, last_ditto);
             return retval != EOF ? retval : 0;
diff --git a/lib/ofp-table.c b/lib/ofp-table.c
index afa9b9103b88..e295441af20a 100644
--- a/lib/ofp-table.c
+++ b/lib/ofp-table.c
@@ -350,11 +350,15 @@  parse_oxms(struct ofpbuf *payload, bool loose,
 /* Converts an OFPMP_TABLE_FEATURES request or reply in 'msg' into an abstract
  * ofputil_table_features in 'tf'.
  *
- * If 'loose' is true, this function ignores properties and values that it does
- * not understand, as a controller would want to do when interpreting
- * capabilities provided by a switch.  If 'loose' is false, this function
- * treats unknown properties and values as an error, as a switch would want to
- * do when interpreting a configuration request made by a controller.
+ * If 'raw_properties' is nonnull function ignores properties and values that
+ * it does not understand, as a controller would want to do when interpreting
+ * capabilities provided by a switch.  In this mode, on success, it initializes
+ * 'raw_properties' to contain the properties that were parsed; this allows the
+ * caller to later re-serialize the same properties without change.
+ *
+ * If 'raw_properties' is null, this function treats unknown properties and
+ * values as an error, as a switch would want to do when interpreting a
+ * configuration request made by a controller.
  *
  * A single OpenFlow message can specify features for multiple tables.  Calling
  * this function multiple times for a single 'msg' iterates through the tables
@@ -365,8 +369,11 @@  parse_oxms(struct ofpbuf *payload, bool loose,
  * a positive "enum ofperr" value. */
 int
 ofputil_decode_table_features(struct ofpbuf *msg,
-                              struct ofputil_table_features *tf, bool loose)
+                              struct ofputil_table_features *tf,
+                              struct ofpbuf *raw_properties)
 {
+    bool loose = raw_properties != NULL;
+
     memset(tf, 0, sizeof *tf);
 
     if (!msg->header) {
@@ -418,8 +425,11 @@  ofputil_decode_table_features(struct ofpbuf *msg,
 
     struct ofpbuf properties = ofpbuf_const_initializer(ofpbuf_pull(msg, len),
                                                         len);
-    tf->any_properties = properties.size > 0;
     ofpbuf_pull(&properties, sizeof *otf);
+    tf->any_properties = properties.size > 0;
+    if (raw_properties) {
+        *raw_properties = properties;
+    }
     uint32_t seen = 0;
     while (properties.size > 0) {
         struct ofpbuf payload;
@@ -514,7 +524,7 @@  ofputil_decode_table_features(struct ofpbuf *msg,
      * OpenFlow 1.5 requires all of them if any property is present. */
     if ((seen & OFPTFPT13_REQUIRED) != OFPTFPT13_REQUIRED
         && (tf->any_properties || oh->version < OFP15_VERSION)) {
-        VLOG_WARN_RL(&rl, "table features message missing required property");
+        VLOG_WARN_RL(&rl, "table features message missing required property %d", seen);
         return OFPERR_OFPTFFC_BAD_FEATURES;
     }
 
@@ -642,16 +652,21 @@  put_table_instruction_features(
                               OFPTFPT13_APPLY_SETFIELD, miss_offset, version);
 }
 
+/* Appends a table features record to 'msgs', which must be a
+ * OFPT_TABLE_FEATURES request or reply.  If 'raw_properties' is nonnull, then
+ * it uses its contents verbatim as the table features properties, ignoring the
+ * corresponding members of 'tf'. */
 void
-ofputil_append_table_features_reply(const struct ofputil_table_features *tf,
-                                    struct ovs_list *replies)
+ofputil_append_table_features(const struct ofputil_table_features *tf,
+                              const struct ofpbuf *raw_properties,
+                              struct ovs_list *msgs)
 {
-    struct ofpbuf *reply = ofpbuf_from_list(ovs_list_back(replies));
-    enum ofp_version version = ofpmp_version(replies);
-    size_t start_ofs = reply->size;
+    struct ofpbuf *msg = ofpbuf_from_list(ovs_list_back(msgs));
+    enum ofp_version version = ofpmp_version(msgs);
+    size_t start_ofs = msg->size;
     struct ofp13_table_features *otf;
 
-    otf = ofpbuf_put_zeros(reply, sizeof *otf);
+    otf = ofpbuf_put_zeros(msg, sizeof *otf);
     otf->table_id = tf->table_id;
     otf->command = version >= OFP15_VERSION ? tf->command : 0;
     ovs_strlcpy_arrays(otf->name, tf->name);
@@ -667,17 +682,21 @@  ofputil_append_table_features_reply(const struct ofputil_table_features *tf,
     }
     otf->max_entries = htonl(tf->max_entries);
 
-    put_table_instruction_features(reply, &tf->nonmiss, 0, version);
-    put_table_instruction_features(reply, &tf->miss, 1, version);
+    if (raw_properties) {
+        ofpbuf_put(msg, raw_properties->data, raw_properties->size);
+    } else if (tf->any_properties) {
+        put_table_instruction_features(msg, &tf->nonmiss, 0, version);
+        put_table_instruction_features(msg, &tf->miss, 1, version);
 
-    put_fields_property(reply, &tf->match, &tf->mask,
-                        OFPTFPT13_MATCH, version);
-    put_fields_property(reply, &tf->wildcard, NULL,
-                        OFPTFPT13_WILDCARDS, version);
+        put_fields_property(msg, &tf->match, &tf->mask,
+                            OFPTFPT13_MATCH, version);
+        put_fields_property(msg, &tf->wildcard, NULL,
+                            OFPTFPT13_WILDCARDS, version);
+    }
 
-    otf = ofpbuf_at_assert(reply, start_ofs, sizeof *otf);
-    otf->length = htons(reply->size - start_ofs);
-    ofpmp_postappend(replies, start_ofs);
+    otf = ofpbuf_at_assert(msg, start_ofs, sizeof *otf);
+    otf->length = htons(msg->size - start_ofs);
+    ofpmp_postappend(msgs, start_ofs);
 }
 
 static enum ofperr
@@ -1236,7 +1255,8 @@  exit:
 
 /* Convert 'table_id' and 'setting' (as described for the "mod-table" command
  * in the ovs-ofctl man page) into 'tm' for sending a table_mod command to a
- * switch.
+ * switch.  If 'setting' sets the name of the table, puts the new name in
+ * '*namep' (a suffix of 'setting'), otherwise stores NULL.
  *
  * Stores a bitmap of the OpenFlow versions that are usable for 'tm' into
  * '*usable_versions'.
@@ -1244,12 +1264,13 @@  exit:
  * Returns NULL if successful, otherwise a malloc()'d string describing the
  * error.  The caller is responsible for freeing the returned string. */
 char * OVS_WARN_UNUSED_RESULT
-parse_ofp_table_mod(struct ofputil_table_mod *tm, const char *table_id,
-                    const char *setting,
+parse_ofp_table_mod(struct ofputil_table_mod *tm, const char **namep,
+                    const char *table_id, const char *setting,
                     const struct ofputil_table_map *table_map,
                     uint32_t *usable_versions)
 {
     *usable_versions = 0;
+    *namep = NULL;
     if (!strcasecmp(table_id, "all")) {
         tm->table_id = OFPTT_ALL;
     } else if (!ofputil_table_from_string(table_id, table_map,
@@ -1293,6 +1314,10 @@  parse_ofp_table_mod(struct ofputil_table_mod *tm, const char *table_id,
     } else if (!strcmp(setting, "novacancy")) {
         tm->vacancy = OFPUTIL_TABLE_VACANCY_OFF;
         *usable_versions = (1 << OFP14_VERSION) | (1u << OFP15_VERSION);
+    } else if (tm->table_id != OFPTT_ALL && !strncmp(setting, "name:", 5)) {
+        *namep = setting + 5;
+        *usable_versions = ((1 << OFP13_VERSION) | (1u << OFP14_VERSION)
+                            | (1u << OFP15_VERSION));
     } else {
         return xasprintf("invalid table_mod setting %s", setting);
     }
@@ -1644,6 +1669,106 @@  ofputil_table_features_format_finish(struct ds *s,
         ds_put_format(s, "  tables %d...%d: ditto\n", first_ditto, last_ditto);
     }
 }
+
+/* Returns true if the 1-bits in 'super' are a superset of the 1-bits in 'sub',
+ * false otherwise. */
+static bool
+be64_is_superset(ovs_be64 super, ovs_be64 sub)
+{
+    return (super & sub) == sub;
+}
+
+/* Returns true if the 1-bits in 'super' are a superset of the 1-bits in 'sub',
+ * false otherwise. */
+static bool
+u32_is_superset(uint32_t super, uint32_t sub)
+{
+    return (super & sub) == sub;
+}
+
+/* Returns true if the 1-bits in 'super' are a superset of the 1-bits in 'sub',
+ * false otherwise. */
+static bool
+u64_is_superset(uint64_t super, uint64_t sub)
+{
+    return (super & sub) == sub;
+}
+
+/* Returns true if the 1-bits in 'super' are a superset of the 1-bits in 'sub',
+ * false otherwise. */
+static bool
+ulong_is_superset(unsigned long int super, unsigned long int sub)
+{
+    return (super & sub) == sub;
+}
+
+/* Returns true if the 1-bits in 'super' are a superset of the 1-bits in 'sub',
+ * false otherwise.  'super' and 'sub' both have 'n_bits' bits. */
+static bool
+bitmap_is_superset(const unsigned long int *super,
+                   const unsigned long int *sub, size_t n_bits)
+{
+    size_t n_longs = bitmap_n_longs(n_bits);
+    for (size_t i = 0; i < n_longs; i++) {
+        if (!ulong_is_superset(super[i], sub[i])) {
+            return false;
+        }
+    }
+    return true;
+}
+
+/* Returns true if the 1-bits in 'super' are a superset of the 1-bits in 'sub',
+ * false otherwise. */
+static bool
+mf_bitmap_is_superset(const struct mf_bitmap *super,
+                      const struct mf_bitmap *sub)
+{
+    return bitmap_is_superset(super->bm, sub->bm, MFF_N_IDS);
+}
+
+/* Returns true if 'super' is a superset of 'sub', false otherwise. */
+static bool
+ofputil_table_action_features_is_superset(
+    const struct ofputil_table_action_features *super,
+    const struct ofputil_table_action_features *sub)
+{
+    return (u64_is_superset(super->ofpacts, sub->ofpacts)
+            && mf_bitmap_is_superset(&super->set_fields, &sub->set_fields));
+}
+
+/* Returns true if 'super' is a superset of 'sub', false otherwise. */
+static bool
+ofputil_table_instruction_features_is_superset(
+    const struct ofputil_table_instruction_features *super,
+    const struct ofputil_table_instruction_features *sub)
+{
+    return (bitmap_is_superset(super->next, sub->next, 255)
+            && u32_is_superset(super->instructions, sub->instructions)
+            && ofputil_table_action_features_is_superset(&super->write,
+                                                         &sub->write)
+            && ofputil_table_action_features_is_superset(&super->apply,
+                                                         &sub->apply));
+}
+
+/* Returns true if 'super' is a superset of 'sub', false otherwise. */
+bool
+ofputil_table_features_are_superset(
+    const struct ofputil_table_features *super,
+    const struct ofputil_table_features *sub)
+{
+    return (be64_is_superset(super->metadata_match, sub->metadata_match)
+            && be64_is_superset(super->metadata_write, sub->metadata_write)
+            && super->max_entries >= sub->max_entries
+            && super->supports_eviction >= sub->supports_eviction
+            && super->supports_vacancy_events >= sub->supports_vacancy_events
+            && ofputil_table_instruction_features_is_superset(&super->nonmiss,
+                                                              &sub->nonmiss)
+            && ofputil_table_instruction_features_is_superset(&super->miss,
+                                                              &sub->miss)
+            && mf_bitmap_is_superset(&super->match, &sub->match)
+            && mf_bitmap_is_superset(&super->mask, &sub->mask)
+            && mf_bitmap_is_superset(&super->wildcard, &sub->wildcard));
+}
 
 /* Table stats. */
 
diff --git a/lib/vconn.c b/lib/vconn.c
index 4d5f308d8836..85fe0249af42 100644
--- a/lib/vconn.c
+++ b/lib/vconn.c
@@ -934,6 +934,46 @@  vconn_transact_multiple_noreply(struct vconn *vconn, struct ovs_list *requests,
     return 0;
 }
 
+/* Sends 'requests' (which should be a multipart request) on 'vconn' and waits
+ * for the replies, which are put into 'replies'.  Returns 0 if successful,
+ * otherwise an errno value. */
+int
+vconn_transact_multipart(struct vconn *vconn,
+                         struct ovs_list *requests,
+                         struct ovs_list *replies)
+{
+    struct ofpbuf *rq = ofpbuf_from_list(ovs_list_front(requests));
+    ovs_be32 send_xid = ((struct ofp_header *) rq->data)->xid;
+
+    ovs_list_init(replies);
+
+    /* Send all the requests. */
+    struct ofpbuf *b, *next;
+    LIST_FOR_EACH_SAFE (b, next, list_node, requests) {
+        ovs_list_remove(&b->list_node);
+        int error = vconn_send_block(vconn, b);
+        if (error) {
+            ofpbuf_delete(b);
+        }
+    }
+
+    /* Receive all the replies. */
+    bool more;
+    do {
+        struct ofpbuf *reply;
+        int error = vconn_recv_xid__(vconn, send_xid, &reply, NULL);
+        if (error) {
+            ofpbuf_list_delete(replies);
+            return error;
+        }
+
+        ovs_list_push_back(replies, &reply->list_node);
+        more = ofpmsg_is_stat_reply(reply->data) && ofpmp_more(reply->data);
+    } while (more);
+
+    return 0;
+}
+
 static int
 recv_flow_stats_reply(struct vconn *vconn, ovs_be32 send_xid,
                       struct ofpbuf **replyp,
diff --git a/ofproto/ofproto-dpif.c b/ofproto/ofproto-dpif.c
index 0a0c69a7aea0..280739d26856 100644
--- a/ofproto/ofproto-dpif.c
+++ b/ofproto/ofproto-dpif.c
@@ -6040,6 +6040,7 @@  const struct ofproto_class ofproto_dpif_class = {
     type_get_memory_usage,
     flush,
     query_tables,
+    NULL,                       /* modify_tables */
     set_tables_version,
     port_alloc,
     port_construct,
diff --git a/ofproto/ofproto-provider.h b/ofproto/ofproto-provider.h
index 2b77b8993ada..3a83d0407978 100644
--- a/ofproto/ofproto-provider.h
+++ b/ofproto/ofproto-provider.h
@@ -219,6 +219,7 @@  struct oftable {
     enum oftable_flags flags;
     struct classifier cls;      /* Contains "struct rule"s. */
     char *name;                 /* Table name exposed via OpenFlow, or NULL. */
+    int name_level;             /* 0=name unset, 1=via OF, 2=via OVSDB. */
 
     /* Maximum number of flows or UINT_MAX if there is no limit besides any
      * limit imposed by resource limitations. */
@@ -932,6 +933,24 @@  struct ofproto_class {
                          struct ofputil_table_features *features,
                          struct ofputil_table_stats *stats);
 
+    /* Helper for the OFPT_TABLE_FEATURES request.
+     *
+     * A controller is requesting that the table features be updated from 'old'
+     * to 'new', where 'old' is the features currently in use as previously
+     * initialized by 'query_tables'.
+     *
+     * If this function is nonnull, then it should either update the table
+     * features or return an OpenFlow error.  The update should be
+     * all-or-nothing.
+     *
+     * If this function is null, then only updates that eliminate table
+     * features will be allowed.  Such updates have no actual effect.  This
+     * implementation is acceptable because OpenFlow says that a table's
+     * features may be a superset of those requested. */
+    enum ofperr (*modify_tables)(struct ofproto *ofproto,
+                                 const struct ofputil_table_features *old,
+                                 const struct ofputil_table_features *new);
+
     /* Sets the current tables version the provider should use for classifier
      * lookups.  This must be called with a new version number after each set
      * of flow table changes has been completed, so that datapath revalidation
diff --git a/ofproto/ofproto.c b/ofproto/ofproto.c
index c9d73c10c0ae..d65a3fea1559 100644
--- a/ofproto/ofproto.c
+++ b/ofproto/ofproto.c
@@ -85,7 +85,9 @@  const enum mf_field_id default_prefix_fields[2] =
 static void oftable_init(struct oftable *);
 static void oftable_destroy(struct oftable *);
 
-static void oftable_set_name(struct oftable *, const char *name);
+static void oftable_set_name(struct oftable *, const char *name, int level);
+static bool oftable_may_set_name(const struct oftable *,
+                                 const char *name, int level);
 
 static enum ofperr evict_rules_from_table(struct oftable *)
     OVS_REQUIRES(ofproto_mutex);
@@ -1476,7 +1478,7 @@  ofproto_configure_table(struct ofproto *ofproto, int table_id,
     ovs_assert(table_id >= 0 && table_id < ofproto->n_tables);
     table = &ofproto->tables[table_id];
 
-    oftable_set_name(table, s->name);
+    oftable_set_name(table, s->name, 2);
 
     if (table->flags & OFTABLE_READONLY) {
         return;
@@ -3234,6 +3236,8 @@  query_tables(struct ofproto *ofproto,
         atomic_read_relaxed(&ofproto->tables[i].miss_config, &f->miss_config);
         f->max_entries = 1000000;
 
+        f->any_properties = true;
+
         bool more_tables = false;
         for (int j = i + 1; j < ofproto->n_tables; j++) {
             if (!(ofproto->tables[j].flags & OFTABLE_HIDDEN)) {
@@ -3765,33 +3769,238 @@  handle_table_stats_request(struct ofconn *ofconn,
     return 0;
 }
 
-static enum ofperr
-handle_table_features_request(struct ofconn *ofconn,
-                              const struct ofp_header *request)
+/* OpenFlow 1.5.1 section 7.3.5.18.1 "Table Features request and reply"
+ * says:
+ *
+ *    If a table feature included in the request has an empty list of
+ *    properties, the list of properties for that flow table is unchanged and
+ *    only the other features of that flow table are updated.
+ *
+ * This function copies the "list of properties" from '*src' to '*dst'. */
+static void
+copy_properties(struct ofputil_table_features *dst,
+                const struct ofputil_table_features *src)
+{
+    dst->any_properties = src->any_properties;
+    if (src->any_properties) {
+        dst->nonmiss = src->nonmiss;
+        dst->miss = src->miss;
+        dst->match = src->match;
+        dst->mask = src->mask;
+        dst->wildcard = src->wildcard;
+    }
+
+}
+
+/* Attempts to change the table features of the ofproto backing 'ofconn' to
+ * those specified in the table features request in 'msgs', given that the
+ * features are currently those in 'old'.
+ *
+ * Returns 0 if successful, an OpenFlow error if the caller should send an
+ * error message for the request as a whole, or -1 if the function already sent
+ * an error message for some message in 'msgs'. */
+static int
+handle_table_features_change(struct ofconn *ofconn,
+                             const struct ovs_list *msgs,
+                             const struct ofputil_table_features old[])
 {
     struct ofproto *ofproto = ofconn_get_ofproto(ofconn);
-    struct ofpbuf msg = ofpbuf_const_initializer(request,
-                                                 ntohs(request->length));
-    ofpraw_pull_assert(&msg);
-    if (msg.size || ofpmp_more(request)) {
+
+    enum ofp15_table_features_command command = OFPTFC15_REPLACE;
+    struct ofputil_table_features new[255];
+
+    unsigned long int seen[BITMAP_N_LONGS(255)];
+    memset(seen, 0, sizeof seen);
+
+    struct ofpbuf *msg;
+    int n = 0;
+    LIST_FOR_EACH (msg, list_node, msgs) {
+        for (;;) {
+            struct ofputil_table_features tf;
+            int retval = ofputil_decode_table_features(msg, &tf, NULL);
+            if (retval == EOF) {
+                break;
+            } else if (retval) {
+                ofconn_send_error(ofconn, msg->header, retval);
+                return -1;
+            }
+
+            /* Get command from first request. */
+            if (!n) {
+                command = tf.command;
+            }
+            n++;
+
+            /* Avoid duplicate tables. */
+            if (bitmap_is_set(seen, tf.table_id)) {
+                VLOG_INFO_RL(&rl, "duplicate table %"PRIu8, tf.table_id);
+                ofconn_send_error(ofconn, msg->header,
+                                  OFPERR_NXTFFC_DUP_TABLE);
+                return -1;
+            }
+            bitmap_set1(seen, tf.table_id);
+
+            /* Save table. */
+            new[tf.table_id] = tf;
+        }
+    }
+
+    if (!n) {
+        return 0;
+    }
+
+    for (size_t i = 0; i < ofproto->n_tables; i++) {
+        if (ofproto->tables[i].flags & OFTABLE_HIDDEN) {
+            if (bitmap_is_set(seen, i)) {
+                VLOG_INFO_RL(&rl, "can't modify hidden table %"PRIuSIZE, i);
+                return OFPERR_OFPTFFC_EPERM;
+            }
+
+            new[i] = old[i];
+            bitmap_set1(seen, i);
+        }
+    }
+
+    switch (command) {
+    case OFPTFC15_REPLACE:
+        break;
+
+    case OFPTFC15_MODIFY:
+        for (size_t i = 0; i < ofproto->n_tables; i++) {
+            if (!bitmap_is_set(seen, i)) {
+                new[i] = old[i];
+                bitmap_set1(seen, i);
+            } else if (!new[i].any_properties) {
+                copy_properties(&new[i], &old[i]);
+            }
+        }
+        break;
+
+    case OFPTFC15_ENABLE:
+    case OFPTFC15_DISABLE:
+        /* It really isn't clear what these commands are supposed to do in an
+         * Open vSwitch context.  OVS doesn't have a concept of tables that
+         * exist but are not in the pipeline, and OVS table ids are always
+         * sequential from 0. */
         return OFPERR_OFPTFFC_EPERM;
     }
 
+    /* Make sure that the new number of tables is the same as the old number,
+     * because we don't support changing the number of tables or disabling
+     * tables. */
+    int n_tables = bitmap_scan(seen, 0, 0, 255);
+    bool skipped_tables = bitmap_scan(seen, 1, n_tables, 255) != 255;
+    if (n_tables != ofproto->n_tables || skipped_tables) {
+        if (skipped_tables) {
+            VLOG_INFO_RL(&rl, "can't disable table %d", n_tables);
+        } else {
+            VLOG_INFO_RL(&rl, "can't change number of tables from %d to %d",
+                         ofproto->n_tables, n_tables);
+        }
+        return (n_tables > ofproto->n_tables
+                ? OFPERR_OFPTFFC_TOO_MANY
+                : OFPERR_OFPTFFC_EPERM);
+    }
+
+    /* OpenFlow 1.5.1 section 7.3.5.18.1 "Table Features request and reply"
+     * says:
+     *
+     *     "All fields in ofp_table_features may be requested to be changed by
+     *     the controller with the exception of the max_entries field, this is
+     *     read only and returned by the switch."
+     *
+     * so forbid the controller from attempting to change it.
+     *
+     * (This seems like a particularly arbitrary prohibition since OVS could
+     * easily implement such a feature.  Whatever.) */
+    for (size_t i = 0; i < n_tables; i++) {
+        if (old[i].max_entries != new[i].max_entries) {
+            VLOG_INFO_RL(&rl, "can't change max_entries");
+            return OFPERR_OFPTFFC_EPERM;
+        }
+    }
+
+    /* Check that we can set table names. */
+    for (size_t i = 0; i < n_tables; i++) {
+        if (!oftable_may_set_name(&ofproto->tables[i], new[i].name, 1)) {
+            const char *name = ofproto->tables[i].name;
+            VLOG_INFO_RL(&rl, "can't change name of table %"PRIuSIZE" "
+                         "to %s because it is already set to %s via OVSDB",
+                         i, new[i].name, name ? name : "\"\"");
+            return OFPERR_OFPTFFC_EPERM;
+        }
+    }
+
+    /* Ask the provider to update its features.
+     *
+     * If the provider can't update features, just make sure that the
+     * controller isn't asking to enable new features.  OpenFlow says it's OK
+     * if a superset of the requested features are actually enabled. */
+    if (ofproto->ofproto_class->modify_tables) {
+        enum ofperr error = ofproto->ofproto_class->modify_tables(ofproto,
+                                                                  old, new);
+        if (error) {
+            VLOG_INFO_RL(&rl, "can't change table features");
+            return error;
+        }
+    } else {
+        for (size_t i = 0; i < n_tables; i++) {
+            if (!ofputil_table_features_are_superset(&old[i], &new[i])) {
+                VLOG_INFO_RL(&rl, "can't increase table features "
+                             "for table %"PRIuSIZE, i);
+                return OFPERR_OFPTFFC_EPERM;
+            }
+        }
+    }
+
+    /* Update table names. */
+    for (size_t i = 0; i < n_tables; i++) {
+        oftable_set_name(&ofproto->tables[i], new[i].name, 1);
+    }
+
+    return 0;
+}
+
+static void
+handle_table_features_request(struct ofconn *ofconn,
+                              const struct ovs_list *msgs)
+{
+    struct ofproto *ofproto = ofconn_get_ofproto(ofconn);
+
+    struct ofpbuf *msg = ofpbuf_from_list(ovs_list_back(msgs));
+    const struct ofp_header *request = msg->data;
+    ofpraw_pull_assert(msg);
+
+    /* Update the table features configuration, if requested. */
     struct ofputil_table_features *features;
     query_tables(ofproto, &features, NULL);
+    if (!ovs_list_is_singleton(msgs) || msg->size) {
+        int retval = handle_table_features_change(ofconn, msgs, features);
+        if (retval) {
+            if (retval < 0) {
+                /* handle_table_features_change() already sent an error. */
+            } else {
+                ofconn_send_error(ofconn, request, retval);
+            }
+            return;
+        }
+
+        /* Features may have changed, re-query. */
+        free(features);
+        query_tables(ofproto, &features, NULL);
+    }
 
+    /* Reply the controller with the table configuration. */
     struct ovs_list replies;
     ofpmp_init(&replies, request);
     for (size_t i = 0; i < ofproto->n_tables; i++) {
         if (!(ofproto->tables[i].flags & OFTABLE_HIDDEN)) {
-            ofputil_append_table_features_reply(&features[i], &replies);
+            ofputil_append_table_features(&features[i], NULL, &replies);
         }
     }
     ofconn_send_replies(ofconn, &replies);
 
     free(features);
-
-    return 0;
 }
 
 /* Returns the vacancy of 'oftable', a number that ranges from 0 (if the table
@@ -8174,7 +8383,7 @@  handle_single_part_openflow(struct ofconn *ofconn, const struct ofp_header *oh,
         return handle_table_stats_request(ofconn, oh);
 
     case OFPTYPE_TABLE_FEATURES_STATS_REQUEST:
-        return handle_table_features_request(ofconn, oh);
+        OVS_NOT_REACHED();
 
     case OFPTYPE_TABLE_DESC_REQUEST:
         return handle_table_desc_request(ofconn, oh);
@@ -8285,7 +8494,9 @@  handle_openflow(struct ofconn *ofconn, const struct ovs_list *msgs)
     enum ofptype type;
     enum ofperr error = ofptype_decode(&type, msg->data);
     if (!error) {
-        if (!ovs_list_is_short(msgs)) {
+        if (type == OFPTYPE_TABLE_FEATURES_STATS_REQUEST) {
+            handle_table_features_request(ofconn, msgs);
+        } else if (!ovs_list_is_short(msgs)) {
             error = OFPERR_OFPBRC_BAD_STAT;
         } else {
             error = handle_single_part_openflow(ofconn, msg->data, type);
@@ -8610,26 +8821,49 @@  oftable_destroy(struct oftable *table)
     free(table->name);
 }
 
-/* Changes the name of 'table' to 'name'.  If 'name' is NULL or the empty
- * string, then 'table' will use its default name.
+/* Changes the name of 'table' to 'name'.  Null or empty string 'name' unsets
+ * the name.
+ *
+ * 'level' should be 1 if the name is being set via OpenFlow, or 2 if the name
+ * is being set via OVSDB.  Higher levels get precedence.
  *
  * This only affects the name exposed for a table exposed through the OpenFlow
  * OFPST_TABLE (as printed by "ovs-ofctl dump-tables"). */
 static void
-oftable_set_name(struct oftable *table, const char *name)
-{
-    if (name && name[0]) {
-        int len = strnlen(name, OFP_MAX_TABLE_NAME_LEN);
-        if (!table->name || strncmp(name, table->name, len)) {
+oftable_set_name(struct oftable *table, const char *name, int level)
+{
+    int len = name ? strnlen(name, OFP_MAX_TABLE_NAME_LEN) : 0;
+    if (level >= table->name_level) {
+        if (name) {
+            if (name[0]) {
+                if (!table->name || strncmp(name, table->name, len)) {
+                    free(table->name);
+                    table->name = xmemdup0(name, len);
+                }
+            } else {
+                free(table->name);
+                table->name = NULL;
+            }
+            table->name_level = level;
+        } else if (table->name_level == level) {
             free(table->name);
-            table->name = xmemdup0(name, len);
+            table->name = NULL;
+            table->name_level = 0;
         }
-    } else {
-        free(table->name);
-        table->name = NULL;
     }
 }
 
+/* Returns true if oftable_set_name(table, name, level) would be effective,
+ * false otherwise.  */
+static bool
+oftable_may_set_name(const struct oftable *table, const char *name, int level)
+{
+    return (level >= table->name_level
+            || !name
+            || !strncmp(name, table->name,
+                        strnlen(name, OFP_MAX_TABLE_NAME_LEN)));
+}
+
 /* oftables support a choice of two policies when adding a rule would cause the
  * number of flows in the table to exceed the configured maximum number: either
  * they can refuse to add the new flow or they can evict some existing flow.
diff --git a/tests/ofproto.at b/tests/ofproto.at
index 2291bfb93225..6fbbd6c32859 100644
--- a/tests/ofproto.at
+++ b/tests/ofproto.at
@@ -2668,6 +2668,19 @@  AT_CLEANUP
 AT_SETUP([ofproto - flow table names])
 OVS_VSWITCHD_START
 add_of_ports br0 1 2
+
+# Set a table name via OpenFlow 1.3 and one via OpenFlow 1.5.
+AT_CHECK([ovs-ofctl -O OpenFlow13 mod-table br0 0 name:xyzzy])
+AT_CHECK([ovs-ofctl -O OpenFlow15 mod-table br0 1 name:quux])
+AT_CHECK([ovs-ofctl -O OpenFlow15 dump-table-features br0 |grep '^  table'],
+  [0], [dnl
+  table 0 ("xyzzy"):
+  table 1 ("quux"): ditto
+  tables 2...252: ditto
+  table 253:
+])
+
+# Set some table names via OVSDB.
 AT_CHECK(
   [ovs-vsctl \
      -- --id=@t0 create Flow_Table name=zero \
@@ -2679,6 +2692,16 @@  AT_CHECK(
 <1>
 <2>
 ])
+AT_CHECK([ovs-ofctl -O OpenFlow15 dump-table-features br0 |grep '^  table'],
+  [0], [dnl
+  table 0 ("zero"):
+  table 1 ("one"): ditto
+  table 2 ("two"): ditto
+  tables 3...252: ditto
+  table 253:
+])
+
+# Check that flow table parsing and dumping uses the names.
 AT_DATA([flows.txt], [dnl
 table=zero in_port=p2 actions=p1,resubmit(,one)
 table=one,in_port=p1,ip,actions=ct(table=two)
@@ -2695,6 +2718,73 @@  AT_CHECK([ovs-ofctl --no-names --no-stats dump-flows br0], [0], [dnl
  table=1, ip,in_port=1 actions=ct(table=2)
  table=1, arp,in_port=1 actions=resubmit(,2)
 ])
+
+# Setting the same table names via OpenFlow 1.3 or OpenFlow 1.5 is a no-op.
+AT_CHECK([ovs-ofctl -O OpenFlow13 mod-table br0 0 name:zero])
+AT_CHECK([ovs-ofctl -O OpenFlow15 mod-table br0 1 name:one])
+AT_CHECK([ovs-ofctl -O OpenFlow15 dump-table-features br0 |grep '^  table'],
+  [0], [dnl
+  table 0 ("zero"):
+  table 1 ("one"): ditto
+  table 2 ("two"): ditto
+  tables 3...252: ditto
+  table 253:
+])
+
+# Setting different tables names via OpenFlow 1.3 or OpenFlow 1.5 yield errors.
+AT_CHECK([ovs-ofctl -O OpenFlow13 mod-table br0 0 name:xyzzy], 1, [], [stderr])
+AT_CHECK([head -1 stderr], [0], [OFPT_ERROR (OF1.3) (xid=0x5): OFPTFFC_EPERM
+])
+AT_CHECK([ovs-ofctl -O OpenFlow15 mod-table br0 1 name:quux], 1, [], [stderr])
+AT_CHECK([head -1 stderr], [0], [OFPT_ERROR (OF1.5) (xid=0x5): OFPTFFC_EPERM
+])
+
+# But we can still set table names for those not set via OVSDB.
+AT_CHECK([ovs-ofctl -O OpenFlow13 mod-table br0 3 name:three])
+AT_CHECK([ovs-ofctl -O OpenFlow15 dump-table-features br0 |grep '^  table'],
+  [0], [dnl
+  table 0 ("zero"):
+  table 1 ("one"): ditto
+  table 2 ("two"): ditto
+  table 3 ("three"): ditto
+  tables 4...252: ditto
+  table 253:
+])
+
+# Unsetting names via OVSDB then setting them via OpenFlow works too.
+AT_CHECK([ovs-vsctl remove bridge br0 Flow_Table 2])
+AT_CHECK([ovs-ofctl -O OpenFlow15 dump-table-features br0 |grep '^  table'],
+  [0], [dnl
+  table 0 ("zero"):
+  table 1 ("one"): ditto
+  table 2: ditto
+  table 3 ("three"): ditto
+  tables 4...252: ditto
+  table 253:
+])
+AT_CHECK([ovs-ofctl -O OpenFlow13 mod-table br0 2 name:foobar])
+AT_CHECK([ovs-ofctl -O OpenFlow15 dump-table-features br0 |grep '^  table'],
+  [0], [dnl
+  table 0 ("zero"):
+  table 1 ("one"): ditto
+  table 2 ("foobar"): ditto
+  table 3 ("three"): ditto
+  tables 4...252: ditto
+  table 253:
+])
+
+# We can clear names via OpenFlow, at least if they were set that way.
+AT_CHECK([ovs-ofctl -O OpenFlow13 mod-table br0 2 name:])
+AT_CHECK([ovs-ofctl -O OpenFlow15 dump-table-features br0 |grep '^  table'],
+  [0], [dnl
+  table 0 ("zero"):
+  table 1 ("one"): ditto
+  table 2: ditto
+  table 3 ("three"): ditto
+  tables 4...252: ditto
+  table 253:
+])
+
 OVS_VSWITCHD_STOP
 AT_CLEANUP
 
diff --git a/utilities/ovs-ofctl.8.in b/utilities/ovs-ofctl.8.in
index 4850f4a25504..968e1ec347af 100644
--- a/utilities/ovs-ofctl.8.in
+++ b/utilities/ovs-ofctl.8.in
@@ -88,6 +88,17 @@  Send to controller.  (This is how an OpenFlow 1.0 switch always
 handles packets that do not match any flow in the last table.)
 .RE
 .IP
+In OpenFlow 1.3 and later (which must be enabled with the \fB\-O\fR
+option) and Open vSwitch 2.11 and later only, \fBmod\-table\fR can
+change the name of a table:
+.RS
+.IP \fBname:\fInew-name\fR
+Changes the name of the table to \fInew-name\fR.  (This will be
+ineffective if the name is set via the \fBname\fR column in the
+\fBFlow_Table\fR table in the \fBOpen_vSwitch\fR database as described
+in \fBovs\-vswitchd.conf.db\fR(5).)
+.RE
+.IP
 In OpenFlow 1.4 and later (which must be enabled with the \fB\-O\fR
 option) only, \fBmod\-table\fR configures the behavior when a
 controller attempts to add a flow to a flow table that is full.  The
diff --git a/utilities/ovs-ofctl.c b/utilities/ovs-ofctl.c
index aa9a1291e60a..82b2b42a92cd 100644
--- a/utilities/ovs-ofctl.c
+++ b/utilities/ovs-ofctl.c
@@ -913,9 +913,9 @@  ofctl_dump_table_features(struct ovs_cmdl_context *ctx)
                 done = !ofpmp_more(reply->data);
                 for (;;) {
                     struct ofputil_table_features tf;
-                    int retval;
-
-                    retval = ofputil_decode_table_features(reply, &tf, true);
+                    struct ofpbuf raw_properties;
+                    int retval = ofputil_decode_table_features(
+                        reply, &tf, &raw_properties);
                     if (retval) {
                         if (retval != EOF) {
                             ovs_fatal(0, "decode error: %s",
@@ -1207,6 +1207,7 @@  struct table_iterator {
     bool more;
 
     struct ofputil_table_features features;
+    struct ofpbuf raw_properties;
 };
 
 /* Initializes 'ti' to prepare for iterating through all of the tables on the
@@ -1248,7 +1249,7 @@  table_iterator_next(struct table_iterator *ti)
                 ovs_assert(ti->variant == TI_FEATURES);
                 retval = ofputil_decode_table_features(ti->reply,
                                                        &ti->features,
-                                                       true);
+                                                       &ti->raw_properties);
             }
             if (!retval) {
                 return &ti->features;
@@ -2580,16 +2581,95 @@  fetch_table_desc(struct vconn *vconn, struct ofputil_table_mod *tm,
     }
 }
 
+static void
+change_table_name(struct vconn *vconn, uint8_t table_id, const char *new_name)
+{
+    /* Get all tables' features and properties. */
+    struct table {
+        struct ofputil_table_features tf;
+        struct ofpbuf *raw_properties;
+    } *tables[256];
+    memset(tables, 0, sizeof tables);
+
+    struct table_iterator ti;
+    table_iterator_init(&ti, vconn);
+    while (table_iterator_next(&ti)) {
+        struct table *t = tables[ti.features.table_id] = xmalloc(sizeof *t);
+        t->tf = ti.features;
+        t->raw_properties = ofpbuf_clone(&ti.raw_properties);
+    }
+    table_iterator_destroy(&ti);
+
+    /* Change the name for table 'table_id'. */
+    struct table *t = tables[table_id];
+    if (!t) {
+        ovs_fatal(0, "switch does not have table %"PRIu8, table_id);
+    }
+    ovs_strlcpy(t->tf.name, new_name, OFP_MAX_TABLE_NAME_LEN);
+
+    /* Compose the transaction. */
+    enum ofp_version version = vconn_get_version(vconn);
+    struct ovs_list requests = OVS_LIST_INITIALIZER(&requests);
+    struct ofpbuf *tfr = ofputil_encode_table_features_request(version);
+    ovs_list_push_back(&requests, &tfr->list_node);
+    if (version >= OFP15_VERSION) {
+        /* For OpenFlow 1.5, we can use a single OFPTFC15_MODIFY without any
+         * properties. */
+        t->tf.command = OFPTFC15_MODIFY;
+        t->tf.any_properties = false;
+        ofputil_append_table_features(&t->tf, NULL, &requests);
+    } else {
+        /* For OpenFlow 1.3 and 1.4, we have to regurgitate all of the tables
+         * and their properties. */
+        for (size_t i = 0; i < 256; i++) {
+            if (tables[i]) {
+                ofputil_append_table_features(&tables[i]->tf,
+                                              tables[i]->raw_properties,
+                                              &requests);
+            }
+        }
+    }
+
+    /* Transact.
+     *
+     * The reply repeats the entire new configuration of the tables, so we
+     * don't bother printing it unless there's an error. */
+    struct ovs_list replies;
+    struct ofpbuf *reply;
+    vconn_transact_multipart(vconn, &requests, &replies);
+    LIST_FOR_EACH (reply, list_node, &replies) {
+        enum ofptype type;
+        enum ofperr error = ofptype_decode(&type, reply->data);
+        if (error) {
+            ovs_fatal(0, "decode error: %s", ofperr_get_name(error));
+        } else if (type == OFPTYPE_ERROR) {
+            ofp_print(stderr, reply->data, reply->size, NULL, NULL,
+                      verbosity + 1);
+            exit(1);
+        }
+    }
+    ofpbuf_list_delete(&replies);
+
+    /* Clean up. */
+    for (size_t i = 0; i < ARRAY_SIZE(tables); i++) {
+        if (tables[i]) {
+            ofpbuf_delete(tables[i]->raw_properties);
+            free(tables[i]);
+        }
+    }
+}
+
 static void
 ofctl_mod_table(struct ovs_cmdl_context *ctx)
 {
     uint32_t usable_versions;
     struct ofputil_table_mod tm;
+    const char *name;
     struct vconn *vconn;
     char *error;
     int i;
 
-    error = parse_ofp_table_mod(&tm, ctx->argv[2], ctx->argv[3],
+    error = parse_ofp_table_mod(&tm, &name, ctx->argv[2], ctx->argv[3],
                                 tables_to_accept(ctx->argv[1]),
                                 &usable_versions);
     if (error) {
@@ -2607,27 +2687,32 @@  ofctl_mod_table(struct ovs_cmdl_context *ctx)
     mask_allowed_ofp_versions(usable_versions);
     enum ofputil_protocol protocol = open_vconn(ctx->argv[1], &vconn);
 
-    /* For OpenFlow 1.4+, ovs-ofctl mod-table should not affect table-config
-     * properties that the user didn't ask to change, so it is necessary to
-     * restore the current configuration of table-config parameters using
-     * OFPMP14_TABLE_DESC request. */
-    if ((allowed_versions & (1u << OFP14_VERSION)) ||
-        (allowed_versions & (1u << OFP15_VERSION))) {
-        struct ofputil_table_desc td;
-
-        if (tm.table_id == OFPTT_ALL) {
-            for (i = 0; i < OFPTT_MAX; i++) {
-                tm.table_id = i;
+    if (name) {
+        change_table_name(vconn, tm.table_id, name);
+    } else {
+        /* For OpenFlow 1.4+, ovs-ofctl mod-table should not affect
+         * table-config properties that the user didn't ask to change, so it is
+         * necessary to restore the current configuration of table-config
+         * parameters using OFPMP14_TABLE_DESC request. */
+        if ((allowed_versions & (1u << OFP14_VERSION)) ||
+            (allowed_versions & (1u << OFP15_VERSION))) {
+            struct ofputil_table_desc td;
+
+            if (tm.table_id == OFPTT_ALL) {
+                for (i = 0; i < OFPTT_MAX; i++) {
+                    tm.table_id = i;
+                    fetch_table_desc(vconn, &tm, &td);
+                    transact_noreply(vconn,
+                                     ofputil_encode_table_mod(&tm, protocol));
+                }
+            } else {
                 fetch_table_desc(vconn, &tm, &td);
-                transact_noreply(vconn,
-                                 ofputil_encode_table_mod(&tm, protocol));
+                transact_noreply(vconn, ofputil_encode_table_mod(&tm,
+                                                                 protocol));
             }
         } else {
-            fetch_table_desc(vconn, &tm, &td);
             transact_noreply(vconn, ofputil_encode_table_mod(&tm, protocol));
         }
-    } else {
-        transact_noreply(vconn, ofputil_encode_table_mod(&tm, protocol));
     }
     vconn_close(vconn);
 }