[Openvpn-devel] dco-linux: implement dco_get_peer_stats{, multi} API

Message ID 20230320232034.14828-1-a@unstable.cc
State Changes Requested
Headers show
Series [Openvpn-devel] dco-linux: implement dco_get_peer_stats{, multi} API | expand

Commit Message

Antonio Quartulli March 20, 2023, 11:20 p.m. UTC
With this API it is possible to retrieve the stats for a specific peer
or for all peers and then update the userspace counters with the value
reported by DCO.

Change-Id: Ia3990b86b1be7ca844fb1674b39ce0d60528ccff
Signed-off-by: Antonio Quartulli <a@unstable.cc>
---

Pleas, use the latest ovpn-dco master branch!

 src/openvpn/dco_linux.c      | 194 ++++++++++++++++++++++++++++++++---
 src/openvpn/ovpn_dco_linux.h |  14 ++-
 2 files changed, 190 insertions(+), 18 deletions(-)

Comments

Lev Stipakov March 21, 2023, 9:52 a.m. UTC | #1
NAK.

When running with --disable-dco:

Program received signal SIGUSR2, User defined signal 2.
0x00007ffff7aee967 in __GI___poll (fds=0x555555668ca0, nfds=2,
timeout=10000) at ../sysdeps/unix/sysv/linux/poll.c:29
29      ../sysdeps/unix/sysv/linux/poll.c: No such file or directory.
(gdb) c
Continuing.
2023-03-21 11:52:25 us=550135 event_wait : Interrupted system call
(fd=-1,code=4)

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7be46a0 in nl_cb_set () from /lib/x86_64-linux-gnu/libnl-3.so.200
(gdb) bt
#0  0x00007ffff7be46a0 in nl_cb_set () from /lib/x86_64-linux-gnu/libnl-3.so.200
#1  0x000055555557047a in ovpn_nl_msg_send
(dco=dco@entry=0x555555656348, nl_msg=0x55555566d1b0,
cb=cb@entry=0x55555556fde0 <dco_parse_peer_multi>,
cb_arg=cb_arg@entry=0x7fffffffc100,
    prefix=prefix@entry=0x5555555f4050 <__func__.33438>
"dco_get_peer_stats_multi") at dco_linux.c:182
#2  0x00005555555715ec in dco_get_peer_stats_multi
(dco=dco@entry=0x555555656348, m=m@entry=0x7fffffffc100) at
dco_linux.c:939
#3  0x0000555555597c63 in multi_print_status
(m=m@entry=0x7fffffffc100, so=so@entry=0x55555564dc90, version=1) at
multi.c:852
#4  0x000055555559c75e in multi_print_status (version=<optimized out>,
so=0x55555564dc90, m=0x7fffffffc100) at multi.c:3848
#5  multi_process_signal (m=m@entry=0x7fffffffc100) at multi.c:3848
#6  0x000055555559537f in tunnel_server_udp (top=0x7fffffffd3d0) at mudp.c:506
#7  0x00005555555a1369 in openvpn_main (argc=4, argv=0x7fffffffe638)
at openvpn.c:319
#8  0x00007ffff7a00083 in __libc_start_main (main=0x555555561730
<main>, argc=4, argv=0x7fffffffe638, init=<optimized out>,
fini=<optimized out>, rtld_fini=<optimized out>,
stack_end=0x7fffffffe628)
    at ../csu/libc-start.c:308
#9  0x000055555556176e in _start ()


ti 21. maalisk. 2023 klo 1.22 Antonio Quartulli (a@unstable.cc) kirjoitti:
>
> With this API it is possible to retrieve the stats for a specific peer
> or for all peers and then update the userspace counters with the value
> reported by DCO.
>
> Change-Id: Ia3990b86b1be7ca844fb1674b39ce0d60528ccff
> Signed-off-by: Antonio Quartulli <a@unstable.cc>
> ---
>
> Pleas, use the latest ovpn-dco master branch!
>
>  src/openvpn/dco_linux.c      | 194 ++++++++++++++++++++++++++++++++---
>  src/openvpn/ovpn_dco_linux.h |  14 ++-
>  2 files changed, 190 insertions(+), 18 deletions(-)
>
> diff --git a/src/openvpn/dco_linux.c b/src/openvpn/dco_linux.c
> index 47961849..1f18fa81 100644
> --- a/src/openvpn/dco_linux.c
> +++ b/src/openvpn/dco_linux.c
> @@ -41,6 +41,7 @@
>  #include "tun.h"
>  #include "ssl.h"
>  #include "fdmisc.h"
> +#include "multi.h"
>  #include "ssl_verify.h"
>
>  #include "ovpn_dco_linux.h"
> @@ -168,16 +169,17 @@ ovpn_nl_recvmsgs(dco_context_t *dco, const char *prefix)
>   * @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
>  ovpn_nl_msg_send(dco_context_t *dco, struct nl_msg *nl_msg, ovpn_nl_cb cb,
> -                 const char *prefix)
> +                 void *cb_arg, const char *prefix)
>  {
>      dco->status = 1;
>
> -    nl_cb_set(dco->nl_cb, NL_CB_VALID, NL_CB_CUSTOM, cb, dco);
> +    nl_cb_set(dco->nl_cb, NL_CB_VALID, NL_CB_CUSTOM, cb, cb_arg);
>      nl_send_auto(dco->nl_sock, nl_msg);
>
>      while (dco->status == 1)
> @@ -268,7 +270,7 @@ dco_new_peer(dco_context_t *dco, unsigned int peerid, int sd,
>      }
>      nla_nest_end(nl_msg, attr);
>
> -    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, __func__);
> +    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
>
>  nla_put_failure:
>      nlmsg_free(nl_msg);
> @@ -489,7 +491,7 @@ dco_swap_keys(dco_context_t *dco, unsigned int peerid)
>      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, __func__);
> +    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
>
>  nla_put_failure:
>      nlmsg_free(nl_msg);
> @@ -513,7 +515,7 @@ dco_del_peer(dco_context_t *dco, unsigned int peerid)
>      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, __func__);
> +    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
>
>  nla_put_failure:
>      nlmsg_free(nl_msg);
> @@ -539,7 +541,7 @@ dco_del_key(dco_context_t *dco, unsigned int 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, __func__);
> +    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
>
>  nla_put_failure:
>      nlmsg_free(nl_msg);
> @@ -596,7 +598,7 @@ dco_new_key(dco_context_t *dco, unsigned int peerid, int keyid,
>
>      nla_nest_end(nl_msg, attr);
>
> -    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, __func__);
> +    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
>
>  nla_put_failure:
>      nlmsg_free(nl_msg);
> @@ -625,7 +627,7 @@ dco_set_peer(dco_context_t *dco, unsigned int peerid,
>                  keepalive_timeout);
>      nla_nest_end(nl_msg, attr);
>
> -    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, __func__);
> +    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
>
>  nla_put_failure:
>      nlmsg_free(nl_msg);
> @@ -706,7 +708,7 @@ ovpn_get_mcast_id(dco_context_t *dco)
>      int ret = -EMSGSIZE;
>      NLA_PUT_STRING(nl_msg, CTRL_ATTR_FAMILY_NAME, OVPN_NL_NAME);
>
> -    ret = ovpn_nl_msg_send(dco, nl_msg, mcast_family_handler, __func__);
> +    ret = ovpn_nl_msg_send(dco, nl_msg, mcast_family_handler, dco, __func__);
>
>  nla_put_failure:
>      nlmsg_free(nl_msg);
> @@ -819,18 +821,184 @@ dco_do_read(dco_context_t *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: %lu", __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: %lu", __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: %lu", __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: %lu", __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;
> +    }
> +
> +    uint32_t peer_id = nla_get_u32(tb_peer[OVPN_GET_PEER_RESP_ATTR_PEER_ID]);
> +    struct multi_context *m = arg;
> +    struct hash_iterator hi;
> +    hash_iterator_init(m->hash, &hi);
> +
> +    struct hash_element *he;
> +    while ((he = hash_iterator_next(&hi)))
> +    {
> +        struct multi_instance *mi = (struct multi_instance *)he->value;
> +
> +        if (mi->context.c2.tls_multi->peer_id != peer_id)
> +        {
> +            continue;
> +        }
> +
> +        dco_update_peer_stat(&mi->context.c2, tb_peer, peer_id);
> +
> +        return NL_OK;
> +    }
> +
> +    msg(D_DCO_DEBUG, "%s: peer %d returned by kernel, but not found locally",
> +        __func__, peer_id);
> +
> +    return NL_SKIP;
> +}
> +
>  int
>  dco_get_peer_stats_multi(dco_context_t *dco, struct multi_context *m)
>  {
> -    /* Not implemented. */
> -    return 0;
> +    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;
> +
> +    return ovpn_nl_msg_send(dco, nl_msg, dco_parse_peer_multi, m, __func__);
> +}
> +
> +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)
>  {
> -    /* Not implemented. */
> -    return 0;
> +    uint32_t peer_id = c->c2.tls_multi->dco_peer_id;
> +    msg(D_DCO_DEBUG, "%s: peer-id %d", __func__, peer_id);
> +
> +    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
> diff --git a/src/openvpn/ovpn_dco_linux.h b/src/openvpn/ovpn_dco_linux.h
> index d3fd9a89..73e19b59 100644
> --- a/src/openvpn/ovpn_dco_linux.h
> +++ b/src/openvpn/ovpn_dco_linux.h
> @@ -2,7 +2,7 @@
>  /*
>   *  OpenVPN data channel accelerator
>   *
> - *  Copyright (C) 2019-2022 OpenVPN, Inc.
> + *  Copyright (C) 2019-2023 OpenVPN, Inc.
>   *
>   *  Author:    James Yonan <james@openvpn.net>
>   *             Antonio Quartulli <antonio@openvpn.net>
> @@ -188,10 +188,14 @@ enum ovpn_netlink_get_peer_response_attrs {
>         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_RX_BYTES,
> -       OVPN_GET_PEER_RESP_ATTR_TX_BYTES,
> -       OVPN_GET_PEER_RESP_ATTR_RX_PACKETS,
> -       OVPN_GET_PEER_RESP_ATTR_TX_PACKETS,
> +       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,
> --
> 2.39.2
>
>
>
> _______________________________________________
> Openvpn-devel mailing list
> Openvpn-devel@lists.sourceforge.net
> https://lists.sourceforge.net/lists/listinfo/openvpn-devel

Patch

diff --git a/src/openvpn/dco_linux.c b/src/openvpn/dco_linux.c
index 47961849..1f18fa81 100644
--- a/src/openvpn/dco_linux.c
+++ b/src/openvpn/dco_linux.c
@@ -41,6 +41,7 @@ 
 #include "tun.h"
 #include "ssl.h"
 #include "fdmisc.h"
+#include "multi.h"
 #include "ssl_verify.h"
 
 #include "ovpn_dco_linux.h"
@@ -168,16 +169,17 @@  ovpn_nl_recvmsgs(dco_context_t *dco, const char *prefix)
  * @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
 ovpn_nl_msg_send(dco_context_t *dco, struct nl_msg *nl_msg, ovpn_nl_cb cb,
-                 const char *prefix)
+                 void *cb_arg, const char *prefix)
 {
     dco->status = 1;
 
-    nl_cb_set(dco->nl_cb, NL_CB_VALID, NL_CB_CUSTOM, cb, dco);
+    nl_cb_set(dco->nl_cb, NL_CB_VALID, NL_CB_CUSTOM, cb, cb_arg);
     nl_send_auto(dco->nl_sock, nl_msg);
 
     while (dco->status == 1)
@@ -268,7 +270,7 @@  dco_new_peer(dco_context_t *dco, unsigned int peerid, int sd,
     }
     nla_nest_end(nl_msg, attr);
 
-    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, __func__);
+    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
 
 nla_put_failure:
     nlmsg_free(nl_msg);
@@ -489,7 +491,7 @@  dco_swap_keys(dco_context_t *dco, unsigned int peerid)
     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, __func__);
+    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
 
 nla_put_failure:
     nlmsg_free(nl_msg);
@@ -513,7 +515,7 @@  dco_del_peer(dco_context_t *dco, unsigned int peerid)
     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, __func__);
+    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
 
 nla_put_failure:
     nlmsg_free(nl_msg);
@@ -539,7 +541,7 @@  dco_del_key(dco_context_t *dco, unsigned int 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, __func__);
+    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
 
 nla_put_failure:
     nlmsg_free(nl_msg);
@@ -596,7 +598,7 @@  dco_new_key(dco_context_t *dco, unsigned int peerid, int keyid,
 
     nla_nest_end(nl_msg, attr);
 
-    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, __func__);
+    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
 
 nla_put_failure:
     nlmsg_free(nl_msg);
@@ -625,7 +627,7 @@  dco_set_peer(dco_context_t *dco, unsigned int peerid,
                 keepalive_timeout);
     nla_nest_end(nl_msg, attr);
 
-    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, __func__);
+    ret = ovpn_nl_msg_send(dco, nl_msg, NULL, NULL, __func__);
 
 nla_put_failure:
     nlmsg_free(nl_msg);
@@ -706,7 +708,7 @@  ovpn_get_mcast_id(dco_context_t *dco)
     int ret = -EMSGSIZE;
     NLA_PUT_STRING(nl_msg, CTRL_ATTR_FAMILY_NAME, OVPN_NL_NAME);
 
-    ret = ovpn_nl_msg_send(dco, nl_msg, mcast_family_handler, __func__);
+    ret = ovpn_nl_msg_send(dco, nl_msg, mcast_family_handler, dco, __func__);
 
 nla_put_failure:
     nlmsg_free(nl_msg);
@@ -819,18 +821,184 @@  dco_do_read(dco_context_t *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: %lu", __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: %lu", __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: %lu", __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: %lu", __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;
+    }
+
+    uint32_t peer_id = nla_get_u32(tb_peer[OVPN_GET_PEER_RESP_ATTR_PEER_ID]);
+    struct multi_context *m = arg;
+    struct hash_iterator hi;
+    hash_iterator_init(m->hash, &hi);
+
+    struct hash_element *he;
+    while ((he = hash_iterator_next(&hi)))
+    {
+        struct multi_instance *mi = (struct multi_instance *)he->value;
+
+        if (mi->context.c2.tls_multi->peer_id != peer_id)
+        {
+            continue;
+        }
+
+        dco_update_peer_stat(&mi->context.c2, tb_peer, peer_id);
+
+        return NL_OK;
+    }
+
+    msg(D_DCO_DEBUG, "%s: peer %d returned by kernel, but not found locally",
+        __func__, peer_id);
+
+    return NL_SKIP;
+}
+
 int
 dco_get_peer_stats_multi(dco_context_t *dco, struct multi_context *m)
 {
-    /* Not implemented. */
-    return 0;
+    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;
+
+    return ovpn_nl_msg_send(dco, nl_msg, dco_parse_peer_multi, m, __func__);
+}
+
+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)
 {
-    /* Not implemented. */
-    return 0;
+    uint32_t peer_id = c->c2.tls_multi->dco_peer_id;
+    msg(D_DCO_DEBUG, "%s: peer-id %d", __func__, peer_id);
+
+    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
diff --git a/src/openvpn/ovpn_dco_linux.h b/src/openvpn/ovpn_dco_linux.h
index d3fd9a89..73e19b59 100644
--- a/src/openvpn/ovpn_dco_linux.h
+++ b/src/openvpn/ovpn_dco_linux.h
@@ -2,7 +2,7 @@ 
 /*
  *  OpenVPN data channel accelerator
  *
- *  Copyright (C) 2019-2022 OpenVPN, Inc.
+ *  Copyright (C) 2019-2023 OpenVPN, Inc.
  *
  *  Author:	James Yonan <james@openvpn.net>
  *		Antonio Quartulli <antonio@openvpn.net>
@@ -188,10 +188,14 @@  enum ovpn_netlink_get_peer_response_attrs {
 	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_RX_BYTES,
-	OVPN_GET_PEER_RESP_ATTR_TX_BYTES,
-	OVPN_GET_PEER_RESP_ATTR_RX_PACKETS,
-	OVPN_GET_PEER_RESP_ATTR_TX_PACKETS,
+	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,