Skip to content
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

chore(gui-client): Bubble up connlib panics as error dialogs in the GUI #5098

Merged
merged 44 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c6caf88
work towards enabling the debug IPC service on Linux
ReactorScram May 21, 2024
78d08fc
Linux has the debug IPC service now
ReactorScram May 21, 2024
2f324ed
refactor
ReactorScram May 21, 2024
979d645
refactor
ReactorScram May 21, 2024
3d2cdf0
fix Windows build
ReactorScram May 21, 2024
8d9e1d5
move almost everything out of windows module
ReactorScram May 21, 2024
ba1a713
Fix Linux build
ReactorScram May 21, 2024
46a9146
fix Windows clippy
ReactorScram May 21, 2024
b605b46
hide this comment from the CLI output
ReactorScram May 21, 2024
86092b9
fix Windows service
ReactorScram May 22, 2024
a13c3f3
Merge branch 'refactor/debug-ipc-service' of github.com:firezone/fire…
ReactorScram May 22, 2024
c21b906
checkpoint
ReactorScram May 22, 2024
52ea524
checkpoint
ReactorScram May 22, 2024
dbafcba
extract almost all stuff to the platform-independent module
ReactorScram May 22, 2024
92dfb05
de-dupe
ReactorScram May 22, 2024
99bbe9e
Merge remote-tracking branch 'origin/main' into refactor/debug-ipc-se…
ReactorScram May 22, 2024
8565d65
this only needs to be a type alias, not a wrapper struct
ReactorScram May 22, 2024
9aa92c9
Merge branch 'refactor/debug-ipc-service' into refactor/dedupe-ipc-cl…
ReactorScram May 22, 2024
9c8b3ea
Merge remote-tracking branch 'origin/refactor/dedupe-ipc-clients' int…
ReactorScram May 22, 2024
7a5667f
tell the GUI whether it's an auth error
ReactorScram May 22, 2024
1d50c23
show dialog for non-auth errors
ReactorScram May 22, 2024
613dd88
remove test panic
ReactorScram May 22, 2024
bfc6ac6
Merge remote-tracking branch 'origin/main' into refactor/debug-ipc-se…
ReactorScram May 24, 2024
f881898
use `PathBuf` here instead of `String`
ReactorScram May 24, 2024
2a71eff
remove outdated comment
ReactorScram May 24, 2024
24b9944
remove unused CLI args
ReactorScram May 24, 2024
43fac19
use `futures::future::select` to simplify the signal polling
ReactorScram May 24, 2024
f55f3d1
Merge remote-tracking branch 'origin/main' into refactor/debug-ipc-se…
ReactorScram May 24, 2024
e89707f
fix Windows build
ReactorScram May 24, 2024
bd86707
fix incorrect SIGHUP behavior
ReactorScram May 24, 2024
c1e7014
fix Linux
ReactorScram May 24, 2024
a82f163
I forgot to commit this
ReactorScram May 24, 2024
f61d543
Merge remote-tracking branch 'origin/refactor/debug-ipc-service' into…
ReactorScram May 24, 2024
c6b4bba
fmt
ReactorScram May 24, 2024
e85bed6
clippy
ReactorScram May 24, 2024
4ee20d7
fix IPC service
ReactorScram May 24, 2024
595bed1
move this log to the right place
ReactorScram May 24, 2024
3f7b8b6
Merge remote-tracking branch 'origin/refactor/debug-ipc-service' into…
ReactorScram May 24, 2024
a245ff0
improve error messages
ReactorScram May 24, 2024
6818f39
fix Windows
ReactorScram May 24, 2024
cbd8f5a
Merge branch 'refactor/dedupe-ipc-clients' into chore/5046-bubble-errors
ReactorScram May 24, 2024
f64cfa2
Merge commit '6b570a6dad3051b76c19370ad8caee39b075ba85' into chore/50…
ReactorScram May 28, 2024
77b4a45
Merge commit 'bfffcedf47cbdc1ed8675e2ac2259389fb4ce42c' into chore/50…
ReactorScram May 28, 2024
5b5d061
Merge remote-tracking branch 'origin/main' into chore/5046-bubble-errors
ReactorScram May 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Environment="LOG_DIR=/var/log/dev.firezone.client"
Environment="RUST_LOG=info"
EnvironmentFile="/etc/default/firezone-client-ipc"

ExecStart=firezone-client-ipc
ExecStart=firezone-client-ipc ipc-service
Type=notify
# Unfortunately we may need root to control DNS
User=root
Expand Down
29 changes: 22 additions & 7 deletions rust/gui-client/src-tauri/src/client/gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,10 @@ pub(crate) enum ControllerRequest {
ApplySettings(AdvancedSettings),
/// Only used for smoke tests
ClearLogs,
Disconnected,
Disconnected {
error_msg: String,
is_authentication_error: bool,
},
/// The same as the arguments to `client::logging::export_logs_to`
ExportLogs {
path: PathBuf,
Expand Down Expand Up @@ -560,13 +563,25 @@ impl Controller {
Req::ClearLogs => logging::clear_logs_inner()
.await
.context("Failed to clear logs")?,
Req::Disconnected => {
tracing::info!("Disconnected by connlib");
Req::Disconnected {
error_msg,
is_authentication_error,
} => {
self.sign_out().await?;
os::show_notification(
"Firezone disconnected",
"To access resources, sign in again.",
)?;
if is_authentication_error {
tracing::info!(?error_msg, "Auth error");
os::show_notification(
"Firezone disconnected",
"To access resources, sign in again.",
)?;
} else {
tracing::error!(?error_msg, "Disconnected");
native_dialog::MessageDialog::new()
.set_title("Firezone Error")
.set_text(&error_msg)
.set_type(native_dialog::MessageType::Error)
.show_alert()?;
}
}
Req::ExportLogs { path, stem } => logging::export_logs_to(path, stem)
.await
Expand Down
105 changes: 87 additions & 18 deletions rust/gui-client/src-tauri/src/client/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ use crate::client::gui::{ControllerRequest, CtlrTx};
use anyhow::{Context as _, Result};
use arc_swap::ArcSwap;
use connlib_client_shared::callbacks::ResourceDescription;
use firezone_headless_client::IpcClientMsg;

use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::Arc,
};
use firezone_headless_client::{IpcClientMsg, IpcServerMsg};
use futures::{SinkExt, StreamExt};
use secrecy::{ExposeSecret, SecretString};
use std::{net::IpAddr, sync::Arc};
use tokio::sync::Notify;

pub(crate) use platform::Client;
use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};

#[cfg(target_os = "linux")]
#[path = "ipc/linux.rs"]
Expand All @@ -32,23 +29,22 @@ pub(crate) struct CallbackHandler {
pub resources: Arc<ArcSwap<Vec<ResourceDescription>>>,
}

// Callbacks must all be non-blocking
impl connlib_client_shared::Callbacks for CallbackHandler {
fn on_disconnect(&self, error: &connlib_client_shared::Error) {
// The errors don't implement `Serialize`, so we don't get a machine-readable
// error here, but we should consider it an error anyway. `on_disconnect`
// is always an error
tracing::error!("on_disconnect {error:?}");
// Almost but not quite implements `Callbacks` from connlib.
// Because of the IPC boundary, we can deviate.
impl CallbackHandler {
fn on_disconnect(&self, error_msg: String, is_authentication_error: bool) {
self.ctlr_tx
.try_send(ControllerRequest::Disconnected)
.try_send(ControllerRequest::Disconnected {
error_msg,
is_authentication_error,
})
.expect("controller channel failed");
}

fn on_set_interface_config(&self, _: Ipv4Addr, _: Ipv6Addr, _: Vec<IpAddr>) -> Option<i32> {
fn on_set_interface_config(&self) {
self.ctlr_tx
.try_send(ControllerRequest::TunnelReady)
.expect("controller channel failed");
None
}

fn on_update_resources(&self, resources: Vec<ResourceDescription>) {
Expand All @@ -58,7 +54,80 @@ impl connlib_client_shared::Callbacks for CallbackHandler {
}
}

pub(crate) struct Client {
task: tokio::task::JoinHandle<Result<()>>,
// Needed temporarily to avoid a big refactor. We can remove this in the future.
tx: FramedWrite<tokio::io::WriteHalf<platform::IpcStream>, LengthDelimitedCodec>,
}

impl Client {
pub(crate) async fn disconnect(mut self) -> Result<()> {
self.send_msg(&IpcClientMsg::Disconnect)
.await
.context("Couldn't send Disconnect")?;
self.tx.close().await?;
self.task.abort();
Ok(())
}

pub(crate) async fn send_msg(&mut self, msg: &IpcClientMsg) -> Result<()> {
self.tx
.send(
serde_json::to_string(msg)
.context("Couldn't encode IPC message as JSON")?
.into(),
)
.await
.context("Couldn't send IPC message")?;
Ok(())
}

pub(crate) async fn connect(
api_url: &str,
token: SecretString,
callback_handler: CallbackHandler,
tokio_handle: tokio::runtime::Handle,
) -> Result<Self> {
tracing::info!(pid = std::process::id(), "Connecting to IPC service...");
let stream = platform::connect_to_service().await?;
let (rx, tx) = tokio::io::split(stream);
// Receives messages from the IPC service
let mut rx = FramedRead::new(rx, LengthDelimitedCodec::new());
let tx = FramedWrite::new(tx, LengthDelimitedCodec::new());

let task = tokio_handle.spawn(async move {
while let Some(msg) = rx.next().await.transpose()? {
match serde_json::from_slice::<IpcServerMsg>(&msg)? {
IpcServerMsg::Ok => {}
IpcServerMsg::OnDisconnect {
error_msg,
is_authentication_error,
} => callback_handler.on_disconnect(error_msg, is_authentication_error),
IpcServerMsg::OnUpdateResources(v) => callback_handler.on_update_resources(v),
IpcServerMsg::OnSetInterfaceConfig {
ipv4: _,
ipv6: _,
dns: _,
} => {
callback_handler.on_set_interface_config();
}
}
}
Ok(())
});

let mut client = Self { task, tx };
let token = token.expose_secret().clone();
client
.send_msg(&IpcClientMsg::Connect {
api_url: api_url.to_string(),
token,
})
.await
.context("Couldn't send Connect message")?;
Ok(client)
}

pub(crate) async fn reconnect(&mut self) -> Result<()> {
self.send_msg(&IpcClientMsg::Reconnect)
.await
Expand Down
92 changes: 12 additions & 80 deletions rust/gui-client/src-tauri/src/client/ipc/linux.rs
Original file line number Diff line number Diff line change
@@ -1,82 +1,14 @@
use anyhow::{Context as _, Result};
use connlib_client_shared::Callbacks;
use firezone_headless_client::{platform::sock_path, IpcClientMsg, IpcServerMsg};
use futures::{SinkExt, StreamExt};
use secrecy::{ExposeSecret, SecretString};
use tokio::net::{unix::OwnedWriteHalf, UnixStream};
use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};

/// Forwards events to and from connlib
pub(crate) struct Client {
recv_task: tokio::task::JoinHandle<Result<()>>,
tx: FramedWrite<OwnedWriteHalf, LengthDelimitedCodec>,
}

impl Client {
pub(crate) async fn disconnect(mut self) -> Result<()> {
self.send_msg(&IpcClientMsg::Disconnect)
.await
.context("Couldn't send Disconnect")?;
self.tx.close().await?;
self.recv_task.abort();
Ok(())
}

pub(crate) async fn send_msg(&mut self, msg: &IpcClientMsg) -> Result<()> {
self.tx
.send(
serde_json::to_string(msg)
.context("Couldn't encode IPC message as JSON")?
.into(),
)
.await
.context("Couldn't send IPC message")?;
Ok(())
}

pub(crate) async fn connect(
api_url: &str,
token: SecretString,
callback_handler: super::CallbackHandler,
tokio_handle: tokio::runtime::Handle,
) -> Result<Self> {
tracing::info!(pid = std::process::id(), "Connecting to IPC service...");
let stream = UnixStream::connect(sock_path())
.await
.context("Couldn't connect to UDS")?;
let (rx, tx) = stream.into_split();
// Receives messages from the IPC service
let mut rx = FramedRead::new(rx, LengthDelimitedCodec::new());
let tx = FramedWrite::new(tx, LengthDelimitedCodec::new());

// TODO: Make sure this joins / drops somewhere
let recv_task = tokio_handle.spawn(async move {
while let Some(msg) = rx.next().await.transpose()? {
let msg: IpcServerMsg = serde_json::from_slice(&msg)?;
match msg {
IpcServerMsg::Ok => {}
IpcServerMsg::OnDisconnect => callback_handler.on_disconnect(
&connlib_client_shared::Error::Other("errors can't be serialized"),
),
IpcServerMsg::OnUpdateResources(v) => callback_handler.on_update_resources(v),
IpcServerMsg::OnSetInterfaceConfig { ipv4, ipv6, dns } => {
callback_handler.on_set_interface_config(ipv4, ipv6, dns);
}
}
}
Ok(())
});

let mut client = Self { recv_task, tx };
let token = token.expose_secret().clone();
client
.send_msg(&IpcClientMsg::Connect {
api_url: api_url.to_string(),
token,
})
.await
.context("Couldn't send Connect message")?;

Ok(client)
}
use firezone_headless_client::platform::sock_path;
use tokio::net::UnixStream;

/// A type alias to abstract over the Windows and Unix IPC primitives
pub(crate) type IpcStream = UnixStream;

/// Connect to the IPC service
pub(crate) async fn connect_to_service() -> Result<IpcStream> {
let stream = UnixStream::connect(sock_path())
.await
.context("Couldn't connect to Unix domain socket")?;
Ok(stream)
}