Skip to content

Commit

Permalink
VPN-5992 Allow extensions to request Access to the Proxy Network (#9639)
Browse files Browse the repository at this point in the history
This pull request introduces support for a new StateOnPartial state in the controller, enhancing the VPN's state management. It also adds an ActivationPrincipal enum to track whether a connection was initiated by a user or an extension, along with corresponding permission checks for deactivation. Additionally, this update includes several new methods and refines existing ones to handle the new state and activation principal, ensuring improved VPN control and security.


* WORKS only tests missing

* Add tests

* Remove backdoor >:(

* Todo: get a spellchecker for vscode

* Skip StateConnecting for the Extension

* Bonus: Remove unused State, making this PR state Neutral

* Lint

* Update tests/functional/helper.js

Co-authored-by: Lesley Norton <lesley@mozilla.com>

* Update src/controller.h

Co-authored-by: Lesley Norton <lesley@mozilla.com>

* revert oops

* Fix functionalTests

* ugh

---------

Co-authored-by: Lesley Norton <lesley@mozilla.com>
  • Loading branch information
strseb and lesleyjanenorton committed Jun 24, 2024
1 parent d943cd8 commit c34e159
Show file tree
Hide file tree
Showing 19 changed files with 413 additions and 123 deletions.
3 changes: 1 addition & 2 deletions nebula/ui/components/MZDropShadowWithStates.qml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ MZDropShadow {
state === VPNController.StateConfirming ||
state === VPNController.StateOn ||
state === VPNController.StateSilentSwitching ||
state === VPNController.StateSwitching ||
state == VPNController.StateCheckSubscription)
state === VPNController.StateSwitching)
PropertyChanges {
target: dropShadow
opacity: .3
Expand Down
2 changes: 0 additions & 2 deletions src/captiveportal/captiveportaldetection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ void CaptivePortalDetection::networkChanged() {

Controller::State state = vpn->controller()->state();
if (state != Controller::StateOn && state != Controller::StateConnecting &&
state != Controller::StateCheckSubscription &&
state != Controller::StateConfirming) {
// Network Changed but we're not connected, no need to test for captive
// portal
Expand Down Expand Up @@ -80,7 +79,6 @@ void CaptivePortalDetection::stateChanged() {
if ((state != Controller::StateOn ||
vpn->connectionHealth()->stability() == ConnectionHealth::Stable) &&
state != Controller::StateConnecting &&
state != Controller::StateCheckSubscription &&
state != Controller::StateConfirming) {
logger.warning() << "No captive portal detection required";
m_impl.reset();
Expand Down
7 changes: 2 additions & 5 deletions src/commands/commandstatus.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,6 @@ int CommandStatus::run(QStringList& tokens) {
case Controller::StateOff:
stream << "off";
break;

case Controller::StateCheckSubscription:
stream << "check subscription";
break;

case Controller::StateConnecting:
stream << "connecting";
break;
Expand All @@ -150,6 +145,8 @@ int CommandStatus::run(QStringList& tokens) {

case Controller::StateOn:
[[fallthrough]];
case Controller::StateOnPartial:
[[fallthrough]];
case Controller::StateSilentSwitching:
stream << "on";
break;
Expand Down
127 changes: 85 additions & 42 deletions src/controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,11 @@
# include "platforms/dummy/dummycontroller.h"
#endif

#ifndef MZ_IOS
// The Mullvad proxy services are located at internal IPv4 addresses in the
// 10.124.0.0/20 address range, which is a subset of the 10.0.0.0/8 Class-A
// private address range.
constexpr const char* MULLVAD_PROXY_RANGE = "10.124.0.0";
constexpr const int MULLVAD_PROXY_RANGE_LENGTH = 20;
#endif

namespace {
Logger logger("Controller");
Expand Down Expand Up @@ -201,7 +199,7 @@ qint64 Controller::time() const {
}

void Controller::timerTimeout() {
Q_ASSERT(m_state == StateOn);
Q_ASSERT(m_state == StateOn || m_state == StateOnPartial);
#ifdef MZ_IOS
// When locking an iOS device with an app in the foreground, the app's JS
// runtime is stopped pretty quick by the system. For this VPN app, that
Expand All @@ -228,9 +226,9 @@ void Controller::quit() {

m_nextStep = Quit;

if (m_state == StateOn || m_state == StateSwitching ||
m_state == StateSilentSwitching || m_state == StateConnecting ||
m_state == StateCheckSubscription) {
if (m_state == StateOn || m_state == StateOnPartial ||
m_state == StateSwitching || m_state == StateSilentSwitching ||
m_state == StateConnecting) {
deactivate();
return;
}
Expand Down Expand Up @@ -272,14 +270,17 @@ void Controller::handshakeTimeout() {
++m_connectionRetry;
emit connectionRetryChanged();
logger.info() << "Connection attempt " << m_connectionRetry;
InterfaceConfig& exit_hop = m_activationQueue.last();
Q_ASSERT(exit_hop.m_hopType != InterfaceConfig::HopType::MultiHopEntry);

if (m_connectionRetry == 1) {
logger.info() << "Connection Attempt: Using Port 53 Option this time.";
// On the first retry, opportunisticly try again using the port 53
// option enabled, if that feature is disabled.
activateInternal(ForceDNSPort, RandomizeServerSelection);
activateInternal(ForceDNSPort, RandomizeServerSelection, m_initiator);
return;
} else if (m_connectionRetry < CONNECTION_MAX_RETRY) {
activateInternal(DoNotForceDNSPort, RandomizeServerSelection);
activateInternal(DoNotForceDNSPort, RandomizeServerSelection, m_initiator);
return;
}

Expand All @@ -296,14 +297,14 @@ void Controller::serverUnavailable() {
// to deactivate the VPN in the case of server location becoming unavailable
// because the user may not notice that they are no longer protected which can
// cause the traffic to leak outside the tunnel without their knowledge.
if (m_state == StateOn) {
if (m_state == StateOn || m_state == StateOnPartial) {
return;
}

m_nextStep = ServerUnavailable;

if (m_state == StateSwitching || m_state == StateConnecting ||
m_state == StateConfirming || m_state == StateCheckSubscription) {
m_state == StateConfirming) {
logger.info() << "Server location is unavailable and we are not in "
"StateOn. Deactivate!";
deactivate();
Expand All @@ -323,7 +324,7 @@ void Controller::updateRequired() {

m_nextStep = Update;

if (m_state == StateOn) {
if (m_state == StateOn || m_state == StateOnPartial) {
deactivate();
return;
}
Expand All @@ -340,16 +341,19 @@ void Controller::logout() {

m_nextStep = Disconnect;

if (m_state == StateOn) {
if (m_state == StateOn || m_state == StateOnPartial) {
deactivate();
return;
}
}

void Controller::activateInternal(DNSPortPolicy dnsPort,
ServerSelectionPolicy serverSelectionPolicy) {
void Controller::activateInternal(
DNSPortPolicy dnsPort,
ServerSelectionPolicy serverSelectionPolicy = RandomizeServerSelection,
ActivationPrincipal initiator = ClientUser) {
logger.debug() << "Activation internal";
Q_ASSERT(m_impl);
m_initiator = initiator;

clearConnectedTime();
m_handshakeTimer.stop();
Expand All @@ -376,6 +380,9 @@ void Controller::activateInternal(DNSPortPolicy dnsPort,
}
SettingsHolder* settingsHolder = SettingsHolder::instance();

auto allowedIPList = initiator == ExtensionUser
? getExtensionProxyAddressRanges(exitServer)
: getAllowedIPAddressRanges(exitServer);
// Prepare the exit server's connection data.
InterfaceConfig exitConfig;
exitConfig.m_privateKey = vpn->keys()->privateKey();
Expand All @@ -387,7 +394,7 @@ void Controller::activateInternal(DNSPortPolicy dnsPort,
exitConfig.m_serverIpv4AddrIn = exitServer.ipv4AddrIn();
exitConfig.m_serverIpv6AddrIn = exitServer.ipv6AddrIn();
exitConfig.m_serverPort = exitServer.choosePort();
exitConfig.m_allowedIPAddressRanges = getAllowedIPAddressRanges(exitServer);
exitConfig.m_allowedIPAddressRanges = allowedIPList;
exitConfig.m_dnsServer = DNSHelper::getDNS(exitServer.ipv4Gateway());
#if defined(MZ_ANDROID) || defined(MZ_IOS)
exitConfig.m_installationId = settingsHolder->installationId();
Expand Down Expand Up @@ -577,6 +584,15 @@ QList<IPAddress> Controller::getAllowedIPAddressRanges(
return list;
}

// static
QList<IPAddress> Controller::getExtensionProxyAddressRanges(
const Server& exitServer) {
return {
IPAddress(QHostAddress(exitServer.ipv4Gateway()), 32),
IPAddress(QHostAddress(exitServer.ipv6Gateway()), 128),
IPAddress(QHostAddress{MULLVAD_PROXY_RANGE}, MULLVAD_PROXY_RANGE_LENGTH)};
}

void Controller::activateNext() {
MozillaVPN* vpn = MozillaVPN::instance();
const Device* device = vpn->deviceModel()->currentDevice(vpn->keys());
Expand All @@ -598,6 +614,9 @@ void Controller::activateNext() {

m_impl->activate(config, stateToReason(m_state));

if (m_initiator == ExtensionUser) {
return;
}
// Move to the confirming state if we are awaiting any connection handshakes.
setState(StateConfirming);
}
Expand Down Expand Up @@ -687,7 +706,7 @@ void Controller::connected(const QString& pubkey) {
m_connectionRetry = 0;
emit connectionRetryChanged();

if (m_state == StateOn) {
if (m_state == StateOn || m_state == StateOnPartial) {
// The only place StateOn is set is in this function, Controller::connected.
// If this function is called when the state is already StateOn, it is
// because there was a silent server switch initiated from the daemon. (Only
Expand All @@ -703,7 +722,11 @@ void Controller::connected(const QString& pubkey) {

// We have succesfully completed all pending connections.
logger.debug() << "Connected from state:" << m_state;
setState(StateOn);
if (m_initiator == ExtensionUser) {
setState(StateOnPartial);
} else {
setState(StateOn);
}
resetConnectedTime();

if (isSwitchingServer == false) {
Expand Down Expand Up @@ -745,8 +768,14 @@ void Controller::disconnected() {
}

if (nextStep == None &&
(m_state == StateSwitching || m_state == StateSilentSwitching)) {
activate(m_nextServerData, m_nextServerSelectionPolicy);
(m_state == StateSilentSwitching || m_state == StateSwitching)) {
// If we are only silently switching, keep the iniator
// Else move the iniator to Client User
// as the extension cannot switch servers.
auto target_iniator = m_state == StateSilentSwitching
? m_initiator
: ActivationPrincipal::ClientUser;
activate(m_nextServerData, target_iniator, m_nextServerSelectionPolicy);
return;
}

Expand All @@ -755,7 +784,7 @@ void Controller::disconnected() {
if (m_state != StateConfirming) {
emit recordConnectionEndTelemetry();
}

m_initiator = Null;
setState(StateOff);
}

Expand Down Expand Up @@ -923,9 +952,9 @@ void Controller::backendFailure() {

m_nextStep = BackendFailure;

if (m_state == StateOn || m_state == StateSwitching ||
m_state == StateSilentSwitching || m_state == StateConnecting ||
m_state == StateCheckSubscription || m_state == StateConfirming) {
if (m_state == StateOn || m_state == StateOnPartial ||
m_state == StateSwitching || m_state == StateSilentSwitching ||
m_state == StateConnecting || m_state == StateConfirming) {
deactivate();
return;
}
Expand All @@ -943,9 +972,11 @@ QString Controller::currentServerString() const {
// for now they will reside here

bool Controller::activate(const ServerData& serverData,
ActivationPrincipal initiator,
ServerSelectionPolicy serverSelectionPolicy) {
logger.debug() << "Activation" << m_state;
if (m_state != Controller::StateOff &&
m_state != Controller::StateOnPartial &&
m_state != Controller::StateSwitching &&
m_state != Controller::StateSilentSwitching) {
logger.debug() << "Already connected";
Expand All @@ -957,7 +988,6 @@ bool Controller::activate(const ServerData& serverData,
logger.debug() << "Set isSwitchingServer to" << isSwitchingServer;

m_serverData = serverData;

#ifdef MZ_DUMMY
emit currentServerChanged();
#endif
Expand Down Expand Up @@ -988,10 +1018,6 @@ bool Controller::activate(const ServerData& serverData,
}
}

// Before attempting to enable VPN connection we should check that the
// subscription is active.
setState(StateCheckSubscription);

// Set up a network request to check the subscription status.
// "task" is an empty task function which is being used to
// replicate the behavior of a TaskAccount.
Expand All @@ -1001,8 +1027,8 @@ bool Controller::activate(const ServerData& serverData,
request->get(Constants::apiUrl(Constants::Account));

connect(request, &NetworkRequest::requestFailed, this,
[this, serverSelectionPolicy](QNetworkReply::NetworkError error,
const QByteArray&) {
[this, serverSelectionPolicy, initiator](
QNetworkReply::NetworkError error, const QByteArray&) {
logger.error() << "Account request failed" << error;
REPORTNETWORKERROR(error, ErrorHandler::DoNotPropagateError,
"PreActivationSubscriptionCheck");
Expand All @@ -1017,36 +1043,53 @@ bool Controller::activate(const ServerData& serverData,
return;
}

setState(StateConnecting);
if (initiator != ExtensionUser) {
setState(StateConnecting);
}

clearRetryCounter();
activateInternal(DoNotForceDNSPort, serverSelectionPolicy);
activateInternal(DoNotForceDNSPort, serverSelectionPolicy,
initiator);
});

connect(request, &NetworkRequest::requestCompleted, this,
[this, serverSelectionPolicy](const QByteArray& data) {
[this, serverSelectionPolicy, initiator](const QByteArray& data) {
MozillaVPN::instance()->accountChecked(data);
setState(StateConnecting);

if (initiator != ExtensionUser) {
setState(StateConnecting);
}

clearRetryCounter();
activateInternal(DoNotForceDNSPort, serverSelectionPolicy);
activateInternal(DoNotForceDNSPort, serverSelectionPolicy,
initiator);
});

connect(request, &QObject::destroyed, task, &QObject::deleteLater);
return true;
}
if (initiator != ExtensionUser) {
setState(StateConnecting);
}

clearRetryCounter();
activateInternal(DoNotForceDNSPort, serverSelectionPolicy);
activateInternal(DoNotForceDNSPort, serverSelectionPolicy, initiator);
return true;
}

bool Controller::deactivate() {
bool Controller::deactivate(ActivationPrincipal user) {
logger.debug() << "Deactivation" << m_state;
if (m_initiator > user) {
// i.e the Firefox Extension cannot deativate the
// vpn if we are in full device protection.
logger.warning()
<< "ActivationPrincipal does not have permission allowed to deactivate";
return false;
}

if (m_state != StateOn && m_state != StateSwitching &&
m_state != StateSilentSwitching && m_state != StateConfirming &&
m_state != StateConnecting && m_state != StateCheckSubscription) {
if (m_state != StateOn && m_state != StateOnPartial &&
m_state != StateSwitching && m_state != StateSilentSwitching &&
m_state != StateConfirming && m_state != StateConnecting) {
logger.warning() << "Already disconnected";
return false;
}
Expand All @@ -1062,8 +1105,8 @@ bool Controller::deactivate() {
m_portalDetected = false;
}

if (m_state == StateOn || m_state == StateConfirming ||
m_state == StateConnecting || m_state == StateCheckSubscription) {
if (m_state == StateOn || m_state == StateOnPartial ||
m_state == StateConfirming || m_state == StateConnecting) {
setState(StateDisconnecting);
}

Expand Down
Loading

0 comments on commit c34e159

Please sign in to comment.