10#include <glibmm/spawn.h>
11#include <glibmm/shell.h>
23namespace fs = std::filesystem;
28 :
iface_(std::move(iface)) {}
34 bool privileged)
const {
36 std::vector<std::string> full;
37 if (privileged) full.push_back(
"pkexec");
38 full.insert(full.end(), argv.begin(), argv.end());
43 Glib::spawn_sync(
"", full, Glib::SpawnFlags::SEARCH_PATH,
44 {}, &out, &err, &exit_status);
45 }
catch (
const Glib::Error& e) {
47 res.
output = std::string(
"spawn error: ") + e.what();
51 res.
ok = (exit_status == 0);
58 const std::string& stdin_data,
59 bool privileged)
const {
61 std::vector<std::string> full;
62 if (privileged) full.push_back(
"pkexec");
63 full.insert(full.end(), argv.begin(), argv.end());
65 std::vector<char*> c_argv;
66 c_argv.reserve(full.size() + 1);
67 for (
auto& s : full) c_argv.push_back(
const_cast<char*
>(s.c_str()));
68 c_argv.push_back(
nullptr);
70 int in_pipe[2], out_pipe[2];
71 if (pipe(in_pipe) != 0 || pipe(out_pipe) != 0) {
72 res.
ok =
false; res.
output =
"pipe() fallita";
return res;
75 if (pid < 0) { res.
ok =
false; res.
output =
"fork() fallita";
return res; }
77 dup2(in_pipe[0], STDIN_FILENO);
78 dup2(out_pipe[1], STDOUT_FILENO);
79 dup2(out_pipe[1], STDERR_FILENO);
80 close(in_pipe[0]); close(in_pipe[1]);
81 close(out_pipe[0]); close(out_pipe[1]);
82 execvp(c_argv[0], c_argv.data());
85 close(in_pipe[0]); close(out_pipe[1]);
87 while (w < stdin_data.size()) {
88 ssize_t n = write(in_pipe[1], stdin_data.data() + w, stdin_data.size() - w);
94 std::string output;
char buf[4096]; ssize_t n;
95 while ((n = read(out_pipe[0], buf,
sizeof(buf))) > 0)
96 output.append(buf, (
size_t)n);
110 const char* p = std::getenv(
"PATH");
111 if (!p) p =
"/usr/sbin:/usr/bin:/sbin:/bin";
112 std::string paths(p);
114 while (start <= paths.size()) {
115 size_t end = paths.find(
':', start);
116 std::string dir = paths.substr(start,
117 end == std::string::npos ? std::string::npos : end - start);
118 if (!dir.empty() && ::access((dir +
"/" + bin).c_str(), X_OK) == 0)
return true;
119 if (end == std::string::npos)
break;
126 if (!
in_path_(
"wg")) {
if (missing) *missing =
"wg";
return false; }
127 if (!
in_path_(
"ip")) {
if (missing) *missing =
"ip";
return false; }
134static std::string
strip_(std::string s) {
135 size_t a = s.find_first_not_of(
" \t\r\n");
136 size_t b = s.find_last_not_of(
" \t\r\n");
137 if (a == std::string::npos)
return {};
138 return s.substr(a, b - a + 1);
141static std::vector<std::string>
split_csv_(
const std::string& s) {
142 std::vector<std::string> out;
143 std::string tok; std::istringstream iss(s);
144 while (std::getline(iss, tok,
',')) {
145 std::string t =
strip_(tok);
146 if (!t.empty()) out.push_back(t);
152 if (s.empty() || s.find_first_not_of(
"0123456789") != std::string::npos)
155 unsigned long v = std::stoul(s);
156 return v >= min && v <= max;
164 static const std::regex re(R
"(^[A-Za-z0-9+/]{43}=$)");
165 return std::regex_match(s, re);
169 struct in_addr a4 {};
170 struct in6_addr a6 {};
171 return ::inet_pton(
AF_INET, s.c_str(), &a4) == 1 ||
172 ::inet_pton(
AF_INET6, s.c_str(), &a6) == 1;
176 const auto slash = s.rfind(
'/');
177 if (slash == std::string::npos || slash == 0 || slash + 1 >= s.size())
179 const std::string ip = s.substr(0, slash);
180 const std::string prefix = s.substr(slash + 1);
183 struct in_addr a4 {};
184 if (::inet_pton(
AF_INET, ip.c_str(), &a4) == 1)
185 return std::stoul(prefix) <= 32;
186 struct in6_addr a6 {};
187 if (::inet_pton(
AF_INET6, ip.c_str(), &a6) == 1)
188 return std::stoul(prefix) <= 128;
193 if (host.empty() || host.size() > 253 || host.front() ==
'.' || host.back() ==
'.')
195 static const std::regex re(R
"(^[A-Za-z0-9.-]+$)");
196 if (!std::regex_match(host, re) || host.find(
"..") != std::string::npos)
198 std::istringstream labels(host);
200 while (std::getline(labels, label,
'.')) {
201 if (label.empty() || label.size() > 63 || label.front() ==
'-' || label.back() ==
'-')
208 std::string host, port;
209 if (!endpoint.empty() && endpoint.front() ==
'[') {
210 const auto close = endpoint.find(
']');
211 if (close == std::string::npos || close + 1 >= endpoint.size() || endpoint[close + 1] !=
':')
213 host = endpoint.substr(1, close - 1);
214 port = endpoint.substr(close + 2);
215 struct in6_addr a6 {};
216 if (::inet_pton(
AF_INET6, host.c_str(), &a6) != 1)
return false;
218 const auto colon = endpoint.rfind(
':');
219 if (colon == std::string::npos || colon == 0 || colon + 1 >= endpoint.size())
221 host = endpoint.substr(0, colon);
222 port = endpoint.substr(colon + 1);
223 struct in_addr a4 {};
231 static const std::regex re(R
"(^[A-Za-z0-9_.-]{1,15}$)");
232 return std::regex_match(iface, re);
236 auto fail = [&](
const char* msg) {
240 if (!sp.
valid())
return fail(
"Configurazione WireGuard incompleta.");
243 return fail(
"ListenPort non valida.");
244 if (sp.
mtu != 0 && (sp.
mtu < 576 || sp.
mtu > 9000))
245 return fail(
"MTU fuori intervallo consentito.");
248 if (!
valid_cidr_(a))
return fail(
"Address deve essere un IP/CIDR valido.");
249 for (
const auto& ns : sp.
dns)
250 if (!
valid_ip_(ns))
return fail(
"DNS deve essere un indirizzo IP valido.");
252 for (
const auto& peer : sp.
peers) {
253 if (!
valid_wg_key_(peer.public_key))
return fail(
"PublicKey peer non valida.");
254 if (!peer.preshared_key.empty() && !
valid_wg_key_(peer.preshared_key))
255 return fail(
"PresharedKey non valida.");
256 if (!
valid_endpoint_(peer.endpoint))
return fail(
"Endpoint WireGuard non valido.");
257 for (
const auto& cidr :
split_csv_(peer.allowed_ips))
258 if (!
valid_cidr_(cidr))
return fail(
"AllowedIPs contiene un CIDR non valido.");
259 if (!peer.persistent_keepalive.empty() &&
261 return fail(
"PersistentKeepalive non valido.");
268 std::ifstream in(conf_path);
269 if (!in) {
if (err) *err =
"Impossibile aprire: " + conf_path;
return false; }
272 enum class Section { None, Interface, Peer } sec = Section::None;
275 auto flush_peer = [&]() {
277 sp.
peers.push_back(cur_peer);
283 while (std::getline(in, line)) {
284 std::string t =
strip_(line);
285 if (t.empty() || t[0] ==
'#' || t[0] ==
';')
continue;
287 if (t.front() ==
'[' && t.back() ==
']') {
288 std::string name = t.substr(1, t.size() - 2);
290 for (
auto& c : name) c = (char)std::tolower((
unsigned char)c);
291 if (name ==
"interface") { flush_peer(); sec = Section::Interface; }
292 else if (name ==
"peer") { flush_peer(); sec = Section::Peer; }
293 else sec = Section::None;
298 size_t eq = t.find(
'=');
299 if (eq == std::string::npos)
continue;
300 std::string k =
strip_(t.substr(0, eq));
301 std::string v =
strip_(t.substr(eq + 1));
303 for (
auto& c : kl) c = (char)std::tolower((
unsigned char)c);
305 if (sec == Section::Interface) {
310 else if (kl ==
"mtu") sp.
mtu = std::atoi(v.c_str());
312 }
else if (sec == Section::Peer) {
313 if (kl ==
"publickey") cur_peer.
public_key = v;
315 else if (kl ==
"endpoint") cur_peer.
endpoint = v;
316 else if (kl ==
"allowedips") cur_peer.
allowed_ips = v;
323 spec_ = std::move(sp);
331 if (err) *err =
"Campi obbligatori mancanti.";
346 sp.
peers.push_back(peer);
349 spec_ = std::move(sp);
358 if (!gen.
ok)
return std::nullopt;
360 if (priv.empty())
return std::nullopt;
362 if (!pub.
ok)
return std::nullopt;
364 if (pk.empty())
return std::nullopt;
372 std::ostringstream os;
373 os <<
"[Interface]\n";
378 os <<
"PublicKey = " << p.public_key <<
"\n";
379 if (!p.preshared_key.empty())
380 os <<
"PresharedKey = " << p.preshared_key <<
"\n";
381 if (!p.endpoint.empty())
382 os <<
"Endpoint = " << p.endpoint <<
"\n";
383 if (!p.allowed_ips.empty())
384 os <<
"AllowedIPs = " << p.allowed_ips <<
"\n";
385 if (!p.persistent_keepalive.empty())
386 os <<
"PersistentKeepalive = " << p.persistent_keepalive <<
"\n";
395 for (
const auto& p : sp.
peers)
396 for (
const auto& a :
split_csv_(p.allowed_ips))
397 if (a ==
"0.0.0.0/0" || a ==
"::/0")
return true;
406 std::string validation_error;
410 res.
output = validation_error.empty() ?
"Interfaccia WireGuard non valida." : validation_error;
414 auto fail = [](
const std::string& step,
const OpResult& r) {
417 out.
output = step + (r.output.empty() ? std::string{} :
": " + r.output);
422 run({
"ip",
"link",
"delete",
"dev",
iface_},
true);
424 OpResult r =
run({
"ip",
"link",
"add",
"dev",
iface_,
"type",
"wireguard"},
true);
425 if (!r.ok)
return fail(
"ip link add", r);
430 run({
"ip",
"link",
"delete",
"dev",
iface_},
true);
431 return fail(
"wg setconf", r);
435 r =
run({
"ip",
"link",
"set",
"dev",
iface_,
"mtu", std::to_string(
spec_.
mtu)},
true);
436 if (!r.ok)
return fail(
"ip link mtu", r);
440 const std::string fam = (a.find(
':') != std::string::npos) ?
"-6" :
"-4";
441 r =
run({
"ip", fam,
"address",
"add", a,
"dev",
iface_},
true);
442 if (!r.ok)
return fail(
"ip address add", r);
445 r =
run({
"ip",
"link",
"set",
"dev",
iface_,
"up"},
true);
446 if (!r.ok)
return fail(
"ip link up", r);
451 run({
"ip",
"-4",
"route",
"add",
"default",
"dev",
iface_,
"table",
"51820"},
true);
452 run({
"ip",
"-4",
"rule",
"add",
"not",
"from",
"all",
"fwmark",
"0x51820",
453 "lookup",
"51820",
"priority",
"32764"},
true);
454 run({
"ip",
"-4",
"rule",
"add",
"table",
"main",
"suppress_prefixlength",
"0",
455 "priority",
"32765"},
true);
456 run({
"wg",
"set",
iface_,
"fwmark",
"0x51820"},
true);
457 run({
"sysctl",
"-q",
"net.ipv4.conf.all.src_valid_mark=1"},
true);
460 for (
const auto& cidr :
split_csv_(peer.allowed_ips)) {
461 if (cidr ==
"0.0.0.0/0" || cidr ==
"::/0")
continue;
462 const std::string fam = (cidr.find(
':') != std::string::npos) ?
"-6" :
"-4";
463 run({
"ip", fam,
"route",
"add", cidr,
"dev",
iface_},
true);
469 static const std::string bak =
"/etc/resolv.conf.bastionguard.bak";
470 if (!fs::exists(bak)) {
471 run({
"cp",
"-a",
"--no-dereference",
"--",
"/etc/resolv.conf", bak},
true);
474 std::ostringstream dns_text;
476 dns_text <<
"nameserver " << ns <<
"\n";
478 r =
run_with_stdin({
"tee",
"/etc/resolv.conf"}, dns_text.str(),
true);
479 if (!r.ok)
return fail(
"aggiornamento DNS", r);
484 res.
output =
"Tunnel WireGuard attivato.";
492 static const std::string bak =
"/etc/resolv.conf.bastionguard.bak";
494 if (fs::exists(bak)) {
495 run({
"rm",
"-f",
"--",
"/etc/resolv.conf"},
true);
496 run({
"cp",
"-a",
"--no-dereference",
"--", bak,
"/etc/resolv.conf"},
true);
497 run({
"rm",
"-f",
"--", bak},
true);
500 run({
"ip",
"-4",
"rule",
"del",
"table",
"main",
"suppress_prefixlength",
"0",
501 "priority",
"32765"},
true);
502 run({
"ip",
"-4",
"rule",
"del",
"not",
"from",
"all",
"fwmark",
"0x51820",
503 "lookup",
"51820",
"priority",
"32764"},
true);
504 run({
"ip",
"-4",
"route",
"flush",
"table",
"51820"},
true);
505 run({
"ip",
"link",
"delete",
"dev",
iface_},
true);
510 res.exit_code = res.ok ? 0 : 1;
511 res.output = res.ok ?
"Tunnel WireGuard disattivato." :
"Interfaccia WireGuard ancora presente.";
525 if (!link.
ok) { st.
up =
false;
return st; }
532 if (!r.ok)
return st;
533 std::istringstream iss(r.output);
535 while (std::getline(iss, line)) {
536 std::string t =
strip_(line);
537 if (t.rfind(
"endpoint:", 0) == 0)
539 else if (t.rfind(
"latest handshake:", 0) == 0)
547 static const char* endpoints[] = {
548 "https://api.ipify.org",
549 "https://ifconfig.me/ip",
550 "https://icanhazip.com",
551 "https://ipinfo.io/ip",
553 for (
const char* url : endpoints) {
554 OpResult r =
run({
"curl",
"-s",
"-4",
"--max-time",
"10", url},
false);
556 std::string ip =
strip_(r.output);
558 if (!ip.empty() && ip.find_first_not_of(
"0123456789.") == std::string::npos)
572 return OpResult{
false, -1,
"Interfaccia WireGuard non valida."};
575 std::string endpoint_rule;
580 if (close != std::string::npos) host = st.
endpoint.substr(1, close - 1);
583 if (pos != std::string::npos) host = st.
endpoint.substr(0, pos);
586 struct in_addr a4 {};
587 struct in6_addr a6 {};
588 if (::inet_pton(
AF_INET, host.c_str(), &a4) == 1)
589 endpoint_rule =
" ip daddr " + host +
" accept\n";
590 else if (::inet_pton(
AF_INET6, host.c_str(), &a6) == 1)
591 endpoint_rule =
" ip6 daddr " + host +
" accept\n";
594 std::ostringstream rules;
595 rules <<
"table inet " <<
kKsTable <<
" {\n"
597 <<
" type filter hook output priority 0; policy drop;\n"
598 <<
" oifname lo accept\n"
599 <<
" oifname \"" <<
iface_ <<
"\" accept\n"
601 <<
" meta l4proto udp accept\n"
619 return run({
"nft",
"list",
"table",
"inet",
kKsTable},
true).ok;
OpResult run(const std::vector< std::string > &argv, bool privileged) const
bool load_from_conf(const std::string &conf_path, std::string *err=nullptr)
WireguardBackend(std::string iface="wg0")
OpResult run_with_stdin(const std::vector< std::string > &argv, const std::string &stdin_data, bool privileged) const
OpResult killswitch_disable()
bool load_from_params(const WgConfigParams &p, std::string *err=nullptr)
OpResult killswitch_enable()
bool tooling_available(std::string *missing=nullptr) const
std::optional< WgKeyPair > generate_keys() const
bool killswitch_active() const
std::optional< std::string > public_ip() const
std::string build_wg_setconf_text() const
static bool valid_ip_(const std::string &s)
static bool valid_cidr_(const std::string &s)
static bool validate_spec_(const WgTunnelSpec &sp, std::string *err)
static const char * kKsTable
static std::string strip_(std::string s)
static std::vector< std::string > split_csv_(const std::string &s)
static bool valid_wg_key_(const std::string &s)
static bool parse_uint_range_(const std::string &s, unsigned min, unsigned max)
static bool valid_iface_(const std::string &iface)
static bool valid_hostname_(const std::string &host)
static bool in_path_(const char *bin)
static bool needs_default_route_(const WgTunnelSpec &sp)
static bool valid_endpoint_(const std::string &endpoint)
std::string server_public_key
std::string persistent_keepalive
std::string persistent_keepalive
std::string preshared_key
std::string last_handshake
std::vector< WgPeer > peers
std::vector< std::string > dns
std::vector< std::string > addresses