librats 0.9.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,367 @@
1
+ /**
2
+ * @file network_monitor.cpp
3
+ * @brief Platform backends for NetworkMonitor (see network_monitor.h).
4
+ */
5
+
6
+ // socket.h pulls in winsock2.h/ws2tcpip.h first on Windows (must precede the
7
+ // iphlpapi / windows headers below to avoid the classic winsock2/windows.h clash).
8
+ #include "socket.h"
9
+ #include "network_monitor.h"
10
+ #include "network_utils.h"
11
+ #include "logger.h"
12
+
13
+ #include <algorithm>
14
+
15
+ #ifdef _WIN32
16
+ #include <iphlpapi.h>
17
+ #include <netioapi.h>
18
+ #elif defined(__linux__)
19
+ #include <sys/socket.h>
20
+ #include <linux/netlink.h>
21
+ #include <linux/rtnetlink.h>
22
+ #include <unistd.h>
23
+ #include <poll.h>
24
+ #include <cstring>
25
+ #include <cerrno>
26
+ #elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || \
27
+ defined(__OpenBSD__) || defined(__DragonFly__)
28
+ #define RATS_MONITOR_BSD_ROUTES 1
29
+ #include <sys/types.h>
30
+ #include <sys/socket.h>
31
+ #include <net/route.h>
32
+ #include <net/if.h>
33
+ #include <unistd.h>
34
+ #include <poll.h>
35
+ #include <cerrno>
36
+ #endif
37
+
38
+ #define LOG_NETMON_DEBUG(message) LOG_DEBUG("netmon", message)
39
+ #define LOG_NETMON_INFO(message) LOG_INFO("netmon", message)
40
+ #define LOG_NETMON_WARN(message) LOG_WARN("netmon", message)
41
+ #define LOG_NETMON_ERROR(message) LOG_ERROR("netmon", message)
42
+
43
+ namespace librats {
44
+
45
+ namespace {
46
+
47
+ std::vector<std::string> snapshot_addresses() {
48
+ auto addrs = network_utils::get_local_interface_addresses();
49
+ std::sort(addrs.begin(), addrs.end());
50
+ addrs.erase(std::unique(addrs.begin(), addrs.end()), addrs.end());
51
+ return addrs;
52
+ }
53
+
54
+ } // namespace
55
+
56
+ // ============================================================================
57
+ // Platform backend state
58
+ // ============================================================================
59
+
60
+ struct NetworkMonitor::Impl {
61
+ #ifdef _WIN32
62
+ HANDLE handle = nullptr;
63
+ #elif defined(__linux__) || defined(RATS_MONITOR_BSD_ROUTES)
64
+ int fd = -1; // netlink (Linux) or PF_ROUTE (BSD) socket
65
+ int stop_pipe[2] = {-1, -1};
66
+ std::thread reader;
67
+ #endif
68
+ };
69
+
70
+ #ifdef _WIN32
71
+ // NotifyUnicastIpAddressChange invokes this from an OS worker thread on any
72
+ // unicast address add/remove/change. Windows gives no usable detail here, so we
73
+ // just trigger a re-enumeration (matches the cross-platform "something changed"
74
+ // contract). Must be __stdcall (WINAPI) to match PUNICAST_IPADDRESS_CHANGE_CALLBACK.
75
+ static void WINAPI rats_ip_change_cb(void* ctx, MIB_UNICASTIPADDRESS_ROW* /*row*/,
76
+ MIB_NOTIFICATION_TYPE /*type*/) {
77
+ auto* self = static_cast<NetworkMonitor*>(ctx);
78
+ if (self) self->check_now();
79
+ }
80
+ #endif
81
+
82
+ // ============================================================================
83
+ // Lifecycle
84
+ // ============================================================================
85
+
86
+ NetworkMonitor::NetworkMonitor() = default;
87
+
88
+ NetworkMonitor::~NetworkMonitor() {
89
+ stop();
90
+ }
91
+
92
+ bool NetworkMonitor::start(ChangeCallback on_change) {
93
+ if (running_.exchange(true)) {
94
+ return event_backend_active_;
95
+ }
96
+ on_change_ = std::move(on_change);
97
+ impl_ = std::make_unique<Impl>();
98
+ last_addresses_ = snapshot_addresses();
99
+
100
+ event_backend_active_ = backend_start();
101
+ LOG_NETMON_INFO("Network monitor started ("
102
+ << (event_backend_active_ ? "event-driven" : "polling")
103
+ << ", " << last_addresses_.size() << " local address(es))");
104
+
105
+ worker_ = std::thread([this]() { worker_loop(); });
106
+ return event_backend_active_;
107
+ }
108
+
109
+ void NetworkMonitor::stop() {
110
+ if (!running_.exchange(false)) {
111
+ return;
112
+ }
113
+ cv_.notify_all(); // wake the worker out of its wait
114
+ backend_stop(); // stop OS notifications / join the reader thread
115
+ if (worker_.joinable()) {
116
+ worker_.join();
117
+ }
118
+ impl_.reset();
119
+ event_backend_active_ = false;
120
+ LOG_NETMON_INFO("Network monitor stopped");
121
+ }
122
+
123
+ void NetworkMonitor::check_now() {
124
+ if (!running_.load()) return;
125
+ {
126
+ std::lock_guard<std::mutex> lock(mutex_);
127
+ change_pending_ = true;
128
+ }
129
+ cv_.notify_all();
130
+ }
131
+
132
+ // ============================================================================
133
+ // Worker: debounce + diff + dispatch
134
+ // ============================================================================
135
+
136
+ void NetworkMonitor::worker_loop() {
137
+ // When push notifications are active, the long interval is just a safety net
138
+ // for events the OS might drop (e.g. across suspend/resume). Without them,
139
+ // this interval is the actual detection latency.
140
+ const auto poll_interval = event_backend_active_
141
+ ? std::chrono::milliseconds(30000)
142
+ : std::chrono::milliseconds(5000);
143
+
144
+ while (running_.load()) {
145
+ bool was_event = false;
146
+ {
147
+ std::unique_lock<std::mutex> lock(mutex_);
148
+ cv_.wait_for(lock, poll_interval,
149
+ [this]() { return !running_.load() || change_pending_; });
150
+ if (!running_.load()) break;
151
+
152
+ was_event = change_pending_;
153
+ change_pending_ = false;
154
+
155
+ if (was_event) {
156
+ // Coalesce the burst: wait out a quiet debounce window (only a
157
+ // stop interrupts it), then drop any events that arrived during it.
158
+ cv_.wait_for(lock, debounce_, [this]() { return !running_.load(); });
159
+ if (!running_.load()) break;
160
+ change_pending_ = false;
161
+ }
162
+ }
163
+
164
+ auto current = snapshot_addresses();
165
+ if (current != last_addresses_) {
166
+ LOG_NETMON_INFO("Local address set changed (" << last_addresses_.size()
167
+ << " -> " << current.size() << ")");
168
+ last_addresses_ = current;
169
+ if (on_change_) {
170
+ on_change_(current);
171
+ }
172
+ } else if (was_event) {
173
+ LOG_NETMON_DEBUG("Network event with no effective address change; ignored");
174
+ }
175
+ }
176
+ }
177
+
178
+ // ============================================================================
179
+ // Windows backend: NotifyUnicastIpAddressChange
180
+ // ============================================================================
181
+ #ifdef _WIN32
182
+
183
+ bool NetworkMonitor::backend_start() {
184
+ DWORD rv = NotifyUnicastIpAddressChange(AF_UNSPEC, &rats_ip_change_cb, this,
185
+ FALSE, &impl_->handle);
186
+ if (rv != NO_ERROR) {
187
+ LOG_NETMON_WARN("NotifyUnicastIpAddressChange failed (" << rv
188
+ << "); falling back to polling");
189
+ impl_->handle = nullptr;
190
+ return false;
191
+ }
192
+ return true;
193
+ }
194
+
195
+ void NetworkMonitor::backend_stop() {
196
+ if (impl_ && impl_->handle != nullptr) {
197
+ // Cancels and waits for any in-flight callback to return, so no callback
198
+ // can run against a half-destroyed monitor afterwards.
199
+ CancelMibChangeNotify2(impl_->handle);
200
+ impl_->handle = nullptr;
201
+ }
202
+ }
203
+
204
+ // ============================================================================
205
+ // Linux backend: NETLINK_ROUTE socket
206
+ // ============================================================================
207
+ #elif defined(__linux__)
208
+
209
+ bool NetworkMonitor::backend_start() {
210
+ int fd = ::socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
211
+ if (fd < 0) {
212
+ LOG_NETMON_WARN("netlink socket() failed (" << errno << "); falling back to polling");
213
+ return false;
214
+ }
215
+
216
+ sockaddr_nl addr{};
217
+ addr.nl_family = AF_NETLINK;
218
+ addr.nl_groups = RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR
219
+ | RTMGRP_LINK | RTMGRP_IPV4_ROUTE | RTMGRP_IPV6_ROUTE;
220
+ if (::bind(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
221
+ LOG_NETMON_WARN("netlink bind() failed (" << errno << "); falling back to polling");
222
+ ::close(fd);
223
+ return false;
224
+ }
225
+
226
+ if (::pipe(impl_->stop_pipe) < 0) {
227
+ ::close(fd);
228
+ return false;
229
+ }
230
+ impl_->fd = fd;
231
+
232
+ impl_->reader = std::thread([this]() {
233
+ char buf[4096];
234
+ struct pollfd fds[2];
235
+ fds[0].fd = impl_->fd; fds[0].events = POLLIN;
236
+ fds[1].fd = impl_->stop_pipe[0]; fds[1].events = POLLIN;
237
+
238
+ while (running_.load()) {
239
+ int pr = ::poll(fds, 2, -1);
240
+ if (pr < 0) {
241
+ if (errno == EINTR) continue;
242
+ break;
243
+ }
244
+ if (fds[1].revents & POLLIN) break; // stop requested
245
+ if (!(fds[0].revents & POLLIN)) continue;
246
+
247
+ ssize_t len = ::recv(impl_->fd, buf, sizeof(buf), 0);
248
+ if (len <= 0) {
249
+ // ENOBUFS just means we missed messages under load — treat as a change.
250
+ if (len < 0 && errno == ENOBUFS) { check_now(); continue; }
251
+ if (len < 0 && errno == EINTR) continue;
252
+ break;
253
+ }
254
+
255
+ bool pertinent = false;
256
+ for (auto* nh = reinterpret_cast<nlmsghdr*>(buf);
257
+ NLMSG_OK(nh, len); nh = NLMSG_NEXT(nh, len)) {
258
+ switch (nh->nlmsg_type) {
259
+ case RTM_NEWADDR: case RTM_DELADDR:
260
+ case RTM_NEWLINK: case RTM_DELLINK:
261
+ case RTM_NEWROUTE: case RTM_DELROUTE:
262
+ pertinent = true;
263
+ break;
264
+ default:
265
+ break;
266
+ }
267
+ }
268
+ if (pertinent) check_now();
269
+ }
270
+ });
271
+ return true;
272
+ }
273
+
274
+ void NetworkMonitor::backend_stop() {
275
+ if (!impl_) return;
276
+ if (impl_->stop_pipe[1] >= 0) {
277
+ char b = 1;
278
+ ssize_t n = ::write(impl_->stop_pipe[1], &b, 1);
279
+ (void)n;
280
+ }
281
+ if (impl_->reader.joinable()) impl_->reader.join();
282
+ if (impl_->fd >= 0) { ::close(impl_->fd); impl_->fd = -1; }
283
+ if (impl_->stop_pipe[0] >= 0) { ::close(impl_->stop_pipe[0]); impl_->stop_pipe[0] = -1; }
284
+ if (impl_->stop_pipe[1] >= 0) { ::close(impl_->stop_pipe[1]); impl_->stop_pipe[1] = -1; }
285
+ }
286
+
287
+ // ============================================================================
288
+ // macOS / BSD backend: PF_ROUTE routing socket
289
+ // ============================================================================
290
+ #elif defined(RATS_MONITOR_BSD_ROUTES)
291
+
292
+ bool NetworkMonitor::backend_start() {
293
+ int fd = ::socket(PF_ROUTE, SOCK_RAW, AF_UNSPEC);
294
+ if (fd < 0) {
295
+ LOG_NETMON_WARN("PF_ROUTE socket() failed (" << errno << "); falling back to polling");
296
+ return false;
297
+ }
298
+ if (::pipe(impl_->stop_pipe) < 0) {
299
+ ::close(fd);
300
+ return false;
301
+ }
302
+ impl_->fd = fd;
303
+
304
+ impl_->reader = std::thread([this]() {
305
+ char buf[2048];
306
+ struct pollfd fds[2];
307
+ fds[0].fd = impl_->fd; fds[0].events = POLLIN;
308
+ fds[1].fd = impl_->stop_pipe[0]; fds[1].events = POLLIN;
309
+
310
+ while (running_.load()) {
311
+ int pr = ::poll(fds, 2, -1);
312
+ if (pr < 0) {
313
+ if (errno == EINTR) continue;
314
+ break;
315
+ }
316
+ if (fds[1].revents & POLLIN) break; // stop requested
317
+ if (!(fds[0].revents & POLLIN)) continue;
318
+
319
+ ssize_t len = ::read(impl_->fd, buf, sizeof(buf));
320
+ if (len <= 0) {
321
+ if (len < 0 && errno == EINTR) continue;
322
+ break;
323
+ }
324
+ if (static_cast<size_t>(len) < sizeof(rt_msghdr)) continue;
325
+
326
+ auto* rtm = reinterpret_cast<rt_msghdr*>(buf);
327
+ switch (rtm->rtm_type) {
328
+ case RTM_NEWADDR: case RTM_DELADDR:
329
+ case RTM_IFINFO:
330
+ #ifdef RTM_IFANNOUNCE
331
+ case RTM_IFANNOUNCE:
332
+ #endif
333
+ case RTM_ADD: case RTM_DELETE: case RTM_CHANGE:
334
+ check_now();
335
+ break;
336
+ default:
337
+ break;
338
+ }
339
+ }
340
+ });
341
+ return true;
342
+ }
343
+
344
+ void NetworkMonitor::backend_stop() {
345
+ if (!impl_) return;
346
+ if (impl_->stop_pipe[1] >= 0) {
347
+ char b = 1;
348
+ ssize_t n = ::write(impl_->stop_pipe[1], &b, 1);
349
+ (void)n;
350
+ }
351
+ if (impl_->reader.joinable()) impl_->reader.join();
352
+ if (impl_->fd >= 0) { ::close(impl_->fd); impl_->fd = -1; }
353
+ if (impl_->stop_pipe[0] >= 0) { ::close(impl_->stop_pipe[0]); impl_->stop_pipe[0] = -1; }
354
+ if (impl_->stop_pipe[1] >= 0) { ::close(impl_->stop_pipe[1]); impl_->stop_pipe[1] = -1; }
355
+ }
356
+
357
+ // ============================================================================
358
+ // Fallback backend: polling only (worker_loop diffs on its poll interval)
359
+ // ============================================================================
360
+ #else
361
+
362
+ bool NetworkMonitor::backend_start() { return false; }
363
+ void NetworkMonitor::backend_stop() {}
364
+
365
+ #endif
366
+
367
+ } // namespace librats
@@ -0,0 +1,104 @@
1
+ #pragma once
2
+
3
+ /**
4
+ * @file network_monitor.h
5
+ * @brief Detects host network configuration changes (IP / interface / route).
6
+ *
7
+ * When the machine's connectivity changes — a new interface comes up, an IP
8
+ * address is added or removed, the default route flips (Wi-Fi <-> cellular,
9
+ * dock/undock, VPN up/down, wake-from-sleep) — a long-lived P2P node must react:
10
+ * re-create router port mappings, re-discover its public address via STUN, and
11
+ * re-announce to the DHT. Otherwise it keeps advertising a stale, unreachable
12
+ * endpoint until the next periodic refresh.
13
+ *
14
+ * NetworkMonitor provides an event-driven signal for exactly that. The design
15
+ * mirrors libtorrent's aux::ip_notifier but is adapted to librats' own threading
16
+ * model (no boost::asio): each platform backend only signals "something changed",
17
+ * and the monitor itself re-enumerates the local interface addresses and invokes
18
+ * the callback ONLY when the address set actually differs. Bursts of OS events
19
+ * (a single interface transition typically emits several) are coalesced with a
20
+ * short debounce window.
21
+ *
22
+ * Platform backends:
23
+ * - Windows: NotifyUnicastIpAddressChange() (iphlpapi)
24
+ * - Linux: NETLINK_ROUTE socket, RTMGRP_*_IFADDR / *_LINK groups
25
+ * - macOS/BSD: PF_ROUTE routing socket
26
+ * - otherwise: periodic polling of the interface address list
27
+ *
28
+ * The callback runs on the monitor's worker thread, so it must not block for
29
+ * long; offload slow recovery work (STUN, port mapping) to another thread.
30
+ */
31
+
32
+ #include <atomic>
33
+ #include <chrono>
34
+ #include <condition_variable>
35
+ #include <functional>
36
+ #include <memory>
37
+ #include <mutex>
38
+ #include <string>
39
+ #include <thread>
40
+ #include <vector>
41
+
42
+ namespace librats {
43
+
44
+ class NetworkMonitor {
45
+ public:
46
+ /// Invoked (debounced) whenever the set of local interface addresses changes.
47
+ /// @param current_addresses the new, full list of local interface addresses.
48
+ using ChangeCallback = std::function<void(const std::vector<std::string>& current_addresses)>;
49
+
50
+ NetworkMonitor();
51
+ ~NetworkMonitor();
52
+
53
+ NetworkMonitor(const NetworkMonitor&) = delete;
54
+ NetworkMonitor& operator=(const NetworkMonitor&) = delete;
55
+
56
+ /**
57
+ * Start monitoring. The callback fires on each detected change (after the
58
+ * debounce window) on the monitor's worker thread.
59
+ *
60
+ * @return true if an OS push-notification backend is active; false if the
61
+ * monitor fell back to periodic polling. The monitor works either
62
+ * way, so the return value is informational only.
63
+ */
64
+ bool start(ChangeCallback on_change);
65
+
66
+ /// Stop monitoring and join the worker thread. Idempotent.
67
+ void stop();
68
+
69
+ bool is_running() const { return running_.load(); }
70
+
71
+ /// Whether OS push notifications (vs. polling) are in use. Valid after start().
72
+ bool is_event_driven() const { return event_backend_active_; }
73
+
74
+ /**
75
+ * Request an immediate re-check, coalesced/debounced exactly like a real OS
76
+ * event. Thread-safe. Useful to call after the device wakes from sleep, or
77
+ * from the platform backends themselves. A no-op if not running.
78
+ */
79
+ void check_now();
80
+
81
+ /// Override the debounce window used to coalesce event bursts (default 2s).
82
+ void set_debounce(std::chrono::milliseconds d) { debounce_ = d; }
83
+
84
+ private:
85
+ void worker_loop();
86
+ bool backend_start(); // set up the OS notifier; returns true if event-driven
87
+ void backend_stop(); // tear the OS notifier / reader thread down
88
+
89
+ ChangeCallback on_change_;
90
+ std::atomic<bool> running_{false};
91
+ bool change_pending_ = false; // guarded by mutex_
92
+ bool event_backend_active_ = false; // true once an OS backend is confirmed up
93
+ std::thread worker_;
94
+ std::mutex mutex_;
95
+ std::condition_variable cv_;
96
+ std::chrono::milliseconds debounce_{2000};
97
+ std::vector<std::string> last_addresses_;
98
+
99
+ // Platform-specific backend state (fds, OS handles, reader thread).
100
+ struct Impl;
101
+ std::unique_ptr<Impl> impl_;
102
+ };
103
+
104
+ } // namespace librats
@@ -16,6 +16,17 @@
16
16
  #else
17
17
  #include <ifaddrs.h>
18
18
  #endif
19
+
20
+ // macOS / BSD default-gateway lookup via the PF_ROUTE sysctl routing table.
21
+ #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || \
22
+ defined(__OpenBSD__) || defined(__DragonFly__)
23
+ #define RATS_HAVE_BSD_ROUTES 1
24
+ #include <sys/types.h>
25
+ #include <sys/socket.h>
26
+ #include <sys/sysctl.h>
27
+ #include <net/route.h>
28
+ #include <net/if.h>
29
+ #endif
19
30
  #endif
20
31
 
21
32
 
@@ -25,6 +36,12 @@
25
36
  #include <cstring>
26
37
  #include <iostream>
27
38
  #include <vector>
39
+ #include <algorithm>
40
+
41
+ #ifndef _WIN32
42
+ #include <cstdio>
43
+ #include <cstdlib>
44
+ #endif
28
45
 
29
46
  // Network utilities module logging macros
30
47
  #define LOG_NETUTILS_DEBUG(message) LOG_DEBUG("network_utils", message)
@@ -379,6 +396,41 @@ bool is_hostname(const std::string& str) {
379
396
  return true;
380
397
  }
381
398
 
399
+ bool is_public_ip(const std::string& ip) {
400
+ if (ip.empty()) return false;
401
+
402
+ if (is_valid_ipv6(ip)) {
403
+ struct in6_addr a;
404
+ if (inet_pton(AF_INET6, ip.c_str(), &a) != 1) return false;
405
+ const uint8_t* b = a.s6_addr;
406
+ bool all_zero = true;
407
+ for (int i = 0; i < 16; ++i) { if (b[i]) { all_zero = false; break; } }
408
+ if (all_zero) return false; // :: (unspecified)
409
+ bool loopback = (b[15] == 1);
410
+ for (int i = 0; i < 15; ++i) { if (b[i]) { loopback = false; break; } }
411
+ if (loopback) return false; // ::1
412
+ if ((b[0] & 0xfe) == 0xfc) return false; // fc00::/7 unique local
413
+ if (b[0] == 0xfe && (b[1] & 0xc0) == 0x80) return false; // fe80::/10 link-local
414
+ if (b[0] == 0xff) return false; // ff00::/8 multicast
415
+ return true;
416
+ }
417
+
418
+ struct in_addr a;
419
+ if (inet_pton(AF_INET, ip.c_str(), &a) != 1) return false;
420
+ uint32_t h = ntohl(a.s_addr);
421
+ uint8_t o1 = static_cast<uint8_t>((h >> 24) & 0xff);
422
+ uint8_t o2 = static_cast<uint8_t>((h >> 16) & 0xff);
423
+ if (o1 == 0) return false; // 0.0.0.0/8
424
+ if (o1 == 127) return false; // loopback
425
+ if (o1 == 10) return false; // 10.0.0.0/8
426
+ if (o1 == 172 && o2 >= 16 && o2 <= 31) return false; // 172.16.0.0/12
427
+ if (o1 == 192 && o2 == 168) return false; // 192.168.0.0/16
428
+ if (o1 == 169 && o2 == 254) return false; // 169.254.0.0/16 link-local
429
+ if (o1 == 100 && o2 >= 64 && o2 <= 127) return false; // 100.64.0.0/10 CGNAT
430
+ if (o1 >= 224) return false; // multicast / reserved
431
+ return true;
432
+ }
433
+
382
434
  std::vector<std::string> get_local_interface_addresses() {
383
435
  LOG_NETUTILS_DEBUG("Getting all local interface addresses (IPv4 and IPv6)");
384
436
 
@@ -390,11 +442,140 @@ std::vector<std::string> get_local_interface_addresses() {
390
442
  auto ipv6_addresses = get_local_interface_addresses_v6();
391
443
  addresses.insert(addresses.end(), ipv6_addresses.begin(), ipv6_addresses.end());
392
444
 
393
- LOG_NETUTILS_INFO("Found " << addresses.size() << " total local interface addresses ("
445
+ LOG_NETUTILS_INFO("Found " << addresses.size() << " total local interface addresses ("
394
446
  << ipv4_addresses.size() << " IPv4, " << ipv6_addresses.size() << " IPv6)");
395
-
447
+
396
448
  return addresses;
397
449
  }
398
450
 
451
+ namespace {
452
+
453
+ // Append unique, non-empty entries preserving order
454
+ void append_unique(std::vector<std::string>& out, const std::string& value) {
455
+ if (value.empty()) return;
456
+ if (std::find(out.begin(), out.end(), value) == out.end()) {
457
+ out.push_back(value);
458
+ }
459
+ }
460
+
461
+ // Best-effort guess: for each local IPv4 assume the gateway is the .1 host of a
462
+ // /24 network. Covers the overwhelming majority of home routers and serves as a
463
+ // fallback when the OS routing table is unavailable.
464
+ void append_gateway_heuristics(std::vector<std::string>& out) {
465
+ for (const auto& ip : get_local_interface_addresses_v4()) {
466
+ if (ip.empty() || ip == "127.0.0.1") continue;
467
+ auto last_dot = ip.find_last_of('.');
468
+ if (last_dot == std::string::npos) continue;
469
+ append_unique(out, ip.substr(0, last_dot) + ".1");
470
+ }
471
+ }
472
+
473
+ } // anonymous namespace
474
+
475
+ std::vector<std::string> get_default_gateways() {
476
+ std::vector<std::string> gateways;
477
+
478
+ #ifdef _WIN32
479
+ ULONG out_buf_len = sizeof(IP_ADAPTER_INFO);
480
+ std::vector<uint8_t> buffer(out_buf_len);
481
+ DWORD ret = GetAdaptersInfo(reinterpret_cast<PIP_ADAPTER_INFO>(buffer.data()), &out_buf_len);
482
+ if (ret == ERROR_BUFFER_OVERFLOW) {
483
+ buffer.resize(out_buf_len);
484
+ ret = GetAdaptersInfo(reinterpret_cast<PIP_ADAPTER_INFO>(buffer.data()), &out_buf_len);
485
+ }
486
+ if (ret == NO_ERROR) {
487
+ for (PIP_ADAPTER_INFO adapter = reinterpret_cast<PIP_ADAPTER_INFO>(buffer.data());
488
+ adapter != nullptr; adapter = adapter->Next) {
489
+ for (const IP_ADDR_STRING* gw = &adapter->GatewayList; gw != nullptr; gw = gw->Next) {
490
+ std::string gw_ip(gw->IpAddress.String);
491
+ if (gw_ip != "0.0.0.0") {
492
+ append_unique(gateways, gw_ip);
493
+ }
494
+ }
495
+ }
496
+ } else {
497
+ LOG_NETUTILS_DEBUG("GetAdaptersInfo failed with error: " << ret);
498
+ }
499
+ #elif defined(__linux__)
500
+ // /proc/net/route columns: Iface Destination Gateway Flags ... (hex, little-endian)
501
+ if (FILE* f = std::fopen("/proc/net/route", "r")) {
502
+ char line[256];
503
+ // Skip header line
504
+ if (std::fgets(line, sizeof(line), f)) {
505
+ char iface[64];
506
+ unsigned long dest = 0, gw = 0;
507
+ while (std::fgets(line, sizeof(line), f)) {
508
+ if (std::sscanf(line, "%63s %lx %lx", iface, &dest, &gw) == 3) {
509
+ if (dest == 0 && gw != 0) {
510
+ struct in_addr addr;
511
+ addr.s_addr = static_cast<in_addr_t>(gw);
512
+ char ip_str[INET_ADDRSTRLEN];
513
+ if (inet_ntop(AF_INET, &addr, ip_str, sizeof(ip_str))) {
514
+ append_unique(gateways, ip_str);
515
+ }
516
+ }
517
+ }
518
+ }
519
+ }
520
+ std::fclose(f);
521
+ }
522
+ #elif defined(RATS_HAVE_BSD_ROUTES)
523
+ // Dump the IPv4 routing table and pick the gateway of the default route(s).
524
+ int mib[6] = { CTL_NET, PF_ROUTE, 0, AF_INET, NET_RT_DUMP, 0 };
525
+ size_t needed = 0;
526
+ if (sysctl(mib, 6, nullptr, &needed, nullptr, 0) == 0 && needed > 0) {
527
+ std::vector<char> buf(needed);
528
+ if (sysctl(mib, 6, buf.data(), &needed, nullptr, 0) == 0) {
529
+ // sockaddrs in a routing message are padded to a 4-byte boundary.
530
+ auto sa_roundup = [](socklen_t len) -> size_t {
531
+ return len ? (1 + ((static_cast<size_t>(len) - 1) | (sizeof(uint32_t) - 1)))
532
+ : sizeof(uint32_t);
533
+ };
534
+ char* lim = buf.data() + needed;
535
+ for (char* next = buf.data(); next + sizeof(struct rt_msghdr) <= lim; ) {
536
+ auto* rtm = reinterpret_cast<struct rt_msghdr*>(next);
537
+ if (rtm->rtm_msglen == 0) break;
538
+ char* msg_end = next + rtm->rtm_msglen;
539
+ next = msg_end;
540
+
541
+ if (!(rtm->rtm_flags & RTF_GATEWAY)) continue;
542
+ if (!(rtm->rtm_addrs & RTA_DST) || !(rtm->rtm_addrs & RTA_GATEWAY)) continue;
543
+
544
+ // Address list follows the header, ordered by the RTA_* bit flags.
545
+ char* sa_ptr = reinterpret_cast<char*>(rtm + 1);
546
+ struct sockaddr* dst = nullptr;
547
+ struct sockaddr* gw = nullptr;
548
+ for (int bit = 1; bit && sa_ptr < msg_end; bit <<= 1) {
549
+ if (!(rtm->rtm_addrs & bit)) continue;
550
+ auto* sa = reinterpret_cast<struct sockaddr*>(sa_ptr);
551
+ if (bit == RTA_DST) dst = sa;
552
+ else if (bit == RTA_GATEWAY) gw = sa;
553
+ sa_ptr += sa_roundup(sa->sa_len);
554
+ }
555
+
556
+ if (!dst || !gw) continue;
557
+ if (dst->sa_family != AF_INET || gw->sa_family != AF_INET) continue;
558
+ // Default route: destination 0.0.0.0
559
+ if (reinterpret_cast<struct sockaddr_in*>(dst)->sin_addr.s_addr != 0) continue;
560
+
561
+ char ip_str[INET_ADDRSTRLEN];
562
+ auto* gw4 = reinterpret_cast<struct sockaddr_in*>(gw);
563
+ if (inet_ntop(AF_INET, &gw4->sin_addr, ip_str, sizeof(ip_str))) {
564
+ append_unique(gateways, ip_str);
565
+ }
566
+ }
567
+ }
568
+ } else {
569
+ LOG_NETUTILS_DEBUG("PF_ROUTE sysctl for default gateway failed");
570
+ }
571
+ #endif
572
+
573
+ // Always add heuristics as a fallback so callers have something to try
574
+ append_gateway_heuristics(gateways);
575
+
576
+ LOG_NETUTILS_INFO("Detected " << gateways.size() << " default gateway candidate(s)");
577
+ return gateways;
578
+ }
579
+
399
580
  } // namespace network_utils
400
581
  } // namespace librats