You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

803 lines
30 KiB
C++

// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "networkmanager/vpnutils.h"
#include "networkmanager/client.h"
#include "networkmanager/inet.h"
#include "networkmanager/text.h"
#include "networkmanager/variants.h"
#include "networkmanager/vpnschema.h"
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <endian.h>
#include <filesystem>
#include <fstream>
#include <sstream>
namespace cutefish::networkmanager::vpn {
using base::dbus::Variant;
using base::dbus::VariantMap;
using namespace variants;
static const std::string servicePrefix = "org.freedesktop.NetworkManager.";
namespace {
struct Plugin
{
std::string name;
std::vector<std::string> services;
};
struct PluginCache
{
std::vector<std::string> directories{"/etc/NetworkManager/VPN", "/usr/lib/NetworkManager/VPN"};
std::vector<std::filesystem::file_time_type> stamps;
bool read = false;
std::vector<Plugin> plugins;
};
} // namespace
static PluginCache& cache()
{
static PluginCache cache;
return cache;
}
static const VariantMap& group(const Settings& settings, const std::string& name)
{
static const VariantMap empty;
const auto found = settings.find(name);
return found == settings.end() ? empty : found->second;
}
static bool contains(const std::vector<std::string>& list, const std::string& value)
{
return std::find(list.begin(), list.end(), value) != list.end();
}
// The [VPN Connection] group of a plugin's .name file.
static Plugin readNameFile(const std::filesystem::path& path)
{
Plugin plugin;
std::ifstream file(path);
if (!file)
return plugin;
bool inGroup = false;
std::string line;
while (std::getline(file, line)) {
line = text::trimmed(line);
if (!line.empty() && line[0] == '[') {
inGroup = line == "[VPN Connection]";
continue;
}
const size_t equals = line.find('=');
if (!inGroup || equals == std::string::npos)
continue;
const std::string key = text::trimmed(line.substr(0, equals));
const std::string value = text::trimmed(line.substr(equals + 1));
if (key == "name") {
plugin.name = value;
} else if (key == "service") {
plugin.services.insert(plugin.services.begin(), value);
} else if (key == "aliases") {
for (const std::string& alias : text::split(value, ';'))
plugin.services.push_back(alias);
}
}
return plugin;
}
// Read again only when a directory changed: plugins come and go with packages.
static const std::vector<Plugin>& plugins()
{
PluginCache& cache = ::cutefish::networkmanager::vpn::cache();
std::vector<std::filesystem::file_time_type> stamps;
for (const std::string& directory : cache.directories) {
std::error_code error;
stamps.push_back(std::filesystem::last_write_time(directory, error));
}
if (cache.read && stamps == cache.stamps)
return cache.plugins;
cache.read = true;
cache.stamps = stamps;
cache.plugins.clear();
for (const std::string& directory : cache.directories) {
std::vector<std::filesystem::path> files;
std::error_code error;
for (const auto& entry : std::filesystem::directory_iterator(directory, error)) {
if (entry.is_regular_file() && entry.path().extension() == ".name")
files.push_back(entry.path());
}
std::sort(files.begin(), files.end());
for (const auto& file : files) {
Plugin plugin = readNameFile(file);
if (!plugin.name.empty() && !plugin.services.empty())
cache.plugins.push_back(std::move(plugin));
}
}
return cache.plugins;
}
static std::string shortName(const std::string& serviceType)
{
for (const Plugin& plugin : plugins()) {
if (contains(plugin.services, serviceType))
return plugin.name;
}
if (text::startsWith(serviceType, servicePrefix))
return serviceType.substr(servicePrefix.size());
const size_t dot = serviceType.rfind('.');
return dot == std::string::npos ? serviceType : serviceType.substr(dot + 1);
}
static bool decodeBase64(const std::string& text, std::string* out)
{
static const std::string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string data = text;
while (!data.empty() && data.back() == '=')
data.pop_back();
if (text.size() - data.size() > 2 || data.size() % 4 == 1)
return false;
std::string decoded;
uint32_t buffer = 0;
int bits = 0;
for (const char c : data) {
const size_t index = alphabet.find(c);
if (index == std::string::npos)
return false;
buffer = (buffer << 6) | uint32_t(index);
bits += 6;
if (bits >= 8) {
bits -= 8;
decoded += char((buffer >> bits) & 0xff);
}
}
*out = decoded;
return true;
}
static bool isWireGuardKey(const std::string& value)
{
std::string decoded;
return decodeBase64(value, &decoded) && decoded.size() == 32;
}
static bool parseUInt(const std::string& text, uint32_t* value, int base = 10)
{
if (text.empty() || text[0] == '-' || text[0] == '+' || std::isspace(static_cast<unsigned char>(text[0])))
return false;
char* end = nullptr;
const unsigned long long number = std::strtoull(text.c_str(), &end, base);
if (!end || *end != '\0' || number > 0xffffffffull)
return false;
*value = uint32_t(number);
return true;
}
// "address" or "address/prefix" into NetworkManager's address-data entry.
static bool parseWireGuardAddress(const std::string& word, VariantMap& out, bool& v4)
{
const size_t slash = word.find('/');
const std::string address = inet::normalized(word.substr(0, slash));
if (address.empty())
return false;
v4 = inet::family(address) == 4;
int prefix = v4 ? 32 : 128;
if (slash != std::string::npos) {
const std::string length = word.substr(slash + 1);
char* end = nullptr;
const long parsed = std::strtol(length.c_str(), &end, 10);
if (length.empty() || !end || *end != '\0' || parsed < 0 || parsed > (v4 ? 32 : 128))
return false;
prefix = int(parsed);
}
out = {{"address", Variant(address)}, {"prefix", Variant(uint32_t(prefix))}};
return true;
}
// Servers go by family, anything else is a search domain.
static void parseWireGuardDns(const std::vector<std::string>& words, std::vector<uint32_t>& dns4,
std::vector<std::vector<uint8_t>>& dns6, std::vector<std::string>& search)
{
for (const std::string& word : words) {
if (const auto v4 = inet::parseIpv4(word))
dns4.push_back(htobe32(*v4));
else if (const auto v6 = inet::parseIpv6(word))
dns6.emplace_back(v6->begin(), v6->end());
else
search.push_back(word);
}
}
// host[:port] as the setting wants it: a bare IPv6 address gets brackets, as
// NetworkManager would take its last group for the port, and the port
// defaults to 51820.
static std::string wireGuardEndpoint(const std::string& server)
{
if (server.empty())
return {};
std::string endpoint = server;
const size_t lastColon = server.rfind(':');
if (inet::family(server) == 6
&& (lastColon == std::string::npos || inet::family(server.substr(0, lastColon)) == 0))
endpoint = "[" + server + "]";
if (endpoint.find(':') == std::string::npos || text::endsWith(endpoint, "]"))
endpoint += ":51820";
return endpoint;
}
// Interface names are at most 15 characters of a few kinds.
static std::string wireGuardInterfaceName(const std::string& name)
{
std::string interface;
for (const char c : name) {
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '-')
interface += c;
}
interface = interface.substr(0, 15);
return interface.empty() ? std::string("wg0") : interface;
}
static std::vector<std::string> trimmedWords(const std::string& value)
{
std::vector<std::string> words;
for (const std::string& word : text::split(value, ',')) {
std::string trimmed = text::trimmed(word);
if (!trimmed.empty())
words.push_back(std::move(trimmed));
}
return words;
}
bool isVpn(const Client& client, const std::string& connection)
{
if (!client.readable(connection))
return false;
const std::string type = toString(client.setting(connection, "connection", "type"));
if (type != "vpn" && type != "wireguard")
return false;
// A port belongs to the profile it is a port of.
return toString(client.setting(connection, "connection", "controller")).empty()
&& toString(client.setting(connection, "connection", "master")).empty();
}
std::string kind(const Client& client, const std::string& connection)
{
if (toString(client.setting(connection, "connection", "type")) == "wireguard")
return "wireguard";
return shortName(toString(client.setting(connection, "vpn", "service-type")));
}
std::string kindLabel(const std::string& kind)
{
if (kind == "wireguard")
return "WireGuard";
if (kind == "openvpn")
return "OpenVPN";
if (kind == "l2tp")
return "L2TP";
if (kind == "pptp")
return "PPTP";
if (kind == "sstp")
return "SSTP";
if (kind == "vpnc")
return "Cisco VPN";
if (kind == "openconnect")
return "OpenConnect";
if (kind == "strongswan" || kind == "libreswan")
return "IPsec";
if (kind == "fortisslvpn")
return "Fortinet SSL VPN";
return kind.empty() ? std::string("VPN") : kind;
}
std::string package(const std::string& kind)
{
return "network-manager-" + kind;
}
bool pluginInstalled(const std::string& serviceType)
{
for (const Plugin& plugin : plugins()) {
if (contains(plugin.services, serviceType))
return true;
}
return false;
}
std::vector<std::string> installedPlugins()
{
std::vector<std::string> names;
for (const Plugin& plugin : plugins()) {
if (!contains(names, plugin.name))
names.push_back(plugin.name);
}
return names;
}
void setPluginDirectories(const std::vector<std::string>& directories)
{
cache() = PluginCache();
cache().directories = directories;
}
std::string activeConnection(const Client& client, const std::string& connection)
{
for (const std::string& active : client.activeConnections()) {
if (objectPath(client.property(active, activeInterface, "Connection")) == connection)
return active;
}
return {};
}
ConnectionStatus state(const Client& client, const std::string& active)
{
if (active.empty() || !client.has(active, activeInterface))
return ConnectionStatus::Deactivated;
// NM_VPN_CONNECTION_STATE_*: the tunnel is up only once the plugin says so.
if (client.has(active, vpnConnectionInterface)) {
const uint32_t vpnState = toUInt(client.property(active, vpnConnectionInterface, "VpnState"));
if (vpnState >= 1 && vpnState <= 4)
return ConnectionStatus::Activating;
if (vpnState == 5)
return ConnectionStatus::Activated;
if (vpnState >= 6)
return ConnectionStatus::Deactivated;
}
return ConnectionStatus(toInt(client.property(active, activeInterface, "State")));
}
std::string failureMessage(uint32_t reason)
{
// NM_ACTIVE_CONNECTION_STATE_REASON_*.
switch (reason) {
case 1:
case 2:
case 9:
case 11:
return {};
case 3:
return "The network connection was interrupted.";
case 4:
return "The VPN service stopped unexpectedly.";
case 5:
return "The VPN service returned invalid configuration.";
case 6:
return "The connection attempt timed out.";
case 7:
return "The VPN service did not start in time.";
case 8:
return "The VPN service failed to start.";
case 10:
return "Sign-in failed. Check the user name and password.";
default:
return "The VPN service reported an error.";
}
}
std::string userNameKey(const std::string& serviceType, const std::map<std::string, std::string>& data)
{
const auto valueOf = [&data](const std::string& key) {
const auto found = data.find(key);
return found == data.end() ? std::string() : found->second;
};
const std::string kind = shortName(serviceType);
if (kind == "openvpn") {
const std::string type = valueOf("connection-type");
return type == "password" || type == "password-tls" ? std::string("username") : std::string();
}
if (kind == "l2tp") {
const std::string type = valueOf("user-auth-type");
return type.empty() || type == "password" ? std::string("user") : std::string();
}
if (kind == "pptp" || kind == "sstp" || kind == "fortisslvpn")
return "user";
if (kind == "vpnc")
return "Xauth username";
if (kind == "strongswan" && valueOf("method") == "eap")
return "user";
return {};
}
std::vector<Kind> manualKinds()
{
std::vector<Kind> kinds;
kinds.push_back({"wireguard", "WireGuard", true, package("wireguard")});
for (const std::string& kind : schema::kinds()) {
// The two IPsec plugins share a label on the list of VPNs, but not here.
std::string label = kindLabel(kind);
if (kind == "strongswan")
label = "IPsec (strongSwan)";
else if (kind == "libreswan")
label = "IPsec (Libreswan)";
else if (kind == "ssh")
label = "SSH";
else if (kind == "iodine")
label = "DNS tunnel (iodine)";
kinds.push_back({kind, label, pluginInstalled(servicePrefix + kind), package(kind)});
}
return kinds;
}
bool isWireGuardConfig(const std::string& content)
{
return content.find("[Interface]") != std::string::npos && content.find("PrivateKey") != std::string::npos;
}
std::optional<Settings> wireGuardProfile(const std::string& content, const std::string& name, std::string* error)
{
const auto fail = [error](const std::string& message) -> std::optional<Settings> {
if (error)
*error = message;
return std::nullopt;
};
enum { None, Interface, Peer } section = None;
std::string privateKey;
uint32_t listenPort = 0;
uint32_t fwmark = 0;
uint32_t mtu = 0;
bool peerRoutes = true;
std::vector<VariantMap> addresses[2];
std::vector<uint32_t> dns4;
std::vector<std::vector<uint8_t>> dns6;
std::vector<std::string> search;
std::vector<VariantMap> peers;
// Like `wg setconf`: whitespace anywhere is dropped and '#' starts a comment.
std::vector<std::string> lines;
{
std::string line;
std::istringstream stream(content);
while (std::getline(stream, line))
lines.push_back(line);
if (!content.empty() && content.back() == '\n')
lines.emplace_back();
}
for (size_t number = 1; number <= lines.size(); ++number) {
std::string line;
for (const char c : lines[number - 1].substr(0, lines[number - 1].find('#'))) {
if (!std::isspace(static_cast<unsigned char>(c)))
line += c;
}
if (line.empty())
continue;
if (text::equalsIgnoringCase(line, "[Interface]")) {
section = Interface;
continue;
}
if (text::equalsIgnoringCase(line, "[Peer]")) {
section = Peer;
peers.emplace_back();
continue;
}
const size_t equals = line.find('=');
const std::string field = line.substr(0, equals);
const std::string value = equals == std::string::npos ? std::string() : line.substr(equals + 1);
const std::vector<std::string> words = text::split(value, ',');
const std::string invalid = text::arg("Line %1 has an invalid %2.", {std::to_string(number), field});
const auto is = [&field](const char* name) { return text::equalsIgnoringCase(field, name); };
bool ok = true;
if (section == Interface && is("PrivateKey")) {
if (!isWireGuardKey(value))
return fail(invalid);
privateKey = value;
} else if (section == Interface && is("Address")) {
for (const std::string& word : words) {
bool v4 = false;
VariantMap address;
if (!parseWireGuardAddress(word, address, v4))
return fail(invalid);
addresses[v4 ? 0 : 1].push_back(address);
}
} else if (section == Interface && is("DNS")) {
parseWireGuardDns(words, dns4, dns6, search);
} else if (section == Interface && is("MTU")) {
ok = parseUInt(value, &mtu);
} else if (section == Interface && is("ListenPort")) {
ok = parseUInt(value, &listenPort) && listenPort <= 0xffff;
} else if (section == Interface && is("FwMark")) {
fwmark = 0;
ok = value == "off" || parseUInt(value, &fwmark, 0);
} else if (section == Interface && is("Table")) {
// Route tables by number or name aren't carried over; "off" means no routes.
peerRoutes = value != "off";
} else if (section == Interface
&& (is("PreUp") || is("PreDown") || is("PostUp") || is("PostDown") || is("SaveConfig"))) {
// No scripts are run.
} else if (section == Peer && is("PublicKey")) {
ok = isWireGuardKey(value);
peers.back()["public-key"] = Variant(value);
} else if (section == Peer && is("PresharedKey")) {
ok = isWireGuardKey(value);
peers.back()["preshared-key"] = Variant(value);
peers.back()["preshared-key-flags"] = Variant(uint32_t(SecretSystemOwned));
} else if (section == Peer && is("Endpoint")) {
ok = value.find(':') != std::string::npos;
peers.back()["endpoint"] = Variant(value);
} else if (section == Peer && is("AllowedIPs")) {
std::vector<std::string> allowed = toStringList(variants::value(peers.back(), "allowed-ips"));
allowed.insert(allowed.end(), words.begin(), words.end());
peers.back()["allowed-ips"] = Variant(allowed);
} else if (section == Peer && is("PersistentKeepalive")) {
uint32_t seconds = 0;
ok = value == "off" || parseUInt(value, &seconds);
ok = ok && seconds <= 0xffff;
peers.back()["persistent-keepalive"] = Variant(seconds);
} else {
return fail(text::arg("Line %1 isn't part of a WireGuard configuration.", {std::to_string(number)}));
}
if (!ok)
return fail(invalid);
}
if (privateKey.empty())
return fail("The configuration has no private key.");
for (const VariantMap& peer : peers) {
if (!variants::contains(peer, "public-key"))
return fail("A peer has no public key.");
}
Settings profile;
profile["connection"] = {
{"id", Variant(name)},
{"uuid", Variant(text::newUuid())},
{"type", Variant(std::string("wireguard"))},
{"interface-name", Variant(wireGuardInterfaceName(name))},
};
profile["wireguard"] = {
{"private-key", Variant(privateKey)},
{"private-key-flags", Variant(uint32_t(SecretSystemOwned))},
{"listen-port", Variant(listenPort)},
{"fwmark", Variant(fwmark)},
{"mtu", Variant(mtu)},
{"peer-routes", Variant(peerRoutes)},
{"peers", Variant(peers)},
};
for (int family = 0; family < 2; ++family) {
VariantMap ip;
ip["method"] = Variant(std::string(addresses[family].empty() ? "disabled" : "manual"));
if (!addresses[family].empty()) {
ip["address-data"] = Variant(addresses[family]);
// DNS needs an address of its family; without search domains, the
// server answers for every name, as wg-quick has it.
const bool hasDns = family == 0 ? !dns4.empty() : !dns6.empty();
if (hasDns) {
ip["dns"] = family == 0 ? Variant(dns4) : Variant(dns6);
ip["dns-search"] = Variant(search.empty() ? std::vector<std::string>{"~"} : search);
}
}
profile[family == 0 ? "ipv4" : "ipv6"] = ip;
}
return profile;
}
WireGuardForm wireGuardForm(const Settings& settings)
{
const VariantMap& wireguard = group(settings, "wireguard");
std::vector<std::string> addresses;
std::vector<std::string> dns;
for (const char* family : {"ipv4", "ipv6"}) {
const VariantMap& ip = group(settings, family);
for (const VariantMap& address : toMapList(value(ip, "address-data")))
addresses.push_back(toString(value(address, "address")) + "/" + std::to_string(toUInt(value(address, "prefix"))));
if (std::string(family) == "ipv4") {
for (const uint32_t server : toUIntList(value(ip, "dns")))
dns.push_back(inet::formatIpv4(be32toh(server)));
} else {
for (const std::string& server : toBytesList(value(ip, "dns"))) {
std::string bytes = server.substr(0, 16);
bytes.resize(16, '\0');
dns.push_back(inet::formatIpv6Bytes(bytes));
}
}
// Both families carry the same search domains.
for (const std::string& domain : toStringList(value(ip, "dns-search"))) {
if (domain != "~" && std::find(dns.begin(), dns.end(), domain) == dns.end())
dns.push_back(domain);
}
}
WireGuardForm form;
for (const VariantMap& peer : toMapList(value(wireguard, "peers"))) {
WireGuardPeer row;
row.publicKey = toString(value(peer, "public-key"));
row.hasPresharedKey = variants::contains(peer, "preshared-key")
|| (variants::contains(peer, "preshared-key-flags")
&& !(toUInt(value(peer, "preshared-key-flags")) & SecretNotRequired));
row.endpoint = toString(value(peer, "endpoint"));
row.allowedIps = text::join(toStringList(value(peer, "allowed-ips")), ", ");
const uint32_t keepalive = toUInt(value(peer, "persistent-keepalive"));
row.keepalive = keepalive > 0 ? std::to_string(keepalive) : std::string();
form.peers.push_back(std::move(row));
}
const auto number = [&wireguard](const char* key) {
const uint32_t number = toUInt(value(wireguard, key));
return number > 0 ? std::to_string(number) : std::string();
};
form.interfaceName = toString(value(group(settings, "connection"), "interface-name"));
form.addresses = text::join(addresses, ", ");
form.dns = text::join(dns, ", ");
form.listenPort = number("listen-port");
form.fwmark = number("fwmark");
form.mtu = number("mtu");
form.peerRoutes = !variants::contains(wireguard, "peer-routes") || toBool(value(wireguard, "peer-routes"));
return form;
}
std::optional<Settings> wireGuardSettings(const WireGuardForm& form, const Settings& existing, std::string* error)
{
const auto fail = [error](const std::string& message) -> std::optional<Settings> {
if (error)
*error = message;
return std::nullopt;
};
const VariantMap& stored = group(existing, "wireguard");
const std::string privateKey = text::trimmed(form.privateKey);
if (privateKey.empty() && !variants::contains(stored, "private-key-flags"))
return fail("Enter the private key.");
if (!privateKey.empty() && !isWireGuardKey(privateKey))
return fail("The private key isn't a WireGuard key.");
std::vector<VariantMap> addresses[2];
for (const std::string& word : trimmedWords(form.addresses)) {
bool v4 = false;
VariantMap address;
if (!parseWireGuardAddress(word, address, v4))
return fail(text::arg("%1 isn't an address with a prefix, like 10.0.0.2/24.", {word}));
addresses[v4 ? 0 : 1].push_back(address);
}
if (addresses[0].empty() && addresses[1].empty())
return fail("Enter the address.");
std::vector<uint32_t> dns4;
std::vector<std::vector<uint8_t>> dns6;
std::vector<std::string> search;
parseWireGuardDns(trimmedWords(form.dns), dns4, dns6, search);
uint32_t listenPort = 0;
if (!form.listenPort.empty() && (!parseUInt(form.listenPort, &listenPort) || listenPort > 0xffff))
return fail("The listen port must be a number up to 65535.");
uint32_t fwmark = 0;
if (!form.fwmark.empty() && !parseUInt(form.fwmark, &fwmark, 0))
return fail("The firewall mark must be a number.");
uint32_t mtu = 0;
if (!form.mtu.empty() && !parseUInt(form.mtu, &mtu))
return fail("The MTU must be a number.");
const std::vector<VariantMap> storedPeers = toMapList(value(stored, "peers"));
std::vector<VariantMap> peers;
if (form.peers.empty())
return fail("Add the server as a peer.");
for (const WireGuardPeer& formPeer : form.peers) {
const std::string publicKey = text::trimmed(formPeer.publicKey);
if (!isWireGuardKey(publicKey))
return fail("A peer's public key isn't a WireGuard key.");
VariantMap peer{{"public-key", Variant(publicKey)}};
const std::string presharedKey = text::trimmed(formPeer.presharedKey);
bool keepPresharedKey = false;
for (const VariantMap& storedPeer : storedPeers)
keepPresharedKey |= toString(value(storedPeer, "public-key")) == publicKey
&& variants::contains(storedPeer, "preshared-key-flags");
if (!presharedKey.empty()) {
if (!isWireGuardKey(presharedKey))
return fail("A peer's preshared key isn't a WireGuard key.");
peer["preshared-key"] = Variant(presharedKey);
peer["preshared-key-flags"] = Variant(uint32_t(SecretSystemOwned));
} else if (keepPresharedKey && formPeer.hasPresharedKey) {
peer["preshared-key-flags"] = Variant(uint32_t(SecretSystemOwned));
}
const std::string endpoint = wireGuardEndpoint(text::trimmed(formPeer.endpoint));
if (!endpoint.empty())
peer["endpoint"] = Variant(endpoint);
const std::vector<std::string> allowed = trimmedWords(formPeer.allowedIps);
for (const std::string& word : allowed) {
bool v4 = false;
VariantMap address;
if (!parseWireGuardAddress(word, address, v4))
return fail(text::arg("%1 isn't a network, like 10.0.0.0/24.", {word}));
}
peer["allowed-ips"] = Variant(allowed.empty() ? std::vector<std::string>{"0.0.0.0/0", "::/0"} : allowed);
uint32_t seconds = 0;
if (!formPeer.keepalive.empty() && (!parseUInt(formPeer.keepalive, &seconds) || seconds > 0xffff))
return fail("The keepalive must be a number of seconds up to 65535.");
peer["persistent-keepalive"] = Variant(seconds);
peers.push_back(std::move(peer));
}
Settings profile = existing;
VariantMap connection = profile["connection"];
if (!variants::contains(connection, "uuid")) {
connection["uuid"] = Variant(text::newUuid());
connection["type"] = Variant(std::string("wireguard"));
}
const std::string name = text::trimmed(form.name);
if (!name.empty())
connection["id"] = Variant(name);
const std::string interfaceName = text::trimmed(form.interfaceName);
connection["interface-name"] = Variant(
wireGuardInterfaceName(interfaceName.empty() ? toString(value(connection, "id")) : interfaceName));
profile["connection"] = connection;
VariantMap wireguard = stored;
if (!privateKey.empty())
wireguard["private-key"] = Variant(privateKey);
wireguard["private-key-flags"] = Variant(uint32_t(SecretSystemOwned));
wireguard["listen-port"] = Variant(listenPort);
wireguard["fwmark"] = Variant(fwmark);
wireguard["mtu"] = Variant(mtu);
wireguard["peer-routes"] = Variant(form.peerRoutes);
wireguard["peers"] = Variant(peers);
profile["wireguard"] = wireguard;
for (int family = 0; family < 2; ++family) {
VariantMap ip = profile[family == 0 ? "ipv4" : "ipv6"];
for (const char* key : {"address-data", "addresses", "dns", "dns-search", "gateway"})
ip.erase(key);
ip["method"] = Variant(std::string(addresses[family].empty() ? "disabled" : "manual"));
if (!addresses[family].empty()) {
ip["address-data"] = Variant(addresses[family]);
const bool hasDns = family == 0 ? !dns4.empty() : !dns6.empty();
if (hasDns) {
ip["dns"] = family == 0 ? Variant(dns4) : Variant(dns6);
ip["dns-search"] = Variant(search.empty() ? std::vector<std::string>{"~"} : search);
}
}
profile[family == 0 ? "ipv4" : "ipv6"] = ip;
}
return profile;
}
std::string wireGuardConfig(const Settings& settings)
{
const WireGuardForm form = wireGuardForm(settings);
const VariantMap& wireguard = group(settings, "wireguard");
std::string out = "[Interface]\n";
out += "PrivateKey = " + toString(value(wireguard, "private-key")) + "\n";
out += "Address = " + form.addresses + "\n";
if (!form.dns.empty())
out += "DNS = " + form.dns + "\n";
if (!form.listenPort.empty())
out += "ListenPort = " + form.listenPort + "\n";
if (!form.fwmark.empty())
out += "FwMark = " + form.fwmark + "\n";
if (!form.mtu.empty())
out += "MTU = " + form.mtu + "\n";
if (!form.peerRoutes)
out += "Table = off\n";
for (const VariantMap& peer : toMapList(value(wireguard, "peers"))) {
out += "\n[Peer]\nPublicKey = " + toString(value(peer, "public-key")) + "\n";
if (variants::contains(peer, "preshared-key"))
out += "PresharedKey = " + toString(value(peer, "preshared-key")) + "\n";
if (variants::contains(peer, "endpoint"))
out += "Endpoint = " + toString(value(peer, "endpoint")) + "\n";
const std::vector<std::string> allowed = toStringList(value(peer, "allowed-ips"));
if (!allowed.empty())
out += "AllowedIPs = " + text::join(allowed, ", ") + "\n";
const uint32_t keepalive = toUInt(value(peer, "persistent-keepalive"));
if (keepalive)
out += "PersistentKeepalive = " + std::to_string(keepalive) + "\n";
}
return out;
}
} // namespace cutefish::networkmanager::vpn