Installation
Debian 12: Using the official repostory https://app.eduvpn.org/linux/v4/deb/
Arch Linux: Using the (user-maintained) AUR packages
Version
Debian 12:
eduvpn-client 4.5.1-1+debian+12+1
eduvpn-client-data 4.5.1-1+debian+12+1
libeduvpn-common 3.0.0-1+debian+12+1
python3-eduvpn-client 4.5.1-1+debian+12+1
python3-eduvpn-common 3.0.0-1+debian+12+1
libnm0 1.42.4-1
network-manager 1.42.4-1
network-manager-openvpn 1.10.2-2
Arch Linux:
python-eduvpn-client 4.5.1-1
python-eduvpn_common 3.0.0-1
libnm 1.52.0-1
networkmanager 1.52.0-1
networkmanager-openvpn 1.12.0-1
Check if your issue is in the known issues list
It is not.
OS/Distribution
Debian 12.8
Arch Linux (Rolling Release)
Logs
Do you have a problem while adding before connecting?
No.
Do you have a problem during or after connecting?
When connected using the OpenVPN protocol, the shown Downloaded/Uploaded statistics are wrong. The shown statistics are from the "primary interface" through which the OpenVPN connection was established, and not the tunnel interface. This is not noticeable with a full-tunnel profile where all the traffic goes though the tunnel, but very noticeable with a split-tunnel profile where only certain traffic (ip prefixes) is routed through the tunnel. I could confirm/reproduce this on multiple different systems.
Attached you find a screenshot from a fresh vanilla Debian 12.8 system. You can see that the shown statistics in the eduVPN client are (roughly) those of the primary ethernet interface enp3s0 (1231306048 B are ~1.147 GiB, and 881471384 B are ~840.6 MiB), and not of the tunnel interface tun0. So those statistics currently don't show the amount of data that actually was sent through the tunnel.
Disclaimer: I have not tested how this behaves with Wireguard.
The eduVPN-Client uses the first device associated with the "eduVPN" connection in NetworkManager that it creates/activates/monitors. The only device there is the primary interface, and not the tunnel interface. I also found no other attribute which directly links the NetworkManager vpn connection to the associated tunnel interface.
Looking at the DBus messages from the official NetworkManager OpenVPN plugin to the NetworkManager daemon, you can see this information is provided in the SetConfig call using the tundev attribute. And while I could verify that this info is correctly stored internally, I found no place where it is exposed; via an API or otherwise.
‣ Type=method_call Endian=l Flags=0 Version=1 Cookie=7 Timestamp="Thu 2025年06月05日 15:26:30.349343 UTC"
Sender=:1.4258 Destination=:1.4257 Path=/org/freedesktop/NetworkManager/VPN/Plugin Interface=org.freedesktop.NetworkManager.VPN.Plugin Member=SetConfig
UniqueName=:1.4258
MESSAGE "a{sv}" {
ARRAY "{sv}" {
DICT_ENTRY "sv" {
STRING "gateway";
VARIANT "u" {
UINT32 2130706433;
};
};
DICT_ENTRY "sv" {
STRING "can-persist";
VARIANT "b" {
BOOLEAN true;
};
};
DICT_ENTRY "sv" {
STRING "tundev";
VARIANT "s" {
STRING "tun0";
};
};
DICT_ENTRY "sv" {
STRING "mtu";
VARIANT "u" {
UINT32 1380;
};
};
DICT_ENTRY "sv" {
STRING "has-ip4";
VARIANT "b" {
BOOLEAN true;
};
};
};
};
The only way I found right now to get the right tunnel interface is to match using the assigned IP address. Below a quick-and-dirty Proof-of-Concept.
diff --git a/eduvpn/nm.py b/eduvpn/nm.py
index ac443b2..e4c451a 100644
--- a/eduvpn/nm.py
+++ b/eduvpn/nm.py
@@ -248,6 +248,25 @@ class NMManager:
if not active_con:
return None
+ if active_con.get_connection_type() == 'vpn':
+ needle_ipv4 = self.ipv4
+ if not needle_ipv4:
+ return None
+ for haystack_con in self.client.get_active_connections():
+ haystack_ipv4 = haystack_con.get_ip4_config()
+ if not haystack_ipv4:
+ continue
+ haystack_ipv4 = haystack_ipv4.get_addresses()
+ if not haystack_ipv4:
+ continue
+ haystack_ipv4 = con4[0].get_address()
+
+ if haystack_con.get_uuid() != active_con.get_uuid() and haystack_ipv4 == needle_ipv4:
+ active_con = haystack_con
+ break
+ else:
+ return None
+
devices = active_con.get_devices()
if not devices:
return None
As a side note, while debugging I found that the (superfluous?) iface check in NMManager.get_stats_bytes() triggers resolution of the iface property twice per second, which makes the caching in NetworkStats a bit pointless.
# Installation
Debian 12: Using the official repostory `https://app.eduvpn.org/linux/v4/deb/`
Arch Linux: Using the (user-maintained) AUR packages
# Version
Debian 12:
```
eduvpn-client 4.5.1-1+debian+12+1
eduvpn-client-data 4.5.1-1+debian+12+1
libeduvpn-common 3.0.0-1+debian+12+1
python3-eduvpn-client 4.5.1-1+debian+12+1
python3-eduvpn-common 3.0.0-1+debian+12+1
libnm0 1.42.4-1
network-manager 1.42.4-1
network-manager-openvpn 1.10.2-2
```
Arch Linux:
```
python-eduvpn-client 4.5.1-1
python-eduvpn_common 3.0.0-1
libnm 1.52.0-1
networkmanager 1.52.0-1
networkmanager-openvpn 1.12.0-1
```
# Check if your issue is in the known issues list
It is not.
# OS/Distribution
Debian 12.8
Arch Linux (Rolling Release)
# Logs
## Do you have a problem while adding before connecting?
No.
## Do you have a problem during or after connecting?
When connected using the OpenVPN protocol, the shown Downloaded/Uploaded statistics are wrong. The shown statistics are from the "primary interface" through which the OpenVPN connection was established, and not the tunnel interface. This is not noticeable with a full-tunnel profile where all the traffic goes though the tunnel, but very noticeable with a split-tunnel profile where only certain traffic (ip prefixes) is routed through the tunnel. I could confirm/reproduce this on multiple different systems.
Attached you find a screenshot from a fresh vanilla Debian 12.8 system. You can see that the shown statistics in the eduVPN client are (roughly) those of the primary ethernet interface `enp3s0` (1231306048 B are ~1.147 GiB, and 881471384 B are ~840.6 MiB), and not of the tunnel interface `tun0`. So those statistics currently don't show the amount of data that actually was sent through the tunnel.
Disclaimer: I have not tested how this behaves with Wireguard.
<hr />
The eduVPN-Client uses the first device associated with the "eduVPN" connection in NetworkManager that it creates/activates/monitors. The only device there is the primary interface, and not the tunnel interface. I also found no other attribute which directly links the NetworkManager vpn connection to the associated tunnel interface.
Looking at the DBus messages from the official NetworkManager OpenVPN plugin to the NetworkManager daemon, you can see this information is provided in [the `SetConfig` call](https://gitlab.gnome.org/GNOME/NetworkManager-openvpn/-/blob/1.12.0/src/nm-openvpn-service-openvpn-helper.c?ref_type=tags#L564) using the [`tundev`](https://github.com/NetworkManager/NetworkManager/blob/1.52.0/src/libnm-core-public/nm-vpn-dbus-interface.h#L180) attribute. And while I could verify that this info is [correctly stored internally](https://github.com/NetworkManager/NetworkManager/blob/1.52.0/src/core/vpn/nm-vpn-connection.c#L1803), I found no place where it is exposed; via an API or otherwise.
```
‣ Type=method_call Endian=l Flags=0 Version=1 Cookie=7 Timestamp="Thu 2025年06月05日 15:26:30.349343 UTC"
Sender=:1.4258 Destination=:1.4257 Path=/org/freedesktop/NetworkManager/VPN/Plugin Interface=org.freedesktop.NetworkManager.VPN.Plugin Member=SetConfig
UniqueName=:1.4258
MESSAGE "a{sv}" {
ARRAY "{sv}" {
DICT_ENTRY "sv" {
STRING "gateway";
VARIANT "u" {
UINT32 2130706433;
};
};
DICT_ENTRY "sv" {
STRING "can-persist";
VARIANT "b" {
BOOLEAN true;
};
};
DICT_ENTRY "sv" {
STRING "tundev";
VARIANT "s" {
STRING "tun0";
};
};
DICT_ENTRY "sv" {
STRING "mtu";
VARIANT "u" {
UINT32 1380;
};
};
DICT_ENTRY "sv" {
STRING "has-ip4";
VARIANT "b" {
BOOLEAN true;
};
};
};
};
```
The only way I found right now to get the right tunnel interface is to match using the assigned IP address. Below a quick-and-dirty Proof-of-Concept.
```diff
diff --git a/eduvpn/nm.py b/eduvpn/nm.py
index ac443b2..e4c451a 100644
--- a/eduvpn/nm.py
+++ b/eduvpn/nm.py
@@ -248,6 +248,25 @@ class NMManager:
if not active_con:
return None
+ if active_con.get_connection_type() == 'vpn':
+ needle_ipv4 = self.ipv4
+ if not needle_ipv4:
+ return None
+ for haystack_con in self.client.get_active_connections():
+ haystack_ipv4 = haystack_con.get_ip4_config()
+ if not haystack_ipv4:
+ continue
+ haystack_ipv4 = haystack_ipv4.get_addresses()
+ if not haystack_ipv4:
+ continue
+ haystack_ipv4 = con4[0].get_address()
+
+ if haystack_con.get_uuid() != active_con.get_uuid() and haystack_ipv4 == needle_ipv4:
+ active_con = haystack_con
+ break
+ else:
+ return None
+
devices = active_con.get_devices()
if not devices:
return None
```
<hr />
As a side note, while debugging I found that the (superfluous?) [`iface` check in `NMManager.get_stats_bytes()`](https://codeberg.org/eduVPN/linux-app/src/tag/4.5.1/eduvpn/nm.py#L145) triggers resolution of the `iface` property twice per second, which makes the caching in [`NetworkStats`](https://codeberg.org/eduVPN/linux-app/src/tag/4.5.1/eduvpn/ui/stats.py#L59) a bit pointless.