diff --git a/.gitmodules b/.gitmodules index dda559a180..0229d34922 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,7 +4,7 @@ shallow = true [submodule "3rdparty/wireguard-apple"] path = 3rdparty/wireguard-apple - url = https://github.com/WireGuard/wireguard-apple + url = https://github.com/mozilla/wireguard-apple.git shallow = true [submodule "3rdparty/openSSL"] path = 3rdparty/openSSL diff --git a/3rdparty/wireguard-apple b/3rdparty/wireguard-apple index 12b095470a..3ba0b4cc2e 160000 --- a/3rdparty/wireguard-apple +++ b/3rdparty/wireguard-apple @@ -1 +1 @@ -Subproject commit 12b095470ad29ecea7436088f6e5fa701e6445a6 +Subproject commit 3ba0b4cc2e187c15c0416378e71e93a5d6252167 diff --git a/docs/Building/ios.md b/docs/Building/ios.md index 61d97cccf8..215ab6eb63 100644 --- a/docs/Building/ios.md +++ b/docs/Building/ios.md @@ -56,4 +56,5 @@ Once Xcode has opened the project, select the `mozillavpn` target and start the Tips: * If you can't see a simulator target in the Xcode interface, look at Product -> Destination -> Destination Architectures -> Show Both -* Due to lack of low level networking support, it is not possible to turn on the VPN from the iOS simulator in Xcode. +* Simulator builds can only be run on Rosetta versions of simulators. Simulator builds use the mocked VPN controller. +* When switching between building for Simulator and building for a device, the build folder must be completely removed. diff --git a/docs/Building/macos.md b/docs/Building/macos.md index f8dda3631d..3a28e69229 100644 --- a/docs/Building/macos.md +++ b/docs/Building/macos.md @@ -18,30 +18,6 @@ Install extra conda packages ./scripts/macos/conda_install_extras.sh -Your Xcode install comes with a copy of the MacOS-SDK. -We need to tell the conda environment where to find it. - -Find the sdk path - - xcrun --sdk macosx --show-sdk-path - -If xcrun didn't work, default paths where you probably find your SDK: - * Default Xcode-command-line tool path: `/Library/Developer/CommandLineTools/SDKs/MacOSX..sdk` - * Default Xcode.app path: `/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk` - -Add it to the conda env - - conda env config vars set SDKROOT=$SDK_PATH - -Reactivate your conda env - - conda activate vpn - -You can view your set variables - - conda env config vars list - -The variable config step only needs to be done once. When you next want to start building the VPN, all you need to do is activate your conda environment (`conda activate vpn`). ## Get Qt diff --git a/env.yml b/env.yml index b3f6f436b5..277c5f271d 100644 --- a/env.yml +++ b/env.yml @@ -18,7 +18,7 @@ dependencies: - rust-std-x86_64-linux-android=1.75 - rust-std-i686-linux-android=1.75 - rust-std-aarch64-linux-android=1.75 - - go=1.18 + - go=1.22 - compiler-rt - cmake=3.26.3 - ninja=1.11.0 diff --git a/ios/networkextension/CMakeLists.txt b/ios/networkextension/CMakeLists.txt index eb6f9b91bd..9aa92a4ead 100644 --- a/ios/networkextension/CMakeLists.txt +++ b/ios/networkextension/CMakeLists.txt @@ -140,3 +140,16 @@ set_source_files_properties( target_sources(networkextension PRIVATE ${CMAKE_BINARY_DIR}/src/generated/VPNMetrics.swift ) + +# The IOSGlean framework is built under the src directory, so we'll need to +# explicitly add it to the framework search paths. However, this gets messy +# in Xcode because the actual path is somewhat somewhat buried in +# subdirectories. +if(XCODE) + # Xcode is a multi-config generator, so technically we need to do this for + # each supported config too. + foreach(CONFIG ${CMAKE_CONFIGURATION_TYPES}) + set_target_properties(networkextension PROPERTIES + "XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS[variant=${CONFIG}]" "${CMAKE_BINARY_DIR}/src/${CONFIG}$(EFFECTIVE_PLATFORM_NAME)") + endforeach() +endif() diff --git a/scripts/cmake/rustlang.cmake b/scripts/cmake/rustlang.cmake index 56f687702f..aa677e837f 100644 --- a/scripts/cmake/rustlang.cmake +++ b/scripts/cmake/rustlang.cmake @@ -381,11 +381,4 @@ function(add_rust_library TARGET_NAME) add_dependencies(${TARGET_NAME} ${TARGET_NAME}_builder) set_property(TARGET ${TARGET_NAME} APPEND PROPERTY INTERFACE_LINK_LIBRARIES ${CMAKE_DL_LIBS}) - - ## When including multiple rust staticlibs, we often wind up with duplicate - ## symbols from the rust runtime. Work around it by permitting duplicates - ## during linking. - set_property(TARGET ${TARGET_NAME} APPEND PROPERTY INTERFACE_LINK_OPTIONS - $<$:-Xlink=-force:multiple> - ) endfunction() diff --git a/src/cmake/ios.cmake b/src/cmake/ios.cmake index a9c0075be3..f983ac14d9 100644 --- a/src/cmake/ios.cmake +++ b/src/cmake/ios.cmake @@ -29,7 +29,6 @@ target_sources(mozillavpn PRIVATE ${CMAKE_SOURCE_DIR}/src/platforms/ios/iosgleanbridge.h ) - ## Install the Network Extension into the bundle. add_dependencies(mozillavpn networkextension) @@ -74,6 +73,10 @@ set_target_properties(mozillavpn PROPERTIES # Do not strip debug symbols on copy XCODE_ATTRIBUTE_COPY_PHASE_STRIP "NO" XCODE_ATTRIBUTE_STRIP_INSTALLED_PRODUCT "NO" + # Do not build with debug dylibs - it breaks the Qt/ios entrypoint. + # This can be fixed by rolling out own entrypoint and invoking + # UIApplicationMain() ourselves. + XCODE_ATTRIBUTE_ENABLE_DEBUG_DYLIB "NO" ) target_include_directories(mozillavpn PRIVATE ${CMAKE_SOURCE_DIR}) diff --git a/src/commands/commandstatus.cpp b/src/commands/commandstatus.cpp index f53761e4ec..a15d2cb386 100644 --- a/src/commands/commandstatus.cpp +++ b/src/commands/commandstatus.cpp @@ -158,6 +158,10 @@ int CommandStatus::run(QStringList& tokens) { case Controller::StateSwitching: stream << "switching"; break; + + case Controller::State::StateOnboarding: + stream << "onboarding"; + break; } stream << Qt::endl; diff --git a/src/commands/commandui.cpp b/src/commands/commandui.cpp index 1ce8b180e2..6b52576716 100644 --- a/src/commands/commandui.cpp +++ b/src/commands/commandui.cpp @@ -164,6 +164,14 @@ int CommandUI::run(QStringList& tokens) { MockDaemon* daemon = new MockDaemon(qApp); qputenv("MVPN_CONTROL_SOCKET", daemon->socketPath().toLocal8Bit()); } +#ifdef MZ_IOS + else if (!qEnvironmentVariable("SIMULATOR_DEVICE_NAME").isEmpty()) { + // If we are running in the iOS device simulator, the network extension + // is not supported in this environment - use a mocked daemon instead. + MockDaemon* daemon = new MockDaemon(qApp); + qputenv("MVPN_CONTROL_SOCKET", daemon->socketPath().toLocal8Bit()); + } +#endif MozillaVPN vpn; logger.info() << "MozillaVPN" << Constants::versionString(); diff --git a/src/controller.cpp b/src/controller.cpp index 23c86c00d9..d46c497b52 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -75,6 +75,10 @@ Controller::Reason stateToReason(Controller::State state) { return Controller::ReasonConfirming; } + if (state == Controller::StateOnboarding) { + return Controller::ReasonOnboarding; + } + return Controller::ReasonNone; } } // namespace @@ -122,6 +126,12 @@ QString Controller::useLocalSocketPath() const { } #elif defined(MZ_WINDOWS) return Constants::WINDOWS_DAEMON_PATH; +#elif defined(MZ_IOS) + // The IOS simulator also uses a mocked daemon. + bool isSimDevice = !qEnvironmentVariable("SIMULATOR_DEVICE_NAME").isEmpty(); + if (isSimDevice && !path.isEmpty()) { + return path; + } #endif // Otherwise, we will need some other controller. @@ -332,6 +342,8 @@ qint64 Controller::connectionTimestamp() const { case Controller::State::StateInitializing: [[fallthrough]]; case Controller::State::StateOff: + [[fallthrough]]; + case Controller::State::StateOnboarding: return 0; case Controller::State::StateOn: [[fallthrough]]; @@ -627,7 +639,7 @@ void Controller::activateNext() { return; } - if (m_state != StateSilentSwitching) { + if ((m_state != StateSilentSwitching) && (m_state != StateOnboarding)) { // Move to the StateConfirming if we are awaiting any connection handshakes setState(StateConfirming); } @@ -774,6 +786,12 @@ void Controller::disconnected() { NextStep nextStep = m_nextStep; + // Mobile onboarding is completed when we receive the disconnected signal. + if (m_state == StateOnboarding) { + logger.debug() << "Onboarding completed"; + MozillaVPN::instance()->onboardingCompleted(); + } + if (processNextStep()) { setState(StateOff); return; @@ -973,6 +991,16 @@ bool Controller::activate(const ServerData& serverData, return true; } + // If we are in the onboarding state, this connection is being made just + // to establish system permissions. We don't actually want to connect to + // a server. + if (App::instance()->state() == App::StateOnboarding) { + setState(StateOnboarding); + clearRetryCounter(); + activateInternal(DoNotForceDNSPort, RandomizeServerSelection, ClientUser); + return true; + } + if (Feature::get(Feature::Feature_checkConnectivityOnActivation) ->isSupported()) { // Ensure that the device is connected to the Internet. @@ -1007,11 +1035,8 @@ bool Controller::activate(const ServerData& serverData, // Check if the error propagation has changed the Mozilla VPN // state. Continue only if the user is still authenticated and - // subscribed. We can ignore this during onboarding because we are - // not actually turning the VPN on (only asking for VPN system - // config permissions) - if (App::instance()->state() != App::StateMain && - App::instance()->state() != App::StateOnboarding) { + // subscribed. + if (App::instance()->state() != App::StateMain) { return; } diff --git a/src/controller.h b/src/controller.h index a059fb877e..76c91b1c1d 100644 --- a/src/controller.h +++ b/src/controller.h @@ -38,6 +38,7 @@ class Controller : public QObject, public LogSerializer { StateDisconnecting, StateSilentSwitching, StateSwitching, + StateOnboarding, }; Q_ENUM(State) @@ -45,7 +46,10 @@ class Controller : public QObject, public LogSerializer { ReasonNone = 0, ReasonSwitching, ReasonConfirming, + ReasonOnboarding, }; + Q_ENUM(Reason) + /** * @brief Who asked the Connection * to be Initiated? A Webextension diff --git a/src/daemon/daemonlocalserverconnection.cpp b/src/daemon/daemonlocalserverconnection.cpp index f5e8bfeb34..6ead186713 100644 --- a/src/daemon/daemonlocalserverconnection.cpp +++ b/src/daemon/daemonlocalserverconnection.cpp @@ -122,6 +122,17 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) { return; } + // If this connection is being made for onboading. Don't actually connect, + // just emit a disconnected signal to confirm. + QString reason = obj.value("reason").toString(); + if (!reason.isEmpty()) { + logger.error() << "Connection reason:" << reason; + } + if (reason == "onboarding") { + emit disconnected(); + return; + } + if (!m_daemon->activate(config)) { logger.error() << "Failed to activate the interface"; emit disconnected(); diff --git a/src/daemon/mock/mockdaemon.cpp b/src/daemon/mock/mockdaemon.cpp index 79e4d870ee..11072e3f90 100644 --- a/src/daemon/mock/mockdaemon.cpp +++ b/src/daemon/mock/mockdaemon.cpp @@ -28,6 +28,15 @@ MockDaemon::MockDaemon(const QString& name, QObject* parent) logger.debug() << "Mock daemon created"; +#ifdef MZ_IOS + // We have to go out of our way to keep the path length under sizeof(sun_path) + // for iOS because the QDir::tempPath() used by QLocalServer winds up being + // way too long. + if (!m_socketName.startsWith('/')) { + m_socketName.prepend("/tmp/"); + } +#endif + #ifndef MZ_WASM m_server.setSocketOptions(QLocalServer::UserAccessOption); if (!m_server.listen(m_socketName)) { diff --git a/src/localsocketcontroller.cpp b/src/localsocketcontroller.cpp index 3400a303af..35b100119f 100644 --- a/src/localsocketcontroller.cpp +++ b/src/localsocketcontroller.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "daemon/daemonerrors.h" #include "errorhandler.h" @@ -49,8 +50,11 @@ LocalSocketController::LocalSocketController(const QString& path) m_socket = new QLocalSocket(this); connect(m_socket, &QLocalSocket::connected, this, &LocalSocketController::daemonConnected); - connect(m_socket, &QLocalSocket::disconnected, this, - [&] { errorOccurred(QLocalSocket::PeerClosedError); }); + connect(m_socket, &QLocalSocket::disconnected, this, [&] { + if (m_daemonState != eInitializing) { + errorOccurred(QLocalSocket::PeerClosedError); + } + }); connect(m_socket, &QLocalSocket::errorOccurred, this, &LocalSocketController::errorOccurred); connect(m_socket, &QLocalSocket::readyRead, this, @@ -124,11 +128,15 @@ void LocalSocketController::daemonConnected() { void LocalSocketController::activate(const InterfaceConfig& config, Controller::Reason reason) { - Q_UNUSED(reason); - QJsonObject json = config.toJson(); json.insert("type", "activate"); + if (reason != Controller::ReasonNone) { + QMetaEnum metaEnum = QMetaEnum::fromType(); + QString reasonString = metaEnum.valueToKey(reason); + json.insert("reason", reasonString.toLower().mid(6)); + } + write(json); } diff --git a/src/models/devicemodel.cpp b/src/models/devicemodel.cpp index e5273a2a2e..3a98ec91e1 100644 --- a/src/models/devicemodel.cpp +++ b/src/models/devicemodel.cpp @@ -71,20 +71,21 @@ bool DeviceModel::fromSettings(const Keys* keys) { namespace { -bool sortCallback(const Device& a, const Device& b, const Keys* keys) { - if (a.isCurrentDevice(keys)) { - return true; - } - - if (b.isCurrentDevice(keys)) { - return false; - } - +bool sortCallback(const Device& a, const Device& b) { return a.createdAt() > b.createdAt(); } } // anonymous namespace +// Check if the current device exists, and if so move it to to the front. +void DeviceModel::moveCurrentDevice(const Keys* keys) { + for (qsizetype index = 0; index < m_devices.length(); index++) { + if (m_devices.at(index).isCurrentDevice(keys)) { + m_devices.move(index, 0); + } + } +} + bool DeviceModel::fromJsonInternal(const Keys* keys, const QByteArray& json) { beginResetModel(); @@ -132,9 +133,8 @@ bool DeviceModel::fromJsonInternal(const Keys* keys, const QByteArray& json) { } } - std::sort(m_devices.begin(), m_devices.end(), - std::bind(sortCallback, std::placeholders::_1, - std::placeholders::_2, keys)); + std::sort(m_devices.begin(), m_devices.end(), sortCallback); + moveCurrentDevice(keys); endResetModel(); emit changed(); @@ -207,9 +207,8 @@ void DeviceModel::stopDeviceRemovalFromPublicKey(const QString& publicKey, m_devices.append(*i); - std::sort(m_devices.begin(), m_devices.end(), - std::bind(sortCallback, std::placeholders::_1, - std::placeholders::_2, keys)); + std::sort(m_devices.begin(), m_devices.end(), sortCallback); + moveCurrentDevice(keys); m_removedDevices.erase(i); diff --git a/src/models/devicemodel.h b/src/models/devicemodel.h index 932362fd0f..8fe5c1e74d 100644 --- a/src/models/devicemodel.h +++ b/src/models/devicemodel.h @@ -74,6 +74,7 @@ class DeviceModel final : public QAbstractListModel, public LogSerializer { private: [[nodiscard]] bool fromJsonInternal(const Keys* keys, const QByteArray& json); + void moveCurrentDevice(const Keys* keys); bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override; diff --git a/src/platforms/android/androidcontroller.cpp b/src/platforms/android/androidcontroller.cpp index 73afffccc2..ed97512e95 100644 --- a/src/platforms/android/androidcontroller.cpp +++ b/src/platforms/android/androidcontroller.cpp @@ -100,14 +100,7 @@ AndroidController::AndroidController() { Qt::QueuedConnection); connect( activity, &AndroidVPNActivity::eventOnboardingCompleted, this, - [this]() { - auto vpn = MozillaVPN::instance(); - if (vpn->state() == App::StateOnboarding) { - vpn->onboardingCompleted(); - emit disconnected(); - } - }, - Qt::QueuedConnection); + [this]() { emit disconnected(); }, Qt::QueuedConnection); connect( activity, &AndroidVPNActivity::eventVpnConfigPermissionResponse, this, [](bool granted) { @@ -251,8 +244,7 @@ void AndroidController::activate(const InterfaceConfig& config, args["isUsingShortTimerSessionPing"] = settingsHolder->shortTimerSessionPing(); - args["isOnboarding"] = - MozillaVPN::instance()->state() == App::StateOnboarding; + args["isOnboarding"] = reason == Controller::ReasonOnboarding; QJsonDocument doc(args); AndroidVPNActivity::sendToService(ServiceAction::ACTION_ACTIVATE, diff --git a/src/platforms/ios/ioscontroller.mm b/src/platforms/ios/ioscontroller.mm index 284456ae7c..2f1983ef15 100644 --- a/src/platforms/ios/ioscontroller.mm +++ b/src/platforms/ios/ioscontroller.mm @@ -115,13 +115,6 @@ if (!impl) { logger.error() << "Controller not correctly initialized"; -#if TARGET_OS_SIMULATOR - if (MozillaVPN::instance()->state() == App::StateOnboarding) { - logger.debug() << "Cannot activate VPN on a simulator. Completing onboarding."; - MozillaVPN::instance()->onboardingCompleted(); - } -#endif - emit disconnected(); return; } @@ -194,12 +187,9 @@ } } onboardingCompletedCallback:^() { - BOOL isOnboarding = MozillaVPN::instance()->state() == App::StateOnboarding; - if (isOnboarding) { + if (reason == Controller::ReasonOnboarding) { logger.debug() << "Onboarding completed"; - MozillaVPN::instance()->onboardingCompleted(); - } else { - logger.debug() << "Not onboarding"; + emit disconnected(); } } vpnConfigPermissionResponseCallback:^(BOOL granted) { diff --git a/src/ui/screens/home/controller/ControllerImage.qml b/src/ui/screens/home/controller/ControllerImage.qml index 2e129e3846..30030ee2b9 100644 --- a/src/ui/screens/home/controller/ControllerImage.qml +++ b/src/ui/screens/home/controller/ControllerImage.qml @@ -136,6 +136,24 @@ Rectangle { opacity: 1 } }, + State { + name: VPNController.StateOnboarding + + PropertyChanges { + target: logo + showVPNOnIcon: false + opacity: 0.55 + } + PropertyChanges { + target: insetCircle + color: MZTheme.colors.successAccent + } + PropertyChanges { + target: insetIcon + source: "qrc:/ui/resources/shield-off.svg" + opacity: 1 + } + }, State { name: VPNController.StateOn PropertyChanges { diff --git a/src/ui/screens/home/controller/ControllerView.qml b/src/ui/screens/home/controller/ControllerView.qml index 09cf3e7e83..55c1260f21 100644 --- a/src/ui/screens/home/controller/ControllerView.qml +++ b/src/ui/screens/home/controller/ControllerView.qml @@ -63,7 +63,8 @@ Item { states: [ State { name: "stateInitializing" - when: VPNController.state === VPNController.StateInitializing + when: VPNController.state === VPNController.StateInitializing || + VPNController.state === VPNController.StateOnboarding PropertyChanges { target: boxBackground diff --git a/src/ui/screens/home/controller/VPNToggle.qml b/src/ui/screens/home/controller/VPNToggle.qml index ce8aacc749..12ca7b6107 100644 --- a/src/ui/screens/home/controller/VPNToggle.qml +++ b/src/ui/screens/home/controller/VPNToggle.qml @@ -237,6 +237,26 @@ MZButtonBase { toggleColor: MZTheme.colors.vpnToggleConnected } + }, + State { + name: VPNController.StateOnboarding + + PropertyChanges { + target: cursor + anchors.leftMargin: 4 + } + + PropertyChanges { + target: toggle + color: MZTheme.colors.vpnToggleDisconnected.defaultColor + border.color: MZTheme.colors.bgColorStronger + } + + PropertyChanges { + target: toggleButton + toggleColor: MZTheme.colors.vpnToggleDisconnected + } + } ] transitions: [