Implement launching a local daemon (#261)

Allow AppImage and non-systemd systems to launch a local burrow daemon.
This commit is contained in:
David Zhong 2024-03-09 17:52:59 -08:00 committed by GitHub
parent c4c342dc8b
commit c755f752a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 192 additions and 27 deletions

View file

@ -8,13 +8,14 @@ Burrow is an open source tool for burrowing through firewalls, built by teenager
## Contributing ## Contributing
Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow! For more information on how to contribute, please see [CONTRIBUTING.md] Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow! Checkout [GETTING_STARTED.md](./docs/GETTING_STARTED.md) for build instructions and [GTK_APP.md](./docs/GTK_APP.md) for the Linux app.
The project structure is divided in the following folders: The project structure is divided in the following folders:
``` ```
Apple/ # Xcode project for burrow on macOS and iOS Apple/ # Xcode project for burrow on macOS and iOS
burrow/ # Higher-level API library for tun and tun-async burrow/ # Higher-level API library for tun and tun-async
burrow-gtk/ # GTK project for burrow on Linux
tun/ # Low-level interface to OS networking tun/ # Low-level interface to OS networking
src/ src/
tokio/ # Async/Tokio code tokio/ # Async/Tokio code

View file

@ -5,6 +5,7 @@ set -ex
BURROW_GTK_ROOT="$(readlink -f $(dirname -- "$(readlink -f -- "$BASH_SOURCE")")/..)" BURROW_GTK_ROOT="$(readlink -f $(dirname -- "$(readlink -f -- "$BASH_SOURCE")")/..)"
BURROW_GTK_BUILD="$BURROW_GTK_ROOT/build-appimage" BURROW_GTK_BUILD="$BURROW_GTK_ROOT/build-appimage"
LINUXDEPLOY_VERSION="${LINUXDEPLOY_VERSION:-"1-alpha-20240109-1"}" LINUXDEPLOY_VERSION="${LINUXDEPLOY_VERSION:-"1-alpha-20240109-1"}"
BURROW_BUILD_TYPE="${BURROW_BUILD_TYPE:-"release"}"
if [ "$BURROW_GTK_ROOT" != $(pwd) ]; then if [ "$BURROW_GTK_ROOT" != $(pwd) ]; then
echo "Make sure to cd into burrow-gtk" echo "Make sure to cd into burrow-gtk"
@ -21,8 +22,9 @@ elif [ "$ARCHITECTURE" == "aarch64" ]; then
chmod a+x /tmp/linuxdeploy chmod a+x /tmp/linuxdeploy
fi fi
meson setup $BURROW_GTK_BUILD --bindir bin --prefix /usr meson setup $BURROW_GTK_BUILD --bindir bin --prefix /usr --buildtype $BURROW_BUILD_TYPE
meson compile -C $BURROW_GTK_BUILD meson compile -C $BURROW_GTK_BUILD
DESTDIR=AppDir meson install -C $BURROW_GTK_BUILD DESTDIR=AppDir meson install -C $BURROW_GTK_BUILD
/tmp/linuxdeploy --appimage-extract-and-run --appdir $BURROW_GTK_BUILD/AppDir --output appimage cargo b --$BURROW_BUILD_TYPE --manifest-path=../Cargo.toml
/tmp/linuxdeploy --appimage-extract-and-run --appdir $BURROW_GTK_BUILD/AppDir -e $BURROW_GTK_BUILD/../../target/$BURROW_BUILD_TYPE/burrow --output appimage
mv *.AppImage $BURROW_GTK_BUILD mv *.AppImage $BURROW_GTK_BUILD

View file

@ -6,7 +6,7 @@ const RECONNECT_POLL_TIME: Duration = Duration::from_secs(5);
pub struct App { pub struct App {
daemon_client: Arc<Mutex<Option<DaemonClient>>>, daemon_client: Arc<Mutex<Option<DaemonClient>>>,
_settings_screen: Controller<settings_screen::SettingsScreen>, settings_screen: Controller<settings_screen::SettingsScreen>,
switch_screen: AsyncController<switch_screen::SwitchScreen>, switch_screen: AsyncController<switch_screen::SwitchScreen>,
} }
@ -109,7 +109,7 @@ impl AsyncComponent for App {
let model = App { let model = App {
daemon_client, daemon_client,
switch_screen, switch_screen,
_settings_screen: settings_screen, settings_screen,
}; };
AsyncComponentParts { model, widgets } AsyncComponentParts { model, widgets }
@ -132,14 +132,23 @@ impl AsyncComponent for App {
disconnected_daemon_client = true; disconnected_daemon_client = true;
self.switch_screen self.switch_screen
.emit(switch_screen::SwitchScreenMsg::DaemonDisconnect); .emit(switch_screen::SwitchScreenMsg::DaemonDisconnect);
self.settings_screen
.emit(settings_screen::SettingsScreenMsg::DaemonStateChange)
} }
} }
if disconnected_daemon_client || daemon_client.is_none() { if disconnected_daemon_client || daemon_client.is_none() {
*daemon_client = DaemonClient::new().await.ok(); match DaemonClient::new().await {
if daemon_client.is_some() { Ok(new_daemon_client) => {
*daemon_client = Some(new_daemon_client);
self.switch_screen self.switch_screen
.emit(switch_screen::SwitchScreenMsg::DaemonReconnect); .emit(switch_screen::SwitchScreenMsg::DaemonReconnect);
self.settings_screen
.emit(settings_screen::SettingsScreenMsg::DaemonStateChange)
}
Err(_e) => {
// TODO: Handle Error
}
} }
} }
} }

View file

@ -18,3 +18,4 @@ mod settings_screen;
mod switch_screen; mod switch_screen;
pub use app::*; pub use app::*;
pub use settings::{DaemonGroupMsg, DiagGroupMsg};

View file

@ -0,0 +1,111 @@
use super::*;
use std::process::Command;
#[derive(Debug)]
pub struct DaemonGroup {
system_setup: SystemSetup,
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
already_running: bool,
}
pub struct DaemonGroupInit {
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
pub system_setup: SystemSetup,
}
#[derive(Debug)]
pub enum DaemonGroupMsg {
LaunchLocal,
DaemonStateChange,
}
#[relm4::component(pub, async)]
impl AsyncComponent for DaemonGroup {
type Init = DaemonGroupInit;
type Input = DaemonGroupMsg;
type Output = ();
type CommandOutput = ();
view! {
#[name(group)]
adw::PreferencesGroup {
#[watch]
set_sensitive:
(model.system_setup == SystemSetup::AppImage || model.system_setup == SystemSetup::Other) &&
!model.already_running,
set_title: "Local Daemon",
set_description: Some("Run Local Daemon"),
gtk::Button {
set_label: "Launch",
connect_clicked => DaemonGroupMsg::LaunchLocal
}
}
}
async fn init(
init: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
// Should be impossible to panic here
let model = DaemonGroup {
system_setup: init.system_setup,
daemon_client: init.daemon_client.clone(),
already_running: init.daemon_client.lock().await.is_some(),
};
let widgets = view_output!();
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
msg: Self::Input,
_sender: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
match msg {
DaemonGroupMsg::LaunchLocal => {
let burrow_original_bin = std::env::vars()
.find(|(k, _)| k == "APPDIR")
.map(|(_, v)| v + "/usr/bin/burrow")
.unwrap_or("/usr/bin/burrow".to_owned());
let mut burrow_bin =
String::from_utf8(Command::new("mktemp").output().unwrap().stdout).unwrap();
burrow_bin.pop();
let privileged_spawn_script = format!(
r#"TEMP=$(mktemp -p /root)
cp {} $TEMP
chmod +x $TEMP
setcap CAP_NET_BIND_SERVICE,CAP_NET_ADMIN+eip $TEMP
mv $TEMP /tmp/burrow-detached-daemon"#,
burrow_original_bin
)
.replace('\n', "&&");
// TODO: Handle error condition
Command::new("pkexec")
.arg("sh")
.arg("-c")
.arg(privileged_spawn_script)
.arg(&burrow_bin)
.output()
.unwrap();
Command::new("/tmp/burrow-detached-daemon")
.env("RUST_LOG", "debug")
.arg("daemon")
.spawn()
.unwrap();
}
DaemonGroupMsg::DaemonStateChange => {
self.already_running = self.daemon_client.lock().await.is_some();
}
}
}
}

View file

@ -1,11 +1,10 @@
use super::*; use super::*;
use diag::{StatusTernary, SystemSetup};
#[derive(Debug)] #[derive(Debug)]
pub struct DiagGroup { pub struct DiagGroup {
daemon_client: Arc<Mutex<Option<DaemonClient>>>, daemon_client: Arc<Mutex<Option<DaemonClient>>>,
init_system: SystemSetup, system_setup: SystemSetup,
service_installed: StatusTernary, service_installed: StatusTernary,
socket_installed: StatusTernary, socket_installed: StatusTernary,
socket_enabled: StatusTernary, socket_enabled: StatusTernary,
@ -14,19 +13,20 @@ pub struct DiagGroup {
pub struct DiagGroupInit { pub struct DiagGroupInit {
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>, pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
pub system_setup: SystemSetup,
} }
impl DiagGroup { impl DiagGroup {
async fn new(daemon_client: Arc<Mutex<Option<DaemonClient>>>) -> Result<Self> { async fn new(daemon_client: Arc<Mutex<Option<DaemonClient>>>) -> Result<Self> {
let setup = SystemSetup::new(); let system_setup = SystemSetup::new();
let daemon_running = daemon_client.lock().await.is_some(); let daemon_running = daemon_client.lock().await.is_some();
Ok(Self { Ok(Self {
service_installed: setup.is_service_installed()?, service_installed: system_setup.is_service_installed()?,
socket_installed: setup.is_socket_installed()?, socket_installed: system_setup.is_socket_installed()?,
socket_enabled: setup.is_socket_enabled()?, socket_enabled: system_setup.is_socket_enabled()?,
daemon_running, daemon_running,
init_system: setup, system_setup,
daemon_client, daemon_client,
}) })
} }
@ -52,7 +52,7 @@ impl AsyncComponent for DiagGroup {
adw::ActionRow { adw::ActionRow {
#[watch] #[watch]
set_title: &format!("Init System: {}", model.init_system) set_title: &format!("System Type: {}", model.system_setup)
}, },
adw::ActionRow { adw::ActionRow {
#[watch] #[watch]

View file

@ -1,5 +1,8 @@
use super::*; use super::*;
use diag::{StatusTernary, SystemSetup};
mod daemon_group;
mod diag_group; mod diag_group;
pub use diag_group::{DiagGroup, DiagGroupInit}; pub use daemon_group::{DaemonGroup, DaemonGroupInit, DaemonGroupMsg};
pub use diag_group::{DiagGroup, DiagGroupInit, DiagGroupMsg};

View file

@ -1,17 +1,24 @@
use super::*; use super::*;
use diag::SystemSetup;
pub struct SettingsScreen { pub struct SettingsScreen {
_diag_group: AsyncController<settings::DiagGroup>, diag_group: AsyncController<settings::DiagGroup>,
daemon_group: AsyncController<settings::DaemonGroup>,
} }
pub struct SettingsScreenInit { pub struct SettingsScreenInit {
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>, pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
} }
#[derive(Debug, PartialEq, Eq)]
pub enum SettingsScreenMsg {
DaemonStateChange,
}
#[relm4::component(pub)] #[relm4::component(pub)]
impl SimpleComponent for SettingsScreen { impl SimpleComponent for SettingsScreen {
type Init = SettingsScreenInit; type Init = SettingsScreenInit;
type Input = (); type Input = SettingsScreenMsg;
type Output = (); type Output = ();
view! { view! {
@ -24,21 +31,41 @@ impl SimpleComponent for SettingsScreen {
root: &Self::Root, root: &Self::Root,
sender: ComponentSender<Self>, sender: ComponentSender<Self>,
) -> ComponentParts<Self> { ) -> ComponentParts<Self> {
let system_setup = SystemSetup::new();
let diag_group = settings::DiagGroup::builder() let diag_group = settings::DiagGroup::builder()
.launch(settings::DiagGroupInit { .launch(settings::DiagGroupInit {
system_setup,
daemon_client: Arc::clone(&init.daemon_client), daemon_client: Arc::clone(&init.daemon_client),
}) })
.forward(sender.input_sender(), |_| ()); .forward(sender.input_sender(), |_| {
SettingsScreenMsg::DaemonStateChange
});
let daemon_group = settings::DaemonGroup::builder()
.launch(settings::DaemonGroupInit {
system_setup,
daemon_client: Arc::clone(&init.daemon_client),
})
.forward(sender.input_sender(), |_| {
SettingsScreenMsg::DaemonStateChange
});
let widgets = view_output!(); let widgets = view_output!();
widgets.preferences.add(diag_group.widget()); widgets.preferences.add(diag_group.widget());
widgets.preferences.add(daemon_group.widget());
let model = SettingsScreen { let model = SettingsScreen { diag_group, daemon_group };
_diag_group: diag_group,
};
ComponentParts { model, widgets } ComponentParts { model, widgets }
} }
fn update(&mut self, _: Self::Input, _sender: ComponentSender<Self>) {} fn update(&mut self, _: Self::Input, _sender: ComponentSender<Self>) {
// Currently, `SettingsScreenMsg` only has one variant, so the if is ambiguous.
//
// if let SettingsScreenMsg::DaemonStateChange = msg {
self.diag_group.emit(DiagGroupMsg::Refresh);
self.daemon_group.emit(DaemonGroupMsg::DaemonStateChange);
// }
}
} }

View file

@ -15,15 +15,18 @@ pub enum StatusTernary {
// Realistically, we may not explicitly "support" non-systemd platforms which would simply this // Realistically, we may not explicitly "support" non-systemd platforms which would simply this
// code greatly. // code greatly.
// Along with replacing [`StatusTernary`] with good old [`bool`]. // Along with replacing [`StatusTernary`] with good old [`bool`].
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SystemSetup { pub enum SystemSetup {
Systemd, Systemd,
AppImage,
Other, Other,
} }
impl SystemSetup { impl SystemSetup {
pub fn new() -> Self { pub fn new() -> Self {
if Command::new("systemctl").arg("--version").output().is_ok() { if is_appimage() {
SystemSetup::AppImage
} else if Command::new("systemctl").arg("--version").output().is_ok() {
SystemSetup::Systemd SystemSetup::Systemd
} else { } else {
SystemSetup::Other SystemSetup::Other
@ -33,6 +36,7 @@ impl SystemSetup {
pub fn is_service_installed(&self) -> Result<StatusTernary> { pub fn is_service_installed(&self) -> Result<StatusTernary> {
match self { match self {
SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SERVICE_LOC).is_ok().into()), SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SERVICE_LOC).is_ok().into()),
SystemSetup::AppImage => Ok(StatusTernary::NA),
SystemSetup::Other => Ok(StatusTernary::NA), SystemSetup::Other => Ok(StatusTernary::NA),
} }
} }
@ -40,6 +44,7 @@ impl SystemSetup {
pub fn is_socket_installed(&self) -> Result<StatusTernary> { pub fn is_socket_installed(&self) -> Result<StatusTernary> {
match self { match self {
SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SOCKET_LOC).is_ok().into()), SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SOCKET_LOC).is_ok().into()),
SystemSetup::AppImage => Ok(StatusTernary::NA),
SystemSetup::Other => Ok(StatusTernary::NA), SystemSetup::Other => Ok(StatusTernary::NA),
} }
} }
@ -55,6 +60,7 @@ impl SystemSetup {
let output = String::from_utf8(output)?; let output = String::from_utf8(output)?;
Ok((output == "enabled\n").into()) Ok((output == "enabled\n").into())
} }
SystemSetup::AppImage => Ok(StatusTernary::NA),
SystemSetup::Other => Ok(StatusTernary::NA), SystemSetup::Other => Ok(StatusTernary::NA),
} }
} }
@ -74,7 +80,12 @@ impl Display for SystemSetup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self { f.write_str(match self {
SystemSetup::Systemd => "Systemd", SystemSetup::Systemd => "Systemd",
SystemSetup::AppImage => "AppImage",
SystemSetup::Other => "Other", SystemSetup::Other => "Other",
}) })
} }
} }
pub fn is_appimage() -> bool {
std::env::vars().any(|(k, _)| k == "APPDIR")
}