[Openvpn-devel,XL] Change in openvpn[master]: Lay the foundation to support both old and new versions of DCO

Message ID b1417790ce4af7783107a29902e8244335918280-HTML@gerrit.openvpn.net
State New
Headers show
Series [Openvpn-devel,XL] Change in openvpn[master]: Lay the foundation to support both old and new versions of DCO | expand

Commit Message

selvanair (Code Review) June 6, 2024, 11:33 a.m. UTC
Attention is currently required from: flichtenheld, plaisthos.

Hello plaisthos, flichtenheld,

I'd like you to do a code review.
Please visit

    http://gerrit.openvpn.net/c/openvpn/+/662?usp=email

to review the following change.


Change subject: Lay the foundation to support both old and new versions of DCO
......................................................................

Lay the foundation to support both old and new versions of DCO

Added a new abstraction layer based on the `struct dco_ops`,
a structure of function pointers that ensures the appropriate
APIs at runtime are used for DCO communication between kernel
and user space, selection is based on the available module's
family name.

The old version, referred to as "ovpn_dco_v2", and its related
commands and functions, will be distinguished with the "v2" suffix.

Change-Id: I6662605b60d4859949970cfe256c55cbcbd4a672
Signed-off-by: Gianmarco De Gregori <gianmarco@mandelbit.com>
---
M CMakeLists.txt
M src/openvpn/Makefile.am
M src/openvpn/dco.c
M src/openvpn/dco.h
M src/openvpn/dco_freebsd.c
M src/openvpn/dco_linux.c
M src/openvpn/dco_linux.h
A src/openvpn/dco_linux_v2.c
M src/openvpn/dco_win.c
M src/openvpn/forward.c
10 files changed, 923 insertions(+), 655 deletions(-)



  git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/62/662/1

Patch

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) */
 }
 
 /*