mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-21 02:01:03 +07:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0c4e2d914 | |||
| 7fe6e283ba | |||
| a296f1b5c9 | |||
| d8e644b032 | |||
| b64b589acd | |||
| fc50835ccb | |||
| 6c467bdb53 |
@@ -260,6 +260,16 @@ if(WIN32)
|
||||
)
|
||||
endif()
|
||||
|
||||
if(APPLE AND NOT IOS AND NOT MACOS_NE)
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/protocols/ikev2VpnProtocolMacos.h
|
||||
)
|
||||
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/protocols/ikev2VpnProtocolMacos.mm
|
||||
)
|
||||
endif()
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
message("Client desktop build")
|
||||
add_compile_definitions(AMNEZIA_DESKTOP)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#ifndef IKEV2_VPN_PROTOCOL_MACOS_H
|
||||
#define IKEV2_VPN_PROTOCOL_MACOS_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
#include "vpnProtocol.h"
|
||||
|
||||
#if defined(__OBJC__)
|
||||
#include <NetworkExtension/NetworkExtension.h>
|
||||
#endif
|
||||
|
||||
class Ikev2ProtocolMacos : public VpnProtocol
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Ikev2ProtocolMacos(const QJsonObject &configuration, QObject *parent = nullptr);
|
||||
~Ikev2ProtocolMacos() override;
|
||||
|
||||
ErrorCode start() override;
|
||||
void stop() override;
|
||||
|
||||
static QString tunnelName() { return "AmneziaVPN IKEv2"; }
|
||||
|
||||
private:
|
||||
void readIkev2Configuration(const QJsonObject &configuration);
|
||||
|
||||
bool storeClientIdentity();
|
||||
|
||||
void handleStatusChange(int rawStatus);
|
||||
void startTunnelNow();
|
||||
void reportError(ErrorCode code);
|
||||
|
||||
void startHandshakeTimeoutTimer();
|
||||
void stopHandshakeTimeoutTimer();
|
||||
|
||||
void removeStatusObserver();
|
||||
|
||||
private:
|
||||
QJsonObject m_config;
|
||||
|
||||
QString m_hostName;
|
||||
QString m_clientId;
|
||||
QString m_clientCertBase64;
|
||||
QString m_clientCertPassword;
|
||||
|
||||
QTimer *m_handshakeTimeoutTimer { nullptr };
|
||||
bool m_handshakeTimedOut { false };
|
||||
bool m_startWhenDisconnected { false };
|
||||
bool m_tunnelStarted { false };
|
||||
int m_startRetries { 0 };
|
||||
|
||||
void *m_statusObserver { nullptr };
|
||||
|
||||
int m_lastVpnStatus { 0 };
|
||||
|
||||
static constexpr int HANDSHAKE_TIMEOUT_SEC = 20;
|
||||
static constexpr int MAX_START_RETRIES = 5;
|
||||
};
|
||||
|
||||
#endif // IKEV2_VPN_PROTOCOL_MACOS_H
|
||||
@@ -0,0 +1,666 @@
|
||||
#include "ikev2VpnProtocolMacos.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QPointer>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/ipcClient.h"
|
||||
#include "ipc.h"
|
||||
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pkcs12.h>
|
||||
#include <openssl/x509.h>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <NetworkExtension/NetworkExtension.h>
|
||||
#import <Security/Security.h>
|
||||
|
||||
namespace {
|
||||
|
||||
const char *kRepackedP12Password = "amnezia";
|
||||
|
||||
const char *kVpnSystemKeychainPath = "/Library/Keychains/System.keychain";
|
||||
|
||||
const char *vpnStatusName(int status)
|
||||
{
|
||||
switch (status) {
|
||||
case NEVPNStatusInvalid: return "Invalid";
|
||||
case NEVPNStatusDisconnected: return "Disconnected";
|
||||
case NEVPNStatusConnecting: return "Connecting";
|
||||
case NEVPNStatusConnected: return "Connected";
|
||||
case NEVPNStatusReasserting: return "Reasserting";
|
||||
case NEVPNStatusDisconnecting: return "Disconnecting";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
bool prepareIdentity(const QByteArray &source, const QString &friendlyName, QByteArray &repackedP12, QByteArray &caCertDer)
|
||||
{
|
||||
const unsigned char *cursor = reinterpret_cast<const unsigned char *>(source.constData());
|
||||
PKCS12 *sourceP12 = d2i_PKCS12(nullptr, &cursor, source.size());
|
||||
if (!sourceP12) {
|
||||
qCritical() << "[IKEv2-mac] failed to parse client p12 container";
|
||||
return false;
|
||||
}
|
||||
|
||||
EVP_PKEY *privateKey = nullptr;
|
||||
X509 *certificate = nullptr;
|
||||
STACK_OF(X509) *caChain = nullptr;
|
||||
int parsed = PKCS12_parse(sourceP12, "", &privateKey, &certificate, &caChain);
|
||||
PKCS12_free(sourceP12);
|
||||
|
||||
if (!parsed || !privateKey || !certificate) {
|
||||
qCritical() << "[IKEv2-mac] failed to extract key/certificate from client p12";
|
||||
if (privateKey) EVP_PKEY_free(privateKey);
|
||||
if (certificate) X509_free(certificate);
|
||||
if (caChain) sk_X509_pop_free(caChain, X509_free);
|
||||
return false;
|
||||
}
|
||||
|
||||
const int caCount = caChain ? sk_X509_num(caChain) : 0;
|
||||
qInfo() << "[IKEv2-mac] CA certificates bundled in client p12:" << caCount;
|
||||
if (caCount > 0) {
|
||||
X509 *caCert = sk_X509_value(caChain, caCount - 1);
|
||||
unsigned char *caEncoded = nullptr;
|
||||
int caLength = i2d_X509(caCert, &caEncoded);
|
||||
if (caLength > 0 && caEncoded) {
|
||||
caCertDer = QByteArray(reinterpret_cast<const char *>(caEncoded), caLength);
|
||||
OPENSSL_free(caEncoded);
|
||||
}
|
||||
}
|
||||
|
||||
PKCS12 *repacked = PKCS12_create(kRepackedP12Password,
|
||||
friendlyName.toUtf8().constData(),
|
||||
privateKey,
|
||||
certificate,
|
||||
caChain,
|
||||
NID_pbe_WithSHA1And3_Key_TripleDES_CBC,
|
||||
NID_pbe_WithSHA1And3_Key_TripleDES_CBC,
|
||||
PKCS12_DEFAULT_ITER,
|
||||
PKCS12_DEFAULT_ITER,
|
||||
0);
|
||||
EVP_PKEY_free(privateKey);
|
||||
X509_free(certificate);
|
||||
if (caChain) sk_X509_pop_free(caChain, X509_free);
|
||||
|
||||
if (!repacked) {
|
||||
qCritical() << "[IKEv2-mac] failed to repackage client p12";
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned char *encoded = nullptr;
|
||||
int encodedLength = i2d_PKCS12(repacked, &encoded);
|
||||
PKCS12_free(repacked);
|
||||
|
||||
if (encodedLength <= 0 || !encoded) {
|
||||
qCritical() << "[IKEv2-mac] failed to serialize repackaged client p12";
|
||||
return false;
|
||||
}
|
||||
|
||||
repackedP12 = QByteArray(reinterpret_cast<const char *>(encoded), encodedLength);
|
||||
OPENSSL_free(encoded);
|
||||
return true;
|
||||
}
|
||||
|
||||
void removeIdentityFromLoginKeychain(const QString &label)
|
||||
{
|
||||
NSDictionary *query = @{
|
||||
(__bridge id)kSecClass : (__bridge id)kSecClassIdentity,
|
||||
(__bridge id)kSecAttrLabel : label.toNSString(),
|
||||
(__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitAll,
|
||||
};
|
||||
SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
}
|
||||
|
||||
bool importIdentityToLoginKeychain(const QByteArray &p12, const QString &label)
|
||||
{
|
||||
SecKeychainRef loginKeychain = NULL;
|
||||
if (SecKeychainCopyDefault(&loginKeychain) != errSecSuccess || loginKeychain == NULL) {
|
||||
qCritical() << "[IKEv2-mac] cannot open the login keychain";
|
||||
return false;
|
||||
}
|
||||
|
||||
CFMutableArrayRef trustedApps = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
|
||||
SecTrustedApplicationRef selfApp = NULL;
|
||||
if (SecTrustedApplicationCreateFromPath(NULL, &selfApp) == errSecSuccess) {
|
||||
CFArrayAppendValue(trustedApps, selfApp);
|
||||
CFRelease(selfApp);
|
||||
}
|
||||
SecTrustedApplicationRef neAgent = NULL;
|
||||
if (SecTrustedApplicationCreateFromPath("/usr/libexec/neagent", &neAgent) == errSecSuccess) {
|
||||
CFArrayAppendValue(trustedApps, neAgent);
|
||||
CFRelease(neAgent);
|
||||
}
|
||||
|
||||
SecAccessRef access = NULL;
|
||||
OSStatus accessStatus = SecAccessCreate((__bridge CFStringRef)label.toNSString(), trustedApps, &access);
|
||||
CFRelease(trustedApps);
|
||||
if (accessStatus != errSecSuccess || access == NULL) {
|
||||
qCritical() << "[IKEv2-mac] SecAccessCreate failed, status" << (int)accessStatus;
|
||||
CFRelease(loginKeychain);
|
||||
return false;
|
||||
}
|
||||
|
||||
NSData *p12Data = [NSData dataWithBytes:p12.constData() length:p12.size()];
|
||||
NSDictionary *options = @{
|
||||
(__bridge id)kSecImportExportPassphrase : [NSString stringWithUTF8String:kRepackedP12Password],
|
||||
(__bridge id)kSecImportExportKeychain : (__bridge id)loginKeychain,
|
||||
(__bridge id)kSecImportExportAccess : (__bridge id)access,
|
||||
};
|
||||
|
||||
CFArrayRef items = NULL;
|
||||
OSStatus importStatus = SecPKCS12Import((__bridge CFDataRef)p12Data, (__bridge CFDictionaryRef)options, &items);
|
||||
if (items) {
|
||||
CFRelease(items);
|
||||
}
|
||||
CFRelease(access);
|
||||
CFRelease(loginKeychain);
|
||||
|
||||
if (importStatus != errSecSuccess) {
|
||||
qCritical() << "[IKEv2-mac] SecPKCS12Import into login keychain failed, status" << (int)importStatus;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
NSData *copyIdentityPersistentRef(const QString &label)
|
||||
{
|
||||
SecKeychainRef loginKeychain = NULL;
|
||||
SecKeychainCopyDefault(&loginKeychain);
|
||||
|
||||
NSMutableDictionary *query = [@{
|
||||
(__bridge id)kSecClass : (__bridge id)kSecClassIdentity,
|
||||
(__bridge id)kSecAttrLabel : label.toNSString(),
|
||||
(__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne,
|
||||
(__bridge id)kSecReturnPersistentRef : @YES,
|
||||
} mutableCopy];
|
||||
if (loginKeychain) {
|
||||
query[(__bridge id)kSecMatchSearchList] = @[ (__bridge id)loginKeychain ];
|
||||
}
|
||||
|
||||
CFTypeRef persistentRef = NULL;
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &persistentRef);
|
||||
if (loginKeychain) {
|
||||
CFRelease(loginKeychain);
|
||||
}
|
||||
|
||||
if (status != errSecSuccess || persistentRef == NULL) {
|
||||
qDebug() << "[IKEv2-mac] client identity not present in login keychain, status" << (int)status;
|
||||
return nil;
|
||||
}
|
||||
|
||||
return (NSData *)CFAutorelease(persistentRef);
|
||||
}
|
||||
|
||||
void runPrivilegedSecurity(const QString &label, const QStringList &arguments)
|
||||
{
|
||||
auto process = IpcClient::CreatePrivilegedProcess();
|
||||
if (!process) {
|
||||
qCritical() << "[IKEv2-mac] privileged service is unavailable for" << label;
|
||||
return;
|
||||
}
|
||||
|
||||
process->setProgram(PermittedProcess::Security);
|
||||
process->setArguments(arguments);
|
||||
process->start();
|
||||
|
||||
bool started = false;
|
||||
{
|
||||
auto reply = process->waitForStarted(5000);
|
||||
reply.waitForFinished();
|
||||
started = reply.returnValue();
|
||||
}
|
||||
|
||||
bool finished = false;
|
||||
{
|
||||
auto reply = process->waitForFinished(15000);
|
||||
reply.waitForFinished();
|
||||
finished = reply.returnValue();
|
||||
}
|
||||
|
||||
QByteArray out;
|
||||
QByteArray err;
|
||||
{
|
||||
auto reply = process->readAllStandardOutput();
|
||||
reply.waitForFinished();
|
||||
out = reply.returnValue();
|
||||
}
|
||||
{
|
||||
auto reply = process->readAllStandardError();
|
||||
reply.waitForFinished();
|
||||
err = reply.returnValue();
|
||||
}
|
||||
|
||||
qInfo() << "[IKEv2-mac]" << label << "started" << started << "finished" << finished
|
||||
<< "| out:" << out.trimmed() << "| err:" << err.trimmed();
|
||||
}
|
||||
|
||||
void disableVpnConfiguration()
|
||||
{
|
||||
NEVPNManager *manager = [NEVPNManager sharedManager];
|
||||
if (!manager.enabled) {
|
||||
return;
|
||||
}
|
||||
manager.enabled = NO;
|
||||
[manager saveToPreferencesWithCompletionHandler:^(NSError *error) {
|
||||
if (error) {
|
||||
qWarning() << "[IKEv2-mac] failed to disable VPN configuration:"
|
||||
<< QString::fromNSString(error.localizedDescription);
|
||||
} else {
|
||||
qInfo() << "[IKEv2-mac] VPN configuration disabled";
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Ikev2ProtocolMacos::Ikev2ProtocolMacos(const QJsonObject &configuration, QObject *parent)
|
||||
: VpnProtocol(configuration, parent)
|
||||
{
|
||||
readIkev2Configuration(configuration);
|
||||
}
|
||||
|
||||
Ikev2ProtocolMacos::~Ikev2ProtocolMacos()
|
||||
{
|
||||
qInfo() << "[IKEv2-mac] ~Ikev2ProtocolMacos()";
|
||||
if (m_handshakeTimeoutTimer) {
|
||||
m_handshakeTimeoutTimer->stop();
|
||||
m_handshakeTimeoutTimer = nullptr;
|
||||
}
|
||||
removeStatusObserver();
|
||||
}
|
||||
|
||||
void Ikev2ProtocolMacos::readIkev2Configuration(const QJsonObject &configuration)
|
||||
{
|
||||
m_config = configuration.value(ProtocolUtils::key_proto_config_data(Proto::Ikev2)).toObject();
|
||||
|
||||
m_hostName = m_config.value(configKey::hostName).toString();
|
||||
m_clientId = m_config.value(configKey::userName).toString();
|
||||
m_clientCertBase64 = m_config.value(configKey::cert).toString();
|
||||
m_clientCertPassword = m_config.value(configKey::password).toString();
|
||||
}
|
||||
|
||||
bool Ikev2ProtocolMacos::storeClientIdentity()
|
||||
{
|
||||
if (copyIdentityPersistentRef(m_clientId) != nil) {
|
||||
qInfo() << "[IKEv2-mac] client identity already in login keychain, reusing it";
|
||||
return true;
|
||||
}
|
||||
|
||||
qInfo() << "[IKEv2-mac] installing client identity into login keychain";
|
||||
|
||||
QByteArray sourceP12 = QByteArray::fromBase64(m_clientCertBase64.toUtf8());
|
||||
QByteArray repackedP12;
|
||||
QByteArray caCertDer;
|
||||
if (!prepareIdentity(sourceP12, m_clientId, repackedP12, caCertDer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
removeIdentityFromLoginKeychain(m_clientId);
|
||||
if (!importIdentityToLoginKeychain(repackedP12, m_clientId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!caCertDer.isEmpty()) {
|
||||
QTemporaryFile caFile(QDir::tempPath() + "/amnezia-ikev2-ca-XXXXXX.cer");
|
||||
caFile.setAutoRemove(false);
|
||||
if (caFile.open()) {
|
||||
const QString caPath = caFile.fileName();
|
||||
caFile.write(caCertDer);
|
||||
caFile.close();
|
||||
runPrivilegedSecurity("CA trust", { "add-trusted-cert", "-d", "-r", "trustRoot",
|
||||
"-k", QString::fromLatin1(kVpnSystemKeychainPath), caPath });
|
||||
QFile::remove(caPath);
|
||||
}
|
||||
} else {
|
||||
qWarning() << "[IKEv2-mac] no CA certificate in client p12; server validation may fail";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Ikev2ProtocolMacos::reportError(ErrorCode code)
|
||||
{
|
||||
QMetaObject::invokeMethod(this, [this, code]() { setLastError(code); }, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void Ikev2ProtocolMacos::removeStatusObserver()
|
||||
{
|
||||
if (m_statusObserver) {
|
||||
NEVPNManager *manager = [NEVPNManager sharedManager];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:(id)m_statusObserver
|
||||
name:NEVPNStatusDidChangeNotification
|
||||
object:manager.connection];
|
||||
m_statusObserver = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode Ikev2ProtocolMacos::start()
|
||||
{
|
||||
qInfo() << "[IKEv2-mac] start() requested, host =" << m_hostName << ", clientId =" << m_clientId;
|
||||
|
||||
if (m_hostName.isEmpty() || m_clientCertBase64.isEmpty()) {
|
||||
qCritical() << "[IKEv2-mac] missing server address or client certificate";
|
||||
setLastError(ErrorCode::IKEv2ConfigError);
|
||||
return ErrorCode::IKEv2ConfigError;
|
||||
}
|
||||
|
||||
if (!storeClientIdentity()) {
|
||||
setLastError(ErrorCode::IKEv2ConfigError);
|
||||
return ErrorCode::IKEv2ConfigError;
|
||||
}
|
||||
|
||||
m_handshakeTimedOut = false;
|
||||
m_lastVpnStatus = NEVPNStatusInvalid;
|
||||
m_startRetries = 0;
|
||||
m_tunnelStarted = false;
|
||||
|
||||
setConnectionState(Vpn::ConnectionState::Connecting);
|
||||
|
||||
NSString *nsServerAddress = m_hostName.toNSString();
|
||||
NSString *nsLocalIdentifier = m_clientId.toNSString();
|
||||
QString clientId = m_clientId;
|
||||
|
||||
QPointer<Ikev2ProtocolMacos> self = this;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (!self) return;
|
||||
NEVPNManager *manager = [NEVPNManager sharedManager];
|
||||
|
||||
[manager loadFromPreferencesWithCompletionHandler:^(NSError *loadError) {
|
||||
if (!self) return;
|
||||
if (loadError) {
|
||||
qCritical() << "[IKEv2-mac] loading VPN preferences failed:"
|
||||
<< QString::fromNSString(loadError.localizedDescription);
|
||||
self->reportError(ErrorCode::IKEv2LoadError);
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *identityReference = copyIdentityPersistentRef(clientId);
|
||||
if (identityReference == nil) {
|
||||
self->reportError(ErrorCode::IKEv2ConfigError);
|
||||
return;
|
||||
}
|
||||
|
||||
NEVPNProtocolIKEv2 *protocol = [[NEVPNProtocolIKEv2 alloc] init];
|
||||
|
||||
protocol.serverAddress = nsServerAddress;
|
||||
protocol.remoteIdentifier = nsServerAddress;
|
||||
protocol.localIdentifier = nsLocalIdentifier;
|
||||
|
||||
protocol.authenticationMethod = NEVPNIKEAuthenticationMethodCertificate;
|
||||
protocol.certificateType = NEVPNIKEv2CertificateTypeRSA;
|
||||
protocol.identityReference = identityReference;
|
||||
protocol.useExtendedAuthentication = NO;
|
||||
protocol.enablePFS = NO;
|
||||
protocol.disconnectOnSleep = NO;
|
||||
protocol.deadPeerDetectionRate = NEVPNIKEv2DeadPeerDetectionRateMedium;
|
||||
|
||||
protocol.IKESecurityAssociationParameters.encryptionAlgorithm = NEVPNIKEv2EncryptionAlgorithmAES256;
|
||||
protocol.IKESecurityAssociationParameters.integrityAlgorithm = NEVPNIKEv2IntegrityAlgorithmSHA256;
|
||||
protocol.IKESecurityAssociationParameters.diffieHellmanGroup = NEVPNIKEv2DiffieHellmanGroup14;
|
||||
protocol.IKESecurityAssociationParameters.lifetimeMinutes = 1410;
|
||||
|
||||
protocol.childSecurityAssociationParameters.encryptionAlgorithm = NEVPNIKEv2EncryptionAlgorithmAES128GCM;
|
||||
protocol.childSecurityAssociationParameters.diffieHellmanGroup = NEVPNIKEv2DiffieHellmanGroup14;
|
||||
protocol.childSecurityAssociationParameters.lifetimeMinutes = 1410;
|
||||
|
||||
[manager setProtocolConfiguration:protocol];
|
||||
[manager setLocalizedDescription:Ikev2ProtocolMacos::tunnelName().toNSString()];
|
||||
[manager setEnabled:YES];
|
||||
[manager setOnDemandEnabled:NO];
|
||||
|
||||
[manager saveToPreferencesWithCompletionHandler:^(NSError *firstSaveError) {
|
||||
if (!self) return;
|
||||
if (firstSaveError) {
|
||||
qCritical() << "[IKEv2-mac] saving VPN preferences failed:"
|
||||
<< QString::fromNSString(firstSaveError.localizedDescription);
|
||||
self->reportError(ErrorCode::IKEv2SaveError);
|
||||
return;
|
||||
}
|
||||
|
||||
[manager loadFromPreferencesWithCompletionHandler:^(NSError *reloadError) {
|
||||
if (!self) return;
|
||||
if (reloadError) {
|
||||
qCritical() << "[IKEv2-mac] reloading VPN preferences failed:"
|
||||
<< QString::fromNSString(reloadError.localizedDescription);
|
||||
self->reportError(ErrorCode::IKEv2LoadError);
|
||||
return;
|
||||
}
|
||||
|
||||
[manager saveToPreferencesWithCompletionHandler:^(NSError *resaveError) {
|
||||
if (!self) return;
|
||||
if (resaveError) {
|
||||
qCritical() << "[IKEv2-mac] re-saving VPN preferences failed:"
|
||||
<< QString::fromNSString(resaveError.localizedDescription);
|
||||
self->reportError(ErrorCode::IKEv2SaveError);
|
||||
return;
|
||||
}
|
||||
|
||||
self->removeStatusObserver();
|
||||
self->m_statusObserver = (void *)[[NSNotificationCenter defaultCenter]
|
||||
addObserverForName:NEVPNStatusDidChangeNotification
|
||||
object:manager.connection
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification *notification) {
|
||||
if (!self) return;
|
||||
NEVPNConnection *connection = notification.object;
|
||||
int rawStatus = (int)connection.status;
|
||||
QMetaObject::invokeMethod(
|
||||
self, [self, rawStatus]() { if (self) self->handleStatusChange(rawStatus); },
|
||||
Qt::QueuedConnection);
|
||||
}];
|
||||
|
||||
NEVPNStatus current = manager.connection.status;
|
||||
if (current == NEVPNStatusDisconnected || current == NEVPNStatusInvalid) {
|
||||
qInfo() << "[IKEv2-mac] preferences saved, connection is"
|
||||
<< vpnStatusName(current) << "- starting tunnel now";
|
||||
self->m_startWhenDisconnected = false;
|
||||
self->startTunnelNow();
|
||||
} else {
|
||||
qInfo() << "[IKEv2-mac] preferences saved, connection is"
|
||||
<< vpnStatusName(current) << "- waiting for it to disconnect first";
|
||||
self->m_startWhenDisconnected = true;
|
||||
[manager.connection stopVPNTunnel];
|
||||
}
|
||||
}];
|
||||
}];
|
||||
}];
|
||||
}];
|
||||
});
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
void Ikev2ProtocolMacos::stop()
|
||||
{
|
||||
qInfo() << "[IKEv2-mac] stop() requested";
|
||||
|
||||
stopHandshakeTimeoutTimer();
|
||||
m_startWhenDisconnected = false;
|
||||
|
||||
NEVPNManager *manager = [NEVPNManager sharedManager];
|
||||
NEVPNStatus status = manager.connection.status;
|
||||
qInfo() << "[IKEv2-mac] stop(): current NEVPNStatus =" << (int)status;
|
||||
|
||||
removeStatusObserver();
|
||||
|
||||
if (status != NEVPNStatusDisconnected && status != NEVPNStatusInvalid) {
|
||||
[manager.connection stopVPNTunnel];
|
||||
qInfo() << "[IKEv2-mac] stop(): stopVPNTunnel issued";
|
||||
}
|
||||
|
||||
disableVpnConfiguration();
|
||||
|
||||
setConnectionState(Vpn::ConnectionState::Disconnected);
|
||||
}
|
||||
|
||||
void Ikev2ProtocolMacos::startTunnelNow()
|
||||
{
|
||||
qInfo() << "[IKEv2-mac] startVPNTunnel (attempt" << (m_startRetries + 1) << ")";
|
||||
NEVPNManager *manager = [NEVPNManager sharedManager];
|
||||
NSError *startError = nil;
|
||||
[manager.connection startVPNTunnelAndReturnError:&startError];
|
||||
|
||||
if (!startError) {
|
||||
m_startRetries = 0;
|
||||
m_tunnelStarted = true;
|
||||
QMetaObject::invokeMethod(this, [this]() { startHandshakeTimeoutTimer(); }, Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_startRetries < MAX_START_RETRIES) {
|
||||
m_startRetries++;
|
||||
qWarning() << "[IKEv2-mac] startVPNTunnel failed, will retry (" << m_startRetries << "):"
|
||||
<< QString::fromNSString(startError.localizedDescription);
|
||||
QPointer<Ikev2ProtocolMacos> self = this;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.7 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
if (!self) return;
|
||||
[[NEVPNManager sharedManager] loadFromPreferencesWithCompletionHandler:^(NSError *reloadError) {
|
||||
if (!self) return;
|
||||
self->startTunnelNow();
|
||||
}];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
qCritical() << "[IKEv2-mac] starting the tunnel failed:"
|
||||
<< QString::fromNSString(startError.localizedDescription);
|
||||
disableVpnConfiguration();
|
||||
reportError(ErrorCode::IKEv2ConnectError);
|
||||
}
|
||||
|
||||
void Ikev2ProtocolMacos::handleStatusChange(int rawStatus)
|
||||
{
|
||||
NEVPNStatus vpnStatus = static_cast<NEVPNStatus>(rawStatus);
|
||||
const Vpn::ConnectionState currentState = connectionState();
|
||||
|
||||
qInfo() << "[IKEv2-mac] status ->" << vpnStatusName(rawStatus)
|
||||
<< "| uiState:" << textConnectionState()
|
||||
<< "| lastStatus:" << vpnStatusName(m_lastVpnStatus)
|
||||
<< "| waitingToStart:" << m_startWhenDisconnected;
|
||||
|
||||
if (m_startWhenDisconnected) {
|
||||
if (vpnStatus == NEVPNStatusDisconnecting) {
|
||||
return;
|
||||
}
|
||||
if (vpnStatus == NEVPNStatusDisconnected) {
|
||||
m_startWhenDisconnected = false;
|
||||
m_lastVpnStatus = NEVPNStatusDisconnected;
|
||||
QPointer<Ikev2ProtocolMacos> self = this;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self) self->startTunnelNow();
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (vpnStatus) {
|
||||
case NEVPNStatusConnecting:
|
||||
setConnectionState(Vpn::ConnectionState::Connecting);
|
||||
break;
|
||||
|
||||
case NEVPNStatusConnected:
|
||||
stopHandshakeTimeoutTimer();
|
||||
m_lastVpnStatus = vpnStatus;
|
||||
m_tunnelStarted = true;
|
||||
qInfo() << "[IKEv2-mac] tunnel established";
|
||||
setConnectionState(Vpn::ConnectionState::Connected);
|
||||
break;
|
||||
|
||||
case NEVPNStatusReasserting:
|
||||
m_lastVpnStatus = vpnStatus;
|
||||
setConnectionState(Vpn::ConnectionState::Reconnecting);
|
||||
break;
|
||||
|
||||
case NEVPNStatusDisconnecting:
|
||||
if (!m_tunnelStarted) {
|
||||
return;
|
||||
}
|
||||
stopHandshakeTimeoutTimer();
|
||||
setConnectionState(Vpn::ConnectionState::Disconnecting);
|
||||
break;
|
||||
|
||||
case NEVPNStatusDisconnected: {
|
||||
if (!m_tunnelStarted) {
|
||||
m_lastVpnStatus = vpnStatus;
|
||||
return;
|
||||
}
|
||||
|
||||
stopHandshakeTimeoutTimer();
|
||||
removeStatusObserver();
|
||||
|
||||
if (m_handshakeTimedOut) {
|
||||
qCritical() << "[IKEv2-mac] connection failed: handshake timed out";
|
||||
setLastError(ErrorCode::IKEv2TimeoutError);
|
||||
} else if (m_lastVpnStatus == NEVPNStatusInvalid && currentState == Vpn::ConnectionState::Connecting) {
|
||||
qCritical() << "[IKEv2-mac] connection failed: server rejected the configuration";
|
||||
setLastError(ErrorCode::IKEv2ConfigError);
|
||||
} else if (m_lastVpnStatus == NEVPNStatusReasserting
|
||||
&& (currentState == Vpn::ConnectionState::Connecting
|
||||
|| currentState == Vpn::ConnectionState::Connected)) {
|
||||
qWarning() << "[IKEv2-mac] connection lost (network unavailable)";
|
||||
setConnectionState(Vpn::ConnectionState::Disconnected);
|
||||
} else if (m_lastVpnStatus == NEVPNStatusConnected
|
||||
&& (currentState == Vpn::ConnectionState::Connecting
|
||||
|| currentState == Vpn::ConnectionState::Connected)) {
|
||||
qWarning() << "[IKEv2-mac] tunnel turned off outside the app (system settings)";
|
||||
setConnectionState(Vpn::ConnectionState::Disconnected);
|
||||
} else {
|
||||
setConnectionState(Vpn::ConnectionState::Disconnected);
|
||||
}
|
||||
m_lastVpnStatus = vpnStatus;
|
||||
|
||||
disableVpnConfiguration();
|
||||
break;
|
||||
}
|
||||
|
||||
case NEVPNStatusInvalid:
|
||||
if (!m_tunnelStarted) {
|
||||
m_lastVpnStatus = vpnStatus;
|
||||
return;
|
||||
}
|
||||
stopHandshakeTimeoutTimer();
|
||||
removeStatusObserver();
|
||||
qCritical() << "[IKEv2-mac] VPN profile became invalid";
|
||||
m_lastVpnStatus = vpnStatus;
|
||||
setLastError(ErrorCode::IKEv2ConfigError);
|
||||
disableVpnConfiguration();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Ikev2ProtocolMacos::startHandshakeTimeoutTimer()
|
||||
{
|
||||
stopHandshakeTimeoutTimer();
|
||||
|
||||
m_handshakeTimeoutTimer = new QTimer(this);
|
||||
m_handshakeTimeoutTimer->setSingleShot(true);
|
||||
m_handshakeTimeoutTimer->setInterval(HANDSHAKE_TIMEOUT_SEC * 1000);
|
||||
connect(m_handshakeTimeoutTimer, &QTimer::timeout, this, [this]() {
|
||||
if (connectionState() == Vpn::ConnectionState::Connecting) {
|
||||
m_handshakeTimedOut = true;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[NEVPNManager sharedManager].connection stopVPNTunnel];
|
||||
});
|
||||
}
|
||||
});
|
||||
m_handshakeTimeoutTimer->start();
|
||||
}
|
||||
|
||||
void Ikev2ProtocolMacos::stopHandshakeTimeoutTimer()
|
||||
{
|
||||
if (m_handshakeTimeoutTimer) {
|
||||
m_handshakeTimeoutTimer->stop();
|
||||
m_handshakeTimeoutTimer->deleteLater();
|
||||
m_handshakeTimeoutTimer = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,10 @@
|
||||
#include "ikev2VpnProtocolWindows.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_MACX) && !defined(MACOS_NE)
|
||||
#include "ikev2VpnProtocolMacos.h"
|
||||
#endif
|
||||
|
||||
VpnProtocol::VpnProtocol(const QJsonObject &configuration, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_connectionState(Vpn::ConnectionState::Unknown),
|
||||
@@ -111,6 +115,8 @@ VpnProtocol *VpnProtocol::factory(DockerContainer container, const QJsonObject &
|
||||
switch (container) {
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
case DockerContainer::Ipsec: return new Ikev2Protocol(configuration);
|
||||
#elif defined(Q_OS_MACX) && !defined(MACOS_NE)
|
||||
case DockerContainer::Ipsec: return new Ikev2ProtocolMacos(configuration);
|
||||
#endif
|
||||
#if defined(Q_OS_WINDOWS) || defined(Q_OS_MACX) and !defined MACOS_NE || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
|
||||
case DockerContainer::OpenVpn: return new OpenVpnProtocol(configuration);
|
||||
|
||||
@@ -265,7 +265,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
#elif defined(Q_OS_MAC)
|
||||
switch (c) {
|
||||
case DockerContainer::WireGuard: return true;
|
||||
case DockerContainer::Ipsec: return false;
|
||||
case DockerContainer::Ipsec: return true;
|
||||
default: return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,11 @@ namespace amnezia
|
||||
OpenVpnUnknownError = 701,
|
||||
OpenVpnTapAdapterError = 702,
|
||||
AddressPoolError = 703,
|
||||
IKEv2ConfigError = 710,
|
||||
IKEv2LoadError = 711,
|
||||
IKEv2SaveError = 712,
|
||||
IKEv2ConnectError = 713,
|
||||
IKEv2TimeoutError = 714,
|
||||
|
||||
// 3rd party utils errors
|
||||
OpenSslFailed = 800,
|
||||
|
||||
@@ -64,6 +64,11 @@ QString errorString(ErrorCode code) {
|
||||
case (ErrorCode::OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break;
|
||||
case (ErrorCode::OpenVpnTapAdapterError): errorMessage = QObject::tr("Can't setup OpenVPN TAP network adapter"); break;
|
||||
case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break;
|
||||
case (ErrorCode::IKEv2ConfigError): errorMessage = QObject::tr("Can't connect: invalid IKEv2 configuration"); break;
|
||||
case (ErrorCode::IKEv2LoadError): errorMessage = QObject::tr("Can't connect: failed to load IKEv2 VPN preferences"); break;
|
||||
case (ErrorCode::IKEv2SaveError): errorMessage = QObject::tr("Can't connect: failed to save IKEv2 VPN preferences"); break;
|
||||
case (ErrorCode::IKEv2ConnectError): errorMessage = QObject::tr("Can't connect: failed to start IKEv2 connection"); break;
|
||||
case (ErrorCode::IKEv2TimeoutError): errorMessage = QObject::tr("Can't connect: IKEv2 connection timeout"); break;
|
||||
|
||||
case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
|
||||
case (ErrorCode::ImportBackupFileUseRestoreInstead): errorMessage = QObject::tr("Backup files cannot be imported here. Use 'Restore from backup' instead."); break;
|
||||
|
||||
@@ -290,6 +290,11 @@ QString Utils::tun2socksPath()
|
||||
return Utils::executable("tun2socks", true);
|
||||
}
|
||||
|
||||
QString Utils::securityPath()
|
||||
{
|
||||
return "/usr/bin/security";
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
// Inspired from http://stackoverflow.com/a/15281070/1529139
|
||||
// and http://stackoverflow.com/q/40059902/1529139
|
||||
|
||||
@@ -34,6 +34,7 @@ public:
|
||||
static QString wireguardExecPath();
|
||||
static QString certUtilPath();
|
||||
static QString tun2socksPath();
|
||||
static QString securityPath();
|
||||
|
||||
static void logException(const std::exception &e);
|
||||
static void logException(const std::exception_ptr &eptr = std::current_exception());
|
||||
|
||||
@@ -15,7 +15,8 @@ enum PermittedProcess {
|
||||
OpenVPN,
|
||||
Wireguard,
|
||||
Tun2Socks,
|
||||
CertUtil
|
||||
CertUtil,
|
||||
Security
|
||||
};
|
||||
|
||||
inline QString permittedProcessPath(PermittedProcess pid)
|
||||
@@ -29,6 +30,8 @@ inline QString permittedProcessPath(PermittedProcess pid)
|
||||
return Utils::certUtilPath();
|
||||
case PermittedProcess::Tun2Socks:
|
||||
return Utils::tun2socksPath();
|
||||
case PermittedProcess::Security:
|
||||
return Utils::securityPath();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user