Browse code

dco-win: add support for multipeer stats

Use the new driver API to fetch per-peer link and VPN byte counters
in both client and server modes.

Two usage modes are supported:

- Single peer: pass the peer ID and a fixed-size output buffer. If the
IOCTL is not supported (old driver), fall back to the legacy API.

- All peers: first call the IOCTL with a small output buffer to get
the required size, then allocate a buffer and call again to fetch
stats for all peers.

Change-Id: I525d7300e49f9a5a18e7146ee35ccc2af8184b8a
Signed-off-by: Lev Stipakov <lev@openvpn.net>
Acked-by: Gert Doering <gert@greenie.muc.de>
Message-Id: <20250902122542.31023-1-gert@greenie.muc.de>
URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg32744.html
Signed-off-by: Gert Doering <gert@greenie.muc.de>

Lev Stipakov authored on 2025/09/02 21:25:36
Showing 3 changed files
... ...
@@ -30,6 +30,7 @@
30 30
 #include "forward.h"
31 31
 #include "tun.h"
32 32
 #include "crypto.h"
33
+#include "multi.h"
33 34
 #include "ssl_common.h"
34 35
 #include "openvpn.h"
35 36
 
... ...
@@ -190,6 +191,8 @@ ovpn_dco_init(struct context *c)
190 190
 {
191 191
     dco_context_t *dco = &c->c1.tuntap->dco;
192 192
 
193
+    dco->c = c;
194
+
193 195
     switch (c->mode)
194 196
     {
195 197
         case MODE_POINT_TO_POINT:
... ...
@@ -714,12 +717,132 @@ dco_do_read(dco_context_t *dco)
714 714
 int
715 715
 dco_get_peer_stats_multi(dco_context_t *dco, const bool raise_sigusr1_on_err)
716 716
 {
717
-    /* Not implemented. */
718
-    return 0;
717
+    struct gc_arena gc = gc_new();
718
+
719
+    int ret = 0;
720
+    struct tuntap *tt = dco->tt;
721
+
722
+    if (!tuntap_defined(tt))
723
+    {
724
+        ret = -1;
725
+        goto done;
726
+    }
727
+
728
+    OVPN_GET_PEER_STATS ps = {
729
+        .PeerId = -1
730
+    };
731
+
732
+    DWORD required_size = 0, bytes_returned = 0;
733
+    /* first, figure out buffer size */
734
+    if (!DeviceIoControl(tt->hand, OVPN_IOCTL_GET_PEER_STATS, &ps, sizeof(ps), &required_size, sizeof(DWORD), &bytes_returned, NULL))
735
+    {
736
+        if (GetLastError() == ERROR_MORE_DATA)
737
+        {
738
+            if (bytes_returned != sizeof(DWORD))
739
+            {
740
+                msg(M_WARN, "%s: invalid bytes returned for size query (%lu, expected %zu)", __func__, bytes_returned, sizeof(DWORD));
741
+                ret = -1;
742
+                goto done;
743
+            }
744
+            /* required_size now contains the size written by the driver */
745
+            if (required_size == 0)
746
+            {
747
+                ret = 0; /* no peers to process */
748
+                goto done;
749
+            }
750
+            if (required_size < sizeof(OVPN_PEER_STATS))
751
+            {
752
+                msg(M_WARN, "%s: invalid required size %lu (minimum %zu)", __func__, required_size, sizeof(OVPN_PEER_STATS));
753
+                ret = -1;
754
+                goto done;
755
+            }
756
+        }
757
+        else
758
+        {
759
+            msg(M_WARN | M_ERRNO, "%s: failed to fetch required buffer size", __func__);
760
+            ret = -1;
761
+            goto done;
762
+        }
763
+    }
764
+    else
765
+    {
766
+        /* unexpected success? */
767
+        if (bytes_returned == 0)
768
+        {
769
+            ret = 0; /* no peers to process */
770
+            goto done;
771
+        }
772
+
773
+        msg(M_WARN, "%s: first DeviceIoControl call succeeded unexpectedly (%lu bytes returned)", __func__, bytes_returned);
774
+        ret = -1;
775
+        goto done;
776
+    }
777
+
778
+
779
+    /* allocate the buffer and fetch stats */
780
+    OVPN_PEER_STATS *peer_stats = gc_malloc(required_size, true, &gc);
781
+    if (!peer_stats)
782
+    {
783
+        msg(M_WARN, "%s: failed to allocate buffer of size %lu", __func__, required_size);
784
+        ret = -1;
785
+        goto done;
786
+    }
787
+
788
+    if (!DeviceIoControl(tt->hand, OVPN_IOCTL_GET_PEER_STATS, &ps, sizeof(ps), peer_stats, required_size, &bytes_returned, NULL))
789
+    {
790
+        /* unlikely case when a peer has been added since fetching buffer size, not an error! */
791
+        if (GetLastError() == ERROR_MORE_DATA)
792
+        {
793
+            msg(M_WARN, "%s: peer has been added, skip fetching stats", __func__);
794
+            ret = 0;
795
+            goto done;
796
+        }
797
+
798
+        msg(M_WARN | M_ERRNO, "%s: failed to fetch multipeer stats", __func__);
799
+        ret = -1;
800
+        goto done;
801
+    }
802
+
803
+    /* iterate over stats and update peers */
804
+    for (int i = 0; i < bytes_returned / sizeof(OVPN_PEER_STATS); ++i)
805
+    {
806
+        OVPN_PEER_STATS *stat = &peer_stats[i];
807
+
808
+        if (stat->PeerId >= dco->c->multi->max_clients)
809
+        {
810
+            msg(M_WARN, "%s: received out of bound peer_id %u (max=%u)", __func__, stat->PeerId,
811
+                dco->c->multi->max_clients);
812
+            continue;
813
+        }
814
+
815
+        struct multi_instance *mi = dco->c->multi->instances[stat->PeerId];
816
+        if (!mi)
817
+        {
818
+            msg(M_WARN, "%s: received data for a non-existing peer %u", __func__, stat->PeerId);
819
+            continue;
820
+        }
821
+
822
+        /* update peer stats */
823
+        struct context_2 *c2 = &mi->context.c2;
824
+        c2->dco_read_bytes = stat->LinkRxBytes;
825
+        c2->dco_write_bytes = stat->LinkTxBytes;
826
+        c2->tun_read_bytes = stat->VpnRxBytes;
827
+        c2->tun_write_bytes = stat->VpnTxBytes;
828
+    }
829
+
830
+done:
831
+    gc_free(&gc);
832
+
833
+    if (raise_sigusr1_on_err && ret < 0)
834
+    {
835
+        register_signal(dco->c->sig, SIGUSR1, "dco peer stats error");
836
+    }
837
+
838
+    return ret;
719 839
 }
720 840
 
721 841
 int
722
-dco_get_peer_stats(struct context *c, const bool raise_sigusr1_on_err)
842
+dco_get_peer_stats_fallback(struct context *c, const bool raise_sigusr1_on_err)
723 843
 {
724 844
     struct tuntap *tt = c->c1.tuntap;
725 845
 
... ...
@@ -747,6 +870,48 @@ dco_get_peer_stats(struct context *c, const bool raise_sigusr1_on_err)
747 747
     return 0;
748 748
 }
749 749
 
750
+int
751
+dco_get_peer_stats(struct context *c, const bool raise_sigusr1_on_err)
752
+{
753
+    struct tuntap *tt = c->c1.tuntap;
754
+
755
+    if (!tuntap_defined(tt))
756
+    {
757
+        return -1;
758
+    }
759
+
760
+    /* first, try a new ioctl */
761
+    OVPN_GET_PEER_STATS ps = { .PeerId = c->c2.tls_multi->dco_peer_id };
762
+
763
+    OVPN_PEER_STATS peer_stats = { 0 };
764
+    DWORD bytes_returned = 0;
765
+    if (!DeviceIoControl(tt->hand, OVPN_IOCTL_GET_PEER_STATS, &ps, sizeof(ps), &peer_stats, sizeof(peer_stats),
766
+                         &bytes_returned, NULL))
767
+    {
768
+        if (GetLastError() == ERROR_INVALID_FUNCTION)
769
+        {
770
+            /* are we using the old driver? */
771
+            return dco_get_peer_stats_fallback(c, raise_sigusr1_on_err);
772
+        }
773
+
774
+        msg(M_WARN | M_ERRNO, "%s: DeviceIoControl(OVPN_IOCTL_GET_PEER_STATS) failed", __func__);
775
+        return -1;
776
+    }
777
+
778
+    if (bytes_returned != sizeof(OVPN_PEER_STATS))
779
+    {
780
+        msg(M_WARN | M_ERRNO, "%s: DeviceIoControl(OVPN_IOCTL_GET_PEER_STATS) returned invalid size", __func__);
781
+        return -1;
782
+    }
783
+
784
+    c->c2.dco_read_bytes = peer_stats.LinkRxBytes;
785
+    c->c2.dco_write_bytes = peer_stats.LinkTxBytes;
786
+    c->c2.tun_read_bytes = peer_stats.VpnRxBytes;
787
+    c->c2.tun_write_bytes = peer_stats.VpnTxBytes;
788
+
789
+    return 0;
790
+}
791
+
750 792
 void
751 793
 dco_event_set(dco_context_t *dco, struct event_set *es, void *arg)
752 794
 {
... ...
@@ -57,6 +57,8 @@ struct dco_context
57 57
 
58 58
     uint64_t dco_read_bytes;
59 59
     uint64_t dco_write_bytes;
60
+
61
+    struct context *c;
60 62
 };
61 63
 
62 64
 typedef struct dco_context dco_context_t;
... ...
@@ -83,6 +83,14 @@ typedef struct _OVPN_STATS {
83 83
 	LONG64 TunBytesReceived;
84 84
 } OVPN_STATS, * POVPN_STATS;
85 85
 
86
+typedef struct _OVPN_PEER_STATS {
87
+    int PeerId;
88
+    LONG64 LinkRxBytes;
89
+    LONG64 LinkTxBytes;
90
+    LONG64 VpnRxBytes;
91
+    LONG64 VpnTxBytes;
92
+} OVPN_PEER_STATS, * POVPN_PEER_STATS;
93
+
86 94
 typedef enum _OVPN_KEY_SLOT {
87 95
 	OVPN_KEY_SLOT_PRIMARY,
88 96
 	OVPN_KEY_SLOT_SECONDARY
... ...
@@ -185,6 +193,10 @@ typedef struct _OVPN_MP_IROUTE {
185 185
     int IPv6;
186 186
 } OVPN_MP_IROUTE, * POVPN_MP_IROUTE;
187 187
 
188
+typedef struct _OVPN_GET_PEER_STATS {
189
+    int PeerId; // -1 for all peers stats
190
+} OVPN_GET_PEER_STATS, * POVPN_GET_PEER_STATS;
191
+
188 192
 #define OVPN_IOCTL_NEW_PEER     CTL_CODE(FILE_DEVICE_UNKNOWN, 1, METHOD_BUFFERED, FILE_ANY_ACCESS)
189 193
 #define OVPN_IOCTL_GET_STATS    CTL_CODE(FILE_DEVICE_UNKNOWN, 2, METHOD_BUFFERED, FILE_ANY_ACCESS)
190 194
 #define OVPN_IOCTL_NEW_KEY      CTL_CODE(FILE_DEVICE_UNKNOWN, 3, METHOD_BUFFERED, FILE_ANY_ACCESS)
... ...
@@ -207,3 +219,5 @@ typedef struct _OVPN_MP_IROUTE {
207 207
 
208 208
 #define OVPN_IOCTL_MP_ADD_IROUTE CTL_CODE(FILE_DEVICE_UNKNOWN, 17, METHOD_BUFFERED, FILE_ANY_ACCESS)
209 209
 #define OVPN_IOCTL_MP_DEL_IROUTE CTL_CODE(FILE_DEVICE_UNKNOWN, 18, METHOD_BUFFERED, FILE_ANY_ACCESS)
210
+
211
+#define OVPN_IOCTL_GET_PEER_STATS CTL_CODE(FILE_DEVICE_UNKNOWN, 19, METHOD_BUFFERED, FILE_ANY_ACCESS)