diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index a96aefa66..a1f17fe06 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -88,33 +88,68 @@ open class Wireguard : Protocol() { addDnsServer(parseInetAddress(dns.trim())) } - val defRoutes = hashSetOf( - InetNetwork("0.0.0.0", 0), - InetNetwork("::", 0) - ) - val routes = hashSetOf() - configData.getJSONArray("allowed_ips").asSequence().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()) } - - val host = configData.getString("hostName").let { parseInetAddress(it.trim()) } - val port = configData.getInt("port") - setEndpoint(InetEndpoint(host, port)) + configData.getString("client_priv_key").let { setPrivateKeyHex(it.base64ToHex()) } if (configData.optBoolean("isObfuscationEnabled")) { setUseProtocolExtension(true) configExtensionParameters(configData) } - 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()) } + 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() + for (i in 0 until peersArray.length()) { + peersArray.getJSONObject(i).getJSONArray("allowed_ips").asSequence() + .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() + .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() + .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() + configData.getJSONArray("allowed_ips").asSequence().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()) } + } } protected fun WireguardConfig.Builder.configExtensionParameters(configData: JSONObject) { @@ -201,7 +236,11 @@ open class Wireguard : Protocol() { Log.e(TAG, "Failed to get tunnel config") return -2 } - val lastHandshake = config.lines().find { it.startsWith("last_handshake_time_sec=") }?.substring(24)?.toLong() + // 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() if (lastHandshake == null) { Log.e(TAG, "Failed to get last_handshake_time_sec") return -2 diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt index 72871b002..6be4ed27b 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt @@ -4,9 +4,18 @@ 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 +) + open class WireguardConfig protected constructor( protocolConfigBuilder: ProtocolConfig.Builder, val endpoint: InetEndpoint, @@ -31,6 +40,8 @@ open class WireguardConfig protected constructor( var i3: String?, var i4: String?, var i5: String?, + val peerAllowedIps: List?, + val additionalPeers: List, ) : ProtocolConfig(protocolConfigBuilder) { protected constructor(builder: Builder) : this( @@ -57,6 +68,8 @@ open class WireguardConfig protected constructor( builder.i3, builder.i4, builder.i5, + builder.peerAllowedIps, + builder.additionalPeers.toList(), ) fun toWgUserspaceString(): String = with(StringBuilder()) { @@ -103,14 +116,22 @@ open class WireguardConfig protected constructor( open fun appendPeerLine(sb: StringBuilder) = with(sb) { appendLine("public_key=$publicKeyHex") - routes.filter { it.include }.forEach { route -> - appendLine("allowed_ip=${route.inetNetwork}") - } + val primaryIps = peerAllowedIps ?: routes.filter { it.include }.map { it.inetNetwork } + primaryIps.forEach { net -> appendLine("allowed_ip=$net") } 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) { @@ -150,6 +171,9 @@ open class WireguardConfig protected constructor( internal var i4: String? = null internal var i5: String? = null + internal var peerAllowedIps: List? = null + internal val additionalPeers: MutableList = mutableListOf() + fun setEndpoint(endpoint: InetEndpoint) = apply { this.endpoint = endpoint } fun setPersistentKeepalive(persistentKeepalive: Int) = apply { this.persistentKeepalive = persistentKeepalive } @@ -179,6 +203,9 @@ 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) = apply { this.peerAllowedIps = ips } + fun addPeer(peer: PeerConfig) = apply { this.additionalPeers += peer } + override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) } } diff --git a/client/daemon/daemon.cpp b/client/daemon/daemon.cpp index e74a613f5..c77c90b74 100644 --- a/client/daemon/daemon.cpp +++ b/client/daemon/daemon.cpp @@ -441,6 +441,37 @@ 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; } diff --git a/client/daemon/interfaceconfig.h b/client/daemon/interfaceconfig.h index 71f326355..fe9ac17c6 100644 --- a/client/daemon/interfaceconfig.h +++ b/client/daemon/interfaceconfig.h @@ -37,6 +37,9 @@ class InterfaceConfig { int m_serverPort = 0; int m_deviceMTU = 1420; QList 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 m_primaryPeerAllowedIPRanges; QStringList m_excludedAddresses; QStringList m_vpnDisabledApps; QStringList m_allowedDnsServers; @@ -58,6 +61,15 @@ class InterfaceConfig { QString m_transportPacketMagicHeader; QMap m_specialJunk; + struct AdditionalPeerConfig { + QString m_serverPublicKey; + QString m_serverPskKey; + QString m_serverIpv4AddrIn; + int m_serverPort = 0; + QList m_allowedIPAddressRanges; + }; + QList m_additionalPeers; + QJsonObject toJson() const; QString toWgConf( const QMap& extra = QMap()) const; diff --git a/client/mozilla/localsocketcontroller.cpp b/client/mozilla/localsocketcontroller.cpp index 17bf5a5e9..30791dbbc 100644 --- a/client/mozilla/localsocketcontroller.cpp +++ b/client/mozilla/localsocketcontroller.cpp @@ -166,68 +166,96 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) { QJsonArray jsAllowedIPAddesses; - QJsonArray plainAllowedIP = wgConfig.value(amnezia::config_key::allowed_ips).toArray(); - QJsonArray defaultAllowedIP = { "0.0.0.0/0", "::/0" }; + 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; + }; - 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); + 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 seenIps; + for (const QJsonValue& peerVal : std::as_const(peersArray)) { + for (const QJsonValue& ipVal : peerVal.toObject().value(amnezia::config_key::allowed_ips).toArray()) { + const QString ipRange = ipVal.toString().trimmed(); + if (seenIps.contains(ipRange)) continue; + seenIps.insert(ipRange); + jsAllowedIPAddesses.append(ipRangeToJson(ipRange)); } } + + // 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::config_key::allowed_ips).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::config_key::server_pub_key)); + additionalPeer.insert("serverPskKey", peerObj.value(amnezia::config_key::psk_key)); + additionalPeer.insert("serverIpv4AddrIn", peerObj.value(amnezia::config_key::hostName)); + additionalPeer.insert("serverPort", peerObj.value(amnezia::config_key::port).toInt()); + QJsonArray additionalPeerIps; + for (const QJsonValue& ipVal : peerObj.value(amnezia::config_key::allowed_ips).toArray()) { + additionalPeerIps.append(ipRangeToJson(ipVal.toString().trimmed())); + } + additionalPeer.insert("allowedIPAddressRanges", additionalPeerIps); + additionalPeersJson.append(additionalPeer); + } + json.insert("additionalPeers", additionalPeersJson); } else { + QJsonArray plainAllowedIP = wgConfig.value(amnezia::config_key::allowed_ips).toArray(); + QJsonArray defaultAllowedIP = { "0.0.0.0/0", "::/0" }; - // Use APP split tunnel + 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 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) { - 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); - } - } + for (auto v : splitTunnelSites) { + jsAllowedIPAddesses.append(ipRangeToJson(v.toString().trimmed())); + } } + } } json.insert("allowedIPAddressRanges", jsAllowedIPAddesses); QJsonArray jsExcludedAddresses; - jsExcludedAddresses.append(wgConfig.value(amnezia::config_key::hostName)); + if (isMultiPeer) { + for (const QJsonValue& peerVal : std::as_const(peersArray)) { + jsExcludedAddresses.append(peerVal.toObject().value(amnezia::config_key::hostName)); + } + } else { + jsExcludedAddresses.append(wgConfig.value(amnezia::config_key::hostName)); + } if (splitTunnelType == 2) { for (auto v : splitTunnelSites) { QString ipRange = v.toString(); diff --git a/client/platforms/ios/PacketTunnelProvider+WireGuard.swift b/client/platforms/ios/PacketTunnelProvider+WireGuard.swift index 30449b219..9bab50ab7 100644 --- a/client/platforms/ios/PacketTunnelProvider+WireGuard.swift +++ b/client/platforms/ios/PacketTunnelProvider+WireGuard.swift @@ -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 { diff --git a/client/platforms/ios/WGConfig.swift b/client/platforms/ios/WGConfig.swift index 0a3940e1f..cf0c742f7 100644 --- a/client/platforms/ios/WGConfig.swift +++ b/client/platforms/ios/WGConfig.swift @@ -1,5 +1,23 @@ 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? @@ -19,6 +37,7 @@ struct WGConfig: Decodable { var persistentKeepAlive: String let splitTunnelType: Int let splitTunnelSites: [String] + let peers: [WGPeerConfig]? enum CodingKeys: String, CodingKey { case initPacketMagicHeader = "H1", responsePacketMagicHeader = "H2" @@ -39,6 +58,7 @@ struct WGConfig: Decodable { case persistentKeepAlive = "persistent_keep_alive" case splitTunnelType case splitTunnelSites + case peers } var settings: String { @@ -103,7 +123,7 @@ struct WGConfig: Decodable { return settingsLines.joined(separator: "\n") } - var str: String { + private var interfaceSection: String { """ [Interface] Address = \(clientIP) @@ -111,9 +131,30 @@ 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 == nil ? "" : "PresharedKey = \(presharedKey!)") + \((presharedKey?.isEmpty ?? true) ? "" : "PresharedKey = \(presharedKey!)") AllowedIPs = \(allowedIPs.joined(separator: ", ")) Endpoint = \(hostName):\(port) PersistentKeepalive = \(persistentKeepAlive) @@ -121,19 +162,21 @@ 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) - [Peer] - PublicKey = *** - PresharedKey = *** - AllowedIPs = \(allowedIPs.joined(separator: ", ")) - Endpoint = \(hostName):\(port) - PersistentKeepalive = \(persistentKeepAlive) + PeerCount = \(peerCount) + \(peerInfo) SplitTunnelType = \(splitTunnelType) SplitTunnelSites = \(splitTunnelSites.joined(separator: ", ")) diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index fc9498d02..5ec84e640 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -652,6 +652,10 @@ bool IosController::setupWireGuard() wgConfig.insert(config_key::persistent_keep_alive, "25"); } + if (config.contains("peers") && config["peers"].isArray()) { + wgConfig.insert("peers", config["peers"]); + } + if (config.contains(config_key::isObfuscationEnabled) && config.value(config_key::isObfuscationEnabled).toBool()) { wgConfig.insert(config_key::initPacketMagicHeader, config[config_key::initPacketMagicHeader]); wgConfig.insert(config_key::responsePacketMagicHeader, config[config_key::responsePacketMagicHeader]); @@ -735,7 +739,29 @@ bool IosController::setupAwg() wgConfig.insert(config_key::hostName, config[config_key::hostName]); wgConfig.insert(config_key::port, config[config_key::port]); - wgConfig.insert(config_key::client_ip, config[config_key::client_ip]); + + bool isMultiPeer = config.contains("peers") && config["peers"].isArray() + && !config["peers"].toArray().isEmpty(); + + if (isMultiPeer) { + // Use only the first client IP (peer 1's IP) + QString fullClientIp = config[config_key::client_ip].toString(); + QStringList ipList = fullClientIp.split(","); + QString firstClientIp = ipList.isEmpty() ? fullClientIp : ipList.first().trimmed(); + wgConfig.insert(config_key::client_ip, firstClientIp); + // Route all traffic through peer 1 + QJsonArray allowed_ips { "0.0.0.0/0", "::/0" }; + wgConfig.insert(config_key::allowed_ips, allowed_ips); + } else { + wgConfig.insert(config_key::client_ip, config[config_key::client_ip]); + if (config.contains(config_key::allowed_ips) && config[config_key::allowed_ips].isArray()) { + wgConfig.insert(config_key::allowed_ips, config[config_key::allowed_ips]); + } else { + QJsonArray allowed_ips { "0.0.0.0/0", "::/0" }; + wgConfig.insert(config_key::allowed_ips, allowed_ips); + } + } + wgConfig.insert(config_key::client_priv_key, config[config_key::client_priv_key]); wgConfig.insert(config_key::server_pub_key, config[config_key::server_pub_key]); wgConfig.insert(config_key::psk_key, config[config_key::psk_key]); @@ -749,13 +775,6 @@ bool IosController::setupAwg() wgConfig.insert(config_key::splitTunnelSites, splitTunnelSites); - if (config.contains(config_key::allowed_ips) && config[config_key::allowed_ips].isArray()) { - wgConfig.insert(config_key::allowed_ips, config[config_key::allowed_ips]); - } else { - QJsonArray allowed_ips { "0.0.0.0/0", "::/0" }; - wgConfig.insert(config_key::allowed_ips, allowed_ips); - } - if (config.contains(config_key::persistent_keep_alive)) { wgConfig.insert(config_key::persistent_keep_alive, config[config_key::persistent_keep_alive]); } else { diff --git a/client/platforms/linux/daemon/iputilslinux.cpp b/client/platforms/linux/daemon/iputilslinux.cpp index 25d4f631e..0e70f4e30 100644 --- a/client/platforms/linux/daemon/iputilslinux.cpp +++ b/client/platforms/linux/daemon/iputilslinux.cpp @@ -5,8 +5,12 @@ #include "iputilslinux.h" #include +#include +#include +#include #include #include +#include #include #include @@ -71,39 +75,104 @@ bool IPUtilsLinux::setMTUAndUp(const InterfaceConfig& config) { return true; } -bool IPUtilsLinux::addIP4AddressToDevice(const InterfaceConfig& config) { - struct ifreq ifr; - struct sockaddr_in* ifrAddr = (struct sockaddr_in*)&ifr.ifr_addr; +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); }); - // Name the interface and set family - strncpy(ifr.ifr_name, WG_INTERFACE, IFNAMSIZ); - ifr.ifr_addr.sa_family = AF_INET; + char buf[512]; + memset(buf, 0, sizeof(buf)); - // Get the device address to add to interface - QPair 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 nlmsghdr* nlmsg = reinterpret_cast(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; - // 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."; + struct ifaddrmsg* ifa = static_cast(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(nlmsg) + NLMSG_ALIGN(nlmsg->nlmsg_len); + struct rtattr* rta = reinterpret_cast(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(&nladdr), + sizeof(nladdr)) < 0) { return false; } - auto guard = qScopeGuard([&] { close(sockfd); }); - // Set ifr to interface - int ret = ioctl(sockfd, SIOCSIFADDR, &ifr); - if (ret) { - logger.error() << "Failed to set IPv4: " << deviceAddr - << "error:" << strerror(errno); - return false; + char ackbuf[1024]; + ssize_t acklen = recv(nlsock, ackbuf, sizeof(ackbuf), 0); + if (acklen >= static_cast(sizeof(struct nlmsghdr))) { + struct nlmsghdr* ackmsg = reinterpret_cast(ackbuf); + if (ackmsg->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr* err = static_cast(NLMSG_DATA(ackmsg)); + if (err->error != 0) { + errno = -err->error; + 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 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; diff --git a/client/platforms/linux/daemon/wireguardutilslinux.cpp b/client/platforms/linux/daemon/wireguardutilslinux.cpp index 1b7cddc8e..9d7dc8008 100644 --- a/client/platforms/linux/daemon/wireguardutilslinux.cpp +++ b/client/platforms/linux/daemon/wireguardutilslinux.cpp @@ -230,7 +230,10 @@ bool WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) { out << "replace_allowed_ips=true\n"; out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n"; - for (const IPAddress& ip : config.m_allowedIPAddressRanges) { + const QList& primaryIPs = config.m_primaryPeerAllowedIPRanges.isEmpty() + ? config.m_allowedIPAddressRanges + : config.m_primaryPeerAllowedIPRanges; + for (const IPAddress& ip : primaryIPs) { out << "allowed_ip=" << ip.toString() << "\n"; } @@ -244,8 +247,38 @@ bool WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) { int err = uapiErrno(uapiCommand(message)); if (err != 0) { logger.error() << "Peer configuration failed:" << strerror(err); + return false; } - return (err == 0); + + 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; } bool WireguardUtilsLinux::deletePeer(const InterfaceConfig& config) { diff --git a/client/platforms/macos/daemon/iputilsmacos.cpp b/client/platforms/macos/daemon/iputilsmacos.cpp index 901436ae7..509823e70 100644 --- a/client/platforms/macos/daemon/iputilsmacos.cpp +++ b/client/platforms/macos/daemon/iputilsmacos.cpp @@ -80,7 +80,9 @@ bool IPUtilsMacos::setMTUAndUp(const InterfaceConfig& config) { } bool IPUtilsMacos::addIP4AddressToDevice(const InterfaceConfig& config) { - Q_UNUSED(config); + if (config.m_deviceIpv4Address.isEmpty()) { + return true; + } QString ifname = MacOSDaemon::instance()->m_wgutils->interfaceName(); struct ifaliasreq ifr; struct sockaddr_in* ifrAddr = (struct sockaddr_in*)&ifr.ifra_addr; @@ -91,25 +93,28 @@ bool IPUtilsMacos::addIP4AddressToDevice(const InterfaceConfig& config) { memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifra_name, qPrintable(ifname), IFNAMSIZ); - // Get the device address to add to interface - QPair parsedAddr = - QHostAddress::parseSubnet(config.m_deviceIpv4Address); - QByteArray _deviceAddr = parsedAddr.first.toString().toLocal8Bit(); + // 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(); char* deviceAddr = _deviceAddr.data(); ifrAddr->sin_family = AF_INET; ifrAddr->sin_len = sizeof(struct sockaddr_in); - inet_pton(AF_INET, deviceAddr, &ifrAddr->sin_addr); + if (inet_pton(AF_INET, deviceAddr, &ifrAddr->sin_addr) != 1) { + logger.error() << "Failed to parse IPv4 address:" << deviceAddr; + return false; + } // 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)); - // Set the broadcast address. + // 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). ifrBcast->sin_family = AF_INET; ifrBcast->sin_len = sizeof(struct sockaddr_in); - ifrBcast->sin_addr.s_addr = - (ifrAddr->sin_addr.s_addr | ~ifrMask->sin_addr.s_addr); + ifrBcast->sin_addr.s_addr = ifrAddr->sin_addr.s_addr; // Create an IPv4 socket to perform the ioctl operations on int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); diff --git a/client/platforms/macos/daemon/wireguardutilsmacos.cpp b/client/platforms/macos/daemon/wireguardutilsmacos.cpp index 55a5526be..a9b063252 100644 --- a/client/platforms/macos/daemon/wireguardutilsmacos.cpp +++ b/client/platforms/macos/daemon/wireguardutilsmacos.cpp @@ -230,7 +230,11 @@ bool WireguardUtilsMacos::updatePeer(const InterfaceConfig& config) { out << "replace_allowed_ips=true\n"; out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n"; - for (const IPAddress& ip : config.m_allowedIPAddressRanges) { + // For multi-peer use only the primary peer's own IPs to avoid routing trie conflicts. + const QList& primaryIPs = config.m_primaryPeerAllowedIPRanges.isEmpty() + ? config.m_allowedIPAddressRanges + : config.m_primaryPeerAllowedIPRanges; + for (const IPAddress& ip : primaryIPs) { out << "allowed_ip=" << ip.toString() << "\n"; } @@ -244,8 +248,38 @@ bool WireguardUtilsMacos::updatePeer(const InterfaceConfig& config) { int err = uapiErrno(uapiCommand(message)); if (err != 0) { logger.error() << "Peer configuration failed:" << strerror(err); + return false; } - return (err == 0); + + 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; } bool WireguardUtilsMacos::deletePeer(const InterfaceConfig& config) { diff --git a/client/platforms/windows/daemon/wireguardutilswindows.cpp b/client/platforms/windows/daemon/wireguardutilswindows.cpp index a5c9c84d1..79a0aeb08 100644 --- a/client/platforms/windows/daemon/wireguardutilswindows.cpp +++ b/client/platforms/windows/daemon/wireguardutilswindows.cpp @@ -181,7 +181,10 @@ bool WireguardUtilsWindows::updatePeer(const InterfaceConfig& config) { out << "replace_allowed_ips=true\n"; out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n"; - for (const IPAddress& ip : config.m_allowedIPAddressRanges) { + const QList& primaryIPs = config.m_primaryPeerAllowedIPRanges.isEmpty() + ? config.m_allowedIPAddressRanges + : config.m_primaryPeerAllowedIPRanges; + for (const IPAddress& ip : primaryIPs) { out << "allowed_ip=" << ip.toString() << "\n"; } @@ -193,6 +196,33 @@ 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; } diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 91d7ec3b5..5d695119a 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -383,24 +383,46 @@ QJsonObject ImportController::extractOpenVpnConfig(const QString &data) QJsonObject ImportController::extractWireGuardConfig(const QString &data) { - QMap configMap; - auto configByLines = data.split("\n"); + QMap interfaceMap; + QList> peerList; + + enum class WgSection { None, Interface, Peer }; + WgSection currentSection = WgSection::None; + + const auto configByLines = data.split("\n"); for (const QString &line : configByLines) { - QString trimmedLine = line.trimmed(); - if (trimmedLine.startsWith("[") && trimmedLine.endsWith("]")) { - continue; - } else { - QStringList parts = trimmedLine.split(" = "); + const QString trimmedLine = line.trimmed(); + if (trimmedLine == "[Interface]") { + currentSection = WgSection::Interface; + } else if (trimmedLine == "[Peer]") { + currentSection = WgSection::Peer; + peerList.append(QMap()); + } else if (!trimmedLine.isEmpty() && !trimmedLine.startsWith("#")) { + const QStringList parts = trimmedLine.split(" = "); if (parts.count() == 2) { - configMap[parts.at(0).trimmed()] = parts.at(1).trimmed(); + 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; + } } } } + if (peerList.isEmpty()) { + qDebug() << "No [Peer] section found in WireGuard config"; + emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false); + return QJsonObject(); + } + + const QMap &firstPeerMap = peerList.first(); + QJsonObject lastConfig; lastConfig[config_key::config] = data; - auto url { QUrl::fromUserInput(configMap.value("Endpoint")) }; + auto url { QUrl::fromUserInput(firstPeerMap.value("Endpoint")) }; QString hostName; QString port; if (!url.host().isEmpty()) { @@ -420,35 +442,56 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data) lastConfig[config_key::hostName] = hostName; lastConfig[config_key::port] = port.toInt(); - if (!configMap.value("PrivateKey").isEmpty() && !configMap.value("Address").isEmpty() && !configMap.value("PublicKey").isEmpty()) { - lastConfig[config_key::client_priv_key] = configMap.value("PrivateKey"); - lastConfig[config_key::client_ip] = configMap.value("Address"); + if (!interfaceMap.value("PrivateKey").isEmpty() && !interfaceMap.value("Address").isEmpty() && !firstPeerMap.value("PublicKey").isEmpty()) { + lastConfig[config_key::client_priv_key] = interfaceMap.value("PrivateKey"); + lastConfig[config_key::client_ip] = interfaceMap.value("Address"); - if (!configMap.value("PresharedKey").isEmpty()) { - lastConfig[config_key::psk_key] = configMap.value("PresharedKey"); - } else if (!configMap.value("PreSharedKey").isEmpty()) { - lastConfig[config_key::psk_key] = configMap.value("PreSharedKey"); + if (!firstPeerMap.value("PresharedKey").isEmpty()) { + lastConfig[config_key::psk_key] = firstPeerMap.value("PresharedKey"); + } else if (!firstPeerMap.value("PreSharedKey").isEmpty()) { + lastConfig[config_key::psk_key] = firstPeerMap.value("PreSharedKey"); } - lastConfig[config_key::server_pub_key] = configMap.value("PublicKey"); + lastConfig[config_key::server_pub_key] = firstPeerMap.value("PublicKey"); } else { qDebug() << "One of the key parameters is missing (PrivateKey, Address, PublicKey)"; emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false); return QJsonObject(); } - if (!configMap.value("MTU").isEmpty()) { - lastConfig[config_key::mtu] = configMap.value("MTU"); + if (!interfaceMap.value("MTU").isEmpty()) { + lastConfig[config_key::mtu] = interfaceMap.value("MTU"); } - if (!configMap.value("PersistentKeepalive").isEmpty()) { - lastConfig[config_key::persistent_keep_alive] = configMap.value("PersistentKeepalive"); + if (!firstPeerMap.value("PersistentKeepalive").isEmpty()) { + lastConfig[config_key::persistent_keep_alive] = firstPeerMap.value("PersistentKeepalive"); } - QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(configMap.value("AllowedIPs").split(", ")); - + QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(firstPeerMap.value("AllowedIPs").split(", ")); lastConfig[config_key::allowed_ips] = allowedIpsJsonArray; + if (peerList.size() > 1) { + QJsonArray peersArray; + for (const auto &peerMap : std::as_const(peerList)) { + QJsonObject peerObj; + const auto peerUrl = QUrl::fromUserInput(peerMap.value("Endpoint")); + peerObj[config_key::server_pub_key] = peerMap.value("PublicKey"); + if (!peerMap.value("PresharedKey").isEmpty()) { + peerObj[config_key::psk_key] = peerMap.value("PresharedKey"); + } else if (!peerMap.value("PreSharedKey").isEmpty()) { + peerObj[config_key::psk_key] = peerMap.value("PreSharedKey"); + } + peerObj[config_key::hostName] = peerUrl.host(); + peerObj[config_key::port] = peerUrl.port() != -1 ? peerUrl.port() : QString(protocols::wireguard::defaultPort).toInt(); + peerObj[config_key::allowed_ips] = QJsonArray::fromStringList(peerMap.value("AllowedIPs").split(", ")); + if (!peerMap.value("PersistentKeepalive").isEmpty()) { + peerObj[config_key::persistent_keep_alive] = peerMap.value("PersistentKeepalive"); + } + peersArray.append(peerObj); + } + lastConfig["peers"] = peersArray; + } + QString protocolName = "wireguard"; QString protocolVersion; @@ -465,25 +508,25 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data) }; bool hasAllRequiredFields = std::all_of(requiredJunkFields.begin(), requiredJunkFields.end(), - [&configMap](const QString &field) { return !configMap.value(field).isEmpty(); }); + [&interfaceMap](const QString &field) { return !interfaceMap.value(field).isEmpty(); }); if (hasAllRequiredFields) { for (const QString &field : requiredJunkFields) { - lastConfig[field] = configMap.value(field); + lastConfig[field] = interfaceMap.value(field); } for (const QString &field : optionalJunkFields) { - if (!configMap.value(field).isEmpty()) { - lastConfig[field] = configMap.value(field); + if (!interfaceMap.value(field).isEmpty()) { + lastConfig[field] = interfaceMap.value(field); } } - bool hasCookieReplyPacketJunkSize = !configMap.value(config_key::cookieReplyPacketJunkSize).isEmpty(); - bool hasTransportPacketJunkSize = !configMap.value(config_key::transportPacketJunkSize).isEmpty(); - bool hasSpecialJunk = !configMap.value(config_key::specialJunk1).isEmpty() || - !configMap.value(config_key::specialJunk2).isEmpty() || - !configMap.value(config_key::specialJunk3).isEmpty() || - !configMap.value(config_key::specialJunk4).isEmpty() || - !configMap.value(config_key::specialJunk5).isEmpty(); + bool hasCookieReplyPacketJunkSize = !interfaceMap.value(config_key::cookieReplyPacketJunkSize).isEmpty(); + bool hasTransportPacketJunkSize = !interfaceMap.value(config_key::transportPacketJunkSize).isEmpty(); + bool hasSpecialJunk = !interfaceMap.value(config_key::specialJunk1).isEmpty() || + !interfaceMap.value(config_key::specialJunk2).isEmpty() || + !interfaceMap.value(config_key::specialJunk3).isEmpty() || + !interfaceMap.value(config_key::specialJunk4).isEmpty() || + !interfaceMap.value(config_key::specialJunk5).isEmpty(); if (hasCookieReplyPacketJunkSize && hasTransportPacketJunkSize) { protocolVersion = "2"; @@ -494,11 +537,11 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data) m_configType = ConfigTypes::Awg; } - if (!configMap.value("MTU").isEmpty()) { - lastConfig[config_key::mtu] = configMap.value("MTU"); + if (!interfaceMap.value("MTU").isEmpty()) { + lastConfig[config_key::mtu] = interfaceMap.value("MTU"); } else { - lastConfig[config_key::mtu] = (protocolName == "awg") - ? protocols::awg::defaultMtu + lastConfig[config_key::mtu] = (protocolName == "awg") + ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu; }