diff --git a/CMakeLists.txt b/CMakeLists.txt
index 096837d..732be9e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -398,6 +398,7 @@
     src/openvpn/dco_win.h
     src/openvpn/dco_linux.c
     src/openvpn/dco_linux.h
+    src/openvpn/dco_linux_v2.c
     src/openvpn/dco_freebsd.c
     src/openvpn/dco_freebsd.h
     src/openvpn/dhcp.c
diff --git a/src/openvpn/Makefile.am b/src/openvpn/Makefile.am
index 56cce9d..286d3c5 100644
--- a/src/openvpn/Makefile.am
+++ b/src/openvpn/Makefile.am
@@ -54,6 +54,7 @@
 	dco.c dco.h dco_internal.h \
 	dco_freebsd.c dco_freebsd.h \
 	dco_linux.c dco_linux.h \
+	dco_linux_v2.c \
 	dco_win.c dco_win.h \
 	dhcp.c dhcp.h \
 	dns.c dns.h \
diff --git a/src/openvpn/dco.c b/src/openvpn/dco.c
index 78243b1..80184f8 100644
--- a/src/openvpn/dco.c
+++ b/src/openvpn/dco.c
@@ -396,7 +396,7 @@
     /* now that all options have been confirmed to be supported, check
      * if DCO is truly available on the system
      */
-    return dco_available(msglevel);
+    return dco_available(msglevel, NULL);
 }
 
 bool
diff --git a/src/openvpn/dco.h b/src/openvpn/dco.h
index 50ebb35..fc34da8 100644
--- a/src/openvpn/dco.h
+++ b/src/openvpn/dco.h
@@ -56,7 +56,7 @@
  * @param msglevel      level to print messages to
  * @return              true if ovpn-dco is available, false otherwise
  */
-bool dco_available(int msglevel);
+bool dco_available(int msglevel, dco_context_t *dco);
 
 
 /**
@@ -254,7 +254,7 @@
 typedef void *dco_context_t;
 
 static inline bool
-dco_available(int msglevel)
+dco_available(int msglevel, dco_context_t *dco)
 {
     return false;
 }
diff --git a/src/openvpn/dco_freebsd.c b/src/openvpn/dco_freebsd.c
index 7c8b29c..93c0813 100644
--- a/src/openvpn/dco_freebsd.c
+++ b/src/openvpn/dco_freebsd.c
@@ -569,7 +569,7 @@
 }
 
 bool
-dco_available(int msglevel)
+dco_available(int msglevel, dco_context_t *dco)
 {
     struct if_clonereq ifcr;
     char *buf = NULL;
diff --git a/src/openvpn/dco_linux.c b/src/openvpn/dco_linux.c
index b2584b9..4feae6d 100644
--- a/src/openvpn/dco_linux.c
+++ b/src/openvpn/dco_linux.c
@@ -51,32 +51,28 @@
 #include <netlink/genl/ctrl.h>
 
 
-/* libnl < 3.5.0 does not set the NLA_F_NESTED on its own, therefore we
- * have to explicitly do it to prevent the kernel from failing upon
- * parsing of the message
- */
-#define nla_nest_start(_msg, _type) \
-    nla_nest_start(_msg, (_type) | NLA_F_NESTED)
-
-static int ovpn_get_mcast_id(dco_context_t *dco);
-
-void dco_check_key_ctx(const struct key_ctx_bi *key);
-
-typedef int (*ovpn_nl_cb)(struct nl_msg *msg, void *arg);
-
-/**
- * @brief resolves the netlink ID for ovpn-dco
- *
- * This function queries the kernel via a netlink socket
- * whether the ovpn-dco netlink namespace is available
- *
- * This function can be used to determine if the kernel
- * supports DCO offloading.
- *
- * @return ID on success, negative error code on error
- */
 static int
-resolve_ovpn_netlink_id(int msglevel)
+do_family_name_resolve(struct nl_sock *nl_sock, int msglevel, const char *family_name,
+                       dco_context_t *dco, const struct dco_ops *ops)
+{
+    int ret = genl_ctrl_resolve(nl_sock, family_name);
+
+    if (ret < 0)
+    {
+        msg(msglevel, "Cannot find %s netlink component: %s", family_name, nl_geterror(ret));
+    }
+
+    if (dco)
+    {
+        /* Ensures that we will use the proper ops for the dco version available */
+        dco->ops = ops;
+    }
+
+    return ret;
+}
+
+int
+resolve_ovpn_netlink_id(int msglevel, dco_context_t *dco)
 {
     int ret;
     struct nl_sock *nl_sock = nl_socket_alloc();
@@ -96,39 +92,14 @@
     }
     set_cloexec(nl_socket_get_fd(nl_sock));
 
-    ret = genl_ctrl_resolve(nl_sock, OVPN_NL_NAME);
-    if (ret < 0)
-    {
-        msg(msglevel, "Cannot find ovpn_dco netlink component: %s",
-            nl_geterror(ret));
-    }
+    ret = do_family_name_resolve(nl_sock, msglevel, OVPN_NL_NAME, dco, &dco_ops_v2);
 
 err_sock:
     nl_socket_free(nl_sock);
     return ret;
 }
 
-static struct nl_msg *
-ovpn_dco_nlmsg_create(dco_context_t *dco, enum ovpn_nl_commands cmd)
-{
-    struct nl_msg *nl_msg = nlmsg_alloc();
-    if (!nl_msg)
-    {
-        msg(M_ERR, "cannot allocate netlink message");
-        return NULL;
-    }
-
-    genlmsg_put(nl_msg, 0, 0, dco->ovpn_dco_id, 0, 0, cmd, 0);
-    NLA_PUT_U32(nl_msg, OVPN_ATTR_IFINDEX, dco->ifindex);
-
-    return nl_msg;
-nla_put_failure:
-    nlmsg_free(nl_msg);
-    msg(M_INFO, "cannot put into netlink message");
-    return NULL;
-}
-
-static int
+int
 ovpn_nl_recvmsgs(dco_context_t *dco, const char *prefix)
 {
     int ret = nl_recvmsgs(dco->nl_sock, dco->nl_cb);
@@ -166,18 +137,7 @@
     return ret;
 }
 
-/**
- * Send a prepared netlink message and registers cb as callback if non-null.
- *
- * The method will also free nl_msg
- * @param dco       The dco context to use
- * @param nl_msg    the message to use
- * @param cb        An optional callback if the caller expects an answer
- * @param cb_arg    An optional param to pass to the callback
- * @param prefix    A prefix to report in the error message to give the user context
- * @return          status of sending the message
- */
-static int
+int
 ovpn_nl_msg_send(dco_context_t *dco, struct nl_msg *nl_msg, ovpn_nl_cb cb,
                  void *cb_arg, const char *prefix)
 {
@@ -218,71 +178,6 @@
 }
 
 int
-dco_new_peer(dco_context_t *dco, unsigned int peerid, int sd,
-             struct sockaddr *localaddr, struct sockaddr *remoteaddr,
-             struct in_addr *remote_in4, struct in6_addr *remote_in6)
-{
-    struct gc_arena gc = gc_new();
-    const char *remotestr = "[undefined]";
-    if (remoteaddr)
-    {
-        remotestr = print_sockaddr(remoteaddr, &gc);
-    }
-    msg(D_DCO_DEBUG, "%s: peer-id %d, fd %d, remote addr: %s", __func__,
-        peerid, sd, remotestr);
-
-    struct nl_msg *nl_msg = ovpn_dco_nlmsg_create(dco, OVPN_CMD_NEW_PEER);
-    struct nlattr *attr = nla_nest_start(nl_msg, OVPN_ATTR_NEW_PEER);
-    int ret = -EMSGSIZE;
-
-    NLA_PUT_U32(nl_msg, OVPN_NEW_PEER_ATTR_PEER_ID, peerid);
-    NLA_PUT_U32(nl_msg, OVPN_NEW_PEER_ATTR_SOCKET, sd);
-
-    /* Set the remote endpoint if defined (for UDP) */
-    if (remoteaddr)
-    {
-        remoteaddr = mapped_v4_to_v6(remoteaddr, &gc);
-        int alen = af_addr_size(remoteaddr->sa_family);
-
-        NLA_PUT(nl_msg, OVPN_NEW_PEER_ATTR_SOCKADDR_REMOTE, alen, remoteaddr);
-    }
-
-    if (localaddr)
-    {
-        localaddr = mapped_v4_to_v6(localaddr, &gc);
-        if (localaddr->sa_family == AF_INET)
-        {
-            NLA_PUT(nl_msg, OVPN_NEW_PEER_ATTR_LOCAL_IP, sizeof(struct in_addr),
-                    &((struct sockaddr_in *)localaddr)->sin_addr);
-        }
-        else if (localaddr->sa_family == AF_INET6)
-        {
-            NLA_PUT(nl_msg, OVPN_NEW_PEER_ATTR_LOCAL_IP, sizeof(struct in6_addr),
-                    &((struct sockaddr_in6 *)localaddr)->sin6_addr);
-        }
-    }
-
-    /* Set the primary VPN IP addresses of the peer */
-    if (remote_in4)
-    {
-        NLA_PUT_U32(nl_msg, OVPN_NEW_PEER_ATTR_IPV4, remote_in4->s_addr);
-    }
-    if (remote_in6)
-    {
-        NLA_PUT(nl_msg, OVPN_NEW_PEER_ATTR_IPV6, sizeof(struct in6_addr),
-                remote_in6);
-    }
-    nla_nest_end(nl_msg, attr);
-
-    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
-
-nla_put_failure:
-    nlmsg_free(nl_msg);
-    gc_free(&gc);
-    return ret;
-}
-
-static int
 ovpn_nl_cb_finish(struct nl_msg (*msg) __attribute__ ((unused)), void *arg)
 {
     int *status = arg;
@@ -291,15 +186,7 @@
     return NL_SKIP;
 }
 
-/* This function is used as error callback on the netlink socket.
- * When something goes wrong and the kernel returns an error, this function is
- * invoked.
- *
- * We pass the error code to the user by means of a variable pointed by *arg
- * (supplied by the user when setting this callback) and we parse the kernel
- * reply to see if it contains a human-readable error. If found, it is printed.
- */
-static int
+int
 ovpn_nl_cb_error(struct sockaddr_nl (*nla) __attribute__ ((unused)),
                  struct nlmsgerr *err, void *arg)
 {
@@ -342,10 +229,16 @@
     return NL_STOP;
 }
 
-static void
+void
 ovpn_dco_init_netlink(dco_context_t *dco)
 {
-    dco->ovpn_dco_id = resolve_ovpn_netlink_id(M_ERR);
+    struct gc_arena gc = gc_new();
+    if (!dco->ops)
+    {
+        ALLOC_OBJ_CLEAR_GC(dco->ops, struct dco_ops, &gc);
+    }
+
+    dco->ovpn_dco_id = resolve_ovpn_netlink_id(M_ERR, dco);
 
     dco->nl_sock = nl_socket_alloc();
 
@@ -388,6 +281,7 @@
      * overrun with very fast connecting/disconnecting clients.
      * TODO: fix this in a better and more reliable way */
     ASSERT(!nl_socket_set_buffer_size(dco->nl_sock, 1024*1024, 1024*1024));
+    gc_free(&gc);
 }
 
 bool
@@ -411,7 +305,7 @@
     return true;
 }
 
-static void
+void
 ovpn_dco_uninit_netlink(dco_context_t *dco)
 {
     nl_socket_free(dco->nl_sock);
@@ -423,7 +317,7 @@
     CLEAR(dco);
 }
 
-static void
+void
 ovpn_dco_register(dco_context_t *dco)
 {
     msg(D_DCO_DEBUG, __func__);
@@ -447,205 +341,11 @@
 int
 open_tun_dco(struct tuntap *tt, openvpn_net_ctx_t *ctx, const char *dev)
 {
-    msg(D_DCO_DEBUG, "%s: %s", __func__, dev);
-    ASSERT(tt->type == DEV_TYPE_TUN);
-
-    int ret = net_iface_new(ctx, dev, "ovpn-dco", &tt->dco);
-    if (ret < 0)
-    {
-        msg(D_DCO_DEBUG, "Cannot create DCO interface %s: %d", dev, ret);
-        return ret;
-    }
-
-    tt->dco.ifindex = if_nametoindex(dev);
-    if (!tt->dco.ifindex)
-    {
-        msg(M_FATAL, "DCO: cannot retrieve ifindex for interface %s", dev);
-    }
-
-    tt->dco.dco_message_peer_id = -1;
-
-    ovpn_dco_register(&tt->dco);
-
-    return 0;
-}
-
-void
-close_tun_dco(struct tuntap *tt, openvpn_net_ctx_t *ctx)
-{
-    msg(D_DCO_DEBUG, __func__);
-
-    net_iface_del(ctx, tt->actual_name);
-    ovpn_dco_uninit_netlink(&tt->dco);
-}
-
-int
-dco_swap_keys(dco_context_t *dco, unsigned int peerid)
-{
-    msg(D_DCO_DEBUG, "%s: peer-id %d", __func__, peerid);
-
-    struct nl_msg *nl_msg = ovpn_dco_nlmsg_create(dco, OVPN_CMD_SWAP_KEYS);
-    if (!nl_msg)
-    {
-        return -ENOMEM;
-    }
-
-    struct nlattr *attr = nla_nest_start(nl_msg, OVPN_ATTR_SWAP_KEYS);
-    int ret = -EMSGSIZE;
-    NLA_PUT_U32(nl_msg, OVPN_SWAP_KEYS_ATTR_PEER_ID, peerid);
-    nla_nest_end(nl_msg, attr);
-
-    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
-
-nla_put_failure:
-    nlmsg_free(nl_msg);
-    return ret;
-}
-
-
-int
-dco_del_peer(dco_context_t *dco, unsigned int peerid)
-{
-    msg(D_DCO_DEBUG, "%s: peer-id %d", __func__, peerid);
-
-    struct nl_msg *nl_msg = ovpn_dco_nlmsg_create(dco, OVPN_CMD_DEL_PEER);
-    if (!nl_msg)
-    {
-        return -ENOMEM;
-    }
-
-    struct nlattr *attr = nla_nest_start(nl_msg, OVPN_ATTR_DEL_PEER);
-    int ret = -EMSGSIZE;
-    NLA_PUT_U32(nl_msg, OVPN_DEL_PEER_ATTR_PEER_ID, peerid);
-    nla_nest_end(nl_msg, attr);
-
-    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
-
-nla_put_failure:
-    nlmsg_free(nl_msg);
-    return ret;
-}
-
-
-int
-dco_del_key(dco_context_t *dco, unsigned int peerid,
-            dco_key_slot_t slot)
-{
-    msg(D_DCO_DEBUG, "%s: peer-id %d, slot %d", __func__, peerid, slot);
-
-    struct nl_msg *nl_msg = ovpn_dco_nlmsg_create(dco, OVPN_CMD_DEL_KEY);
-    if (!nl_msg)
-    {
-        return -ENOMEM;
-    }
-
-    struct nlattr *attr = nla_nest_start(nl_msg, OVPN_ATTR_DEL_KEY);
-    int ret = -EMSGSIZE;
-    NLA_PUT_U32(nl_msg, OVPN_DEL_KEY_ATTR_PEER_ID, peerid);
-    NLA_PUT_U8(nl_msg, OVPN_DEL_KEY_ATTR_KEY_SLOT, slot);
-    nla_nest_end(nl_msg, attr);
-
-    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
-
-nla_put_failure:
-    nlmsg_free(nl_msg);
+    int ret = tt->dco.ops->open_tun_dco(tt, ctx, dev);
     return ret;
 }
 
 int
-dco_new_key(dco_context_t *dco, unsigned int peerid, int keyid,
-            dco_key_slot_t slot,
-            const uint8_t *encrypt_key, const uint8_t *encrypt_iv,
-            const uint8_t *decrypt_key, const uint8_t *decrypt_iv,
-            const char *ciphername)
-{
-    msg(D_DCO_DEBUG, "%s: slot %d, key-id %d, peer-id %d, cipher %s",
-        __func__, slot, keyid, peerid, ciphername);
-
-    const size_t key_len = cipher_kt_key_size(ciphername);
-    const int nonce_tail_len = 8;
-
-    struct nl_msg *nl_msg = ovpn_dco_nlmsg_create(dco, OVPN_CMD_NEW_KEY);
-    if (!nl_msg)
-    {
-        return -ENOMEM;
-    }
-
-    dco_cipher_t dco_cipher = dco_get_cipher(ciphername);
-
-    int ret = -EMSGSIZE;
-    struct nlattr *attr = nla_nest_start(nl_msg, OVPN_ATTR_NEW_KEY);
-    NLA_PUT_U32(nl_msg, OVPN_NEW_KEY_ATTR_PEER_ID, peerid);
-    NLA_PUT_U8(nl_msg, OVPN_NEW_KEY_ATTR_KEY_SLOT, slot);
-    NLA_PUT_U8(nl_msg, OVPN_NEW_KEY_ATTR_KEY_ID, keyid);
-    NLA_PUT_U16(nl_msg, OVPN_NEW_KEY_ATTR_CIPHER_ALG, dco_cipher);
-
-    struct nlattr *key_enc = nla_nest_start(nl_msg,
-                                            OVPN_NEW_KEY_ATTR_ENCRYPT_KEY);
-    if (dco_cipher != OVPN_CIPHER_ALG_NONE)
-    {
-        NLA_PUT(nl_msg, OVPN_KEY_DIR_ATTR_CIPHER_KEY, key_len, encrypt_key);
-        NLA_PUT(nl_msg, OVPN_KEY_DIR_ATTR_NONCE_TAIL, nonce_tail_len,
-                encrypt_iv);
-    }
-    nla_nest_end(nl_msg, key_enc);
-
-    struct nlattr *key_dec = nla_nest_start(nl_msg,
-                                            OVPN_NEW_KEY_ATTR_DECRYPT_KEY);
-    if (dco_cipher != OVPN_CIPHER_ALG_NONE)
-    {
-        NLA_PUT(nl_msg, OVPN_KEY_DIR_ATTR_CIPHER_KEY, key_len, decrypt_key);
-        NLA_PUT(nl_msg, OVPN_KEY_DIR_ATTR_NONCE_TAIL, nonce_tail_len,
-                decrypt_iv);
-    }
-    nla_nest_end(nl_msg, key_dec);
-
-    nla_nest_end(nl_msg, attr);
-
-    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
-
-nla_put_failure:
-    nlmsg_free(nl_msg);
-    return ret;
-}
-
-int
-dco_set_peer(dco_context_t *dco, unsigned int peerid,
-             int keepalive_interval, int keepalive_timeout, int mss)
-{
-    msg(D_DCO_DEBUG, "%s: peer-id %d, keepalive %d/%d, mss %d", __func__,
-        peerid, keepalive_interval, keepalive_timeout, mss);
-
-    struct nl_msg *nl_msg = ovpn_dco_nlmsg_create(dco, OVPN_CMD_SET_PEER);
-    if (!nl_msg)
-    {
-        return -ENOMEM;
-    }
-
-    struct nlattr *attr = nla_nest_start(nl_msg, OVPN_ATTR_SET_PEER);
-    int ret = -EMSGSIZE;
-    NLA_PUT_U32(nl_msg, OVPN_SET_PEER_ATTR_PEER_ID, peerid);
-    NLA_PUT_U32(nl_msg, OVPN_SET_PEER_ATTR_KEEPALIVE_INTERVAL,
-                keepalive_interval);
-    NLA_PUT_U32(nl_msg, OVPN_SET_PEER_ATTR_KEEPALIVE_TIMEOUT,
-                keepalive_timeout);
-    nla_nest_end(nl_msg, attr);
-
-    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
-
-nla_put_failure:
-    nlmsg_free(nl_msg);
-    return ret;
-}
-
-/* This function parses the reply provided by the kernel to the CTRL_CMD_GETFAMILY
- * message. We parse the reply and we retrieve the multicast group ID associated
- * with the "ovpn-dco" netlink family.
- *
- * The ID is later used to subscribe to the multicast group and be notified
- * about any multicast message sent by the ovpn-dco kernel module.
- */
-static int
 mcast_family_handler(struct nl_msg *msg, void *arg)
 {
     dco_context_t *dco = arg;
@@ -687,12 +387,8 @@
 
     return NL_SKIP;
 }
-/**
- * Lookup the multicast id for OpenVPN. This method and its help method currently
- * hardcode the lookup to OVPN_NL_NAME and OVPN_NL_MULTICAST_GROUP_PEERS but
- * extended in the future if we need to lookup more than one mcast id.
- */
-static int
+
+int
 ovpn_get_mcast_id(dco_context_t *dco)
 {
     dco->ovpn_dco_mcast_id = -ENOENT;
@@ -719,299 +415,15 @@
     return ret;
 }
 
-/* This function parses any netlink message sent by ovpn-dco to userspace */
-static int
-ovpn_handle_msg(struct nl_msg *msg, void *arg)
-{
-    dco_context_t *dco = arg;
-
-    struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
-    struct nlattr *attrs[OVPN_ATTR_MAX + 1];
-    struct nlmsghdr *nlh = nlmsg_hdr(msg);
-
-    if (!genlmsg_valid_hdr(nlh, 0))
-    {
-        msg(D_DCO, "ovpn-dco: invalid header");
-        return NL_SKIP;
-    }
-
-    if (nla_parse(attrs, OVPN_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
-                  genlmsg_attrlen(gnlh, 0), NULL))
-    {
-        msg(D_DCO, "received bogus data from ovpn-dco");
-        return NL_SKIP;
-    }
-
-    /* we must know which interface this message is referring to in order to
-     * avoid mixing messages for other instances
-     */
-    if (!attrs[OVPN_ATTR_IFINDEX])
-    {
-        msg(D_DCO, "ovpn-dco: Received message without ifindex");
-        return NL_SKIP;
-    }
-
-    uint32_t ifindex = nla_get_u32(attrs[OVPN_ATTR_IFINDEX]);
-    if (ifindex != dco->ifindex)
-    {
-        msg(D_DCO_DEBUG,
-            "ovpn-dco: ignoring message (type=%d) for foreign ifindex %d",
-            gnlh->cmd, ifindex);
-        return NL_SKIP;
-    }
-
-    /* based on the message type, we parse the subobject contained in the
-     * message, that stores the type-specific attributes.
-     *
-     * the "dco" object is then filled accordingly with the information
-     * retrieved from the message, so that the rest of the OpenVPN code can
-     * react as need be.
-     */
-    switch (gnlh->cmd)
-    {
-        case OVPN_CMD_DEL_PEER:
-        {
-            if (!attrs[OVPN_ATTR_DEL_PEER])
-            {
-                msg(D_DCO, "ovpn-dco: no attributes in OVPN_DEL_PEER message");
-                return NL_SKIP;
-            }
-
-            struct nlattr *dp_attrs[OVPN_DEL_PEER_ATTR_MAX + 1];
-            if (nla_parse_nested(dp_attrs, OVPN_DEL_PEER_ATTR_MAX,
-                                 attrs[OVPN_ATTR_DEL_PEER], NULL))
-            {
-                msg(D_DCO, "received bogus del peer packet data from ovpn-dco");
-                return NL_SKIP;
-            }
-
-            if (!dp_attrs[OVPN_DEL_PEER_ATTR_REASON])
-            {
-                msg(D_DCO, "ovpn-dco: no reason in DEL_PEER message");
-                return NL_SKIP;
-            }
-            if (!dp_attrs[OVPN_DEL_PEER_ATTR_PEER_ID])
-            {
-                msg(D_DCO, "ovpn-dco: no peer-id in DEL_PEER message");
-                return NL_SKIP;
-            }
-            int reason = nla_get_u8(dp_attrs[OVPN_DEL_PEER_ATTR_REASON]);
-            unsigned int peerid = nla_get_u32(dp_attrs[OVPN_DEL_PEER_ATTR_PEER_ID]);
-
-            msg(D_DCO_DEBUG, "ovpn-dco: received CMD_DEL_PEER, ifindex: %d, peer-id %d, reason: %d",
-                ifindex, peerid, reason);
-            dco->dco_message_peer_id = peerid;
-            dco->dco_del_peer_reason = reason;
-            dco->dco_message_type = OVPN_CMD_DEL_PEER;
-
-            break;
-        }
-
-        default:
-            msg(D_DCO, "ovpn-dco: received unknown command: %d", gnlh->cmd);
-            dco->dco_message_type = 0;
-            return NL_SKIP;
-    }
-
-    return NL_OK;
-}
-
-int
-dco_do_read(dco_context_t *dco)
-{
-    msg(D_DCO_DEBUG, __func__);
-    nl_cb_set(dco->nl_cb, NL_CB_VALID, NL_CB_CUSTOM, ovpn_handle_msg, dco);
-
-    return ovpn_nl_recvmsgs(dco, __func__);
-}
-
-static void
-dco_update_peer_stat(struct context_2 *c2, struct nlattr *tb[], uint32_t id)
-{
-    if (tb[OVPN_GET_PEER_RESP_ATTR_LINK_RX_BYTES])
-    {
-        c2->dco_read_bytes = nla_get_u64(tb[OVPN_GET_PEER_RESP_ATTR_LINK_RX_BYTES]);
-        msg(D_DCO_DEBUG, "%s / dco_read_bytes: " counter_format, __func__,
-            c2->dco_read_bytes);
-    }
-    else
-    {
-        msg(M_WARN, "%s: no link RX bytes provided in reply for peer %u",
-            __func__, id);
-    }
-
-    if (tb[OVPN_GET_PEER_RESP_ATTR_LINK_TX_BYTES])
-    {
-        c2->dco_write_bytes = nla_get_u64(tb[OVPN_GET_PEER_RESP_ATTR_LINK_TX_BYTES]);
-        msg(D_DCO_DEBUG, "%s / dco_write_bytes: " counter_format, __func__,
-            c2->dco_write_bytes);
-    }
-    else
-    {
-        msg(M_WARN, "%s: no link TX bytes provided in reply for peer %u",
-            __func__, id);
-    }
-
-    if (tb[OVPN_GET_PEER_RESP_ATTR_VPN_RX_BYTES])
-    {
-        c2->tun_read_bytes = nla_get_u64(tb[OVPN_GET_PEER_RESP_ATTR_VPN_RX_BYTES]);
-        msg(D_DCO_DEBUG, "%s / tun_read_bytes: " counter_format, __func__,
-            c2->tun_read_bytes);
-    }
-    else
-    {
-        msg(M_WARN, "%s: no VPN RX bytes provided in reply for peer %u",
-            __func__, id);
-    }
-
-    if (tb[OVPN_GET_PEER_RESP_ATTR_VPN_TX_BYTES])
-    {
-        c2->tun_write_bytes = nla_get_u64(tb[OVPN_GET_PEER_RESP_ATTR_VPN_TX_BYTES]);
-        msg(D_DCO_DEBUG, "%s / tun_write_bytes: " counter_format, __func__,
-            c2->tun_write_bytes);
-    }
-    else
-    {
-        msg(M_WARN, "%s: no VPN TX bytes provided in reply for peer %u",
-            __func__, id);
-    }
-}
-
-int
-dco_parse_peer_multi(struct nl_msg *msg, void *arg)
-{
-    struct nlattr *tb[OVPN_ATTR_MAX + 1];
-    struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
-
-    msg(D_DCO_DEBUG, "%s: parsing message...", __func__);
-
-    nla_parse(tb, OVPN_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
-              genlmsg_attrlen(gnlh, 0), NULL);
-
-    if (!tb[OVPN_ATTR_GET_PEER])
-    {
-        return NL_SKIP;
-    }
-
-    struct nlattr *tb_peer[OVPN_GET_PEER_RESP_ATTR_MAX + 1];
-
-    nla_parse(tb_peer, OVPN_GET_PEER_RESP_ATTR_MAX,
-              nla_data(tb[OVPN_ATTR_GET_PEER]),
-              nla_len(tb[OVPN_ATTR_GET_PEER]), NULL);
-
-    if (!tb_peer[OVPN_GET_PEER_RESP_ATTR_PEER_ID])
-    {
-        msg(M_WARN, "%s: no peer-id provided in reply", __func__);
-        return NL_SKIP;
-    }
-
-    struct multi_context *m = arg;
-    uint32_t peer_id = nla_get_u32(tb_peer[OVPN_GET_PEER_RESP_ATTR_PEER_ID]);
-
-    if (peer_id >= m->max_clients || !m->instances[peer_id])
-    {
-        msg(M_WARN, "%s: cannot store DCO stats for peer %u", __func__,
-            peer_id);
-        return NL_SKIP;
-    }
-
-    dco_update_peer_stat(&m->instances[peer_id]->context.c2, tb_peer, peer_id);
-
-    return NL_OK;
-}
-
-int
-dco_get_peer_stats_multi(dco_context_t *dco, struct multi_context *m)
-{
-    msg(D_DCO_DEBUG, "%s", __func__);
-
-    struct nl_msg *nl_msg = ovpn_dco_nlmsg_create(dco, OVPN_CMD_GET_PEER);
-
-    nlmsg_hdr(nl_msg)->nlmsg_flags |= NLM_F_DUMP;
-
-    int ret = ovpn_nl_msg_send(dco, nl_msg, dco_parse_peer_multi, m, __func__);
-
-    nlmsg_free(nl_msg);
-    return ret;
-}
-
-static int
-dco_parse_peer(struct nl_msg *msg, void *arg)
-{
-    struct context *c = arg;
-    struct nlattr *tb[OVPN_ATTR_MAX + 1];
-    struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
-
-    msg(D_DCO_DEBUG, "%s: parsing message...", __func__);
-
-    nla_parse(tb, OVPN_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
-              genlmsg_attrlen(gnlh, 0), NULL);
-
-    if (!tb[OVPN_ATTR_GET_PEER])
-    {
-        msg(D_DCO_DEBUG, "%s: malformed reply", __func__);
-        return NL_SKIP;
-    }
-
-    struct nlattr *tb_peer[OVPN_GET_PEER_RESP_ATTR_MAX + 1];
-
-    nla_parse(tb_peer, OVPN_GET_PEER_RESP_ATTR_MAX,
-              nla_data(tb[OVPN_ATTR_GET_PEER]),
-              nla_len(tb[OVPN_ATTR_GET_PEER]), NULL);
-
-    if (!tb_peer[OVPN_GET_PEER_RESP_ATTR_PEER_ID])
-    {
-        msg(M_WARN, "%s: no peer-id provided in reply", __func__);
-        return NL_SKIP;
-    }
-
-    uint32_t peer_id = nla_get_u32(tb_peer[OVPN_GET_PEER_RESP_ATTR_PEER_ID]);
-    if (c->c2.tls_multi->dco_peer_id != peer_id)
-    {
-        return NL_SKIP;
-    }
-
-    dco_update_peer_stat(&c->c2, tb_peer, peer_id);
-
-    return NL_OK;
-}
-
-int
-dco_get_peer_stats(struct context *c)
-{
-    uint32_t peer_id = c->c2.tls_multi->dco_peer_id;
-    msg(D_DCO_DEBUG, "%s: peer-id %d", __func__, peer_id);
-
-    if (!c->c1.tuntap)
-    {
-        return 0;
-    }
-
-    dco_context_t *dco = &c->c1.tuntap->dco;
-    struct nl_msg *nl_msg = ovpn_dco_nlmsg_create(dco, OVPN_CMD_GET_PEER);
-    struct nlattr *attr = nla_nest_start(nl_msg, OVPN_ATTR_GET_PEER);
-    int ret = -EMSGSIZE;
-
-    NLA_PUT_U32(nl_msg, OVPN_GET_PEER_ATTR_PEER_ID, peer_id);
-    nla_nest_end(nl_msg, attr);
-
-    ret = ovpn_nl_msg_send(dco, nl_msg, dco_parse_peer, c, __func__);
-
-nla_put_failure:
-    nlmsg_free(nl_msg);
-    return ret;
-}
-
 bool
-dco_available(int msglevel)
+dco_available(int msglevel, dco_context_t *dco)
 {
-    if (resolve_ovpn_netlink_id(D_DCO_DEBUG) < 0)
+    if (resolve_ovpn_netlink_id(D_DCO_DEBUG, dco) < 0)
     {
         msg(msglevel,
             "Note: Kernel support for ovpn-dco missing, disabling data channel offload.");
         return false;
     }
-
     return true;
 }
 
@@ -1043,6 +455,94 @@
     return BSTR(&out);
 }
 
+int
+dco_new_peer(dco_context_t *dco, unsigned int peerid, int sd,
+             struct sockaddr *localaddr, struct sockaddr *remoteaddr,
+             struct in_addr *remote_in4, struct in6_addr *remote_in6)
+{
+    int ret = dco->ops->dco_new_peer(dco, peerid, sd, localaddr, remoteaddr, remote_in4, remote_in6);
+    return ret;
+}
+
+void
+close_tun_dco(struct tuntap *tt, openvpn_net_ctx_t *ctx)
+{
+    tt->dco.ops->close_tun_dco(tt, ctx);
+}
+
+int
+dco_swap_keys(dco_context_t *dco, unsigned int peerid)
+{
+    int ret = dco->ops->dco_swap_keys(dco, peerid);
+    return ret;
+}
+
+
+int
+dco_del_peer(dco_context_t *dco, unsigned int peerid)
+{
+    int ret = dco->ops->dco_del_peer(dco, peerid);
+    return ret;
+}
+
+
+int
+dco_del_key(dco_context_t *dco, unsigned int peerid,
+            dco_key_slot_t slot)
+{
+    int ret = dco->ops->dco_del_key(dco, peerid, slot);
+    return ret;
+}
+
+int
+dco_new_key(dco_context_t *dco, unsigned int peerid, int keyid,
+            dco_key_slot_t slot,
+            const uint8_t *encrypt_key, const uint8_t *encrypt_iv,
+            const uint8_t *decrypt_key, const uint8_t *decrypt_iv,
+            const char *ciphername)
+{
+    int ret = dco->ops->dco_new_key(dco, peerid, keyid, slot, encrypt_key, encrypt_iv, decrypt_key, decrypt_iv, ciphername);
+    return ret;
+}
+
+int
+dco_set_peer(dco_context_t *dco, unsigned int peerid,
+             int keepalive_interval, int keepalive_timeout, int mss)
+{
+    int ret = dco->ops->dco_set_peer(dco, peerid, keepalive_interval, keepalive_timeout, mss);
+    return ret;
+}
+
+int
+dco_do_read(dco_context_t *dco)
+{
+    int ret = dco->ops->dco_do_read(dco);
+    return ret;
+}
+
+int
+dco_parse_peer_multi(struct nl_msg *msg, void *arg)
+{
+    struct multi_context *m = arg;
+    int ret = m->top.c1.tuntap->dco.ops->dco_parse_peer_multi(msg, arg);
+
+    return ret;
+}
+
+int
+dco_get_peer_stats_multi(dco_context_t *dco, struct multi_context *m)
+{
+    int ret = dco->ops->dco_get_peer_stats_multi(dco, m);
+    return ret;
+}
+
+int
+dco_get_peer_stats(struct context *c)
+{
+    int ret = c->c1.tuntap->dco.ops->dco_get_peer_stats(c);
+    return ret;
+}
+
 void
 dco_event_set(dco_context_t *dco, struct event_set *es, void *arg)
 {
diff --git a/src/openvpn/dco_linux.h b/src/openvpn/dco_linux.h
index 511519a..a3cf528 100644
--- a/src/openvpn/dco_linux.h
+++ b/src/openvpn/dco_linux.h
@@ -31,9 +31,30 @@
 #include <netlink/socket.h>
 #include <netlink/netlink.h>
 
+#include <netlink/genl/genl.h>
+#include <netlink/genl/family.h>
+#include <netlink/genl/ctrl.h>
+
+#include "networking.h"
+#include "fdmisc.h"
+
+/* forward declarations (including other headers leads to nasty include
+ * order problems)
+ */
+struct multi_context;
+struct context_2;
+struct key_ctx_bi;
+
 typedef enum ovpn_key_slot dco_key_slot_t;
 typedef enum ovpn_cipher_alg dco_cipher_t;
 
+/* libnl < 3.5.0 does not set the NLA_F_NESTED on its own, therefore we
+ * have to explicitly do it to prevent the kernel from failing upon
+ * parsing of the message
+ */
+#define nla_nest_start(_msg, _type) \
+    nla_nest_start(_msg, (_type) | NLA_F_NESTED)
+
 
 typedef struct
 {
@@ -53,7 +74,114 @@
     int dco_del_peer_reason;
     uint64_t dco_read_bytes;
     uint64_t dco_write_bytes;
+
+    const struct dco_ops *ops;
 } dco_context_t;
 
+struct dco_ops
+{
+    struct nl_msg *(*ovpn_dco_nlmsg_create)(dco_context_t *dco, int cmd);
+    int (*dco_new_peer)(dco_context_t *dco, unsigned int peerid, int sd, struct sockaddr *localaddr, struct sockaddr *remoteaddr, struct in_addr *remote_in4, struct in6_addr *remote_in6);
+    int (*open_tun_dco)(struct tuntap *tt, openvpn_net_ctx_t *ctx, const char *dev);
+    void (*close_tun_dco)(struct tuntap *tt, openvpn_net_ctx_t *ctx);
+    int (*dco_swap_keys)(dco_context_t *dco, unsigned int peerid);
+    int (*dco_del_peer)(dco_context_t *dco, unsigned int peerid);
+    int (*dco_del_key)(dco_context_t *dco, unsigned int peerid, dco_key_slot_t slot);
+    int (*dco_new_key)(dco_context_t *dco, unsigned int peerid, int keyid, dco_key_slot_t slot, const uint8_t *encrypt_key, const uint8_t *encrypt_iv, const uint8_t *decrypt_key, const uint8_t *decrypt_iv, const char *ciphername);
+    int (*dco_set_peer)(dco_context_t *dco, unsigned int peerid, int keepalive_interval, int keepalive_timeout, int mss);
+    int (*ovpn_handle_msg)(struct nl_msg *msg, void *arg);
+    int (*dco_do_read)(dco_context_t *dco);
+    void (*dco_update_peer_stat)(struct context_2 *c2, struct nlattr *tb[], uint32_t id);
+    int (*dco_parse_peer_multi)(struct nl_msg *msg, void *arg);
+    int (*dco_get_peer_stats_multi)(dco_context_t *dco, struct multi_context *m);
+    int (*dco_parse_peer)(struct nl_msg *msg, void *arg);
+    int (*dco_get_peer_stats)(struct context *c);
+};
+
+extern const struct dco_ops dco_ops_v2;
+
+int ovpn_get_mcast_id(dco_context_t *dco);
+
+void dco_check_key_ctx(const struct key_ctx_bi *key);
+
+typedef int (*ovpn_nl_cb)(struct nl_msg *msg, void *arg);
+
+/**
+ * @brief resolves the netlink ID for ovpn-dco
+ *
+ * This function queries the kernel via a netlink socket
+ * whether the ovpn-dco netlink namespace is available
+ *
+ * This function can be used to determine if the kernel
+ * supports DCO offloading.
+ *
+ * @return ID on success, negative error code on error
+ */
+int resolve_ovpn_netlink_id(int msglevel, dco_context_t *dco);
+
+int ovpn_nl_recvmsgs(dco_context_t *dco, const char *prefix);
+
+/**
+ * Send a prepared netlink message and registers cb as callback if non-null.
+ *
+ * The method will also free nl_msg
+ * @param dco       The dco context to use
+ * @param nl_msg    the message to use
+ * @param cb        An optional callback if the caller expects an answer
+ * @param cb_arg    An optional param to pass to the callback
+ * @param prefix    A prefix to report in the error message to give the user context
+ * @return          status of sending the message
+ */
+int ovpn_nl_msg_send(dco_context_t *dco, struct nl_msg *nl_msg, ovpn_nl_cb cb,
+                     void *cb_arg, const char *prefix);
+
+struct sockaddr *
+mapped_v4_to_v6(struct sockaddr *sock, struct gc_arena *gc);
+
+int ovpn_nl_cb_finish(struct nl_msg (*msg) __attribute__ ((unused)), void *arg);
+
+/* This function is used as error callback on the netlink socket.
+ * When something goes wrong and the kernel returns an error, this function is
+ * invoked.
+ *
+ * We pass the error code to the user by means of a variable pointed by *arg
+ * (supplied by the user when setting this callback) and we parse the kernel
+ * reply to see if it contains a human-readable error. If found, it is printed.
+ */
+int ovpn_nl_cb_error(struct sockaddr_nl (*nla) __attribute__ ((unused)),
+                     struct nlmsgerr *err, void *arg);
+
+void ovpn_dco_init_netlink(dco_context_t *dco);
+
+bool ovpn_dco_init(int mode, dco_context_t *dco);
+
+void ovpn_dco_uninit_netlink(dco_context_t *dco);
+
+void ovpn_dco_register(dco_context_t *dco);
+
+/* This function parses the reply provided by the kernel to the CTRL_CMD_GETFAMILY
+ * message. We parse the reply and we retrieve the multicast group ID associated
+ * with the "ovpn-dco" netlink family.
+ *
+ * The ID is later used to subscribe to the multicast group and be notified
+ * about any multicast message sent by the ovpn-dco kernel module.
+ */
+int mcast_family_handler(struct nl_msg *msg, void *arg);
+
+/**
+ * Lookup the multicast id for OpenVPN. This method and its help method currently
+ * hardcode the lookup to OVPN_NL_NAME and OVPN_NL_MULTICAST_GROUP_PEERS but
+ * extended in the future if we need to lookup more than one mcast id.
+ */
+int ovpn_get_mcast_id(dco_context_t *dco);
+
+bool dco_available(int msglevel, dco_context_t *dco);
+
+const char *dco_version_string(struct gc_arena *gc);
+
+void dco_event_set(dco_context_t *dco, struct event_set *es, void *arg);
+
+const char *dco_get_supported_ciphers();
+
 #endif /* defined(ENABLE_DCO) && defined(TARGET_LINUX) */
 #endif /* ifndef DCO_LINUX_H */
diff --git a/src/openvpn/dco_linux_v2.c b/src/openvpn/dco_linux_v2.c
new file mode 100644
index 0000000..6fc47fa
--- /dev/null
+++ b/src/openvpn/dco_linux_v2.c
@@ -0,0 +1,636 @@
+/*
+ *  Interface to linux dco networking code
+ *
+ *  Copyright (C) 2020-2024 Antonio Quartulli <a@unstable.cc>
+ *  Copyright (C) 2020-2024 Arne Schwabe <arne@rfc2549.org>
+ *  Copyright (C) 2020-2024 Gianmarco De Gregori <gianmarco@mandelbit.com>
+ *  Copyright (C) 2020-2024 OpenVPN Inc <sales@openvpn.net>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License version 2
+ *  as published by the Free Software Foundation.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program (see the file COPYING included with this
+ *  distribution); if not, write to the Free Software Foundation, Inc.,
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#if defined(ENABLE_DCO) && defined(TARGET_LINUX)
+
+#include "syshead.h"
+
+#include "dco_linux.h"
+#include "errlevel.h"
+#include "buffer.h"
+#include "networking.h"
+#include "openvpn.h"
+
+#include "socket.h"
+#include "tun.h"
+#include "ssl.h"
+#include "fdmisc.h"
+#include "multi.h"
+#include "ssl_verify.h"
+
+#include "ovpn_dco_linux.h"
+
+#include <netlink/socket.h>
+#include <netlink/netlink.h>
+#include <netlink/genl/genl.h>
+#include <netlink/genl/family.h>
+#include <netlink/genl/ctrl.h>
+
+int
+open_tun_dco_v2(struct tuntap *tt, openvpn_net_ctx_t *ctx, const char *dev)
+{
+    msg(D_DCO_DEBUG, "%s: %s", __func__, dev);
+    ASSERT(tt->type == DEV_TYPE_TUN);
+
+    int ret = net_iface_new(ctx, dev, "ovpn-dco", &tt->dco);
+    if (ret < 0)
+    {
+        msg(D_DCO_DEBUG, "Cannot create DCO interface %s: %d", dev, ret);
+        return ret;
+    }
+
+    tt->dco.ifindex = if_nametoindex(dev);
+    if (!tt->dco.ifindex)
+    {
+        msg(M_FATAL, "DCO: cannot retrieve ifindex for interface %s", dev);
+    }
+
+    tt->dco.dco_message_peer_id = -1;
+
+    ovpn_dco_register(&tt->dco);
+
+    return 0;
+}
+
+static struct nl_msg *
+ovpn_dco_v2_nlmsg_create(dco_context_t *dco, int cmd)
+{
+    enum ovpn_nl_commands enum_cmd = (enum ovpn_nl_commands)cmd;
+    struct nl_msg *nl_msg = nlmsg_alloc();
+    if (!nl_msg)
+    {
+        msg(M_ERR, "cannot allocate netlink message");
+        return NULL;
+    }
+
+    genlmsg_put(nl_msg, 0, 0, dco->ovpn_dco_id, 0, 0, enum_cmd, 0);
+    NLA_PUT_U32(nl_msg, OVPN_ATTR_IFINDEX, dco->ifindex);
+
+    return nl_msg;
+nla_put_failure:
+    nlmsg_free(nl_msg);
+    msg(M_INFO, "cannot put into netlink message");
+    return NULL;
+}
+
+int
+dco_v2_new_peer(dco_context_t *dco, unsigned int peerid, int sd,
+                struct sockaddr *localaddr, struct sockaddr *remoteaddr,
+                struct in_addr *remote_in4, struct in6_addr *remote_in6)
+{
+    struct gc_arena gc = gc_new();
+    const char *remotestr = "[undefined]";
+    if (remoteaddr)
+    {
+        remotestr = print_sockaddr(remoteaddr, &gc);
+    }
+    msg(D_DCO_DEBUG, "%s: peer-id %d, fd %d, remote addr: %s", __func__,
+        peerid, sd, remotestr);
+
+    struct nl_msg *nl_msg = ovpn_dco_v2_nlmsg_create(dco, OVPN_CMD_NEW_PEER);
+    struct nlattr *attr = nla_nest_start(nl_msg, OVPN_ATTR_NEW_PEER);
+    int ret = -EMSGSIZE;
+
+    NLA_PUT_U32(nl_msg, OVPN_NEW_PEER_ATTR_PEER_ID, peerid);
+    NLA_PUT_U32(nl_msg, OVPN_NEW_PEER_ATTR_SOCKET, sd);
+
+    /* Set the remote endpoint if defined (for UDP) */
+    if (remoteaddr)
+    {
+        remoteaddr = mapped_v4_to_v6(remoteaddr, &gc);
+        int alen = af_addr_size(remoteaddr->sa_family);
+
+        NLA_PUT(nl_msg, OVPN_NEW_PEER_ATTR_SOCKADDR_REMOTE, alen, remoteaddr);
+    }
+
+    if (localaddr)
+    {
+        localaddr = mapped_v4_to_v6(localaddr, &gc);
+        if (localaddr->sa_family == AF_INET)
+        {
+            NLA_PUT(nl_msg, OVPN_NEW_PEER_ATTR_LOCAL_IP, sizeof(struct in_addr),
+                    &((struct sockaddr_in *)localaddr)->sin_addr);
+        }
+        else if (localaddr->sa_family == AF_INET6)
+        {
+            NLA_PUT(nl_msg, OVPN_NEW_PEER_ATTR_LOCAL_IP, sizeof(struct in6_addr),
+                    &((struct sockaddr_in6 *)localaddr)->sin6_addr);
+        }
+    }
+
+    /* Set the primary VPN IP addresses of the peer */
+    if (remote_in4)
+    {
+        NLA_PUT_U32(nl_msg, OVPN_NEW_PEER_ATTR_IPV4, remote_in4->s_addr);
+    }
+    if (remote_in6)
+    {
+        NLA_PUT(nl_msg, OVPN_NEW_PEER_ATTR_IPV6, sizeof(struct in6_addr),
+                remote_in6);
+    }
+    nla_nest_end(nl_msg, attr);
+
+    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
+
+nla_put_failure:
+    nlmsg_free(nl_msg);
+    gc_free(&gc);
+    return ret;
+}
+
+void
+close_tun_dco_v2(struct tuntap *tt, openvpn_net_ctx_t *ctx)
+{
+    msg(D_DCO_DEBUG, __func__);
+
+    net_iface_del(ctx, tt->actual_name);
+    ovpn_dco_uninit_netlink(&tt->dco);
+}
+
+int
+dco_v2_swap_keys(dco_context_t *dco, unsigned int peerid)
+{
+    msg(D_DCO_DEBUG, "%s: peer-id %d", __func__, peerid);
+
+    struct nl_msg *nl_msg = ovpn_dco_v2_nlmsg_create(dco, OVPN_CMD_SWAP_KEYS);
+    if (!nl_msg)
+    {
+        return -ENOMEM;
+    }
+
+    struct nlattr *attr = nla_nest_start(nl_msg, OVPN_ATTR_SWAP_KEYS);
+    int ret = -EMSGSIZE;
+    NLA_PUT_U32(nl_msg, OVPN_SWAP_KEYS_ATTR_PEER_ID, peerid);
+    nla_nest_end(nl_msg, attr);
+
+    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
+
+nla_put_failure:
+    nlmsg_free(nl_msg);
+    return ret;
+}
+
+
+int
+dco_v2_del_peer(dco_context_t *dco, unsigned int peerid)
+{
+    msg(D_DCO_DEBUG, "%s: peer-id %d", __func__, peerid);
+
+    struct nl_msg *nl_msg = ovpn_dco_v2_nlmsg_create(dco, OVPN_CMD_DEL_PEER);
+    if (!nl_msg)
+    {
+        return -ENOMEM;
+    }
+
+    struct nlattr *attr = nla_nest_start(nl_msg, OVPN_ATTR_DEL_PEER);
+    int ret = -EMSGSIZE;
+    NLA_PUT_U32(nl_msg, OVPN_DEL_PEER_ATTR_PEER_ID, peerid);
+    nla_nest_end(nl_msg, attr);
+
+    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
+
+nla_put_failure:
+    nlmsg_free(nl_msg);
+    return ret;
+}
+
+
+int
+dco_v2_del_key(dco_context_t *dco, unsigned int peerid,
+               dco_key_slot_t slot)
+{
+    msg(D_DCO_DEBUG, "%s: peer-id %d, slot %d", __func__, peerid, slot);
+
+    struct nl_msg *nl_msg = ovpn_dco_v2_nlmsg_create(dco, OVPN_CMD_DEL_KEY);
+    if (!nl_msg)
+    {
+        return -ENOMEM;
+    }
+
+    struct nlattr *attr = nla_nest_start(nl_msg, OVPN_ATTR_DEL_KEY);
+    int ret = -EMSGSIZE;
+    NLA_PUT_U32(nl_msg, OVPN_DEL_KEY_ATTR_PEER_ID, peerid);
+    NLA_PUT_U8(nl_msg, OVPN_DEL_KEY_ATTR_KEY_SLOT, slot);
+    nla_nest_end(nl_msg, attr);
+
+    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
+
+nla_put_failure:
+    nlmsg_free(nl_msg);
+    return ret;
+}
+
+int
+dco_v2_new_key(dco_context_t *dco, unsigned int peerid, int keyid,
+               dco_key_slot_t slot,
+               const uint8_t *encrypt_key, const uint8_t *encrypt_iv,
+               const uint8_t *decrypt_key, const uint8_t *decrypt_iv,
+               const char *ciphername)
+{
+    msg(D_DCO_DEBUG, "%s: slot %d, key-id %d, peer-id %d, cipher %s",
+        __func__, slot, keyid, peerid, ciphername);
+
+    const size_t key_len = cipher_kt_key_size(ciphername);
+    const int nonce_tail_len = 8;
+
+    struct nl_msg *nl_msg = ovpn_dco_v2_nlmsg_create(dco, OVPN_CMD_NEW_KEY);
+    if (!nl_msg)
+    {
+        return -ENOMEM;
+    }
+
+    dco_cipher_t dco_cipher = dco_get_cipher(ciphername);
+
+    int ret = -EMSGSIZE;
+    struct nlattr *attr = nla_nest_start(nl_msg, OVPN_ATTR_NEW_KEY);
+    NLA_PUT_U32(nl_msg, OVPN_NEW_KEY_ATTR_PEER_ID, peerid);
+    NLA_PUT_U8(nl_msg, OVPN_NEW_KEY_ATTR_KEY_SLOT, slot);
+    NLA_PUT_U8(nl_msg, OVPN_NEW_KEY_ATTR_KEY_ID, keyid);
+    NLA_PUT_U16(nl_msg, OVPN_NEW_KEY_ATTR_CIPHER_ALG, dco_cipher);
+
+    struct nlattr *key_enc = nla_nest_start(nl_msg,
+                                            OVPN_NEW_KEY_ATTR_ENCRYPT_KEY);
+    if (dco_cipher != OVPN_CIPHER_ALG_NONE)
+    {
+        NLA_PUT(nl_msg, OVPN_KEY_DIR_ATTR_CIPHER_KEY, key_len, encrypt_key);
+        NLA_PUT(nl_msg, OVPN_KEY_DIR_ATTR_NONCE_TAIL, nonce_tail_len,
+                encrypt_iv);
+    }
+    nla_nest_end(nl_msg, key_enc);
+
+    struct nlattr *key_dec = nla_nest_start(nl_msg,
+                                            OVPN_NEW_KEY_ATTR_DECRYPT_KEY);
+    if (dco_cipher != OVPN_CIPHER_ALG_NONE)
+    {
+        NLA_PUT(nl_msg, OVPN_KEY_DIR_ATTR_CIPHER_KEY, key_len, decrypt_key);
+        NLA_PUT(nl_msg, OVPN_KEY_DIR_ATTR_NONCE_TAIL, nonce_tail_len,
+                decrypt_iv);
+    }
+    nla_nest_end(nl_msg, key_dec);
+
+    nla_nest_end(nl_msg, attr);
+
+    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
+
+nla_put_failure:
+    nlmsg_free(nl_msg);
+    return ret;
+}
+
+int
+dco_v2_set_peer(dco_context_t *dco, unsigned int peerid,
+                int keepalive_interval, int keepalive_timeout, int mss)
+{
+    msg(D_DCO_DEBUG, "%s: peer-id %d, keepalive %d/%d, mss %d", __func__,
+        peerid, keepalive_interval, keepalive_timeout, mss);
+
+    struct nl_msg *nl_msg = ovpn_dco_v2_nlmsg_create(dco, OVPN_CMD_SET_PEER);
+    if (!nl_msg)
+    {
+        return -ENOMEM;
+    }
+
+    struct nlattr *attr = nla_nest_start(nl_msg, OVPN_ATTR_SET_PEER);
+    int ret = -EMSGSIZE;
+    NLA_PUT_U32(nl_msg, OVPN_SET_PEER_ATTR_PEER_ID, peerid);
+    NLA_PUT_U32(nl_msg, OVPN_SET_PEER_ATTR_KEEPALIVE_INTERVAL,
+                keepalive_interval);
+    NLA_PUT_U32(nl_msg, OVPN_SET_PEER_ATTR_KEEPALIVE_TIMEOUT,
+                keepalive_timeout);
+    nla_nest_end(nl_msg, attr);
+
+    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
+
+nla_put_failure:
+    nlmsg_free(nl_msg);
+    return ret;
+}
+
+/* This function parses any netlink message sent by ovpn-dco to userspace */
+static int
+ovpn_v2_handle_msg(struct nl_msg *msg, void *arg)
+{
+    dco_context_t *dco = arg;
+
+    struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
+    struct nlattr *attrs[OVPN_ATTR_MAX + 1];
+    struct nlmsghdr *nlh = nlmsg_hdr(msg);
+
+    if (!genlmsg_valid_hdr(nlh, 0))
+    {
+        msg(D_DCO, "ovpn-dco: invalid header");
+        return NL_SKIP;
+    }
+
+    if (nla_parse(attrs, OVPN_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
+                  genlmsg_attrlen(gnlh, 0), NULL))
+    {
+        msg(D_DCO, "received bogus data from ovpn-dco");
+        return NL_SKIP;
+    }
+
+    /* we must know which interface this message is referring to in order to
+     * avoid mixing messages for other instances
+     */
+    if (!attrs[OVPN_ATTR_IFINDEX])
+    {
+        msg(D_DCO, "ovpn-dco: Received message without ifindex");
+        return NL_SKIP;
+    }
+
+    uint32_t ifindex = nla_get_u32(attrs[OVPN_ATTR_IFINDEX]);
+    if (ifindex != dco->ifindex)
+    {
+        msg(D_DCO_DEBUG,
+            "ovpn-dco: ignoring message (type=%d) for foreign ifindex %d",
+            gnlh->cmd, ifindex);
+        return NL_SKIP;
+    }
+
+    /* based on the message type, we parse the subobject contained in the
+     * message, that stores the type-specific attributes.
+     *
+     * the "dco" object is then filled accordingly with the information
+     * retrieved from the message, so that the rest of the OpenVPN code can
+     * react as need be.
+     */
+    switch (gnlh->cmd)
+    {
+        case OVPN_CMD_DEL_PEER:
+        {
+            if (!attrs[OVPN_ATTR_DEL_PEER])
+            {
+                msg(D_DCO, "ovpn-dco: no attributes in OVPN_DEL_PEER message");
+                return NL_SKIP;
+            }
+
+            struct nlattr *dp_attrs[OVPN_DEL_PEER_ATTR_MAX + 1];
+            if (nla_parse_nested(dp_attrs, OVPN_DEL_PEER_ATTR_MAX,
+                                 attrs[OVPN_ATTR_DEL_PEER], NULL))
+            {
+                msg(D_DCO, "received bogus del peer packet data from ovpn-dco");
+                return NL_SKIP;
+            }
+
+            if (!dp_attrs[OVPN_DEL_PEER_ATTR_REASON])
+            {
+                msg(D_DCO, "ovpn-dco: no reason in DEL_PEER message");
+                return NL_SKIP;
+            }
+            if (!dp_attrs[OVPN_DEL_PEER_ATTR_PEER_ID])
+            {
+                msg(D_DCO, "ovpn-dco: no peer-id in DEL_PEER message");
+                return NL_SKIP;
+            }
+            int reason = nla_get_u8(dp_attrs[OVPN_DEL_PEER_ATTR_REASON]);
+            unsigned int peerid = nla_get_u32(dp_attrs[OVPN_DEL_PEER_ATTR_PEER_ID]);
+
+            msg(D_DCO_DEBUG, "ovpn-dco: received CMD_DEL_PEER, ifindex: %d, peer-id %d, reason: %d",
+                ifindex, peerid, reason);
+            dco->dco_message_peer_id = peerid;
+            dco->dco_del_peer_reason = reason;
+            dco->dco_message_type = OVPN_CMD_DEL_PEER;
+
+            break;
+        }
+
+        default:
+            msg(D_DCO, "ovpn-dco: received unknown command: %d", gnlh->cmd);
+            dco->dco_message_type = 0;
+            return NL_SKIP;
+    }
+
+    return NL_OK;
+}
+
+static int
+dco_v2_do_read(dco_context_t *dco)
+{
+    msg(D_DCO_DEBUG, __func__);
+    nl_cb_set(dco->nl_cb, NL_CB_VALID, NL_CB_CUSTOM, ovpn_v2_handle_msg, dco);
+
+    return ovpn_nl_recvmsgs(dco, __func__);
+}
+
+static void
+dco_v2_update_peer_stat(struct context_2 *c2, struct nlattr *tb[], uint32_t id)
+{
+    if (tb[OVPN_GET_PEER_RESP_ATTR_LINK_RX_BYTES])
+    {
+        c2->dco_read_bytes = nla_get_u64(tb[OVPN_GET_PEER_RESP_ATTR_LINK_RX_BYTES]);
+        msg(D_DCO_DEBUG, "%s / dco_read_bytes: " counter_format, __func__,
+            c2->dco_read_bytes);
+    }
+    else
+    {
+        msg(M_WARN, "%s: no link RX bytes provided in reply for peer %u",
+            __func__, id);
+    }
+
+    if (tb[OVPN_GET_PEER_RESP_ATTR_LINK_TX_BYTES])
+    {
+        c2->dco_write_bytes = nla_get_u64(tb[OVPN_GET_PEER_RESP_ATTR_LINK_TX_BYTES]);
+        msg(D_DCO_DEBUG, "%s / dco_write_bytes: " counter_format, __func__,
+            c2->dco_write_bytes);
+    }
+    else
+    {
+        msg(M_WARN, "%s: no link TX bytes provided in reply for peer %u",
+            __func__, id);
+    }
+
+    if (tb[OVPN_GET_PEER_RESP_ATTR_VPN_RX_BYTES])
+    {
+        c2->tun_read_bytes = nla_get_u64(tb[OVPN_GET_PEER_RESP_ATTR_VPN_RX_BYTES]);
+        msg(D_DCO_DEBUG, "%s / tun_read_bytes: " counter_format, __func__,
+            c2->tun_read_bytes);
+    }
+    else
+    {
+        msg(M_WARN, "%s: no VPN RX bytes provided in reply for peer %u",
+            __func__, id);
+    }
+
+    if (tb[OVPN_GET_PEER_RESP_ATTR_VPN_TX_BYTES])
+    {
+        c2->tun_write_bytes = nla_get_u64(tb[OVPN_GET_PEER_RESP_ATTR_VPN_TX_BYTES]);
+        msg(D_DCO_DEBUG, "%s / tun_write_bytes: " counter_format, __func__,
+            c2->tun_write_bytes);
+    }
+    else
+    {
+        msg(M_WARN, "%s: no VPN TX bytes provided in reply for peer %u",
+            __func__, id);
+    }
+}
+
+int
+dco_v2_parse_peer_multi(struct nl_msg *msg, void *arg)
+{
+    struct nlattr *tb[OVPN_ATTR_MAX + 1];
+    struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
+
+    msg(D_DCO_DEBUG, "%s: parsing message...", __func__);
+
+    nla_parse(tb, OVPN_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
+              genlmsg_attrlen(gnlh, 0), NULL);
+
+    if (!tb[OVPN_ATTR_GET_PEER])
+    {
+        return NL_SKIP;
+    }
+
+    struct nlattr *tb_peer[OVPN_GET_PEER_RESP_ATTR_MAX + 1];
+
+    nla_parse(tb_peer, OVPN_GET_PEER_RESP_ATTR_MAX,
+              nla_data(tb[OVPN_ATTR_GET_PEER]),
+              nla_len(tb[OVPN_ATTR_GET_PEER]), NULL);
+
+    if (!tb_peer[OVPN_GET_PEER_RESP_ATTR_PEER_ID])
+    {
+        msg(M_WARN, "%s: no peer-id provided in reply", __func__);
+        return NL_SKIP;
+    }
+
+    struct multi_context *m = arg;
+    uint32_t peer_id = nla_get_u32(tb_peer[OVPN_GET_PEER_RESP_ATTR_PEER_ID]);
+
+    if (peer_id >= m->max_clients || !m->instances[peer_id])
+    {
+        msg(M_WARN, "%s: cannot store DCO stats for peer %u", __func__,
+            peer_id);
+        return NL_SKIP;
+    }
+
+    dco_v2_update_peer_stat(&m->instances[peer_id]->context.c2, tb_peer, peer_id);
+
+    return NL_OK;
+}
+
+int
+dco_v2_get_peer_stats_multi(dco_context_t *dco, struct multi_context *m)
+{
+    msg(D_DCO_DEBUG, "%s", __func__);
+
+    struct nl_msg *nl_msg = ovpn_dco_v2_nlmsg_create(dco, OVPN_CMD_GET_PEER);
+
+    nlmsg_hdr(nl_msg)->nlmsg_flags |= NLM_F_DUMP;
+
+    int ret = ovpn_nl_msg_send(dco, nl_msg, dco_v2_parse_peer_multi, m, __func__);
+
+    nlmsg_free(nl_msg);
+    return ret;
+}
+
+static int
+dco_v2_parse_peer(struct nl_msg *msg, void *arg)
+{
+    struct context *c = arg;
+    struct nlattr *tb[OVPN_ATTR_MAX + 1];
+    struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
+
+    msg(D_DCO_DEBUG, "%s: parsing message...", __func__);
+
+    nla_parse(tb, OVPN_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
+              genlmsg_attrlen(gnlh, 0), NULL);
+
+    if (!tb[OVPN_ATTR_GET_PEER])
+    {
+        msg(D_DCO_DEBUG, "%s: malformed reply", __func__);
+        return NL_SKIP;
+    }
+
+    struct nlattr *tb_peer[OVPN_GET_PEER_RESP_ATTR_MAX + 1];
+
+    nla_parse(tb_peer, OVPN_GET_PEER_RESP_ATTR_MAX,
+              nla_data(tb[OVPN_ATTR_GET_PEER]),
+              nla_len(tb[OVPN_ATTR_GET_PEER]), NULL);
+
+    if (!tb_peer[OVPN_GET_PEER_RESP_ATTR_PEER_ID])
+    {
+        msg(M_WARN, "%s: no peer-id provided in reply", __func__);
+        return NL_SKIP;
+    }
+
+    uint32_t peer_id = nla_get_u32(tb_peer[OVPN_GET_PEER_RESP_ATTR_PEER_ID]);
+    if (c->c2.tls_multi->dco_peer_id != peer_id)
+    {
+        return NL_SKIP;
+    }
+
+    dco_v2_update_peer_stat(&c->c2, tb_peer, peer_id);
+
+    return NL_OK;
+}
+
+int
+dco_v2_get_peer_stats(struct context *c)
+{
+    uint32_t peer_id = c->c2.tls_multi->dco_peer_id;
+    msg(D_DCO_DEBUG, "%s: peer-id %d", __func__, peer_id);
+
+    if (!c->c1.tuntap)
+    {
+        return 0;
+    }
+
+    dco_context_t *dco = &c->c1.tuntap->dco;
+    struct nl_msg *nl_msg = ovpn_dco_v2_nlmsg_create(dco, OVPN_CMD_GET_PEER);
+    struct nlattr *attr = nla_nest_start(nl_msg, OVPN_ATTR_GET_PEER);
+    int ret = -EMSGSIZE;
+
+    NLA_PUT_U32(nl_msg, OVPN_GET_PEER_ATTR_PEER_ID, peer_id);
+    nla_nest_end(nl_msg, attr);
+
+    ret = ovpn_nl_msg_send(dco, nl_msg, dco_v2_parse_peer, c, __func__);
+
+nla_put_failure:
+    nlmsg_free(nl_msg);
+    return ret;
+}
+
+const struct dco_ops dco_ops_v2 = {
+    .ovpn_dco_nlmsg_create = ovpn_dco_v2_nlmsg_create,
+    .dco_new_peer = &dco_v2_new_peer,
+    .open_tun_dco = open_tun_dco_v2,
+    .close_tun_dco = close_tun_dco_v2,
+    .dco_swap_keys = &dco_v2_swap_keys,
+    .dco_del_peer = &dco_v2_del_peer,
+    .dco_del_key = &dco_v2_del_key,
+    .dco_new_key = &dco_v2_new_key,
+    .dco_set_peer = &dco_v2_set_peer,
+    .ovpn_handle_msg = &ovpn_v2_handle_msg,
+    .dco_do_read = &dco_v2_do_read,
+    .dco_update_peer_stat = &dco_v2_update_peer_stat,
+    .dco_parse_peer_multi = &dco_v2_parse_peer_multi,
+    .dco_get_peer_stats_multi = &dco_v2_get_peer_stats_multi,
+    .dco_parse_peer = &dco_v2_parse_peer,
+    .dco_get_peer_stats = &dco_v2_get_peer_stats
+};
+
+#endif /* defined(ENABLE_DCO) && defined(TARGET_LINUX) */
diff --git a/src/openvpn/dco_win.c b/src/openvpn/dco_win.c
index e3ada76..65be766 100644
--- a/src/openvpn/dco_win.c
+++ b/src/openvpn/dco_win.c
@@ -357,7 +357,7 @@
 }
 
 bool
-dco_available(int msglevel)
+dco_available(int msglevel, dco_context_t *dco)
 {
     /* try to open device by symbolic name */
     HANDLE h = CreateFile("\\\\.\\ovpn-dco", GENERIC_READ | GENERIC_WRITE,
diff --git a/src/openvpn/forward.c b/src/openvpn/forward.c
index 01165b2..118c32a 100644
--- a/src/openvpn/forward.c
+++ b/src/openvpn/forward.c
@@ -1230,6 +1230,9 @@
 process_incoming_dco(struct context *c)
 {
 #if defined(ENABLE_DCO) && (defined(TARGET_LINUX) || defined(TARGET_FREEBSD))
+    int cmd_del_peer = OVPN_CMD_DEL_PEER;
+    int cmd_swap_keys = OVPN_CMD_SWAP_KEYS;
+
     dco_context_t *dco = &c->c1.tuntap->dco;
 
     dco_do_read(dco);
@@ -1245,31 +1248,30 @@
         return;
     }
 
-    switch (dco->dco_message_type)
+    if (dco->dco_message_type == cmd_del_peer)
     {
-        case OVPN_CMD_DEL_PEER:
-            if (dco->dco_del_peer_reason == OVPN_DEL_PEER_REASON_EXPIRED)
-            {
-                msg(D_DCO_DEBUG, "%s: received peer expired notification of for peer-id "
-                    "%d", __func__, dco->dco_message_peer_id);
-                trigger_ping_timeout_signal(c);
-                return;
-            }
-            break;
-
-        case OVPN_CMD_SWAP_KEYS:
-            msg(D_DCO_DEBUG, "%s: received key rotation notification for peer-id %d",
-                __func__, dco->dco_message_peer_id);
-            tls_session_soft_reset(c->c2.tls_multi);
-            break;
-
-        default:
-            msg(D_DCO_DEBUG, "%s: received message of type %u - ignoring", __func__,
-                dco->dco_message_type);
+        if (dco->dco_del_peer_reason == OVPN_DEL_PEER_REASON_EXPIRED)
+        {
+            msg(D_DCO_DEBUG, "%s: received peer expired notification of for peer-id "
+                "%d", __func__, dco->dco_message_peer_id);
+            trigger_ping_timeout_signal(c);
             return;
+        }
+    }
+    else if (dco->dco_message_type == cmd_swap_keys)
+    {
+        msg(D_DCO_DEBUG, "%s: received key rotation notification for peer-id %d",
+            __func__, dco->dco_message_peer_id);
+        tls_session_soft_reset(c->c2.tls_multi);
+    }
+    else
+    {
+        msg(D_DCO_DEBUG, "%s: received message of type %u - ignoring", __func__,
+            dco->dco_message_type);
+        return;
     }
 
-#endif /* if defined(ENABLE_DCO) && (defined(TARGET_LINUX) || defined(TARGET_FREEBSD)) */
+#endif /* #if defined(ENABLE_DCO) && defined(TARGET_FREEBSD) */
 }
 
 /*
diff --git a/src/openvpn/ovpn_dco_linux.h b/src/openvpn/ovpn_dco_linux.h
index 73e19b5..4d198db 100644
--- a/src/openvpn/ovpn_dco_linux.h
+++ b/src/openvpn/ovpn_dco_linux.h
@@ -4,14 +4,15 @@
  *
  *  Copyright (C) 2019-2023 OpenVPN, Inc.
  *
- *  Author:	James Yonan <james@openvpn.net>
- *		Antonio Quartulli <antonio@openvpn.net>
+ *  Author:     James Yonan <james@openvpn.net>
+ *              Antonio Quartulli <antonio@openvpn.net>
  */
 
 #ifndef _UAPI_LINUX_OVPN_DCO_H_
 #define _UAPI_LINUX_OVPN_DCO_H_
 
 #define OVPN_NL_NAME "ovpn-dco-v2"
+#define OVPN_FAMILY_NAME        "ovpn"
 
 #define OVPN_NL_MULTICAST_GROUP_PEERS "peers"
 
@@ -19,238 +20,238 @@
  * enum ovpn_nl_commands - supported netlink commands
  */
 enum ovpn_nl_commands {
-	/**
-	 * @OVPN_CMD_UNSPEC: unspecified command to catch errors
-	 */
-	OVPN_CMD_UNSPEC = 0,
+    /**
+     * @OVPN_CMD_UNSPEC: unspecified command to catch errors
+     */
+    OVPN_CMD_UNSPEC = 0,
 
-	/**
-	 * @OVPN_CMD_NEW_PEER: Configure peer with its crypto keys
-	 */
-	OVPN_CMD_NEW_PEER,
+    /**
+     * @OVPN_CMD_NEW_PEER: Configure peer with its crypto keys
+     */
+    OVPN_CMD_NEW_PEER,
 
-	/**
-	 * @OVPN_CMD_SET_PEER: Tweak parameters for an existing peer
-	 */
-	OVPN_CMD_SET_PEER,
+    /**
+     * @OVPN_CMD_SET_PEER: Tweak parameters for an existing peer
+     */
+    OVPN_CMD_SET_PEER,
 
-	/**
-	 * @OVPN_CMD_DEL_PEER: Remove peer from internal table
-	 */
-	OVPN_CMD_DEL_PEER,
+    /**
+     * @OVPN_CMD_DEL_PEER: Remove peer from internal table
+     */
+    OVPN_CMD_DEL_PEER,
 
-	OVPN_CMD_NEW_KEY,
+    OVPN_CMD_NEW_KEY,
 
-	OVPN_CMD_SWAP_KEYS,
+    OVPN_CMD_SWAP_KEYS,
 
-	OVPN_CMD_DEL_KEY,
+    OVPN_CMD_DEL_KEY,
 
-	/**
-	 * @OVPN_CMD_GET_PEER: Retrieve the status of a peer or all peers
-	 */
-	OVPN_CMD_GET_PEER,
+    /**
+     * @OVPN_CMD_GET_PEER: Retrieve the status of a peer or all peers
+     */
+    OVPN_CMD_GET_PEER,
 };
 
 enum ovpn_cipher_alg {
-	/**
-	 * @OVPN_CIPHER_ALG_NONE: No encryption - reserved for debugging only
-	 */
-	OVPN_CIPHER_ALG_NONE = 0,
-	/**
-	 * @OVPN_CIPHER_ALG_AES_GCM: AES-GCM AEAD cipher with any allowed key size
-	 */
-	OVPN_CIPHER_ALG_AES_GCM,
-	/**
-	 * @OVPN_CIPHER_ALG_CHACHA20_POLY1305: ChaCha20Poly1305 AEAD cipher
-	 */
-	OVPN_CIPHER_ALG_CHACHA20_POLY1305,
+/**
+ * @OVPN_CIPHER_ALG_NONE: No encryption - reserved for debugging only
+ */
+    OVPN_CIPHER_ALG_NONE = 0,
+/**
+ * @OVPN_CIPHER_ALG_AES_GCM: AES-GCM AEAD cipher with any allowed key size
+ */
+    OVPN_CIPHER_ALG_AES_GCM,
+/**
+ * @OVPN_CIPHER_ALG_CHACHA20_POLY1305: ChaCha20Poly1305 AEAD cipher
+ */
+    OVPN_CIPHER_ALG_CHACHA20_POLY1305,
 };
 
 enum ovpn_del_peer_reason {
-	__OVPN_DEL_PEER_REASON_FIRST,
-	OVPN_DEL_PEER_REASON_TEARDOWN = __OVPN_DEL_PEER_REASON_FIRST,
-	OVPN_DEL_PEER_REASON_USERSPACE,
-	OVPN_DEL_PEER_REASON_EXPIRED,
-	OVPN_DEL_PEER_REASON_TRANSPORT_ERROR,
-	OVPN_DEL_PEER_REASON_TRANSPORT_DISCONNECT,
-	__OVPN_DEL_PEER_REASON_AFTER_LAST
+    __OVPN_DEL_PEER_REASON_FIRST,
+    OVPN_DEL_PEER_REASON_TEARDOWN = __OVPN_DEL_PEER_REASON_FIRST,
+    OVPN_DEL_PEER_REASON_USERSPACE,
+    OVPN_DEL_PEER_REASON_EXPIRED,
+    OVPN_DEL_PEER_REASON_TRANSPORT_ERROR,
+    OVPN_DEL_PEER_REASON_TRANSPORT_DISCONNECT,
+    __OVPN_DEL_PEER_REASON_AFTER_LAST
 };
 
 enum ovpn_key_slot {
-	__OVPN_KEY_SLOT_FIRST,
-	OVPN_KEY_SLOT_PRIMARY = __OVPN_KEY_SLOT_FIRST,
-	OVPN_KEY_SLOT_SECONDARY,
-	__OVPN_KEY_SLOT_AFTER_LAST,
+    __OVPN_KEY_SLOT_FIRST,
+    OVPN_KEY_SLOT_PRIMARY = __OVPN_KEY_SLOT_FIRST,
+    OVPN_KEY_SLOT_SECONDARY,
+    __OVPN_KEY_SLOT_AFTER_LAST,
 };
 
-enum ovpn_netlink_attrs {
-	OVPN_ATTR_UNSPEC = 0,
-	OVPN_ATTR_IFINDEX,
-	OVPN_ATTR_NEW_PEER,
-	OVPN_ATTR_SET_PEER,
-	OVPN_ATTR_DEL_PEER,
-	OVPN_ATTR_NEW_KEY,
-	OVPN_ATTR_SWAP_KEYS,
-	OVPN_ATTR_DEL_KEY,
-	OVPN_ATTR_GET_PEER,
+enum ovpn_v2_netlink_attrs {
+    OVPN_ATTR_UNSPEC = 0,
+    OVPN_ATTR_IFINDEX,
+    OVPN_ATTR_NEW_PEER,
+    OVPN_ATTR_SET_PEER,
+    OVPN_ATTR_DEL_PEER,
+    OVPN_ATTR_NEW_KEY,
+    OVPN_ATTR_SWAP_KEYS,
+    OVPN_ATTR_DEL_KEY,
+    OVPN_ATTR_GET_PEER,
 
-	__OVPN_ATTR_AFTER_LAST,
-	OVPN_ATTR_MAX = __OVPN_ATTR_AFTER_LAST - 1,
+    __OVPN_ATTR_AFTER_LAST,
+    OVPN_ATTR_MAX = __OVPN_ATTR_AFTER_LAST - 1,
 };
 
 enum ovpn_netlink_key_dir_attrs {
-	OVPN_KEY_DIR_ATTR_UNSPEC = 0,
-	OVPN_KEY_DIR_ATTR_CIPHER_KEY,
-	OVPN_KEY_DIR_ATTR_NONCE_TAIL,
+    OVPN_KEY_DIR_ATTR_UNSPEC = 0,
+    OVPN_KEY_DIR_ATTR_CIPHER_KEY,
+    OVPN_KEY_DIR_ATTR_NONCE_TAIL,
 
-	__OVPN_KEY_DIR_ATTR_AFTER_LAST,
-	OVPN_KEY_DIR_ATTR_MAX = __OVPN_KEY_DIR_ATTR_AFTER_LAST - 1,
+    __OVPN_KEY_DIR_ATTR_AFTER_LAST,
+    OVPN_KEY_DIR_ATTR_MAX = __OVPN_KEY_DIR_ATTR_AFTER_LAST - 1,
 };
 
 enum ovpn_netlink_new_key_attrs {
-	OVPN_NEW_KEY_ATTR_UNSPEC = 0,
-	OVPN_NEW_KEY_ATTR_PEER_ID,
-	OVPN_NEW_KEY_ATTR_KEY_SLOT,
-	OVPN_NEW_KEY_ATTR_KEY_ID,
-	OVPN_NEW_KEY_ATTR_CIPHER_ALG,
-	OVPN_NEW_KEY_ATTR_ENCRYPT_KEY,
-	OVPN_NEW_KEY_ATTR_DECRYPT_KEY,
+    OVPN_NEW_KEY_ATTR_UNSPEC = 0,
+    OVPN_NEW_KEY_ATTR_PEER_ID,
+    OVPN_NEW_KEY_ATTR_KEY_SLOT,
+    OVPN_NEW_KEY_ATTR_KEY_ID,
+    OVPN_NEW_KEY_ATTR_CIPHER_ALG,
+    OVPN_NEW_KEY_ATTR_ENCRYPT_KEY,
+    OVPN_NEW_KEY_ATTR_DECRYPT_KEY,
 
-	__OVPN_NEW_KEY_ATTR_AFTER_LAST,
-	OVPN_NEW_KEY_ATTR_MAX = __OVPN_NEW_KEY_ATTR_AFTER_LAST - 1,
+    __OVPN_NEW_KEY_ATTR_AFTER_LAST,
+    OVPN_NEW_KEY_ATTR_MAX = __OVPN_NEW_KEY_ATTR_AFTER_LAST - 1,
 };
 
 enum ovpn_netlink_del_key_attrs {
-	OVPN_DEL_KEY_ATTR_UNSPEC = 0,
-	OVPN_DEL_KEY_ATTR_PEER_ID,
-	OVPN_DEL_KEY_ATTR_KEY_SLOT,
+    OVPN_DEL_KEY_ATTR_UNSPEC = 0,
+    OVPN_DEL_KEY_ATTR_PEER_ID,
+    OVPN_DEL_KEY_ATTR_KEY_SLOT,
 
-	__OVPN_DEL_KEY_ATTR_AFTER_LAST,
-	OVPN_DEL_KEY_ATTR_MAX = __OVPN_DEL_KEY_ATTR_AFTER_LAST - 1,
+    __OVPN_DEL_KEY_ATTR_AFTER_LAST,
+    OVPN_DEL_KEY_ATTR_MAX = __OVPN_DEL_KEY_ATTR_AFTER_LAST - 1,
 };
 
 enum ovpn_netlink_swap_keys_attrs {
-	OVPN_SWAP_KEYS_ATTR_UNSPEC = 0,
-	OVPN_SWAP_KEYS_ATTR_PEER_ID,
+    OVPN_SWAP_KEYS_ATTR_UNSPEC = 0,
+    OVPN_SWAP_KEYS_ATTR_PEER_ID,
 
-	__OVPN_SWAP_KEYS_ATTR_AFTER_LAST,
-	OVPN_SWAP_KEYS_ATTR_MAX = __OVPN_SWAP_KEYS_ATTR_AFTER_LAST - 1,
+    __OVPN_SWAP_KEYS_ATTR_AFTER_LAST,
+    OVPN_SWAP_KEYS_ATTR_MAX = __OVPN_SWAP_KEYS_ATTR_AFTER_LAST - 1,
 
 };
 
 enum ovpn_netlink_new_peer_attrs {
-	OVPN_NEW_PEER_ATTR_UNSPEC = 0,
-	OVPN_NEW_PEER_ATTR_PEER_ID,
-	OVPN_NEW_PEER_ATTR_SOCKADDR_REMOTE,
-	OVPN_NEW_PEER_ATTR_SOCKET,
-	OVPN_NEW_PEER_ATTR_IPV4,
-	OVPN_NEW_PEER_ATTR_IPV6,
-	OVPN_NEW_PEER_ATTR_LOCAL_IP,
+    OVPN_NEW_PEER_ATTR_UNSPEC = 0,
+    OVPN_NEW_PEER_ATTR_PEER_ID,
+    OVPN_NEW_PEER_ATTR_SOCKADDR_REMOTE,
+    OVPN_NEW_PEER_ATTR_SOCKET,
+    OVPN_NEW_PEER_ATTR_IPV4,
+    OVPN_NEW_PEER_ATTR_IPV6,
+    OVPN_NEW_PEER_ATTR_LOCAL_IP,
 
-	__OVPN_NEW_PEER_ATTR_AFTER_LAST,
-	OVPN_NEW_PEER_ATTR_MAX = __OVPN_NEW_PEER_ATTR_AFTER_LAST - 1,
+    __OVPN_NEW_PEER_ATTR_AFTER_LAST,
+    OVPN_NEW_PEER_ATTR_MAX = __OVPN_NEW_PEER_ATTR_AFTER_LAST - 1,
 };
 
 enum ovpn_netlink_set_peer_attrs {
-	OVPN_SET_PEER_ATTR_UNSPEC = 0,
-	OVPN_SET_PEER_ATTR_PEER_ID,
-	OVPN_SET_PEER_ATTR_KEEPALIVE_INTERVAL,
-	OVPN_SET_PEER_ATTR_KEEPALIVE_TIMEOUT,
+    OVPN_SET_PEER_ATTR_UNSPEC = 0,
+    OVPN_SET_PEER_ATTR_PEER_ID,
+    OVPN_SET_PEER_ATTR_KEEPALIVE_INTERVAL,
+    OVPN_SET_PEER_ATTR_KEEPALIVE_TIMEOUT,
 
-	__OVPN_SET_PEER_ATTR_AFTER_LAST,
-	OVPN_SET_PEER_ATTR_MAX = __OVPN_SET_PEER_ATTR_AFTER_LAST - 1,
+    __OVPN_SET_PEER_ATTR_AFTER_LAST,
+    OVPN_SET_PEER_ATTR_MAX = __OVPN_SET_PEER_ATTR_AFTER_LAST - 1,
 };
 
 enum ovpn_netlink_del_peer_attrs {
-	OVPN_DEL_PEER_ATTR_UNSPEC = 0,
-	OVPN_DEL_PEER_ATTR_REASON,
-	OVPN_DEL_PEER_ATTR_PEER_ID,
+    OVPN_DEL_PEER_ATTR_UNSPEC = 0,
+    OVPN_DEL_PEER_ATTR_REASON,
+    OVPN_DEL_PEER_ATTR_PEER_ID,
 
-	__OVPN_DEL_PEER_ATTR_AFTER_LAST,
-	OVPN_DEL_PEER_ATTR_MAX = __OVPN_DEL_PEER_ATTR_AFTER_LAST - 1,
+    __OVPN_DEL_PEER_ATTR_AFTER_LAST,
+    OVPN_DEL_PEER_ATTR_MAX = __OVPN_DEL_PEER_ATTR_AFTER_LAST - 1,
 };
 
 enum ovpn_netlink_get_peer_attrs {
-	OVPN_GET_PEER_ATTR_UNSPEC = 0,
-	OVPN_GET_PEER_ATTR_PEER_ID,
+    OVPN_GET_PEER_ATTR_UNSPEC = 0,
+    OVPN_GET_PEER_ATTR_PEER_ID,
 
-	__OVPN_GET_PEER_ATTR_AFTER_LAST,
-	OVPN_GET_PEER_ATTR_MAX = __OVPN_GET_PEER_ATTR_AFTER_LAST - 1,
+    __OVPN_GET_PEER_ATTR_AFTER_LAST,
+    OVPN_GET_PEER_ATTR_MAX = __OVPN_GET_PEER_ATTR_AFTER_LAST - 1,
 };
 
 enum ovpn_netlink_get_peer_response_attrs {
-	OVPN_GET_PEER_RESP_ATTR_UNSPEC = 0,
-	OVPN_GET_PEER_RESP_ATTR_PEER_ID,
-	OVPN_GET_PEER_RESP_ATTR_SOCKADDR_REMOTE,
-	OVPN_GET_PEER_RESP_ATTR_IPV4,
-	OVPN_GET_PEER_RESP_ATTR_IPV6,
-	OVPN_GET_PEER_RESP_ATTR_LOCAL_IP,
-	OVPN_GET_PEER_RESP_ATTR_LOCAL_PORT,
-	OVPN_GET_PEER_RESP_ATTR_KEEPALIVE_INTERVAL,
-	OVPN_GET_PEER_RESP_ATTR_KEEPALIVE_TIMEOUT,
-	OVPN_GET_PEER_RESP_ATTR_VPN_RX_BYTES,
-	OVPN_GET_PEER_RESP_ATTR_VPN_TX_BYTES,
-	OVPN_GET_PEER_RESP_ATTR_VPN_RX_PACKETS,
-	OVPN_GET_PEER_RESP_ATTR_VPN_TX_PACKETS,
-	OVPN_GET_PEER_RESP_ATTR_LINK_RX_BYTES,
-	OVPN_GET_PEER_RESP_ATTR_LINK_TX_BYTES,
-	OVPN_GET_PEER_RESP_ATTR_LINK_RX_PACKETS,
-	OVPN_GET_PEER_RESP_ATTR_LINK_TX_PACKETS,
+    OVPN_GET_PEER_RESP_ATTR_UNSPEC = 0,
+    OVPN_GET_PEER_RESP_ATTR_PEER_ID,
+    OVPN_GET_PEER_RESP_ATTR_SOCKADDR_REMOTE,
+    OVPN_GET_PEER_RESP_ATTR_IPV4,
+    OVPN_GET_PEER_RESP_ATTR_IPV6,
+    OVPN_GET_PEER_RESP_ATTR_LOCAL_IP,
+    OVPN_GET_PEER_RESP_ATTR_LOCAL_PORT,
+    OVPN_GET_PEER_RESP_ATTR_KEEPALIVE_INTERVAL,
+    OVPN_GET_PEER_RESP_ATTR_KEEPALIVE_TIMEOUT,
+    OVPN_GET_PEER_RESP_ATTR_VPN_RX_BYTES,
+    OVPN_GET_PEER_RESP_ATTR_VPN_TX_BYTES,
+    OVPN_GET_PEER_RESP_ATTR_VPN_RX_PACKETS,
+    OVPN_GET_PEER_RESP_ATTR_VPN_TX_PACKETS,
+    OVPN_GET_PEER_RESP_ATTR_LINK_RX_BYTES,
+    OVPN_GET_PEER_RESP_ATTR_LINK_TX_BYTES,
+    OVPN_GET_PEER_RESP_ATTR_LINK_RX_PACKETS,
+    OVPN_GET_PEER_RESP_ATTR_LINK_TX_PACKETS,
 
-	__OVPN_GET_PEER_RESP_ATTR_AFTER_LAST,
-	OVPN_GET_PEER_RESP_ATTR_MAX = __OVPN_GET_PEER_RESP_ATTR_AFTER_LAST - 1,
+    __OVPN_GET_PEER_RESP_ATTR_AFTER_LAST,
+    OVPN_GET_PEER_RESP_ATTR_MAX = __OVPN_GET_PEER_RESP_ATTR_AFTER_LAST - 1,
 };
 
 enum ovpn_netlink_peer_stats_attrs {
-	OVPN_PEER_STATS_ATTR_UNSPEC = 0,
-	OVPN_PEER_STATS_BYTES,
-	OVPN_PEER_STATS_PACKETS,
+    OVPN_PEER_STATS_ATTR_UNSPEC = 0,
+    OVPN_PEER_STATS_BYTES,
+    OVPN_PEER_STATS_PACKETS,
 
-	__OVPN_PEER_STATS_ATTR_AFTER_LAST,
-	OVPN_PEER_STATS_ATTR_MAX = __OVPN_PEER_STATS_ATTR_AFTER_LAST - 1,
+    __OVPN_PEER_STATS_ATTR_AFTER_LAST,
+    OVPN_PEER_STATS_ATTR_MAX = __OVPN_PEER_STATS_ATTR_AFTER_LAST - 1,
 };
 
 enum ovpn_netlink_peer_attrs {
-	OVPN_PEER_ATTR_UNSPEC = 0,
-	OVPN_PEER_ATTR_PEER_ID,
-	OVPN_PEER_ATTR_SOCKADDR_REMOTE,
-	OVPN_PEER_ATTR_IPV4,
-	OVPN_PEER_ATTR_IPV6,
-	OVPN_PEER_ATTR_LOCAL_IP,
-	OVPN_PEER_ATTR_KEEPALIVE_INTERVAL,
-	OVPN_PEER_ATTR_KEEPALIVE_TIMEOUT,
-	OVPN_PEER_ATTR_ENCRYPT_KEY,
-	OVPN_PEER_ATTR_DECRYPT_KEY,
-	OVPN_PEER_ATTR_RX_STATS,
-	OVPN_PEER_ATTR_TX_STATS,
+    OVPN_PEER_ATTR_UNSPEC = 0,
+    OVPN_PEER_ATTR_PEER_ID,
+    OVPN_PEER_ATTR_SOCKADDR_REMOTE,
+    OVPN_PEER_ATTR_IPV4,
+    OVPN_PEER_ATTR_IPV6,
+    OVPN_PEER_ATTR_LOCAL_IP,
+    OVPN_PEER_ATTR_KEEPALIVE_INTERVAL,
+    OVPN_PEER_ATTR_KEEPALIVE_TIMEOUT,
+    OVPN_PEER_ATTR_ENCRYPT_KEY,
+    OVPN_PEER_ATTR_DECRYPT_KEY,
+    OVPN_PEER_ATTR_RX_STATS,
+    OVPN_PEER_ATTR_TX_STATS,
 
-	__OVPN_PEER_ATTR_AFTER_LAST,
-	OVPN_PEER_ATTR_MAX = __OVPN_PEER_ATTR_AFTER_LAST - 1,
+    __OVPN_PEER_ATTR_AFTER_LAST,
+    OVPN_PEER_ATTR_MAX = __OVPN_PEER_ATTR_AFTER_LAST - 1,
 };
 
 enum ovpn_netlink_packet_attrs {
-	OVPN_PACKET_ATTR_UNSPEC = 0,
-	OVPN_PACKET_ATTR_PACKET,
-	OVPN_PACKET_ATTR_PEER_ID,
+    OVPN_PACKET_ATTR_UNSPEC = 0,
+    OVPN_PACKET_ATTR_PACKET,
+    OVPN_PACKET_ATTR_PEER_ID,
 
-	__OVPN_PACKET_ATTR_AFTER_LAST,
-	OVPN_PACKET_ATTR_MAX = __OVPN_PACKET_ATTR_AFTER_LAST - 1,
+    __OVPN_PACKET_ATTR_AFTER_LAST,
+    OVPN_PACKET_ATTR_MAX = __OVPN_PACKET_ATTR_AFTER_LAST - 1,
 };
 
 enum ovpn_ifla_attrs {
-	IFLA_OVPN_UNSPEC = 0,
-	IFLA_OVPN_MODE,
+    IFLA_OVPN_UNSPEC = 0,
+    IFLA_OVPN_MODE,
 
-	__IFLA_OVPN_AFTER_LAST,
-	IFLA_OVPN_MAX = __IFLA_OVPN_AFTER_LAST - 1,
+    __IFLA_OVPN_AFTER_LAST,
+    IFLA_OVPN_MAX = __IFLA_OVPN_AFTER_LAST - 1,
 };
 
 enum ovpn_mode {
-	__OVPN_MODE_FIRST = 0,
-	OVPN_MODE_P2P = __OVPN_MODE_FIRST,
-	OVPN_MODE_MP,
+    __OVPN_MODE_FIRST = 0,
+    OVPN_MODE_P2P = __OVPN_MODE_FIRST,
+    OVPN_MODE_MP,
 
-	__OVPN_MODE_AFTER_LAST,
+    __OVPN_MODE_AFTER_LAST,
 };
 
 #endif /* _UAPI_LINUX_OVPN_DCO_H_ */
