diff --git a/lib/dpif-netlink.c b/lib/dpif-netlink.c
index 566edff..59b87ff 100644
--- a/lib/dpif-netlink.c
+++ b/lib/dpif-netlink.c
@@ -72,6 +72,7 @@ enum { MAX_PORTS = USHRT_MAX };
 #define ETH_FLAG_LRO      (1 << 15)    /* LRO is enabled */
 
 #define FLOW_DUMP_MAX_BATCH 50
+#define OPERATE_MAX_OPS 50
 
 struct dpif_netlink_dp {
     /* Generic Netlink header. */
@@ -1763,8 +1764,6 @@ static size_t
 dpif_netlink_operate__(struct dpif_netlink *dpif,
                        struct dpif_op **ops, size_t n_ops)
 {
-    enum { MAX_OPS = 50 };
-
     struct op_auxdata {
         struct nl_transaction txn;
 
@@ -1773,12 +1772,12 @@ dpif_netlink_operate__(struct dpif_netlink *dpif,
 
         struct ofpbuf reply;
         uint64_t reply_stub[1024 / 8];
-    } auxes[MAX_OPS];
+    } auxes[OPERATE_MAX_OPS];
 
-    struct nl_transaction *txnsp[MAX_OPS];
+    struct nl_transaction *txnsp[OPERATE_MAX_OPS];
     size_t i;
 
-    n_ops = MIN(n_ops, MAX_OPS);
+    n_ops = MIN(n_ops, OPERATE_MAX_OPS);
     for (i = 0; i < n_ops; i++) {
         struct op_auxdata *aux = &auxes[i];
         struct dpif_op *op = ops[i];
@@ -1921,18 +1920,218 @@ dpif_netlink_operate__(struct dpif_netlink *dpif,
     return n_ops;
 }
 
+static int
+parse_flow_put(struct dpif_netlink *dpif, struct dpif_flow_put *put)
+{
+    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
+    struct match match;
+    odp_port_t in_port;
+    const struct nlattr *nla;
+    size_t left;
+    int outputs = 0;
+    struct netdev *dev;
+    struct offload_info info;
+    ovs_be16 dst_port = 0;
+    int err;
+
+    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;
+    }
+
+    /* When we try to install a dummy flow from a probed feature. */
+    if (match.flow.dl_type == htons(0x1234)) {
+        return EOPNOTSUPP;
+    }
+
+    in_port = match.flow.in_port.odp_port;
+    dev = netdev_hmap_port_get(in_port, DPIF_HMAP_KEY(&dpif->dpif));
+    if (!dev) {
+        return EOPNOTSUPP;
+    }
+
+    /* Get tunnel dst port and count outputs */
+    NL_ATTR_FOR_EACH(nla, left, put->actions, put->actions_len) {
+        if (nl_attr_type(nla) == OVS_ACTION_ATTR_OUTPUT) {
+            const struct netdev_tunnel_config *tnl_cfg;
+            struct netdev *outdev;
+            odp_port_t out_port;
+
+            outputs++;
+            if (outputs > 1) {
+                VLOG_DBG_RL(&rl, "offloading multiple ports isn't supported");
+                err = EOPNOTSUPP;
+                goto out;
+            }
+
+            out_port = nl_attr_get_odp_port(nla);
+            outdev = netdev_hmap_port_get(out_port, DPIF_HMAP_KEY(&dpif->dpif));
+            if (!outdev) {
+                err = EOPNOTSUPP;
+                goto out;
+            }
+            tnl_cfg = netdev_get_tunnel_config(outdev);
+            if (tnl_cfg && tnl_cfg->dst_port != 0) {
+                dst_port = tnl_cfg->dst_port;
+            }
+            netdev_close(outdev);
+        }
+    }
+
+    info.port_hmap_obj = DPIF_HMAP_KEY(&dpif->dpif);
+    info.tp_dst_port = dst_port;
+    err = netdev_flow_put(dev, &match,
+                          CONST_CAST(struct nlattr *, put->actions),
+                          put->actions_len, put->stats,
+                          CONST_CAST(ovs_u128 *, put->ufid), &info);
+
+    if (!err) {
+        if (put->flags & DPIF_FP_MODIFY) {
+            struct dpif_op *opp;
+            struct dpif_op op;
+
+            op.type = DPIF_OP_FLOW_DEL;
+            op.u.flow_del.key = put->key;
+            op.u.flow_del.key_len = put->key_len;
+            op.u.flow_del.ufid = put->ufid;
+            op.u.flow_del.pmd_id = put->pmd_id;
+            op.u.flow_del.stats = NULL;
+            op.u.flow_del.terse = false;
+
+            opp = &op;
+            dpif_netlink_operate__(dpif, &opp, 1);
+        }
+
+        VLOG_DBG("added flow");
+    } else if (err != EEXIST) {
+        VLOG_ERR_RL(&rl, "failed adding flow: %s", ovs_strerror(err));
+    }
+
+out:
+    if (err && err != EEXIST && (put->flags & DPIF_FP_MODIFY)) {
+        /* Modified rule can't be offloaded, try and delete from HW */
+        int del_err = netdev_flow_del(dev, put->ufid, put->stats);
+
+        if (!del_err) {
+            /* Delete from hw success, so old flow was offloaded.
+             * Change flags to create the flow in kernel */
+            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 proccesing the flow in kernel */
+            err = 0;
+        }
+    }
+
+    netdev_close(dev);
+
+    return err;
+}
+
 static void
-dpif_netlink_operate(struct dpif *dpif_, struct dpif_op **ops, size_t n_ops)
+dbg_print_flow(const struct nlattr *key, size_t key_len,
+               const struct nlattr *mask, size_t mask_len,
+               const struct nlattr *actions, size_t actions_len,
+               const ovs_u128 *ufid,
+               const char *op)
+{
+        struct ds s;
+
+        ds_init(&s);
+        ds_put_cstr(&s, op);
+        ds_put_cstr(&s, " (");
+        odp_format_ufid(ufid, &s);
+        ds_put_cstr(&s, ")");
+        if (key_len) {
+            ds_put_cstr(&s, "\nflow (verbose): ");
+            odp_flow_format(key, key_len, mask, mask_len, NULL, &s, true);
+            ds_put_cstr(&s, "\nflow: ");
+            odp_flow_format(key, key_len, mask, mask_len, NULL, &s, false);
+        }
+        ds_put_cstr(&s, "\nactions: ");
+        format_odp_actions(&s, actions, actions_len);
+        VLOG_DBG("\n%s", ds_cstr(&s));
+        ds_destroy(&s);
+}
+
+static int
+try_send_to_netdev(struct dpif_netlink *dpif, struct dpif_op *op)
 {
-    struct dpif_netlink *dpif = dpif_netlink_cast(dpif_);
+    switch (op->type) {
+    case DPIF_OP_FLOW_PUT: {
+        struct dpif_flow_put *put = &op->u.flow_put;
 
+        if (!put->ufid) {
+            break;
+        }
+        dbg_print_flow(put->key, put->key_len, put->mask, put->mask_len,
+                       put->actions, put->actions_len, put->ufid,
+                       (put->flags & DPIF_FP_MODIFY ? "PUT(MODIFY)" : "PUT"));
+        return parse_flow_put(dpif, put);
+    }
+    case DPIF_OP_FLOW_DEL:
+    case DPIF_OP_FLOW_GET:
+    case DPIF_OP_EXECUTE:
+    default:
+        break;
+    }
+    return EOPNOTSUPP;
+}
+
+static void
+dpif_netlink_operate_chunks(struct dpif_netlink *dpif, struct dpif_op **ops,
+                            size_t n_ops)
+{
     while (n_ops > 0) {
         size_t chunk = dpif_netlink_operate__(dpif, ops, n_ops);
+
         ops += chunk;
         n_ops -= chunk;
     }
 }
 
+static void
+dpif_netlink_operate(struct dpif *dpif_, struct dpif_op **ops, size_t n_ops)
+{
+    struct dpif_netlink *dpif = dpif_netlink_cast(dpif_);
+    struct dpif_op *new_ops[OPERATE_MAX_OPS];
+    int count = 0;
+    int i = 0;
+    int err = 0;
+
+    if (netdev_flow_api_enabled) {
+        while (n_ops > 0) {
+            count = 0;
+
+            while (n_ops > 0 && count < OPERATE_MAX_OPS) {
+                struct dpif_op *op = ops[i++];
+
+                err = try_send_to_netdev(dpif, op);
+                if (err && err != EEXIST) {
+                    new_ops[count++] = op;
+                } else {
+                    op->error = err;
+                }
+
+                n_ops--;
+            }
+
+            dpif_netlink_operate_chunks(dpif, new_ops, count);
+        }
+
+        return;
+    }
+
+    dpif_netlink_operate_chunks(dpif, ops, n_ops);
+}
+
 #if _WIN32
 static void
 dpif_netlink_handler_uninit(struct dpif_handler *handler)
diff --git a/lib/odp-util.c b/lib/odp-util.c
index 4106738..274d2ee 100644
--- a/lib/odp-util.c
+++ b/lib/odp-util.c
@@ -41,6 +41,7 @@
 #include "util.h"
 #include "uuid.h"
 #include "openvswitch/vlog.h"
+#include "openvswitch/match.h"
 
 VLOG_DEFINE_THIS_MODULE(odp_util);
 
@@ -5294,6 +5295,58 @@ odp_flow_key_to_mask(const struct nlattr *mask_key, size_t mask_key_len,
     }
 }
 
+/* Converts the netlink formated key/mask to match.
+ * Fails if odp_flow_key_from_key/mask and odp_flow_key_key/mask
+ * disagree on the acceptable form of flow */
+int
+parse_key_and_mask_to_match(const struct nlattr *key, size_t key_len,
+                            const struct nlattr *mask, size_t mask_len,
+                            struct match *match)
+{
+    enum odp_key_fitness fitness;
+
+    fitness = odp_flow_key_to_flow(key, key_len, &match->flow);
+    if (fitness) {
+        /* This should not happen: it indicates that odp_flow_key_from_flow()
+         * and odp_flow_key_to_flow() disagree on the acceptable form of a
+         * flow.  Log the problem as an error, with enough details to enable
+         * debugging. */
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+
+        if (!VLOG_DROP_ERR(&rl)) {
+            struct ds s;
+
+            ds_init(&s);
+            odp_flow_format(key, key_len, NULL, 0, NULL, &s, true);
+            VLOG_ERR("internal error parsing flow key %s", ds_cstr(&s));
+            ds_destroy(&s);
+        }
+
+        return EINVAL;
+    }
+
+    fitness = odp_flow_key_to_mask(mask, mask_len, &match->wc, &match->flow);
+    if (fitness) {
+        /* This should not happen: it indicates that
+         * odp_flow_key_from_mask() and odp_flow_key_to_mask()
+         * disagree on the acceptable form of a mask.  Log the problem
+         * as an error, with enough details to enable debugging. */
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+
+        if (!VLOG_DROP_ERR(&rl)) {
+            struct ds s;
+
+            VLOG_ERR("internal error parsing flow mask %s (%s)",
+                     ds_cstr(&s), odp_key_fitness_to_string(fitness));
+            ds_destroy(&s);
+        }
+
+        return EINVAL;
+    }
+
+    return 0;
+}
+
 /* Returns 'fitness' as a string, for use in debug messages. */
 const char *
 odp_key_fitness_to_string(enum odp_key_fitness fitness)
diff --git a/lib/odp-util.h b/lib/odp-util.h
index 42011bc..3f59ab7 100644
--- a/lib/odp-util.h
+++ b/lib/odp-util.h
@@ -236,6 +236,9 @@ enum odp_key_fitness odp_flow_key_to_mask(const struct nlattr *mask_key,
                                           size_t mask_key_len,
                                           struct flow_wildcards *mask,
                                           const struct flow *flow);
+int parse_key_and_mask_to_match(const struct nlattr *key, size_t key_len,
+                                const struct nlattr *mask, size_t mask_len,
+                                struct match *match);
 
 const char *odp_key_fitness_to_string(enum odp_key_fitness);
 
