Skip to content

Commit 823a307

Browse files
authored
AG-57160 fix trusttunnel-client: add 'vpn_network_manager_acquire_tunnel_activity' to NetworkManager's API
* AG-57160 fix trusttunnel-client: add 'vpn_network_manager_set_tunnel_active' to NetworkManager's API * use unique tokens for every event-loop instead of one global atomic_bool * add info in development.md + comments in network_manager.h
1 parent 04c3219 commit 823a307

16 files changed

Lines changed: 223 additions & 8 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
cmake-build-*/
22
build/
3+
env/
34
.idea/
45
.vscode/
56
.DS_Store

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
88

99
### Added
1010

11+
- Ownership-tracked process-wide tunnel state to fail closed when an outgoing socket cannot be protected from VPN routing while any tunnel is active.
12+
1113
### Changed
1214

1315
### Deprecated

DEVELOPMENT.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,67 @@ To test local changes in the library when used as a Conan package dependency, fo
303303
Note:
304304
- If you have already exported the library in this way, the cached version must be purged: `conan remove -c vpn-libs/<commit_hash>`.
305305
306+
## Integrating Tunnel Activity Tracking
307+
308+
The library tracks whether a VPN TUN interface is active in the process to fail
309+
closed when an outgoing socket cannot be protected from being routed into the
310+
tunnel. Applications that create a TUN interface themselves (as opposed to
311+
using the built-in tunnels from `net/os_tunnel.h`) **must** register it with
312+
this API, otherwise the socket protection handlers have no way to distinguish
313+
"no tunnel" from "tunnel active but unprotected".
314+
315+
### API overview
316+
317+
- `vpn_network_manager_acquire_tunnel_activity()` — registers an active TUN
318+
interface and returns a non-zero ownership token.
319+
- `vpn_network_manager_release_tunnel_activity(token)` — unregisters a TUN
320+
interface previously registered with `acquire`. Invalid or already released
321+
tokens are ignored, so duplicate releases are safe.
322+
- `vpn_network_manager_get_tunnel_active()` — returns `true` while at least one
323+
TUN interface is registered.
324+
- `vpn_network_manager_set_outbound_interface(idx)` — sets the index of the
325+
physical interface that outgoing sockets should be bound to. See below.
326+
327+
### Integration checklist
328+
329+
1. **Acquire when the TUN interface is created.** Call
330+
`vpn_network_manager_acquire_tunnel_activity()` only after the TUN interface
331+
has been successfully established (e.g. after the TUN fd has been opened),
332+
and store the returned token. Do not acquire before the interface exists:
333+
on failure there is nothing to release, and the token must not outlive the interface.
334+
335+
2. **Release when the TUN interface is closed.** Call
336+
`vpn_network_manager_release_tunnel_activity(token)` with the stored token
337+
after the TUN interface (and all its duplicated descriptors) are closed.
338+
Releasing earlier would allow sockets to fall back to the system default
339+
route while the tunnel is still capturing it. Keep the token for the whole
340+
lifetime of the interface.
341+
342+
3. **Set the outbound interface before notifying about network changes.**
343+
Call `vpn_network_manager_set_outbound_interface()` with the index of the
344+
physical interface **before** notifying the running VPN instance with
345+
`vpn_notify_network_change()`. The DNS handler restarts the system DNS proxy
346+
on network changes and reads the current outbound interface at that moment.
347+
348+
4. **Multiple TUN interfaces are supported.** Each interface acquires its own
349+
token; the state stays active until the last token is released. Never reuse
350+
a token for two interfaces, and never release a token owned by another
351+
interface.
352+
353+
5. **Platform equivalents of a bound interface.** On Android, sockets are
354+
protected with `VpnService.protect(fd)` instead of an interface binding; the
355+
application is responsible for routing its outgoing sockets through that
356+
mechanism while the tunnel is active. On Apple, the built-in `AGTunnel`
357+
registers the activity automatically.
358+
359+
### Behavior
360+
361+
While `vpn_network_manager_get_tunnel_active()` returns `true` and the outbound
362+
interface is zero (or stale), platform socket protection handlers reject
363+
outgoing sockets instead of letting them use the system default route. This
364+
prevents the VPN's own traffic from looping back into the TUN interface. The
365+
DNS proxy startup is also gated on the same condition on non-Android platforms.
366+
306367
## Companion Endpoint Repository
307368

308369
Complementary endpoint implementation for the TrustTunnel VPN can be found in

core/src/dns_proxy_accessor.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ bool DnsProxyAccessor::start() {
107107
return false;
108108
}
109109

110+
#ifndef __ANDROID__
111+
if (vpn_network_manager_get_tunnel_active() && vpn_network_manager_get_outbound_interface() == 0) {
112+
log_accessor(this, err, "Cannot start DNS proxy without an outbound interface while tunnel is active");
113+
return false;
114+
}
115+
#endif // __ANDROID__
116+
110117
m_dns_proxy = std::make_unique<dns::DnsProxy>();
111118
auto [ok, msg] = m_dns_proxy->init(make_dns_proxy_settings(m_parameters),
112119
{

net/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,4 @@ add_live_test(test_locations_pinger_runner "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}
121121
add_unit_test(test_dns_utils "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE TRUE)
122122
add_unit_test(test_tls_serialize "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE TRUE)
123123
add_unit_test(test_net_utils "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE TRUE)
124+
add_unit_test(test_network_manager "${TEST_DIR}" "${TEST_EXTRA_INCLUDES}" TRUE TRUE)

net/include/net/network_manager.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include <cstdint>
34
#include <string>
45
#include <vector>
56

@@ -64,4 +65,32 @@ extern "C" WIN_EXPORT void vpn_network_manager_set_outbound_interface(uint32_t i
6465
*/
6566
uint32_t vpn_network_manager_get_outbound_interface();
6667

68+
using VpnTunnelActivityToken = uint64_t;
69+
70+
/** Invalid tunnel activity token. */
71+
constexpr VpnTunnelActivityToken VPN_TUNNEL_ACTIVITY_TOKEN_INVALID = 0;
72+
73+
/**
74+
* Register an active VPN TUN interface in this process.
75+
*
76+
* While at least one TUN interface is active, outgoing connections must not use the system default route: platform
77+
* socket protection handlers must fail if no outbound interface or equivalent platform protection is available.
78+
* The returned ownership token must be released exactly once with
79+
* `vpn_network_manager_release_tunnel_activity()` after the TUN interface is closed.
80+
*
81+
* @return A non-zero ownership token.
82+
*/
83+
extern "C" WIN_EXPORT VpnTunnelActivityToken vpn_network_manager_acquire_tunnel_activity();
84+
85+
/**
86+
* Unregister an active VPN TUN interface previously registered with `vpn_network_manager_acquire_tunnel_activity()`.
87+
* Invalid or already released tokens are ignored.
88+
*/
89+
extern "C" WIN_EXPORT void vpn_network_manager_release_tunnel_activity(VpnTunnelActivityToken token);
90+
91+
/**
92+
* Check whether a VPN TUN interface is active in this process.
93+
*/
94+
bool vpn_network_manager_get_tunnel_active();
95+
6796
} // namespace ag

net/include/net/os_tunnel.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "common/error.h"
1616
#include "common/socket_address.h"
1717
#include "common/utils.h"
18+
#include "net/network_manager.h"
1819
#include "vpn/platform.h"
1920
#include "vpn/utils.h"
2021

@@ -198,6 +199,9 @@ class VpnOsTunnel {
198199
VpnOsTunnel &operator=(VpnOsTunnel &&) = delete;
199200

200201
protected:
202+
void mark_tunnel_active();
203+
void clear_tunnel_active();
204+
201205
void init_settings(const VpnOsTunnelSettings *settings) {
202206
m_settings.reset(vpn_os_tunnel_settings_clone(settings));
203207
}
@@ -206,6 +210,7 @@ class VpnOsTunnel {
206210
uint32_t m_if_index = 0;
207211
bool m_system_dns_setup_success = false;
208212
bool m_ipv6_available = false;
213+
VpnTunnelActivityToken m_tunnel_activity_token = VPN_TUNNEL_ACTIVITY_TOKEN_INVALID;
209214
};
210215

211216
#ifdef __linux__

net/src/network_manager.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <atomic>
44
#include <mutex>
5+
#include <unordered_set>
56
#include <utility>
67

78
#include "common/cache.h"
@@ -28,6 +29,10 @@ static struct NetworkManagerHolder {
2829
std::mutex guard;
2930
ag::LruTimeoutCache<std::string, bool> app_domain_cache;
3031
std::atomic<uint32_t> outbound_interface = 0;
32+
std::mutex tunnel_activity_guard;
33+
std::unordered_set<VpnTunnelActivityToken> tunnel_activity_tokens;
34+
VpnTunnelActivityToken next_tunnel_activity_token = 1;
35+
std::atomic_size_t tunnel_activity_count = 0;
3136

3237
NetworkManagerHolder()
3338
: app_domain_cache(DEFAULT_CACHE_SIZE, std::chrono::minutes(10)) {
@@ -81,4 +86,36 @@ uint32_t vpn_network_manager_get_outbound_interface() {
8186
return g_network_manager_holder.outbound_interface;
8287
}
8388

89+
VpnTunnelActivityToken vpn_network_manager_acquire_tunnel_activity() {
90+
std::scoped_lock l(g_network_manager_holder.tunnel_activity_guard);
91+
VpnTunnelActivityToken token;
92+
do {
93+
token = g_network_manager_holder.next_tunnel_activity_token++;
94+
} while (token == VPN_TUNNEL_ACTIVITY_TOKEN_INVALID
95+
|| g_network_manager_holder.tunnel_activity_tokens.contains(token));
96+
97+
g_network_manager_holder.tunnel_activity_tokens.insert(token);
98+
g_network_manager_holder.tunnel_activity_count = g_network_manager_holder.tunnel_activity_tokens.size();
99+
dbglog(g_logger, "Tunnel activity acquired, active owners: {}",
100+
g_network_manager_holder.tunnel_activity_tokens.size());
101+
return token;
102+
}
103+
104+
void vpn_network_manager_release_tunnel_activity(VpnTunnelActivityToken token) {
105+
std::scoped_lock l(g_network_manager_holder.tunnel_activity_guard);
106+
if (token == VPN_TUNNEL_ACTIVITY_TOKEN_INVALID
107+
|| g_network_manager_holder.tunnel_activity_tokens.erase(token) == 0) {
108+
warnlog(g_logger, "Ignoring invalid or already released tunnel activity token");
109+
return;
110+
}
111+
112+
g_network_manager_holder.tunnel_activity_count = g_network_manager_holder.tunnel_activity_tokens.size();
113+
dbglog(g_logger, "Tunnel activity released, active owners: {}",
114+
g_network_manager_holder.tunnel_activity_tokens.size());
115+
}
116+
117+
bool vpn_network_manager_get_tunnel_active() {
118+
return g_network_manager_holder.tunnel_activity_count != 0;
119+
}
120+
84121
} // namespace ag

net/src/os_tunnel.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include <vector>
22

33
#include "common/utils.h"
4+
#include "net/network_manager.h"
45
#include "net/os_tunnel.h"
56
#include "net/utils.h"
67

@@ -22,6 +23,19 @@ static constexpr std::string_view DEFAULT_IPV4_ROUTE = "0.0.0.0/0";
2223
static constexpr std::string_view DEFAULT_IPV6_ROUTE = "::/0";
2324
static constexpr std::string_view DEFAULT_IPV6_ROUTE_UNICAST = "2000::/3";
2425

26+
void ag::VpnOsTunnel::mark_tunnel_active() {
27+
if (m_tunnel_activity_token == VPN_TUNNEL_ACTIVITY_TOKEN_INVALID) {
28+
m_tunnel_activity_token = vpn_network_manager_acquire_tunnel_activity();
29+
}
30+
}
31+
32+
void ag::VpnOsTunnel::clear_tunnel_active() {
33+
if (m_tunnel_activity_token != VPN_TUNNEL_ACTIVITY_TOKEN_INVALID) {
34+
vpn_network_manager_release_tunnel_activity(m_tunnel_activity_token);
35+
m_tunnel_activity_token = VPN_TUNNEL_ACTIVITY_TOKEN_INVALID;
36+
}
37+
}
38+
2539
void ag::tunnel_utils::split_default_route(std::vector<ag::CidrRange> &routes, ag::CidrRange route) {
2640
for (size_t idx = 0; idx < routes.size(); ++idx) {
2741
if (routes[idx] == route) {

net/src/os_tunnel_linux.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ ag::VpnError ag::VpnLinuxTunnel::init(const ag::VpnOsTunnelSettings *settings, s
8181
return {-1, "Failed to init tunnel"};
8282
}
8383
setup_if();
84+
mark_tunnel_active();
8485

8586
if (managed_routing) {
8687
m_sport_supported = check_sport_rule_support();
@@ -108,6 +109,7 @@ void ag::VpnLinuxTunnel::deinit() {
108109
teardown_routes(TABLE_ID);
109110
}
110111
m_system_dns_setup_success = false;
112+
clear_tunnel_active();
111113
}
112114

113115
evutil_socket_t ag::VpnLinuxTunnel::get_fd() {

0 commit comments

Comments
 (0)