-
Notifications
You must be signed in to change notification settings - Fork 349
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Broadcast API access method changes #5605
Conversation
c4ff630
to
f78b40e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably outside the scope of this PR, but the CLI for api-access could be improved. One issue I found is the error message when you try to remove the direct or mullvad bridge methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably outside the scope of this PR, but the CLI for api-access could be improved. One issue I found is the error message when you try to remove the direct or mullvad bridge methods.
Also the list
command should probably give information about which API access method is currently active.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, the UX here could be improved. Let's create a separate issue for that 😊
Reviewable status: 0 of 16 files reviewed, 1 unresolved discussion (waiting on @Serock3)
mullvad-api/src/rest.rs
line 196 at r4 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
Why is the log level a warning? Is retrieving the next API connection mode not hte intended behaviour?
It is, the correct log level should be info. Well spotted!
mullvad-daemon/src/api.rs
Outdated
#[derive(Clone)] | ||
pub struct ResolvedConnectionMode { | ||
/// The connection strategy to be used by the `mullvad-api` crate when | ||
/// initialzing API requests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// initialzing API requests. | |
/// initializing API requests. |
mullvad-daemon/src/api.rs
Outdated
/// * When a [`mullvad_api::rest::RequestService`] requests a new | ||
/// [`ApiConnectionMode`] from the running [`AccessModeSelector`]. | ||
/// | ||
/// * When testing some [`AccessMethodSetting`] to see if can be used to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// * When testing some [`AccessMethodSetting`] to see if can be used to | |
/// * When testing some [`AccessMethodSetting`] to see if it can be used to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 0 of 16 files reviewed, 3 unresolved discussions (waiting on @Serock3)
mullvad-daemon/src/api.rs
line 44 at r4 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
/// * When testing some [`AccessMethodSetting`] to see if it can be used to
done
mullvad-daemon/src/api.rs
line 90 at r4 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
/// initializing API requests.
done
6a0d05f
to
c791bc4
Compare
I rebased this PR on top of #5617, which should be prioritized. |
11c3032
to
16ecc52
Compare
af930ed
to
117c935
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work! Although I cannot say I fully understand all the complexities of this change, I will start by given some minor comments with thoughts and suggestions that I picked up along the way of reviewing.
You can ignore most of the suggested changes if you want, they were mostly a way for me to figure out what the code was doing by toying around with it.
mullvad-daemon/src/lib.rs
Outdated
log::debug!( | ||
"API access method {method} {result}", | ||
method = test_subject.setting.name, | ||
result = if result.as_ref().is_ok_and(|is_true| *is_true) { | ||
"could successfully connect to the Mullvad API".to_string() | ||
} else { | ||
"could not connect to the Mullvad API".to_string() | ||
} | ||
); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
log::debug!( | |
"API access method {method} {result}", | |
method = test_subject.setting.name, | |
result = if result.as_ref().is_ok_and(|is_true| *is_true) { | |
"could successfully connect to the Mullvad API".to_string() | |
} else { | |
"could not connect to the Mullvad API".to_string() | |
} | |
); | |
log::debug!( | |
"API access method {method} {result}", | |
method = test_subject.setting.name, | |
result = match result { | |
Ok(true) => "could successfully connect to the Mullvad API", | |
_ => "could not connect to the Mullvad API", | |
} | |
); | |
mullvad-daemon/src/lib.rs
Outdated
}); | ||
} | ||
|
||
async fn test_api_access_method_inner( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think of making this an inner function of on_test_api_access_method
?
mullvad-daemon/src/api.rs
Outdated
/// A [`NewAccessMethodEvent`] is emitted when the active access method changes. | ||
/// Which access method that should be active at any given time is decided by | ||
/// the [`AccessModeSelector`] spawned when the daemon starts. | ||
/// | ||
/// This event may eventually lead to a | ||
/// [`mullvad_management_interface::client::DaemonEvent::NewAccessMethod`] to be | ||
/// broadcasted to clients. The event is emitted in two scenarios: | ||
/// | ||
/// * When a [`mullvad_api::rest::RequestService`] requests a new | ||
/// [`ApiConnectionMode`] from the running [`AccessModeSelector`]. | ||
/// | ||
/// * When testing some [`AccessMethodSetting`] to see if it can be used to | ||
/// successfully reach the Mullvad API. This will temporarily switch the | ||
/// currently active access method, but since this is just a test it should not | ||
/// produce any unwanted noise for clients. | ||
pub struct NewAccessMethodEvent { | ||
/// The new active [`AccessMethodSetting`]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice to see some quality documentation! Just two thoughts:
- Perhaps a link to
crate::InternalDaemonEvent::AccessMethodEvent
instead ofNewAccessMethodEvent
is more informative? - When testing an access method, does it emit a
DaemonEvent::NewAccessMethod
or not? Maybe the phrasing could be a bit clearer?
mullvad-daemon/src/api.rs
Outdated
/// It is up to the daemon to actually allow traffic to/from | ||
/// `api_endpoint` by updating the firewall. This `Sender` allows the | ||
/// daemon to communicate when that action is done. | ||
pub update_finished_tx: Option<oneshot::Sender<()>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this could have a clearer name? What do you think about endpoint_active_tx
?
mullvad-daemon/src/api.rs
Outdated
let (cmd_tx, cmd_rx) = mpsc::unbounded(); | ||
|
||
let connection_modes = match ConnectionModesIterator::new(connection_modes) { | ||
let mut connection_modes = match ConnectionModesIterator::new(connection_modes) { | ||
Ok(provider) => provider, | ||
Err(Error::NoAccessMethods) | Err(_) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this not just the same as any error variant? Could it be replaced with .unwrap_or_else
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let mut connection_modes =
ConnectionModesIterator::new(connection_modes).unwrap_or_else(|_| {
// No settings seem to have been found. Default to using the the
// direct access method.
let default = mullvad_types::access_method::Settings::direct();
ConnectionModesIterator::new(vec![default]).expect(
"Failed to create the data structure responsible for managing access methods",
)
});
mullvad-daemon/src/api.rs
Outdated
Message::Next(_) => f.write_str("Next"), | ||
Message::Update(_, _) => f.write_str("Update"), | ||
Message::Resolve(_, _) => f.write_str("Resolve"), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My rustfmt automatically makes this change whenever I format
Message::Next(_) => f.write_str("Next"), | |
Message::Update(_, _) => f.write_str("Update"), | |
Message::Resolve(_, _) => f.write_str("Resolve"), | |
} | |
Message::Set(..) => f.write_str("Set"), | |
Message::Next(_) => f.write_str("Next"), | |
Message::Update(..) => f.write_str("Update"), | |
Message::Resolve(..) => f.write_str("Resolve"), |
mullvad-daemon/src/api.rs
Outdated
let next = { | ||
let resolved = self.resolve(access_method).await; | ||
// Note: If the daemon is busy waiting for a call to this function | ||
// to complete while we wait for the daemon to fully handle this | ||
// `NewAccessMethodEvent`, then we find ourselves in a deadlock. | ||
// This can happen during daemon startup when spawning a new | ||
// `MullvadRestHandle`, which will call and await `next` on a Stream | ||
// created from this `AccessModeSelector` instance. As such, the | ||
// completion channel is discarded in this instance. | ||
let _completion = | ||
NewAccessMethodEvent::new(resolved.setting.clone(), resolved.endpoint.clone()) | ||
.send(&self.access_method_event_sender); | ||
self.current = resolved.clone(); | ||
resolved | ||
}; | ||
|
||
// Save the new connection mode to cache! | ||
{ | ||
let cache_dir = self.cache_dir.clone(); | ||
let next = next.clone(); | ||
let new_connection_mode = next.connection_mode.clone(); | ||
tokio::spawn(async move { | ||
if next.save(&cache_dir).await.is_err() { | ||
if new_connection_mode.save(&cache_dir).await.is_err() { | ||
log::warn!( | ||
"Failed to save {connection_mode} to cache", | ||
connection_mode = next | ||
connection_mode = new_connection_mode | ||
) | ||
} | ||
}); | ||
} | ||
self.reply(tx, next) | ||
} | ||
|
||
fn next_connection_mode(&mut self) -> ApiConnectionMode { | ||
let access_method = self | ||
.connection_modes | ||
.next() | ||
.map(|access_method_setting| access_method_setting.access_method) | ||
.unwrap_or(AccessMethod::from(BuiltInAccessMethod::Direct)); | ||
|
||
let connection_mode = self.from(access_method); | ||
log::info!("New API connection mode selected: {connection_mode}"); | ||
connection_mode | ||
Ok(next.connection_mode) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small nit, remove unneeded block
let resolved = self.resolve(access_method).await;
// Note: If the daemon is busy waiting for a call to this function
// to complete while we wait for the daemon to fully handle this
// `NewAccessMethodEvent`, then we find ourselves in a deadlock.
// This can happen during daemon startup when spawning a new
// `MullvadRestHandle`, which will call and await `next` on a Stream
// created from this `AccessModeSelector` instance. As such, the
// completion channel is discarded in this instance.
let _completion =
NewAccessMethodEvent::new(resolved.setting.clone(), resolved.endpoint.clone())
.send(&self.access_method_event_sender);
// Save the new connection mode to cache!
{
let cache_dir = self.cache_dir.clone();
let new_connection_mode = resolved.connection_mode.clone();
tokio::spawn(async move {
if new_connection_mode.save(&cache_dir).await.is_err() {
log::warn!(
"Failed to save {connection_mode} to cache",
connection_mode = new_connection_mode
)
}
});
}
self.current = resolved;
Ok(self.current.connection_mode.clone())
117c935
to
3a56175
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 0 of 17 files reviewed, 16 unresolved discussions (waiting on @Serock3)
mullvad-api/src/rest.rs
line 122 at r12 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
The
mullvad_api::ApiEndpointUpdateCallback
trait doesn't seem to be used at all after this change. Perhaps it can be removed competely?
Nice catch! It is definitely not needed anymore, so I will go ahead and remove it immediately 😊
mullvad-daemon/src/access_method.rs
line 0 at r12 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
(Reviewable was unable to map this GitHub inline comment thread to the right spot — sorry!)
I suspect that the removal of this function belongs to the previous commit.
You would be correct 😊
mullvad-daemon/src/access_method.rs
line 83 at r12 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
Is it possible fors only one endpoint to be available? What Happends then? Does the
force_api_endpoint_rotation
fn return an error or does the currently active on get removed anyway?
Yes, there might be only one available API endpoint if only one access method is enabled. Calling force_api_endpoint_rotation
in this state will perform a 'rotation' of the active API endpoint, but the active API endpoint will never actually change. Also, there can only every be one valid endpoint at any given time, due to how we apply firewall rules. 😊
Technically force_api_endpoint_rotation
returns an error if the RequestService
service goes down for some reason, but it won't return an error in case there is no next API endpoint. 😊
mullvad-daemon/src/api.rs
line 50 at r12 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
Nice to see some quality documentation! Just two thoughts:
- Perhaps a link to
crate::InternalDaemonEvent::AccessMethodEvent
instead ofNewAccessMethodEvent
is more informative?- When testing an access method, does it emit a
DaemonEvent::NewAccessMethod
or not? Maybe the phrasing could be a bit clearer?
Tried to clarify the doc comment by linking to crate::InternalDaemonEvent::AccessMethodEvent
and re-phrasing the bit about testing.
mullvad-daemon/src/api.rs
line 62 at r12 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
Maybe this could have a clearer name? What do you think about
endpoint_active_tx
?
Sure, the update
prefix is a bit generic in this case. 😊
mullvad-daemon/src/api.rs
line 106 at r12 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
This functions return type is a bit confusing. Returning a
oneshot::Receiver
make it behave almost as if it was async, but with eager execution and no requirement of beingawait
ed. Maybe it could be refactored to just be a normal async function?
The oneshot::Receiver
does not always have to be await
ed, which is why I chose to return it instead. But to avoid future confussion I will turn this function async, and callers who don't care to await
the result can simply move the call inside a new async task with tokio::spawn
😊
mullvad-daemon/src/api.rs
line 147 at r12 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
My rustfmt automatically makes this change whenever I format
Message::Set(..) => f.write_str("Set"), Message::Next(_) => f.write_str("Next"), Message::Update(..) => f.write_str("Update"), Message::Resolve(..) => f.write_str("Resolve"),
How strange that mine didn't .. Updated the formatting manually now, nice catch! 😊
mullvad-daemon/src/api.rs
line 274 at r12 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
let mut connection_modes = ConnectionModesIterator::new(connection_modes).unwrap_or_else(|_| { // No settings seem to have been found. Default to using the the // direct access method. let default = mullvad_types::access_method::Settings::direct(); ConnectionModesIterator::new(vec![default]).expect( "Failed to create the data structure responsible for managing access methods", ) });
Yes, it is and it can be replaced with that expression! Nice one 😊
mullvad-daemon/src/api.rs
line 391 at r12 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
Small nit, remove unneeded block
let resolved = self.resolve(access_method).await; // Note: If the daemon is busy waiting for a call to this function // to complete while we wait for the daemon to fully handle this // `NewAccessMethodEvent`, then we find ourselves in a deadlock. // This can happen during daemon startup when spawning a new // `MullvadRestHandle`, which will call and await `next` on a Stream // created from this `AccessModeSelector` instance. As such, the // completion channel is discarded in this instance. let _completion = NewAccessMethodEvent::new(resolved.setting.clone(), resolved.endpoint.clone()) .send(&self.access_method_event_sender); // Save the new connection mode to cache! { let cache_dir = self.cache_dir.clone(); let new_connection_mode = resolved.connection_mode.clone(); tokio::spawn(async move { if new_connection_mode.save(&cache_dir).await.is_err() { log::warn!( "Failed to save {connection_mode} to cache", connection_mode = new_connection_mode ) } }); } self.current = resolved; Ok(self.current.connection_mode.clone())
done
mullvad-daemon/src/api.rs
line 438 at r12 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
The link to
ApiConnectionModeProvider
doesn't work. Has the type been removed?
The comment linked to the wrong datatype, it should be RelaySelector
. Fixed!
mullvad-daemon/src/api.rs
line 472 at r12 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
Super small suggestion: maybe replace nested match with this?
AccessMethod::Custom(CustomAccessMethod::Shadowsocks(shadowsocks)) => { ApiConnectionMode::Proxied(ProxyConfig::Shadowsocks(shadowsocks)) } AccessMethod::Custom(CustomAccessMethod::Socks5(socks)) => { ApiConnectionMode::Proxied(ProxyConfig::Socks(socks)) }
Nice suggestion! I managed to flatten the entire function even more by implementing From<proxy::CustomProxy> for ProxyConfig
😊
mullvad-daemon/src/lib.rs
line 2487 at r12 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
log::debug!( "API access method {method} {result}", method = test_subject.setting.name, result = match result { Ok(true) => "could successfully connect to the Mullvad API", _ => "could not connect to the Mullvad API", } );
Done.
mullvad-daemon/src/lib.rs
line 2492 at r12 (raw file):
Previously, Serock3 (Sebastian Holmin) wrote…
What do you think of making this an inner function of
on_test_api_access_method
?
I think that it makes on_test_api_access_method
really big. 😄
3a56175
to
4186b9e
Compare
7692344
to
2f918b7
Compare
Add a new `InternalDaemonEvent` for announcing when the current API access method changes.
Previously, the `mullvad-api` would tell the `mullvad-daemon` that it wanted a new API endpoint by calling a certain callback (`ApiEndpointUpdateCallback`), which would asynchronously resolve a new API endpoint and tell the daemon to punch an appropriate hole in the firewall for that particular endpoint before the `mullvad-api` crate would consume it. The logic of the callback can be moved inside `AccessModeSelector`, which simplifies the contract between `mullvad-daemon` and `mullvad-api` somewhat.
Until now, `AccessModeSelector` has not been able to resolve API endpoints on it's own. This has happened at some later stage, for example in the `mullvad-api` crate. However, for testing the `Direct` access method, it is very useful to be able to resolve the actual endpoint without involving the daemon's "API runtime".
This commit implements the daemon logic for handling a `NewAccessMethodEvent`. Such an event occur when `AccessModeSelector` announces that a new access method is active, and it will cause the daemon to except some API endpoint in the firewall. It may conditionally broadcast the new access method to all clients.
Since the `AccessModeSelector` knows how to resolve endpoints on it's own, we no longer have to re-use the existing `MullvadRestHandle` from the daemon. Instead, we may spawn a completely new `ApiProxy` and except the appropriate endpoint in the firewall without affecting the running `MullvadRestHandle`.
2f918b7
to
32ba86f
Compare
This PR changes how the
AccessModeSelector
interacts with the daemon. A new internal daemon (AccessMethodEvent
) event has been added in order for theAccessModeSelector
to properly be able to delegate the job of applying the correct firewall rules for any given access method. This allowed us to get rid ofApiEndpointUpdateCallback
from theRequestService
.Fixes DES-521
This change is