diff mbox series

[ovs-dev,27/41] dpif-offload: Add flow operations to dpif-offload-tc.

Message ID 06698787215d0b91c702a700e57241cf1ff096a2.1762950453.git.echaudro@redhat.com
State Changes Requested
Headers show
Series Architectural refactoring of hardware offload infrastructure | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_Build_and_Test success github build: passed
ovsrobot/github-robot-_Build_and_Test success github build: passed

Commit Message

Eelco Chaudron Nov. 12, 2025, 3:35 p.m. UTC
Call the netdev-offload flow put/del/get APIs through the
dpif-offload-tc layer, and remove them from the netdev-offload layer.

Note that we still need to remove the dependency on the
netdev-offload infrastructure for initializing and locating netdevs.
This will be addressed in a separate patch.

Signed-off-by: Eelco Chaudron <echaudro@redhat.com>
---
 lib/dpif-netlink.c      |   2 +-
 lib/dpif-offload-tc.c   | 241 ++++++++++++++++++++++++++++++++++++++++
 lib/netdev-offload-tc.c |  37 +++---
 lib/netdev-offload-tc.h |   9 ++
 lib/netdev-offload.h    |   1 +
 5 files changed, 269 insertions(+), 21 deletions(-)

Comments

0-day Robot Nov. 14, 2025, 2:17 p.m. UTC | #1
git sha: 06698787215d0b91c702a700e57241cf1ff096a2
Author: Eelco Chaudron <echaudro@redhat.com>
Subject: dpif-offload: Add flow operations to dpif-offload-tc.

This commit moves flow put/del/get APIs from netdev-offload layer to
dpif-offload-tc layer, removing the dependency on netdev-offload
infrastructure while still maintaining it for netdev initialization.

> diff --git a/lib/dpif-netlink.c b/lib/dpif-netlink.c
> index e161930be..165f76ace 100644
> --- a/lib/dpif-netlink.c
> +++ b/lib/dpif-netlink.c
> @@ -2216,7 +2216,7 @@ out:
>              * Change flags to create the flow in kernel */
>             put->flags &= ~DPIF_FP_MODIFY;
>             put->flags |= DPIF_FP_CREATE;
> -        } else if (del_err != ENOENT) {
> +        } else if (del_err != ENOENT && del_err != EOPNOTSUPP) {
>             VLOG_ERR_RL(&rl, "failed to delete offloaded flow: %s",
>                         ovs_strerror(del_err));
>             /* stop proccesing the flow in kernel */

This error handling change affects when error messages are logged. Does
this code now silently ignore EOPNOTSUPP deletion failures that should
be reported?

> diff --git a/lib/dpif-offload-tc.c b/lib/dpif-offload-tc.c
> index c343cd9c1..d8fae7ed6 100644
> --- a/lib/dpif-offload-tc.c
> +++ b/lib/dpif-offload-tc.c

[ ... ]

> +static int
> +dpif_offload_tc_parse_flow_put(struct dpif_offload_tc *offload_tc,
> +                               struct dpif *dpif, struct dpif_flow_put *put)
> +{
> +    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
> +    struct dpif_offload_port_mgr_port *port;
> +    const struct nlattr *nla;
> +    struct offload_info info;
> +    struct match match;
> +    odp_port_t in_port;
> +    size_t left;
> +    int err;
> +
> +    info.tc_modify_flow_deleted = false;
> +    info.tc_modify_flow = false;

Is it safe to leave other fields in info uninitialized? Does this code
introduce potential use of uninitialized fields like flow_mark or
orig_in_port?

> +
> +    if (put->flags & DPIF_FP_PROBE) {
> +        return EOPNOTSUPP;
> +    }
> +
> +    err = parse_key_and_mask_to_match(put->key, put->key_len, put->mask,
> +                                      put->mask_len, &match);
> +    if (err) {
> +        return err;
> +    }
> +
> +    in_port = match.flow.in_port.odp_port;
> +    port = dpif_offload_port_mgr_find_by_odp_port(offload_tc->port_mgr,
> +                                                  in_port);
> +    if (!port) {
> +        return EOPNOTSUPP;
> +    }
> +
> +    /* Check the output port for a tunnel. */
> +    NL_ATTR_FOR_EACH (nla, left, put->actions, put->actions_len) {
> +        if (nl_attr_type(nla) == OVS_ACTION_ATTR_OUTPUT) {
> +            struct dpif_offload_port_mgr_port *mgr_port;
> +            odp_port_t out_port;
> +
> +            out_port = nl_attr_get_odp_port(nla);
> +            mgr_port = dpif_offload_port_mgr_find_by_odp_port(
> +                offload_tc->port_mgr, out_port);
> +
> +            if (!mgr_port) {
> +                err = EOPNOTSUPP;
> +                goto out;
> +            }
> +        }
> +    }

This loop only handles the first OUTPUT action it encounters due to the
goto on error. If there are multiple OUTPUT actions in the same flow,
does this code incorrectly fail on the second one even if both ports
exist?

[ ... ]

> +out:
> +    if (err && err != EEXIST && (put->flags & DPIF_FP_MODIFY)) {
> +        /* Modified rule can't be offloaded, try and delete from HW. */
> +        int del_err = 0;
> +
> +        if (!info.tc_modify_flow_deleted) {
> +            del_err = netdev_offload_tc_flow_del(put->ufid, put->stats);
> +        }
> +
> +        if (!del_err) {
> +            /* Delete from hw success, so old flow was offloaded.
> +             * Change flags to create the flow at the dpif level. */
> +            put->flags &= ~DPIF_FP_MODIFY;
> +            put->flags |= DPIF_FP_CREATE;
> +        } else if (del_err != ENOENT) {
> +            VLOG_ERR_RL(&rl, "failed to delete offloaded flow: %s",
> +                        ovs_strerror(del_err));
> +            /* Stop processing the flow in kernel. */
> +            err = 0;
> +        }
> +    }

The error handling logic here seems duplicated with dpif-netlink.c.
Does this create conflicting error handling paths when both layers try
to manage the same flow modification failures?

> +static int
> +dpif_offload_tc_parse_flow_get(struct dpif_offload_tc *offload_tc,
> +                               struct dpif_flow_get *get)
> +{
> +    struct dpif_offload_port_mgr_port *port;
> +    struct dpif_flow *dpif_flow = get->flow;
> +    struct odputil_keybuf maskbuf;
> +    struct odputil_keybuf keybuf;
> +    struct odputil_keybuf actbuf;
> +    struct ofpbuf key, mask, act;
> +    struct dpif_flow_stats stats;
> +    struct dpif_flow_attrs attrs;
> +    uint64_t act_buf[1024 / 8];
> +    struct nlattr *actions;
> +    struct match match;
> +    struct ofpbuf buf;
> +    int err = ENOENT;
> +
> +    ofpbuf_use_stack(&buf, &act_buf, sizeof act_buf);
> +
> +     DPIF_OFFLOAD_PORT_MGR_PORT_FOR_EACH (port, offload_tc->port_mgr) {
> +         if (!netdev_offload_tc_flow_get(port->netdev, &match, &actions,
> +                                         get->ufid, &stats, &attrs, &buf)) {
> +             err = 0;
> +             break;
> +         }
> +     }

This loop searches all ports to find the flow. Does this scale poorly
with large numbers of ports, and could it return incorrect results if
the same UFID exists on multiple ports?

[ ... ]

> diff --git a/lib/netdev-offload-tc.c b/lib/netdev-offload-tc.c
> index 0f9d97d6d..8c8c72a65 100644
> --- a/lib/netdev-offload-tc.c
> +++ b/lib/netdev-offload-tc.c

[ ... ]

> +int
> +netdev_offload_tc_flow_put(struct netdev *netdev, struct match *match,
> +                           struct nlattr *actions, size_t actions_len,
> +                           const ovs_u128 *ufid, struct offload_info *info,
> +                           struct dpif_flow_stats *stats)
>  {

[ ... ]

> @@ -2672,6 +2672,7 @@ netdev_tc_flow_put(struct netdev *netdev, struct match *match,
>     if (get_ufid_tc_mapping(ufid, &id) == 0) {
>         VLOG_DBG_RL(&rl, "updating old handle: %d prio: %d",
>                     id.handle, id.prio);
> +        info->tc_modify_flow = true;
>         info->tc_modify_flow_deleted = !del_filter_and_ufid_mapping(
>             &id, ufid, &adjust_stats);
>     }

With this change, tc_modify_flow is only set true inside this if block.
Can the flag remain false for legitimate flow modifications when
get_ufid_tc_mapping fails?

> +int
> +netdev_offload_tc_flow_del(const ovs_u128 *ufid,
> +                           struct dpif_flow_stats *stats)
>  {

Does this function signature change break compatibility since it no longer
takes a netdev parameter? Could this cause issues with flows that need
device-specific cleanup?

> @@ -3464,8 +3464,5 @@ dpif_offload_tc_meter_del(const struct dpif_offload *offload OVS_UNUSED,
> 
> const struct netdev_flow_api netdev_offload_tc = {
>    .type = "linux_tc",
> -   .flow_put = netdev_tc_flow_put,
> -   .flow_get = netdev_tc_flow_get,
> -   .flow_del = netdev_tc_flow_del,
>    .init_flow_api = netdev_tc_init_flow_api,
> };

Removing these function pointers from the netdev_flow_api structure
while keeping them as global functions seems to create an inconsistent
API. Does this break any existing callers that expect these operations
to be available through the netdev layer?
diff mbox series

Patch

diff --git a/lib/dpif-netlink.c b/lib/dpif-netlink.c
index e161930be..165f76ace 100644
--- a/lib/dpif-netlink.c
+++ b/lib/dpif-netlink.c
@@ -2216,7 +2216,7 @@  out:
              * Change flags to create the flow in kernel */
             put->flags &= ~DPIF_FP_MODIFY;
             put->flags |= DPIF_FP_CREATE;
-        } else if (del_err != ENOENT) {
+        } else if (del_err != ENOENT && del_err != EOPNOTSUPP) {
             VLOG_ERR_RL(&rl, "failed to delete offloaded flow: %s",
                         ovs_strerror(del_err));
             /* stop proccesing the flow in kernel */
diff --git a/lib/dpif-offload-tc.c b/lib/dpif-offload-tc.c
index c343cd9c1..d8fae7ed6 100644
--- a/lib/dpif-offload-tc.c
+++ b/lib/dpif-offload-tc.c
@@ -40,6 +40,7 @@  struct dpif_offload_tc {
 
     /* Configuration specific variables. */
     struct ovsthread_once once_enable; /* Track first-time enablement. */
+    bool recirc_id_shared;
 };
 
 /* tc's flow dump specific data structures. */
@@ -184,6 +185,11 @@  dpif_offload_tc_open(const struct dpif_offload_class *offload_class,
     offload_tc->port_mgr = dpif_offload_port_mgr_init();
     offload_tc->once_enable = (struct ovsthread_once) \
                               OVSTHREAD_ONCE_INITIALIZER;
+    offload_tc->recirc_id_shared = !!(dpif_get_features(dpif)
+                                      & OVS_DP_F_TC_RECIRC_SHARING);
+
+    VLOG_DBG("Datapath %s recirculation id sharing ",
+             offload_tc->recirc_id_shared ? "supports" : "does not support");
 
     dpif_offload_tc_meter_init();
 
@@ -587,6 +593,240 @@  dpif_offload_tc_flow_dump_thread_destroy(
     free(thread);
 }
 
+static int
+dpif_offload_tc_parse_flow_put(struct dpif_offload_tc *offload_tc,
+                               struct dpif *dpif, struct dpif_flow_put *put)
+{
+    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
+    struct dpif_offload_port_mgr_port *port;
+    const struct nlattr *nla;
+    struct offload_info info;
+    struct match match;
+    odp_port_t in_port;
+    size_t left;
+    int err;
+
+    info.tc_modify_flow_deleted = false;
+    info.tc_modify_flow = false;
+
+    if (put->flags & DPIF_FP_PROBE) {
+        return EOPNOTSUPP;
+    }
+
+    err = parse_key_and_mask_to_match(put->key, put->key_len, put->mask,
+                                      put->mask_len, &match);
+    if (err) {
+        return err;
+    }
+
+    in_port = match.flow.in_port.odp_port;
+    port = dpif_offload_port_mgr_find_by_odp_port(offload_tc->port_mgr,
+                                                  in_port);
+    if (!port) {
+        return EOPNOTSUPP;
+    }
+
+    /* Check the output port for a tunnel. */
+    NL_ATTR_FOR_EACH (nla, left, put->actions, put->actions_len) {
+        if (nl_attr_type(nla) == OVS_ACTION_ATTR_OUTPUT) {
+            struct dpif_offload_port_mgr_port *mgr_port;
+            odp_port_t out_port;
+
+            out_port = nl_attr_get_odp_port(nla);
+            mgr_port = dpif_offload_port_mgr_find_by_odp_port(
+                offload_tc->port_mgr, out_port);
+
+            if (!mgr_port) {
+                err = EOPNOTSUPP;
+                goto out;
+            }
+        }
+    }
+
+    info.recirc_id_shared_with_tc = offload_tc->recirc_id_shared;
+
+    err = netdev_offload_tc_flow_put(port->netdev, &match,
+                                     CONST_CAST(struct nlattr *, put->actions),
+                                     put->actions_len,
+                                     CONST_CAST(ovs_u128 *, put->ufid),
+                                     &info, put->stats);
+
+    if (!err) {
+        if (put->flags & DPIF_FP_MODIFY && !info.tc_modify_flow) {
+            struct dpif_op *opp;
+            struct dpif_op op;
+
+            op.type = DPIF_OP_FLOW_DEL;
+            op.flow_del.key = put->key;
+            op.flow_del.key_len = put->key_len;
+            op.flow_del.ufid = put->ufid;
+            op.flow_del.pmd_id = put->pmd_id;
+            op.flow_del.stats = NULL;
+            op.flow_del.terse = false;
+
+            opp = &op;
+            dpif_operate(dpif, &opp, 1, DPIF_OFFLOAD_NEVER);
+        }
+
+        VLOG_DBG("added flow");
+    } else if (err != EEXIST) {
+        struct netdev *oor_netdev = NULL;
+        enum vlog_level level;
+
+        if (err == ENOSPC
+            && dpif_offload_is_offload_rebalance_policy_enabled()) {
+            /*
+             * We need to set OOR on the input netdev (i.e, 'dev') for the
+             * flow.  But if the flow has a tunnel attribute (i.e, decap
+             * action, with a virtual device like a VxLAN interface as its
+             * in-port), then lookup and set OOR on the underlying tunnel
+             * (real) netdev. */
+            oor_netdev = flow_get_tunnel_netdev(&match.flow.tunnel);
+            if (!oor_netdev) {
+                /* Not a 'tunnel' flow. */
+                oor_netdev = port->netdev;
+            }
+            netdev_set_hw_info(oor_netdev, HW_INFO_TYPE_OOR, true);
+        }
+        level = (err == ENOSPC || err == EOPNOTSUPP) ? VLL_DBG : VLL_ERR;
+        VLOG_RL(&rl, level, "failed to offload flow: %s: %s",
+                ovs_strerror(err),
+                (oor_netdev ? netdev_get_name(oor_netdev) :
+                              netdev_get_name(port->netdev)));
+    }
+
+out:
+    if (err && err != EEXIST && (put->flags & DPIF_FP_MODIFY)) {
+        /* Modified rule can't be offloaded, try and delete from HW. */
+        int del_err = 0;
+
+        if (!info.tc_modify_flow_deleted) {
+            del_err = netdev_offload_tc_flow_del(put->ufid, put->stats);
+        }
+
+        if (!del_err) {
+            /* Delete from hw success, so old flow was offloaded.
+             * Change flags to create the flow at the dpif level. */
+            put->flags &= ~DPIF_FP_MODIFY;
+            put->flags |= DPIF_FP_CREATE;
+        } else if (del_err != ENOENT) {
+            VLOG_ERR_RL(&rl, "failed to delete offloaded flow: %s",
+                        ovs_strerror(del_err));
+            /* Stop processing the flow in kernel. */
+            err = 0;
+        }
+    }
+
+    return err;
+}
+
+static int
+dpif_offload_tc_parse_flow_get(struct dpif_offload_tc *offload_tc,
+                               struct dpif_flow_get *get)
+{
+    struct dpif_offload_port_mgr_port *port;
+    struct dpif_flow *dpif_flow = get->flow;
+    struct odputil_keybuf maskbuf;
+    struct odputil_keybuf keybuf;
+    struct odputil_keybuf actbuf;
+    struct ofpbuf key, mask, act;
+    struct dpif_flow_stats stats;
+    struct dpif_flow_attrs attrs;
+    uint64_t act_buf[1024 / 8];
+    struct nlattr *actions;
+    struct match match;
+    struct ofpbuf buf;
+    int err = ENOENT;
+
+    ofpbuf_use_stack(&buf, &act_buf, sizeof act_buf);
+
+     DPIF_OFFLOAD_PORT_MGR_PORT_FOR_EACH (port, offload_tc->port_mgr) {
+         if (!netdev_offload_tc_flow_get(port->netdev, &match, &actions,
+                                         get->ufid, &stats, &attrs, &buf)) {
+             err = 0;
+             break;
+         }
+     }
+
+     if (err) {
+         return err;
+     }
+
+    VLOG_DBG("found flow from netdev, translating to dpif flow");
+
+    ofpbuf_use_stack(&key, &keybuf, sizeof keybuf);
+    ofpbuf_use_stack(&act, &actbuf, sizeof actbuf);
+    ofpbuf_use_stack(&mask, &maskbuf, sizeof maskbuf);
+    dpif_offload_tc_netdev_match_to_dpif_flow(&match, &key, &mask, actions,
+                                              &stats, &attrs,
+                                              (ovs_u128 *) get->ufid,
+                                              dpif_flow,
+                                              false);
+    ofpbuf_put(get->buffer, nl_attr_get(actions), nl_attr_get_size(actions));
+    dpif_flow->actions = ofpbuf_at(get->buffer, 0, 0);
+    dpif_flow->actions_len = nl_attr_get_size(actions);
+
+    return 0;
+}
+
+static void
+dpif_offload_tc_operate(struct dpif *dpif, const struct dpif_offload *offload,
+                        struct dpif_op **ops, size_t n_ops)
+{
+    struct dpif_offload_tc *offload_tc = dpif_offload_tc_cast(offload);
+
+    for (size_t i = 0; i < n_ops; i++) {
+        struct dpif_op *op = ops[i];
+        int error = EOPNOTSUPP;
+
+        if (op->error >= 0) {
+            continue;
+        }
+
+        switch (op->type) {
+        case DPIF_OP_FLOW_PUT: {
+            struct dpif_flow_put *put = &op->flow_put;
+
+            if (!put->ufid) {
+                break;
+            }
+
+            error = dpif_offload_tc_parse_flow_put(offload_tc, dpif, put);
+            break;
+        }
+        case DPIF_OP_FLOW_DEL: {
+            struct dpif_flow_del *del = &op->flow_del;
+
+            if (!del->ufid) {
+                break;
+            }
+
+            error = netdev_offload_tc_flow_del(del->ufid, del->stats);
+            break;
+        }
+        case DPIF_OP_FLOW_GET: {
+            struct dpif_flow_get *get = &op->flow_get;
+
+            if (!get->ufid) {
+                break;
+            }
+
+            error = dpif_offload_tc_parse_flow_get(offload_tc, get);
+            break;
+        }
+        case DPIF_OP_EXECUTE:
+            break;
+        } /* End of 'switch (op->type)'. */
+
+        if (error != EOPNOTSUPP && error != ENOENT) {
+            /* If the operation is unsupported or the entry was not found,
+             * we are skipping this flow operation.  Otherwise, it was
+             * processed and we should report the result. */
+            op->error = error;
+        }
+    }
+}
+
 struct dpif_offload_class dpif_offload_tc_class = {
     .type = "tc",
     .impl_type = DPIF_OFFLOAD_IMPL_HW_ONLY,
@@ -609,6 +849,7 @@  struct dpif_offload_class dpif_offload_tc_class = {
     .flow_dump_destroy = dpif_offload_tc_flow_dump_destroy,
     .flow_dump_thread_create = dpif_offload_tc_flow_dump_thread_create,
     .flow_dump_thread_destroy = dpif_offload_tc_flow_dump_thread_destroy,
+    .operate = dpif_offload_tc_operate,
     .flow_get_n_offloaded = dpif_offload_tc_flow_get_n_offloaded,
     .meter_set = dpif_offload_tc_meter_set,
     .meter_get = dpif_offload_tc_meter_get,
diff --git a/lib/netdev-offload-tc.c b/lib/netdev-offload-tc.c
index 0f9d97d6d..8c8c72a65 100644
--- a/lib/netdev-offload-tc.c
+++ b/lib/netdev-offload-tc.c
@@ -2300,11 +2300,11 @@  netdev_tc_parse_nl_actions(struct netdev *netdev, struct tc_flower *flower,
     return 0;
 }
 
-static int
-netdev_tc_flow_put(struct netdev *netdev, struct match *match,
-                   struct nlattr *actions, size_t actions_len,
-                   const ovs_u128 *ufid, struct offload_info *info,
-                   struct dpif_flow_stats *stats)
+int
+netdev_offload_tc_flow_put(struct netdev *netdev, struct match *match,
+                           struct nlattr *actions, size_t actions_len,
+                           const ovs_u128 *ufid, struct offload_info *info,
+                           struct dpif_flow_stats *stats)
 {
     static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
     enum tc_qdisc_hook hook = get_tc_qdisc_hook(netdev);
@@ -2672,6 +2672,7 @@  netdev_tc_flow_put(struct netdev *netdev, struct match *match,
     if (get_ufid_tc_mapping(ufid, &id) == 0) {
         VLOG_DBG_RL(&rl, "updating old handle: %d prio: %d",
                     id.handle, id.prio);
+        info->tc_modify_flow = true;
         info->tc_modify_flow_deleted = !del_filter_and_ufid_mapping(
             &id, ufid, &adjust_stats);
     }
@@ -2720,14 +2721,14 @@  netdev_tc_flow_put(struct netdev *netdev, struct match *match,
     return err;
 }
 
-static int
-netdev_tc_flow_get(struct netdev *netdev,
-                   struct match *match,
-                   struct nlattr **actions,
-                   const ovs_u128 *ufid,
-                   struct dpif_flow_stats *stats,
-                   struct dpif_flow_attrs *attrs,
-                   struct ofpbuf *buf)
+int
+netdev_offload_tc_flow_get(struct netdev *netdev,
+                           struct match *match,
+                           struct nlattr **actions,
+                           const ovs_u128 *ufid,
+                           struct dpif_flow_stats *stats,
+                           struct dpif_flow_attrs *attrs,
+                           struct ofpbuf *buf)
 {
     static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
     struct tc_flower flower;
@@ -2777,10 +2778,9 @@  netdev_tc_flow_get(struct netdev *netdev,
     return 0;
 }
 
-static int
-netdev_tc_flow_del(struct netdev *netdev OVS_UNUSED,
-                   const ovs_u128 *ufid,
-                   struct dpif_flow_stats *stats)
+int
+netdev_offload_tc_flow_del(const ovs_u128 *ufid,
+                           struct dpif_flow_stats *stats)
 {
     struct tcf_id id;
     int error;
@@ -3464,8 +3464,5 @@  dpif_offload_tc_meter_del(const struct dpif_offload *offload OVS_UNUSED,
 
 const struct netdev_flow_api netdev_offload_tc = {
    .type = "linux_tc",
-   .flow_put = netdev_tc_flow_put,
-   .flow_get = netdev_tc_flow_get,
-   .flow_del = netdev_tc_flow_del,
    .init_flow_api = netdev_tc_init_flow_api,
 };
diff --git a/lib/netdev-offload-tc.h b/lib/netdev-offload-tc.h
index e4740c799..1b5f33524 100644
--- a/lib/netdev-offload-tc.h
+++ b/lib/netdev-offload-tc.h
@@ -42,6 +42,15 @@  bool netdev_offload_tc_flow_dump_next(struct netdev_tc_flow_dump *,
                                       struct dpif_flow_attrs *, ovs_u128 *ufid,
                                       struct ofpbuf *rbuffer,
                                       struct ofpbuf *wbuffer);
+int netdev_offload_tc_flow_put(struct netdev *, struct match *,
+                               struct nlattr *actions, size_t actions_len,
+                               const ovs_u128 *ufid, struct offload_info *,
+                               struct dpif_flow_stats *);
+int netdev_offload_tc_flow_del(const ovs_u128 *ufid, struct dpif_flow_stats *);
+int netdev_offload_tc_flow_get(struct netdev *, struct match *,
+                               struct nlattr **actions, const ovs_u128 *ufid,
+                               struct dpif_flow_stats *,
+                               struct dpif_flow_attrs *, struct ofpbuf *);
 void dpif_offload_tc_meter_init(void);
 int dpif_offload_tc_meter_set(const struct dpif_offload *, ofproto_meter_id,
                               struct ofputil_meter_config *);
diff --git a/lib/netdev-offload.h b/lib/netdev-offload.h
index 4cdf7102f..0dfebae1b 100644
--- a/lib/netdev-offload.h
+++ b/lib/netdev-offload.h
@@ -71,6 +71,7 @@  struct offload_info {
      */
     uint32_t flow_mark;
 
+    bool tc_modify_flow; /* Indicates tc modified the flow. */
     bool tc_modify_flow_deleted; /* Indicate the tc modify flow put success
                                   * to delete the original flow. */
     odp_port_t orig_in_port; /* Originating in_port for tnl flows. */