mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-07-03 14:07:39 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5979faba0 | |||
| 303cd58354 | |||
| ffe07ff860 | |||
| 471842b363 | |||
| 40abac8725 | |||
| 23b7c26609 | |||
| 0ca5279e02 | |||
| 7112ab98c0 | |||
| 6801c00aeb | |||
| c83e748def | |||
| 2c85e99129 | |||
| 637ea758d7 |
+21
-60
@@ -88,68 +88,33 @@ open class Wireguard : Protocol() {
|
||||
addDnsServer(parseInetAddress(dns.trim()))
|
||||
}
|
||||
|
||||
val defRoutes = hashSetOf(
|
||||
InetNetwork("0.0.0.0", 0),
|
||||
InetNetwork("::", 0)
|
||||
)
|
||||
val routes = hashSetOf<InetNetwork>()
|
||||
configData.getJSONArray("allowed_ips").asSequence<String>().map { route ->
|
||||
InetNetwork.parse(route.trim())
|
||||
}.forEach(routes::add)
|
||||
// if the allowed IPs list contains at least one non-default route, disable global split tunneling
|
||||
if (routes.any { it !in defRoutes }) disableSplitTunneling()
|
||||
addRoutes(routes)
|
||||
|
||||
configData.optStringOrNull("mtu")?.let { setMtu(it.toInt()) }
|
||||
configData.getString("client_priv_key").let { setPrivateKeyHex(it.base64ToHex()) }
|
||||
|
||||
val host = configData.getString("hostName").let { parseInetAddress(it.trim()) }
|
||||
val port = configData.getInt("port")
|
||||
setEndpoint(InetEndpoint(host, port))
|
||||
|
||||
if (configData.optBoolean("isObfuscationEnabled")) {
|
||||
setUseProtocolExtension(true)
|
||||
configExtensionParameters(configData)
|
||||
}
|
||||
|
||||
val defRoutes = hashSetOf(InetNetwork("0.0.0.0", 0), InetNetwork("::", 0))
|
||||
val peersArray = configData.optJSONArray("peers")
|
||||
|
||||
if (peersArray != null && peersArray.length() > 0) {
|
||||
// Multi-peer: collect union of all peers' allowed IPs for the VPN interface routing table
|
||||
val allRoutes = hashSetOf<InetNetwork>()
|
||||
for (i in 0 until peersArray.length()) {
|
||||
peersArray.getJSONObject(i).getJSONArray("allowed_ips").asSequence<String>()
|
||||
.map { InetNetwork.parse(it.trim()) }.forEach(allRoutes::add)
|
||||
}
|
||||
if (allRoutes.any { it !in defRoutes }) disableSplitTunneling()
|
||||
addRoutes(allRoutes)
|
||||
|
||||
// Primary peer from first entry
|
||||
val firstPeer = peersArray.getJSONObject(0)
|
||||
val firstAllowedIps = firstPeer.getJSONArray("allowed_ips").asSequence<String>()
|
||||
.map { InetNetwork.parse(it.trim()) }.toList()
|
||||
setPeerAllowedIps(firstAllowedIps)
|
||||
setEndpoint(InetEndpoint(parseInetAddress(firstPeer.getString("hostName").trim()), firstPeer.getInt("port")))
|
||||
firstPeer.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it.toInt()) }
|
||||
firstPeer.getString("server_pub_key").let { setPublicKeyHex(it.base64ToHex()) }
|
||||
firstPeer.optStringOrNull("psk_key")?.let { setPreSharedKeyHex(it.base64ToHex()) }
|
||||
|
||||
// Additional peers
|
||||
for (i in 1 until peersArray.length()) {
|
||||
val peerData = peersArray.getJSONObject(i)
|
||||
val peerAllowedIps = peerData.getJSONArray("allowed_ips").asSequence<String>()
|
||||
.map { InetNetwork.parse(it.trim()) }.toList()
|
||||
addPeer(
|
||||
PeerConfig(
|
||||
publicKeyHex = peerData.getString("server_pub_key").base64ToHex(),
|
||||
preSharedKeyHex = peerData.optStringOrNull("psk_key")?.base64ToHex(),
|
||||
persistentKeepalive = peerData.optStringOrNull("persistent_keep_alive")?.toInt() ?: 0,
|
||||
endpoint = InetEndpoint(parseInetAddress(peerData.getString("hostName").trim()), peerData.getInt("port")),
|
||||
allowedIps = peerAllowedIps
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Single peer (original behavior)
|
||||
val routes = hashSetOf<InetNetwork>()
|
||||
configData.getJSONArray("allowed_ips").asSequence<String>().map { route ->
|
||||
InetNetwork.parse(route.trim())
|
||||
}.forEach(routes::add)
|
||||
if (routes.any { it !in defRoutes }) disableSplitTunneling()
|
||||
addRoutes(routes)
|
||||
|
||||
val host = configData.getString("hostName").let { parseInetAddress(it.trim()) }
|
||||
val port = configData.getInt("port")
|
||||
setEndpoint(InetEndpoint(host, port))
|
||||
configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it.toInt()) }
|
||||
configData.getString("server_pub_key").let { setPublicKeyHex(it.base64ToHex()) }
|
||||
configData.optStringOrNull("psk_key")?.let { setPreSharedKeyHex(it.base64ToHex()) }
|
||||
}
|
||||
configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it.toInt()) }
|
||||
configData.getString("client_priv_key").let { setPrivateKeyHex(it.base64ToHex()) }
|
||||
configData.getString("server_pub_key").let { setPublicKeyHex(it.base64ToHex()) }
|
||||
configData.optStringOrNull("psk_key")?.let { setPreSharedKeyHex(it.base64ToHex()) }
|
||||
}
|
||||
|
||||
protected fun WireguardConfig.Builder.configExtensionParameters(configData: JSONObject) {
|
||||
@@ -236,11 +201,7 @@ open class Wireguard : Protocol() {
|
||||
Log.e(TAG, "Failed to get tunnel config")
|
||||
return -2
|
||||
}
|
||||
// For multi-peer: take the max handshake time across all peers (any connected peer = tunnel active)
|
||||
val lastHandshake = config.lines()
|
||||
.filter { it.startsWith("last_handshake_time_sec=") }
|
||||
.mapNotNull { it.substring(24).toLongOrNull() }
|
||||
.maxOrNull()
|
||||
val lastHandshake = config.lines().find { it.startsWith("last_handshake_time_sec=") }?.substring(24)?.toLong()
|
||||
if (lastHandshake == null) {
|
||||
Log.e(TAG, "Failed to get last_handshake_time_sec")
|
||||
return -2
|
||||
|
||||
+3
-30
@@ -4,18 +4,9 @@ import android.util.Base64
|
||||
import org.amnezia.vpn.protocol.BadConfigException
|
||||
import org.amnezia.vpn.protocol.ProtocolConfig
|
||||
import org.amnezia.vpn.util.net.InetEndpoint
|
||||
import org.amnezia.vpn.util.net.InetNetwork
|
||||
|
||||
private const val WIREGUARD_DEFAULT_MTU = 1280
|
||||
|
||||
data class PeerConfig(
|
||||
val publicKeyHex: String,
|
||||
val preSharedKeyHex: String?,
|
||||
val persistentKeepalive: Int,
|
||||
val endpoint: InetEndpoint,
|
||||
val allowedIps: List<InetNetwork>
|
||||
)
|
||||
|
||||
open class WireguardConfig protected constructor(
|
||||
protocolConfigBuilder: ProtocolConfig.Builder,
|
||||
val endpoint: InetEndpoint,
|
||||
@@ -40,8 +31,6 @@ open class WireguardConfig protected constructor(
|
||||
var i3: String?,
|
||||
var i4: String?,
|
||||
var i5: String?,
|
||||
val peerAllowedIps: List<InetNetwork>?,
|
||||
val additionalPeers: List<PeerConfig>,
|
||||
) : ProtocolConfig(protocolConfigBuilder) {
|
||||
|
||||
protected constructor(builder: Builder) : this(
|
||||
@@ -68,8 +57,6 @@ open class WireguardConfig protected constructor(
|
||||
builder.i3,
|
||||
builder.i4,
|
||||
builder.i5,
|
||||
builder.peerAllowedIps,
|
||||
builder.additionalPeers.toList(),
|
||||
)
|
||||
|
||||
fun toWgUserspaceString(): String = with(StringBuilder()) {
|
||||
@@ -116,22 +103,14 @@ open class WireguardConfig protected constructor(
|
||||
|
||||
open fun appendPeerLine(sb: StringBuilder) = with(sb) {
|
||||
appendLine("public_key=$publicKeyHex")
|
||||
val primaryIps = peerAllowedIps ?: routes.filter { it.include }.map { it.inetNetwork }
|
||||
primaryIps.forEach { net -> appendLine("allowed_ip=$net") }
|
||||
routes.filter { it.include }.forEach { route ->
|
||||
appendLine("allowed_ip=${route.inetNetwork}")
|
||||
}
|
||||
appendLine("endpoint=$endpoint")
|
||||
if (persistentKeepalive != 0)
|
||||
appendLine("persistent_keepalive_interval=$persistentKeepalive")
|
||||
if (preSharedKeyHex != null)
|
||||
appendLine("preshared_key=$preSharedKeyHex")
|
||||
for (peer in additionalPeers) {
|
||||
appendLine("public_key=${peer.publicKeyHex}")
|
||||
peer.allowedIps.forEach { net -> appendLine("allowed_ip=$net") }
|
||||
appendLine("endpoint=${peer.endpoint}")
|
||||
if (peer.persistentKeepalive != 0)
|
||||
appendLine("persistent_keepalive_interval=${peer.persistentKeepalive}")
|
||||
if (peer.preSharedKeyHex != null)
|
||||
appendLine("preshared_key=${peer.preSharedKeyHex}")
|
||||
}
|
||||
}
|
||||
|
||||
open class Builder : ProtocolConfig.Builder(true) {
|
||||
@@ -171,9 +150,6 @@ open class WireguardConfig protected constructor(
|
||||
internal var i4: String? = null
|
||||
internal var i5: String? = null
|
||||
|
||||
internal var peerAllowedIps: List<InetNetwork>? = null
|
||||
internal val additionalPeers: MutableList<PeerConfig> = mutableListOf()
|
||||
|
||||
fun setEndpoint(endpoint: InetEndpoint) = apply { this.endpoint = endpoint }
|
||||
|
||||
fun setPersistentKeepalive(persistentKeepalive: Int) = apply { this.persistentKeepalive = persistentKeepalive }
|
||||
@@ -203,9 +179,6 @@ open class WireguardConfig protected constructor(
|
||||
fun setI4(i4: String) = apply { this.i4 = i4 }
|
||||
fun setI5(i5: String) = apply { this.i5 = i5 }
|
||||
|
||||
fun setPeerAllowedIps(ips: List<InetNetwork>) = apply { this.peerAllowedIps = ips }
|
||||
fun addPeer(peer: PeerConfig) = apply { this.additionalPeers += peer }
|
||||
|
||||
override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) }
|
||||
}
|
||||
|
||||
|
||||
@@ -203,6 +203,9 @@ void CoreController::initControllers()
|
||||
m_ipSplitTunnelingUiController = new IpSplitTunnelingUiController(m_ipSplitTunnelingController, m_ipSplitTunnelingModel, this);
|
||||
setQmlContextProperty("IpSplitTunnelingController", m_ipSplitTunnelingUiController);
|
||||
|
||||
m_serversBackupController = new ServersBackupController(m_settings, m_serversModel, m_serversUiController, m_serversController);
|
||||
setQmlContextProperty("ServersBackupController", m_serversBackupController);
|
||||
|
||||
m_allowedDnsUiController = new AllowedDnsUiController(m_allowedDnsController, m_allowedDnsModel, this);
|
||||
setQmlContextProperty("AllowedDnsController", m_allowedDnsUiController);
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "ui/controllers/settingsUiController.h"
|
||||
#include "ui/controllers/serversUiController.h"
|
||||
#include "ui/controllers/ipSplitTunnelingUiController.h"
|
||||
#include "ui/controllers/serversBackupController.h"
|
||||
#include "ui/controllers/systemController.h"
|
||||
#include "ui/controllers/languageUiController.h"
|
||||
#include "ui/controllers/updateUiController.h"
|
||||
@@ -174,6 +175,7 @@ private:
|
||||
AllowedDnsUiController* m_allowedDnsUiController;
|
||||
LanguageUiController* m_languageUiController;
|
||||
UpdateUiController* m_updateUiController;
|
||||
ServersBackupController* m_serversBackupController;
|
||||
|
||||
SubscriptionUiController* m_subscriptionUiController;
|
||||
ApiNewsUiController* m_apiNewsUiController;
|
||||
|
||||
@@ -504,45 +504,24 @@ QJsonObject ImportController::extractOpenVpnConfig(const QString &data) const
|
||||
|
||||
QJsonObject ImportController::extractWireGuardConfig(const QString &data, ConfigTypes &configType) const
|
||||
{
|
||||
QMap<QString, QString> interfaceMap;
|
||||
QList<QMap<QString, QString>> peerList;
|
||||
|
||||
enum class WgSection { None, Interface, Peer };
|
||||
WgSection currentSection = WgSection::None;
|
||||
|
||||
const auto configByLines = data.split("\n");
|
||||
QMap<QString, QString> configMap;
|
||||
auto configByLines = data.split("\n");
|
||||
for (const QString &line : configByLines) {
|
||||
const QString trimmedLine = line.trimmed();
|
||||
if (trimmedLine == "[Interface]") {
|
||||
currentSection = WgSection::Interface;
|
||||
} else if (trimmedLine == "[Peer]") {
|
||||
currentSection = WgSection::Peer;
|
||||
peerList.append(QMap<QString, QString>());
|
||||
} else if (!trimmedLine.isEmpty() && !trimmedLine.startsWith("#")) {
|
||||
const QStringList parts = trimmedLine.split(" = ");
|
||||
QString trimmedLine = line.trimmed();
|
||||
if (trimmedLine.startsWith("[") && trimmedLine.endsWith("]")) {
|
||||
continue;
|
||||
} else {
|
||||
QStringList parts = trimmedLine.split(" = ");
|
||||
if (parts.count() == 2) {
|
||||
const QString key = parts.at(0).trimmed();
|
||||
const QString value = parts.at(1).trimmed();
|
||||
if (currentSection == WgSection::Interface) {
|
||||
interfaceMap[key] = value;
|
||||
} else if (currentSection == WgSection::Peer && !peerList.isEmpty()) {
|
||||
peerList.last()[key] = value;
|
||||
}
|
||||
configMap[parts.at(0).trimmed()] = parts.at(1).trimmed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (peerList.isEmpty()) {
|
||||
qDebug() << "No [Peer] section found in WireGuard config";
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
const QMap<QString, QString> &firstPeerMap = peerList.first();
|
||||
|
||||
QJsonObject lastConfig;
|
||||
lastConfig[configKey::config] = data;
|
||||
|
||||
auto url { QUrl::fromUserInput(firstPeerMap.value(protocols::wireguard::Endpoint)) };
|
||||
auto url { QUrl::fromUserInput(configMap.value(protocols::wireguard::Endpoint)) };
|
||||
QString hostName;
|
||||
QString port;
|
||||
if (!url.host().isEmpty()) {
|
||||
@@ -561,55 +540,37 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
|
||||
lastConfig[configKey::hostName] = hostName;
|
||||
lastConfig[configKey::port] = port.toInt();
|
||||
|
||||
if (!interfaceMap.value(protocols::wireguard::PrivateKey).isEmpty()
|
||||
&& !interfaceMap.value(protocols::wireguard::Address).isEmpty()
|
||||
&& !firstPeerMap.value(protocols::wireguard::PublicKey).isEmpty()) {
|
||||
lastConfig[configKey::clientPrivKey] = interfaceMap.value(protocols::wireguard::PrivateKey);
|
||||
lastConfig[configKey::clientIp] = interfaceMap.value(protocols::wireguard::Address);
|
||||
if (!configMap.value(protocols::wireguard::PrivateKey).isEmpty()
|
||||
&& !configMap.value(protocols::wireguard::Address).isEmpty()
|
||||
&& !configMap.value(protocols::wireguard::PublicKey).isEmpty()) {
|
||||
lastConfig[configKey::clientPrivKey] = configMap.value(protocols::wireguard::PrivateKey);
|
||||
lastConfig[configKey::clientIp] = configMap.value(protocols::wireguard::Address);
|
||||
|
||||
if (!firstPeerMap.value(protocols::wireguard::PresharedKey).isEmpty()) {
|
||||
lastConfig[configKey::pskKey] = firstPeerMap.value(protocols::wireguard::PresharedKey);
|
||||
} else if (!firstPeerMap.value(protocols::wireguard::PreSharedKey).isEmpty()) {
|
||||
lastConfig[configKey::pskKey] = firstPeerMap.value(protocols::wireguard::PreSharedKey);
|
||||
if (!configMap.value(protocols::wireguard::PresharedKey).isEmpty()) {
|
||||
lastConfig[configKey::pskKey] = configMap.value(protocols::wireguard::PresharedKey);
|
||||
} else if (!configMap.value(protocols::wireguard::PreSharedKey).isEmpty()) {
|
||||
lastConfig[configKey::pskKey] = configMap.value(protocols::wireguard::PreSharedKey);
|
||||
}
|
||||
|
||||
lastConfig[configKey::serverPubKey] = firstPeerMap.value(protocols::wireguard::PublicKey);
|
||||
lastConfig[configKey::serverPubKey] = configMap.value(protocols::wireguard::PublicKey);
|
||||
} else {
|
||||
qDebug() << "One of the key parameters is missing (PrivateKey, Address, PublicKey)";
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (!firstPeerMap.value(protocols::wireguard::PersistentKeepalive).isEmpty()) {
|
||||
lastConfig[configKey::persistentKeepAlive] = firstPeerMap.value(protocols::wireguard::PersistentKeepalive);
|
||||
if (!configMap.value(protocols::wireguard::MTU).isEmpty()) {
|
||||
lastConfig[configKey::mtu] = configMap.value(protocols::wireguard::MTU);
|
||||
}
|
||||
|
||||
if (!configMap.value(protocols::wireguard::PersistentKeepalive).isEmpty()) {
|
||||
lastConfig[configKey::persistentKeepAlive] = configMap.value(protocols::wireguard::PersistentKeepalive);
|
||||
}
|
||||
|
||||
QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(
|
||||
firstPeerMap.value(protocols::wireguard::AllowedIPs).split(", "));
|
||||
configMap.value(protocols::wireguard::AllowedIPs).split(", "));
|
||||
|
||||
lastConfig[configKey::allowedIps] = allowedIpsJsonArray;
|
||||
|
||||
if (peerList.size() > 1) {
|
||||
QJsonArray peersArray;
|
||||
for (const auto &peerMap : std::as_const(peerList)) {
|
||||
QJsonObject peerObj;
|
||||
const auto peerUrl = QUrl::fromUserInput(peerMap.value(protocols::wireguard::Endpoint));
|
||||
peerObj[configKey::serverPubKey] = peerMap.value(protocols::wireguard::PublicKey);
|
||||
if (!peerMap.value(protocols::wireguard::PresharedKey).isEmpty()) {
|
||||
peerObj[configKey::pskKey] = peerMap.value(protocols::wireguard::PresharedKey);
|
||||
} else if (!peerMap.value(protocols::wireguard::PreSharedKey).isEmpty()) {
|
||||
peerObj[configKey::pskKey] = peerMap.value(protocols::wireguard::PreSharedKey);
|
||||
}
|
||||
peerObj[configKey::hostName] = peerUrl.host();
|
||||
peerObj[configKey::port] = peerUrl.port() != -1 ? peerUrl.port() : QString(protocols::wireguard::defaultPort).toInt();
|
||||
peerObj[configKey::allowedIps] = QJsonArray::fromStringList(peerMap.value(protocols::wireguard::AllowedIPs).split(", "));
|
||||
if (!peerMap.value(protocols::wireguard::PersistentKeepalive).isEmpty()) {
|
||||
peerObj[configKey::persistentKeepAlive] = peerMap.value(protocols::wireguard::PersistentKeepalive);
|
||||
}
|
||||
peersArray.append(peerObj);
|
||||
}
|
||||
lastConfig["peers"] = peersArray;
|
||||
}
|
||||
|
||||
QString protocolName = configKey::wireguard;
|
||||
QString protocolVersion;
|
||||
ConfigTypes detectedType = ConfigTypes::WireGuard;
|
||||
@@ -627,25 +588,25 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
|
||||
};
|
||||
|
||||
bool hasAllRequiredFields = std::all_of(requiredJunkFields.begin(), requiredJunkFields.end(),
|
||||
[&interfaceMap](const QString &field) { return !interfaceMap.value(field).isEmpty(); });
|
||||
[&configMap](const QString &field) { return !configMap.value(field).isEmpty(); });
|
||||
if (hasAllRequiredFields) {
|
||||
for (const QString &field : requiredJunkFields) {
|
||||
lastConfig[field] = interfaceMap.value(field);
|
||||
lastConfig[field] = configMap.value(field);
|
||||
}
|
||||
|
||||
for (const QString &field : optionalJunkFields) {
|
||||
if (!interfaceMap.value(field).isEmpty()) {
|
||||
lastConfig[field] = interfaceMap.value(field);
|
||||
if (!configMap.value(field).isEmpty()) {
|
||||
lastConfig[field] = configMap.value(field);
|
||||
}
|
||||
}
|
||||
|
||||
bool hasCookieReplyPacketJunkSize = !interfaceMap.value(configKey::cookieReplyPacketJunkSize).isEmpty();
|
||||
bool hasTransportPacketJunkSize = !interfaceMap.value(configKey::transportPacketJunkSize).isEmpty();
|
||||
bool hasSpecialJunk = !interfaceMap.value(configKey::specialJunk1).isEmpty() ||
|
||||
!interfaceMap.value(configKey::specialJunk2).isEmpty() ||
|
||||
!interfaceMap.value(configKey::specialJunk3).isEmpty() ||
|
||||
!interfaceMap.value(configKey::specialJunk4).isEmpty() ||
|
||||
!interfaceMap.value(configKey::specialJunk5).isEmpty();
|
||||
bool hasCookieReplyPacketJunkSize = !configMap.value(configKey::cookieReplyPacketJunkSize).isEmpty();
|
||||
bool hasTransportPacketJunkSize = !configMap.value(configKey::transportPacketJunkSize).isEmpty();
|
||||
bool hasSpecialJunk = !configMap.value(configKey::specialJunk1).isEmpty() ||
|
||||
!configMap.value(configKey::specialJunk2).isEmpty() ||
|
||||
!configMap.value(configKey::specialJunk3).isEmpty() ||
|
||||
!configMap.value(configKey::specialJunk4).isEmpty() ||
|
||||
!configMap.value(configKey::specialJunk5).isEmpty();
|
||||
|
||||
if (hasCookieReplyPacketJunkSize && hasTransportPacketJunkSize) {
|
||||
protocolVersion = "2";
|
||||
@@ -656,8 +617,8 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
|
||||
detectedType = ConfigTypes::Awg;
|
||||
}
|
||||
|
||||
if (!interfaceMap.value(protocols::wireguard::MTU).isEmpty()) {
|
||||
lastConfig[configKey::mtu] = interfaceMap.value(protocols::wireguard::MTU);
|
||||
if (!configMap.value(protocols::wireguard::MTU).isEmpty()) {
|
||||
lastConfig[configKey::mtu] = configMap.value(protocols::wireguard::MTU);
|
||||
} else {
|
||||
lastConfig[configKey::mtu] = (protocolName == configKey::awg)
|
||||
? protocols::awg::defaultMtu
|
||||
|
||||
@@ -205,11 +205,7 @@ QJsonObject AwgClientConfig::toJson() const
|
||||
if (isObfuscationEnabled) {
|
||||
obj[configKey::isObfuscationEnabled] = isObfuscationEnabled;
|
||||
}
|
||||
|
||||
if (!peers.isEmpty()) {
|
||||
obj["peers"] = peers;
|
||||
}
|
||||
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -254,9 +250,7 @@ AwgClientConfig AwgClientConfig::fromJson(const QJsonObject& json)
|
||||
config.specialJunk5 = json.value(configKey::specialJunk5).toString();
|
||||
|
||||
config.isObfuscationEnabled = json.value(configKey::isObfuscationEnabled).toBool(false);
|
||||
|
||||
config.peers = json.value("peers").toArray();
|
||||
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#ifndef AWGPROTOCOLCONFIG_H
|
||||
#define AWGPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
@@ -61,7 +60,6 @@ struct AwgClientConfig {
|
||||
QStringList allowedIps;
|
||||
QString persistentKeepAlive;
|
||||
QString mtu;
|
||||
QJsonArray peers;
|
||||
QString junkPacketCount;
|
||||
QString junkPacketMinSize;
|
||||
QString junkPacketMaxSize;
|
||||
|
||||
@@ -290,6 +290,86 @@ namespace libssh {
|
||||
return watcher.result();
|
||||
}
|
||||
|
||||
ErrorCode Client::scpFileDownload(const QString& remotePath, const QString& localPath)
|
||||
{
|
||||
// Use full path for SCP download
|
||||
m_scpSession = ssh_scp_new(m_session, SSH_SCP_READ, remotePath.toStdString().c_str());
|
||||
|
||||
if (m_scpSession == nullptr) {
|
||||
return fromLibsshErrorCode();
|
||||
}
|
||||
|
||||
if (ssh_scp_init(m_scpSession) != SSH_OK) {
|
||||
auto errorCode = fromLibsshErrorCode();
|
||||
closeScpSession();
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
QFutureWatcher<ErrorCode> watcher;
|
||||
connect(&watcher, &QFutureWatcher<ErrorCode>::finished, this, &Client::scpFileDownloadFinished);
|
||||
QFuture<ErrorCode> future = QtConcurrent::run([this, &remotePath, &localPath]() {
|
||||
// Pull request - this gets file info
|
||||
int result = ssh_scp_pull_request(m_scpSession);
|
||||
if (result != SSH_SCP_REQUEST_NEWFILE) {
|
||||
return fromLibsshErrorCode();
|
||||
}
|
||||
|
||||
// Accept the request
|
||||
ssh_scp_accept_request(m_scpSession);
|
||||
|
||||
// Get file size
|
||||
int fileSize = ssh_scp_request_get_size(m_scpSession);
|
||||
if (fileSize <= 0) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
// Open local file for writing
|
||||
QFile fout(localPath);
|
||||
if (!fout.open(QIODevice::WriteOnly)) {
|
||||
return fromFileErrorCode(fout.error());
|
||||
}
|
||||
|
||||
// Read file data in chunks
|
||||
constexpr size_t bufferSize = 16384;
|
||||
int transferred = 0;
|
||||
|
||||
while (transferred < fileSize) {
|
||||
int chunkSize = qMin(bufferSize, static_cast<size_t>(fileSize - transferred));
|
||||
QByteArray buffer(chunkSize, 0);
|
||||
|
||||
int bytesRead = ssh_scp_read(m_scpSession, buffer.data(), chunkSize);
|
||||
if (bytesRead == SSH_ERROR) {
|
||||
fout.close();
|
||||
return fromLibsshErrorCode();
|
||||
}
|
||||
|
||||
if (bytesRead != chunkSize) {
|
||||
fout.close();
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
qint64 bytesWritten = fout.write(buffer);
|
||||
if (bytesWritten != chunkSize) {
|
||||
fout.close();
|
||||
return fromFileErrorCode(fout.error());
|
||||
}
|
||||
|
||||
transferred += bytesRead;
|
||||
}
|
||||
|
||||
fout.close();
|
||||
return ErrorCode::NoError;
|
||||
});
|
||||
watcher.setFuture(future);
|
||||
|
||||
QEventLoop wait;
|
||||
QObject::connect(this, &Client::scpFileDownloadFinished, &wait, &QEventLoop::quit);
|
||||
wait.exec();
|
||||
|
||||
closeScpSession();
|
||||
return watcher.result();
|
||||
}
|
||||
|
||||
void Client::closeScpSession()
|
||||
{
|
||||
if (m_scpSession != nullptr) {
|
||||
|
||||
@@ -38,6 +38,8 @@ namespace libssh {
|
||||
const QString &localPath,
|
||||
const QString &remotePath,
|
||||
const QString &fileDesc);
|
||||
ErrorCode scpFileDownload(const QString &remotePath,
|
||||
const QString &localPath);
|
||||
ErrorCode getDecryptedPrivateKey(const ServerCredentials &credentials, QString &decryptedPrivateKey, const std::function<QString()> &passphraseCallback);
|
||||
private:
|
||||
ErrorCode closeChannel();
|
||||
@@ -54,6 +56,7 @@ namespace libssh {
|
||||
signals:
|
||||
void writeToChannelFinished();
|
||||
void scpFileCopyFinished();
|
||||
void scpFileDownloadFinished();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,11 @@ SshSession::~SshSession()
|
||||
m_sshClient.disconnectFromHost();
|
||||
}
|
||||
|
||||
void SshSession::resetConnection()
|
||||
{
|
||||
m_sshClient.disconnectFromHost();
|
||||
}
|
||||
|
||||
ErrorCode SshSession::runScript(const ServerCredentials &credentials, QString script,
|
||||
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdOut,
|
||||
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdErr)
|
||||
|
||||
@@ -47,6 +47,9 @@ public:
|
||||
ErrorCode uploadFileToHost(const ServerCredentials &credentials, const QByteArray &data, const QString &remotePath,
|
||||
libssh::ScpOverwriteMode overwriteMode = libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
|
||||
/** Force-close the current SSH connection so the next operation starts fresh. */
|
||||
void resetConnection();
|
||||
|
||||
private:
|
||||
libssh::Client m_sshClient;
|
||||
};
|
||||
|
||||
@@ -441,37 +441,6 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
|
||||
config.m_specialJunk["I5"] = obj.value("I5").toString();
|
||||
}
|
||||
|
||||
if (obj.contains("primaryPeerAllowedIPAddressRanges") &&
|
||||
obj.value("primaryPeerAllowedIPAddressRanges").isArray()) {
|
||||
for (const QJsonValue& ipVal : obj.value("primaryPeerAllowedIPAddressRanges").toArray()) {
|
||||
if (!ipVal.isObject()) continue;
|
||||
QJsonObject ipObj = ipVal.toObject();
|
||||
config.m_primaryPeerAllowedIPRanges.append(
|
||||
IPAddress(QHostAddress(ipObj.value("address").toString()),
|
||||
ipObj.value("range").toInt()));
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.contains("additionalPeers") && obj.value("additionalPeers").isArray()) {
|
||||
for (const QJsonValue& peerVal : obj.value("additionalPeers").toArray()) {
|
||||
if (!peerVal.isObject()) continue;
|
||||
QJsonObject peerObj = peerVal.toObject();
|
||||
InterfaceConfig::AdditionalPeerConfig peer;
|
||||
peer.m_serverPublicKey = peerObj.value("serverPublicKey").toString();
|
||||
peer.m_serverPskKey = peerObj.value("serverPskKey").toString();
|
||||
peer.m_serverIpv4AddrIn = peerObj.value("serverIpv4AddrIn").toString();
|
||||
peer.m_serverPort = peerObj.value("serverPort").toInt();
|
||||
for (const QJsonValue& ipVal : peerObj.value("allowedIPAddressRanges").toArray()) {
|
||||
if (!ipVal.isObject()) continue;
|
||||
QJsonObject ipObj = ipVal.toObject();
|
||||
peer.m_allowedIPAddressRanges.append(
|
||||
IPAddress(QHostAddress(ipObj.value("address").toString()),
|
||||
ipObj.value("range").toInt()));
|
||||
}
|
||||
config.m_additionalPeers.append(peer);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,6 @@ class InterfaceConfig {
|
||||
int m_serverPort = 0;
|
||||
int m_deviceMTU = 1420;
|
||||
QList<IPAddress> m_allowedIPAddressRanges;
|
||||
// For multi-peer: primary peer's own IPs only (used for UAPI allowed_ips).
|
||||
// Empty for single-peer (falls back to m_allowedIPAddressRanges).
|
||||
QList<IPAddress> m_primaryPeerAllowedIPRanges;
|
||||
QStringList m_excludedAddresses;
|
||||
QStringList m_vpnDisabledApps;
|
||||
QStringList m_allowedDnsServers;
|
||||
@@ -61,15 +58,6 @@ class InterfaceConfig {
|
||||
QString m_transportPacketMagicHeader;
|
||||
QMap<QString, QString> m_specialJunk;
|
||||
|
||||
struct AdditionalPeerConfig {
|
||||
QString m_serverPublicKey;
|
||||
QString m_serverPskKey;
|
||||
QString m_serverIpv4AddrIn;
|
||||
int m_serverPort = 0;
|
||||
QList<IPAddress> m_allowedIPAddressRanges;
|
||||
};
|
||||
QList<AdditionalPeerConfig> m_additionalPeers;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
QString toWgConf(
|
||||
const QMap<QString, QString>& extra = QMap<QString, QString>()) const;
|
||||
|
||||
@@ -169,96 +169,68 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
|
||||
QJsonArray jsAllowedIPAddesses;
|
||||
|
||||
auto ipRangeToJson = [](const QString& ipRange) -> QJsonObject {
|
||||
QJsonObject range;
|
||||
const QStringList parts = ipRange.split('/');
|
||||
range.insert("address", parts[0]);
|
||||
range.insert("range", parts.size() > 1 ? parts[1].toInt() : 32);
|
||||
range.insert("isIpv6", ipRange.contains(':'));
|
||||
return range;
|
||||
};
|
||||
QJsonArray plainAllowedIP = wgConfig.value(amnezia::configKey::allowedIps).toArray();
|
||||
QJsonArray defaultAllowedIP = { "0.0.0.0/0", "::/0" };
|
||||
|
||||
QJsonArray peersArray = wgConfig.value("peers").toArray();
|
||||
bool isMultiPeer = peersArray.size() > 1;
|
||||
|
||||
if (isMultiPeer) {
|
||||
// Union of all peers' IPs goes into allowedIPAddressRanges (used for route setup).
|
||||
QSet<QString> seenIps;
|
||||
for (const QJsonValue& peerVal : std::as_const(peersArray)) {
|
||||
for (const QJsonValue& ipVal : peerVal.toObject().value(amnezia::configKey::allowedIps).toArray()) {
|
||||
const QString ipRange = ipVal.toString().trimmed();
|
||||
if (seenIps.contains(ipRange)) continue;
|
||||
seenIps.insert(ipRange);
|
||||
jsAllowedIPAddesses.append(ipRangeToJson(ipRange));
|
||||
if (plainAllowedIP != defaultAllowedIP && !plainAllowedIP.isEmpty()) {
|
||||
// Use AllowedIP list from WG config because of higher priority
|
||||
for (auto v : plainAllowedIP) {
|
||||
QString ipRange = v.toString();
|
||||
if (ipRange.split('/').size() > 1){
|
||||
QJsonObject range;
|
||||
range.insert("address", ipRange.split('/')[0]);
|
||||
range.insert("range", atoi(ipRange.split('/')[1].toLocal8Bit()));
|
||||
range.insert("isIpv6", false);
|
||||
jsAllowedIPAddesses.append(range);
|
||||
} else {
|
||||
QJsonObject range;
|
||||
range.insert("address",ipRange);
|
||||
range.insert("range", 32);
|
||||
range.insert("isIpv6", false);
|
||||
jsAllowedIPAddesses.append(range);
|
||||
}
|
||||
}
|
||||
|
||||
// Primary peer's own IPs only — used for UAPI allowed_ips to avoid trie conflicts.
|
||||
QJsonArray primaryPeerIpsJson;
|
||||
for (const QJsonValue& ipVal : peersArray[0].toObject().value(amnezia::configKey::allowedIps).toArray()) {
|
||||
primaryPeerIpsJson.append(ipRangeToJson(ipVal.toString().trimmed()));
|
||||
}
|
||||
json.insert("primaryPeerAllowedIPAddressRanges", primaryPeerIpsJson);
|
||||
|
||||
QJsonArray additionalPeersJson;
|
||||
for (int i = 1; i < peersArray.size(); ++i) {
|
||||
const QJsonObject peerObj = peersArray[i].toObject();
|
||||
QJsonObject additionalPeer;
|
||||
additionalPeer.insert("serverPublicKey", peerObj.value(amnezia::configKey::serverPubKey));
|
||||
additionalPeer.insert("serverPskKey", peerObj.value(amnezia::configKey::pskKey));
|
||||
additionalPeer.insert("serverIpv4AddrIn", peerObj.value(amnezia::configKey::hostName));
|
||||
additionalPeer.insert("serverPort", peerObj.value(amnezia::configKey::port).toInt());
|
||||
QJsonArray additionalPeerIps;
|
||||
for (const QJsonValue& ipVal : peerObj.value(amnezia::configKey::allowedIps).toArray()) {
|
||||
additionalPeerIps.append(ipRangeToJson(ipVal.toString().trimmed()));
|
||||
}
|
||||
additionalPeer.insert("allowedIPAddressRanges", additionalPeerIps);
|
||||
additionalPeersJson.append(additionalPeer);
|
||||
}
|
||||
json.insert("additionalPeers", additionalPeersJson);
|
||||
} else {
|
||||
QJsonArray plainAllowedIP = wgConfig.value(amnezia::configKey::allowedIps).toArray();
|
||||
QJsonArray defaultAllowedIP = { "0.0.0.0/0", "::/0" };
|
||||
|
||||
if (plainAllowedIP != defaultAllowedIP && !plainAllowedIP.isEmpty()) {
|
||||
// Use AllowedIP list from WG config because of higher priority
|
||||
for (auto v : plainAllowedIP) {
|
||||
jsAllowedIPAddesses.append(ipRangeToJson(v.toString().trimmed()));
|
||||
}
|
||||
} else {
|
||||
// Use APP split tunnel
|
||||
// Use APP split tunnel
|
||||
if (splitTunnelType == 0 || splitTunnelType == 2) {
|
||||
QJsonObject range_ipv4;
|
||||
range_ipv4.insert("address", "0.0.0.0");
|
||||
range_ipv4.insert("range", 0);
|
||||
range_ipv4.insert("isIpv6", false);
|
||||
jsAllowedIPAddesses.append(range_ipv4);
|
||||
QJsonObject range_ipv4;
|
||||
range_ipv4.insert("address", "0.0.0.0");
|
||||
range_ipv4.insert("range", 0);
|
||||
range_ipv4.insert("isIpv6", false);
|
||||
jsAllowedIPAddesses.append(range_ipv4);
|
||||
|
||||
QJsonObject range_ipv6;
|
||||
range_ipv6.insert("address", "::");
|
||||
range_ipv6.insert("range", 0);
|
||||
range_ipv6.insert("isIpv6", true);
|
||||
jsAllowedIPAddesses.append(range_ipv6);
|
||||
QJsonObject range_ipv6;
|
||||
range_ipv6.insert("address", "::");
|
||||
range_ipv6.insert("range", 0);
|
||||
range_ipv6.insert("isIpv6", true);
|
||||
jsAllowedIPAddesses.append(range_ipv6);
|
||||
}
|
||||
|
||||
if (splitTunnelType == 1) {
|
||||
for (auto v : splitTunnelSites) {
|
||||
jsAllowedIPAddesses.append(ipRangeToJson(v.toString().trimmed()));
|
||||
}
|
||||
for (auto v : splitTunnelSites) {
|
||||
QString ipRange = v.toString();
|
||||
if (ipRange.split('/').size() > 1){
|
||||
QJsonObject range;
|
||||
range.insert("address", ipRange.split('/')[0]);
|
||||
range.insert("range", atoi(ipRange.split('/')[1].toLocal8Bit()));
|
||||
range.insert("isIpv6", false);
|
||||
jsAllowedIPAddesses.append(range);
|
||||
} else {
|
||||
QJsonObject range;
|
||||
range.insert("address",ipRange);
|
||||
range.insert("range", 32);
|
||||
range.insert("isIpv6", false);
|
||||
jsAllowedIPAddesses.append(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json.insert("allowedIPAddressRanges", jsAllowedIPAddesses);
|
||||
|
||||
QJsonArray jsExcludedAddresses;
|
||||
if (isMultiPeer) {
|
||||
for (const QJsonValue& peerVal : std::as_const(peersArray)) {
|
||||
jsExcludedAddresses.append(peerVal.toObject().value(amnezia::configKey::hostName));
|
||||
}
|
||||
} else {
|
||||
jsExcludedAddresses.append(wgConfig.value(amnezia::configKey::hostName));
|
||||
}
|
||||
jsExcludedAddresses.append(wgConfig.value(amnezia::configKey::hostName));
|
||||
if (splitTunnelType == 2) {
|
||||
for (auto v : splitTunnelSites) {
|
||||
QString ipRange = v.toString();
|
||||
|
||||
@@ -20,7 +20,7 @@ extension PacketTunnelProvider {
|
||||
|
||||
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: wgConfigStr)
|
||||
|
||||
if tunnelConfiguration.peers.first?.allowedIPs
|
||||
if tunnelConfiguration.peers.first!.allowedIPs
|
||||
.map({ $0.stringRepresentation })
|
||||
.joined(separator: ", ") == "0.0.0.0/0, ::/0" {
|
||||
if wgConfig.splitTunnelType == 1 {
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
struct WGPeerConfig: Decodable {
|
||||
let serverPublicKey: String
|
||||
let presharedKey: String?
|
||||
let allowedIPs: [String]
|
||||
let hostName: String
|
||||
let port: Int
|
||||
let persistentKeepAlive: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case serverPublicKey = "server_pub_key"
|
||||
case presharedKey = "psk_key"
|
||||
case allowedIPs = "allowed_ips"
|
||||
case hostName
|
||||
case port
|
||||
case persistentKeepAlive = "persistent_keep_alive"
|
||||
}
|
||||
}
|
||||
|
||||
struct WGConfig: Decodable {
|
||||
let initPacketMagicHeader, responsePacketMagicHeader: String?
|
||||
let underloadPacketMagicHeader, transportPacketMagicHeader: String?
|
||||
@@ -37,7 +19,6 @@ struct WGConfig: Decodable {
|
||||
var persistentKeepAlive: String
|
||||
let splitTunnelType: Int
|
||||
let splitTunnelSites: [String]
|
||||
let peers: [WGPeerConfig]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case initPacketMagicHeader = "H1", responsePacketMagicHeader = "H2"
|
||||
@@ -58,7 +39,6 @@ struct WGConfig: Decodable {
|
||||
case persistentKeepAlive = "persistent_keep_alive"
|
||||
case splitTunnelType
|
||||
case splitTunnelSites
|
||||
case peers
|
||||
}
|
||||
|
||||
var settings: String {
|
||||
@@ -123,7 +103,7 @@ struct WGConfig: Decodable {
|
||||
return settingsLines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private var interfaceSection: String {
|
||||
var str: String {
|
||||
"""
|
||||
[Interface]
|
||||
Address = \(clientIP)
|
||||
@@ -131,30 +111,9 @@ struct WGConfig: Decodable {
|
||||
MTU = \(mtu)
|
||||
PrivateKey = \(clientPrivateKey)
|
||||
\(settings)
|
||||
"""
|
||||
}
|
||||
|
||||
var str: String {
|
||||
if let peers = peers, !peers.isEmpty {
|
||||
let peerSections = peers.map { peer -> String in
|
||||
var lines = ["[Peer]", "PublicKey = \(peer.serverPublicKey)"]
|
||||
if let psk = peer.presharedKey, !psk.isEmpty {
|
||||
lines.append("PresharedKey = \(psk)")
|
||||
}
|
||||
lines.append("AllowedIPs = \(peer.allowedIPs.joined(separator: ", "))")
|
||||
lines.append("Endpoint = \(peer.hostName):\(peer.port)")
|
||||
if let ka = peer.persistentKeepAlive {
|
||||
lines.append("PersistentKeepalive = \(ka)")
|
||||
}
|
||||
return lines.joined(separator: "\n")
|
||||
}.joined(separator: "\n")
|
||||
return interfaceSection + "\n" + peerSections
|
||||
}
|
||||
return """
|
||||
\(interfaceSection)
|
||||
[Peer]
|
||||
PublicKey = \(serverPublicKey)
|
||||
\((presharedKey?.isEmpty ?? true) ? "" : "PresharedKey = \(presharedKey!)")
|
||||
\(presharedKey == nil ? "" : "PresharedKey = \(presharedKey!)")
|
||||
AllowedIPs = \(allowedIPs.joined(separator: ", "))
|
||||
Endpoint = \(hostName):\(port)
|
||||
PersistentKeepalive = \(persistentKeepAlive)
|
||||
@@ -162,21 +121,19 @@ struct WGConfig: Decodable {
|
||||
}
|
||||
|
||||
var redux: String {
|
||||
let peerCount = peers?.count ?? 1
|
||||
let peerInfo = peers.map { peers in
|
||||
peers.enumerated().map { i, peer in
|
||||
"[Peer \(i + 1)] Endpoint = \(peer.hostName):\(peer.port), AllowedIPs = \(peer.allowedIPs.joined(separator: ", "))"
|
||||
}.joined(separator: "\n")
|
||||
} ?? "Endpoint = \(hostName):\(port), AllowedIPs = \(allowedIPs.joined(separator: ", "))"
|
||||
return """
|
||||
"""
|
||||
[Interface]
|
||||
Address = \(clientIP)
|
||||
DNS = \(dns1), \(dns2)
|
||||
MTU = \(mtu)
|
||||
PrivateKey = ***
|
||||
\(settings)
|
||||
PeerCount = \(peerCount)
|
||||
\(peerInfo)
|
||||
[Peer]
|
||||
PublicKey = ***
|
||||
PresharedKey = ***
|
||||
AllowedIPs = \(allowedIPs.joined(separator: ", "))
|
||||
Endpoint = \(hostName):\(port)
|
||||
PersistentKeepalive = \(persistentKeepAlive)
|
||||
|
||||
SplitTunnelType = \(splitTunnelType)
|
||||
SplitTunnelSites = \(splitTunnelSites.joined(separator: ", "))
|
||||
|
||||
@@ -595,10 +595,6 @@ bool IosController::setupWireGuard()
|
||||
wgConfig.insert(configKey::persistentKeepAlive, "25");
|
||||
}
|
||||
|
||||
if (config.contains("peers") && config["peers"].isArray()) {
|
||||
wgConfig.insert("peers", config["peers"]);
|
||||
}
|
||||
|
||||
if (config.contains(configKey::isObfuscationEnabled) && config.value(configKey::isObfuscationEnabled).toBool()) {
|
||||
wgConfig.insert(configKey::initPacketMagicHeader, config[configKey::initPacketMagicHeader]);
|
||||
wgConfig.insert(configKey::responsePacketMagicHeader, config[configKey::responsePacketMagicHeader]);
|
||||
@@ -678,23 +674,7 @@ bool IosController::setupAwg()
|
||||
|
||||
wgConfig.insert(configKey::hostName, config[configKey::hostName]);
|
||||
wgConfig.insert(configKey::port, config[configKey::port]);
|
||||
|
||||
bool isMultiPeer = config.contains("peers") && config["peers"].isArray()
|
||||
&& !config["peers"].toArray().isEmpty();
|
||||
|
||||
wgConfig.insert(configKey::clientIp, config[configKey::clientIp]);
|
||||
if (isMultiPeer) {
|
||||
wgConfig.insert("peers", config["peers"]);
|
||||
wgConfig.insert(configKey::allowedIps, QJsonArray{}); // required by WGConfig decoder, unused in multi-peer path
|
||||
} else {
|
||||
if (config.contains(configKey::allowedIps) && config[configKey::allowedIps].isArray()) {
|
||||
wgConfig.insert(configKey::allowedIps, config[configKey::allowedIps]);
|
||||
} else {
|
||||
QJsonArray allowed_ips { "0.0.0.0/0", "::/0" };
|
||||
wgConfig.insert(configKey::allowedIps, allowed_ips);
|
||||
}
|
||||
}
|
||||
|
||||
wgConfig.insert(configKey::clientPrivKey, config[configKey::clientPrivKey]);
|
||||
wgConfig.insert(configKey::serverPubKey, config[configKey::serverPubKey]);
|
||||
wgConfig.insert(configKey::pskKey, config[configKey::pskKey]);
|
||||
@@ -708,6 +688,13 @@ bool IosController::setupAwg()
|
||||
|
||||
wgConfig.insert(configKey::splitTunnelSites, splitTunnelSites);
|
||||
|
||||
if (config.contains(configKey::allowedIps) && config[configKey::allowedIps].isArray()) {
|
||||
wgConfig.insert(configKey::allowedIps, config[configKey::allowedIps]);
|
||||
} else {
|
||||
QJsonArray allowed_ips { "0.0.0.0/0", "::/0" };
|
||||
wgConfig.insert(configKey::allowedIps, allowed_ips);
|
||||
}
|
||||
|
||||
if (config.contains(configKey::persistentKeepAlive)) {
|
||||
wgConfig.insert(configKey::persistentKeepAlive, config[configKey::persistentKeepAlive]);
|
||||
} else {
|
||||
|
||||
@@ -5,12 +5,8 @@
|
||||
#include "iputilslinux.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <linux/if_addr.h>
|
||||
#include <linux/netlink.h>
|
||||
#include <linux/rtnetlink.h>
|
||||
#include <net/if.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <QHostAddress>
|
||||
@@ -75,104 +71,39 @@ bool IPUtilsLinux::setMTUAndUp(const InterfaceConfig& config) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool addIPv4AddressNetlink(int ifindex, const QHostAddress& addr,
|
||||
int prefixlen) {
|
||||
int nlsock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
|
||||
if (nlsock < 0) return false;
|
||||
auto guard = qScopeGuard([&] { close(nlsock); });
|
||||
bool IPUtilsLinux::addIP4AddressToDevice(const InterfaceConfig& config) {
|
||||
struct ifreq ifr;
|
||||
struct sockaddr_in* ifrAddr = (struct sockaddr_in*)&ifr.ifr_addr;
|
||||
|
||||
char buf[512];
|
||||
memset(buf, 0, sizeof(buf));
|
||||
// Name the interface and set family
|
||||
strncpy(ifr.ifr_name, WG_INTERFACE, IFNAMSIZ);
|
||||
ifr.ifr_addr.sa_family = AF_INET;
|
||||
|
||||
struct nlmsghdr* nlmsg = reinterpret_cast<struct nlmsghdr*>(buf);
|
||||
nlmsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifaddrmsg));
|
||||
nlmsg->nlmsg_type = RTM_NEWADDR;
|
||||
nlmsg->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE | NLM_F_ACK;
|
||||
nlmsg->nlmsg_seq = 1;
|
||||
nlmsg->nlmsg_pid = 0;
|
||||
// Get the device address to add to interface
|
||||
QPair<QHostAddress, int> parsedAddr =
|
||||
QHostAddress::parseSubnet(config.m_deviceIpv4Address);
|
||||
QByteArray _deviceAddr = parsedAddr.first.toString().toLocal8Bit();
|
||||
char* deviceAddr = _deviceAddr.data();
|
||||
inet_pton(AF_INET, deviceAddr, &ifrAddr->sin_addr);
|
||||
|
||||
struct ifaddrmsg* ifa = static_cast<struct ifaddrmsg*>(NLMSG_DATA(nlmsg));
|
||||
ifa->ifa_family = AF_INET;
|
||||
ifa->ifa_prefixlen = prefixlen;
|
||||
ifa->ifa_flags = IFA_F_PERMANENT;
|
||||
ifa->ifa_scope = RT_SCOPE_UNIVERSE;
|
||||
ifa->ifa_index = ifindex;
|
||||
|
||||
struct in_addr ip4;
|
||||
QByteArray addrBytes = addr.toString().toLocal8Bit();
|
||||
inet_pton(AF_INET, addrBytes.constData(), &ip4);
|
||||
|
||||
auto appendAttr = [](struct nlmsghdr* nlmsg, size_t maxlen, int type,
|
||||
const void* data, size_t len) {
|
||||
size_t newlen = NLMSG_ALIGN(nlmsg->nlmsg_len) + RTA_SPACE(len);
|
||||
if (newlen > maxlen) return;
|
||||
char* p = reinterpret_cast<char*>(nlmsg) + NLMSG_ALIGN(nlmsg->nlmsg_len);
|
||||
struct rtattr* rta = reinterpret_cast<struct rtattr*>(p);
|
||||
rta->rta_type = type;
|
||||
rta->rta_len = RTA_LENGTH(len);
|
||||
memcpy(RTA_DATA(rta), data, len);
|
||||
nlmsg->nlmsg_len = newlen;
|
||||
};
|
||||
|
||||
appendAttr(nlmsg, sizeof(buf), IFA_LOCAL, &ip4, sizeof(ip4));
|
||||
appendAttr(nlmsg, sizeof(buf), IFA_ADDRESS, &ip4, sizeof(ip4));
|
||||
|
||||
struct sockaddr_nl nladdr;
|
||||
memset(&nladdr, 0, sizeof(nladdr));
|
||||
nladdr.nl_family = AF_NETLINK;
|
||||
|
||||
if (sendto(nlsock, buf, nlmsg->nlmsg_len, 0,
|
||||
reinterpret_cast<struct sockaddr*>(&nladdr),
|
||||
sizeof(nladdr)) < 0) {
|
||||
// Create IPv4 socket to perform the ioctl operations on
|
||||
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
||||
if (sockfd < 0) {
|
||||
logger.error() << "Failed to create ioctl socket.";
|
||||
return false;
|
||||
}
|
||||
auto guard = qScopeGuard([&] { close(sockfd); });
|
||||
|
||||
char ackbuf[1024];
|
||||
ssize_t acklen = recv(nlsock, ackbuf, sizeof(ackbuf), 0);
|
||||
if (acklen >= static_cast<ssize_t>(sizeof(struct nlmsghdr))) {
|
||||
struct nlmsghdr* ackmsg = reinterpret_cast<struct nlmsghdr*>(ackbuf);
|
||||
if (ackmsg->nlmsg_type == NLMSG_ERROR) {
|
||||
struct nlmsgerr* err = static_cast<struct nlmsgerr*>(NLMSG_DATA(ackmsg));
|
||||
if (err->error != 0) {
|
||||
errno = -err->error;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Set ifr to interface
|
||||
int ret = ioctl(sockfd, SIOCSIFADDR, &ifr);
|
||||
if (ret) {
|
||||
logger.error() << "Failed to set IPv4: " << deviceAddr
|
||||
<< "error:" << strerror(errno);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IPUtilsLinux::addIP4AddressToDevice(const InterfaceConfig& config) {
|
||||
if (config.m_deviceIpv4Address.isEmpty()) return true;
|
||||
|
||||
int ifindex = if_nametoindex(WG_INTERFACE);
|
||||
if (ifindex == 0) {
|
||||
logger.error() << "Failed to get ifindex for" << WG_INTERFACE;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const QStringList addresses =
|
||||
config.m_deviceIpv4Address.split(',', Qt::SkipEmptyParts);
|
||||
for (const QString& entry : addresses) {
|
||||
QPair<QHostAddress, int> parsed =
|
||||
QHostAddress::parseSubnet(entry.trimmed());
|
||||
if (parsed.first.isNull()) {
|
||||
logger.warning() << "Failed to parse IPv4 address:" << entry.trimmed();
|
||||
continue;
|
||||
}
|
||||
if (!addIPv4AddressNetlink(ifindex, parsed.first, parsed.second)) {
|
||||
logger.error() << "Failed to add IPv4" << parsed.first.toString() << "/"
|
||||
<< parsed.second << ":" << strerror(errno);
|
||||
} else {
|
||||
logger.debug() << "Added IPv4" << parsed.first.toString() << "/"
|
||||
<< parsed.second << "to" << WG_INTERFACE;
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool IPUtilsLinux::addIP6AddressToDevice(const InterfaceConfig& config) {
|
||||
// Set up the ifr and the companion ifr6
|
||||
struct in6_ifreq ifr6;
|
||||
|
||||
@@ -230,10 +230,7 @@ bool WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) {
|
||||
|
||||
out << "replace_allowed_ips=true\n";
|
||||
out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
|
||||
const QList<IPAddress>& primaryIPs = config.m_primaryPeerAllowedIPRanges.isEmpty()
|
||||
? config.m_allowedIPAddressRanges
|
||||
: config.m_primaryPeerAllowedIPRanges;
|
||||
for (const IPAddress& ip : primaryIPs) {
|
||||
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
|
||||
out << "allowed_ip=" << ip.toString() << "\n";
|
||||
}
|
||||
|
||||
@@ -247,38 +244,8 @@ bool WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) {
|
||||
int err = uapiErrno(uapiCommand(message));
|
||||
if (err != 0) {
|
||||
logger.error() << "Peer configuration failed:" << strerror(err);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const InterfaceConfig::AdditionalPeerConfig& peer : config.m_additionalPeers) {
|
||||
QByteArray pubKey = QByteArray::fromBase64(peer.m_serverPublicKey.toUtf8());
|
||||
QByteArray pskKey = QByteArray::fromBase64(peer.m_serverPskKey.toUtf8());
|
||||
|
||||
QString peerMsg;
|
||||
QTextStream peerOut(&peerMsg);
|
||||
peerOut << "set=1\n";
|
||||
peerOut << "public_key=" << QString(pubKey.toHex()) << "\n";
|
||||
if (!peer.m_serverPskKey.isEmpty()) {
|
||||
peerOut << "preshared_key=" << QString(pskKey.toHex()) << "\n";
|
||||
}
|
||||
peerOut << "endpoint=" << peer.m_serverIpv4AddrIn << ":" << peer.m_serverPort << "\n";
|
||||
peerOut << "replace_allowed_ips=true\n";
|
||||
peerOut << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
|
||||
for (const IPAddress& ip : peer.m_allowedIPAddressRanges) {
|
||||
peerOut << "allowed_ip=" << ip.toString() << "\n";
|
||||
}
|
||||
|
||||
if ((config.m_hopType != InterfaceConfig::MultiHopExit) && m_rtmonitor) {
|
||||
m_rtmonitor->addExclusionRoute(IPAddress(peer.m_serverIpv4AddrIn));
|
||||
}
|
||||
|
||||
int peerErr = uapiErrno(uapiCommand(peerMsg));
|
||||
if (peerErr != 0) {
|
||||
logger.error() << "Additional peer configuration failed:" << strerror(peerErr);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return (err == 0);
|
||||
}
|
||||
|
||||
bool WireguardUtilsLinux::deletePeer(const InterfaceConfig& config) {
|
||||
|
||||
@@ -80,9 +80,7 @@ bool IPUtilsMacos::setMTUAndUp(const InterfaceConfig& config) {
|
||||
}
|
||||
|
||||
bool IPUtilsMacos::addIP4AddressToDevice(const InterfaceConfig& config) {
|
||||
if (config.m_deviceIpv4Address.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
Q_UNUSED(config);
|
||||
QString ifname = MacOSDaemon::instance()->m_wgutils->interfaceName();
|
||||
struct ifaliasreq ifr;
|
||||
struct sockaddr_in* ifrAddr = (struct sockaddr_in*)&ifr.ifra_addr;
|
||||
@@ -93,28 +91,25 @@ bool IPUtilsMacos::addIP4AddressToDevice(const InterfaceConfig& config) {
|
||||
memset(&ifr, 0, sizeof(ifr));
|
||||
strncpy(ifr.ifra_name, qPrintable(ifname), IFNAMSIZ);
|
||||
|
||||
// Extract the host IP from CIDR notation (e.g. "10.8.0.2/24" → "10.8.0.2").
|
||||
// parseSubnet() zeroes host bits so we split manually to preserve the host address.
|
||||
QByteArray _deviceAddr = config.m_deviceIpv4Address.split('/').first().toLocal8Bit();
|
||||
// Get the device address to add to interface
|
||||
QPair<QHostAddress, int> parsedAddr =
|
||||
QHostAddress::parseSubnet(config.m_deviceIpv4Address);
|
||||
QByteArray _deviceAddr = parsedAddr.first.toString().toLocal8Bit();
|
||||
char* deviceAddr = _deviceAddr.data();
|
||||
ifrAddr->sin_family = AF_INET;
|
||||
ifrAddr->sin_len = sizeof(struct sockaddr_in);
|
||||
if (inet_pton(AF_INET, deviceAddr, &ifrAddr->sin_addr) != 1) {
|
||||
logger.error() << "Failed to parse IPv4 address:" << deviceAddr;
|
||||
return false;
|
||||
}
|
||||
inet_pton(AF_INET, deviceAddr, &ifrAddr->sin_addr);
|
||||
|
||||
// Set the netmask to /32
|
||||
ifrMask->sin_family = AF_INET;
|
||||
ifrMask->sin_len = sizeof(struct sockaddr_in);
|
||||
memset(&ifrMask->sin_addr, 0xff, sizeof(ifrMask->sin_addr));
|
||||
|
||||
// For P2P (utun) interfaces, ifra_broadaddr is the destination address.
|
||||
// Set it equal to the local address to create only a host route (not a network
|
||||
// route that would cause a routing loop).
|
||||
// Set the broadcast address.
|
||||
ifrBcast->sin_family = AF_INET;
|
||||
ifrBcast->sin_len = sizeof(struct sockaddr_in);
|
||||
ifrBcast->sin_addr.s_addr = ifrAddr->sin_addr.s_addr;
|
||||
ifrBcast->sin_addr.s_addr =
|
||||
(ifrAddr->sin_addr.s_addr | ~ifrMask->sin_addr.s_addr);
|
||||
|
||||
// Create an IPv4 socket to perform the ioctl operations on
|
||||
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
||||
|
||||
@@ -230,11 +230,7 @@ bool WireguardUtilsMacos::updatePeer(const InterfaceConfig& config) {
|
||||
|
||||
out << "replace_allowed_ips=true\n";
|
||||
out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
|
||||
// For multi-peer use only the primary peer's own IPs to avoid routing trie conflicts.
|
||||
const QList<IPAddress>& primaryIPs = config.m_primaryPeerAllowedIPRanges.isEmpty()
|
||||
? config.m_allowedIPAddressRanges
|
||||
: config.m_primaryPeerAllowedIPRanges;
|
||||
for (const IPAddress& ip : primaryIPs) {
|
||||
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
|
||||
out << "allowed_ip=" << ip.toString() << "\n";
|
||||
}
|
||||
|
||||
@@ -248,38 +244,8 @@ bool WireguardUtilsMacos::updatePeer(const InterfaceConfig& config) {
|
||||
int err = uapiErrno(uapiCommand(message));
|
||||
if (err != 0) {
|
||||
logger.error() << "Peer configuration failed:" << strerror(err);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const InterfaceConfig::AdditionalPeerConfig& peer : config.m_additionalPeers) {
|
||||
QByteArray pubKey = QByteArray::fromBase64(peer.m_serverPublicKey.toUtf8());
|
||||
QByteArray pskKey = QByteArray::fromBase64(peer.m_serverPskKey.toUtf8());
|
||||
|
||||
QString peerMsg;
|
||||
QTextStream peerOut(&peerMsg);
|
||||
peerOut << "set=1\n";
|
||||
peerOut << "public_key=" << QString(pubKey.toHex()) << "\n";
|
||||
if (!peer.m_serverPskKey.isEmpty()) {
|
||||
peerOut << "preshared_key=" << QString(pskKey.toHex()) << "\n";
|
||||
}
|
||||
peerOut << "endpoint=" << peer.m_serverIpv4AddrIn << ":" << peer.m_serverPort << "\n";
|
||||
peerOut << "replace_allowed_ips=true\n";
|
||||
peerOut << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
|
||||
for (const IPAddress& ip : peer.m_allowedIPAddressRanges) {
|
||||
peerOut << "allowed_ip=" << ip.toString() << "\n";
|
||||
}
|
||||
|
||||
if ((config.m_hopType != InterfaceConfig::MultiHopExit) && m_rtmonitor) {
|
||||
m_rtmonitor->addExclusionRoute(IPAddress(peer.m_serverIpv4AddrIn));
|
||||
}
|
||||
|
||||
int peerErr = uapiErrno(uapiCommand(peerMsg));
|
||||
if (peerErr != 0) {
|
||||
logger.error() << "Additional peer configuration failed:" << strerror(peerErr);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return (err == 0);
|
||||
}
|
||||
|
||||
bool WireguardUtilsMacos::deletePeer(const InterfaceConfig& config) {
|
||||
|
||||
@@ -181,10 +181,7 @@ bool WireguardUtilsWindows::updatePeer(const InterfaceConfig& config) {
|
||||
|
||||
out << "replace_allowed_ips=true\n";
|
||||
out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
|
||||
const QList<IPAddress>& primaryIPs = config.m_primaryPeerAllowedIPRanges.isEmpty()
|
||||
? config.m_allowedIPAddressRanges
|
||||
: config.m_primaryPeerAllowedIPRanges;
|
||||
for (const IPAddress& ip : primaryIPs) {
|
||||
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
|
||||
out << "allowed_ip=" << ip.toString() << "\n";
|
||||
}
|
||||
|
||||
@@ -196,33 +193,6 @@ bool WireguardUtilsWindows::updatePeer(const InterfaceConfig& config) {
|
||||
|
||||
QString reply = m_tunnel.uapiCommand(message);
|
||||
logger.debug() << "DATA:" << reply;
|
||||
|
||||
for (const InterfaceConfig::AdditionalPeerConfig& peer : config.m_additionalPeers) {
|
||||
QByteArray pubKey = QByteArray::fromBase64(peer.m_serverPublicKey.toUtf8());
|
||||
QByteArray pskKey = QByteArray::fromBase64(peer.m_serverPskKey.toUtf8());
|
||||
|
||||
QString peerMsg;
|
||||
QTextStream peerOut(&peerMsg);
|
||||
peerOut << "set=1\n";
|
||||
peerOut << "public_key=" << QString(pubKey.toHex()) << "\n";
|
||||
if (!peer.m_serverPskKey.isEmpty()) {
|
||||
peerOut << "preshared_key=" << QString(pskKey.toHex()) << "\n";
|
||||
}
|
||||
peerOut << "endpoint=" << peer.m_serverIpv4AddrIn << ":" << peer.m_serverPort << "\n";
|
||||
peerOut << "replace_allowed_ips=true\n";
|
||||
peerOut << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
|
||||
for (const IPAddress& ip : peer.m_allowedIPAddressRanges) {
|
||||
peerOut << "allowed_ip=" << ip.toString() << "\n";
|
||||
}
|
||||
|
||||
if (m_routeMonitor && config.m_hopType != InterfaceConfig::MultiHopExit) {
|
||||
m_routeMonitor->addExclusionRoute(IPAddress(peer.m_serverIpv4AddrIn));
|
||||
}
|
||||
|
||||
QString peerReply = m_tunnel.uapiCommand(peerMsg);
|
||||
logger.debug() << "Additional peer DATA:" << peerReply;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -2880,6 +2880,16 @@ Thank you for staying with us!</source>
|
||||
<source>Add them to the application if they were not displayed</source>
|
||||
<translation>Добавить их в приложение, если они не отображаются</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="126"/>
|
||||
<source>Backup</source>
|
||||
<translation>Резервное копирование</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="127"/>
|
||||
<source>Local copy of VPN protocols, services, all server settings and users</source>
|
||||
<translation>Локальная копия VPN-протоколов, сервисов, всех настроек сервера и пользователей</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsServerData.qml" line="124"/>
|
||||
<source>Reboot server</source>
|
||||
@@ -3569,6 +3579,34 @@ Thank you for staying with us!</source>
|
||||
<source>Continue</source>
|
||||
<translation>Продолжить</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>No containers found in backup file</source>
|
||||
<translation>В файле резервной копии не найдено контейнеров</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Installing %1 (%2/%3)...</source>
|
||||
<translation>Устанавливается %1 (%2/%3)...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>backup.tgz</source>
|
||||
<translation>backup.tgz</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>RestoredServer</source>
|
||||
<translation>Восстановленный сервер</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Restore from backup</source>
|
||||
<translation>Восстановить из резервной копии</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Restoration of VPN protocols, services, all server settings and users</source>
|
||||
<translation>Восстановление VPN-протоколов, сервисов, всех настроек сервера и пользователей</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Select Backup to Restore</source>
|
||||
<translation>Выберите файл резервной копии</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PageSetupWizardInstalling</name>
|
||||
@@ -5386,6 +5424,131 @@ FileZilla или другие SFTP-клиенты, а также смонтир
|
||||
<translation>Будет установлен протокол AmneziaWG. Он обеспечивает высокую скорость соединения и гарантирует стабильную работу даже в самых сложных условиях.</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PageSettingsServerBackup</name>
|
||||
<message>
|
||||
<source>Backup</source>
|
||||
<translation>Резервное копирование</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Local copy of VPN protocols, services, all server settings and users.</source>
|
||||
<translation>Локальная копия VPN-протоколов, сервисов, всех настроек сервера и пользователей.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>More about backups</source>
|
||||
<translation>Подробнее о резервных копиях</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Create backup</source>
|
||||
<translation>Создать резервную копию</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Restore from backup</source>
|
||||
<translation>Восстановить из резервной копии</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Create backup and download to device?</source>
|
||||
<translation>Создать резервную копию и скачать на устройство?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Backup will be created on server and automatically downloaded to your device</source>
|
||||
<translation>Резервная копия будет создана на сервере и автоматически скачана на ваше устройство</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Create server configuration backup?</source>
|
||||
<translation>Создать резервную копию конфигурации сервера?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>This will create a backup of your server containers configuration on the server</source>
|
||||
<translation>На сервере будет создана резервная копия конфигурации контейнеров</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Create</source>
|
||||
<translation>Создать</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cancel</source>
|
||||
<translation>Отмена</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Select Backup to Restore</source>
|
||||
<translation>Выберите файл резервной копии</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Backup files (*.tar.gz *.backup *.tgz *.gz)</source>
|
||||
<translation>Файлы резервных копий (*.tar.gz *.backup *.tgz *.gz)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Server</source>
|
||||
<translation>Сервер</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Backup created successfully: %1</source>
|
||||
<translation>Резервная копия успешно создана: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Backup downloaded successfully!\n\nSaved to:\n%1</source>
|
||||
<translation>Резервная копия успешно скачана!\n\nСохранена в:\n%1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Backup error: %1</source>
|
||||
<translation>Ошибка резервного копирования: %1</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PageSettingsServerRestoreMode</name>
|
||||
<message>
|
||||
<source>%1 on %2</source>
|
||||
<translation>%1 на %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Restore from backup</source>
|
||||
<translation>Восстановление из резервной копии</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Add data from backup</source>
|
||||
<translation>Добавить данные из резервной копии</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If the same protocols are already installed on the server, they will be updated. Created users and access will be saved</source>
|
||||
<translation>Если на сервере уже установлены такие же протоколы, они будут обновлены. Созданные пользователи и доступы сохранятся</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Replace</source>
|
||||
<translation>Заменить</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>All installed protocols, users and their access will not be saved</source>
|
||||
<translation>Все установленные протоколы, пользователи и их доступы не будут сохранены</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Backup restore error: %1</source>
|
||||
<translation>Ошибка восстановления из резервной копии: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Server</source>
|
||||
<translation>Сервер</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PageSettingsServerBackupRestored</name>
|
||||
<message>
|
||||
<source>%1 on "%2"</source>
|
||||
<translation>%1 на "%2"</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Backup restored</source>
|
||||
<translation>Резервная копия восстановлена</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>To home</source>
|
||||
<translation>На главную</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>To server settings</source>
|
||||
<translation>К настройкам сервера</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>main2</name>
|
||||
<message>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
#include "subscriptionUiController.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
#include "amneziaApplication.h"
|
||||
#include "core/configurators/wireguardConfigurator.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
|
||||
@@ -32,6 +32,9 @@ namespace PageLoader
|
||||
PageSettingsNewsNotifications,
|
||||
PageSettingsNewsDetail,
|
||||
PageSettingsBackup,
|
||||
PageSettingsServerBackup,
|
||||
PageSettingsServerRestoreMode,
|
||||
PageSettingsServerBackupRestored,
|
||||
PageSettingsAbout,
|
||||
PageSettingsLogging,
|
||||
PageSettingsSplitTunneling,
|
||||
@@ -151,6 +154,7 @@ signals:
|
||||
void goToPageSettings();
|
||||
void goToPageViewConfig();
|
||||
void goToPageSettingsServerServices();
|
||||
void goToPageSettingsServerManagement();
|
||||
void goToPageSettingsBackup();
|
||||
void goToShareConnectionPage(QString headerText, QString configContentHeaderText, QString configCaption, QString configExtension,
|
||||
QString configFileName);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,406 @@
|
||||
#ifndef SERVERSBACKUPCONTROLLER_H
|
||||
#define SERVERSBACKUPCONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QDateTime>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QFileInfo>
|
||||
|
||||
class QTemporaryFile;
|
||||
class ServersModel;
|
||||
class ServersUiController;
|
||||
class ServersController;
|
||||
class SecureQSettings;
|
||||
|
||||
#include <QPointer>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/commonStructs.h"
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
/**
|
||||
* @brief Controller for managing Amnezia VPN configuration backups
|
||||
*
|
||||
* Uses existing ServerController and libssh::Client from Amnezia
|
||||
* Bash scripts are embedded directly in C++ code
|
||||
* Supports direct container backup via docker cp
|
||||
*
|
||||
* Fully cross-platform: Windows, macOS, Linux, iOS, Android
|
||||
*/
|
||||
class ServersBackupController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ServersBackupController(SecureQSettings *settings, ServersModel *serversModel,
|
||||
ServersUiController *serversUiController, ServersController *serversController,
|
||||
QObject *parent = nullptr);
|
||||
~ServersBackupController();
|
||||
|
||||
/**
|
||||
* @brief Backup information
|
||||
*/
|
||||
struct BackupInfo {
|
||||
QString filename;
|
||||
QString fullPath;
|
||||
QDateTime createdAt;
|
||||
qint64 size;
|
||||
bool isValid;
|
||||
QStringList containers;
|
||||
};
|
||||
|
||||
enum BackupStatus {
|
||||
Idle,
|
||||
InProgress,
|
||||
Success,
|
||||
Failed
|
||||
};
|
||||
Q_ENUM(BackupStatus)
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Create backup on server (all containers)
|
||||
* @param credentials Server credentials
|
||||
*/
|
||||
void createBackup(const ServerCredentials &credentials);
|
||||
|
||||
/**
|
||||
* @brief Create backup and automatically download to device (for QML)
|
||||
* @param downloadToDevice Download to device after creation?
|
||||
* @param deleteFromServer Delete from server after download?
|
||||
*/
|
||||
Q_INVOKABLE void createBackupWithDownload(bool downloadToDevice = true,
|
||||
bool deleteFromServer = true);
|
||||
|
||||
/**
|
||||
* @brief Create backup of specific container
|
||||
* @param credentials Server credentials
|
||||
* @param container Container type for backup
|
||||
*/
|
||||
void createContainerBackup(const ServerCredentials &credentials, DockerContainer container);
|
||||
|
||||
/**
|
||||
* @brief Create backup of specific container by name
|
||||
* @param credentials Server credentials
|
||||
* @param containerName Container name (e.g. "amnezia-awg")
|
||||
*/
|
||||
void createBackupByName(const ServerCredentials &credentials, const QString &containerName);
|
||||
|
||||
/**
|
||||
* @brief Create backup of multiple containers
|
||||
* @param credentials Server credentials
|
||||
* @param containers List of containers for backup
|
||||
*/
|
||||
void createContainersBackup(const ServerCredentials &credentials, const QList<DockerContainer> &containers);
|
||||
|
||||
/**
|
||||
* @brief Get list of backups from server
|
||||
* @param credentials Server credentials
|
||||
*/
|
||||
void fetchBackupList(const ServerCredentials &credentials);
|
||||
|
||||
/**
|
||||
* @brief Restore from backup
|
||||
* @param credentials Server credentials
|
||||
* @param backupFilename Backup file name
|
||||
* @param containers List of containers (empty = all)
|
||||
* @param replaceMode If true - clears container first, then restores. If false - adds data on top of existing
|
||||
*/
|
||||
void restoreBackup(const ServerCredentials &credentials,
|
||||
const QString &backupFilename,
|
||||
const QStringList &containers = QStringList(),
|
||||
bool replaceMode = false);
|
||||
|
||||
/**
|
||||
* @brief Check backup status on server
|
||||
* @param credentials Server credentials
|
||||
*/
|
||||
void checkBackupStatus(const ServerCredentials &credentials);
|
||||
|
||||
/**
|
||||
* @brief Download backup to local machine
|
||||
* @param credentials Server credentials
|
||||
* @param backupFilename Backup file name
|
||||
* @param localPath Save path
|
||||
*/
|
||||
void downloadBackup(const ServerCredentials &credentials,
|
||||
const QString &backupFilename,
|
||||
const QString &localPath);
|
||||
|
||||
/**
|
||||
* @brief Upload backup to server
|
||||
* @param credentials Server credentials
|
||||
* @param localPath Path to local file
|
||||
* @param replaceMode Restore mode (true = replace, false = add). Saved for later use in restoreBackup
|
||||
*/
|
||||
void uploadBackup(const ServerCredentials &credentials,
|
||||
const QString &localPath,
|
||||
bool replaceMode = false);
|
||||
|
||||
// Overloaded method for setup wizard with separate credential parameters
|
||||
Q_INVOKABLE void uploadBackupWithStrings(const QString &hostname,
|
||||
const QString &username,
|
||||
const QString &secretData,
|
||||
const QString &localPath,
|
||||
bool replaceMode = false);
|
||||
|
||||
/**
|
||||
* @brief Universal method to start restore (from QML)
|
||||
* Automatically selects correct path depending on parameters
|
||||
* @param isFromSetupWizard Restore from setup wizard?
|
||||
* @param backupFilePath Path to local backup file
|
||||
* @param replaceMode Restore mode (true = replace, false = add)
|
||||
* @param wizardHostname Hostname for setup wizard (optional)
|
||||
* @param wizardUsername Username for setup wizard (optional)
|
||||
* @param wizardSecretData Secret data for setup wizard (optional)
|
||||
*/
|
||||
Q_INVOKABLE void startRestore(bool isFromSetupWizard,
|
||||
const QString &backupFilePath,
|
||||
bool replaceMode,
|
||||
const QString &wizardHostname = QString(),
|
||||
const QString &wizardUsername = QString(),
|
||||
const QString &wizardSecretData = QString());
|
||||
|
||||
/**
|
||||
* @brief Prepare restore information from backup file
|
||||
* Parses filename, extracts IP, prepares metadata
|
||||
* @param backupFilePath Path to backup file
|
||||
* @return QVariantMap with keys: fileName, serverIp
|
||||
*/
|
||||
Q_INVOKABLE QVariantMap getBackupFileInfo(const QString &backupFilePath);
|
||||
|
||||
/**
|
||||
* @brief Scan backup file and determine which containers it contains
|
||||
* @param localPath Path to local backup file
|
||||
* @return List of container names found in backup
|
||||
*/
|
||||
Q_INVOKABLE QStringList scanBackupForContainers(const QString &localPath);
|
||||
|
||||
/**
|
||||
* @brief Set default server and container after restore (for setup wizard)
|
||||
* @param isFromSetupWizard Was restore called from setup wizard
|
||||
* @return true if successful, false if no servers or containers
|
||||
*/
|
||||
Q_INVOKABLE bool setDefaultServerAfterRestore(bool isFromSetupWizard);
|
||||
|
||||
/**
|
||||
* @brief Install containers from backup on empty server (for setup wizard)
|
||||
* Scans backup, adds empty server and sends signal to install containers
|
||||
* @param backupFilePath Path to local backup file
|
||||
* @param hostname Server hostname
|
||||
* @param username Username for SSH
|
||||
* @param secretData Password/key for SSH
|
||||
*/
|
||||
Q_INVOKABLE void prepareRestoreFromBackup(const QString &backupFilePath,
|
||||
const QString &hostname,
|
||||
const QString &username,
|
||||
const QString &secretData);
|
||||
|
||||
/**
|
||||
* @brief Delete backup from server
|
||||
* @param credentials Server credentials
|
||||
* @param backupFilename Backup file name
|
||||
*/
|
||||
void deleteBackup(const ServerCredentials &credentials,
|
||||
const QString &backupFilename);
|
||||
|
||||
/**
|
||||
* @brief Set backup directory on server
|
||||
*/
|
||||
void setBackupDirectory(const QString &directory);
|
||||
|
||||
/**
|
||||
* @brief Get backup directory
|
||||
*/
|
||||
QString backupDirectory() const { return m_backupDir; }
|
||||
|
||||
signals:
|
||||
/**
|
||||
* @brief Operation status changed
|
||||
*/
|
||||
void statusChanged(BackupStatus status);
|
||||
|
||||
/**
|
||||
* @brief Operation progress (0-100)
|
||||
*/
|
||||
void progressChanged(int percent, const QString &message);
|
||||
|
||||
/**
|
||||
* @brief Backup list received
|
||||
*/
|
||||
void backupListReceived(const QList<BackupInfo> &backups);
|
||||
|
||||
/**
|
||||
* @brief Backup created successfully
|
||||
*/
|
||||
void backupCreated(const QString &backupFilename);
|
||||
|
||||
/**
|
||||
* @brief Backup restored successfully
|
||||
*/
|
||||
void backupRestored();
|
||||
|
||||
/**
|
||||
* @brief Need to set default server and container (for setup wizard)
|
||||
* This signal is sent after backupRestored() if restore was from setup wizard
|
||||
*/
|
||||
void needSetDefaultServer();
|
||||
|
||||
/**
|
||||
* @brief Default server and container successfully set
|
||||
* Can navigate to result page
|
||||
*/
|
||||
void defaultServerAndContainerSet();
|
||||
|
||||
/**
|
||||
* @brief All containers from backup installed
|
||||
* Can proceed to data restore
|
||||
* @param backupFilePath Path to backup file
|
||||
* @param hostname Hostname
|
||||
* @param username Username
|
||||
* @param secretData Secret data
|
||||
* @param serverIp IP address (for display)
|
||||
* @param fileName File name (for display)
|
||||
*/
|
||||
void readyForRestore(const QString &backupFilePath,
|
||||
const QString &hostname,
|
||||
const QString &username,
|
||||
const QString &secretData,
|
||||
const QString &serverIp,
|
||||
const QString &fileName);
|
||||
|
||||
/**
|
||||
* @brief Backup downloaded
|
||||
*/
|
||||
void backupDownloaded(const QString &localPath);
|
||||
|
||||
/**
|
||||
* @brief Backup uploaded to server
|
||||
*/
|
||||
void backupUploaded(const QString &serverPath);
|
||||
|
||||
/**
|
||||
* @brief Backup status information received
|
||||
*/
|
||||
void backupStatusReceived(const QJsonObject &status);
|
||||
|
||||
/**
|
||||
* @brief Error occurred
|
||||
*/
|
||||
void errorOccurred(const QString &errorMessage, ErrorCode errorCode);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Get bash script for creating backup of all containers
|
||||
* @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11")
|
||||
*/
|
||||
QString getBackupScript(const QString &ipAddress) const;
|
||||
|
||||
/**
|
||||
* @brief Get bash script for creating backup of specific container
|
||||
* @param container Container type
|
||||
* @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11")
|
||||
*/
|
||||
QString getContainerBackupScript(DockerContainer container, const QString &ipAddress) const;
|
||||
|
||||
/**
|
||||
* @brief Get bash script for creating backup of multiple containers
|
||||
* @param containers List of containers
|
||||
* @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11")
|
||||
*/
|
||||
QString getContainersBackupScript(const QList<DockerContainer> &containers, const QString &ipAddress) const;
|
||||
|
||||
/**
|
||||
* @brief Get bash script for restore
|
||||
* @param backupFilename Backup file name
|
||||
* @param containers List of containers
|
||||
* @param replaceMode If true - clears container first, then restores
|
||||
*/
|
||||
QString getRestoreScript(const QString &backupFilename, const QStringList &containers, bool replaceMode = false) const;
|
||||
|
||||
/**
|
||||
* @brief Get bash script for status check
|
||||
*/
|
||||
QString getCheckStatusScript() const;
|
||||
|
||||
/**
|
||||
* @brief Get bash script for backup list
|
||||
*/
|
||||
QString getListBackupsScript() const;
|
||||
|
||||
/**
|
||||
* @brief Parse backup list from output
|
||||
*/
|
||||
QList<BackupInfo> parseBackupList(const QString &output);
|
||||
|
||||
/**
|
||||
* @brief Parse status from output
|
||||
*/
|
||||
QJsonObject parseBackupStatus(const QString &output);
|
||||
|
||||
/**
|
||||
* @brief Handle standard output
|
||||
*/
|
||||
ErrorCode handleStdOut(const QString &data, QString &output);
|
||||
|
||||
/**
|
||||
* @brief Handle error output
|
||||
*/
|
||||
ErrorCode handleStdErr(const QString &data, QString &error);
|
||||
|
||||
/**
|
||||
* @brief Set status
|
||||
*/
|
||||
void setStatus(BackupStatus status);
|
||||
|
||||
/**
|
||||
* @brief Set progress
|
||||
*/
|
||||
void setProgress(int percent, const QString &message);
|
||||
|
||||
/**
|
||||
* @brief Attempt to set default container (called from timer)
|
||||
*/
|
||||
void trySetDefaultContainer();
|
||||
|
||||
ErrorCode runHostScript(const ServerCredentials &credentials, const QString &script,
|
||||
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbStdOut = nullptr,
|
||||
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbStdErr = nullptr);
|
||||
ErrorCode downloadFileFromHost(const ServerCredentials &credentials, const QString &remotePath, const QString &localPath);
|
||||
ErrorCode uploadFileToHostPublic(const ServerCredentials &credentials, const QString &localPath, const QString &remotePath,
|
||||
libssh::ScpOverwriteMode overwriteMode = libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
|
||||
private:
|
||||
QPointer<SecureQSettings> m_settings;
|
||||
ServersModel *m_serversModel;
|
||||
ServersUiController *m_serversUiController;
|
||||
ServersController *m_serversController;
|
||||
SshSession m_sshSession;
|
||||
BackupStatus m_status;
|
||||
QString m_backupDir;
|
||||
QString m_currentOutput;
|
||||
QString m_currentError;
|
||||
bool m_restoreReplaceMode; // Save restore mode for use after uploadBackup
|
||||
QTemporaryFile *m_tempUploadFile; // Temp file for Android URI (to prevent deletion before upload completes)
|
||||
|
||||
// For setting default container
|
||||
int m_containerRetryCount;
|
||||
static constexpr int m_maxContainerRetries = 3;
|
||||
|
||||
// For automatic restore after upload
|
||||
ServerCredentials m_pendingRestoreCredentials;
|
||||
bool m_autoRestoreAfterUpload;
|
||||
|
||||
// For automatic backup download/delete
|
||||
bool m_autoDownloadAfterCreate;
|
||||
bool m_autoDeleteAfterDownload;
|
||||
QString m_lastCreatedBackupFilename;
|
||||
};
|
||||
|
||||
#endif // SERVERSBACKUPCONTROLLER_H
|
||||
|
||||
@@ -168,6 +168,42 @@ void SystemController::setQmlRoot(QObject *qmlRoot)
|
||||
m_qmlRoot = qmlRoot;
|
||||
}
|
||||
|
||||
QString SystemController::getFileNameFromPath(const QString &filePath)
|
||||
{
|
||||
if (filePath.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
// Для Android URI используем специальный метод для получения имени файла
|
||||
if (filePath.startsWith("content://")) {
|
||||
QString fileName = AndroidController::instance()->getFileName(filePath);
|
||||
if (!fileName.isEmpty()) {
|
||||
return fileName;
|
||||
}
|
||||
// Если не удалось получить имя через ContentResolver, пытаемся извлечь из URI
|
||||
}
|
||||
#endif
|
||||
|
||||
// Для обычных путей или если Android метод не сработал
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString fileName = fileInfo.fileName();
|
||||
|
||||
// Если имя файла пустое, пытаемся извлечь из пути
|
||||
if (fileName.isEmpty()) {
|
||||
QStringList parts = filePath.split('/');
|
||||
if (!parts.isEmpty()) {
|
||||
fileName = parts.last();
|
||||
// Декодируем URL-кодированные символы
|
||||
if (fileName.contains('%')) {
|
||||
fileName = QUrl::fromPercentEncoding(fileName.toUtf8());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
bool SystemController::isAuthenticated()
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
|
||||
@@ -18,6 +18,13 @@ public:
|
||||
public slots:
|
||||
QString getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile = "",
|
||||
const bool isSaveMode = false, const QString &defaultSuffix = "");
|
||||
|
||||
/**
|
||||
* @brief Получить имя файла из пути или URI (для Android)
|
||||
* @param filePath Путь к файлу или URI
|
||||
* @return Имя файла
|
||||
*/
|
||||
Q_INVOKABLE QString getFileNameFromPath(const QString &filePath);
|
||||
|
||||
void setQmlRoot(QObject *qmlRoot);
|
||||
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Components"
|
||||
import "../Config"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if(backButton.enabled && backButton.activeFocus) {
|
||||
flickable.contentY = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flickable
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
contentHeight: contentColumn.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
width: flickable.width
|
||||
|
||||
spacing: 16
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
|
||||
headerText: qsTr("Backup")
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: qsTr("Local copy of VPN protocols, services, all server settings and users.")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: -8
|
||||
|
||||
text: qsTr("More about backups")
|
||||
color: AmneziaStyle.color.goldenApricot
|
||||
font.pixelSize: 14
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
// TODO: Open help page or show more info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
spacing: 12
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Create backup")
|
||||
|
||||
clickedFunc: function() {
|
||||
createBackup(true)
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Restore from backup")
|
||||
defaultColor: AmneziaStyle.color.transparent
|
||||
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
|
||||
pressedColor: Qt.rgba(1, 1, 1, 0.12)
|
||||
disabledColor: AmneziaStyle.color.mutedGray
|
||||
textColor: AmneziaStyle.color.goldenApricot
|
||||
borderWidth: 1
|
||||
|
||||
clickedFunc: function() {
|
||||
restoreBackup()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createBackup(shouldDownload) {
|
||||
// Default shouldDownload = true
|
||||
var downloadAfterCreate = (shouldDownload !== undefined) ? shouldDownload : true
|
||||
|
||||
var headerText = downloadAfterCreate ?
|
||||
qsTr("Create backup and download to device?") :
|
||||
qsTr("Create server configuration backup?")
|
||||
var descriptionText = downloadAfterCreate ?
|
||||
qsTr("Backup will be created on server and automatically downloaded to your device") :
|
||||
qsTr("This will create a backup of your server containers configuration on the server")
|
||||
var yesButtonText = qsTr("Create")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
|
||||
var yesButtonFunction = function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
// Call C++ method that manages download and delete automatically
|
||||
ServersBackupController.createBackupWithDownload(downloadAfterCreate, true)
|
||||
}
|
||||
var noButtonFunction = function() {}
|
||||
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
|
||||
function restoreBackup() {
|
||||
var filter = GC.isMobile() ? "*.gz *.tgz *.tar.gz" : "Backup files (*.tar.gz *.backup *.tgz *.gz)"
|
||||
var localPath = SystemController.getFileName(
|
||||
qsTr("Select Backup to Restore"),
|
||||
filter,
|
||||
"",
|
||||
false,
|
||||
""
|
||||
)
|
||||
|
||||
if (!localPath || localPath.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get file information via C++
|
||||
var fileInfo = ServersBackupController.getBackupFileInfo(localPath)
|
||||
var fileName = fileInfo.fileName || "backup.tgz"
|
||||
var serverIp = fileInfo.serverIp || ""
|
||||
|
||||
// If IP not found in filename, use current server
|
||||
if (!serverIp || serverIp.length === 0) {
|
||||
serverIp = ServersUiController.serverHostName(ServersUiController.processedServerId) || ""
|
||||
}
|
||||
|
||||
var serverName = ServersUiController.serverName(ServersUiController.processedServerId) || qsTr("Server")
|
||||
|
||||
// Open restore mode selection page
|
||||
var parentItem = root.parent
|
||||
while (parentItem && parentItem.objectName !== "tabBarStackView") {
|
||||
parentItem = parentItem.parent
|
||||
}
|
||||
|
||||
if (parentItem && typeof parentItem.push === "function") {
|
||||
parentItem.push(PageController.getPagePath(PageEnum.PageSettingsServerRestoreMode), {
|
||||
"backupFilePath": localPath,
|
||||
"backupFileName": fileName,
|
||||
"serverName": serverName,
|
||||
"serverIp": serverIp
|
||||
})
|
||||
} else {
|
||||
console.warn("Could not find StackView to navigate to restore mode page")
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Backup Controller Connections ============
|
||||
|
||||
Connections {
|
||||
target: ServersBackupController
|
||||
|
||||
function onBackupCreated(backupFilename) {
|
||||
// If auto-download is not enabled, show success message
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.showNotificationMessage(qsTr("Backup created successfully: %1").arg(backupFilename))
|
||||
}
|
||||
|
||||
function onBackupDownloaded(localPath) {
|
||||
PageController.showBusyIndicator(false)
|
||||
console.log("Backup downloaded to:", localPath)
|
||||
PageController.showNotificationMessage(qsTr("Backup downloaded successfully!\n\nSaved to:\n%1").arg(localPath))
|
||||
}
|
||||
|
||||
function onProgressChanged(percent, message) {
|
||||
console.log("Backup progress:", percent, "%", message)
|
||||
}
|
||||
|
||||
function onErrorOccurred(errorMessage, errorCode) {
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.showErrorMessage(qsTr("Backup error: %1").arg(errorMessage))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Components"
|
||||
import "../Config"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property string backupFileName: ""
|
||||
property string serverName: ""
|
||||
property string serverIp: ""
|
||||
property bool isFromSetupWizard: false
|
||||
|
||||
Component.onCompleted: {
|
||||
// Убеждаемся, что все свойства инициализированы
|
||||
if (!backupFileName) backupFileName = ""
|
||||
if (!serverName) serverName = ""
|
||||
if (!serverIp) serverIp = ""
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
backButtonFunction: function() {
|
||||
// После успешного restore всегда идем на главную страницу
|
||||
PageController.goToPageHome()
|
||||
}
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if(backButton.enabled && backButton.activeFocus) {
|
||||
flickable.contentY = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flickable
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
contentHeight: contentColumn.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
width: flickable.width
|
||||
|
||||
spacing: 16
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
|
||||
headerText: qsTr("Backup restored")
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: {
|
||||
var baseText = qsTr("%1 on \"%2\"").arg(backupFileName).arg(serverName)
|
||||
if (serverIp && serverIp.length > 0) {
|
||||
return baseText + ", " + serverIp
|
||||
}
|
||||
return baseText
|
||||
}
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
spacing: 12
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("To home")
|
||||
implicitHeight: 56
|
||||
|
||||
clickedFunc: function() {
|
||||
// Переход на главную страницу (PageHome)
|
||||
PageController.goToPageHome()
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("To server settings")
|
||||
implicitHeight: 56
|
||||
defaultColor: AmneziaStyle.color.transparent
|
||||
hoveredColor: AmneziaStyle.color.transparent
|
||||
pressedColor: AmneziaStyle.color.transparent
|
||||
borderWidth: 1
|
||||
borderColor: "#FFFFFF"
|
||||
textColor: "#FFFFFF"
|
||||
|
||||
clickedFunc: function() {
|
||||
// Открываем страницу настроек сервера с активной вкладкой "Управление"
|
||||
PageController.goToPage(PageEnum.PageSettingsServerInfo)
|
||||
// Устанавливаем активной вкладку "Управление" через сигнал
|
||||
PageController.goToPageSettingsServerManagement()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,7 @@ PageType {
|
||||
|
||||
property list<QtObject> serverActions: [
|
||||
check,
|
||||
backupSection,
|
||||
reboot,
|
||||
remove,
|
||||
clear,
|
||||
@@ -106,6 +107,7 @@ PageType {
|
||||
id: check
|
||||
|
||||
property bool isVisible: root.isServerWithWriteAccess
|
||||
readonly property bool isBackupSection: false
|
||||
readonly property string title: qsTr("Check the server for previously installed Amnezia services")
|
||||
readonly property string description: qsTr("Add them to the application if they were not displayed")
|
||||
readonly property var tColor: AmneziaStyle.color.paleGray
|
||||
@@ -116,10 +118,25 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: backupSection
|
||||
|
||||
property bool isVisible: root.isServerWithWriteAccess
|
||||
readonly property bool isBackupSection: false
|
||||
readonly property string title: qsTr("Backup")
|
||||
readonly property string description: qsTr("Local copy of VPN protocols, services, all server settings and users")
|
||||
readonly property var tColor: AmneziaStyle.color.paleGray
|
||||
readonly property var clickedHandler: function() {
|
||||
// Navigate to server backup page using PageController
|
||||
PageController.goToPage(PageEnum.PageSettingsServerBackup)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: reboot
|
||||
|
||||
property bool isVisible: root.isServerWithWriteAccess
|
||||
readonly property bool isBackupSection: false
|
||||
readonly property string title: qsTr("Reboot server")
|
||||
readonly property string description: ""
|
||||
readonly property var tColor: AmneziaStyle.color.vibrantRed
|
||||
@@ -150,6 +167,7 @@ PageType {
|
||||
id: remove
|
||||
|
||||
property bool isVisible: true
|
||||
readonly property bool isBackupSection: false
|
||||
readonly property string title: qsTr("Remove server from application")
|
||||
readonly property string description: ""
|
||||
readonly property var tColor: AmneziaStyle.color.vibrantRed
|
||||
@@ -180,6 +198,7 @@ PageType {
|
||||
id: clear
|
||||
|
||||
property bool isVisible: root.isServerWithWriteAccess
|
||||
readonly property bool isBackupSection: false
|
||||
readonly property string title: qsTr("Clear server from Amnezia software")
|
||||
readonly property string description: ""
|
||||
readonly property var tColor: AmneziaStyle.color.vibrantRed
|
||||
@@ -209,6 +228,7 @@ PageType {
|
||||
id: reset
|
||||
|
||||
property bool isVisible: ServersUiController.isServerFromApi(ServersUiController.processedServerId)
|
||||
readonly property bool isBackupSection: false
|
||||
readonly property string title: qsTr("Reset API config")
|
||||
readonly property string description: ""
|
||||
readonly property var tColor: AmneziaStyle.color.vibrantRed
|
||||
@@ -235,4 +255,5 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ PageType {
|
||||
function onGoToPageSettingsServerServices() {
|
||||
tabBar.setCurrentIndex(root.pageSettingsServerServices)
|
||||
}
|
||||
|
||||
function onGoToPageSettingsServerManagement() {
|
||||
tabBar.setCurrentIndex(root.pageSettingsServerData)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Components"
|
||||
import "../Config"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property string backupFilePath: ""
|
||||
property string backupFileName: ""
|
||||
property string serverName: ""
|
||||
property string serverIp: ""
|
||||
property bool isFromSetupWizard: false
|
||||
|
||||
// Credentials for setup wizard (when server is not yet added to ServersModel)
|
||||
property string wizardHostname: ""
|
||||
property string wizardUsername: ""
|
||||
property string wizardSecretData: ""
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if(backButton.enabled && backButton.activeFocus) {
|
||||
flickable.contentY = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flickable
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
contentHeight: contentColumn.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
width: flickable.width
|
||||
|
||||
spacing: 16
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
|
||||
headerText: qsTr("Restore from backup")
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: {
|
||||
// Show only filename and IP address, without server name
|
||||
if (serverIp && serverIp.length > 0) {
|
||||
return qsTr("%1 on %2").arg(backupFileName).arg(serverIp)
|
||||
}
|
||||
return backupFileName
|
||||
}
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
spacing: 0
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Add data from backup")
|
||||
descriptionText: qsTr("If the same protocols are already installed on the server, they will be updated. Created users and access will be saved")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
startRestore(false) // false = add mode
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Replace")
|
||||
descriptionText: qsTr("All installed protocols, users and their access will not be saved")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
textColor: AmneziaStyle.color.vibrantRed
|
||||
|
||||
clickedFunction: function() {
|
||||
startRestore(true) // true = replace mode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property bool restoreReplaceMode: false
|
||||
|
||||
function startRestore(replaceMode) {
|
||||
restoreReplaceMode = replaceMode
|
||||
PageController.showBusyIndicator(true)
|
||||
|
||||
// Call universal C++ method that will determine how to perform restore
|
||||
ServersBackupController.startRestore(
|
||||
isFromSetupWizard,
|
||||
backupFilePath,
|
||||
replaceMode,
|
||||
wizardHostname || "",
|
||||
wizardUsername || "",
|
||||
wizardSecretData || ""
|
||||
)
|
||||
}
|
||||
|
||||
property string lastUploadedBackupFilename: ""
|
||||
|
||||
Connections {
|
||||
target: ServersBackupController
|
||||
|
||||
function onBackupRestored() {
|
||||
console.log(" onBackupRestored, isFromSetupWizard:", isFromSetupWizard)
|
||||
|
||||
// For setup wizard, call C++ method to set default server and container
|
||||
if (isFromSetupWizard) {
|
||||
ServersBackupController.setDefaultServerAfterRestore(true)
|
||||
} else {
|
||||
// For regular mode, navigate directly
|
||||
PageController.showBusyIndicator(false)
|
||||
navigateToRestoredPage()
|
||||
}
|
||||
}
|
||||
|
||||
function onDefaultServerAndContainerSet() {
|
||||
console.log(" onDefaultServerAndContainerSet - navigating to restored page")
|
||||
// C++ has set default server and container, navigate to result page
|
||||
PageController.showBusyIndicator(false)
|
||||
navigateToRestoredPage()
|
||||
}
|
||||
|
||||
function onErrorOccurred(errorMessage, errorCode) {
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.showErrorMessage(qsTr("Backup restore error: %1").arg(errorMessage))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function navigateToRestoredPage() {
|
||||
// Navigate to successful restore page
|
||||
// Get actual server name from model
|
||||
var actualServerName = serverName
|
||||
if (root.isFromSetupWizard && ServersUiController.getServersCount() > 0) {
|
||||
var lastServerId = ServersUiController.getServerId(ServersUiController.getServersCount() - 1)
|
||||
actualServerName = ServersUiController.serverName(lastServerId) || qsTr("Server")
|
||||
} else if (!serverName || serverName.length === 0) {
|
||||
actualServerName = ServersUiController.serverName(ServersUiController.processedServerId) || qsTr("Server")
|
||||
}
|
||||
|
||||
var parentItem = root.parent
|
||||
|
||||
// For setup wizard use regular StackView
|
||||
if (root.isFromSetupWizard) {
|
||||
while (parentItem && typeof parentItem.push !== "function") {
|
||||
parentItem = parentItem.parent
|
||||
}
|
||||
if (parentItem && typeof parentItem.push === "function") {
|
||||
parentItem.push(PageController.getPagePath(PageEnum.PageSettingsServerBackupRestored), {
|
||||
"backupFileName": backupFileName,
|
||||
"serverName": actualServerName,
|
||||
"serverIp": serverIp,
|
||||
"isFromSetupWizard": true
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// For management menu, find tabBarStackView
|
||||
while (parentItem && parentItem.objectName !== "tabBarStackView") {
|
||||
parentItem = parentItem.parent
|
||||
}
|
||||
if (parentItem && typeof parentItem.push === "function") {
|
||||
parentItem.push(PageController.getPagePath(PageEnum.PageSettingsServerBackupRestored), {
|
||||
"backupFileName": backupFileName,
|
||||
"serverName": actualServerName,
|
||||
"serverIp": serverIp,
|
||||
"isFromSetupWizard": false
|
||||
})
|
||||
} else {
|
||||
console.warn("Could not find StackView to navigate to restored page")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,12 @@ import "../Controls2/TextTypes"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property var setupWizardEasy: null
|
||||
|
||||
property string savedHostname: ""
|
||||
property string savedUsername: ""
|
||||
property string savedSecretData: ""
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
@@ -130,6 +136,11 @@ PageType {
|
||||
return
|
||||
}
|
||||
|
||||
root.savedHostname = _hostname
|
||||
root.savedUsername = _username
|
||||
root.savedSecretData = _secretData
|
||||
console.log("Saved credentials in PageSetupWizardCredentials:", _hostname, _username)
|
||||
|
||||
PageController.goToPage(PageEnum.PageSetupWizardEasy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,216 @@ import "../Config"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
objectName: "pageSetupWizardEasy"
|
||||
|
||||
property bool isEasySetup: true
|
||||
property bool isRestoreFromBackup: false
|
||||
property string backupFilePath: ""
|
||||
property string restoreHostname: ""
|
||||
property string restoreUsername: ""
|
||||
property string restoreSecretData: ""
|
||||
property bool waitingForServerToAdd: false
|
||||
|
||||
// For installing containers from backup
|
||||
property var containersToInstall: []
|
||||
property int currentContainerIndex: 0
|
||||
property bool isInstallingContainers: false
|
||||
|
||||
// Connections for ServersBackupController
|
||||
Connections {
|
||||
target: ServersBackupController
|
||||
|
||||
function onReadyForRestore(backupFilePath, hostname, username, secretData, serverIp, fileName) {
|
||||
console.log("onReadyForRestore received from C++")
|
||||
console.log(" backupFilePath:", backupFilePath)
|
||||
console.log(" hostname:", hostname)
|
||||
console.log(" serverIp:", serverIp)
|
||||
console.log(" fileName:", fileName)
|
||||
|
||||
// Scan backup to determine containers (C++ already did this, but needed for QML)
|
||||
var foundContainers = ServersBackupController.scanBackupForContainers(backupFilePath)
|
||||
console.log("Found containers:", foundContainers)
|
||||
|
||||
if (foundContainers.length === 0) {
|
||||
PageController.showErrorMessage(qsTr("No containers found in backup file"))
|
||||
root.isRestoreFromBackup = false
|
||||
return
|
||||
}
|
||||
|
||||
root.containersToInstall = foundContainers
|
||||
root.currentContainerIndex = 0
|
||||
|
||||
// Now add empty server with these credentials
|
||||
InstallController.setProcessedServerCredentials(hostname, username, secretData)
|
||||
|
||||
// Set waiting flag
|
||||
root.waitingForServerToAdd = true
|
||||
|
||||
console.log("Backup scanned, adding server...")
|
||||
// Add server (asynchronously)
|
||||
InstallController.addEmptyServer()
|
||||
|
||||
// Further execution will happen in onInstallServerFinished
|
||||
}
|
||||
}
|
||||
|
||||
// Connections for tracking server addition
|
||||
Connections {
|
||||
target: InstallController
|
||||
|
||||
function onInstallServerFinished(finishedMessage) {
|
||||
if (root.waitingForServerToAdd && root.isRestoreFromBackup && root.backupFilePath.length > 0) {
|
||||
console.log("Server added successfully, now installing containers from backup...")
|
||||
root.waitingForServerToAdd = false
|
||||
|
||||
// Start installing containers
|
||||
root.isInstallingContainers = true
|
||||
installNextContainer()
|
||||
}
|
||||
}
|
||||
|
||||
function onInstallContainerFinished(finishedMessage, isServiceInstall) {
|
||||
if (root.isInstallingContainers) {
|
||||
console.log("Container installed:", finishedMessage)
|
||||
|
||||
// Move to next container
|
||||
root.currentContainerIndex++
|
||||
|
||||
if (root.currentContainerIndex < root.containersToInstall.length) {
|
||||
// Install next container
|
||||
installNextContainer()
|
||||
} else {
|
||||
// All containers installed, now do restore
|
||||
console.log("All containers installed, starting restore...")
|
||||
root.isInstallingContainers = false
|
||||
|
||||
// IMPORTANT: Turn off busy indicator before navigation
|
||||
PageController.showBusyIndicator(false)
|
||||
|
||||
// Start navigation to restore mode selection page
|
||||
navigationTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to install next container from list
|
||||
function installNextContainer() {
|
||||
if (root.currentContainerIndex >= root.containersToInstall.length) {
|
||||
return
|
||||
}
|
||||
|
||||
var containerName = root.containersToInstall[root.currentContainerIndex]
|
||||
console.log("Installing container:", containerName, "(", root.currentContainerIndex + 1, "/", root.containersToInstall.length, ")")
|
||||
|
||||
// Convert container name to DockerContainer enum
|
||||
var dockerContainer = ContainerProps.containerFromString(containerName)
|
||||
|
||||
if (dockerContainer === 0) { // None
|
||||
console.log("Unknown container:", containerName, "skipping...")
|
||||
root.currentContainerIndex++
|
||||
installNextContainer()
|
||||
return
|
||||
}
|
||||
|
||||
// Get default settings for container
|
||||
var defaultProtocol = ContainerProps.defaultProtocol(dockerContainer)
|
||||
var defaultPort = InstallController.getPortForInstall(defaultProtocol)
|
||||
var defaultTransport = InstallController.defaultTransportProto(defaultProtocol)
|
||||
|
||||
// Set server index
|
||||
var serverIdx = ServersModel.getServersCount() - 1
|
||||
ServersModel.processedIndex = serverIdx
|
||||
var serverId = ServersUiController.getServerId(serverIdx)
|
||||
|
||||
// Show loading indicator with message
|
||||
PageController.showBusyIndicator(true)
|
||||
PageController.showNotificationMessage(qsTr("Installing %1 (%2/%3)...")
|
||||
.arg(containerName)
|
||||
.arg(root.currentContainerIndex + 1)
|
||||
.arg(root.containersToInstall.length))
|
||||
|
||||
// Ensure credentials are set
|
||||
InstallController.setProcessedServerCredentials(root.restoreHostname, root.restoreUsername, root.restoreSecretData)
|
||||
|
||||
// Install container
|
||||
console.log("Installing container:", containerName, "serverId:", serverId)
|
||||
ContainersModel.setProcessedContainerIndex(dockerContainer)
|
||||
InstallController.install(dockerContainer, defaultPort, defaultTransport, serverId)
|
||||
}
|
||||
|
||||
// Timer for navigating to restore mode selection page after file selection
|
||||
Timer {
|
||||
id: navigationTimer
|
||||
interval: 500
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (root.backupFilePath.length > 0 && root.isRestoreFromBackup) {
|
||||
console.log("Navigation timer triggered, going to restore mode page")
|
||||
console.log("Credentials available:", root.restoreHostname, root.restoreUsername, root.restoreSecretData.length > 0 ? "***" : "EMPTY")
|
||||
|
||||
// Get filename
|
||||
var fileName = SystemController.getFileNameFromPath(root.backupFilePath)
|
||||
if (!fileName || fileName === undefined || fileName.length === 0) {
|
||||
var fallbackName = root.backupFilePath.split('/').pop()
|
||||
fileName = (fallbackName && fallbackName.length > 0) ? fallbackName : qsTr("backup.tgz")
|
||||
}
|
||||
fileName = String(fileName)
|
||||
|
||||
// Extract IP address from filename
|
||||
var serverIp = ""
|
||||
var ipMatch = fileName.match(/^([\d_]+)\s*-/)
|
||||
if (ipMatch && ipMatch.length > 1) {
|
||||
serverIp = ipMatch[1].replace(/_/g, ".")
|
||||
}
|
||||
if (!serverIp || serverIp.length === 0) {
|
||||
serverIp = root.restoreHostname
|
||||
}
|
||||
|
||||
var serverName = root.restoreHostname
|
||||
if (!serverName || serverName.length === 0) {
|
||||
serverName = qsTr("RestoredServer")
|
||||
}
|
||||
|
||||
// Navigate to installation page
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
|
||||
// Immediately find StackView and navigate to restore page
|
||||
// Server already added, as we waited for onInstallServerFinished
|
||||
Qt.callLater(function() {
|
||||
var pagePath = "qrc:/ui/qml/Pages2/PageSettingsServerRestoreMode.qml"
|
||||
|
||||
// Traverse upward from root to find the containing StackView.
|
||||
// StackView has both `push` function and `depth` property.
|
||||
// This avoids a recursive downward search that causes stack overflow
|
||||
// on iOS when the component tree is large (many VPN managers).
|
||||
var stackView = root.parent
|
||||
while (stackView) {
|
||||
if (typeof stackView.push === "function" && stackView.hasOwnProperty("depth")) {
|
||||
break
|
||||
}
|
||||
stackView = stackView.parent
|
||||
}
|
||||
|
||||
if (stackView) {
|
||||
console.log("Found StackView, pushing restore mode page")
|
||||
stackView.push(pagePath, {
|
||||
"backupFilePath": root.backupFilePath,
|
||||
"backupFileName": fileName,
|
||||
"serverName": "",
|
||||
"serverIp": serverIp,
|
||||
"isFromSetupWizard": true,
|
||||
"wizardHostname": root.restoreHostname,
|
||||
"wizardUsername": root.restoreUsername,
|
||||
"wizardSecretData": root.restoreSecretData
|
||||
})
|
||||
} else {
|
||||
console.error("Could not find StackView")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SortFilterProxyModel {
|
||||
id: proxyContainersModel
|
||||
@@ -149,6 +357,83 @@ PageType {
|
||||
Keys.onReturnPressed: this.clicked()
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
}
|
||||
|
||||
CardType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
headerText: qsTr("Restore from backup")
|
||||
bodyText: qsTr("Restoration of VPN protocols, services, all server settings and users")
|
||||
|
||||
ButtonGroup.group: buttonGroup
|
||||
|
||||
onClicked: function() {
|
||||
|
||||
var filter = GC.isMobile() ? "*.gz *.tgz *.tar.gz" : "Backup files (*.tar.gz *.backup *.tgz *.gz)"
|
||||
var localPath = SystemController.getFileName(
|
||||
qsTr("Select Backup to Restore"),
|
||||
filter,
|
||||
"",
|
||||
false,
|
||||
""
|
||||
)
|
||||
|
||||
console.log("Selected file path:", localPath)
|
||||
|
||||
if (!localPath || localPath.length === 0) {
|
||||
console.log("No file selected")
|
||||
return
|
||||
}
|
||||
|
||||
// Save backup file path
|
||||
root.backupFilePath = localPath
|
||||
root.isRestoreFromBackup = true
|
||||
|
||||
// Get credentials from PageSetupWizardCredentials via StackView search
|
||||
var credentialsPage = null
|
||||
var item = root
|
||||
|
||||
// Find StackView
|
||||
while (item && !item.hasOwnProperty("depth")) {
|
||||
item = item.parent
|
||||
}
|
||||
|
||||
// If found StackView, search for PageSetupWizardCredentials in its history
|
||||
if (item && item.depth > 0) {
|
||||
for (var i = 0; i < item.depth; i++) {
|
||||
var page = item.get(i)
|
||||
if (page && page.hasOwnProperty("savedHostname")) {
|
||||
credentialsPage = page
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (credentialsPage && credentialsPage.savedHostname.length > 0) {
|
||||
root.restoreHostname = credentialsPage.savedHostname
|
||||
root.restoreUsername = credentialsPage.savedUsername
|
||||
root.restoreSecretData = credentialsPage.savedSecretData
|
||||
console.log("Got credentials from PageSetupWizardCredentials:", root.restoreHostname, root.restoreUsername)
|
||||
|
||||
// Call C++ method to prepare restore
|
||||
// It will scan backup and send readyForRestore signal
|
||||
ServersBackupController.prepareRestoreFromBackup(localPath, root.restoreHostname, root.restoreUsername, root.restoreSecretData)
|
||||
} else {
|
||||
console.log("WARNING: No credentials found")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: this.clicked()
|
||||
Keys.onReturnPressed: this.clicked()
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: continueButton
|
||||
|
||||
|
||||
@@ -106,8 +106,11 @@
|
||||
<file>Pages2/PageSettingsKillSwitch.qml</file>
|
||||
<file>Pages2/PageSettingsKillSwitchExceptions.qml</file>
|
||||
<file>Pages2/PageSettingsLogging.qml</file>
|
||||
<file>Pages2/PageSettingsServerBackup.qml</file>
|
||||
<file>Pages2/PageSettingsServerBackupRestored.qml</file>
|
||||
<file>Pages2/PageSettingsServerData.qml</file>
|
||||
<file>Pages2/PageSettingsServerInfo.qml</file>
|
||||
<file>Pages2/PageSettingsServerRestoreMode.qml</file>
|
||||
<file>Pages2/PageSettingsServerProtocol.qml</file>
|
||||
<file>Pages2/PageSettingsServerProtocols.qml</file>
|
||||
<file>Pages2/PageSettingsServerServices.qml</file>
|
||||
|
||||
Reference in New Issue
Block a user