Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c62d29fa14 | ||
|
|
a6d9edabee | ||
|
|
2f7f9980c7 |
No files matched your search
+1
-1
@@ -15,7 +15,7 @@ mainline = { path = "vendor/mainline" }
|
||||
edition = "2024"
|
||||
rust-version = "1.91"
|
||||
license = "WTFPL"
|
||||
repository = "https://github.com/tsunagi-net/tsunagi"
|
||||
repository = "https://github.com/house-of-vanity/tsunagi"
|
||||
|
||||
[workspace.lints.rust]
|
||||
missing_docs = "warn"
|
||||
|
||||
@@ -50,11 +50,12 @@ pub(crate) struct Snapshot {
|
||||
pub generation: u64,
|
||||
/// When this status was read, for computing rates between refreshes.
|
||||
pub at: Option<Instant>,
|
||||
/// The socket the last request went to.
|
||||
pub socket: PathBuf,
|
||||
}
|
||||
|
||||
/// Handle the UI holds: send commands, read the latest snapshot.
|
||||
pub(crate) struct AgentClient {
|
||||
socket: PathBuf,
|
||||
tx: mpsc::UnboundedSender<Command>,
|
||||
shared: Arc<Mutex<Snapshot>>,
|
||||
/// A secret fetched for the clipboard, taken by the UI thread which owns
|
||||
@@ -65,23 +66,20 @@ pub(crate) struct AgentClient {
|
||||
|
||||
impl AgentClient {
|
||||
/// Starts the worker on `runtime`, reporting to `ctx` (for repaints).
|
||||
pub(crate) fn spawn(
|
||||
runtime: &tokio::runtime::Handle,
|
||||
ctx: egui::Context,
|
||||
socket: PathBuf,
|
||||
) -> Self {
|
||||
pub(crate) fn spawn(runtime: &tokio::runtime::Handle, ctx: egui::Context) -> Self {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let shared = Arc::new(Mutex::new(Snapshot::default()));
|
||||
let shared = Arc::new(Mutex::new(Snapshot {
|
||||
socket: resolve_socket(),
|
||||
..Snapshot::default()
|
||||
}));
|
||||
let pending_copy = Arc::new(Mutex::new(None));
|
||||
runtime.spawn(worker(
|
||||
socket.clone(),
|
||||
rx,
|
||||
Arc::clone(&shared),
|
||||
Arc::clone(&pending_copy),
|
||||
ctx,
|
||||
));
|
||||
Self {
|
||||
socket,
|
||||
tx,
|
||||
shared,
|
||||
pending_copy,
|
||||
@@ -93,9 +91,9 @@ impl AgentClient {
|
||||
lock(&self.pending_copy).take()
|
||||
}
|
||||
|
||||
/// The control socket this client talks to.
|
||||
pub(crate) fn socket(&self) -> &std::path::Path {
|
||||
&self.socket
|
||||
/// The control socket this client talks to, as last resolved.
|
||||
pub(crate) fn socket(&self) -> PathBuf {
|
||||
lock(&self.shared).socket.clone()
|
||||
}
|
||||
|
||||
/// Sends a command; a dropped worker simply means nothing happens.
|
||||
@@ -118,33 +116,37 @@ fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
|
||||
/// The worker loop: refresh on an interval and whenever a command runs.
|
||||
async fn worker(
|
||||
socket: PathBuf,
|
||||
mut rx: mpsc::UnboundedReceiver<Command>,
|
||||
shared: Arc<Mutex<Snapshot>>,
|
||||
pending_copy: Arc<Mutex<Option<String>>>,
|
||||
ctx: egui::Context,
|
||||
) {
|
||||
refresh(&socket, &shared, &ctx).await;
|
||||
refresh(&shared, &ctx).await;
|
||||
let mut tick = tokio::time::interval(POLL_INTERVAL);
|
||||
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = tick.tick() => {
|
||||
refresh(&socket, &shared, &ctx).await;
|
||||
refresh(&shared, &ctx).await;
|
||||
}
|
||||
command = rx.recv() => {
|
||||
let Some(command) = command else { return };
|
||||
run(&socket, command, &shared, &pending_copy, &ctx).await;
|
||||
refresh(&socket, &shared, &ctx).await;
|
||||
run(&resolve_socket(), command, &shared, &pending_copy, &ctx).await;
|
||||
refresh(&shared, &ctx).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads status and stores it, mapping any failure to a human string.
|
||||
async fn refresh(socket: &std::path::Path, shared: &Arc<Mutex<Snapshot>>, ctx: &egui::Context) {
|
||||
let status = ipc::request_status(socket)
|
||||
///
|
||||
/// The socket is looked for again each time: a window opened before the agent
|
||||
/// was started would otherwise keep asking the per-user path for ever, while
|
||||
/// the service came up at the system one.
|
||||
async fn refresh(shared: &Arc<Mutex<Snapshot>>, ctx: &egui::Context) {
|
||||
let socket = resolve_socket();
|
||||
let status = ipc::request_status(&socket)
|
||||
.await
|
||||
.map_err(|err| err.to_string());
|
||||
{
|
||||
@@ -152,6 +154,7 @@ async fn refresh(socket: &std::path::Path, shared: &Arc<Mutex<Snapshot>>, ctx: &
|
||||
guard.status = Some(status);
|
||||
guard.generation = guard.generation.wrapping_add(1);
|
||||
guard.at = Some(Instant::now());
|
||||
guard.socket = socket;
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::time::Duration;
|
||||
use eframe::egui;
|
||||
|
||||
use crate::about;
|
||||
use crate::agent::{AgentClient, resolve_socket};
|
||||
use crate::agent::AgentClient;
|
||||
use crate::devices;
|
||||
use crate::stats::Traffic;
|
||||
use crate::ui::{self, UiState};
|
||||
@@ -38,7 +38,7 @@ impl App {
|
||||
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
|
||||
ctx.set_fonts(fonts);
|
||||
|
||||
let agent = AgentClient::spawn(&runtime, ctx, resolve_socket());
|
||||
let agent = AgentClient::spawn(&runtime, ctx);
|
||||
Ok(Self {
|
||||
agent,
|
||||
state: UiState::default(),
|
||||
@@ -88,7 +88,7 @@ impl App {
|
||||
egui::ViewportId::from_hash_of("about"),
|
||||
builder,
|
||||
|vctx, _class| {
|
||||
about::show(vctx, socket, snapshot);
|
||||
about::show(vctx, &socket, snapshot);
|
||||
!vctx.input(|i| i.viewport().close_requested())
|
||||
},
|
||||
);
|
||||
@@ -112,7 +112,7 @@ impl App {
|
||||
let viewport_id = egui::ViewportId::from_hash_of(("devices", &id));
|
||||
let builder = egui::ViewportBuilder::default()
|
||||
.with_title(devices::title(network))
|
||||
.with_inner_size([720.0, 540.0])
|
||||
.with_inner_size([900.0, 540.0])
|
||||
.with_min_inner_size([480.0, 320.0]);
|
||||
let agent = &self.agent;
|
||||
let traffic = &self.traffic;
|
||||
|
||||
@@ -99,7 +99,7 @@ pub(crate) fn show(
|
||||
))
|
||||
.strong(),
|
||||
);
|
||||
egui::ScrollArea::vertical()
|
||||
egui::ScrollArea::both()
|
||||
.auto_shrink([false, false])
|
||||
.show(ui, |ui| {
|
||||
device_table(ui, unit, &network.network_id, &devices, traffic);
|
||||
|
||||
@@ -81,6 +81,26 @@ fn build() -> Result<Built, String> {
|
||||
})
|
||||
}
|
||||
|
||||
/// [`build`], with a missing indicator library reported instead of panicking.
|
||||
///
|
||||
/// The library is loaded on first use, and its binding panics when it is not
|
||||
/// installed, which reads as a bug in this program rather than a package that
|
||||
/// is not there.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn build_checked() -> Result<Built, String> {
|
||||
let hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(|_| {}));
|
||||
let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(build));
|
||||
std::panic::set_hook(hook);
|
||||
built.unwrap_or_else(|_| {
|
||||
Err(
|
||||
"the tray needs libayatana-appindicator (Arch: libayatana-appindicator, \
|
||||
Debian/Ubuntu: libayatana-appindicator3-1), which is not installed"
|
||||
.to_string(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Acts on menu choices as the platform's loop delivers them.
|
||||
fn handle_menu(built: &Built, window: Arc<Window>) {
|
||||
let (open, quit) = (built.open.clone(), built.quit.clone());
|
||||
@@ -101,7 +121,7 @@ fn handle_menu(built: &Built, window: Arc<Window>) {
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
gtk::init().map_err(|err| format!("cannot initialise GTK: {err}"))?;
|
||||
let built = build()?;
|
||||
let built = build_checked()?;
|
||||
handle_menu(
|
||||
&built,
|
||||
Arc::new(Window {
|
||||
|
||||
Vendored
+9
-4
@@ -37,10 +37,15 @@ creates and owns its directories:
|
||||
| cache | `/var/cache/tsunagi` |
|
||||
| control socket | `/run/tsunagi/agent.sock` |
|
||||
|
||||
The polkit rule lets anyone in the `tsunagi` group configure systemd-resolved
|
||||
for the overlay zones (the service user is in that group). To run the agent as
|
||||
your own user as well, add yourself to the group: `sudo usermod -aG tsunagi
|
||||
"$USER"`.
|
||||
Installing the package enables and starts the service, and adds the user who ran
|
||||
pacman to the `tsunagi` group (log out and in again for it to apply). The
|
||||
control socket is open to that group, so members can run `tsunagi status`, the
|
||||
CLI and the tray GUI without sudo; anyone else adds themselves with `sudo
|
||||
usermod -aG tsunagi "$USER"`. The group also lets its members configure
|
||||
systemd-resolved for the overlay zones through the polkit rule.
|
||||
|
||||
The GUI package depends on `gtk3`, `libayatana-appindicator` (the tray icon),
|
||||
`xdotool` (its `libxdo`), and the usual windowing libraries.
|
||||
|
||||
## Removal
|
||||
|
||||
|
||||
Vendored
+39
-5
@@ -50,6 +50,11 @@ if [ -n "$tray" ]; then
|
||||
pkgname=tsunagi-gui
|
||||
pkgdesc="Serverless private mesh networking agent, with the tray GUI"
|
||||
relations=$'conflict = tsunagi\nprovides = tsunagi\nreplaces = tsunagi\n'
|
||||
# What the tray and its window load at run time; none of it is linked in
|
||||
# statically, so pacman has to be told.
|
||||
for dep in gtk3 libayatana-appindicator xdotool libxkbcommon wayland libx11 libglvnd; do
|
||||
relations+="depend = ${dep}"$'\n'
|
||||
done
|
||||
install -D -m 0755 "$tray" "$root/usr/bin/tsunagi-tray"
|
||||
install -D -m 0644 "$here/../linux/tsunagi-tray.desktop" \
|
||||
"$root/usr/share/applications/tsunagi-tray.desktop"
|
||||
@@ -64,7 +69,7 @@ cat > "$root/.PKGINFO" <<PKGINFO
|
||||
pkgname = ${pkgname}
|
||||
pkgver = ${pkgver}-${pkgrel}
|
||||
pkgdesc = ${pkgdesc}
|
||||
url = https://github.com/Ultradesu/tsunagi
|
||||
url = https://github.com/house-of-vanity/tsunagi
|
||||
builddate = $(date +%s)
|
||||
packager = AB <ab@hexor.cy>
|
||||
size = ${size}
|
||||
@@ -72,21 +77,50 @@ arch = ${arch}
|
||||
${relations}license = WTFPL
|
||||
PKGINFO
|
||||
|
||||
# Scriptlet: create the dedicated system user the service runs as, keep systemd
|
||||
# in step, and refresh the desktop/icon caches (a no-op without a GUI payload).
|
||||
# Scriptlet: create the dedicated system user the service runs as, start the
|
||||
# service, and let whoever ran pacman use it. The control socket belongs to the
|
||||
# `tsunagi` group, so the tray and the CLI work for any member of it; the
|
||||
# person installing is added, since the package cannot know anyone else.
|
||||
cat > "$root/.INSTALL" <<'INSTALL'
|
||||
post_install() {
|
||||
getent group tsunagi >/dev/null 2>&1 || groupadd -r tsunagi
|
||||
getent passwd tsunagi >/dev/null 2>&1 || \
|
||||
useradd -r -g tsunagi -d /var/lib/tsunagi -s /usr/bin/nologin -c "tsunagi agent" tsunagi
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
command -v update-desktop-database >/dev/null 2>&1 && \
|
||||
update-desktop-database -q /usr/share/applications >/dev/null 2>&1 || true
|
||||
command -v gtk-update-icon-cache >/dev/null 2>&1 && \
|
||||
gtk-update-icon-cache -q -t /usr/share/icons/hicolor >/dev/null 2>&1 || true
|
||||
|
||||
# Whoever ran pacman through sudo or pkexec (an AUR helper does too).
|
||||
installer="${SUDO_USER:-}"
|
||||
if [ -z "$installer" ] && [ -n "${PKEXEC_UID:-}" ]; then
|
||||
installer="$(getent passwd "$PKEXEC_UID" | cut -d: -f1)"
|
||||
fi
|
||||
if [ -n "$installer" ] && [ "$installer" != root ] && id "$installer" >/dev/null 2>&1; then
|
||||
usermod -aG tsunagi "$installer" >/dev/null 2>&1 || true
|
||||
echo ":: $installer was added to the tsunagi group; log out and in again so the"
|
||||
echo " tray and the CLI can reach the agent without sudo."
|
||||
else
|
||||
echo ":: add yourself to the tsunagi group to use the tray and the CLI without"
|
||||
echo " sudo: sudo usermod -aG tsunagi \$USER, then log out and in again."
|
||||
fi
|
||||
|
||||
if [ -d /run/systemd/system ]; then
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl enable --now tsunagi.service >/dev/null 2>&1 || \
|
||||
echo ":: could not start tsunagi.service; see: systemctl status tsunagi"
|
||||
fi
|
||||
}
|
||||
post_upgrade() {
|
||||
post_install
|
||||
# Not enabled again: it may have been turned off on purpose. A running
|
||||
# agent is restarted, because the control protocol changes between builds
|
||||
# and a new client cannot talk to an old agent.
|
||||
command -v update-desktop-database >/dev/null 2>&1 && \
|
||||
update-desktop-database -q /usr/share/applications >/dev/null 2>&1 || true
|
||||
if [ -d /run/systemd/system ]; then
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl try-restart tsunagi.service >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
pre_remove() {
|
||||
systemctl disable --now tsunagi.service >/dev/null 2>&1 || true
|
||||
|
||||
Vendored
+3
-1
@@ -45,6 +45,8 @@ if [ -n "$tray" ]; then
|
||||
# The GUI package is a superset of the headless one and must not coexist
|
||||
# with it (both ship /usr/bin/tsunagi).
|
||||
extra_control=$'Conflicts: tsunagi\nReplaces: tsunagi\nProvides: tsunagi\n'
|
||||
# What the tray and its window load at run time.
|
||||
extra_control+=$'Depends: libgtk-3-0 | libgtk-3-0t64, libayatana-appindicator3-1 | libappindicator3-1, libxdo3, libxkbcommon0, libwayland-client0, libx11-6, libgl1\n'
|
||||
desc_gui=" It also installs the tray GUI (tsunagi-tray) and a desktop launcher so it can be started from the applications menu."
|
||||
install -D -m 0755 "$tray" "$root/usr/bin/tsunagi-tray"
|
||||
install -D -m 0644 "$here/../linux/tsunagi-tray.desktop" \
|
||||
@@ -62,7 +64,7 @@ Architecture: ${arch}
|
||||
Maintainer: AB <ab@hexor.cy>
|
||||
Section: net
|
||||
Priority: optional
|
||||
Homepage: https://github.com/Ultradesu/tsunagi
|
||||
Homepage: https://github.com/house-of-vanity/tsunagi
|
||||
${extra_control}Description: Serverless private mesh networking agent
|
||||
tsunagi forms small private mesh IP networks between peers, with NAT
|
||||
traversal and no central server. This package installs a static,
|
||||
|
||||
Vendored
+3
-1
@@ -22,7 +22,9 @@ and `CAP_NET_BIND_SERVICE` (granted by systemd), not root. systemd creates its
|
||||
state (`/var/lib/tsunagi`), cache (`/var/cache/tsunagi`) and runtime
|
||||
(`/run/tsunagi`) directories, and the polkit rule lets it configure the
|
||||
resolver. Control it with `sudo` — the CLI finds the running service's socket at
|
||||
`/run/tsunagi/agent.sock` with no flags:
|
||||
`/run/tsunagi/agent.sock` with no flags. The socket is open to the `tsunagi`
|
||||
group, so after `sudo usermod -aG tsunagi "$USER"` (and logging in again) no
|
||||
`sudo` is needed:
|
||||
|
||||
```sh
|
||||
sudo tsunagi status
|
||||
|
||||
Vendored
+4
-2
@@ -1,6 +1,6 @@
|
||||
[Unit]
|
||||
Description=tsunagi mesh agent
|
||||
Documentation=https://github.com/Ultradesu/tsunagi
|
||||
Documentation=https://github.com/house-of-vanity/tsunagi
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -16,7 +16,8 @@ CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
|
||||
|
||||
# systemd creates and owns these for the service user, and the agent is pointed
|
||||
# at them: state in /var/lib/tsunagi, cache in /var/cache/tsunagi, and the
|
||||
# control socket in /run/tsunagi, which stays private to the service user.
|
||||
# control socket in /run/tsunagi. The socket is opened to the `tsunagi` group,
|
||||
# so a user who joins it can run the CLI and the tray without sudo.
|
||||
StateDirectory=tsunagi
|
||||
StateDirectoryMode=0700
|
||||
CacheDirectory=tsunagi
|
||||
@@ -25,6 +26,7 @@ RuntimeDirectoryMode=0755
|
||||
Environment=TSUNAGI_STATE_DIR=%S/tsunagi
|
||||
Environment=TSUNAGI_CACHE_DIR=%C/tsunagi
|
||||
Environment=TSUNAGI_CONTROL_SOCKET=%t/tsunagi/agent.sock
|
||||
Environment=TSUNAGI_CONTROL_GROUP=tsunagi
|
||||
|
||||
# The tarball installer puts the binary here; the deb and the Arch package
|
||||
# rewrite this to /usr/bin where they install it.
|
||||
|
||||
Reference in new issue
Block a user