Compare commits

...

8 Commits

Author SHA1 Message Date
NickVs2015
f9131af4ad feat: multipeer support iOS/Android + Linux multi-IP + build fixes
- iOS AWG: forward peers array to WGConfig.swift for true two-peer support;
  insert empty allowedIPs to satisfy required WGConfig field in multi-peer path
- iOS AWG: fix config_key → configKey namespace throughout setupAwg()
- Linux daemon: replace ioctl single-address with netlink RTM_NEWADDR to
  support multiple comma-separated IPv4 addresses on WireGuard interface
- Add missing IosController include in subscriptionUiController.cpp
- Remove stale ui/controllers/importController.cpp (duplicate causing moc
  redefinition errors)
2026-05-30 13:25:33 +03:00
NickVs2015
423648a280 fix: add peers array 2026-05-30 13:25:33 +03:00
NickVs2015
a356b25fa5 feat: multipeer support mac/linux/windows 2026-05-30 13:25:33 +03:00
NickVs2015
299c3bab1d feat: multipeer support Android/IOS 2026-05-30 13:25:33 +03:00
yyy-amnezia
60686fde24 fix: link OpenVPNAdapter statically (#2645)
* fix(ios): link OpenVPNAdapter statically

* chore(conan): simplify openvpnadapter merge process and make everything via XCrun

---------

Co-authored-by: Yaroslav Gurov <ygurov@proton.me>
2026-05-30 13:59:35 +08:00
Yaroslav Gurov
bd0747296e fix: networkextension proper framework linking (#2668) 2026-05-28 23:09:49 +08:00
Yaroslav Gurov
ba61019a50 fix: enable bundled openssl for every platform except NE-based ones (#2660)
* fix: enable bundled openssl for every platform except NE-based ones

* fix(conan): trigger CI/CD on crutial cmake changes

* fix: install dylibs/dlls from conan and use proper RPATH

* fix: adjust windows runtime deps
2026-05-28 19:17:27 +08:00
vkamn
113f967006 fix: various fixes (#2664)
* fix: fixed nextAvailableServerName

* fix: fixed password request for ssh key
2026-05-28 15:14:04 +08:00
33 changed files with 750 additions and 230 deletions

View File

@@ -23,6 +23,9 @@ jobs:
- 'recipes/**'
- 'conanfile.py'
- '.github/workflows/deploy.yml'
- 'cmake/conan_provider.cmake'
- 'cmake/platform_settings.cmake'
- 'cmake/recipes_bootstrap.cmake'
Bake-Prebuilts-Linux:
runs-on: ubuntu-latest

View File

@@ -18,9 +18,9 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
HOMEPAGE_URL "https://amnezia.org/"
)
# trigger conan to kick off `conan install` globally
find_package(OpenSSL REQUIRED)
if (PREBUILTS_ONLY)
# trigger conan to kick off `conan install`
find_package(OpenSSL REQUIRED)
return()
endif()

View File

@@ -212,11 +212,32 @@ endif()
install(TARGETS ${PROJECT}
DESTINATION ${CMAKE_INSTALL_BINDIR}
RUNTIME_DEPENDENCY_SET client_deps
COMPONENT AmneziaVPN
)
install(FILES $<TARGET_RUNTIME_DLLS:${PROJECT}>
DESTINATION ${CMAKE_INSTALL_BINDIR}
if(APPLE)
set(RUNTIME_DEPS_DIR ${CMAKE_INSTALL_BINDIR}/AmneziaVPN.app/Contents/Frameworks)
else()
set(RUNTIME_DEPS_DIR ${CMAKE_INSTALL_BINDIR})
endif()
install(RUNTIME_DEPENDENCY_SET client_deps
PRE_EXCLUDE_REGEXES
[[api-ms-win-.*]]
[[ext-ms-.*]]
[[kernel32\.dll]]
[[hvsifiletrust\.dll]]
[[libc\.so\..*]] [[libgcc_s\.so\..*]] [[libm\.so\..*]] [[libstdc\+\+\.so\..*]]
[[.*\.framework]]
[[^[Qq]t.*]]
POST_EXCLUDE_REGEXES
[[^.*[\\/]system32[\\/].*\.dll$]]
[[^/lib.*]]
[[^/usr/lib.*]]
DIRECTORIES ${CONAN_RUNTIME_LIB_DIRS}
COMPONENT AmneziaVPN
DESTINATION "${RUNTIME_DEPS_DIR}"
)
set(deploy_tool_options "")

View File

@@ -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<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()) }
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<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()) }
}
}
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

View File

@@ -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<InetNetwork>
)
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<InetNetwork>?,
val additionalPeers: List<PeerConfig>,
) : 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<InetNetwork>? = null
internal val additionalPeers: MutableList<PeerConfig> = 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<InetNetwork>) = apply { this.peerAllowedIps = ips }
fun addPeer(peer: PeerConfig) = apply { this.additionalPeers += peer }
override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) }
}

View File

@@ -54,7 +54,6 @@ target_include_directories(${PROJECT} PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
set_target_properties(${PROJECT} PROPERTIES
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info.plist.in
MACOSX_BUNDLE_ICON_FILE "AppIcon"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"

View File

@@ -486,7 +486,7 @@ QJsonObject ImportController::extractOpenVpnConfig(const QString &data) const
QJsonObject config;
config[configKey::containers] = arr;
config[configKey::defaultContainer] = configKey::amneziaOpenvpn;
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
config[configKey::description] = m_serversRepository->nextAvailableServerName();
const static QRegularExpression dnsRegExp("dhcp-option DNS (\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b)");
QRegularExpressionMatchIterator dnsMatch = dnsRegExp.globalMatch(data);
@@ -504,24 +504,45 @@ QJsonObject ImportController::extractOpenVpnConfig(const QString &data) const
QJsonObject ImportController::extractWireGuardConfig(const QString &data, ConfigTypes &configType) const
{
QMap<QString, QString> configMap;
auto configByLines = data.split("\n");
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");
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<QString, QString>());
} 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";
return QJsonObject();
}
const QMap<QString, QString> &firstPeerMap = peerList.first();
QJsonObject lastConfig;
lastConfig[configKey::config] = data;
auto url { QUrl::fromUserInput(configMap.value(protocols::wireguard::Endpoint)) };
auto url { QUrl::fromUserInput(firstPeerMap.value(protocols::wireguard::Endpoint)) };
QString hostName;
QString port;
if (!url.host().isEmpty()) {
@@ -540,37 +561,55 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
lastConfig[configKey::hostName] = hostName;
lastConfig[configKey::port] = port.toInt();
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 (!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::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);
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);
}
lastConfig[configKey::serverPubKey] = configMap.value(protocols::wireguard::PublicKey);
lastConfig[configKey::serverPubKey] = firstPeerMap.value(protocols::wireguard::PublicKey);
} else {
qDebug() << "One of the key parameters is missing (PrivateKey, Address, PublicKey)";
return QJsonObject();
}
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);
if (!firstPeerMap.value(protocols::wireguard::PersistentKeepalive).isEmpty()) {
lastConfig[configKey::persistentKeepAlive] = firstPeerMap.value(protocols::wireguard::PersistentKeepalive);
}
QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(
configMap.value(protocols::wireguard::AllowedIPs).split(", "));
firstPeerMap.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;
@@ -588,25 +627,25 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
};
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(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();
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();
if (hasCookieReplyPacketJunkSize && hasTransportPacketJunkSize) {
protocolVersion = "2";
@@ -617,8 +656,8 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
detectedType = ConfigTypes::Awg;
}
if (!configMap.value(protocols::wireguard::MTU).isEmpty()) {
lastConfig[configKey::mtu] = configMap.value(protocols::wireguard::MTU);
if (!interfaceMap.value(protocols::wireguard::MTU).isEmpty()) {
lastConfig[configKey::mtu] = interfaceMap.value(protocols::wireguard::MTU);
} else {
lastConfig[configKey::mtu] = (protocolName == configKey::awg)
? protocols::awg::defaultMtu
@@ -645,7 +684,7 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
QJsonObject config;
config[configKey::containers] = arr;
config[configKey::defaultContainer] = containerName;
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
config[configKey::description] = m_serversRepository->nextAvailableServerName();
const static QRegularExpression dnsRegExp(
"DNS = "
@@ -699,7 +738,7 @@ QJsonObject ImportController::extractXrayConfig(const QString &data, ConfigTypes
? configKey::amneziaSsxray
: configKey::amneziaXray;
if (description.isEmpty()) {
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
config[configKey::description] = m_serversRepository->nextAvailableServerName();
} else {
config[configKey::description] = description;
}

View File

@@ -358,7 +358,7 @@ void InstallController::addEmptyServer(const ServerCredentials &credentials)
serverConfig.userName = credentials.userName;
serverConfig.password = credentials.secretData;
serverConfig.port = credentials.port;
serverConfig.description = m_appSettingsRepository->nextAvailableServerName();
serverConfig.description = m_serversRepository->nextAvailableServerName();
serverConfig.displayName = serverConfig.description.isEmpty() ? serverConfig.hostName : serverConfig.description;
serverConfig.defaultContainer = DockerContainer::None;
@@ -1170,7 +1170,7 @@ ErrorCode InstallController::installServer(const ServerCredentials &credentials,
serverConfig.userName = credentials.userName;
serverConfig.password = credentials.secretData;
serverConfig.port = credentials.port;
serverConfig.description = m_appSettingsRepository->nextAvailableServerName();
serverConfig.description = m_serversRepository->nextAvailableServerName();
for (auto iterator = preparedContainers.begin(); iterator != preparedContainers.end(); iterator++) {
serverConfig.containers.insert(iterator.key(), iterator.value());
@@ -1240,28 +1240,26 @@ ErrorCode InstallController::installContainer(const QString &serverId, DockerCon
return ErrorCode::NoError;
}
ErrorCode InstallController::checkSshConnection(const ServerCredentials &credentials, QString &output,
ErrorCode InstallController::checkSshConnection(ServerCredentials &credentials, QString &output,
std::function<QString()> passphraseCallback)
{
SshSession sshSession(this);
ErrorCode errorCode = ErrorCode::NoError;
ServerCredentials processedCredentials = credentials;
if (processedCredentials.secretData.contains("BEGIN") && processedCredentials.secretData.contains("PRIVATE KEY")) {
if (credentials.secretData.contains("BEGIN") && credentials.secretData.contains("PRIVATE KEY")) {
if (!passphraseCallback) {
return ErrorCode::SshPrivateKeyError;
}
QString decryptedPrivateKey;
errorCode = sshSession.getDecryptedPrivateKey(processedCredentials, decryptedPrivateKey, passphraseCallback);
errorCode = sshSession.getDecryptedPrivateKey(credentials, decryptedPrivateKey, passphraseCallback);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
processedCredentials.secretData = decryptedPrivateKey;
credentials.secretData = decryptedPrivateKey;
}
output = sshSession.checkSshConnection(processedCredentials, errorCode);
output = sshSession.checkSshConnection(credentials, errorCode);
return errorCode;
}

View File

@@ -64,7 +64,8 @@ public:
bool isUpdateDockerContainerRequired(DockerContainer container, const ContainerConfig &oldConfig, const ContainerConfig &newConfig);
ErrorCode checkSshConnection(const ServerCredentials &credentials, QString &output, std::function<QString()> passphraseCallback = nullptr);
ErrorCode checkSshConnection(ServerCredentials &credentials, QString &output,
std::function<QString()> passphraseCallback = nullptr);
bool isServerAlreadyExists(const ServerCredentials &credentials, int &existingServerIndex);

View File

@@ -363,6 +363,6 @@ void SettingsController::disablePremV1MigrationReminder()
QString SettingsController::nextAvailableServerName() const
{
return m_appSettingsRepository->nextAvailableServerName();
return m_serversRepository->nextAvailableServerName();
}

View File

@@ -13,7 +13,6 @@
#include "version.h"
#include "core/controllers/gatewayController.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/errorStrings.h"
#include "core/utils/selfhosted/scriptsRegistry.h"
namespace
@@ -109,7 +108,7 @@ void UpdateController::fetchGatewayUrl()
.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
auto [err, gatewayResponse] = result;
if (err != ErrorCode::NoError) {
logger.error() << errorString(err);
logger.error() << "Gateway request failed, error code:" << static_cast<int>(err);
finishUpdateCheck();
return;
}
@@ -250,17 +249,9 @@ void UpdateController::runInstaller()
runLinuxInstaller(kInstallerLocalPath);
#endif
} else {
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
} else {
QString err = reply->errorString();
logger.error() << QString::fromUtf8(reply->readAll());
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
logger.error() << "Error message:" << err;
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
}
logger.error() << "Installer download failed, network error:" << static_cast<int>(reply->error())
<< reply->errorString();
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
reply->deleteLater();
});

View File

@@ -205,7 +205,11 @@ QJsonObject AwgClientConfig::toJson() const
if (isObfuscationEnabled) {
obj[configKey::isObfuscationEnabled] = isObfuscationEnabled;
}
if (!peers.isEmpty()) {
obj["peers"] = peers;
}
return obj;
}
@@ -250,7 +254,9 @@ 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;
}

View File

@@ -1,6 +1,7 @@
#ifndef AWGPROTOCOLCONFIG_H
#define AWGPROTOCOLCONFIG_H
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QStringList>
@@ -60,6 +61,7 @@ struct AwgClientConfig {
QStringList allowedIps;
QString persistentKeepAlive;
QString mtu;
QJsonArray peers;
QString junkPacketCount;
QString junkPacketMinSize;
QString junkPacketMaxSize;

View File

@@ -426,26 +426,6 @@ void SecureAppSettingsRepository::clearSettings()
emit settingsCleared();
}
QString SecureAppSettingsRepository::nextAvailableServerName() const
{
int i = 0;
bool nameExist = false;
do {
i++;
nameExist = false;
QJsonArray servers = QJsonDocument::fromJson(value("Servers/serversList").toByteArray()).array();
for (const QJsonValue &server : servers) {
if (server.toObject().value(configKey::description).toString() == QString("Server") + " " + QString::number(i)) {
nameExist = true;
break;
}
}
} while (nameExist);
return QString("Server") + " " + QString::number(i);
}
void SecureAppSettingsRepository::setInstallationUuid(const QString &uuid)
{
m_settings->setValue("Conf/installationUuid", uuid);

View File

@@ -90,8 +90,6 @@ public:
bool restoreAppConfig(const QByteArray &cfg);
void clearSettings();
QString nextAvailableServerName() const;
QByteArray xraySavedConfigs() const;
void setXraySavedConfigs(const QByteArray &data);

View File

@@ -3,6 +3,7 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonValue>
#include <QSet>
#include <QUuid>
#include "core/utils/serverConfigUtils.h"
@@ -32,6 +33,45 @@ QJsonObject embedStorageServerId(const QString &serverId, const QJsonObject &pay
return o;
}
QString storedServerDisplayName(const SecureServersRepository *repository, const QString &serverId)
{
using Kind = serverConfigUtils::ConfigType;
switch (repository->serverKind(serverId)) {
case Kind::SelfHostedAdmin:
if (const auto cfg = repository->selfHostedAdminConfig(serverId)) {
return cfg->displayName;
}
break;
case Kind::SelfHostedUser:
if (const auto cfg = repository->selfHostedUserConfig(serverId)) {
return cfg->displayName;
}
break;
case Kind::Native:
if (const auto cfg = repository->nativeConfig(serverId)) {
return cfg->displayName;
}
break;
case Kind::AmneziaPremiumV2:
case Kind::AmneziaFreeV3:
case Kind::ExternalPremium:
if (const auto cfg = repository->apiV2Config(serverId)) {
return cfg->displayName;
}
break;
case Kind::AmneziaPremiumV1:
case Kind::AmneziaFreeV2:
if (const auto cfg = repository->legacyApiConfig(serverId)) {
return cfg->displayName;
}
break;
case Kind::Invalid:
default:
break;
}
return {};
}
} // namespace
SecureServersRepository::SecureServersRepository(SecureQSettings *settings, QObject *parent)
@@ -153,6 +193,28 @@ void SecureServersRepository::clearServers()
syncToStorage();
}
QString SecureServersRepository::nextAvailableServerName() const
{
QSet<QString> usedNames;
usedNames.reserve(m_orderedServerIds.size());
for (const QString &serverId : m_orderedServerIds) {
const QString displayName = storedServerDisplayName(this, serverId);
if (!displayName.isEmpty()) {
usedNames.insert(displayName);
}
}
int i = 0;
QString candidate;
do {
i++;
candidate = QStringLiteral("Server %1").arg(i);
} while (usedNames.contains(candidate));
return candidate;
}
QString SecureServersRepository::addServer(const QString &serverId, const QJsonObject &serverJson, serverConfigUtils::ConfigType kind)
{
const QString id = normalizedOrGeneratedServerId(serverId);

View File

@@ -48,6 +48,8 @@ public:
void clearServers();
QString nextAvailableServerName() const;
void invalidateCache();
signals:

View File

@@ -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;
}

View File

@@ -37,6 +37,9 @@ 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;
@@ -58,6 +61,15 @@ 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;

View File

@@ -26,6 +26,8 @@ set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks"
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
)
if(DEPLOY)
@@ -114,10 +116,20 @@ target_include_directories(networkextension PRIVATE ${CLIENT_ROOT_DIR})
target_include_directories(networkextension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
find_package(openvpnadapter REQUIRED)
# FIXME(ygurov): https://github.com/conan-io/conan/issues/20034
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL)
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO)
target_link_libraries(networkextension PRIVATE amnezia::openvpnadapter)
find_package(awg-apple REQUIRED)
target_link_libraries(networkextension PRIVATE amnezia::awg-apple)
find_package(hev-socks5-tunnel REQUIRED)
# FIXME(ygurov): https://github.com/conan-io/conan/issues/20034
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL)
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO)
target_link_libraries(networkextension PRIVATE heiher::hev-socks5-tunnel)

View File

@@ -169,68 +169,96 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
QJsonArray jsAllowedIPAddesses;
QJsonArray plainAllowedIP = wgConfig.value(amnezia::configKey::allowedIps).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<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));
}
}
// 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" };
// 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::configKey::hostName));
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));
}
if (splitTunnelType == 2) {
for (auto v : splitTunnelSites) {
QString ipRange = v.toString();

View File

@@ -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 {

View File

@@ -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: ", "))

View File

@@ -595,6 +595,10 @@ 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]);
@@ -674,7 +678,23 @@ 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]);
@@ -688,13 +708,6 @@ 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 {

View File

@@ -5,8 +5,12 @@
#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>
@@ -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<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 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;
// 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<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) {
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<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;
}
}
}
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;

View File

@@ -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<IPAddress>& 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) {

View File

@@ -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<QHostAddress, int> 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);

View File

@@ -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<IPAddress>& 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) {

View File

@@ -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<IPAddress>& 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;
}

View File

@@ -1,5 +1,9 @@
#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"

View File

@@ -20,8 +20,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Android")
set(_CONAN_INSTALL_ARGS
"-c=tools.android:cmake_legacy_toolchain=false"
"-c=tools.build:sharedlinkflags=['-Wl,-z,max-page-size=16384']"
"-c=tools.build:exelinkflags=['-Wl,-z,max-page-size=16384']"
"-o=openssl/*:shared=True")
"-c=tools.build:exelinkflags=['-Wl,-z,max-page-size=16384']")
set(CMAKE_ANDROID_STL_TYPE "c++_shared" CACHE STRING "")
endif()
@@ -29,6 +28,12 @@ if (WIN32 OR APPLE)
set(CMAKE_INSTALL_BINDIR ".")
endif()
# Apple NE-based apps do not support any dylibs or variations
# So Qt would use the openssl bundled with system, not application
if (NOT(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR (APPLE AND MACOS_NE)))
list(APPEND _CONAN_INSTALL_ARGS "-o=openssl/*:shared=True")
endif()
list(PREPEND _CONAN_INSTALL_ARGS "--build=missing")
list(JOIN _CONAN_INSTALL_ARGS ";" _CONAN_INSTALL_ARGS_JOINED)
set(CONAN_INSTALL_ARGS ${_CONAN_INSTALL_ARGS_JOINED} CACHE STRING "" FORCE)

View File

@@ -5,6 +5,7 @@ from conan.errors import ConanInvalidConfiguration
from conan.tools.scm import Git
from conan.internal.model.pkg_type import PackageType
from conan.tools.files import chdir
from conan.tools.apple import XCRun
import os
import shutil
@@ -49,7 +50,10 @@ class OpenVPNAdapter(ConanFile):
def build(self):
with chdir(self, self.source_folder):
self.run("xcrun xcodebuild"
xcrun = XCRun(self)
xcodebuild = xcrun.find("xcodebuild")
self.run(f"{xcodebuild}"
" -project OpenVPNAdapter.xcodeproj"
" -scheme OpenVPNAdapter"
" -configuration Release"
@@ -57,10 +61,20 @@ class OpenVPNAdapter(ConanFile):
f" -sdk {self._sdk}"
f' "CONFIGURATION_BUILD_DIR={self.build_folder}"'
f' "BUILT_PRODUCTS_DIR={self.build_folder}"'
" MACH_O_TYPE=staticlib"
" BUILD_LIBRARY_FOR_DISTRIBUTION=YES"
" CODE_SIGNING_ALLOWED=NO"
)
openvpnadapter = os.path.join(self.build_folder, "OpenVPNAdapter.framework", "OpenVPNAdapter")
self.run(f"{xcrun.libtool} -static -o"
f" {openvpnadapter}"
f" {openvpnadapter}"
f' {os.path.join(self.build_folder, "OpenVPNClient.framework", "OpenVPNClient")}'
f' {os.path.join(self.build_folder, "LZ4.framework", "LZ4")}'
f' {os.path.join(self.build_folder, "mbedTLS.framework", "mbedTLS")}'
)
def package(self):
shutil.copytree(os.path.join(self.build_folder, "OpenVPNAdapter.framework"),
os.path.join(self.package_folder, "OpenVPNAdapter.framework"))
@@ -70,3 +84,4 @@ class OpenVPNAdapter(ConanFile):
self.cpp_info.type = PackageType.STATIC
self.cpp_info.package_framework = True
self.cpp_info.location = os.path.join(self.package_folder, "OpenVPNAdapter.framework")
self.cpp_info.frameworks = ["SystemConfiguration"]

View File

@@ -316,12 +316,9 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug")
endif()
if(APPLE)
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
set_target_properties(${PROJECT} PROPERTIES
INSTALL_RPATH "@executable_path/../Frameworks"
BUILD_WITH_INSTALL_RPATH TRUE
)
endif()
set_target_properties(${PROJECT} PROPERTIES
INSTALL_RPATH "@executable_path/../Frameworks"
)
find_library(FW_COREFOUNDATION CoreFoundation)
find_library(FW_SYSTEMCONFIG SystemConfiguration)
@@ -428,11 +425,32 @@ endif()
# install target
install(TARGETS ${PROJECT}
DESTINATION ${CMAKE_INSTALL_BINDIR}
RUNTIME_DEPENDENCY_SET service_deps
COMPONENT AmneziaVPN
)
install(FILES $<TARGET_RUNTIME_DLLS:${PROJECT}>
DESTINATION ${CMAKE_INSTALL_BINDIR}
if(APPLE)
set(RUNTIME_DEPS_DIR ${CMAKE_INSTALL_BINDIR}/../Frameworks)
else()
set(RUNTIME_DEPS_DIR ${CMAKE_INSTALL_BINDIR})
endif()
install(RUNTIME_DEPENDENCY_SET service_deps
PRE_EXCLUDE_REGEXES
[[api-ms-win-.*]]
[[ext-ms-.*]]
[[kernel32\.dll]]
[[hvsifiletrust\.dll]]
[[libc\.so\..*]] [[libgcc_s\.so\..*]] [[libm\.so\..*]] [[libstdc\+\+\.so\..*]]
[[.*\.framework]]
[[^[Qq]t.*]]
POST_EXCLUDE_REGEXES
[[^.*[\\/]system32[\\/].*\.dll$]]
[[^/lib.*]]
[[^/usr/lib.*]]
DIRECTORIES ${CONAN_RUNTIME_LIB_DIRS}
COMPONENT AmneziaVPN
DESTINATION "${RUNTIME_DEPS_DIR}"
)
qt_generate_deploy_app_script(