Skip to content
API Documentation
BastionGuard™
GeoResolver.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™ Secure Connection
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 * GPL-3.0
5 */
6
7#include "GeoResolver.hpp"
8
9#include <arpa/inet.h>
10#include <cstring>
11#include <netdb.h>
12#include <nlohmann/json.hpp>
13#include <sys/socket.h>
14#include <sys/types.h>
15#include <unistd.h>
16
17namespace bsc::ui_util {
18namespace {
19
20// Cache TTL: 24h. Gli hosting providers cambiano rete molto raramente.
21constexpr auto kCacheTtl = std::chrono::hours(24);
22
23// Rate: 40 richieste al minuto -> 1500ms tra una e l'altra per stare sicuri.
24// ip-api.com free tier permette 45/min; se superiamo il limite ci blocca per
25// 1 ora.
26constexpr auto kMinInterval = std::chrono::milliseconds(1500);
27
28// HTTP request minima senza dipendenze: socket TCP + GET
29std::string http_get(const std::string& host, const std::string& path,
30 std::chrono::milliseconds timeout)
31{
32 struct addrinfo hints{};
33 hints.ai_family = AF_UNSPEC;
34 hints.ai_socktype = SOCK_STREAM;
35 struct addrinfo* res = nullptr;
36 if (getaddrinfo(host.c_str(), "80", &hints, &res) != 0 || !res) return {};
37
38 int fd = ::socket(res->ai_family, res->ai_socktype, res->ai_protocol);
39 if (fd < 0) { freeaddrinfo(res); return {}; }
40
41 // Timeout su recv/send
42 struct timeval tv{};
43 tv.tv_sec = timeout.count() / 1000;
44 tv.tv_usec = (timeout.count() % 1000) * 1000;
45 ::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
46 ::setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
47
48 if (::connect(fd, res->ai_addr, res->ai_addrlen) < 0) {
49 ::close(fd); freeaddrinfo(res); return {};
50 }
51 freeaddrinfo(res);
52
53 std::string req = "GET " + path + " HTTP/1.0\r\n";
54 req += "Host: " + host + "\r\n";
55 req += "User-Agent: BastionGuard-SC/1.0\r\n";
56 req += "Connection: close\r\n\r\n";
57 if (::send(fd, req.data(), req.size(), 0) < 0) { ::close(fd); return {}; }
58
59 std::string response;
60 char buf[4096];
61 while (true) {
62 ssize_t n = ::recv(fd, buf, sizeof(buf), 0);
63 if (n <= 0) break;
64 response.append(buf, static_cast<size_t>(n));
65 if (response.size() > 64 * 1024) break; // antipattern abuse guard
66 }
67 ::close(fd);
68
69 // Separa body dalle headers (\r\n\r\n)
70 const auto sep = response.find("\r\n\r\n");
71 if (sep == std::string::npos) return {};
72 return response.substr(sep + 4);
73}
74
75} // namespace
76
78 static GeoResolver inst;
79 return inst;
80}
81
83{
84 worker_ = std::thread([this]() { worker_loop(); });
85}
86
88
90 running_ = false;
91 cv_.notify_all();
92 if (worker_.joinable()) worker_.join();
93}
94
95bool GeoResolver::is_private_ip(const std::string& ip) {
96 if (ip.empty()) return true;
97 // IPv4 privati: 10/8, 172.16/12, 192.168/16, 127/8, 169.254/16
98 in_addr a4{};
99 if (inet_pton(AF_INET, ip.c_str(), &a4) == 1) {
100 const std::uint32_t h = ntohl(a4.s_addr);
101 if ((h & 0xFF000000) == 0x0A000000) return true; // 10.0.0.0/8
102 if ((h & 0xFFF00000) == 0xAC100000) return true; // 172.16.0.0/12
103 if ((h & 0xFFFF0000) == 0xC0A80000) return true; // 192.168.0.0/16
104 if ((h & 0xFF000000) == 0x7F000000) return true; // 127.0.0.0/8
105 if ((h & 0xFFFF0000) == 0xA9FE0000) return true; // 169.254/16
106 return false;
107 }
108 // IPv6: fe80::/10 link-local, fc00::/7 ULA, ::1 loopback
109 in6_addr a6{};
110 if (inet_pton(AF_INET6, ip.c_str(), &a6) == 1) {
111 if ((a6.s6_addr[0] & 0xFE) == 0xFC) return true; // fc00::/7
112 if (a6.s6_addr[0] == 0xFE && (a6.s6_addr[1] & 0xC0) == 0x80) return true;
113 bool is_loopback = (a6.s6_addr[15] == 1);
114 for (int i = 0; i < 15 && is_loopback; ++i)
115 if (a6.s6_addr[i] != 0) is_loopback = false;
116 if (is_loopback) return true;
117 }
118 return false;
119}
120
121std::optional<GeoEntry> GeoResolver::resolve(const std::string& ip) {
122 if (ip.empty() || is_private_ip(ip)) {
123 GeoEntry local;
124 local.country_code = "";
125 local.country_name = "Locale";
126 local.fetched_at = std::chrono::steady_clock::now();
127 return local;
128 }
129
130 const auto now = std::chrono::steady_clock::now();
131 {
132 std::lock_guard lk(mu_);
133 auto it = cache_.find(ip);
134 if (it != cache_.end()) {
135 if (now - it->second.fetched_at < kCacheTtl) return it->second;
136 cache_.erase(it);
137 }
138 if (in_flight_.count(ip)) return std::nullopt; // gia' in coda
139 in_flight_.insert(ip);
140 queue_.push(ip);
141 }
142 cv_.notify_one();
143 return std::nullopt;
144}
145
146std::optional<GeoEntry> GeoResolver::resolve_self() {
147 // Chiave speciale per la posizione del PC locale. Il worker
148 // vedendo "__self__" in coda chiama fetch_from_ipapi("") — ip-api
149 // interpreta la richiesta senza IP come "geolocalizza il chiamante"
150 // dall'IP pubblico dal quale arriva la connessione.
151 const std::string kSelfKey = "__self__";
152
153 const auto now = std::chrono::steady_clock::now();
154 {
155 std::lock_guard lk(mu_);
156 auto it = cache_.find(kSelfKey);
157 if (it != cache_.end()) {
158 if (now - it->second.fetched_at < kCacheTtl) return it->second;
159 cache_.erase(it);
160 }
161 if (in_flight_.count(kSelfKey)) return std::nullopt;
162 in_flight_.insert(kSelfKey);
163 queue_.push(kSelfKey);
164 }
165 cv_.notify_one();
166 return std::nullopt;
167}
168
170 std::lock_guard lk(mu_);
171 callbacks_.push_back(std::move(cb));
172}
173
174std::optional<GeoEntry> GeoResolver::fetch_from_ipapi(const std::string& ip) {
175 using namespace std::chrono;
176 // Rate-limit
177 const auto now = steady_clock::now();
178 const auto since = now - last_request_;
179 if (since < kMinInterval) std::this_thread::sleep_for(kMinInterval - since);
180 last_request_ = steady_clock::now();
181
182 // ip-api.com REST API (http, no TLS per semplicita' e perche' il
183 // free tier e' comunque solo http)
184 // Fields mask: https://ip-api.com/docs/api:json
185 const std::string path = "/json/" + ip +
186 "?fields=status,countryCode,country,regionName,city,lat,lon,isp,as";
187
188 const auto body = http_get("ip-api.com", path, milliseconds(2000));
189 if (body.empty()) return std::nullopt;
190
191 try {
192 auto j = nlohmann::json::parse(body);
193 if (j.value("status", std::string{}) != "success") return std::nullopt;
194
195 GeoEntry e;
196 e.country_code = j.value("countryCode", std::string{});
197 e.country_name = j.value("country", std::string{});
198 e.region = j.value("regionName", std::string{});
199 e.city = j.value("city", std::string{});
200 e.isp = j.value("isp", std::string{});
201 e.asn = j.value("as", std::string{});
202 e.lat = j.value("lat", 0.0);
203 e.lon = j.value("lon", 0.0);
204 e.fetched_at = steady_clock::now();
205 return e;
206 } catch (...) {
207 return std::nullopt;
208 }
209}
210
212 while (running_) {
213 std::string ip;
214 {
215 std::unique_lock lk(mu_);
216 cv_.wait(lk, [this]() { return !running_ || !queue_.empty(); });
217 if (!running_) return;
218 ip = queue_.front();
219 queue_.pop();
220 }
221
222 // "__self__" = richiesta speciale per il PC locale: si chiama
223 // ip-api.com senza specificare IP, il servizio geolocalizza il
224 // chiamante dall'IP sorgente della richiesta HTTP.
225 const bool is_self = (ip == "__self__");
226 auto maybe = fetch_from_ipapi(is_self ? std::string{} : ip);
227
228 std::vector<ResolvedCallback> cbs_snapshot;
229 GeoEntry entry;
230 bool have_entry = false;
231
232 {
233 std::lock_guard lk(mu_);
234 in_flight_.erase(ip);
235 if (maybe) {
236 cache_[ip] = *maybe;
237 entry = *maybe;
238 have_entry = true;
239 }
240 cbs_snapshot = callbacks_;
241 }
242
243 if (have_entry) {
244 for (auto& cb : cbs_snapshot) {
245 try { cb(ip, entry); } catch (...) {}
246 }
247 }
248 }
249}
250
251} // namespace bsc::ui_util
#define AF_INET
Definition bsc_ebpf.c:56
#define AF_INET6
Definition bsc_ebpf.c:57
static bool is_private_ip(const std::string &ip)
std::condition_variable cv_
std::optional< GeoEntry > fetch_from_ipapi(const std::string &ip)
std::optional< GeoEntry > resolve_self()
std::chrono::steady_clock::time_point last_request_
static GeoResolver & instance()
GeoResolver(const GeoResolver &)=delete
std::vector< ResolvedCallback > callbacks_
std::unordered_map< std::string, GeoEntry > cache_
std::optional< GeoEntry > resolve(const std::string &ip)
std::atomic_bool running_
void on_resolved(ResolvedCallback cb)
std::queue< std::string > queue_
std::unordered_set< std::string > in_flight_
std::function< void(const std::string &ip, const GeoEntry &)> ResolvedCallback
std::string http_get(const std::string &host, const std::string &path, std::chrono::milliseconds timeout)
std::chrono::steady_clock::time_point fetched_at