Files
amnezia-client/client/platforms/ios/ios_controller.mm
Yaroslav 9fdcf5ab13 feat: macos with network extension Implementation (#1468)
* There's a common issue of building iOS apps on Qt 6.8 because of new introduced ffmpeg dependency in multimedia Qt package
ref: https://community.esri.com/t5/qt-maps-sdk-questions/build-failure-on-ios-with-qt-6-8/m-p/1548701#M5339

* Cmake related changes

* Source code changes

* Various entitlements

* Ci-cd config update

* Resources changes

* Submodules updated

* Remove me

* QtWidget exclusion omitted

* Distribution errors fixed

* Outdated files deleted

* macos_ne cmake fixed

* fix: update provisioning profile specifiers for macOS network extension

* fix: update provisioning profile specifiers and code sign flags for macOS build

* Revert me
(temporary 3rd-build commit pointer)

* fix: Welcome screen fix

* fix: ci/cd hanging forever fix

* fix: Fixed error popup on macos on file save

* refactor: rename networkextension target to AmneziaVPNNetworkExtension in macos build configuration

* feat: add autostart support for Mac App Store builds on macOS

Fixes: QA-8

* feat: add debug logging to Autostart functionality on macOS

* Revert "feat: add autostart support for Mac App Store builds on macOS"

This reverts commit 3bd25656fb.

* feat: add platform-specific close window behavior for macOS App Store build with Network Extension

Closes: QA-12

* When the application starts with "Start minimized" enabled on macOS (especially the
sandboxed App-Store build compiled with MACOS_NE), fully hiding the window prevents it
from being restored by clicking the Dock icon. The proper behaviour is to start the
window in the *minimized* state instead. That way the window is still part of the
window list and the system automatically brings it back when the user clicks the Dock
icon, replicating the native experience.

On the other platforms we keep the old behaviour (hide the window completely and rely
on the tray icon), therefore we switch at runtime by checking the current OS.

Closes: QA-7

Closes: QA-8

* Revert "When the application starts with "Start minimized" enabled on macOS (especially the"

This reverts commit 7b0d17987c.

* feat: MACOS_NE systray menu support

* feat: add macOS notification handler and install event filter on main window

* feat: implement custom close behavior for Amnezia application on different platforms

* fix: update provisioning profile specifiers for macos builds

* fix: Fatal error in logs

CLI-216

* fix: disabled unavailable on macos ne service logs

* fix: dock icon now hides only when window is closed; menubar icon shows always

Initial state of the docker icon to be presented follows "Start minimized" setting in app settings.

* temp-fix: temporary disable all OpenVPN options of VPN on MACOS_NE since it's not working yet.

* fix: build script updated

* feat: add macOS NE build workflow to GitHub Actions

* fix: Not working Auto start toggle is hidden

* fix: Log spamming during xray connection fixed

* 3rd-prebuild points to commit that stores macos_ne universal binaries.

* fix: missing native dependency on linking stage fixed

* chore: update link to submodule

---------

Co-authored-by: vladimir.kuznetsov <nethiuswork@gmail.com>
2025-08-10 11:12:19 +08:00

900 lines
38 KiB
Plaintext

#include "ios_controller.h"
#include <QDebug>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QThread>
#include <QEventLoop>
#include "../protocols/vpnprotocol.h"
#import "ios_controller_wrapper.h"
const char* Action::start = "start";
const char* Action::restart = "restart";
const char* Action::stop = "stop";
const char* Action::getTunnelId = "getTunnelId";
const char* Action::getStatus = "status";
const char* MessageKey::action = "action";
const char* MessageKey::tunnelId = "tunnelId";
const char* MessageKey::config = "config";
const char* MessageKey::errorCode = "errorCode";
const char* MessageKey::host = "host";
const char* MessageKey::port = "port";
const char* MessageKey::isOnDemand = "is-on-demand";
const char* MessageKey::SplitTunnelType = "SplitTunnelType";
const char* MessageKey::SplitTunnelSites = "SplitTunnelSites";
#if !MACOS_NE
static UIViewController* getViewController() {
NSArray *windows = [[UIApplication sharedApplication]windows];
for (UIWindow *window in windows) {
if (window.isKeyWindow) {
return window.rootViewController;
}
}
return nil;
}
#endif
Vpn::ConnectionState iosStatusToState(NEVPNStatus status) {
switch (status) {
case NEVPNStatusInvalid:
return Vpn::ConnectionState::Unknown;
case NEVPNStatusDisconnected:
return Vpn::ConnectionState::Disconnected;
case NEVPNStatusConnecting:
return Vpn::ConnectionState::Connecting;
case NEVPNStatusConnected:
return Vpn::ConnectionState::Connected;
case NEVPNStatusReasserting:
return Vpn::ConnectionState::Connecting;
case NEVPNStatusDisconnecting:
return Vpn::ConnectionState::Disconnecting;
default:
return Vpn::ConnectionState::Unknown;
}
}
namespace {
IosController* s_instance = nullptr;
}
IosController::IosController() : QObject()
{
s_instance = this;
m_iosControllerWrapper = [[IosControllerWrapper alloc] initWithCppController:this];
[[NSNotificationCenter defaultCenter]
removeObserver: (__bridge NSObject *)m_iosControllerWrapper];
[[NSNotificationCenter defaultCenter]
addObserver: (__bridge NSObject *)m_iosControllerWrapper selector:@selector(vpnStatusDidChange:) name:NEVPNStatusDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter]
addObserver: (__bridge NSObject *)m_iosControllerWrapper selector:@selector(vpnConfigurationDidChange:) name:NEVPNConfigurationChangeNotification object:nil];
}
IosController* IosController::Instance() {
if (!s_instance) {
s_instance = new IosController();
}
return s_instance;
}
bool IosController::initialize()
{
__block bool ok = true;
[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
@try {
if (error) {
qDebug() << "IosController::initialize : Error:" << [error.localizedDescription UTF8String];
emit connectionStateChanged(Vpn::ConnectionState::Error);
ok = false;
return;
}
NSInteger managerCount = managers.count;
qDebug() << "IosController::initialize : We have received managers:" << (long)managerCount;
for (NETunnelProviderManager *manager in managers) {
qDebug() << "IosController::initialize : VPNC: " << manager.localizedDescription;
if (manager.connection.status == NEVPNStatusConnected) {
m_currentTunnel = manager;
qDebug() << "IosController::initialize : VPN already connected with" << manager.localizedDescription;
emit connectionStateChanged(Vpn::ConnectionState::Connected);
break;
// TODO: show connected state
}
}
}
@catch (NSException *exception) {
qDebug() << "IosController::setTunnel : exception" << QString::fromNSString(exception.reason);
ok = false;
}
}];
return ok;
}
bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configuration)
{
m_proto = proto;
m_rawConfig = configuration;
m_serverAddress = configuration.value(config_key::hostName).toString().toNSString();
QString tunnelName;
if (configuration.value(config_key::description).toString().isEmpty()) {
tunnelName = QString("%1 %2")
.arg(configuration.value(config_key::hostName).toString())
.arg(ProtocolProps::protoToString(proto));
}
else {
tunnelName = QString("%1 (%2) %3")
.arg(configuration.value(config_key::description).toString())
.arg(configuration.value(config_key::hostName).toString())
.arg(ProtocolProps::protoToString(proto));
}
qDebug() << "IosController::connectVpn" << tunnelName;
m_currentTunnel = nullptr;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block bool ok = true;
__block bool isNewTunnelCreated = false;
[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
@try {
if (error) {
qDebug() << "IosController::connectVpn : VPNC: loadAllFromPreferences error:" << [error.localizedDescription UTF8String];
emit connectionStateChanged(Vpn::ConnectionState::Error);
ok = false;
return;
}
NSInteger managerCount = managers.count;
qDebug() << "IosController::connectVpn : We have received managers:" << (long)managerCount;
for (NETunnelProviderManager *manager in managers) {
if ([manager.localizedDescription isEqualToString:tunnelName.toNSString()]) {
m_currentTunnel = manager;
qDebug() << "IosController::connectVpn : Using existing tunnel:" << manager.localizedDescription;
if (manager.connection.status == NEVPNStatusConnected) {
emit connectionStateChanged(Vpn::ConnectionState::Connected);
return;
}
break;
}
}
if (!m_currentTunnel) {
isNewTunnelCreated = true;
m_currentTunnel = [[NETunnelProviderManager alloc] init];
m_currentTunnel.localizedDescription = [NSString stringWithUTF8String:tunnelName.toStdString().c_str()];
qDebug() << "IosController::connectVpn : Creating new tunnel" << m_currentTunnel.localizedDescription;
}
}
@catch (NSException *exception) {
qDebug() << "IosController::connectVpn : exception" << QString::fromNSString(exception.reason);
ok = false;
m_currentTunnel = nullptr;
}
@finally {
dispatch_semaphore_signal(semaphore);
}
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (!ok) return false;
[[NSNotificationCenter defaultCenter]
removeObserver:(__bridge NSObject *)m_iosControllerWrapper];
[[NSNotificationCenter defaultCenter]
addObserver:(__bridge NSObject *)m_iosControllerWrapper
selector:@selector(vpnStatusDidChange:)
name:NEVPNStatusDidChangeNotification
object:m_currentTunnel.connection];
if (proto == amnezia::Proto::OpenVpn) {
return setupOpenVPN();
}
if (proto == amnezia::Proto::Cloak) {
return setupCloak();
}
if (proto == amnezia::Proto::WireGuard) {
return setupWireGuard();
}
if (proto == amnezia::Proto::Awg) {
return setupAwg();
}
if (proto == amnezia::Proto::Xray) {
return setupXray();
}
if (proto == amnezia::Proto::SSXray) {
return setupSSXray();
}
return false;
}
void IosController::disconnectVpn()
{
if (!m_currentTunnel) {
return;
}
if ([m_currentTunnel.connection isKindOfClass:[NETunnelProviderSession class]]) {
[(NETunnelProviderSession *)m_currentTunnel.connection stopTunnel];
}
}
void IosController::checkStatus()
{
NSString *actionKey = [NSString stringWithUTF8String:MessageKey::action];
NSString *actionValue = [NSString stringWithUTF8String:Action::getStatus];
NSString *tunnelIdKey = [NSString stringWithUTF8String:MessageKey::tunnelId];
NSString *tunnelIdValue = !m_tunnelId.isEmpty() ? m_tunnelId.toNSString() : @"";
NSDictionary* message = @{actionKey: actionValue, tunnelIdKey: tunnelIdValue};
sendVpnExtensionMessage(message, [&](NSDictionary* response){
uint64_t txBytes = [response[@"tx_bytes"] intValue];
uint64_t rxBytes = [response[@"rx_bytes"] intValue];
uint64_t last_handshake_time_sec = 0;
#if !MACOS_NE
if (response[@"last_handshake_time_sec"] && ![response[@"last_handshake_time_sec"] isKindOfClass:[NSNull class]]) {
last_handshake_time_sec = [response[@"last_handshake_time_sec"] intValue];
} else {
qDebug() << "Key last_handshake_time_sec is missing or null";
}
if (last_handshake_time_sec < 0) {
disconnectVpn();
qDebug() << "Invalid handshake time, disconnecting VPN.";
}
#endif
emit bytesChanged(rxBytes - m_rxBytes, txBytes - m_txBytes);
m_rxBytes = rxBytes;
m_txBytes = txBytes;
});
}
void IosController::vpnStatusDidChange(void *pNotification)
{
NETunnelProviderSession *session = (NETunnelProviderSession *)pNotification;
if (session /* && session == TunnelManager.session */ ) {
qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session;
if (session.status == NEVPNStatusDisconnected) {
if (@available(iOS 16.0, *)) {
[session fetchLastDisconnectErrorWithCompletionHandler:^(NSError * _Nullable error) {
if (error != nil) {
qDebug() << "Disconnect error" << error.domain << error.code << error.localizedDescription;
if ([error.domain isEqualToString:NEVPNConnectionErrorDomain]) {
switch (error.code) {
case NEVPNConnectionErrorOverslept:
qDebug() << "Disconnect error info" << "The VPN connection was terminated because the system slept for an extended period of time.";
break;
case NEVPNConnectionErrorNoNetworkAvailable:
qDebug() << "Disconnect error info" << "The VPN connection could not be established because the system is not connected to a network.";
break;
case NEVPNConnectionErrorUnrecoverableNetworkChange:
qDebug() << "Disconnect error info" << "The VPN connection was terminated because the network conditions changed in such a way that the VPN connection could not be maintained.";
break;
case NEVPNConnectionErrorConfigurationFailed:
qDebug() << "Disconnect error info" << "The VPN connection could not be established because the configuration is invalid. ";
break;
case NEVPNConnectionErrorServerAddressResolutionFailed:
qDebug() << "Disconnect error info" << "The address of the VPN server could not be determined.";
break;
case NEVPNConnectionErrorServerNotResponding:
qDebug() << "Disconnect error info" << "Network communication with the VPN server has failed.";
break;
case NEVPNConnectionErrorServerDead:
qDebug() << "Disconnect error info" << "The VPN server is no longer functioning.";
break;
case NEVPNConnectionErrorAuthenticationFailed:
qDebug() << "Disconnect error info" << "The user credentials were rejected by the VPN server.";
break;
case NEVPNConnectionErrorClientCertificateInvalid:
qDebug() << "Disconnect error info" << "The client certificate is invalid.";
break;
case NEVPNConnectionErrorClientCertificateNotYetValid:
qDebug() << "Disconnect error info" << "The client certificate will not be valid until some future point in time.";
break;
case NEVPNConnectionErrorClientCertificateExpired:
qDebug() << "Disconnect error info" << "The validity period of the client certificate has passed.";
break;
case NEVPNConnectionErrorPluginFailed:
qDebug() << "Disconnect error info" << "The VPN plugin died unexpectedly.";
break;
case NEVPNConnectionErrorConfigurationNotFound:
qDebug() << "Disconnect error info" << "The VPN configuration could not be found.";
break;
case NEVPNConnectionErrorPluginDisabled:
qDebug() << "Disconnect error info" << "The VPN plugin could not be found or needed to be updated.";
break;
case NEVPNConnectionErrorNegotiationFailed:
qDebug() << "Disconnect error info" << "The VPN protocol negotiation failed.";
break;
case NEVPNConnectionErrorServerDisconnected:
qDebug() << "Disconnect error info" << "The VPN server terminated the connection.";
break;
case NEVPNConnectionErrorServerCertificateInvalid:
qDebug() << "Disconnect error info" << "The server certificate is invalid.";
break;
case NEVPNConnectionErrorServerCertificateNotYetValid:
qDebug() << "Disconnect error info" << "The server certificate will not be valid until some future point in time.";
break;
case NEVPNConnectionErrorServerCertificateExpired:
qDebug() << "Disconnect error info" << "The validity period of the server certificate has passed.";
break;
default:
qDebug() << "Disconnect error info" << "Unknown code.";
break;
}
}
NSError *underlyingError = error.userInfo[@"NSUnderlyingError"];
if (underlyingError != nil) {
qDebug() << "Disconnect underlying error" << underlyingError.domain << underlyingError.code << underlyingError.localizedDescription;
if ([underlyingError.domain isEqualToString:@"NEAgentErrorDomain"]) {
switch (underlyingError.code) {
case 1:
qDebug() << "Disconnect underlying error" << "General. Use sysdiagnose.";
break;
case 2:
qDebug() << "Disconnect underlying error" << "Plug-in unavailable. Use sysdiagnose.";
break;
default:
qDebug() << "Disconnect underlying error" << "Unknown code. Use sysdiagnose.";
break;
}
}
}
}
}];
} else {
qDebug() << "Disconnect error is unavailable on iOS < 16.0";
}
}
emit connectionStateChanged(iosStatusToState(session.status));
}
}
void IosController::vpnConfigurationDidChange(void *pNotification)
{
qDebug() << "IosController::vpnConfigurationDidChange" << pNotification;
}
bool IosController::setupOpenVPN()
{
QJsonObject ovpn = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::OpenVpn)].toObject();
QString ovpnConfig = ovpn[config_key::config].toString();
QJsonObject openVPNConfig {};
openVPNConfig.insert(config_key::config, ovpnConfig);
if (ovpn.contains(config_key::mtu)) {
openVPNConfig.insert(config_key::mtu, ovpn[config_key::mtu]);
} else {
openVPNConfig.insert(config_key::mtu, protocols::openvpn::defaultMtu);
}
openVPNConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]);
QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray();
for(int index = 0; index < splitTunnelSites.count(); index++) {
splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" ");
}
openVPNConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
QJsonDocument openVPNConfigDoc(openVPNConfig);
QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact));
return startOpenVPN(openVPNConfigStr);
}
bool IosController::setupCloak()
{
m_serverAddress = @"127.0.0.1";
QJsonObject ovpn = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::OpenVpn)].toObject();
QString ovpnConfig = ovpn[config_key::config].toString();
QJsonObject cloak = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Cloak)].toObject();
cloak["NumConn"] = 1;
if (cloak.contains("remote")) {
cloak["RemoteHost"] = cloak["remote"].toString();
}
if (cloak.contains("port")) {
cloak["RemotePort"] = cloak["port"].toString();
}
cloak.remove("remote");
cloak.remove("port");
cloak.remove("transport_proto");
QJsonObject jsonObject {};
foreach(const QString& key, cloak.keys()) {
if(key == "NumConn" or key == "StreamTimeout"){
jsonObject.insert(key, cloak.value(key).toInt());
}else{
jsonObject.insert(key, cloak.value(key).toString());
}
}
QJsonDocument doc(jsonObject);
QString strJson(doc.toJson(QJsonDocument::Compact));
QString cloakBase64 = strJson.toUtf8().toBase64();
ovpnConfig.append("\n<cloak>\n");
ovpnConfig.append(cloakBase64);
ovpnConfig.append("\n</cloak>\n");
QJsonObject openVPNConfig {};
openVPNConfig.insert(config_key::config, ovpnConfig);
if (ovpn.contains(config_key::mtu)) {
openVPNConfig.insert(config_key::mtu, ovpn[config_key::mtu]);
} else {
openVPNConfig.insert(config_key::mtu, protocols::openvpn::defaultMtu);
}
openVPNConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]);
QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray();
for(int index = 0; index < splitTunnelSites.count(); index++) {
splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" ");
}
openVPNConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
QJsonDocument openVPNConfigDoc(openVPNConfig);
QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact));
return startOpenVPN(openVPNConfigStr);
}
bool IosController::setupWireGuard()
{
QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::WireGuard)].toObject();
QJsonObject wgConfig {};
wgConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1]);
wgConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2]);
if (config.contains(config_key::mtu)) {
wgConfig.insert(config_key::mtu, config[config_key::mtu]);
} else {
wgConfig.insert(config_key::mtu, protocols::wireguard::defaultMtu);
}
wgConfig.insert(config_key::hostName, config[config_key::hostName]);
wgConfig.insert(config_key::port, config[config_key::port]);
wgConfig.insert(config_key::client_ip, config[config_key::client_ip]);
wgConfig.insert(config_key::client_priv_key, config[config_key::client_priv_key]);
wgConfig.insert(config_key::server_pub_key, config[config_key::server_pub_key]);
wgConfig.insert(config_key::psk_key, config[config_key::psk_key]);
wgConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]);
QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray();
for(int index = 0; index < splitTunnelSites.count(); index++) {
splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" ");
}
wgConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
if (config.contains(config_key::allowed_ips) && config[config_key::allowed_ips].isArray()) {
wgConfig.insert(config_key::allowed_ips, config[config_key::allowed_ips]);
} else {
QJsonArray allowed_ips { "0.0.0.0/0", "::/0" };
wgConfig.insert(config_key::allowed_ips, allowed_ips);
}
if (config.contains(config_key::persistent_keep_alive)) {
wgConfig.insert(config_key::persistent_keep_alive, config[config_key::persistent_keep_alive]);
} else {
wgConfig.insert(config_key::persistent_keep_alive, "25");
}
if (config.contains(config_key::isObfuscationEnabled) && config.value(config_key::isObfuscationEnabled).toBool()) {
wgConfig.insert(config_key::initPacketMagicHeader, config[config_key::initPacketMagicHeader]);
wgConfig.insert(config_key::responsePacketMagicHeader, config[config_key::responsePacketMagicHeader]);
wgConfig.insert(config_key::underloadPacketMagicHeader, config[config_key::underloadPacketMagicHeader]);
wgConfig.insert(config_key::transportPacketMagicHeader, config[config_key::transportPacketMagicHeader]);
wgConfig.insert(config_key::initPacketJunkSize, config[config_key::initPacketJunkSize]);
wgConfig.insert(config_key::responsePacketJunkSize, config[config_key::responsePacketJunkSize]);
wgConfig.insert(config_key::cookieReplyPacketJunkSize, config[config_key::cookieReplyPacketJunkSize]);
wgConfig.insert(config_key::transportPacketJunkSize, config[config_key::transportPacketJunkSize]);
wgConfig.insert(config_key::junkPacketCount, config[config_key::junkPacketCount]);
wgConfig.insert(config_key::junkPacketMinSize, config[config_key::junkPacketMinSize]);
wgConfig.insert(config_key::junkPacketMaxSize, config[config_key::junkPacketMaxSize]);
}
QJsonDocument wgConfigDoc(wgConfig);
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
return startWireGuard(wgConfigDocStr);
}
bool IosController::setupXray()
{
QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Xray)].toObject();
QJsonDocument xrayConfigDoc(config);
QString xrayConfigStr(xrayConfigDoc.toJson(QJsonDocument::Compact));
QJsonObject finalConfig;
finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString());
finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2].toString());
finalConfig.insert(config_key::config, xrayConfigStr);
QJsonDocument finalConfigDoc(finalConfig);
QString finalConfigStr(finalConfigDoc.toJson(QJsonDocument::Compact));
return startXray(finalConfigStr);
}
bool IosController::setupSSXray()
{
QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::SSXray)].toObject();
QJsonDocument ssXrayConfigDoc(config);
QString ssXrayConfigStr(ssXrayConfigDoc.toJson(QJsonDocument::Compact));
QJsonObject finalConfig;
finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1]);
finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2]);
finalConfig.insert(config_key::config, ssXrayConfigStr);
QJsonDocument finalConfigDoc(finalConfig);
QString finalConfigStr(finalConfigDoc.toJson(QJsonDocument::Compact));
return startXray(finalConfigStr);
}
bool IosController::setupAwg()
{
QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Awg)].toObject();
QJsonObject wgConfig {};
wgConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1]);
wgConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2]);
if (config.contains(config_key::mtu)) {
wgConfig.insert(config_key::mtu, config[config_key::mtu]);
} else {
wgConfig.insert(config_key::mtu, protocols::awg::defaultMtu);
}
wgConfig.insert(config_key::hostName, config[config_key::hostName]);
wgConfig.insert(config_key::port, config[config_key::port]);
wgConfig.insert(config_key::client_ip, config[config_key::client_ip]);
wgConfig.insert(config_key::client_priv_key, config[config_key::client_priv_key]);
wgConfig.insert(config_key::server_pub_key, config[config_key::server_pub_key]);
wgConfig.insert(config_key::psk_key, config[config_key::psk_key]);
wgConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]);
QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray();
for(int index = 0; index < splitTunnelSites.count(); index++) {
splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" ");
}
wgConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
if (config.contains(config_key::allowed_ips) && config[config_key::allowed_ips].isArray()) {
wgConfig.insert(config_key::allowed_ips, config[config_key::allowed_ips]);
} else {
QJsonArray allowed_ips { "0.0.0.0/0", "::/0" };
wgConfig.insert(config_key::allowed_ips, allowed_ips);
}
if (config.contains(config_key::persistent_keep_alive)) {
wgConfig.insert(config_key::persistent_keep_alive, config[config_key::persistent_keep_alive]);
} else {
wgConfig.insert(config_key::persistent_keep_alive, "25");
}
wgConfig.insert(config_key::initPacketMagicHeader, config[config_key::initPacketMagicHeader]);
wgConfig.insert(config_key::responsePacketMagicHeader, config[config_key::responsePacketMagicHeader]);
wgConfig.insert(config_key::underloadPacketMagicHeader, config[config_key::underloadPacketMagicHeader]);
wgConfig.insert(config_key::transportPacketMagicHeader, config[config_key::transportPacketMagicHeader]);
wgConfig.insert(config_key::initPacketJunkSize, config[config_key::initPacketJunkSize]);
wgConfig.insert(config_key::responsePacketJunkSize, config[config_key::responsePacketJunkSize]);
wgConfig.insert(config_key::cookieReplyPacketJunkSize, config[config_key::cookieReplyPacketJunkSize]);
wgConfig.insert(config_key::transportPacketJunkSize, config[config_key::transportPacketJunkSize]);
wgConfig.insert(config_key::junkPacketCount, config[config_key::junkPacketCount]);
wgConfig.insert(config_key::junkPacketMinSize, config[config_key::junkPacketMinSize]);
wgConfig.insert(config_key::junkPacketMaxSize, config[config_key::junkPacketMaxSize]);
wgConfig.insert(config_key::specialJunk1, config[config_key::specialJunk1]);
wgConfig.insert(config_key::specialJunk2, config[config_key::specialJunk2]);
wgConfig.insert(config_key::specialJunk3, config[config_key::specialJunk3]);
wgConfig.insert(config_key::specialJunk4, config[config_key::specialJunk4]);
wgConfig.insert(config_key::specialJunk5, config[config_key::specialJunk5]);
wgConfig.insert(config_key::controlledJunk1, config[config_key::controlledJunk1]);
wgConfig.insert(config_key::controlledJunk2, config[config_key::controlledJunk2]);
wgConfig.insert(config_key::controlledJunk3, config[config_key::controlledJunk3]);
wgConfig.insert(config_key::specialHandshakeTimeout, config[config_key::specialHandshakeTimeout]);
QJsonDocument wgConfigDoc(wgConfig);
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
return startWireGuard(wgConfigDocStr);
}
bool IosController::startOpenVPN(const QString &config)
{
qDebug() << "IosController::startOpenVPN";
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
tunnelProtocol.providerConfiguration = @{@"ovpn": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
tunnelProtocol.serverAddress = m_serverAddress;
m_currentTunnel.protocolConfiguration = tunnelProtocol;
startTunnel();
}
bool IosController::startWireGuard(const QString &config)
{
qDebug() << "IosController::startWireGuard";
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
tunnelProtocol.providerConfiguration = @{@"wireguard": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
tunnelProtocol.serverAddress = m_serverAddress;
m_currentTunnel.protocolConfiguration = tunnelProtocol;
startTunnel();
}
bool IosController::startXray(const QString &config)
{
qDebug() << "IosController::startXray";
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
tunnelProtocol.providerConfiguration = @{@"xray": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
tunnelProtocol.serverAddress = m_serverAddress;
m_currentTunnel.protocolConfiguration = tunnelProtocol;
startTunnel();
}
void IosController::startTunnel()
{
NSString *protocolName = @"Unknown";
NETunnelProviderProtocol *tunnelProtocol = (NETunnelProviderProtocol *)m_currentTunnel.protocolConfiguration;
if (tunnelProtocol.providerConfiguration[@"wireguard"] != nil) {
protocolName = @"WireGuard";
} else if (tunnelProtocol.providerConfiguration[@"ovpn"] != nil) {
protocolName = @"OpenVPN";
}
m_rxBytes = 0;
m_txBytes = 0;
[m_currentTunnel setEnabled:YES];
[m_currentTunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (saveError) {
qDebug().nospace() << "IosController::startTunnel" << protocolName << ": Connect " << protocolName << " Tunnel Save Error" << saveError.localizedDescription.UTF8String;
emit connectionStateChanged(Vpn::ConnectionState::Error);
return;
}
[m_currentTunnel loadFromPreferencesWithCompletionHandler:^(NSError *loadError) {
if (loadError) {
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << ": Connect " << protocolName << " Tunnel Load Error" << loadError.localizedDescription.UTF8String;
emit connectionStateChanged(Vpn::ConnectionState::Error);
return;
}
NSError *startError = nil;
qDebug() << iosStatusToState(m_currentTunnel.connection.status);
BOOL started = [m_currentTunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError];
if (!started || startError) {
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Connect " << protocolName << " Tunnel Start Error"
<< (startError ? startError.localizedDescription.UTF8String : "");
emit connectionStateChanged(Vpn::ConnectionState::Error);
} else {
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Starting the tunnel succeeded";
}
}];
});
}];
}
bool IosController::isOurManager(NETunnelProviderManager* manager) {
NETunnelProviderProtocol* tunnelProto = (NETunnelProviderProtocol*)manager.protocolConfiguration;
if (!tunnelProto) {
qDebug() << "Ignoring manager because the proto is invalid";
return false;
}
if (!tunnelProto.providerBundleIdentifier) {
qDebug() << "Ignoring manager because the bundle identifier is null";
return false;
}
if (![tunnelProto.providerBundleIdentifier isEqualToString:[NSString stringWithUTF8String:VPN_NE_BUNDLEID]]) {
qDebug() << "Ignoring manager because the bundle identifier doesn't match";
return false;
}
qDebug() << "Found the manager with the correct bundle identifier:" << QString::fromNSString(tunnelProto.providerBundleIdentifier);
return true;
}
void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function<void(NSDictionary*)> callback)
{
if (!m_currentTunnel) {
qDebug() << "Cannot set an extension callback without a tunnel manager";
return;
}
NSError *error = nil;
NSData *data = [NSJSONSerialization dataWithJSONObject:message options:0 error:&error];
if (!data || error) {
qDebug() << "Failed to serialize message to VpnExtension as JSON. Error:"
<< [error.localizedDescription UTF8String];
return;
}
void (^completionHandler)(NSData *) = ^(NSData *responseData) {
if (!responseData) {
if (callback) callback(nil);
return;
}
NSError *deserializeError = nil;
NSDictionary *response = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&deserializeError];
if (response && [response isKindOfClass:[NSDictionary class]]) {
if (callback) callback(response);
return;
} else if (deserializeError) {
qDebug() << "Failed to deserialize the VpnExtension response";
}
if (callback) callback(nil);
};
NETunnelProviderSession *session = (NETunnelProviderSession *)m_currentTunnel.connection;
NSError *sendError = nil;
if ([session respondsToSelector:@selector(sendProviderMessage:returnError:responseHandler:)]) {
[session sendProviderMessage:data returnError:&sendError responseHandler:completionHandler];
} else {
qDebug() << "Method sendProviderMessage:responseHandler:error: does not exist";
}
if (sendError) {
qDebug() << "Failed to send message to VpnExtension. Error:"
<< [sendError.localizedDescription UTF8String];
}
}
bool IosController::shareText(const QStringList& filesToSend) {
NSMutableArray *sharingItems = [NSMutableArray new];
for (int i = 0; i < filesToSend.size(); i++) {
NSURL *logFileUrl = [[NSURL alloc] initFileURLWithPath:filesToSend[i].toNSString()];
[sharingItems addObject:logFileUrl];
}
#if !MACOS_NE
UIViewController *qtController = getViewController();
if (!qtController) return;
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil];
#endif
__block bool isAccepted = false;
#if !MACOS_NE
[activityController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
isAccepted = completed;
emit finished();
}];
[qtController presentViewController:activityController animated:YES completion:nil];
UIPopoverPresentationController *popController = activityController.popoverPresentationController;
if (popController) {
popController.sourceView = qtController.view;
popController.sourceRect = CGRectMake(100, 100, 100, 100);
}
#endif
QEventLoop wait;
QObject::connect(this, &IosController::finished, &wait, &QEventLoop::quit);
wait.exec();
return isAccepted;
}
QString IosController::openFile() {
#if !MACOS_NE
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen];
DocumentPickerDelegate *documentPickerDelegate = [[DocumentPickerDelegate alloc] init];
documentPicker.delegate = documentPickerDelegate;
UIViewController *qtController = getViewController();
if (!qtController) return;
[qtController presentViewController:documentPicker animated:YES completion:nil];
#endif
__block QString filePath;
#if !MACOS_NE
documentPickerDelegate.documentPickerClosedCallback = ^(NSString *path) {
if (path) {
filePath = QString::fromUtf8(path.UTF8String);
} else {
filePath = QString();
}
emit finished();
};
#endif
QEventLoop wait;
QObject::connect(this, &IosController::finished, &wait, &QEventLoop::quit);
wait.exec();
return filePath;
}
void IosController::requestInetAccess() {
NSURL *url = [NSURL URLWithString:@"http://captive.apple.com/generate_204"];
if (!url) {
qDebug() << "IosController::requestInetAccess URL error";
return;
}
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
qDebug() << "IosController::requestInetAccess error:" << error.localizedDescription;
} else {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
QString responseBody = QString::fromUtf8((const char*)data.bytes, data.length);
}
}];
[task resume];
}